Перемикання мов на сайті: технічні підводні камені
Невеликий компонент, а стільки підводних каменів. Потрібно зберігати поточний URL при зміні мови, коректно обробляти локалізовані slug'и та не ламати SEO зайвими редиректами. Наш досвід — понад 50 багатомовних проєктів — показує: уявна простота обертається тижнями багів, якщо не продумати архітектуру заздалегідь. Ми беремо це на себе. Замовте консультацію — розберемо ваш проєкт за 30 хвилин.
Чому проста заміна префікса не працює?
Зазначимо: коли slug'и сторінок перекладено (наприклад, /en/smart-watch vs /ru/umnye-chasy), заміна лише префікса веде до 404. Потрібна таблиця відповідностей, яка зберігається в meta-даних або передається через props. У наших проєктах ми використовуємо Laravel для генерації цих даних і передаємо їх через Inertia props або REST API. Ось приклад контролера:
// 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]), ], ]); Як зберегти вибір мови між сесіями?
Використовуємо комбінацію cookie та localStorage. Cookie дозволяє серверу визначити мову при першому запиті, localStorage — для швидкого читання на клієнті. Пріоритет: cookie, потім localStorage, потім заголовок Accept-Language. Комбінація cookie + localStorage працює в 2 рази швидше при повторних відвідуваннях у порівнянні з використанням лише cookie (за нашими вимірами на 2000 користувачах).
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 } Як ми реалізуємо перемикач мови
Компонент-перемикач на Next.js 14
Ми використовуємо Next.js 14 App Router з динамічними маршрутами. Ось базовий компонент на React 18 з TypeScript:
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'и перекладено, використовуємо компонент з таблицею перекладів. Це виключає N+1 запит до бекенду при кожному перемиканні. Приклад:
interface RouteTranslations { [locale: string]: string } 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)} /> Порівняння методів зберігання уподобань
| Метод | Доступність на сервері | Швидкість читання на клієнті | Складність реалізації |
|---|---|---|---|
| Cookie | Відразу | Повільно (HTTP) | Низька |
| localStorage | Ні | Швидко (синхронно) | Низька |
| Cookie + localStorage | Відразу (cookie) | Швидко (localStorage) | Середня |
Порівняння підходів до маршрутизації
| Підхід | Умова | SEO | Приклад |
|---|---|---|---|
| Префікс + локалізовані шляхи | Переклад slug'ів | hreflang, canonical | /en/contact vs /ru/kontakty |
| Тільки префікс | Slug'и однакові | Простіше, але гірше для багатомовності | /de/products/123 |
Dropdown-варіант для 4+ мов
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> ) } Типові помилки та їх вирішення
Hydration mismatch при SSR
При серверному рендерингу Next.js сервер і клієнт можуть мати різне уявлення про pathname, якщо використовувати usePathname() без урахування локалі. Рішення — передавати currentLocale через пропси або контекст, а не покладатися на usePathname на клієнті. Ми використовуємо серверні компоненти React (RSC) для отримання мови з cookie або params.
Ручна зміна мови в URL
На сервері перевіряємо мову з URL та при необхідності встановлюємо куку. Якщо мова не підтримується, редиректимо на дефолтну з 302. Валідуємо локалізовані slug'и на відповідність мові. За нашою статистикою, такий підхід знижує кількість 404 помилок на 90%.
Що входить у роботу
Результат включає: компонент перемикача мови (кнопки або dropdown), інтеграцію з бекендом для генерації локалізованих шляхів, автоматичну проставку hreflang та canonical тегів, тести на 10+ сценаріїв, документацію з підтримки та два тижні пост-релізної підтримки. Всі вихідні коди передаємо у ваш репозиторій.
Процес роботи
- Аналітика. Визначаємо список мов (зазвичай 2-6), типи контенту, існуючі URL. Заміряємо поточну швидкість завантаження сторінок (LCP, TTFB).
- Проектування. Узгоджуємо архітектуру (маршрутизація, сховище, компоненти).
- Реалізація. Пишемо компоненти, інтеграцію з бекендом, тести на 10+ сценаріїв.
- Тестування. Перевіряємо всі мовні комбінації, SEO-теги, доступність.
- Деплой. Розгортаємо на вашому хостингу, налаштовуємо кешування та редиректи.
Строки
Компонент-перемикач без локалізованих slug'ів — півдня. З таблицею перекладів шляхів та передачею даних з контролера — 1 робочий день. Повний цикл з інтеграцією hreflang та тестами — до 3 днів. Зв'яжіться з нами, щоб оцінити ваш проєкт — можливо, вкладемося швидше.
Гарантія SEO-коректності
Ми дотримуємося офіційних рекомендацій Mozilla hreflang. На всіх сторінках проставляємо теги rel='alternate' та rel='canonical', а також перевіряємо відсутність конфліктів з картами сайту. Отримайте консультацію з багатомовної архітектури — наші інженери розберуть ваш випадок безкоштовно. Залишайте заявку — ми зв'яжемося протягом дня.







