juzaweb/laravel-translation-loader
Load and manage Laravel translation strings from the database. Extends Laravel's translation loader so you can store, edit, and serve language lines dynamically instead of relying only on file-based lang files. Great for admin panels and CMS apps needing runtime translation management.
This is a small, Spatie-derived translation loader: it swaps Laravel's file-only translation loader for one that can also read language lines from a database table (and, in this fork, YAML/other sources). Day-to-day, you use it when content editors need to change UI strings without a deploy.
Minimal setup
juzaweb/laravel-translation) and package name (juzaweb/laravel-translation-loader) differ:
composer require juzaweb/laravel-translation-loader
composer.json:
cat vendor/juzaweb/laravel-translation-loader/composer.json
# look at extra.laravel.providers and extra.laravel.aliases
php artisan vendor:publish --provider="Juzaweb\Translation\TranslationServiceProvider"
php artisan migrate
config/translation-loader.php or config/translation.php). The key setting is the source/loader — file, database, YAML, or a custom class.lang/en/messages.php into the DB, then call it normally.use Juzaweb\Translation\Models\LanguageLine; // confirm namespace in src/Models
LanguageLine::create([
'group' => 'messages',
'key' => 'welcome',
'text' => ['en' => 'Welcome', 'fr' => 'Bienvenue'],
]);
{{ __('messages.welcome') }} {{-- now resolves from the configured source --}}
Where to look first
src/TranslationServiceProvider.php — bindings, loader registration, config merge.config/*.php — source selection and fallback order.src/TranslationLoaders/ (or similar) — the actual loader implementations; this is the extension point.src/Models/ — the Eloquent model backing DB lines.database/migrations/ — schema for the language_lines table (group, key, JSON text).Because this package has ~1 star and 0 dependents, treat it as a fork you may need to read rather than a heavily documented dependency. Budget 30 minutes to read the provider and loader before wiring it into a project.
1. Loader chaining / fallback
The usual pattern is: database (or YAML) first, then Laravel's file loader as fallback. That lets you override only the strings editors care about while keeping the rest in lang/:
// config/translation-loader.php (illustrative)
return [
'source' => Juzaweb\Translation\TranslationLoaders\Database::class,
// or an array/chain if the fork supports multiple sources
];
If the config only accepts a single source, keep the file loader as the ultimate fallback by ensuring the package's loader delegates misses to parent::load() (check the loader implementation).
2. CRUD through the model, not raw SQL
Create a small admin service or use the package model directly. Wrap writes so cache invalidation and text array normalization happen in one place:
class TranslationRepository
{
public function put(string $group, string $key, array $text): void
{
LanguageLine::updateOrCreate(
['group' => $group, 'key' => $key],
['text' => $text]
);
}
}
3. Import/export existing lang files
The highest-value first workflow is migrating existing lang/{locale}/*.php files into the chosen source. A one-off artisan command using File::files(lang_path('en')) and require per file is usually enough. Keep the import idempotent via updateOrCreate. Export back to PHP/YAML for version control if editors need Git review.
4. Group/key conventions matter more than with files
Since group and key are now columns, standardize early:
group = the old filename (messages, auth, validation).key = the dotted key without the group prefix (welcome, not messages.welcome).text JSON.5. Runtime overrides for tests and local dev
Point the source at the file loader in phpunit.xml / .env.testing so tests don't touch the translation table:
<env name="TRANSLATION_SOURCE" value="file"/>
If the config doesn't read an env var, add one in config/translation-loader.php yourself.
6. Custom source integration
The main extension point is a loader class. Implement whatever interface the package's built-in loaders implement (inspect src/TranslationLoaders/), then reference it in config. This is how you'd add Redis, an external TMS, or a tenant-scoped source:
class RedisTranslationLoader implements TranslationLoader
{
public function loadTranslations(string $locale, string $group): array
{
return Cache::remember("translations:$locale:$group", 3600, function () use ($locale, $group) {
// return ['key' => 'value', ...]
});
}
}
7. Blade, validation, and packages
Because it hooks Laravel's translator, __(), @lang, trans_choice(), validation messages, and lang:: package keys all keep working. You do not change call sites — that's the main selling point. Just don't mix DB keys with vendor package keys (package::file.key); the loader typically only handles non-namespaced groups.
8. Caching in production
If you cache translations (Laravel's optimize or your own layer), invalidate on write. Hook the model:
static::saved(fn () => Cache::forget("translations:{$this->group}"));
static::deleted(fn () => Cache::forget("translations:{$this->group}"));
juzaweb/laravel-translation vs package juzaweb/laravel-translation-loader. Confirm with composer show and composer.json before copying code from Spatie docs; namespace (Juzaweb\Translation vs Spatie\TranslationLoader) and class names may differ.spatie/laravel-translation-loader. APIs are close, but verify model namespace, config filename, and migration columns rather than assuming."juzaweb/laravel-translation-loader": "1.2.*") and commit the lockfile.__('messages.foo') returning messages.foo is the normal Laravel miss behavior, but with DB sources it's easy to misdiagnose as a loader bug. Log misses (app('translator')->getLoader() or a missing-translation listener) during migration.php artisan cache:clear (or optimize:clear) and confirm no opcache/view cache is involved. Wire model events to cache invalidation early.text column stores a locale→string map. On MySQL use json (or at least text with json_encode); on SQLite/Postgres confirm the migration matches your driver. Malformed JSON silently becomes a missing translation.group+locale per request. For pages with many groups, either eager-load all lines for the locale into the translator once, or add a cache layer in a custom loader. Watch DB::listen during a page render.vendor/. Publish config and migrations; if you need behavior changes, subclass the loader/model in app/ and bind it in a service provider. That keeps upgrades survivable.updated_by/timestamps if editors are non-technical.symfony/yaml is installed and that YAML files live where the loader expects (usually lang/{locale}/*.yml). Flat dotted keys and nested keys behave differently — pick one convention.How can I help you explore Laravel packages today?