Перейти к содержанию
Acecore

Как сделать сайт на Astro 6 поддерживающим 9 языков ― Автоматический перевод 136 статей блога и мультиязычная архитектура

by Gui
Содержание
Как сделать сайт на Astro 6 поддерживающим 9 языков ― Автоматический перевод 136 статей блога и мультиязычная архитектура

Мы обновили официальный сайт Acecore с поддержки только японского языка до поддержки 9 языков. Эта статья описывает весь процесс: интернационализация UI, перевод 17 статей блога × 8 языков = 136 файлов и мультиязычная настройка Pages CMS.

Стратегия мультиязычности

Определение объёма

Мы реализовали мультиязычную поддержку в три этапа:

  1. Основа i18n: Настройка встроенной маршрутизации i18n в Astro, утилиты перевода и JSON-файлы переводов для 9 языков
  2. Перевод текстов UI: Тексты компонентов в шапке, подвале, боковой панели и на всех страницах
  3. Перевод статей блога: Все 17 статей переведены на 8 языков (сгенерировано 136 файлов)

Дизайн URL

Мы использовали prefixDefaultLocale: false в Astro, обслуживая японский на корневом пути (/blog/...), а другие языки с префиксами (/en/blog/..., /zh-cn/blog/... и т.д.).

# Японский (по умолчанию)
/blog/astro-performance-tuning/

# Английский
/en/blog/astro-performance-tuning/

# Упрощённый китайский
/zh-cn/blog/astro-performance-tuning/

Использование одного и того же slug для всех языков упрощает сопоставление URL при переключении языка.

Реализация основы i18n

Конфигурация i18n в Astro

Маршрутизация i18n настраивается в astro.config.mjs.

// astro.config.mjs
export default defineConfig({
  i18n: {
    defaultLocale: 'ja',
    locales: ['ja', 'en', 'zh-cn', 'es', 'pt', 'fr', 'ko', 'de', 'ru'],
    routing: {
      prefixDefaultLocale: false,
    },
  },
})

Утилиты перевода

Файлы конфигурации, утилитные функции и JSON-файлы переводов объединены в src/i18n/.

// src/i18n/utils.ts
export function t(locale: Locale, key: string): string {
  return translations[locale]?.[key]
    ?? translations[defaultLocale][key]
    ?? key
}

Файлы переводов хранятся в формате JSON в src/i18n/locales/, управляя примерно 100 ключами для навигации, подвала, UI блога и метаданных.

Паттерн View Component

Реализация страниц использует паттерн View Component. Вёрстка и логика централизованы в src/views/, а файлы маршрутов (src/pages/) — это лёгкие обёртки, которые просто передают locale.

---
// src/pages/[locale]/about.astro (файл маршрута)
import AboutPage from '../../views/AboutPage.astro'
const { locale } = Astro.params
---
<AboutPage locale={locale} />

Этот дизайн устраняет дублирование логики между японским маршрутом (/about) и мультиязычными маршрутами (/en/about).

Мультиязычная поддержка контента блога

Структура каталогов

