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

Laravel Translatable Routes Laravel Package

codedor/laravel-translatable-routes

codedor/laravel-translatable-routes adds multilingual, SEO-friendly URLs to Laravel. Define translated route segments per locale, generate locale-aware links, and switch languages without losing the current page. Ideal for localized apps that need clean, human-readable URLs in every language.

View on GitHub
Deep Wiki
Context7

Getting Started

wotzebra/laravel-translatable-routes lets you declare a route once and have its URI segments (and therefore its generated URLs) differ per locale — /en/about vs /fr/a-propos — while keeping a single stable route name so route('about') keeps working everywhere.

Minimum steps:

  1. composer require wotzebra/laravel-translatable-routes
  2. Publish the config (and any shipped lang stub) and inspect it before touching routes:
    php artisan vendor:publish --tag="translatable-routes-config"
    
  3. Confirm your supported locales are defined in config/app.php / the package config, and that your SetLocale middleware (or App::setLocale()) runs before routing resolves the URL — that is the single most common first-run failure.
  4. Translate your URI segments in the package's lang files (typically lang/{locale}/routes.php), keyed by a slug identifier rather than by the English text.
  5. Wrap your first route group and check output per locale:
    Route::translatable(function () {
        Route::get('/', [HomeController::class, 'index'])->name('home');
        Route::get('/about', [PageController::class, 'about'])->name('about');
    });
    
    php artisan route:list            # should show one entry per locale, same name
    php artisan tinker --execute="app()->setLocale('fr'); echo route('about');"
    

First use case to get working end-to-end: home + about + contact pages in two locales, plus a working language switcher that redirects to the same page in the other locale. If the switcher works, your middleware ordering, route naming and URL generation are all correct — everything else in this package is a variation on that.

Where to look first: the README's route-group example, then the config file (locale list + fallback behaviour), then route:list. Do not start by reading the source — the value of this package is the registration API, and route:list tells you faster than code whether it did what you expected.

Implementation Patterns

One route name, many URLs. The whole point is that route('about') is locale-agnostic. Resist creating about.en / about.fr names — you lose the abstraction and every route() call becomes a match. Keep ->name('about') singular and let the package resolve the segment.

Route group per locale with a prefix, or a single translated group. Two viable shapes:

// Shape A: locale prefix in the URL, one group
Route::group(['prefix' => '{locale}'], function () {
    Route::translatable(function () {
        Route::get('/about', ...)->name('about');
    });
});

// Shape B: each locale is its own host/prefix, registered in a loop
foreach (config('app.supported_locales') as $locale) {
    Route::group(['prefix' => $locale, 'middleware' => "locale:$locale"], function () {
        Route::translatable(fn () => require base_path('routes/web.php'));
    });
}

Shape B is easier to reason about for SEO (every locale is crawlable, no locale negotiation), Shape A is less duplication. Pick one early — mixing them mid-project is painful.

Language switcher. The canonical implementation is: take the current route name and parameters, then regenerate in the target locale.

public function switch(string $locale)
{
    abort_unless(in_array($locale, config('app.supported_locales')), 404);

    $route = request()->route();
    $params = $route->parameters();          // keep model bindings intact
    session(['locale' => $locale]);

    return redirect()->to(
        route($route->getName(), $params + ['locale' => $locale], absolute: true)
    );
}

Passing the bound model (not the slug string) is important: a translated slug for a Post will not resolve back to the model if you pass the raw {post} string into a differently-localized route.

Localized slugs with route model binding. If you translate the slug of a resource route, resolve models by a translated column rather than id:

Route::get('/articles/{post:slug_'.app()->getLocale().'}', ...);

This couples routing to schema. The cleaner pattern is a slug table keyed by (model, locale) and a resolveRouteBinding() override that queries it — the package handles the inbound match, your model handles the lookup.

