- every script node gets a permanent id, say nodes a Ren'Py-style tid - all languages from the game's tl/ are imported at conversion: dialogue blocks matched by their source-location comment or by label + original text, `translate strings` become a per-language strings table - source language from config.language or detected from the dialogue - manifest stores locales/<lang>/<module>.json and strings.json; game.json lists sourceLang and available locales with progress - player: language picker in main menu and settings, fallback to source, __(), [var!t] and _preferences.language - parser: `default` with init priority, and one bad statement no longer drops the whole file - docs/LOCALIZATION.md: design for translations, publishing and mods Line endings of several converter/player files go back to LF: an earlier commit accidentally stored them as CRLF. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
183 lines
12 KiB
Markdown
183 lines
12 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/`.
|
||
* Машинный предперевод — **позже, как подключаемый провайдер**. Сейчас
|
||
закладывается только состояние строки `machine`.
|
||
* Начинаем с этапа 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 их тоже не показывает.
|
||
|
||
Уже сконвертированные версии остаются без ID и без переводов, их нужно
|
||
переимпортировать. Пока в библиотеке только тестовые данные, отдельную
|
||
миграцию не делаем.
|
||
|
||
## Этапы
|
||
|
||
1. **Фундамент (сейчас, до наполнения библиотеки):** ID узлов и строк в
|
||
конвертере, импорт `tl/` всех языков, локали в манифесте, выбор языка и
|
||
запасные языки в плеере, `_()`/`!t`/`_preferences.language`.
|
||
Миграция уже сконвертированных версий: ID детерминированно выводятся
|
||
из текущего IR.
|
||
2. **Перевод в библиотеке:** главная версия, черновик и публикация, права,
|
||
редактор перевода, прогресс, экспорт и импорт PO/CSV.
|
||
3. **Удобство:** контекст сцены, «проиграть отсюда», глоссарий,
|
||
рецензирование, шрифты и ассеты локалей, машинный предперевод.
|
||
4. **Моды:** редактор скриптов, замена ассетов, слияние и перенос модов.
|