Переведённые статьи размещаются в подкаталогах с кодом языка. Glob-загрузчик Astro автоматически обнаруживает их рекурсивно с паттерном **/*.md.

src/content/blog/
  astro-performance-tuning.md          # Японский (базовый)
  website-renewal.md
  en/
    astro-performance-tuning.md        # Английская версия
    website-renewal.md
  zh-cn/
    astro-performance-tuning.md        # Версия на упрощённом китайском
    website-renewal.md
  es/
    ...

Утилиты разрешения контента

В src/utils/blog-i18n.ts реализованы 3 функции.

// Определить, является ли пост базовой статьёй (нет слэша в ID = базовая)
export function isBasePost(post: CollectionEntry<'blog'>): boolean {
  return !post.id.includes('/')
}

// Удалить префикс locale из ID для получения базового slug
export function getBaseSlug(postId: string): string {
  const idx = postId.indexOf('/')
  return idx !== -1 ? postId.slice(idx + 1) : postId
}

// Получить локализованную версию базовой статьи (фолбэк на оригинал)
export function localizePost(
  post: CollectionEntry<'blog'>,
  allPosts: CollectionEntry<'blog'>[],
  locale: Locale,
): CollectionEntry<'blog'> {
  if (locale === defaultLocale) return post
  return allPosts.find((p) => p.id === `${locale}/${post.id}`) ?? post
}

Ключевой момент — не изменять существующую схему коллекции контента. Glob-загрузчик Astro автоматически распознаёт файлы в подкаталогах с ID вида en/astro-performance-tuning, поэтому изменения конфигурации не требуются.

Правила файлов перевода

Файлы перевода генерировались по следующим правилам:

  • Ключи frontmatter остаются на английском (title, description, date и т.д.)
  • Значения тегов сохраняются на японском (['技術', 'Astro'] и т.д.)
  • URL, пути к изображениям, блоки кода и HTML не изменяются
  • Дата и автор остаются без изменений
  • Текст тела и текстовые значения frontmatter (title, description, callout, FAQ и т.д.) переводятся

Рабочий процесс перевода

Процесс перевода проходит в следующем порядке:

  1. Создание английского как промежуточного языка: Перевод с японского оригинала на английский
  2. Перевод с английского на каждый язык: Расширение с английского на 7 языков
  3. Пакетная обработка: Обработка 5–6 статей за раз с GitHub Copilot

Двухэтапный перевод (японский → английский → целевые языки) снижает разброс качества. Путь через английский как промежуточный язык даёт более стабильное качество, чем прямой перевод с японского на каждый язык.

Мультиязычные View Components

Реализация BlogPostPage

Страница статьи блога получает локализованную версию контента с помощью localizePost() и присваивает её переменной шаблона.

---
// src/views/BlogPostPage.astro
const localizedPost = localizePost(basePost, allPosts, locale)
const post = localizedPost // существующие ссылки шаблона работают как есть
---

Этот подход обеспечивает мультиязычную поддержку без изменения каких-либо ссылок на post.data.title или post.body в шаблоне.

Реализация страниц-списков

Списки блога, списки тегов, списки авторов и архивные страницы фильтруют только базовые статьи с помощью isBasePost(), затем подменяют их переведёнными версиями через localizePost() при отображении.

---
const allPosts = await getCollection('blog')
const basePosts = allPosts.filter(isBasePost)
const displayPosts = basePosts.map(p => localizePost(p, allPosts, locale))
---

Особенности сборки

Экранирование во frontmatter YAML

Французские переводы вызывали проблемы, когда апострофы (l'atelier, qu'on и т.д.) конфликтовали с одинарными кавычками YAML.

# NG: Ошибка парсинга YAML
title: 'Le métavers est plus proche qu'on ne le pense'

# OK: Переключиться на двойные кавычки
title: "Le métavers est plus proche qu'on ne le pense"

Скрипт Node.js использовался для массового исправления всех файлов. Английский текст вроде Acecore's имеет ту же проблему, поэтому тип кавычек нужно учитывать при генерации файлов перевода.

Фильтрация маршрутов OG-изображений

/blog/og/[slug].png.ts также захватывал slug переведённых статей (en/aceserver-hijacked и т.д.), вызывая ошибки параметров. Решено фильтрацией через isBasePost().

export const getStaticPaths: GetStaticPaths = async () => {
  const allPosts = await getCollection('blog')
  const posts = allPosts.filter(isBasePost)
  return posts.map((post) => ({
    params: { slug: post.id },
    props: { title: post.data.title },
  }))
}

Мультиязычная поддержка Pages CMS

Pages CMS (.pages.yml) работает только с файлами непосредственно в указанном каталоге path, поэтому подкаталоги переводов были зарегистрированы как отдельные коллекции.

content:
  - name: blog
    label: ブログ(日本語)
    path: src/content/blog
  - name: blog-en
    label: Blog(English)
    path: src/content/blog/en
  - name: blog-zh-cn
    label: 博客(简体中文)
    path: src/content/blog/zh-cn
  # ... настроено для каждого языка

Метки написаны на каждом языке, чтобы в CMS было сразу понятно, какая коллекция соответствует какому языку.

UI переключения языка

В шапку добавлен компонент LanguageSwitcher, предоставляющий UI переключения языка для десктопа и мобильных. При переключении языка пользователи переходят к соответствующей locale-версии той же страницы. При первом посещении определяется navigator.language браузера для автоматического перенаправления.

Мультиязычное отображение тегов

Теги статей сохраняют японские slug в URL, при этом переводится только отображаемое название. Это позволяет избежать сложности маршрутизации, показывая теги на родном языке пользователя.

// src/i18n/utils.ts
export function translateTag(tag: string, locale: Locale): string {
  return t(locale, `tags.${tag}`) !== `tags.${tag}`
    ? t(locale, `tags.${tag}`)
    : tag
}

В каждый JSON перевода добавлен раздел tags, определяющий переводы для всех 25 типов тегов.

// en.json (выдержка)
{
  "tags": {
    "技術": "Technology",
    "セキュリティ": "Security",
    "パフォーマンス": "Performance",
    "アクセシビリティ": "Accessibility"
  }
}

translateTag() используется в 6 местах — карточки статей, боковая панель, индекс тегов и детали статьи — обеспечивая единообразное отображение всех тегов на соответствующем языке.

Мультиязычные данные авторов

Биографии и списки навыков авторов также переключаются в зависимости от языка. В src/data/authors.json добавлено поле i18n для хранения переводов на каждый язык.

{
  "id": "hatt",
  "name": "hatt",
  "bio": "代表取締役。Web制作・システム開発…",
  "skills": ["TypeScript", "Astro", "..."]
  "i18n": {
    "en": {
      "bio": "CEO and representative director. Web development...",
      "skills": ["TypeScript", "Astro", "..."]
    }
  }
}

Утилита getLocalizedAuthor() получает информацию об авторе, соответствующую locale.

// src/utils/blog-i18n.ts
export function getLocalizedAuthor(author: Author, locale: Locale) {
  const localized = author.i18n?.[locale]
  return localized ? { ...author, ...localized } : author
}

SEO для мультиязычного сайта

Для максимизации SEO-преимуществ мультиязычной поддержки мы реализовали механизмы, позволяющие поисковым системам правильно определять и индексировать каждую языковую версию.

Поддержка hreflang в карте сайта

Настроена опция i18n в @astrojs/sitemap для автоматической генерации тегов xhtml:link rel="alternate" в карте сайта.

// astro.config.mjs
sitemap({
  i18n: {
    defaultLocale: 'ja',
    locales: {
      ja: 'ja',
      en: 'en',
      'zh-cn': 'zh-CN',
      es: 'es',
      pt: 'pt',
      fr: 'fr',
      ko: 'ko',
      de: 'de',
      ru: 'ru',
    },
  },
})

Это генерирует ссылки hreflang для всех 9 языков на каждом URL, позволяя Google точно понимать соответствие между языковыми версиями.

Поддержка языка в структурированных данных JSON-LD

Поле inLanguage добавлено в структурированные данные BlogPosting статей, информируя поисковые системы о языке каждой статьи.

// BlogPostPage.astro (выдержка JSON-LD)
{
  "@type": "BlogPosting",
  "inLanguage": htmlLangMap[locale],  // "ja", "en", "zh-CN" и т.д.
  "headline": post.data.title,
  // ...
}

Мультиязычные RSS-ленты

Помимо японского /rss.xml, генерируются RSS-ленты для каждой языковой версии (/en/rss.xml, /zh-cn/rss.xml и т.д.). Заголовки и описания лент переведены на каждый язык, тег <language> выводит коды языков, совместимые с BCP47.

// src/pages/[locale]/rss.xml.ts
export const getStaticPaths = () =>
  locales.filter((l) => l !== defaultLocale).map((l) => ({ params: { locale: l } }))

<link rel="alternate" type="application/rss+xml"> в BaseLayout.astro также автоматически устанавливает соответствующий locale URL RSS.

Итоги

Используя встроенные возможности i18n Astro 6, мы достигли высококачественной мультиязычной поддержки даже на статическом сайте.

  • Основа i18n: Без префикса для японского с prefixDefaultLocale: false в Astro
  • Перевод UI: Нулевое дублирование логики благодаря паттерну View Component
  • Перевод контента: Подход с подкаталогами без изменений схемы
  • Перевод тегов: Японские slug в URL, отображаемые названия переведены по языкам
  • Перевод данных авторов: Bio и навыки переключаются по языкам
  • SEO: Hreflang в карте сайта, inLanguage в JSON-LD, мультиязычные RSS-ленты
  • Фолбэк: Непереведённые статьи автоматически показывают японскую версию
  • Поддержка CMS: Статьи каждого языка редактируются индивидуально в Pages CMS

В дальнейшем файлы перевода будут добавляться постепенно по мере публикации новых статей. Благодаря функции фолбэка японская версия отображается до завершения перевода, сохраняя качество сайта.

Мультиязычный рабочий процесс

Основа i18n

Настройка встроенной маршрутизации i18n в Astro и утилит перевода.

Перевод текстов UI

Перевод отображаемых текстов в шапке, подвале и всех компонентах.

Перевод статей блога

Генерация 136 файлов перевода (17 статей × 8 языков).

CMS и верификация сборки

Мультиязычная настройка Pages CMS и верификация сборки всех страниц.

Сравнение до и после

Только японский
  • Только 1 язык (японский)
  • 17 статей в блоге
  • 523 сгенерированных страницы (после мультиязычной поддержки UI)
  • Pages CMS с 1 коллекцией блога
  • Теги и данные авторов только на японском
  • 1 RSS-лента

9 языков
  • Японский + 8 языков (en, zh-cn, es, pt, fr, ko, de, ru)
  • 17 статей + 136 переводов = 153 всего
  • 541 сгенерированная страница (переведённые статьи с фолбэком)
  • Pages CMS с 9 коллекциями по языкам
  • 25 тегов и данные авторов переведены на каждый язык
  • Мультиязычные RSS-ленты (9 языков)
Часто задаваемые вопросы
Почему были выбраны 9 языков?
Для максимального глобального охвата мы покрыли основные языковые рынки. Английский, китайский, испанский и португальский охватывают большинство пользователей интернета, а французский, немецкий, русский и корейский дополняют оставшиеся крупные рынки.
Как обеспечивается качество перевода?
Мы используем AI-перевод через GitHub Copilot. Сначала создаётся английская версия как промежуточный язык, затем переводится с английского на каждый целевой язык для снижения разброса качества. Значения тегов во frontmatter остаются на японском, URL, блоки кода и пути к изображениям не изменяются.
Что происходит, когда переведённая статья не существует?
Функция фолбэка отображает оригинальную японскую статью, когда перевод отсутствует. Переводы можно добавлять постепенно.
Нужно ли переводить при добавлении новой статьи?
Перевод не обязателен — если файл перевода отсутствует, отображается японская версия как фолбэк. Чтобы добавить перевод, достаточно поместить файл Markdown с тем же именем в каталог соответствующего языка.
G

Gui

Генеральный директор Acecore. Универсальный инженер, охватывающий разработку систем, веб-производство, управление инфраструктурой и IT-образование. Любит решать организационные и человеческие задачи с помощью технологий.

Разработка систем Веб-производство Управление инфраструктурой IT-образование

Хотите узнать больше о наших услугах?

Мы обеспечиваем комплексную поддержку: разработка систем, веб-дизайн, графический дизайн и IT-образование.

Похожие статьи

Поиск статей