veltix / wayfinder-locales
Localized route URLs and TypeScript translation catalogs for Laravel Wayfinder (next branch).
Requires
- php: ^8.2
- illuminate/console: ^12.0|^13.0
- illuminate/filesystem: ^12.0|^13.0
- illuminate/routing: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
- laravel/ranger: ^0.2.4
- laravel/wayfinder: dev-next
Requires (Dev)
- laravel/pint: ^1.29
- orchestra/testbench: ^10.1|^11.0
README
Localized route URLs and type-safe TypeScript translation catalogs for Laravel Wayfinder.
One logical route, a different URL per locale:
products → /products /de/produkte /fr/produits
products.show → /products/{id} /de/produkte/{id} /fr/produits/{id}
…plus t() / tChoice() over your lang/ files, with a TranslationKey union so a typo is a
build error rather than a string that renders as itself.
This is v3. It targets
laravel/wayfinder: dev-next(thenextbranch) and nothing else. The stable^0.1line is not supported. See UPGRADING.md.
Requirements
- PHP 8.2+
- Laravel 12 or 13
laravel/wayfinder: dev-next
Installation
composer require veltix/wayfinder-locales php artisan vendor:publish --tag=wayfinder-locales-config
The service provider is auto-discovered. On boot it registers:
- the
Route::localized()macro, - the
setlocalemiddleware alias, - the
wayfinder-locales:generateartisan command, - a
Routesconverter binding that adds localized URL templates to Wayfinder's own generation.
How it works
The two halves of the package are independent, and only one of them generates files.
Routes. Laravel serves a single {locale}-parameterised URI. Route::localized() tags the
route with a per-locale path segment map; at generation time the package's converter — bound over
Wayfinder's Converters\Routes — turns that into a template table the generated function picks
from. So localized routes come out of wayfinder:generate, not out of a second generator.
Translations. wayfinder-locales:generate reads lang/ and writes the frontend catalogs. It
has nothing to do with routing and never writes into resources/js/wayfinder — that directory is
Wayfinder's, and wayfinder:generate deletes anything there it did not write itself.
Configuration
Everything lives in config/wayfinder-locales.php. There is one locale list and one default
locale, shared by both halves.
return [ 'locales' => ['en', 'de'], 'default_locale' => env('WAYFINDER_DEFAULT_LOCALE', 'en'), 'enabled' => env('WAYFINDER_LOCALES_ENABLED', true), 'mode' => env('WAYFINDER_LOCALES_MODE', 'segment'), 'strict' => env('WAYFINDER_LOCALES_STRICT', true), 'locale_parameter' => env('WAYFINDER_LOCALE_PARAMETER', 'locale'), 'hide_default_prefix' => env('WAYFINDER_HIDE_DEFAULT_PREFIX', false), 'exclude_groups' => ['routes'], 'action_key' => 'wayfinder_locales', ];
| key | what it does |
|---|---|
locales |
Every locale generated for. Drives the Locale union, the catalog modules, and the locales setlocale will accept. |
default_locale |
Seeds the TranslationKey union, is the runtime's fallback for a missing key, and is the locale whose URL prefix hide_default_prefix drops. Should be in locales. |
enabled |
Turn off localized URL emission without unwinding your Route::localized() calls. |
mode |
segment replaces the first static slug segment after the locale. tail treats the translation as the whole localized path tail. |
strict |
Throw on malformed localized() metadata instead of skipping the route. |
locale_parameter |
The URI parameter carrying the locale. |
hide_default_prefix |
Register an unprefixed twin ({name}.default) for the default locale and emit its URL without the prefix. |
exclude_groups |
Lang groups kept out of the frontend catalogs. |
action_key |
Route action key localized() stashes its map under. Change only on a collision. |
Localized routes
use Illuminate\Support\Facades\Route; Route::middleware('setlocale')->group(function () { Route::get('/{locale}/products', [ProductController::class, 'index']) ->name('products') ->localized(['en' => 'products', 'de' => 'produkte']); Route::get('/{locale}/products/{product}', [ProductController::class, 'show']) ->name('products.show') ->localized(['en' => 'products', 'de' => 'produkte']); });
Use {locale?} if the segment may be omitted; the generated function fills in default_locale.
With hide_default_prefix => true and default_locale => 'en', localized() also registers an
unprefixed twin named products.default bound to en, so /products and /en/products both
resolve.
Then run Wayfinder as usual:
php artisan wayfinder:generate
import products from '@/wayfinder/routes/products'; products.url({ locale: 'de' }); // "/de/produkte" products.show.url({ locale: 'de', product: 7 }); // "/de/produkte/7"
The locale argument is typed to the locales that route declares, so products.url({ locale: 'es' })
is a type error.
On the server, lroute() fills the locale parameter in for you:
lroute('products'); // active locale lroute('products', [], 'de'); // "/de/produkte"
Translations
Point locales at your lang directories and generate:
php artisan wayfinder-locales:generate
resources/js/translations/
├── en.ts # flat catalog, its own lazily-loaded chunk
├── de.ts
├── keys.ts # TranslationKey union + per-key placeholder types
├── locales.ts # Locale union, locales[], defaultLocale, setLocale/getLocale
└── index.ts # t(), tChoice(), loadLocale()
lang/{locale}/{group}.php becomes dotted keys (messages.nested.key), lang/{locale}.json
contributes its keys verbatim, and lang/vendor/{package}/{locale} becomes package::group.key.
The default locale's catalog is the source of truth for the key union.
Tell the runtime which locale is active once, at boot — a getter is re-read on every lookup, which is what you want with Inertia:
import { loadLocale } from '@/translations'; import { setLocale } from '@/translations/locales'; setLocale(() => usePage().props.locale); await loadLocale('de');
import { t, tChoice } from '@/translations'; t('messages.greeting', { name: 'Ada' }); // placeholders are typed per key tChoice('messages.apples', 3);
Missing keys fall back to the default locale's catalog, then to the key itself — the same order
Laravel's __() uses.
Commands
| command | |
|---|---|
php artisan wayfinder:generate |
Wayfinder's own. Emits routes and actions, localized URLs included. |
php artisan wayfinder-locales:generate [--path=] |
Emits the translation output. --path is the JS root, default resources/js. |
License
MIT