0% прочитано

Laravel Localization: логічні назви маршрутів для локалізованих URL

Локалізовані URL не обов’язково мають змушувати весь застосунок працювати з locale-specific назвами маршрутів. Розбираю підхід, у якому Laravel зберігає локалізовані URL, а frontend і application code працюють зі стабільними logical route names.

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

Про що йде мова?

Коли в Laravel-проєкті з’являється кілька мов, перша задача здається доволі простою: додати locale до URL і зареєструвати локалізовані маршрути.
text
/blog/en/blog

Але складність починається трохи пізніше - коли локалізація маршрутів починає проникати в решту застосунку.

Frontend, Blade templates, services або SSR-код раптом мають знати, яка саме фізична назва маршруту відповідає поточній мові.

Замість звичайного:

php
route('posts.index')

application code може поступово почати працювати з чимось на кшталт:

text
uk.posts.indexen.posts.index

На перший погляд це лише naming convention. Насправді це вже витік implementation details за межі localization layer.

У моєму випадку саме ця проблема привела до ідеї розділити logical route name і physical localized route.

Physical route і logical route - це різні речі

Фізичний маршрут - це те, що Laravel реально реєструє у своєму router.

Його структура може залежати від current locale, prefix mode, default locale, translated URL segments та внутрішньої реалізації localization package.

А logical route name - це ім’я, яке знає application code.

Наприклад:

text
posts.indexposts.showcategories.index

Для application layer не повинно мати значення, який фізичний маршрут зараз використовується для української чи англійської версії.

Application code просто просить:

php
route('posts.index')

А localization layer уже визначає правильний фізичний маршрут і URL для поточного locale.

Чому це розділення важливе

Коли application code починає знати locale-specific route names, зв’язність між шарами різко зростає.

Зміна localization strategy тоді може означати зміни одразу в багатьох частинах застосунку.

  • Blade templates

  • Vue components

  • JavaScript services

  • redirects

  • tests

  • SSR

  • Ziggy configuration

Це особливо помітно у великих застосунках, де route helper використовується в десятках або сотнях місць.

Logical route projection дозволяє залишити один стабільний контракт:

text
posts.index

незалежно від того, як саме локалізація представлена всередині Laravel router.

Локалізований URL без locale-specific route name

Це важливе розділення. Logical route name:

text
posts.index

може генерувати:

text
/blog

для default locale та:

text
/en/blog

для англійської версії.

Якщо локалізуються й самі URL segments, принцип залишається тим самим.

Application code знає що він хоче відкрити.

Localization layer вирішує який URL відповідає цьому маршруту в поточному locale.

Blade - лише один із consumers

У традиційному Laravel application може здаватися, що достатньо вирішити server-side URL generation.

Але сучасний Laravel stack часто має одразу кілька consumers routing configuration.

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

Важливо, щоб усі вони бачили один і той самий routing contract.

Якщо Blade отримує logical routes, а Ziggy - фізичні localized route names, abstraction фактично перестає бути повною.

Ziggy робить routing boundary особливо помітною

Ziggy переносить Laravel routes у JavaScript.

Звичайний Vue component може використовувати:

javascript
route('posts.index')

Це дуже зручно, але означає, що frontend стає ще одним consumer Laravel routing configuration.

Якщо localization package проєктує logical names тільки для Blade, браузер може отримати зовсім інший набір route names.

Logical projection тому має працювати не лише під час HTML generation, а й для програмної конфігурації Ziggy.

Інакше frontend усе одно буде змушений знати про фізичну структуру localized routes.

SSR додає ще один routing context

З Inertia SSR ситуація стає ще цікавішою.

Vue application тепер виконується у двох середовищах:

  • browser

  • Node SSR process

У браузері Ziggy може отримувати configuration через Inertia props.

SSR повинен отримати той самий logical routing contract.

Наприклад:

javascript
.use(ZiggyVue, initialProps.ziggy)

Якщо initialProps.ziggy містить інший routing projection, component може нормально працювати у браузері й водночас падати під час SSR.

Це один із тих типів помилок, де build проходить, route існує у Laravel, сторінка після hydration працює, але SSR усе одно не може знайти потрібний route.

Саме тому routing configuration варто розглядати як контракт, а не просто як набір helper functions.

Dependency injection для Ziggy route helper

Якщо Vue application уже встановлює ZiggyVue, components можуть використовувати route helper, який надає сам plugin.

Наприклад:

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

Тоді component працює з configuration, яку application передала Ziggy plugin.

Для SSR це особливо важливо: component не створює власний незалежний routing context.

Він використовує configuration, яка належить поточному application request.

Programmatic Ziggy usage теж належить до контракту

Є ще один менш очевидний consumer.

Ziggy configuration може використовуватися не через Blade directive і не через Vue plugin, а безпосередньо через Laravel container.

Наприклад:

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

Якщо localization integration повна, такий consumer теж має отримувати logical route projection.

Інакше одна частина application бачить:

text
posts.index

а інша - internal locale-specific route names.

У результаті всередині одного application фактично з’являються два різні routing contracts.

Container binding як integration boundary

Один із варіантів вирішення - інтегрувати logical Ziggy configuration на рівні container binding.

Тоді application продовжує залежати від стандартного Ziggy contract:

php
Ziggy::class

але container повертає configuration, адаптовану до поточного localization context.

Для host application це важливо: йому не потрібно знати concrete localization implementation.

Application просить Ziggy configuration.

Localization package відповідає за те, щоб ця configuration відповідала current locale та routing rules.

Який контракт я хочу від localization layer

Після роботи над цією інтеграцією для мене сформувався досить простий invariant:

Деталі локалізації повинні залишатися в межах локалізації.

Application code повинен мати можливість використовувати:

text
posts.index

у Blade, Vue, SSR та programmatic code.

А localization layer уже повинен знати всі деталі локалізації.

  • current locale

  • default locale

  • prefix mode

  • physical localized route

  • final localized URL

Це робить routing API значно стабільнішим і зменшує залежність application code від конкретної реалізації локалізації.

Абстракція має працювати в усіх routing contexts

Logical route names самі по собі ще не вирішують проблему.

Якщо вони працюють лише в одному місці, наприклад, у Blade - це неповна abstraction.

Один routing contract має діяти для всіх основних consumers.

Саме на межах між цими середовищами найчастіше й проявляються routing problems.

Висновок

Localized routing легко починається як задача про URL prefixes.

Але в міру розвитку application вона швидко перетворюється на задачу про boundaries та contracts.

Мені значно ближча модель:

text
Application → logical routeLocalization → physical routeRouter → final URL

Тоді URL може залишатися повністю локалізованим, не змушуючи весь application code знати, як саме ця локалізація реалізована.

І саме різниця між logical та physical routes стала основою наступної проблеми, з якою я зіткнулася: як змусити Ziggy, Vue та Inertia SSR використовувати один і той самий routing contract.