Структура URL для мультиязычных сайтов

При локализации веб-приложений и сайтов необходимо определить способ представления языка в адресной строке. От выбранной структуры URL зависят SEO-показатели, удобство пользователей, работа маршрутизации, настройка серверов и интеграция с библиотекой I18next.

Типичная задача состоит в том, чтобы один и тот же контент отображался на разных языках:

  • Русский: /ru/about
  • Английский: /en/about
  • Немецкий: /de/about

I18next предоставляет механизмы определения текущего языка из различных источников, включая URL, параметры запроса, cookie, localStorage и HTTP-заголовки браузера.


Основные подходы к организации мультиязычных URL

Язык в пути URL

Наиболее распространённый вариант.

Примеры:

/ru/
/en/
/de/

/ru/products
/en/products
/de/products

Преимущества:

  • понятная структура;
  • хорошая индексация поисковыми системами;
  • возможность прямых ссылок на конкретный язык;
  • удобная интеграция с маршрутизацией SPA и SSR-приложений.

Недостатки:

  • требуется настройка маршрутов;
  • необходимо учитывать язык при генерации ссылок.

Пример определения языка:

https://site.com/ru/products

Язык извлекается из первого сегмента пути.


Язык через поддомен

Примеры:

ru.site.com
en.site.com
de.site.com

Преимущества:

  • логическое разделение локалей;
  • возможность независимой инфраструктуры для каждого языка;
  • хорошая SEO-поддержка.

Недостатки:

  • более сложная настройка DNS;
  • дополнительные настройки сертификатов;
  • усложнение развёртывания.

Язык через домен верхнего уровня

Примеры:

site.ru
site.com
site.de

Преимущества:

  • максимальная географическая привязка;
  • высокая эффективность для международного SEO.

Недостатки:

  • необходимость поддержки нескольких доменов;
  • усложнение администрирования;
  • более высокая стоимость поддержки.

Язык через query-параметр

Примеры:

/products?lang=ru
/products?lang=en

Преимущества:

  • простая реализация;
  • минимальные изменения маршрутов.

Недостатки:

  • менее привлекательные URL;
  • слабее SEO-показатели;
  • сложнее организовывать канонические ссылки.

Такой подход чаще применяется во внутренних системах и административных панелях.


Интеграция URL и I18next

Для извлечения языка из URL используется плагин Language Detector.

Установка:

npm install i18next-browser-languagedetector

Подключение:

import i18next from "i18next";
import LanguageDetector from "i18next-browser-languagedetector";

i18next
  .use(LanguageDetector)
  .init({
    fallbackLng: "en"
  });

По умолчанию детектор анализирует несколько источников языка.


Настройка определения языка из пути URL

Наиболее популярная конфигурация:

i18next
  .use(LanguageDetector)
  .init({
    detection: {
      order: ["path"]
    }
  });

Для URL:

/ru/profile

будет выбран язык:

ru

Индекс языкового сегмента

Плагин позволяет указать положение языкового кода в URL.

Пример адреса:

/app/ru/profile

Здесь язык находится во втором сегменте.

Настройка:

i18next
  .use(LanguageDetector)
  .init({
    detection: {
      order: ["path"],
      lookupFromPathIndex: 1
    }
  });

Разбор пути:

/app/ru/profile
  0   1    2

Индекс:

1

соответствует языку:

ru

Использование нескольких источников определения языка

Практически всегда используется цепочка проверок.

Пример:

i18next
  .use(LanguageDetector)
  .init({
    detection: {
      order: [
        "path",
        "localStorage",
        "cookie",
        "navigator"
      ]
    }
  });

Алгоритм работы:

  1. Проверить URL.
  2. Проверить localStorage.
  3. Проверить cookie.
  4. Проверить язык браузера.

Если ничего не найдено, используется fallbackLng.


Настройка резервного языка

Даже при использовании URL возможна ситуация, когда язык отсутствует или не поддерживается.

Настройка:

i18next.init({
  fallbackLng: "en"
});

Если пользователь открывает:

/fr/about

а французская локаль отсутствует, приложение переключится на:

en

Ограничение списка языков

Для предотвращения выбора неподдерживаемых локалей рекомендуется использовать список допустимых языков.

i18next.init({
  supportedLngs: ["en", "ru", "de"],
  fallbackLng: "en"
});

Теперь язык:

/es

будет автоматически заменён на резервный вариант.


Автоматическое обновление URL после смены языка

I18next изменяет внутреннее состояние приложения, но не модифицирует адресную строку автоматически.

Пример:

await i18next.changeLanguage("de");

После вызова необходимо обновить маршрут вручную.

Для React Router:

navigate("/de/profile");

Для Next.js:

router.push("/de/profile");

Для Vue Router:

