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:
1 parent
5d9ddb6376
commit
e1d7b29f77
16 files changed
+4280
-3530
No files matched your search
@@ -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`.
|
||||
|
||||
@@ -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. **Моды:** редактор скриптов, замена ассетов, слияние и перенос модов.
|
||||
Reference in new issue
Block a user