pmochine/laravel-tongue
Laravel Tongue is a lightweight localization package for Laravel that simplifies multilingual routes and language switching. Define translated URLs, detect and persist the visitor's locale, and serve localized content with minimal configuration.
Laravel-Tongue (this package) gives you translated URLs, locale detection, and locale-aware redirects without pulling in a full i18n framework. The 6.1.0 release is an additive, non-breaking release — nothing to migrate, but two features are worth adopting early because they change how you should structure routes.
composer require pmochine/laravel-tongue
php artisan vendor:publish --tag=tongue
config/tongue.php first — this is the source of truth for your locals, default locale, and detection strategy. Almost every "why is my locale wrong?" question is answered here.detects-tongue middleware in the web group (or bootstrap/app.php on Laravel 11+). It sets the active locale before the route is dispatched./en/about and /fr/a-propos. Define the route once with dialect()->localizedRoutes() so each locale gets its own URI, then hit it with a browser in both locales and confirm the detected locale is correct.If you plan to deploy behind Octane or run php artisan route:cache (you almost certainly do), start with localizedRoutes() from day one rather than adding it later — retrofitting it means rewiring route definitions and every route()/url() call site that used the locale-aware approach.
Localized route registration (the new default). Use dialect()->localizedRoutes() to register the translated route once per locale, each with its own localized path. This produces concrete routes for every locale, which is what makes route:cache and Octane safe: route registration no longer depends on the current request's locale. Treat this as the standard way to define translated routes in new code.
Canonicalizing URLs with speaks-tongue. Register speaks-tongue alongside detects-tongue. When a request arrives at a route belonging to a different locale than the active one, the middleware redirects to the localized path (fr.example.com/welcome → fr.example.com/bienvenue). Because it uses 307 for form requests, method and payload survive the round trip — useful for POST-heavy apps where a translated slug shouldn't cause a silent loss of form data.
Middleware ordering is part of your route design. Both detects-tongue and speaks-tongue now run before SubstituteBindings. That is deliberate: route model binding resolves translated slugs in the request's locale, and the redirect happens before a binding can 404. If you have custom middleware that touches locale (auth scoping, tenant resolution), slot it around this pair rather than before it.
Redirects with explicit statuses. dialect()->redirect() now takes a status as its second argument. Use 302 for GET-only navigation, 307 when the target is a form endpoint or the caller may be a non-browser client, and 308 if you want the method-and-body preservation to be cached permanently. Don't rely on the default when the distinction matters.
Day-to-day workflow. Keep translated slugs in your language files, define each route through localizedRoutes(), and let the generated names be locale-specific. Build links with the normal route() helper — since routes are materialized per locale, php artisan route:list is a reliable way to see exactly what URLs exist. Add hreflang/canonical tags per locale to avoid duplicate-content penalties, because you genuinely are serving multiple URIs for the same content.
Integration tip. When writing tests, assert both the redirect status and the target path. Feature tests that use assertRedirect() will not catch a 307 that should have been a 302, and HTTP clients that follow redirects may silently downgrade POST to GET.
detects-tongue ends up after SubstituteBindings (easy to do when registering it ad-hoc or in a custom kernel), translated-slug binding resolves against the wrong locale and you get 404s that look like missing routes. In Laravel 11+, append these aliases to the priority list in bootstrap/app.php explicitly. In older apps, add them to $middlewarePriority.dialect()->getLocale() at route-definition time will break route:cache and leak state under Octane. The whole point of localizedRoutes() is that definitions are locale-independent at boot.config/tongue.php or a translated path, run php artisan route:clear and php artisan config:clear — stale cached routes still serve the old locale paths and will look like a middleware bug.fr.example.com/en/welcome). Set the precedence deliberately in config rather than relying on which middleware happens to run first.dialect()->getLocale() at the start of a request-scoped middleware and compare against app()->getLocale(). Divergence tells you whether detection failed or something later reset it.Accept-Language fallback or a user preference column). The config file is the supported seam for locale lists and defaults; avoid editing package internals, since 6.1.0's ordering guarantees are enforced from within the middleware themselves.route:list --path= filters when auditing, and keep locale codes in generated route names so route('fr.about')-style calls stay readable.How can I help you explore Laravel packages today?