Localization stage 1: stable IDs, import of tl/ translations, language picker

- 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>
This commit is contained in:
mrleo1nidandClaude Opus 5.5 committed 2026-10-25 03:39:40 +03:00
1 parent 5d9ddb6376
commit e1d7b29f77
16 files changed
+4280 -3530

No files matched your search

+4 -1
View File
@@ -105,7 +105,10 @@ LOCKED`), Redis не нужен.
редактировать), состояние плеера — стек `{label, path}` где `path` — индексы
внутри вложенных блоков, так что сохранения стабильны и компактны.
Узлы (`op`): `say`, `scene`, `show`, `hide`, `with`, `play`, `stop`, `queue`,
У каждого узла есть постоянный `id`, у реплик — ещё `tid` для перевода.
Переводы лежат отдельно, в `locales/<язык>/…` (см. [LOCALIZATION.md](LOCALIZATION.md)).
Узлы (`op`): `label` (точка входа внутри блока), `camera`, `say`, `scene`, `show`, `hide`, `with`, `play`, `stop`, `queue`,
`pause`, `menu`, `if`, `while`, `jump`, `call`, `return`, `exec`, `window`,
`noop`. Выражения: `const`, `name`, `attr`, `sub`, `bin`, `bool`, `cmp`,
`unary`, `if`, `list`, `tuple`, `dict`, `call`, `fmt`.
+182
View File
@@ -0,0 +1,182 @@
# Локализация и моды — проект
Цель: переводить и модифицировать игры прямо в библиотеке, нативно и удобно.
Переводы из самих игр импортируются при конвертации, дальше живут только в
нашем формате.
## Почему это нужно решить сейчас
Исходники игры удаляются после конвертации. Всё, что не вытащено в момент
конвертации (готовые переводы из `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. **Моды:** редактор скриптов, замена ассетов, слияние и перенос модов.