Locale persistence middleware. Order matters: SetLocale (session → App::setLocale) must run before SubstituteBindings, and the package's own resolution must see the final locale. If URLs are locale-prefixed, derive locale from the prefix and ignore session for that request — otherwise a French session on an English URL produces inconsistent route() output inside the same response.

hreflang and sitemaps. The package makes this trivial and it is a strong reason to adopt it. In your layout:

@foreach (config('app.supported_locales') as $locale)
    <link rel="alternate" hreflang="{{ $locale }}"
          href="{{ route($routeName, $params + ['locale' => $locale]) }}">
@endforeach

Same loop builds sitemap entries — no hand-maintained URL lists that drift when someone edits a translation file.

Emails, jobs and queued notifications. Never rely on ambient locale. Wrap generation explicitly:

$url = app()->setLocale($user->locale) && false ?: route('about');
// or, cleaner:
$url = trans_url('about', [], $user->locale);

Whatever the package's explicit-locale helper is (check the README — it exposes one for exactly this), use it. Queue workers have no request locale; a URL generated in a job will silently be the default locale.

Testing. Assert per-locale URLs directly and assert cross-locale reachability:

it('serves translated urls', function () {
    $this->get('/fr/a-propos')->assertOk();
    expect(route('about', [], false))->toBe('/fr/a-propos');
});

Add one test that every named route resolves in every configured locale — it catches missing translation keys at CI time instead of in production.

Gotchas and Tips

PHP 8.5 / Laravel 13 (v2.1.0). This release is compatibility-only: no API changes, no deprecations. Two practical implications: (a) you can bump the constraint without touching code, and (b) if you are not yet on Laravel 13 this buys you nothing — pin ^2.0 and move on. The other two changes are Dependabot bumps to CI actions, irrelevant at runtime. Nothing here changes your implementation, but check whether your other route-touching packages (localization, sitemap, SEO) also support Laravel 13 before upgrading the framework itself.

route:cache will bite you. Translated routes are still routes. Any change to a lang file changes the compiled route table — deploy must run php artisan route:clear (or a fresh route:cache) or your new French slug simply won't exist. Put it in your deploy script; this is the number-one "it works locally" bug with this package.

Duplicate route names are the classic failure. If you register ->name('about') inside a per-locale loop and the package also registers one per locale, you get N entries with the same name. route() returns the last-registered one, so URLs silently come out in the wrong locale. Sanity check: php artisan route:list --name=about should show exactly one row per locale, no more.

route() output is locale-of-the-moment, not locale-of-the-URL. Inside a request to /fr/... they agree, only because your middleware set the locale. In a Blade partial rendered from a cache, an API response, or a queued job they will not. Treat ambient locale as a rendering concern, never as a data concern.

Missing translation keys fail quietly. Depending on config, an untranslated segment falls back to the base locale or to the raw key. Either is worse than a loud failure in dev — set the fallback behaviour deliberately and add the "all routes resolve in all locales" test above.

Signed URLs and localized paths don't mix well. A temporary signed URL built in one locale will 403 if the user's session/locale causes the signature to be validated against a different path. Generate and validate signed routes under an explicit locale, or keep signed routes outside the translatable group.

Non-ASCII segments. Translating to /fr/à-propos produces percent-encoded URLs that look wrong in logs and can mismatch in tests. Prefer ASCII-only translated slugs, and assert with rawurldecode() when it's unavoidable.

Config and lang caching. php artisan config:cache freezes the locale list; lang files are read at runtime. If you add a locale in .env but don't re-cache, the route disappears. Same deploy-script discipline as route:cache.

Extension point worth knowing. If the package's segment resolution doesn't cover a case (dynamic translated slugs, translated query strings), it is almost always cleaner to wrap route() in a small helper of your own than to subclass the router. Keep the customization in one helper so a future package upgrade stays a one-file diff.

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.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
robertmarney/lara-hierarchial-collections
emissaoja/emissaoja
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
c-point-labs/laravel-hostyourai
cgv/localized-resource
noith/settings
kda/laravel-package