Переключение языков на сайте: технические подводные камни
Небольшой компонент, а столько подводных камней. Нужно сохранять текущий 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', а также проверяем отсутствие конфликтов с картами сайта. Получите консультацию по мультиязычной архитектуре — наши инженеры разберут ваш случай бесплатно. Оставляйте заявку — мы свяжемся в течение дня.







