andywer/js-localization
Bring Laravel's translations to JavaScript. An artisan command exports your PHP lang files, and a lightweight client-side translator looks up keys and replaces placeholders at runtime, so backend and frontend share the same strings with no duplication. Works with any frontend stack.
The package's whole job is to turn Laravel lang/ files into a public/js/localization.js that exposes a browser-side Lang.get() / Lang.choice() helper. Integration is three steps.
Install and publish
composer require andywer/js-localization
php artisan vendor:publish --provider="Andywer\JsLocalization\JsLocalizationServiceProvider"
On Laravel 5.5+ the provider is auto-discovered. On older versions, register Andywer\JsLocalization\JsLocalizationServiceProvider in config/app.php and add the JS facade alias — you need the alias if any code calls JS::.
Publishing gives you config/js-localization.php (the output path, typically public/js/localization.js) and a JS stub.
Export a group of strings
Don't scatter JS::put() across controllers. Put it in one place — AppServiceProvider::boot() or a dedicated LocalizationServiceProvider:
use JS;
public function boot()
{
JS::put(['messages' => 'messages', 'validation' => 'validation']);
}
The array is ['filename' => 'lang/group-name']. Only list groups the front end actually renders — this is the single biggest lever on payload size.
Consume in the browser
<script src="{{ asset('js/localization.js') }}?v={{ filemtime(public_path('js/localization.js')) }}"></script>
<script>
console.log(Lang.get('messages.welcome'));
console.log(Lang.choice('messages.apples', 3));
</script>
Where to look first when it doesn't work
public/js/localization.js exist after a request? If not, JS::put() never ran or the web server can't write to public/js.public/js writable in dev? chmod/chown the directory.Everything after that is ordinary Laravel translations plus a small JS helper. There is no route, no middleware, no database — one file.
Treat the export as a build step, not a request step. The default behavior writes on boot, on every request. That's fine in local, but in staging/production write it at deploy time instead:
// app/Console/Commands/ExportLang.php
public function handle()
{
JS::put(['messages' => 'messages']);
}
php artisan app:export-lang # in your deploy script
This eliminates runtime writes to public/ (mandatory on Heroku, Lambda, read-only Docker images) and removes the concurrent-overwrite race entirely.
Pin the group set; never vary it by page. If page A exports messages and page B exports validation, whichever request finishes last wins and you get nondeterministic missing keys in users' browsers. Export a stable superset.
Cache-bust aggressively. Browsers and CDNs will happily serve yesterday's file. filemtime() in the query string is the minimum; running it through Mix/Vite is cleaner; setting Cache-Control on the file at the web-server level is best.
One source of truth per key. Pick Lang.get() for anything client-rendered and stop mirroring those keys with @lang(). Mixing both for the same key guarantees drift the moment copy changes.
Multi-locale: generate per locale, don't regenerate per request. The package captures whichever locale is active when JS::put() runs, so a locale-switching app needs either separate output files (localization.en.js, localization.de.js) or a regeneration step that sets app()->setLocale() first. If neither is comfortable, this is the point to replace the package with a small controller returning per-locale JSON and have a ~30-line Lang helper hydrate from it — you keep the Lang.get() contract and gain caching headers, lazy loading, and no file writes.
Keep the export behind your own interface. Wrap JS::put() in a LangExporter service or a Blade directive. The package is ~100 lines of glue; isolating it makes a future swap to a hand-rolled Artisan command or a TMS-driven JSON artifact a one-file change rather than a hunt.
Test parity where it matters. Write one test comparing PHP trans() output against the generated JS Lang.get() for keys with :placeholders and for Lang.choice with {0}/[1,19] ranges. Divergence here is quiet and user-visible.
public/ in production is a trap. Read-only filesystems fail loudly or silently no-op. Switch to deploy-time generation before you hit this, not after.trans() structures don't round-trip reliably. Validate plural ranges, :placeholder replacement, JSON lang/en.json files, and vendor:: namespaced keys against what actually lands in the generated file — don't assume Laravel parity.</script> can break the file or produce XSS. Test with a deliberately nasty string.Lang is undefined almost always means one of three things: the <script> tag is missing, the generated file is empty, or JS::put() was never called. Inspect public/js/localization.js directly before opening devtools.localization.js with different group sets can yield partial content. Deploy-time generation is the real fix.JS:: — add the alias or import the class fully qualified.vendor:publish. Better: don't customize the stub.composer.json constraints and the last commit date against your Laravel/PHP versions; expect to need a fork or an "illuminate/support": "*" constraint override.Lang) is genuinely cheap.How can I help you explore Laravel packages today?