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 is a thin serialization adapter sitting on top of Illuminate\Translation\Translator. It contributes a service provider, a config file, a facade (JS in the published docs; JSLocalization in the source-era writeups — see key questions), and a Blade directive, then renders the active locale's translation array into a browser-global Lang object exposing get, has, and choice.
Where it fits: a Laravel monolith that renders Blade server-side with islands of JavaScript — form validation copy, toasts, confirmation dialogs, small widget labels. Where it does not fit: Inertia/React/Vue SPAs, Vite module graphs, TypeScript frontends, or anything requiring runtime locale switching. There is no ESM export, no type definitions, no build-time extraction, and no per-locale artifact model. Treat the architectural decision as "PHP lang/ files are the single source of truth and the browser is a read-only consumer of a generated artifact" — a commitment that affects who can change copy and how copy ships.
Mechanically it is a half-day: composer require, vendor:publish, add one <script> tag, call JS::put() from a service provider. The real cost is elsewhere and should dominate the estimate:
composer.json constraints to fail resolution against Laravel 9/10/11 and PHP 8.1+, and expect removed helpers (array_get, str_*), facade accessor signatures, and Blade directive registration to need patching. Budget a 2–3 day spike with a hard go/no-go before any roadmap commitment.andywer/laravel-js-localization) and the documented install target (andywer/js-localization) are not obviously the same artifact, and the documented facade (JS) does not match the class name in the older writeups (JSLocalization). Confirm which Composer package the docs actually describe before anyone runs composer require.Ranked for a risk register:
| Risk | Severity | Note |
|---|---|---|
| Unmaintained dependency in the page-render path | High | Single maintainer, no release cadence, no CI evidence. Mitigate by vendoring. |
| Over-exposure of translation payload | High | Bulk export ships admin-panel, email, and back-office validation strings to anonymous users. Must be an explicit allowlist, never putAll(). |
Runtime file writes to public/ |
High | Fails or silently no-ops on read-only images, Lambda, Heroku ephemeral filesystems. Must move to deploy-time generation. |
| Injection / escaping | Medium | Translation values are content interpolated into a JS file; quotes, backticks, </script> produce XSS or syntax breakage. |
| Pluralization parity drift | Medium | JS Lang.choice reimplements Laravel's pipe/range rules; divergence is silent and untested. |
| Locale frozen per page load | Medium | One global file, single active locale. Per-user locale switching requires per-locale artifacts — a redesign, not a config change. |
| Concurrent overwrite races | Low–Medium | Varying group sets per request yield partial or last-writer-wins content. |
lang/*.json files, vendor::file.key namespaced keys, and nested arrays — or only flat PHP arrays?Lang.choice against trans_choice for range-based rules, and is there a test harness for it?Qualify the target application against four criteria before committing: (a) Laravel monolith with Blade as the primary rendering layer; (b) PHP lang/ files already the trusted source of copy; (c) a bounded, enumerable set of client-side strings — hundreds, not thousands; (d) no non-engineer translator workflow in the near term. Missing any of these shifts the recommendation to a JSON endpoint plus a ~30-line Lang helper, which delivers the same pattern with none of the dependency risk.
Adopt the pattern, not the package. Sequence the work so the package is a temporary implementation detail behind a boundary we own:
TranslationExporter::export(string $locale, array $groups): array — and a companion Artisan command app:export-lang.Lang.get / Lang.choice contract the frontend will use regardless of backend.The swap to a JSON route, a Vite-imported locale module, or a TMS CDN artifact then becomes a one-file change with no frontend churn. This boundary is the single most important deliverable of the initiative, and it is what makes the "unmaintained" risk acceptable to sign off on.
Do not accept a green composer require as proof. Gate on:
"illuminate/support": "*" escape hatch.</script> must not break the bundle.trans_choice output to Lang.choice output across {0}, [1,19], {1} and default-branch cases, in the target locales.| Phase | Scope | Effort | Exit criteria |
|---|---|---|---|
| 0 — Spike | Compatibility, API surface confirmation, adversarial escaping test | 2–3 days | Documented go/no-go; facade and package identity resolved |
| 1 — Boundary | Internal exporter contract, Artisan command, deploy-time generation | 3–5 days | app:export-lang runs in CI; no runtime writes in prod |
| 2 — Pilot | One surface only — validation messages or global alerts | 2–3 days | Zero PHP/JS copy drift on that surface; cache-busting verified |
| 3 — Rollout | Inventory remaining hardcoded strings; allowlisted expansion group by group | 1–2 weeks, incremental | Every exported group has an owner and a reason to be public |
| 4 — Normalize | Replace package internals with owned implementation if fork was needed | 2 days | Package removable from composer.json without frontend change |
Phase 3 is the schedule risk, not Phase 0. String inventory across a mature Blade codebase is unbounded until someone actually counts — force a count in Phase 0 and re-baseline the roadmap on it.
Assume full ownership from day one. Assign a named maintainer, keep the vendored/forked code in-repo under version control, and add it to the dependency-review list so it is not orphaned when the original engineer rotates off. Do not track upstream; pin to a commit and treat divergence as expected. Republishing overwrites the JS stub — if any customization survives, it must live in our code, not in vendor/.
On-call needs a one-page runbook, because the failure signature is opaque to anyone who has not seen it:
Lang is not defined → missing <script> include, empty generated file, or JS::put() never called on that route. Check the generated file on disk before debugging JavaScript.Publish this triage tree; it converts a recurring 30-minute investigation into a 2-minute check.
lang/de directory" plus a regeneration step — cheap, and the main strategic payoff. But per-user locale switching does not scale on this design without per-locale artifacts; if the roadmap includes user-selectable language, that requirement must be surfaced now, not discovered in Phase 3.app:export-lang runs in CI; no runtime writes to public/ in production; package removable without frontend change.How can I help you explore Laravel packages today?