- 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
105 lines
9.0 KiB
Markdown
105 lines
9.0 KiB
Markdown
# Локализация и перевод
|
||
|
||
Сайт локализован **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 (не коммитить) |
|