From 1d25dcee47034d4554eabf6560eb90eea1063717 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 15 Sep 2026 22:51:37 +0200 Subject: [PATCH] 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 --- AGENTS.md | 1 + docs/translation.md | 103 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 docs/translation.md diff --git a/AGENTS.md b/AGENTS.md index d99309d..1bcdcb7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 | diff --git a/docs/translation.md b/docs/translation.md new file mode 100644 index 0000000..9c54be8 --- /dev/null +++ b/docs/translation.md @@ -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//` (`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/.html` — русский (дефолт). + - `src/pages/.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. `` подменяется на текущую локаль (``). + +> Видимый контент страниц переводится через отдельные `*.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:…}}` и содержимое `` (через 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 (токены и защищены) +echo '

{{assetPrefix}} siteThemeV1

' | 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/.en.html` (копия `.html`). +2. Прогнать через скрипт: + ```bash + node scripts/translate.mjs --target EN --source RU --file src/pages/.en.html --out src/pages/.en.html + ``` + Скрипт переведёт видимый текст, оставив `{{…}}` и `` нетронутыми. +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// | grep -o '' + curl -s http://localhost:4175/en// | grep -cE 'остался русский текст' # ожидаем 0 + ``` + +## Добавить новый язык + +1. Добавить код в массив `LOCALES` в `scripts/build-pages.mjs`. +2. Создать `src/i18n/.json` (UI-строки). +3. Создавать `..html` / `..html` override для нужных страниц/партилов (или полагаться на фоллбэк ru). +4. Пересобрать. URL новой локали — `//`. + +## Где что лежит + +| Файл | Назначение | +|---|---| +| `scripts/build-pages.mjs` | Сборка локалей, резолв `{{i18n:}}`, override страниц/партилов, `assetPrefix`, `lang` | +| `src/i18n/ru.json`, `src/i18n/en.json` | Словари UI-строк | +| `src/pages/.en.html` | Переведённое тело страницы (override) | +| `src/partials/.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 (не коммитить) |