router.push("/de/profile");

Синхронизация маршрутов и локалей

Важно, чтобы язык URL и язык I18next всегда совпадали.

Неправильная ситуация:

URL: /ru/profile

Текущий язык I18next: en

Правильная ситуация:

URL: /ru/profile

Текущий язык I18next: ru

При загрузке страницы язык должен определяться из URL и сразу передаваться в I18next.


Реализация языкового переключателя

Типичный переключатель языков строится на изменении маршрута.

Пример:

const changeLanguage = (lng) => {
  i18next.changeLanguage(lng);

  const currentPath = window.location.pathname;

  const newPath = currentPath.replace(
    /^\/(ru|en|de)/,
    `/${lng}`
  );

  window.location.pathname = newPath;
};

Текущий путь:

/ru/products

После выбора немецкого:

/de/products

Локализованные маршруты

Иногда переводится не только содержимое страницы, но и сами маршруты.

Пример:

/en/about
/ru/o-kompanii
/de/uber-uns

В этом случае возникает дополнительный уровень локализации.

Структура может храниться в отдельном объекте:

const routes = {
  about: {
    en: "/about",
    ru: "/o-kompanii",
    de: "/uber-uns"
  }
};

Получение маршрута:

routes.about[i18next.language];

URL и SEO

Для поисковых систем каждая языковая версия должна иметь собственный адрес.

Хороший вариант:

/en/about
/ru/about
/de/about

Плохой вариант:

/about

с динамической подменой языка на клиенте.

Поисковые роботы значительно лучше работают с отдельными URL для каждой локали.


Использование hreflang

Для каждой страницы рекомендуется указывать альтернативные языковые версии.

Пример:

<link
  rel="alternate"
  hreflang="en"
  href="https://site.com/en/about"
/>

<link
  rel="alternate"
  hreflang="ru"
  href="https://site.com/ru/about"
/>

<link
  rel="alternate"
  hreflang="de"
  href="https://site.com/de/about"
/>

Поисковые системы понимают связь между локализованными страницами и корректно показывают нужную версию пользователям разных стран.


URL и серверный рендеринг

В SSR-приложениях язык определяется ещё на сервере.

Пример запроса:

GET /ru/products

Сервер:

  1. извлекает язык из URL;
  2. загружает переводческие ресурсы;
  3. рендерит страницу на русском языке;
  4. отправляет готовый HTML клиенту.

Такой подход обеспечивает:

  • лучшую SEO-индексацию;
  • более быструю первую отрисовку;
  • отсутствие визуального переключения языка после загрузки страницы.

Часто применяется комбинированная стратегия.

Приоритет:

detection: {
  order: [
    "path",
    "cookie",
    "navigator"
  ]
}

Сценарий:

  1. Пользователь открывает /de/products.
  2. I18next выбирает немецкий язык.
  3. Язык сохраняется в cookie.
  4. При последующих посещениях предпочтение пользователя уже известно системе.

Структура каталогов переводов при использовании языковых URL

Типичная организация ресурсов:

public
└── locales
    ├── en
    │   ├── common.json
    │   └── profile.json
    │
    ├── ru
    │   ├── common.json
    │   └── profile.json
    │
    └── de
        ├── common.json
        └── profile.json

Конфигурация:

i18next.init({
  supportedLngs: [
    "en",
    "ru",
    "de"
  ],
  fallbackLng: "en"
});

Код локали в URL напрямую соответствует названию каталога переводов.


Типичные ошибки

Несоответствие URL и активного языка

Неправильно:

URL: /ru/products
i18next.language === "en"

Такая ситуация приводит к путанице в интерфейсе и проблемам индексации.


Отсутствие резервного языка

Неправильно:

i18next.init({});

Правильно:

i18next.init({
  fallbackLng: "en"
});

Использование только языка браузера

Неправильно:

order: ["navigator"]

В этом случае невозможно создать постоянную ссылку на конкретную локаль.


Отсутствие языкового сегмента в ссылках

Неправильно:

<a href="/products">

Правильно:

<a href="/ru/products">

или

<a href={`/${i18next.language}/products`}>

Рекомендуемая архитектура

Для большинства современных проектов оптимальным считается следующий подход:

/ru/...
/en/...
/de/...

Конфигурация I18next:

i18next
  .use(LanguageDetector)
  .init({
    fallbackLng: "en",
    supportedLngs: [
      "en",
      "ru",
      "de"
    ],
    detection: {
      order: ["path"],
      lookupFromPathIndex: 0
    }
  });

Такая схема обеспечивает:

  • предсказуемые URL;
  • удобную маршрутизацию;
  • корректную SEO-индексацию;
  • простую интеграцию с React Router, Vue Router и Next.js;
  • прозрачную работу механизма определения языка в I18next.