0% прочитано

Laravel Localization: using logical route names with localized URLs

Localized URLs do not have to make the entire application depend on locale-specific route names. This article explores an approach where Laravel keeps localized URLs while frontend and application code use stable logical route names.

12 серпня 2026 р. 7 хв читанняLaravel

What is this about?

When a Laravel application gets a second language, the first routing task looks relatively simple: add a locale to the URL and register localized routes.
text
/blog/en/blog

The harder problem appears later, when localization starts leaking into the rest of the application.

Frontend components, Blade templates, services, or SSR code may suddenly need to know which physical route name belongs to the current locale.

Instead of simply using:

php
route('posts.index')

application code can gradually become dependent on names such as:

text
uk.posts.indexen.posts.index

At first, this looks like nothing more than a naming convention. In practice, it is an implementation detail leaking outside the localization layer.

That problem led me to separate logical route names from physical localized routes.

Physical routes and logical routes solve different problems

A physical route is what Laravel actually registers in its router.

Its structure may depend on the current locale, the configured prefix mode, the default locale, translated URL segments, or the internal implementation of a localization package.

A logical route name is what application code knows about.

For example:

text
posts.indexposts.showcategories.index

The application layer should not need to know which physical route represents the Ukrainian or English version.

Application code simply asks for:

php
route('posts.index')

and the localization layer determines which physical route and URL belong to the current locale.

Why this distinction matters

Once application code starts depending on locale-specific route names, coupling grows quickly.

Changing the localization strategy may then require changes across many parts of the application.

  • Blade templates

  • Vue components

  • JavaScript services

  • redirects

  • tests

  • SSR

  • Ziggy configuration

This becomes especially noticeable in larger applications where route helpers are used in dozens or hundreds of places.

A logical route projection gives the application one stable contract:

text
posts.index

regardless of how localization is represented internally by Laravel's router.

Localized URLs without locale-specific route names

This distinction is important.

The logical route name:

text
posts.index

may generate:

text
/blog

for the default locale and:

text
/en/blog

for English.

The same principle still applies if the URL segments themselves are translated.

Application code knows what it wants to open.

The localization layer decides which URL represents that route for the current locale.

Blade is only one consumer

In a traditional Laravel application, solving server-side URL generation may seem sufficient.

A modern Laravel stack, however, usually has several consumers of routing configuration.

text
Laravel Routerlogical route projection ┌─────────┬───────────────┐ Blade     Ziggy      Inertia SSR           Vue

All of them need to see the same routing contract.

If Blade receives logical route names while Ziggy still receives physical localized route names, the abstraction is incomplete.

Ziggy makes the routing boundary visible

Ziggy exposes Laravel routes to JavaScript.

A Vue component can conveniently use:

javascript
route('posts.index')

But that also means the frontend becomes another consumer of Laravel's routing configuration.

If a localization package creates logical route names only for Blade, the browser may receive a completely different set of route names.

Logical projection therefore needs to work not only during HTML generation but also for programmatic Ziggy configuration.

Otherwise, the frontend still has to know about the internal structure of localized routes.

SSR introduces another routing context

With Inertia SSR, the situation becomes even more interesting.

The Vue application now executes in two environments:

  • the browser

  • the Node SSR process

In the browser, Ziggy may receive its configuration through Inertia props.

SSR must receive the same logical routing contract.

For example:

javascript
.use(ZiggyVue, initialProps.ziggy)

If initialProps.ziggy contains a different routing projection, a component may work perfectly in the browser and still fail during SSR.

This produces a particularly confusing class of bugs: the build succeeds, the route exists in Laravel, and the hydrated page works, but SSR still cannot resolve the route.

That is why routing configuration is better treated as a contract rather than just a collection of helper functions.

Dependency injection for the Ziggy route helper

If the Vue application already installs ZiggyVue, components can use the route helper provided by the plugin itself.

For example:

javascript
import { inject } from 'vue'; const route = inject('route');

The component then uses the configuration supplied to the application-level Ziggy plugin.

This matters especially for SSR because the component does not create an independent routing context.

It uses the configuration that belongs to the current application request.

Programmatic Ziggy usage belongs to the same contract

There is another less obvious consumer.

Ziggy configuration can be requested directly from the Laravel container rather than through Blade or Vue.

For example:

php
$ziggy = app(\Tighten\Ziggy\Ziggy::class);

If the localization integration is complete, this consumer should also receive the logical route projection.

Otherwise, one part of the application sees:

text
posts.index

while another sees internal locale-specific route names.

At that point, the application effectively has two different routing contracts.

Container binding as an integration boundary

One way to solve this is to integrate the logical Ziggy configuration at the container-binding level.

The application can continue depending on Ziggy's standard contract:

php
Ziggy::class

while the container provides a configuration adapted to the current localization context.

This matters for the host application because it does not need to know about a concrete localization implementation.

The application asks for Ziggy configuration.

The localization package is responsible for ensuring that this configuration reflects the current locale and routing rules.

The contract I want from a localization layer

After working through this integration, I ended up with a simple invariant:

Localization details should stay inside the localization boundary.

Application code should be able to use:

text
posts.index

from Blade, Vue, SSR, and programmatic code.

The localization layer should own the localization-specific details.

  • the current locale

  • the default locale

  • prefix mode

  • the physical localized route

  • the final localized URL

This creates a much more stable routing API and reduces the application's dependency on a particular localization implementation.

The abstraction must work in every routing context

Logical route names alone do not solve the problem.

If they work in only one environment — Blade, for example — the abstraction is incomplete.

The same routing contract should apply to every major consumer.

The boundaries between these environments are exactly where routing problems tend to appear.

Conclusion

Localized routing can start as a simple problem about URL prefixes.

As an application grows, it quickly becomes a problem about boundaries and contracts.

The model I prefer is:

text
Application → logical routeLocalization → physical routeRouter → final URL

The URL can remain fully localized without forcing the rest of the application to understand how that localization is implemented.

And this distinction between logical and physical routes leads directly to the next problem I encountered: making Ziggy, Vue, and Inertia SSR use the exact same routing contract.