Перейти к контенту
Обновления продукта

Как мы локализовали наш сайт на 9 языков с помощью Astro

Alex Rivera5 мин. чтения

В этом спринте мы выпустили локализованную маршрутизацию для 9 языков. Вот честная версия того, как это произошло — включая баг, который незаметно вредил нам в продакшене, пока мы его не заметили.

Обнаружение

Ещё в Спринте 1 мы построили ожидаемый каркас i18n: JSON-файлы локалей для всех 9 языков (английский, испанский, арабский, немецкий, французский, японский, португальский, русский, китайский), хелпер getTranslation(), getLocalePath() для построения URL с учётом локали и теги hreflang на каждой странице, указывающие на все 9 вариантов локалей.

Чего мы не сделали: не создали реальных страниц по этим локализованным URL.

Каждая страница в src/pages/ жёстко задавала const locale = "en" as const. Инфраструктура перевода существовала и работала — но за ней ничего не стояло. Это означало, что BaseLayout.astro выдавал теги hreflang вида https://proman365.com/es/pricing/ на каждой странице, для URL, которые в продакшене возвращали 404.

Это не косметический баг. Поисковые системы используют теги hreflang, чтобы понять, какие URL являются языковыми вариантами друг друга. Массово рекламировать 404-ошибки — это активный негативный сигнал для SEO: он говорит краулерам, что метаданные вашего сайта не соответствуют реальности, что подрывает доверие ко всем вашим метаданным, а не только к сломанной части.

Решение: извлечение шаблонов, а не дублирование

Наивное решение — скопировать каждую страницу 8 раз, по одной на каждую не-английскую локаль, и жёстко задать в каждой копии свою константу локали. Мы сразу это отвергли — 8-кратное количество файлов означает 8-кратный рост рассинхронизации в тот момент, когда кто-то изменит одну страницу и забудет про остальные 7 копий.

Вместо этого мы выбрали подход: вынести содержимое каждой страницы в компонент-шаблон, принимающий locale как проп, а затем рендерить этот шаблон из двух видов маршрутов.

Для 12 страниц первого уровня (главная, тарифы, контакты, партнёры, помощь и шесть страниц функций) каждая была перенесена из src/pages/*.astro в src/page-templates/*.astro, а жёстко заданная константа локали заменена на:

interface Props {
  locale: Locale;
}
const { locale } = Astro.props;

Исходный английский маршрут стал тонкой обёрткой — несколько строк, которые просто импортируют шаблон и рендерят его с locale="en". Новый зеркальный маршрут под src/pages/[locale]/ рендерит тот же шаблон для 8 не-английских локалей, используя getStaticPaths() для генерации по одной странице на локаль.

Это сработало чисто, потому что логика каждой страницы первого уровня — getTranslation(locale), построители мета-тегов, разметка schema — уже была параметризована этой единственной константой locale из каркаса Спринта 1. Мы не встраивали i18n в эти страницы задним числом — мы наконец подключили i18n, который простаивал без дела.

Результат: 96 новых статических страниц (12 страниц × 8 локалей) из внедрения первого уровня, за которым последовал второй проход, распространивший тот же паттерн на 64 страницы сравнения (8 конкурентов × 8 локалей).

Ограничение hreflang через проп localized

Исправление пробела в маршрутизации не решает автоматически проблему hreflang — нужно ещё, чтобы теги заявляли только то, что действительно верно. Мы добавили в BaseLayout и PageLayout проп localized?: boolean со значением по умолчанию false.

Когда localized равен false, страница выдаёт ровно два тега hreflang: en и x-default. Когда true — она выдаёт полный набор из 10 (все 9 локалей плюс x-default). Только шаблоны первого уровня и страницы сравнения передают localized={true}.

Значение по умолчанию false было осознанным решением ради безопасности. Страница должна явно подтвердить, что у неё есть 9 языковых вариантов. Если завтра кто-то добавит новую страницу и забудет подумать о локалях, ошибка будет безопасной — метаданные останутся только англоязычными, а не породят очередную волну фантомных URL.

Что мы намеренно не локализовали

Не всё получило локализацию, и это было решение, а не упущение:

  • Блог остаётся только на английском на неопределённый срок. Это стандартная практика для маркетинговых блогов SaaS-компаний, а машинный перевод длинных текстов даёт худший результат, чем отсутствие перевода вообще. Если аналитика в конце концов оправдает создание отдельного контента для каждой локали, мы вернёмся к этому вопросу.
  • Юридические страницы (конфиденциальность, условия использования, политика cookie) остаются на английском. Этот текст сгенерирован Termly; переводить его — не то решение, которое мы можем принимать в одностороннем порядке.
  • Страница 404 остаётся на английском — статический хостинг отдаёт одну и ту же страницу 404 независимо от запрошенного пути, поэтому нет чистого способа локализовать её без другой модели хостинга.
  • RSS уже был только на английском и корректно исключён из hreflang — изменений там не потребовалось.

Итоговый чек-лист

Если вы делаете нечто похожее на статическом сайте Astro:

  1. Проведите аудит перед тем, как строить. Проверьте, указывают ли ваши теги hreflang на страницы, которые действительно существуют. Если ваш каркас i18n появился раньше маршрутизации, у вас, возможно, уже есть этот баг.
  2. Извлекайте шаблоны, а не дублируйте страницы. Один шаблон на страницу, поверх — тонкие обёрточные маршруты для конкретных локалей. Рассинхронизация, если и происходит, происходит на уровне обёрток, а обёртки дёшево поддерживать в актуальном состоянии.
  3. Заявления в метаданных по умолчанию должны быть максимально узкими и правдивыми. Флаг localized со значением по умолчанию «нет» означает, что новые страницы не могут случайно заявить лишнее.
  4. Решите, что осознанно остаётся без перевода, и зафиксируйте это. Блог, юридические страницы и страницы ошибок — разумные кандидаты на исключение, но это должно быть задокументированное решение, а не молчаливое умолчание.
  5. Перевод и маршрутизация — разные проблемы. Выпуск маршрутов не требует завершения переводов — английский текст как запасной вариант на реальном локализованном маршруте всё равно честнее, чем работающий ключ перевода без маршрута за ним.

Впереди у нас ещё ручная работа над переводами — во многих строках в не-английских файлах локалей пока стоят английские заглушки, ожидающие переводчика-человека. Но URL теперь реальны, и метаданные перестали лгать. Это была более срочная проблема.

ПоделитьсяXLinkedIn

Alex Rivera

Менеджер проектов и консультант по рабочим процессам. Помогает командам находить инструменты, соответствующие их реальному стилю работы.

Оставайтесь в курсе

Получайте советы по УП и новости о продукте. Без спама, отписка в любой момент.

Готовы упростить свой стек?

Попробуйте Proman бесплатно — кредитная карта не требуется.

Начать бесплатно →