What is this about?
/blog/en/blogThe 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:
route('posts.index')application code can gradually become dependent on names such as:
uk.posts.indexen.posts.indexAt 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:
posts.indexposts.showcategories.indexThe application layer should not need to know which physical route represents the Ukrainian or English version.
Application code simply asks for:
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:
posts.indexregardless 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:
posts.indexmay generate:
/blogfor the default locale and:
/en/blogfor 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.
Laravel Router ↓logical route projection ↓ ┌─────────┬───────────────┐ Blade Ziggy Inertia SSR ↓ VueAll 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:
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:
.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:
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:
$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:
posts.indexwhile 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:
Ziggy::classwhile 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:
posts.indexfrom 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.
Blade
Ziggy
Vue
Inertia SSR
programmatic configuration
tests
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:
Application → logical routeLocalization → physical routeRouter → final URLThe 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.



