show text _("...") and literal screen texts are listed in Names and menu; typing in search switches the filter from To do to All; the translator is told how stutters are spelled.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
23 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: <игра> (<исходный> → <язык>): контекст из описания игры, глоссарий из вкладки «Глоссарий», пожелания пользователя. Проект находится по имени и обновляется перед каждым запуском; - запуск живёт в провайдере выше страниц (
translator/runner.tsx): перевод идёт, пока вкладка открыта, даже если уйти из редактора; его карточка с прогрессом и кнопкой «Остановить» стоит в том же плавающем окне, что и загрузки. Одновременно работает только один перевод (на все игры и языки; между вкладками это держит Web Lock), пока он идёт, кнопки отдельных строк недоступны; закрытие вкладки браузер подтверждает. На каждой странице запуск записывается вlocalStorage(pg.translation.run: что переводится, где остановились, какое задание отправлено в переводчик). После обновления страницы сайт сам продолжает: забирает готовое задание из переводчика, сохраняет его и идёт дальше, а карточка и бейджи снова обновляются. Запись старше 30 минут не подхватывается; - автоперевод идёт по списку так, как его видит пользователь: текущая вкладка («Реплики» или «Имена и меню»), фильтр по файлу и поиск, сверху вниз. Берутся строки «не переведено» (и «устарело» по желанию) страницами по 40; страница уходит одним заданием, готовое сразу сохраняется, появляется в открытых строках и обновляет прогресс в шапке;
- список разбит на страницы (20/40/100/200 строк, номера страниц, переход по
номеру); сервер в
GET …/entriesпо параметруlocate=<ключ>возвращаетlocated, позицию строки в том же отфильтрованном списке. По нему режим «Следить за переводом» (включён по умолчанию, выключается кнопкой над списком, флажком в панели или ручным листанием) открывает страницу, на которой модель работает сейчас, и подсвечивает строки текущей пачки; - у каждой строки есть кнопка «Перевести» (пока переводчик подключён): она переводит одну строку с тем же контекстом и глоссарием и помечает её «Машинный». Редактор ищет переводчик сам, только если он уже работал в этом браузере, чтобы Chrome не спрашивал про локальную сеть без причины;
- вкладка «Имена и меню» собирает не только имена, пункты меню и
show text "…", но и строки_("…")из скриптов и постоянный текст экранов (text,textbutton,label): у них нет идентификатора реплики, поэтому они переводятся по тексту оригинала, как в Ren'Py. Список строится из уже конвертированной версии, повторный импорт игры не нужен; - декоративные теги Ren'Py (
{size=…},{i},{b},{color=…}и т. п.) внутри слов (d{size=38}e{/size}ep) после перевода на место не вернуть, поэтому такие строки уходят модели без этих тегов, а результат помечается «Неточно». Строки с тегами в других местах сначала переводятся как есть, а если переводчик отверг ответ (Protected tokens were changed…), переводятся ещё раз без декоративных тегов и тоже получают «Неточно». Служебные теги ({w},{nw},{p}),[переменные]и прочее остаются под защитой, такая строка при сбое просто не переводится; - флажок «Передавать описание и жанры игры как контекст» (по умолчанию включён) добавляет описание и теги игры в контекст проекта; название игры уходит всегда; Остановка отменяет задание в переводчике; повторный запуск продолжает с оставшегося. Непереведённые из-за ошибки строки (например, модель повредила плейсхолдер) пропускаются и видны в счётчике ошибок;
- переводчик по умолчанию закрыт для браузера. Адрес сайта вписывается в
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.
- Удобство: контекст сцены, «проиграть отсюда», глоссарий, рецензирование, шрифты и ассеты локалей, машинный предперевод.
- Моды: редактор скриптов, замена ассетов, слияние и перенос модов.