mgcodeur/laravel-translation-loader
A Laravel package that swaps the default file-based translator for a database-driven loader, letting you store, manage, and edit translations in the database. Includes caching, migration and model support, and works with Laravel's existing trans() and __() helpers.
This package swaps Laravel's default file-based translation loader for a database-backed one, so __(), @lang, and the Translator facade keep working unchanged while the strings live in a table you can edit at runtime (admin panel, CMS, per-tenant overrides). Version 1.2.0 widens the support matrix to Laravel 10/11/12/13 and PHP 8.1–8.4, so it drops into both legacy and freshly created applications.
Minimal path from composer require mgcodeur/laravel-translation-loader:
php artisan vendor:publish --provider="Mgcodeur\TranslationLoader\TranslationLoaderServiceProvider" (add --tag=config / --tag=migrations to be selective). You want the config file and the translations migration.php artisan migrate. Confirm the table exists before touching any Blade view; an empty table means every __('...') silently falls back to the key string, which looks like a broken install.translation.loader in its register() method. If you have a TranslationServiceProvider override or a package that also touches the loader (e.g. a tenant-aware translator), verify boot order: the last binding wins.lang/**/*.php and insert one row per key/locale. A one-off artisan command is worth the 20 lines; without it you'll ship a UI with nothing in it.translations table so a content editor can fix copy without a deploy. That is where the package pays for itself; everything else (tenant overrides, A/B copy) builds on the same table.Look first at the published config (it controls connection, table name, and whether the DB loader is active) and the loader class itself — that's the whole contract, and reading it takes five minutes. Then confirm behaviour with php artisan tinker: app('translator')->get('messages.welcome') should hit the database, and you can watch it in DB::listen().
Drop-in replacement, not a parallel API. Keep calling __('messages.welcome'), trans_choice, and @lang exactly as before. The value of the package is that no view code changes; migrating means moving rows, not rewriting templates. Resist the temptation to inject the model into views.
Import once, then treat the DB as the source of truth. A typical workflow: keep lang/ in git as the canonical set of keys for code review and CI, and run an idempotent translations:import (update-or-create on locale + group + key) as part of deploy, followed by an export step if editors changed rows. Decide early whether the file tree or the table wins on conflict — mixing both without a rule produces the classic "my edit disappeared after deploy" ticket.
Locale and group namespacing. Mirror Laravel's own semantics: JSON-style keys ("Welcome back") versus group files (messages.welcome) behave differently in the loader, and vendor namespaces (package::file.key) need a column or parse convention. Model the table with a unique index on (locale, group, key) — you will otherwise accumulate duplicates that resolve unpredictably depending on query order.
Tenancy and per-user overrides. Because the loader is just a resolver, a clean pattern is a scoped resolver: resolve the tenant from the request, query the tenant's rows, fall back to the global row, then to the file loader. Implement it as a decorator around the package's loader rather than editing vendor code — the package's binding is the seam.
Cache the result, not the query. Translation reads happen dozens of times per request. Wrap the loader in a cache layer keyed on locale (and tenant), invalidated on write. Laravel's translator already memoises within a request, so the win is across requests.
Testing. Use RefreshDatabase plus a seeder for the handful of keys the suite asserts on; don't assert against production copy. Keep a thin test asserting that a known key returns the seeded value in both the default and fallback locale — that catches binding regressions after a framework upgrade far faster than a full feature test.
Upgrading to 1.2.0. Bump and run the suite on your actual Laravel/PHP pair. The CI matrix in the release excludes some framework/PHP combinations, so verify your exact pairing rather than assuming the constraint solver's choice is a tested one.
fallback_locale behaviour first when a string "doesn't translate".php artisan config:cache, the published config is frozen; changing the connection or table name requires a re-cache. In deployment scripts, order matters: migrate → import → config:cache → optimize.AppServiceProvider rebinds translation.loader, the DB loader silently stops being used and everything falls back to files. Add the tinker check above to your smoke tests.trans_choice and :name replacement are handled by Laravel, not the loader — but your stored strings must still contain the | separators and placeholders. Editors will delete them; validate on save.orchestra/testbench ^11.0 and pest ^4.0 bumps are dev-only — they don't affect your runtime, but they do signal the package's own tests now target the newer toolchain.DB::listen() plus a breakpoint in the loader resolves most "wrong string" reports in one pass: is the row present, is the locale correct, is the tenant scope correct, is the cache stale. Clear the translation cache when in doubt — stale cache is the single most common cause of "I changed it and nothing happened".How can I help you explore Laravel packages today?