Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

I18N Routing Bundle Laravel Package

h2invent/i18n-routing-bundle

A Symfony bundle for internationalized routing, letting you define locale-aware routes with translated path segments and automatic locale prefixes. Simplifies building multilingual URLs and language switching in Symfony applications.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and register. This is a Symfony bundle, not a Laravel package — composer-install it and register it manually, because there is almost certainly no Flex recipe for a 0-star fork:

composer require h2invent/i18n-routing-bundle
// config/bundles.php
H2invent\I18nRoutingBundle\JMSI18nRoutingBundle::class => ['all' => true],

Read the vendored source before writing config. With no dependents and no docs site, the authoritative reference is the package itself:

  • vendor/h2invent/i18n-routing-bundle/README.md — upstream JMS README, usually intact in the fork
  • DependencyInjection/Configuration.php — the actual config keys and defaults for this fork (upstream options occasionally get renamed or dropped)
  • Router/I18nLoader.php — the class that rewrites your route collection; every behaviour question ("why is this route not prefixed?") is answered here
  • Router/I18nRouter.php — how generate() resolves locale-suffixed route names

Minimal config. Two keys get you 90% of the value:

# config/packages/jms_i18n_routing.yaml
jms_i18n_routing:
    default_locale: en
    locales: [en, de, fr]
    strategy: prefix          # or: prefix_except_default

Then opt a controller in:

#[Route('/contact', name: 'contact', defaults: ['i18n' => true])]
public function contact(): Response { /* ... */ }

First use case. Localize a handful of public marketing routes, verify with bin/console debug:router, and confirm you get contact.en → /en/contact, contact.de → /de/contact. Do not start by localizing every route in the app — start with 3–5 and expand once URL generation and the language switcher work end-to-end.

First integration point that will bite you. Set the request locale to match the matched route. The bundle owns URLs, not Request::getLocale(). Add a kernel request listener:

$request->setLocale($request->attributes->get('_locale') ?? $request->getPreferredLanguage($supported));

Implementation Patterns

Strategy choice drives everything downstream.

  • prefix — /en/contact, /de/contact, and the default locale is also prefixed. Cleanest for SEO and hreflang, no ambiguity about which URL is canonical.
  • prefix_except_default — /contact for en, /de/contact for others. Prettier URLs, but now you have two URLs per page in the default locale (/contact and /en/contact) unless you 301 one to the other. Pick this deliberately, not by default.

URL generation is locale-aware. Never hand-build "/{$locale}/contact". Use the router:

$this->generateUrl('contact', ['_locale' => 'de']);   // /de/contact

The I18nRouter decorator maps contact + _locale: de onto contact.de. If a route doesn't exist for that locale (see i18n_locales), you get a RouteNotFoundException at generation time — which is exactly the error feedback you want, but it means a language switcher must be built from the route's actual available locales, not the global locales list.

Language switcher in Twig. Reuse the current route and params, override only _locale:

{% set route = app.request.attributes.get('_route') %}
{% set params = app.request.attributes.get('_route_params') %}
{% for locale in supported_locales %}
    {% if route %}
        <a href="{{ path(route, params|merge({_locale: locale})) }}" hreflang="{{ locale }}">{{ locale }}</a>
    {% endif %}
{% endfor %}

Wrap it in {% if route %} — error pages and forwarded controllers have no _route attribute.

Restrict locales per route. Not every page is translated. defaults: ['i18n_locales' => ['en', 'de']] on a controller means only those variants are generated; the switcher (above) will throw for fr, so filter the switcher against the route's locales rather than the global list.

Redirect routes for legacy/shared URLs. Set i18n_redirect on a route to emit an unprefixed URL that redirects into the localized one based on the request locale / Accept-Language. This is how you keep an existing /contact link or a partner's hardcoded URL working after you introduce prefixes. Prefer 301s for permanent moves — configure it once, because changing redirect semantics later means editing every route.

Exclude infrastructure routes explicitly. Mark _profiler, health checks, webhooks, API endpoints consumed by machines, and anything the security firewall matches as defaults: ['i18n' => false]. Localizing a webhook path silently breaks the integration that calls it.

Localize security paths carefully. If form_login.check_path is login_check and that route is i18n'd, the firewall will post to an unprefixed URL. Either exclude auth routes from i18n, or set check_path to the locale-suffixed route name and add {_locale} requirements. Same for logout and firewall pattern regexes — a pattern: ^/admin won't match /de/admin.

SEO plumbing is your job, not the bundle's. Emit <link rel="canonical"> and hreflang alternates per locale from the same generation logic as the switcher. The bundle gives you URLs; it does not give you correct metadata.

Testing. Use RouterInterface::generate() with explicit _locale in functional tests, and assert the matched _locale attribute, not just the status code — a 200 on the wrong locale is the most common silent regression.

Gotchas and Tips

Framework mismatch is the first gotcha. If you're actually on Laravel, stop: this bundle is Symfony-only (it hooks the Symfony routing loader/compiler pass). The Laravel equivalent is mcamara/laravel-localization. Do not attempt to bolt this onto a Laravel app.

Zero stars, zero dependents, last release 2024-05-24. Treat adoption as a fork-and-own decision. Pin the exact version in composer.json, and before committing, check: does it support your Symfony major? Are the tests green in the vendored repo? Is the upstream jms/i18n-routing-bundle still maintained, and would it be a safer base? Have a plan for taking over the fork — you will likely need to patch it when Symfony bumps.

Route names get suffixed. The route named contact in your attribute becomes contact.en, contact.de, and so on in the collection. debug:router will show those names; grepping for contact.de in your codebase finds nothing because nobody writes that literal. When a URL 404s, run bin/console router:match /de/contact --method=GET and check which route name actually matched.

Missing _locale requirement = unpredictable matching. Add requirements: ['_locale' => 'en|de|fr'] (or rely on the bundle applying it from config — verify in Configuration.php). Without a constraint, a path segment like /products/contact can be swallowed as a locale and you'll get a confusing 404 with a valid-looking URL.

Cache is compiled, so locale config changes don't hot-reload. After editing locales, run bin/console cache:clear. If routes look stale in dev, inspect var/cache/dev/url_matching_routes.php — you can literally read the generated localized routes there.

Redirect loops with prefix_except_default. /contact → /en/contact → /contact happens when your canonicalization listener and the bundle's default-locale handling disagree. Keep exactly one component responsible for the default-locale redirect and make it a 301.

Don't cache localized responses without Vary. If you put a CDN or HTTP cache in front, Accept-Language-based redirect routes must either be uncached or send Vary: Accept-Language; otherwise a German user gets the English redirect from cache.

_locale in the middle of a path. /{_locale}/products/{id} works, but the locale must satisfy the route requirement and must not collide with a real first path segment. If you have a top-level route at /de (a category slug, say), it will be matched as the locale. Namespace or constrain.

Extension points, in order of usefulness. Decorate I18nRouter to centralize fallback logic; add a kernel.request listener to sync Request::setLocale() and your translator; and if you need custom URL shapes, the route-generation strategy interface is the intended seam — but read I18nLoader end-to-end before subclassing, since the fork's internals are undocumented beyond the code.

Debugging order of operations. (1) debug:router | grep contact — are the localized routes generated at all? (2) router:match — which route matched? (3) $request->attributes->get('_locale') in a listener — is the locale actually reaching your app? (4) only then look at path generation. Skipping to step 4 wastes the most time.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource