docs: document i18n localization and apinex translation workflow

- docs/translation.md: build-time ru/en architecture, build-pages mechanics,
  apinex translator API + scripts/translate.mjs usage, page translation
  workflow, adding a new language
- AGENTS.md: link docs/translation.md in key-files table
This commit is contained in:
root committed 2026-09-15 22:51:37 +02:00
1 parent 86ea6731fa
commit 1d25dcee47
2 files changed
+104

No files matched your search

+1
View File
@@ -80,3 +80,4 @@
| `src/pages/cases/mini-projects-showcase.html` | Карточки активности (mp-card) |
| `docs/case-page-blocks.md` | Инструкция по блокам лонгридов |
| `docs/resume-pdf.md` | Генерация PDF резюме (Puppeteer) |
| `docs/translation.md` | Локализация (ru/en) и перевод через apinex |
+103
View File
@@ -0,0 +1,103 @@
# Локализация и перевод
Сайт локализован **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/`. Навигационные ссылки `{{casesPrefix}}cases/...` остаются относительными и для `en` корректно ведут в `/en/cases/...`.
5. **Статик-ассеты** (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.
6. `<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-режим авто-включается, если во входе есть `<…>`.
```bash
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. Прогнать через скрипт:
```bash
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. Проверить:
```bash
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 (не коммитить) |