fixhub/js-localization
Bridges Laravel's localization system and your JavaScript front end, exposing PHP language files as JS-accessible translations so both sides share the same strings. Keeps messages in sync across locales and avoids duplicating text between server and client.
Install
composer require fixhub/js-localization
The service provider is auto-discovered on Laravel 5.5+. On older versions add it manually to config/app.php:
'providers' => [
// ...
Fixhub\JsLocalization\JsLocalizationServiceProvider::class,
],
Publish the config first — this is the file you should read before anything else
php artisan vendor:publish --provider="Fixhub\JsLocalization\JsLocalizationServiceProvider"
Open config/js-localization.php. It tells you which lang paths are scanned, which key group is exported, and where the generated file lands. Every "it's not working" issue traces back to this file.
First use case: inline translations on a single page
<script>
window.Lang = {!! Lang::js() !!};
</script>
<script>
console.log(Lang.get('messages.welcome'));
</script>
Or generate a static bundle for the whole app
php artisan lang:js
This reads your lang/ (or resources/lang/) directory and writes a JS file — typically public/js/messages.js — which you then <script src> in your layout. Use the static approach if you have more than a handful of strings; use the inline approach for quick one-offs or per-page overrides.
Verify it worked before writing any JS against it
php artisan lang:js && head -c 500 public/js/messages.js
If that file is empty or missing, stop and fix the config; no amount of JS debugging will help.
The most common setup is a deploy-time artifact:
# In your deploy script, after composer install / config cache
php artisan lang:js
{{-- resources/views/layouts/app.blade.php --}}
<script src="{{ asset('js/messages.js') }}?v={{ filemtime(public_path('js/messages.js')) }}"></script>
Add php artisan lang:js to your deploy pipeline (Envoyer, Forge, GitHub Actions, Dockerfile RUN). Translations change rarely, so you don't want to regenerate on every request.
<script>
window.Lang = {!! Lang::js() !!};
</script>
Good for SPAs bootstrapped by Blade, or when the locale is resolved per-request from the session/user record and you don't want to ship 20 locale bundles.
Generate one file per locale and load on demand:
php artisan lang:js --locale=fr
<script>
window.__locale = '{{ app()->getLocale() }}';
</script>
<script src="{{ asset('js/messages.' . app()->getLocale() . '.js') }}"></script>
For a locale switcher without a full reload, fetch the bundle and merge:
async function switchLocale(locale) {
const res = await fetch(`/js/messages.${locale}.js`);
const text = await res.text();
// Bundle assigns window.Lang, so eval or re-inject via a <script> tag
const s = document.createElement('script');
s.textContent = text;
document.head.appendChild(s);
}
Re-injecting a <script> tag is cleaner than eval and keeps CSP simpler than unsafe-eval.
Treat window.Lang.get() as the single boundary between Blade and your components:
// resources/js/i18n.js
export const t = (key, replacements = {}) => {
if (!window.Lang) return key;
return window.Lang.get(key, replacements);
};
<template><button>{{ t('messages.save') }}</button></template>
This keeps the package out of your component tree entirely — the bundle is loaded once by Blade, components just read the global.
Laravel's :name placeholders work in JS if you pass replacements:
// lang/en/messages.php
return ['welcome' => 'Welcome, :name!'];
Lang.get('messages.welcome', { name: 'Ada' }); // "Welcome, Ada!"
Prefer this over string concatenation — it survives RTL, word order changes, and pluralization quirks in other languages.
If you resolve locale from Accept-Language or a user setting, do it in middleware that runs before the view renders, so Lang::js() and the --locale flag pick up the right value:
public function handle($request, Closure $next)
{
app()->setLocale($request->user()?->locale ?? $request->getPreferredLanguage(['en', 'fr', 'de']));
return $next($request);
}
Zero stars, zero dependents, and a low opportunity score. Before committing, verify:
Lang::js() API and config shape are inherited from mariuzzo/laravel-js-localization, which stopped seeing active development years ago.lang/ and json_encode-ing it) with ~30 lines of your own code.For most apps the whole package is: walk the lang directory, json_encode it, write a file. Don't take a dependency you can't maintain.
{!! Lang::js() !!} is an unescaped output. If a translator puts </script> in a string, you get an XSS vector. Mitigations:
<script>
window.Lang = {!! json_encode(Lang::js()) !!}; // still unescaped output
</script>
Better, use the JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT flags or the static file approach, which avoids the inline context entirely. Verify with:
grep -R '</script' lang/
Lang.get('messages.welcome') assumes the bundle is nested. If your config flattens keys to "messages.welcome": "...", the same call works — but passing an already-dotted key into a nested lookup with Lang.get('messages') returns the whole array. Pick one access convention and stick to it in i18n.js.
Lang.get('messages.typo') returns "messages.typo" — visible on screen but silent in the console. In dev, add a check:
const originalGet = Lang.get;
Lang.get = (key, ...rest) => {
const val = originalGet(key, ...rest);
if (val === key) console.warn('[i18n] missing:', key);
return val;
};
php artisan config:cache bakes config/js-localization.php in. If you edit lang files and regenerate, also php artisan config:clear when debugging, or you'll chase a stale config.
The generated JS file is a static asset — browsers will serve a stale copy after deploy. Append ?v=filemtime(...) or ?v={{ md5_file(...) }} to the <script src> URL. This is the single most common "translations didn't update" report.
Laravel uses :name. Some translation files use :Name (capitalized, substituted differently). Confirm both sides use the same casing — the package does a straight replacement and won't warn on a miss.
The package exports strings; it does not resolve | plural forms or {0}, {1} ICU syntax. If your lang/ files use trans_choice-style plurals:
'apples' => 'one apple|:count apples',
…you'll get the raw | string in JS. Either post-process the bundle in i18n.js, or export separate apples_one / apples_other keys. Don't assume Laravel's pluralization makes it across the bridge.
All lang files × all groups in one file gets heavy for large apps. Scope it in config (single group, or --locale) rather than shipping every translation to every user.
If you need per-page bundles, key filtering, or a translated-and-flattened format for a specific frontend library, wrap the package behind your own helper and a small Blade directive rather than calling Lang::js() scattered across views:
Blade::directive('translations', fn () => "<?php echo app('translator.js')->render(); ?>");
Then swapping the package out later is a one-file change.
How can I help you explore Laravel packages today?