gjuric/i18n-routing-bundle
A Symfony bundle for internationalized routing that lets you define locale-aware routes and generate URLs per language. It simplifies building multilingual applications with translated route paths and locale-prefixed URLs, keeping routing configuration clean and consistent across languages.
Read this first: gjuric/i18n-routing-bundle is a Symfony bundle, not a Laravel package. There is no service provider, facade, or config/i18n-routing.php. On Packagist the upstream ships as jms/i18n-routing-bundle; this repo is a fork of schmittjoh/JMSI18nRoutingBundle, archived with 1 star and 0 dependents. You cannot composer require it into a Laravel app and expect anything to happen. Treat it strictly as a design reference for localized routing, then implement the pattern natively in Laravel.
Where to look in the source (worth ~30 minutes if you're porting the pattern): DependencyInjection/Configuration.php for the config surface, Router/I18nRouter.php for the router decorator that resolves route names per locale, and the compiler pass that clones routes per locale. That's the entire mental model — each opted-in route is expanded once per locale, the route name gets a locale suffix, and the path gets a prefix.
Minimal native Laravel equivalent — the same capability in ~20 minutes of code:
// routes/web.php
$locales = ['en', 'de', 'fr'];
$default = 'en';
Route::group(['prefix' => '{locale?}', 'middleware' => 'setlocale'], function () use ($locales) {
Route::get('/', [HomeController::class, 'index'])->name('home');
Route::get('products', [ProductController::class, 'index'])->name('products');
})->whereIn('locale', $locales);
// app/Http/Middleware/SetLocale.php
public function handle($request, Closure $next) {
$locale = $request->route('locale') ?? config('app.fallback_locale');
if (!in_array($locale, config('app.locales'))) abort(404);
app()->setLocale($locale);
return $next($request);
}
First use case: a marketing site where /de/produkte and /en/products resolve to the same controller but emit different URLs, and /products (no prefix) redirects to the default locale. That's your canonical acceptance test — if you can pass it on a fresh Laravel app in an afternoon, you have no reason to depend on an archived Symfony bundle.
Keep canonical route names locale-free. Whatever you build, register products once and let the locale live in the URL and in App::getLocale(). Everything that calls route('products') should keep working; only the generated URL changes. The Symfony bundle enforces this via a route_name_pattern (e.g. {name}.{locale}) applied by the compiler pass — mimic the discipline, not the mechanism.
Generating links for a specific locale is the pattern you'll write most (language switcher, sitemap, hreflang). Always pass _locale explicitly rather than relying on request state:
// Native Laravel with a small route() helper extension, or:
route('products', ['locale' => 'de'])
For a language switcher, use the matched route and current params, swap locale, and preserve everything else — users must land on the same product, not the homepage:
$route = $request->route()->getName();
$params = $request->route()->parameters();
$params['locale'] = 'fr';
url = route($route, $params);
Translated path segments (/de/produkte vs /en/products) are the expensive part. The Symfony bundle pulls localized paths from translation catalogs keyed by the route name. In Laravel, the pragmatic options are:
routes/web.de.php registering /produkte → ProductController@index with the same name). Clean, cacheable, explicit.Start with option 1. Only reach for option 2 when translators need to change slugs without a deploy.
Pick a prefix strategy deliberately. prefix_except_default (default locale served at /, others at /de/...) gives clean short URLs but two URLs per page — you must redirect one canonically. prefix_all (/en/, /de/) is simpler to reason about and cheaper for SEO, at the cost of a longer default-locale URL. Decide before you ship; switching later invalidates every indexed URL.
Hreflang, sitemap, and canonical belong in the same PR as the routing. Every localized page needs <link rel="alternate" hreflang="..."> pointing at the same logical content in each locale, plus a canonical. Generate these from the same route-name-plus-_locale invocation you use for the switcher, so they can never drift.
Hard stop on adoption. The repo is archived, has 1 star and 0 dependents, and targets a Symfony/PHP matrix from a prior era. No security patches, no compatibility guarantees, no escalation path. If someone proposes pulling it in, the answer is "replicate, don't require." The design worth extracting is ~1–2 days of Laravel work; the dependency is permanent ownership.
Two URLs per page is a silent SEO leak. With prefix_except_default, /products and /en/products both exist. Without a 301 from the non-canonical one, you ship duplicate content. Pick the default-locale form and redirect the other — one middleware, one test.
Middleware ordering is not optional. Locale resolution must run before any route-aware middleware (auth redirects that generate URLs, breadcrumb builders, hreflang emitters, signed-URL checks). If a 302 to login fires before the locale is known, the redirect lands on the wrong locale and the flash message is in the wrong language. Register SetLocale immediately after StartSession in web.
Route caching interacts badly with translated slugs. Native {locale} groups cache fine. Slug-translation middleware that resolves segments before Route::matches bypasses the route cache and re-hits the DB or translator on every request. Either warm a slug map into the config cache at deploy, or don't translate segments in v1.
Fallback behavior is where 404s hide. Decide explicitly: does /de/unknown-page 404, or fall back to the English route? Does a missing translation for a slug fall back to the default-language slug, or break? The Symfony bundle's per-locale cloning means a route present in en but missing from de produces a 404 in German only — hard to catch. In Laravel, add a test that iterates config('app.locales') and asserts every named route generates in every locale.
Locale and cookie can disagree. If you support a lang cookie or Accept-Language detection and URL prefixes, the URL must win. Otherwise a user on /fr/... with a stale lang=en cookie gets English content on a French URL — disastrous for hreflang and for share links. Write the middleware so the {locale} param always overrides session/cookie/header detection.
Non-request URL generation is a trap. Queued jobs, emails, console commands, and API responses have no request locale. Anything that generates a URL outside HTTP must pass _locale explicitly or run under an explicit App::setLocale(). Grep for route( in jobs and mailables before you ship — a customer in Germany receiving an English password-reset link is a support ticket, not a bug report.
Test the redirect chain. / → /en/ → locale cookie set → /en/products should be one hop, not three. Redirects that also mutate cookies are the classic source of infinite loops behind load balancers where cookies don't stick. Integration-test the full chain, not just individual routes.
Extension point worth stealing from the Symfony side: the router decorator pattern. If you later need host-based locale detection (de.example.com) alongside path prefixes, wrap UrlGenerator in a decorator rather than sprinkling app()->getLocale() through controllers. One resolution point, one place to change.
How can I help you explore Laravel packages today?