Shared translator session, game description/genres as optional context, live header progress. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
19 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/. - Машинный предперевод — на стороне клиента: переводит программа local-translator на компьютере переводчика, сервер библиотеки о ней не знает (см. ниже).
- Начинаем с этапа 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), картинки и озвучка для отдельного языка, правка реплик прямо в плеере.
Машинный перевод через локальный переводчик
Страница /translator («Локальный переводчик» в меню профиля) объясняет, как
поставить LM Studio и local-translator, какую модель взять по объёму
видеопамяти, и проверяет связь прямо из браузера. В редакторе перевода кнопка
«Машинный перевод» открывает панель: источник и модель, строк за запрос,
пожелания к стилю (запоминаются для игры), «имена и интерфейс», «обновить
устаревшие».
Как это работает:
- браузер сам ходит на
http://127.0.0.1:8787(адрес можно сменить на странице настройки); библиотека получает только обычныеPUT …/i18n/{lang}с состояниемmachine, ни адреса, ни моделей она не видит; - для каждой игры и пары языков в переводчике создаётся проект
PGames: <игра> (<исходный> → <язык>): контекст из описания игры, глоссарий из вкладки «Глоссарий», пожелания пользователя. Проект находится по имени и обновляется перед каждым запуском; - автоперевод идёт по списку так, как его видит пользователь: текущая вкладка («Реплики» или «Имена и меню»), фильтр по файлу и поиск, сверху вниз. Берутся строки «не переведено» (и «устарело» по желанию) страницами по 40; страница уходит одним заданием, готовое сразу сохраняется, появляется в открытых строках и обновляет прогресс в шапке;
- у каждой строки есть кнопка «Перевести» (пока переводчик подключён): она переводит одну строку с тем же контекстом и глоссарием и помечает её «Машинный». Редактор ищет переводчик сам, только если он уже работал в этом браузере, чтобы Chrome не спрашивал про локальную сеть без причины;
- флажок «Передавать описание и жанры игры как контекст» (по умолчанию включён) добавляет описание и теги игры в контекст проекта; название игры уходит всегда; Остановка отменяет задание в переводчике; повторный запуск продолжает с оставшегося. Непереведённые из-за ошибки строки (например, модель повредила плейсхолдер) пропускаются и видны в счётчике ошибок;
- переводчик по умолчанию закрыт для браузера. Адрес сайта вписывается в
allowed_originsегоconfig.toml(точный адрес, без*); для Chrome сервер отвечает на заголовок Private Network Access, а сам браузер один раз спросит разрешение на доступ к локальной сети. Страница настройки отличает «не запущен» от «запущен, но сайт не разрешён»; - код:
frontend/src/translator/(client.ts— API переводчика,run.ts— цикл перевода,MachineTranslate.tsx— панель,TranslatorSetup.tsx— страница настройки); проверки вfrontend/tests/translator-run.test.mjs.
Ограничения: перевод идёт, пока открыта страница редактора; контекст (кто говорит, соседние реплики) модели пока не передаётся, только текст строки; Safari может запрещать https-странице обращаться к локальному http.
Уже сконвертированные версии остаются без ID и без переводов, их нужно переимпортировать. Пока в библиотеке только тестовые данные, отдельную миграцию не делаем.
Этапы
- Фундамент (сейчас, до наполнения библиотеки): ID узлов и строк в
конвертере, импорт
tl/всех языков, локали в манифесте, выбор языка и запасные языки в плеере,_()/!t/_preferences.language. Миграция уже сконвертированных версий: ID детерминированно выводятся из текущего IR. - Перевод в библиотеке: главная версия, черновик и публикация, права, редактор перевода, прогресс, экспорт и импорт PO/CSV.
- Удобство: контекст сцены, «проиграть отсюда», глоссарий, рецензирование, шрифты и ассеты локалей, машинный предперевод.
- Моды: редактор скриптов, замена ассетов, слияние и перенос модов.