You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Next-gen version of Javaabu/Laravel-JS-Localization. Exports Laravel translations for @javaabu/i18n, ships @javaabu/i18n through javaabu/manifest, hooks into the Vite build, and can generate missing translations with AI (Gemini, OpenAI, etc.).
Built to the next-gen general rules: CI with test and coverage badges, full docs for people and agents, Laravel Boost guideline and skill, Javaabu codestyle, PHP 8.4 / Laravel 12+ (Laravel packages) or TypeScript (JS packages), Dhivehi and RTL, Blade and Inertia React for views, overridable controllers and policies with javaabu/permissions.
Studied: Javaabu/Laravel-JS-Localization (mariuzzo's lang:js: walks lang/, keys like en.messages, en.strings for JSON, en.vendor::group), @javaabu/i18n (load() shapes, window.Lang pickup, locale detection from <html lang>), and javaabu/manifest (contracts on Javaabu/manifest#13).
Architecture
PHP (Javaabu\JsLocalization\)
TranslationExporter collects lines through Laravel's own translation loader, not by walking folders, so it sees exactly what __() sees in PHP: lang/{locale}/*.php (including nested groups such as forum/thread), lang/{locale}.json, JSON paths that packages register (loadJsonTranslationsFrom), and namespaced package groups (loadTranslationsFrom) merged with the app's lang/vendor/{ns} overrides.
Output is the by-locale shape @javaabu/i18n loads natively: { "dv": { "Save": "…", "validation": { … }, "forum/thread": { … }, "filters::labels": { … } } }. Group names stay as Laravel names them, so __('forum/thread.title') and __('filters::labels.from') behave the same in JS as in PHP.
Config (config/js-localization.php, publishable, everything defaulted): locales (auto-detect), groups / except (wildcards), namespaces (*, a list, or false), json (include JSON lines), output path, split (one file per locale vs one file), format (json, or js that sets window.Lang for script-tag pages), route, blade.
php artisan lang:js writes the files (--locale, --output, --format, --single, --pretty, --stdout). Old flags that still make sense keep working (--json, -s/--source, --no-sort, a positional target), so the migration is mostly a composer require swap.
A cached, ETag-ed JSON endpoint GET /js-localization/{locale}.json serves one locale on demand, so Blade pages need no build step and no export. It's registered automatically (config route.enabled) and also available as Route::jsLocalization() for apps that want their own prefix/middleware.
JsLocalization::forLocale('dv') (facade) for Inertia shared props and SSR.
The provider registers package js-localization with manifest: ->js() (alias @javaabu/js-localization), a script asset js-localization::i18n (Blade entry: installs the __/trans/trans_choice globals and a loader pointing at the endpoint, locale from <html lang>), ->npm(['@javaabu/i18n' => …]), and ->dist() with a prebuilt bundle (i18n included) for apps without Vite.
Blade pages get it with no setup: a middleware requires the asset on HTML responses (config blade.auto, on by default; Inertia/JSON responses are untouched by manifest anyway). @manifestScripts('js-localization') for manual placement.
Inertia React/Vue: documented setup with configure({ loader }) from the Vite plugin's module, or shared props.
A Vite plugin in js/src, built to dist/vite.js and committed (manifest's pattern). On vite build it runs php artisan lang:js before bundling; in dev it watches lang/ (and vendor lang paths), re-exports, and pushes the changed locale to the page over HMR so useTranslation() re-renders without a reload.
It provides @javaabu/js-localization/translations (a lazy per-locale loader over the exported files) for SPA/Inertia bundles.
Building on manifest's plugin: manifest's manifest() has no hook for package-provided Vite plugins yet. I'll open a cross-package issue asking for one (packages register a Vite plugin module through discovery; manifest() runs it). Until then the plugin works standalone next to manifest(); once manifest supports it, apps won't need to add it.
AI translation (#5) — lang:translate --to=dv [--from=en] [--driver=] [--model=] [--group=] [--dry-run] [--force], only when laravel/ai is installed (suggest, feature-detected, clear message otherwise). Sends missing lines in batches with a structured-output schema, validates that :placeholders, <tags> and |-plural segments survive (rejects lines that don't), applies a glossary (config array or file), writes PHP/JSON lang files without touching existing lines unless --force, and writes a review report (Markdown + JSON) to storage/. Tests use laravel/ai's fakes only.
Missing report (#6) — lang:missing [--locale=] [--base=en] [--paths=] [--json]: scans PHP/Blade/JS/TS/TSX/Vue for __(), trans(), trans_choice(), @lang, Lang::get(), $t-style helpers (configurable function names), and reports keys missing per locale, plus lines present in the base locale but missing in the others. Non-zero exit for CI.
@javaabu/i18n isn't on npm. The JS in this repo develops against link:../i18n; CI clones and builds Javaabu/i18n first. Apps get github:Javaabu/i18n#main via ->npm() until it's published (needs Build dist when installed from Git i18n#1).
Requires laravel/framework (as manifest does), since it builds on manifest's middleware and Vite integration.
Vendor namespaces are exported by default (namespaces => '*') so package UIs work without setup; apps can narrow it. Everything exported is public, as with the old messages.js — documented.
Writing PHP lang files from lang:translate uses short-array var_export; comments in the edited file are lost. Documented, and --dry-run + the report make it reviewable.
Final summary: javaabu/js-localization v1.0 is done on main
Every next-gen issue is closed except #11 (docs.javaabu.com registration, for you). CI is green on the full matrix: PHP 8.4/8.5 × Laravel 12/13 × lowest/stable, Pint, a job without laravel/ai, and a JS job that runs lint, format, typecheck, checks the committed dist/, runs unit tests, real Vite builds and a Vite dev server.
What was built
Export (Export Laravel translations in @javaabu/i18n's format #2): JsLocalization reads lines through Laravel's translation loader, so JS sees exactly what __() sees: app and framework files, nested groups (forum/thread), JSON lines including packages', and namespaced package groups with lang/vendor overrides. Output is the by-locale shape @javaabu/i18n loads natively. php artisan lang:js writes one file per locale ({locale} in the path), one for all, or a window.Lang script, and keeps Laravel-JS-Localization's target argument, --json/-j, --source/-s, --no-sort and -c. Config: locales, groups/except with wildcards, json, path, sort, pretty.
Endpoint: GET /js-localization/{locale}.json, cached by a fingerprint of every language file, with ETag + no-cache revalidation (304s). Auto-registered (configurable prefix/middleware) and Route::jsLocalization() for custom routes.
Blade through manifest (Ship @javaabu/i18n through javaabu/manifest #3): asset js-localization::i18n puts __, trans, trans_choice and I18n on window and loads the page's locale (<html lang>) from the endpoint. A middleware inside manifest's injection requires it on full HTML pages only (not JSON, fragments, Inertia responses or Inertia root views). Prebuilt in dist/browser (i18n bundled) for apps without Vite. Inertia React/Vue setup is documented (Vite loader, endpoint, or shared props).
Vite plugin (Hook into the Vite build #4): vendor/javaabu/js-localization/dist/vite.js. Runs php artisan lang:js --stdout on start and every build, serves virtual:js-localization (one lazy chunk per locale) behind @javaabu/js-localization/translations, watches lang/ in dev and pushes changed locales over HMR (React/Vue re-render without a reload; Blade pages listen too).
lang:missing (Missing translations report #6): scans PHP/Blade/JS/TS/TSX/Vue for literal keys and reports keys a locale lacks (with file:line), lines missing vs. the base locale, and keys JS uses that aren't exported. Non-zero exit for CI, --json.
lang:translate (Generate missing translations with AI #5): optional laravel/ai (suggest, feature-detected, require-dev for tests, tested both ways). Batches with structured output, per-locale notes (Thaana/formal Dhivehi for dv) and glossary; rejects translations that change :placeholders, plural segments/ranges or HTML tags; never overwrites unless --force; --dry-run; Markdown + JSON review report. Tests only use TranslationAgent::fake().
Parity: tests/Fixtures/parity.json is checked against Laravel's __()/trans_choice() in PHPUnit and against @javaabu/i18n loaded with lang:js output in Vitest.
Dhivehi (Dhivehi localization and RTL #10): all command output in lang/en and lang/dv; tests use Thaana; uncertain terms are listed in docs/advanced-usage/localization.md.
Everything exported is public, as with the old messages.js; except leaves groups out (documented). The defaults leave out this package's and manifest's command output.
The automatic route is registered by default (like manifest's dist route), so Blade needs no setup; the macro is there for apps that want their own group.
PHP files written by lang:translate lose comments (documented; --dry-run and the report make it reviewable).
Command descriptions and exception messages stay in English; command output is translated.
No examples/ folder or changesets: this is a Composer package whose JS ships committed in dist/ (as manifest does); CHANGELOG.md is manual.
What changes once @javaabu/i18n is on npm
JsLocalizationServiceProvider::I18N_VERSION → ^1.0 (apps are told github:Javaabu/i18n#main today, which needs Build dist when installed from Git i18n#1 merged to install with dist).
package.json: "@javaabu/i18n": "link:../i18n" → ^1.0, and drop the "Build @javaabu/i18n" step and I18N_REF from .github/workflows/tests.yml (CI currently clones Javaabu/i18n at a pinned commit).
Run pnpm build and commit dist/browser (the bundled i18n changes the hash).
Follow-up: the Vite plugin is now registered with javaabu/manifest (Javaabu/manifest#14), so apps only add manifest() to vite.config.js. Options go in manifest({ plugins: { "js-localization": { ... } } }), and false turns the plugin off. Adding jsLocalization() by hand still works with the registered one turned off; manifest warns if both run. Real Vite builds and a dev server are tested both ways, and the docs, llms.txt and Boost skill show the manifest() setup. CI is green (f359e0f, manifest b88528d).
Activity
n4ff4h commentedon Nov 4, 2026
Implementation plan
Studied: Javaabu/Laravel-JS-Localization (mariuzzo's
lang:js: walkslang/, keys likeen.messages,en.stringsfor JSON,en.vendor::group),@javaabu/i18n(load()shapes,window.Langpickup, locale detection from<html lang>), and javaabu/manifest (contracts on Javaabu/manifest#13).Architecture
PHP (
Javaabu\JsLocalization\)TranslationExportercollects lines through Laravel's own translation loader, not by walking folders, so it sees exactly what__()sees in PHP:lang/{locale}/*.php(including nested groups such asforum/thread),lang/{locale}.json, JSON paths that packages register (loadJsonTranslationsFrom), and namespaced package groups (loadTranslationsFrom) merged with the app'slang/vendor/{ns}overrides.{ "dv": { "Save": "…", "validation": { … }, "forum/thread": { … }, "filters::labels": { … } } }. Group names stay as Laravel names them, so__('forum/thread.title')and__('filters::labels.from')behave the same in JS as in PHP.config/js-localization.php, publishable, everything defaulted):locales(auto-detect),groups/except(wildcards),namespaces(*, a list, orfalse),json(include JSON lines),outputpath,split(one file per locale vs one file),format(json, orjsthat setswindow.Langfor script-tag pages),route,blade.php artisan lang:jswrites the files (--locale,--output,--format,--single,--pretty,--stdout). Old flags that still make sense keep working (--json,-s/--source,--no-sort, a positional target), so the migration is mostly acomposer requireswap.GET /js-localization/{locale}.jsonserves one locale on demand, so Blade pages need no build step and no export. It's registered automatically (configroute.enabled) and also available asRoute::jsLocalization()for apps that want their own prefix/middleware.JsLocalization::forLocale('dv')(facade) for Inertia shared props and SSR.Shipping @javaabu/i18n through manifest (#3)
js-localizationwith manifest:->js()(alias@javaabu/js-localization), a script assetjs-localization::i18n(Blade entry: installs the__/trans/trans_choiceglobals and a loader pointing at the endpoint, locale from<html lang>),->npm(['@javaabu/i18n' => …]), and->dist()with a prebuilt bundle (i18n included) for apps without Vite.blade.auto, on by default; Inertia/JSON responses are untouched by manifest anyway).@manifestScripts('js-localization')for manual placement.configure({ loader })from the Vite plugin's module, or shared props.Vite (#4)
js/src, built todist/vite.jsand committed (manifest's pattern). Onvite buildit runsphp artisan lang:jsbefore bundling; in dev it watcheslang/(and vendor lang paths), re-exports, and pushes the changed locale to the page over HMR souseTranslation()re-renders without a reload.@javaabu/js-localization/translations(a lazy per-locale loader over the exported files) for SPA/Inertia bundles.manifest()has no hook for package-provided Vite plugins yet. I'll open across-packageissue asking for one (packages register a Vite plugin module through discovery;manifest()runs it). Until then the plugin works standalone next tomanifest(); once manifest supports it, apps won't need to add it.AI translation (#5) —
lang:translate --to=dv [--from=en] [--driver=] [--model=] [--group=] [--dry-run] [--force], only whenlaravel/aiis installed (suggest, feature-detected, clear message otherwise). Sends missing lines in batches with a structured-output schema, validates that:placeholders,<tags>and|-plural segments survive (rejects lines that don't), applies a glossary (config array or file), writes PHP/JSON lang files without touching existing lines unless--force, and writes a review report (Markdown + JSON) tostorage/. Tests use laravel/ai's fakes only.Missing report (#6) —
lang:missing [--locale=] [--base=en] [--paths=] [--json]: scans PHP/Blade/JS/TS/TSX/Vue for__(),trans(),trans_choice(),@lang,Lang::get(),$t-style helpers (configurable function names), and reports keys missing per locale, plus lines present in the base locale but missing in the others. Non-zero exit for CI.Order of work
lang:js, config, endpoint.Decisions and open questions
link:../i18n; CI clones and builds Javaabu/i18n first. Apps getgithub:Javaabu/i18n#mainvia->npm()until it's published (needs Build dist when installed from Git i18n#1).laravel/framework(as manifest does), since it builds on manifest's middleware and Vite integration.namespaces => '*') so package UIs work without setup; apps can narrow it. Everything exported is public, as with the oldmessages.js— documented.lang:translateuses short-arrayvar_export; comments in the edited file are lost. Documented, and--dry-run+ the report make it reviewable.n4ff4h commentedon Nov 4, 2026
Final summary: javaabu/js-localization v1.0 is done on
mainEvery
next-genissue is closed except #11 (docs.javaabu.com registration, for you). CI is green on the full matrix: PHP 8.4/8.5 × Laravel 12/13 × lowest/stable, Pint, a job without laravel/ai, and a JS job that runs lint, format, typecheck, checks the committeddist/, runs unit tests, real Vite builds and a Vite dev server.What was built
JsLocalizationreads lines through Laravel's translation loader, so JS sees exactly what__()sees: app and framework files, nested groups (forum/thread), JSON lines including packages', and namespaced package groups withlang/vendoroverrides. Output is the by-locale shape@javaabu/i18nloads natively.php artisan lang:jswrites one file per locale ({locale}in the path), one for all, or awindow.Langscript, and keeps Laravel-JS-Localization's target argument,--json/-j,--source/-s,--no-sortand-c. Config:locales,groups/exceptwith wildcards,json,path,sort,pretty.GET /js-localization/{locale}.json, cached by a fingerprint of every language file, withETag+no-cacherevalidation (304s). Auto-registered (configurable prefix/middleware) andRoute::jsLocalization()for custom routes.js-localization::i18nputs__,trans,trans_choiceandI18nonwindowand loads the page's locale (<html lang>) from the endpoint. A middleware inside manifest's injection requires it on full HTML pages only (not JSON, fragments, Inertia responses or Inertia root views). Prebuilt indist/browser(i18n bundled) for apps without Vite. Inertia React/Vue setup is documented (Vite loader, endpoint, or shared props).vendor/javaabu/js-localization/dist/vite.js. Runsphp artisan lang:js --stdouton start and every build, servesvirtual:js-localization(one lazy chunk per locale) behind@javaabu/js-localization/translations, watcheslang/in dev and pushes changed locales over HMR (React/Vue re-render without a reload; Blade pages listen too).lang:missing(Missing translations report #6): scans PHP/Blade/JS/TS/TSX/Vue for literal keys and reports keys a locale lacks (with file:line), lines missing vs. the base locale, and keys JS uses that aren't exported. Non-zero exit for CI,--json.lang:translate(Generate missing translations with AI #5): optional laravel/ai (suggest, feature-detected,require-devfor tests, tested both ways). Batches with structured output, per-locale notes (Thaana/formal Dhivehi fordv) and glossary; rejects translations that change:placeholders, plural segments/ranges or HTML tags; never overwrites unless--force;--dry-run; Markdown + JSON review report. Tests only useTranslationAgent::fake().tests/Fixtures/parity.jsonis checked against Laravel's__()/trans_choice()in PHPUnit and against@javaabu/i18nloaded withlang:jsoutput in Vitest.docs/migrating.md,llms.txt, AGENTS.md/CLAUDE.md, the Boost guideline andjavaabu-js-localizationskill (with command and troubleshooting references). A test checks every page is linked, links resolve, and every config key and command option is documented.lang/enandlang/dv; tests use Thaana; uncertain terms are listed indocs/advanced-usage/localization.md.Cross-package
manifest()runs. Until then apps addjsLocalization()next tomanifest(); nothing is blocked on it.@javaabu/i18n/global's types don't declare the window globals. The Blade entry redeclares them for now.Intentionally out of scope / decisions
laravel/framework(as manifest does).messages.js;exceptleaves groups out (documented). The defaults leave out this package's and manifest's command output.lang:translatelose comments (documented;--dry-runand the report make it reviewable).examples/folder or changesets: this is a Composer package whose JS ships committed indist/(as manifest does);CHANGELOG.mdis manual.What changes once @javaabu/i18n is on npm
JsLocalizationServiceProvider::I18N_VERSION→^1.0(apps are toldgithub:Javaabu/i18n#maintoday, which needs Build dist when installed from Git i18n#1 merged to install withdist).package.json:"@javaabu/i18n": "link:../i18n"→^1.0, and drop the "Build @javaabu/i18n" step andI18N_REFfrom.github/workflows/tests.yml(CI currently clones Javaabu/i18n at a pinned commit).pnpm buildand commitdist/browser(the bundled i18n changes the hash).resources/js/blade.ts.What you need to do
CODECOV_TOKENavailable (org secret).docs/advanced-usage/localization.md.n4ff4h commentedon Nov 6, 2026
Follow-up: the Vite plugin is now registered with javaabu/manifest (Javaabu/manifest#14), so apps only add
manifest()tovite.config.js. Options go inmanifest({ plugins: { "js-localization": { ... } } }), andfalseturns the plugin off. AddingjsLocalization()by hand still works with the registered one turned off; manifest warns if both run. Real Vite builds and a dev server are tested both ways, and the docs, llms.txt and Boost skill show themanifest()setup. CI is green (f359e0f, manifestb88528d).