Files
pgames-manager/docs/LOCALIZATION.md
T
mrleo1nidandClaude Opus 5.5 ac164d1782 Localization stages 2-3: drafts, publishing, translation editor
- 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>
2026-10-25 03:56:19 +03:00

15 KiB
Raw Blame History

Локализация и моды — проект

Цель: переводить и модифицировать игры прямо в библиотеке, нативно и удобно. Переводы из самих игр импортируются при конвертации, дальше живут только в нашем формате.

Почему это нужно решить сейчас

Исходники игры удаляются после конвертации. Всё, что не вытащено в момент конвертации (готовые переводы из 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:

    { "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 их тоже не показывает.

Этапы 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 и без переводов, их нужно переимпортировать. Пока в библиотеке только тестовые данные, отдельную миграцию не делаем.

Этапы

  1. Фундамент (сейчас, до наполнения библиотеки): ID узлов и строк в конвертере, импорт tl/ всех языков, локали в манифесте, выбор языка и запасные языки в плеере, _()/!t/_preferences.language. Миграция уже сконвертированных версий: ID детерминированно выводятся из текущего IR.
  2. Перевод в библиотеке: главная версия, черновик и публикация, права, редактор перевода, прогресс, экспорт и импорт PO/CSV.
  3. Удобство: контекст сцены, «проиграть отсюда», глоссарий, рецензирование, шрифты и ассеты локалей, машинный предперевод.
  4. Моды: редактор скриптов, замена ассетов, слияние и перенос модов.