Реалізація переключення мови на сайті
Переключатель мови — невеликий компонент, але з нюансами: потрібно зберігати поточний URL при смені мови, коректно обробляти локалізовані slug'и та не ламати SEO зайвими редиректами.
Базовий компонент
// components/LanguageSwitcher.tsx
import { useRouter, usePathname } from 'next/navigation'
const LOCALES = [
{ code: 'ru', label: 'Русский', flag: '🇷🇺' },
{ code: 'en', label: 'English', flag: '🇬🇧' },
{ code: 'de', label: 'Deutsch', flag: '🇩🇪' },
{ code: 'uk', label: 'Українська', flag: '🇺🇦' },
]
export function LanguageSwitcher({ currentLocale }: { currentLocale: string }) {
const router = useRouter()
const pathname = usePathname()
const switchLocale = (locale: string) => {
// Змінюємо префікс локалі в поточному шляху
const newPath = pathname.replace(/^\/(ru|en|de|uk)/, `/${locale}`)
router.push(newPath)
}
return (
<nav aria-label="Вибір мови">
<ul className="flex gap-2">
{LOCALES.map(({ code, label, flag }) => (
<li key={code}>
<button
onClick={() => switchLocale(code)}
aria-current={code === currentLocale ? 'true' : undefined}
className={code === currentLocale ? 'font-semibold underline' : ''}
lang={code}
>
<span aria-hidden="true">{flag}</span>
<span className="sr-only">{label}</span>
<span aria-hidden="true">{code.toUpperCase()}</span>
</button>
</li>
))}
</ul>
</nav>
)
}
Переключення зі збереженням локалізованого шляху
Якщо slug'и сторінок перекладені (/en/smart-watch vs /ru/umnye-chasy), проста заміна префіксу не працює. Потрібна таблиця відповідностей:
// hooks/useLocalizedPath.ts
interface RouteTranslations {
[locale: string]: string
}
// Зберігається в meta-даних сторінки або передається через props
export function useLocalizedPath(translations: RouteTranslations) {
return (targetLocale: string): string => {
return translations[targetLocale] ?? `/${targetLocale}/`
}
}
// У компоненті сторінки продукту
const routeTranslations = {
ru: '/ru/catalog/umnye-chasy',
en: '/en/catalog/smart-watch',
de: '/de/katalog/smartwatch',
}
<LanguageSwitcher
currentLocale="ru"
getLocalizedPath={useLocalizedPath(routeTranslations)}
/>
На стороні Laravel ці дані можна передати через Inertia props:
// ProductController
return Inertia::render('Product/Show', [
'product' => $product,
'localizedUrls' => [
'ru' => route('product', ['locale' => 'ru', 'slug' => $product->translate('ru')->slug]),
'en' => route('product', ['locale' => 'en', 'slug' => $product->translate('en')->slug]),
'de' => route('product', ['locale' => 'de', 'slug' => $product->translate('de')->slug]),
],
]);
Варіант dropdown
import * as Select from '@radix-ui/react-select'
export function LanguageDropdown({ current, onChange }: {
current: string
onChange: (locale: string) => void
}) {
const current_locale = LOCALES.find(l => l.code === current)
return (
<Select.Root value={current} onValueChange={onChange}>
<Select.Trigger aria-label="Мова сайту" className="flex items-center gap-2 px-3 py-1.5 border rounded">
<Select.Value>
{current_locale?.flag} {current_locale?.code.toUpperCase()}
</Select.Value>
<Select.Icon>▾</Select.Icon>
</Select.Trigger>
<Select.Portal>
<Select.Content className="bg-white border rounded shadow-md z-50">
<Select.Viewport>
{LOCALES.map(({ code, label, flag }) => (
<Select.Item
key={code}
value={code}
className="flex items-center gap-2 px-4 py-2 cursor-pointer hover:bg-muted"
>
<span aria-hidden="true">{flag}</span>
<Select.ItemText>{label}</Select.ItemText>
</Select.Item>
))}
</Select.Viewport>
</Select.Content>
</Select.Portal>
</Select.Root>
)
}
Збереження вибору користувача
// Пріоритет: cookie > localStorage > заголовок браузера
// Установка при переключенні
function setLocalePreference(locale: string) {
localStorage.setItem('preferred-locale', locale)
document.cookie = `locale=${locale}; path=/; max-age=${365 * 24 * 3600}; SameSite=Lax`
}
// Читання при ініціалізації
function getLocalePreference(): string | null {
return localStorage.getItem('preferred-locale')
?? document.cookie.match(/locale=([^;]+)/)?.[1]
?? null
}
Доступність
- Атрибут
langна кнопках з іноземними мовами (скринридер правильно вимовить назву) -
aria-current="true"на активній мові -
aria-labelна контейнері навігації - Кнопки, не ссилки
<a>— AJAX-переключення не вимагає переходу
Терміни
Компонент-переключатель без локалізованих slug'ів — з дні. З таблицею перекладів шляхів та передачею даних з контролера — 1 робочий день.







