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

105 lines
9.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Локализация и перевод
Сайт локализован **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-режим авто-включается, если во входе есть `<…>`.
```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 (не коммитить) |