Files
site-portfolio/docs/translation.md
T
root e02ce1c629 fix(i18n): keep en navigation within /en/ via pagePrefix
- build-pages.mjs: add pagePrefix (locale-root-relative, no en offset)
- use pagePrefix for nav links: logo, back-home, footer design-system/
  cookie-policy/, FAB #contact, cookie-notice link, cookie-policy body
  link, case-page #skills/#projects anchors (assetPrefix pointed to site
  root and leaked en pages to ru)
- docs/translation.md: document assetPrefix vs pagePrefix
2026-09-15 23:00:32 +02:00

9.0 KiB
Raw Blame History

Локализация и перевод

Сайт локализован build-time: для каждого языка собирается отдельная папка/URL (/ — русский, /en/ — английский). Клиентского переключателя нет — это сделано ради SEO, корректной генерации PDF (Puppeteer) и совместимости с content-edit-mode.

Архитектура

  • LOCALES = ["ru", "en"], DEFAULT_LOCALE = "ru" — задаётся в scripts/build-pages.mjs.
  • Дефолтная локаль собирается в корень public/, остальные — в public/<lang>/ (public/en/).
  • Переключатель языка: src/partials/lang-switch.html (показывает RU/EN, aria-current на активном).

Как работает сборка (build-pages.mjs)

  1. UI-строки (навигация, футер, кнопки) вынесены в словари src/i18n/{ru,en}.json и вставляются токеном {{i18n:key}}. Для не-дефолтной локали значение берётся из en.json, при отсутствии ключа — фоллбэк на ru.json. Неразрезолвенный токен остаётся как {{i18n:key}} (признак пропущенного ключа).
  2. Тело страницы (главная, cookie-policy, кейсы) живёт в per-locale исходниках:
    • src/pages/<page>.html — русский (дефолт).
    • src/pages/<page>.en.html — английский override. Если файла нет, берётся русский.
    • В сборке базовый walk исключает *.en.html, чтобы они не собирались как отдельные страницы.
  3. Partials: для не-дефолтной локали инклуд {{> name}} сначала ищет src/partials/name.en.html, иначе name.html. Так переводятся частично захардкоженные partials (например, projects-section.en.html).
  4. assetPrefix: относительный префикс к ассетам. Для en к значению из baseVars добавляется один уровень ../, потому что страницы лежат в подпапке public/en/. Используй только для путей к ассетам (css/js/изображения).
  5. pagePrefix: префикс до корня локали (/en/ для en), без en-сдвига. Используй его для ВСЕХ навигационных ссылок (лого, «назад», футер, якоря #section, ссылки на другие страницы), чтобы они вели в ту же локаль, а не в корень сайта. assetPrefix предназначен только для путей к ассетам — для навигации он «перепрыгивает» через /en/ и ведёт на ru, поэтому в навигационных href его НЕ применяй.
  6. Статик-ассеты (css/js/vendor/favicon) в partials (head-links.html, head-scripts.html, theme-init-script.html, head-plyr.html, page-scripts.html) прописаны root-absolute (/css/..., /js/..., /vendor/..., /assets/shared/...). Причина: dev-сервер (serve-dev.mjs) отдаёт index.html (200) для отсутствующих файлов, маскируя 404, поэтому относительные {{assetPrefix}}css на /en/ ломались бы. Держи статик-ссылки root-absolute.
  7. <html lang="ru"> подменяется на текущую локаль (<html lang="en">).

Видимый контент страниц переводится через отдельные *.en.html исходники, а НЕ тысячами {{i18n:}}-токенов — так проще править большие тексты.

Перевод через apinex

Бесплатный инструмент перевода: POST https://api.apinex.bond/v1/tools/translator (дока: https://apinex.bond/developers/tools/translator).

  • Ключ — в /root/.hermes/.env, переменная HERMES_CUSTOM_APINEX_API_KEY (или APINEX_API_KEY). Никогда не выводи значение ключа в чат/логи.
  • Заголовок: Authorization: Bearer $KEY.
  • Тело: { "text": "строка|массив", "target_lang": "EN", "source_lang": "RU", "tag_handling": "html", "preserve_formatting": true }. Батч до 50 строк / 100k символов, лимит 60 запросов/мин.

Скрипт scripts/translate.mjs

Обёртка над API (Node, без зависимостей). Автоматически защищает от перевода шаблонные токены {{assetPrefix}}, {{> partial}}, {{i18n:…}} и содержимое <code> (через HTML-комментарии, восстанавливаются после). HTML-режим авто-включается, если во входе есть <…>.

cd /root/.hermes/projects/site-portfolio
# ключ в окружение (без печати значения)
export HERMES_CUSTOM_APINEX_API_KEY="$(grep -iE 'HERMES_CUSTOM_APINEX_API_KEY' /root/.hermes/.env | head -1 | cut -d= -f2- | sed 's/^["'\''"]//;s/["'\''"]$//')"

# список поддерживаемых языков
node scripts/translate.mjs --list-languages

# одна строка
node scripts/translate.mjs --target EN --source RU --text "Политика cookie"

# HTML из stdin (токены и <code> защищены)
echo '<p>{{assetPrefix}} <code>siteThemeV1</code></p>' | node scripts/translate.mjs --target EN --html

# файл страницы -> per-locale override
node scripts/translate.mjs --target EN --source RU --file src/pages/foo.html --out src/pages/foo.en.html

# пакет: одна строка на line
printf 'строка1\nстрока2\n' | node scripts/translate.mjs --target EN --batch

# npm-алиас
npm run translate -- --target EN --text "..."
  • --target обязателен; --source опционален (авто-детект).
  • --html/--no-html — принудительно вкл/выкл HTML-режим (иначе авто).
  • --out пишет в файл, иначе вывод в stdout.

Скилл translate-apinex

Загружается агентом при запросе перевода: содержит API, где ключ, защиту токенов и воркфлоу. Используй его вместо ручного копирования curl.

Воркфлоу: перевести страницу

  1. Создать src/pages/<page>.en.html (копия <page>.html).
  2. Прогнать через скрипт:
    node scripts/translate.mjs --target EN --source RU --file src/pages/<page>.en.html --out src/pages/<page>.en.html
    
    Скрипт переведёт видимый текст, оставив {{…}} и <code> нетронутыми.
  3. Если страница инклудит partials с захардкоженным русским (projects-section, contact-section, cookie-notice, fab-stack, mini-projects-section), создать их .en.html и перевести так же — сборка подхватит .en-вариант для en.
  4. UI-строки, оставшиеся как {{i18n:key}}, добавить в src/i18n/en.json.
  5. npm run build:pages.
  6. Проверить:
    curl -s http://localhost:4175/en/<page>/ | grep -o '<html lang="en">'
    curl -s http://localhost:4175/en/<page>/ | grep -cE 'остался русский текст'   # ожидаем 0
    

Добавить новый язык

  1. Добавить код в массив LOCALES в scripts/build-pages.mjs.
  2. Создать src/i18n/<code>.json (UI-строки).
  3. Создавать <page>.<code>.html / <name>.<code>.html override для нужных страниц/партилов (или полагаться на фоллбэк ru).
  4. Пересобрать. URL новой локали — /<code>/.

Где что лежит

Файл Назначение
scripts/build-pages.mjs Сборка локалей, резолв {{i18n:}}, override страниц/партилов, assetPrefix, lang
src/i18n/ru.json, src/i18n/en.json Словари UI-строк
src/pages/<page>.en.html Переведённое тело страницы (override)
src/partials/<name>.en.html Переведённый partial (override)
src/partials/lang-switch.html Переключатель RU/EN
scripts/translate.mjs CLI-обёртка над apinex translator
.env (/root/.hermes/.env) HERMES_CUSTOM_APINEX_API_KEY — ключ apinex (не коммитить)