The run is recorded at every page and picked up from the translator's finished job; one run across tabs via Web Locks. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
276 lines
21 KiB
Markdown
276 lines
21 KiB
Markdown
# Локализация и моды — проект
|
||
|
||
Цель: переводить и модифицировать игры прямо в библиотеке, нативно и удобно.
|
||
Переводы из самих игр импортируются при конвертации, дальше живут только в
|
||
нашем формате.
|
||
|
||
## Почему это нужно решить сейчас
|
||
|
||
Исходники игры удаляются после конвертации. Всё, что не вытащено в момент
|
||
конвертации (готовые переводы из `tl/`, привязка реплик к их месту в
|
||
исходнике), потом взять будет неоткуда. Поэтому стабильные идентификаторы
|
||
и импорт переводов должны появиться в конвертере до того, как библиотека
|
||
наполнится играми.
|
||
|
||
## Что есть в играх (по примерам)
|
||
|
||
| Игра | Языки в `tl/` | Переведённых реплик |
|
||
|---|---|---|
|
||
| That New Teacher | 19 (в т.ч. russian) | ~350 000 блоков |
|
||
| Twisted Thoughts | 9; исходник на французском, english — перевод | ~117 000 |
|
||
| остальные 4 | только исходный | — |
|
||
|
||
Формат Ren'Py:
|
||
|
||
```
|
||
# game/Days/dayEight.rpy:248
|
||
translate russian D8_Clarissa_c7855708:
|
||
# "You lie on your side, facing away..."
|
||
"Ты лежишь на боку, отвернувшись..."
|
||
|
||
translate russian strings:
|
||
old "Iris"
|
||
new "Айрис"
|
||
```
|
||
|
||
Реплики переводятся блоками с идентификатором (`метка_хэш`) и ссылкой на
|
||
строку оригинала; имена, пункты меню и строки интерфейса — таблицей old/new.
|
||
Бывает и игровая логика, зависящая от языка (`_preferences.language`).
|
||
|
||
## Главная идея: текст отдельно от скрипта, у всего стабильные ID
|
||
|
||
1. **ID узлов.** Каждый оператор скрипта получает постоянный `id`. Он
|
||
присваивается один раз при конвертации и дальше не меняется при правках.
|
||
На этих ID держатся переводы, сохранения и в будущем слияние модов.
|
||
2. **ID строк.** Реплики получают `tid` в стиле Ren'Py (`метка_хэш`) и
|
||
хранят его в скрипте, так что правки текста ID не меняют. Пункты меню,
|
||
имена, `_()` и `show text` переводятся таблицей строк по исходному тексту,
|
||
как в Ren'Py.
|
||
3. **Исходный текст остаётся в скрипте.** Скрипт читается как есть, язык
|
||
оригинала — это просто «исходная локаль» (у Twisted Thoughts — французский).
|
||
4. **Переводы — отдельные таблицы** `locales/<язык>/<модуль>.json`:
|
||
|
||
```json
|
||
{ "start_577f33b3": { "t": "Ммм...", "src": "a1b2c3", "state": "translated" } }
|
||
```
|
||
|
||
`src` — хэш исходного текста на момент перевода. Если в моде или новой
|
||
версии оригинал реплики изменился, перевод помечается «устарел», а не
|
||
теряется молча. Состояния: `translated`, `fuzzy`/устарел, `machine`,
|
||
`approved`.
|
||
5. **Файлы локали режутся по модулям скрипта.** Правка одной реплики создаёт
|
||
один новый маленький блоб — остальное общее (наш content-addressed store).
|
||
|
||
`locales/<язык>/meta.json`: код языка (BCP-47: `ru`), название, авторы,
|
||
прогресс, шрифт-замена (у многих игр в шрифтах нет кириллицы или CJK),
|
||
переопределения ассетов для языка: картинки с надписями, озвучка.
|
||
|
||
## Импорт переводов из игры
|
||
|
||
При конвертации разбираем `tl/<lang>/`:
|
||
|
||
* блок `translate <lang> <id>` сопоставляем с нашей репликой по комментарию
|
||
`# game/file.rpy:строка` (точно и просто), запасной вариант — по ID Ren'Py;
|
||
* `translate <lang> strings` превращаем в таблицу old→new для имён, меню и `_()`;
|
||
* языки Ren'Py (`russian`, `spanish_latam`, `None`) переводим в коды `ru`,
|
||
`es-419` и исходную локаль;
|
||
* импортируются все языки игры — это только текст, места почти не занимает.
|
||
|
||
## Плеер
|
||
|
||
* Выбор языка в главном меню и настройках, запоминается для каждого
|
||
пользователя.
|
||
* Цепочка запасных языков: `ru → en → исходный`. Непереведённая строка
|
||
показывается на запасном языке и не ломает игру.
|
||
* `_()`, флаг `!t` в `[переменных]`, имена персонажей переводятся из таблиц;
|
||
`_preferences.language` отдаёт выбранный язык, поэтому языковая логика
|
||
игры (как в Twisted Thoughts) работает.
|
||
* Шрифт и ассеты подменяются из `meta.json` локали.
|
||
|
||
## Версии и публикация
|
||
|
||
Сценарий:
|
||
|
||
1. Модератор делает форк главной версии: «Перевод RU (черновик)».
|
||
2. Переводит в редакторе. Форк виден только ему и тем, у кого есть права
|
||
на рецензию.
|
||
3. Нажимает «Опубликовать перевод».
|
||
|
||
**Публикация переносит локаль, а не подменяет версию.** Если два
|
||
переводчика параллельно делают RU и DE в двух форках, «сделать форк главным»
|
||
потеряет работу одного из них. Поэтому публикация создаёт новую главную
|
||
версию: главная + локаль из форка. Переводы склеиваются по `tid` построчно,
|
||
это тривиально. Перенос перевода в любую совместимую версию (например, в мод)
|
||
работает так же — одной кнопкой, по тем же ID.
|
||
|
||
Модели:
|
||
|
||
* `games.main_version_id` — главная версия (её открывает кнопка «Играть»);
|
||
* у версии `visibility`: `draft` / `published`, а также `kind`: оригинал,
|
||
перевод, мод;
|
||
* новые права: `translations.edit` — переводить в своих форках,
|
||
`versions.publish` — публиковать, назначать главную.
|
||
|
||
## Редактор перевода
|
||
|
||
* Таблица строк по сценам и меткам: оригинал | перевод | статус, фильтры
|
||
«не переведено / устарело / машинное», поиск, прогресс в процентах.
|
||
* Контекст строки: кто говорит, соседние реплики, фон и спрайты сцены на этом
|
||
месте, кнопка «проиграть с этой строки» в плеере.
|
||
* Глоссарий: имена и термины подставляются и проверяются на единообразие.
|
||
* Экспорт и импорт PO/XLIFF/CSV для внешних инструментов, экспорт обратно
|
||
в формат Ren'Py `tl/`.
|
||
* Позже: режим «править прямо в игре» — клик по реплике в плеере;
|
||
машинный предперевод с пометкой `machine`.
|
||
|
||
## Моды
|
||
|
||
Те же основы делают моды нативными:
|
||
|
||
* скрипт редактируется на уровне меток и операторов с постоянными ID;
|
||
* мод — форк с изменёнными модулями скрипта и ассетами: в хранилище только
|
||
отличия;
|
||
* переводы оригинала автоматически применяются к неизменённым репликам мода,
|
||
новые реплики мода видны как непереведённые;
|
||
* так как у всего есть ID и родитель, можно делать трёхстороннее слияние:
|
||
мод A + мод B, или перенос мода на новую версию оригинала.
|
||
|
||
## Принятые решения
|
||
|
||
* Публикация перевода — **слияние локали** в новую главную версию, а не
|
||
подмена главной версии форком.
|
||
* Из игр импортируются **все языки** `tl/`.
|
||
* Машинный предперевод — **на стороне клиента**: переводит программа
|
||
[local-translator](https://gitea.hsrv.site/mrleo1nid/local-translator) на
|
||
компьютере переводчика, сервер библиотеки о ней не знает (см. ниже).
|
||
* Начинаем с этапа 1.
|
||
|
||
## Состояние
|
||
|
||
Этап 1 сделан:
|
||
|
||
* у всех узлов скрипта есть `id`, у реплик — `tid`;
|
||
* при конвертации импортируются все языки из `tl/`. Блок сопоставляется
|
||
с репликой по комментарию `# game/file.rpy:N`, а если он устарел — по
|
||
метке и исходному тексту. Отдельные таблицы `strings` тоже импортируются;
|
||
* исходный язык берётся из `config.language` или определяется по тексту
|
||
(у Twisted Thoughts — французский);
|
||
* в манифесте лежат `locales/<код>/<модуль>.json` и `locales/<код>/strings.json`,
|
||
в `game.json` — `sourceLang` и `locales` с прогрессом;
|
||
* в плеере есть выбор языка в главном меню и настройках, запасной язык —
|
||
исходный, работают `__()`, `!t`, `_preferences.language`.
|
||
|
||
Проверено на примерах: Twisted Thoughts — 9 языков, по 12 949 реплик,
|
||
100% сопоставлено. That New Teacher — 18 языков, около 75% реплик: остальное
|
||
авторы ещё не перевели, а ~750 блоков на язык относятся к уже изменённым
|
||
английским репликам. Ren'Py их тоже не показывает.
|
||
|
||
Этапы 2 и 3 сделаны:
|
||
|
||
* у игры есть главная версия (`games.main_version_id`), у версий —
|
||
`visibility` (`draft`/`published`) и `kind` (`original`/`translation`/`mod`);
|
||
миграция `0002` проставила главную версию существующим играм;
|
||
* права `translations.edit` и `versions.publish`, у системной роли
|
||
moderator они включены;
|
||
* кнопка «Перевести» создаёт черновик-форк вида «перевод», «Опубликовать»
|
||
вливает изменённые языки **в саму главную версию**: её ID не меняется,
|
||
сохранения игроков остаются. Прежний манифест пишется в `version_history`
|
||
(`GET /api/versions/{id}/history`, откат —
|
||
`POST …/history/{hid}/restore`);
|
||
* редактор перевода `/versions/{id}/translate`:
|
||
* языки с прогрессом по состояниям, реплики и строки (имена, меню, текст
|
||
на экране);
|
||
* фильтры «к работе / не переведено / устарело / неточно / машинный /
|
||
переведено / одобрено», поиск, фильтр по файлу скрипта;
|
||
* автосохранение, Ctrl+Enter — к следующей строке;
|
||
* кнопки «одобрить» (только у публикующих), «актуально» для устаревших,
|
||
«неточно», «скопировать оригинал»;
|
||
* контекст строки: фон сцены, соседние реплики, «играть отсюда»
|
||
(`/play/{id}?at=<узел>&lang=<язык>`, картинка сцены восстанавливается);
|
||
* глоссарий с подсветкой терминов в оригинале;
|
||
* экспорт и импорт PO/CSV, шрифт для языка;
|
||
* API: `GET/PUT /api/versions/{id}/i18n[/{lang}]`, `…/entries`, `…/meta`,
|
||
`…/font`, `…/export`, `…/import`, `DELETE …/{lang}`.
|
||
|
||
Пока не сделано: UI истории версии (сейчас есть только API), картинки и
|
||
озвучка для отдельного языка, правка реплик прямо в плеере.
|
||
|
||
### Машинный перевод через локальный переводчик
|
||
|
||
Страница `/translator` («Локальный переводчик» в меню профиля) объясняет, как
|
||
поставить LM Studio и local-translator, какую модель взять по объёму
|
||
видеопамяти, и проверяет связь прямо из браузера. В редакторе перевода кнопка
|
||
«Машинный перевод» открывает панель: источник и модель, строк за запрос,
|
||
пожелания к стилю (запоминаются для игры), «имена и интерфейс», «обновить
|
||
устаревшие».
|
||
|
||
Как это работает:
|
||
|
||
* браузер сам ходит на `http://127.0.0.1:8787` (адрес можно сменить на странице
|
||
настройки); библиотека получает только обычные `PUT …/i18n/{lang}` с
|
||
состоянием `machine`, ни адреса, ни моделей она не видит;
|
||
* для каждой игры и пары языков в переводчике создаётся проект
|
||
`PGames: <игра> (<исходный> → <язык>)`: контекст из описания игры, глоссарий
|
||
из вкладки «Глоссарий», пожелания пользователя. Проект находится по имени и
|
||
обновляется перед каждым запуском;
|
||
* запуск живёт в провайдере выше страниц (`translator/runner.tsx`): перевод идёт,
|
||
пока вкладка открыта, даже если уйти из редактора; его карточка с прогрессом и
|
||
кнопкой «Остановить» стоит в том же плавающем окне, что и загрузки. Одновременно
|
||
работает только один перевод (на все игры и языки; между вкладками это
|
||
держит Web Lock), пока он идёт, кнопки
|
||
отдельных строк недоступны; закрытие вкладки браузер подтверждает. На каждой странице запуск
|
||
записывается в `localStorage` (`pg.translation.run`: что переводится, где
|
||
остановились, какое задание отправлено в переводчик). После обновления страницы
|
||
сайт сам продолжает: забирает готовое задание из переводчика, сохраняет его и идёт
|
||
дальше, а карточка и бейджи снова обновляются. Запись старше 30 минут не
|
||
подхватывается;
|
||
* автоперевод идёт по списку так, как его видит пользователь: текущая
|
||
вкладка («Реплики» или «Имена и меню»), фильтр по файлу и поиск, сверху вниз.
|
||
Берутся строки «не переведено» (и «устарело» по желанию) страницами по 40;
|
||
страница уходит одним заданием, готовое сразу сохраняется, появляется в
|
||
открытых строках и обновляет прогресс в шапке;
|
||
* список разбит на страницы (20/40/100/200 строк, номера страниц, переход по
|
||
номеру); сервер в `GET …/entries` по параметру `locate=<ключ>` возвращает
|
||
`located`, позицию строки в том же отфильтрованном списке. По нему режим
|
||
«Следить за переводом» (включён по умолчанию, выключается кнопкой над списком,
|
||
флажком в панели или ручным листанием) открывает страницу, на которой модель
|
||
работает сейчас, и подсвечивает строки текущей пачки;
|
||
* у каждой строки есть кнопка «Перевести» (пока переводчик подключён): она
|
||
переводит одну строку с тем же контекстом и глоссарием и помечает её
|
||
«Машинный». Редактор ищет переводчик сам, только если он уже работал в этом
|
||
браузере, чтобы Chrome не спрашивал про локальную сеть без причины;
|
||
* флажок «Передавать описание и жанры игры как контекст» (по умолчанию включён)
|
||
добавляет описание и теги игры в контекст проекта; название игры уходит всегда; Остановка
|
||
отменяет задание в переводчике; повторный запуск продолжает с оставшегося.
|
||
Непереведённые из-за ошибки строки (например, модель повредила плейсхолдер)
|
||
пропускаются и видны в счётчике ошибок;
|
||
* переводчик по умолчанию закрыт для браузера. Адрес сайта вписывается в
|
||
`allowed_origins` его `config.toml` (точный адрес, без `*`); для Chrome сервер
|
||
отвечает на заголовок Private Network Access, а сам браузер один раз спросит
|
||
разрешение на доступ к локальной сети. Страница настройки отличает «не запущен»
|
||
от «запущен, но сайт не разрешён»;
|
||
* код: `frontend/src/translator/` (`client.ts` — API переводчика, `run.ts` —
|
||
цикл перевода, `MachineTranslate.tsx` — панель, `TranslatorSetup.tsx` —
|
||
страница настройки); проверки в `frontend/tests/translator-run.test.mjs`.
|
||
|
||
Ограничения: перевод идёт, пока открыта страница редактора; контекст
|
||
(кто говорит, соседние реплики) модели пока не передаётся, только текст строки;
|
||
Safari может запрещать https-странице обращаться к локальному http.
|
||
|
||
Уже сконвертированные версии остаются без ID и без переводов, их нужно
|
||
переимпортировать. Пока в библиотеке только тестовые данные, отдельную
|
||
миграцию не делаем.
|
||
|
||
## Этапы
|
||
|
||
1. **Фундамент (сейчас, до наполнения библиотеки):** ID узлов и строк в
|
||
конвертере, импорт `tl/` всех языков, локали в манифесте, выбор языка и
|
||
запасные языки в плеере, `_()`/`!t`/`_preferences.language`.
|
||
Миграция уже сконвертированных версий: ID детерминированно выводятся
|
||
из текущего IR.
|
||
2. **Перевод в библиотеке:** главная версия, черновик и публикация, права,
|
||
редактор перевода, прогресс, экспорт и импорт PO/CSV.
|
||
3. **Удобство:** контекст сцены, «проиграть отсюда», глоссарий,
|
||
рецензирование, шрифты и ассеты локалей, машинный предперевод.
|
||
4. **Моды:** редактор скриптов, замена ассетов, слияние и перенос модов.
|