# Локализация и моды — проект Цель: переводить и модифицировать игры прямо в библиотеке, нативно и удобно. Переводы из самих игр импортируются при конвертации, дальше живут только в нашем формате. ## Почему это нужно решить сейчас Исходники игры удаляются после конвертации. Всё, что не вытащено в момент конвертации (готовые переводы из `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//`: * блок `translate ` сопоставляем с нашей репликой по комментарию `# game/file.rpy:строка` (точно и просто), запасной вариант — по ID Ren'Py; * `translate 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. **Моды:** редактор скриптов, замена ассетов, слияние и перенос модов.