elegantly/laravel-translator
Elegantly translate your Laravel app: laravel-translator scans your code and language files for missing keys, fills them through translation services like Google, DeepL and OpenAI, and keeps every locale in sync—all via simple Artisan commands and clean configuration.
1. Install and publish
composer require elegantly/laravel-translator
php artisan vendor:publish --tag="translator-config"
If you plan to store translations in the database (the usual reason to reach for this package), also publish and run the migrations:
php artisan vendor:publish --tag="translator-migrations"
php artisan migrate
2. Configure config/translator.php
The two settings that matter on day one:
driver — file, database, or a combination. database makes strings editable from the UI and merged over your lang/ files at runtime; file keeps everything in version control and uses the UI only as an editor that writes .php/.json files back.locales — the list of locales the UI shows as columns. Keep this in sync with what your app actually serves.Also set scan_paths (usually app, resources/views, routes) so the scanner knows where to look for __(), @lang, and trans() calls.
3. Look at the routes first
Before touching anything else, open the published config and find the UI route/prefix and its middleware. The management screen ships with a minimal middleware stack — put it behind auth (or at least auth + an ability gate) before you deploy, not after. Then hit /translator (or your configured prefix) and you should see the locale grid.
4. First real use case
The canonical first run:
php artisan translator:scan # collect keys used in code
php artisan translator:sync # push file-based keys into the DB
Open the UI, translate the new keys in the missing-locale columns, then export back:
php artisan translator:export
Commit the regenerated lang/ files. You now have a loop: scan → translate in the browser → export → commit.
UI-as-editor, files-as-source-of-truth
Keep driver on the file layer and treat the database as a workspace. Translators edit in the browser; the deploy pipeline runs translator:export and the resulting lang/{locale}/*.php and lang/{locale}.json files are what get reviewed and merged. This gives non-developers a spreadsheet-like editor without making the database a runtime dependency, and it keeps diffs visible in code review.
DB-as-runtime, files-as-seed
The inverse: run translator:sync once to seed the translations table from your existing lang/ files, then let the database be authoritative. Strings become editable in production without a deploy. Use this only when you accept that translation state lives outside version control — back up the table, and export to files periodically so you have a recoverable snapshot in git.
Gate the UI per environment
A common pattern is to register the routes only where they're useful:
// config/translator.php or a service provider
'middleware' => ['web', 'auth', 'can:manage-translations'],
and wrap the UI in an environment check (app()->isLocal() || app()->isStaging()). Translation UIs are an easy path to content injection if left open.
Scanning as a CI check, not just a convenience
Add the scan to a scheduled job or a CI step and fail (or open a PR) when new keys appear without translations. Because the scanner reads your Blade/PHP source, a key typo like __('auth.lgin') shows up as a brand-new untranslated key immediately — which catches a class of bugs that otherwise only surface when a user reports a literal key on screen.
Nested vs flat key conventions
The package deals with both lang/en/messages.php returning nested arrays and JSON-style flat keys (lang/en.json). Pick one and stay consistent per namespace. For UI-heavy apps, JSON keys ("Welcome back" as the key) make the translation grid far more readable than messages.welcome_back; for domain-heavy apps the nested array approach scales better because the key path doubles as context.
Pluralization and parameters
The UI edits strings, not ICU structures. For anything with :count, :name, or | plural separators, keep the parameters in the string exactly as they appear in code and verify both plural forms in every locale after editing — a grid cell is easy to truncate. Where a locale needs more than two plural forms (e.g. Slavic languages), prefer file-based edits and review carefully.
Multi-tenant / dynamic locales
If tenants have their own locale sets, don't lean on the global locales array. Instead scope the DB loader by a tenant discriminator (a tenant_id column plus a global scope on the model) and let each tenant's rows merge over the base files. Keep the UI read-only for tenants you don't own.
Pair with a translation service
Export → send .php/.json bundles to an external translation vendor → import the results via translator:sync. The scan step tells you exactly which keys are new, so you only pay for deltas rather than re-translating the whole catalog.
The management routes are not protected by default. This is the single biggest footgun. Before your first deploy, confirm what middleware the UI routes carry and add auth. A publicly reachable translation editor lets anyone rewrite user-facing copy — or inject markup if you render strings with {!! !!} anywhere.
Caching will make you think the package is broken. Laravel caches loaded translation groups per request and, with the DB loader, the merged result can outlive your edit. After changing strings, clear the framework cache (php artisan cache:clear, or php artisan optimize:clear in older setups) and re-check. If you see a stale string in the UI but a fresh one in tinker, it's the cache, not the loader.
Driver precedence is the whole mental model. With database enabled, DB rows win over file entries for the same key+locale. When someone says "I edited the file and nothing changed," the answer is almost always a lingering DB row shadowing it. translator:sync is the reconcile step in one direction; treat it as destructive-by-default and read the help text before running it on production data.
Scanning produces false positives — and misses. Dynamic keys like __($key) or __("messages.{$type}.title") can't be resolved statically, so they'll never appear in the grid. Concatenated and variable-driven keys need to be added manually or by seeding a base file. Conversely, commented-out code and string literals that look like translation keys will show up. Expect to prune.
Don't scan vendor/ or node_modules/. Restrict scan_paths tightly; a wide scan both slows the command and pollutes the UI with hundreds of keys from packages you'll never translate.
Export overwrites files. Running translator:export regenerates lang/ content, which means hand-written comments, custom ordering, and any manual tweaks in those files are gone. If developers edit files directly, establish that exports happen on a clean branch and that the diff is reviewed — otherwise you'll silently lose work.
Git conflicts on large locale files. lang/en.json and big messages.php files are conflict magnets with multiple contributors. Keeping translations in the DB and exporting only on release, or splitting files by domain (lang/en/auth.php, lang/en/billing.php), dramatically reduces merge pain.
Fallback behavior hides missing translations. With fallback_locale set, a missing fr key silently renders English. That's good for users and bad for QA — the grid's "missing" indicator is your real coverage report, so check it rather than clicking through the app.
Locale codes must match exactly. en_US vs en-us vs en will create separate columns and separate rows, and nothing will warn you. Normalize on the config('app.locale') / config('app.supported_locales') values you actually use and audit the config when adding a language.
Performance on the DB driver. Every request that touches translations queries the translations table unless something caches it. On a busy app, wrap the loader with a cache layer keyed by locale and bust it on write — or run the file driver in production and use the DB only in staging.
Small package, thin ecosystem. With a modest star count and no dependents, expect to read the source when something behaves oddly — it's a small codebase and that's usually faster than searching for answers. Pin the version in composer.json, and before upgrading, check the changelog for changes to the config keys, route names, and command signatures, since pre-1.0-style packages move those around.
Extension point worth knowing: the package swaps Laravel's translation loader. If you need custom behavior — tenant scoping, a Redis cache in front of the DB, a fallback chain per channel — subclass or decorate the loader and rebind translation.loader in your own provider rather than forking. If you've previously used spatie/laravel-translation-loader, they both want that binding; enable only one.
How can I help you explore Laravel packages today?