- games.main_version_id, version visibility (draft/published) and kind
(original/translation/mod), version_history; migration 0002
- permissions translations.edit and versions.publish
- "Translate" creates a translation draft; publishing merges the changed
languages into the main version in place (players keep their saves), the
previous manifest goes to the history and can be restored
- translation API: overview with per-state stats, entries with filters,
search and scene context, save, glossary, locale font, PO/CSV export/import
- translation editor page: languages and progress, lines and strings,
filters, autosave, approve/outdated/fuzzy actions, glossary highlighting,
"play from here" with the scene restored
- player: ?lang= and ?at= from the editor, per-locale font
(models.py and permissions.py for this landed earlier in 9ee3e1f)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
15 KiB
Локализация и моды — проект
Цель: переводить и модифицировать игры прямо в библиотеке, нативно и удобно. Переводы из самих игр импортируются при конвертации, дальше живут только в нашем формате.
Почему это нужно решить сейчас
Исходники игры удаляются после конвертации. Всё, что не вытащено в момент
конвертации (готовые переводы из 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
-
ID узлов. Каждый оператор скрипта получает постоянный
id. Он присваивается один раз при конвертации и дальше не меняется при правках. На этих ID держатся переводы, сохранения и в будущем слияние модов. -
ID строк. Реплики получают
tidв стиле Ren'Py (метка_хэш) и хранят его в скрипте, так что правки текста ID не меняют. Пункты меню, имена,_()иshow textпереводятся таблицей строк по исходному тексту, как в Ren'Py. -
Исходный текст остаётся в скрипте. Скрипт читается как есть, язык оригинала — это просто «исходная локаль» (у Twisted Thoughts — французский).
-
Переводы — отдельные таблицы
locales/<язык>/<модуль>.json:{ "start_577f33b3": { "t": "Ммм...", "src": "a1b2c3", "state": "translated" } }src— хэш исходного текста на момент перевода. Если в моде или новой версии оригинал реплики изменился, перевод помечается «устарел», а не теряется молча. Состояния:translated,fuzzy/устарел,machine,approved. -
Файлы локали режутся по модулям скрипта. Правка одной реплики создаёт один новый маленький блоб — остальное общее (наш 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локали.
Версии и публикация
Сценарий:
- Модератор делает форк главной версии: «Перевод RU (черновик)».
- Переводит в редакторе. Форк виден только ему и тем, у кого есть права на рецензию.
- Нажимает «Опубликовать перевод».
Публикация переносит локаль, а не подменяет версию. Если два
переводчика параллельно делают 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 их тоже не показывает.
Этапы 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), картинки и
озвучка для отдельного языка, правка реплик прямо в плеере, машинный
предперевод (заложено только состояние machine).
Уже сконвертированные версии остаются без ID и без переводов, их нужно переимпортировать. Пока в библиотеке только тестовые данные, отдельную миграцию не делаем.
Этапы
- Фундамент (сейчас, до наполнения библиотеки): ID узлов и строк в
конвертере, импорт
tl/всех языков, локали в манифесте, выбор языка и запасные языки в плеере,_()/!t/_preferences.language. Миграция уже сконвертированных версий: ID детерминированно выводятся из текущего IR. - Перевод в библиотеке: главная версия, черновик и публикация, права, редактор перевода, прогресс, экспорт и импорт PO/CSV.
- Удобство: контекст сцены, «проиграть отсюда», глоссарий, рецензирование, шрифты и ассеты локалей, машинный предперевод.
- Моды: редактор скриптов, замена ассетов, слияние и перенос модов.