Мою игру на 7 языков переводит нейросеть

Мою игру на 7 языков переводит нейросеть

С Unity Localization у неё бы не вышло

В Hexardia (пошаговая тактика на гексах, Unity) семь языков: английский, русский, немецкий, испанский, французский, польский, турецкий. 470 файлов локализации, больше тысячи ключей. Агентства переводчиков нет, штатного локализатора нет, бюджета на них тоже нет. Черновой перевод делает нейросеть, я вычитываю два языка из семи, остальные ждут носителей.

Чтобы такая схема работала и не разваливалась, локализация в проекте должна быть удобной не только человеку, но и машине. Официальный пакет Unity Localization я не взял именно поэтому. Ниже объясню на пальцах, в чём была проблема, как она решена у меня, и честно перечислю места, где пакет Unity сильнее.

Проблема, о которую разбивается каждый новичок

Локализация в любом движке устроена одинаково: в коде живёт строковый ключ, в таблице лежит перевод. И у этой схемы есть врождённая болезнь: опечатка в ключе не ломает ничего. Компилятор молчит. Консоль молчит. Игра запускается. Просто на кнопке в турецкой версии вместо «Начать игру» красуется надпись Menu_Strat_Button, и узнаёшь ты об этом из отзыва в Steam.

Вторая болезнь приходит с ростом: дубли. Слово «Назад» появляется в меню, потом в инвентаре, потом в настройках, и каждый раз кто-то заводит новый ключ. Через год у тебя три разных «Назад», переводчик переводит их по-разному, и интерфейс выглядит так, будто его писали три человека. Его и писали три человека, просто все трое это ты.

Unity Localization эти проблемы частично решает, но тащит за собой три вещи, которые для маленького проекта хуже самих проблем.

Он требует Addressables. Это отдельная система сборки контента со своими группами, профилями и отдельным шагом билда. Появляется целый класс багов вида «в редакторе текст есть, в билде нет, потому что контент не пересобрали». Ты хотел перевести кнопку, а настраиваешь профили сборки.

Его таблицы строк лежат в .asset файлах с GUID вместо человеческих ключей. Два человека поправили одну таблицу, и мерж в гите превращается в разбор бинарной каши руками. Ревью перевода в пул-реквесте прочитать невозможно.

Его API асинхронный. Строку нельзя просто взять, её нужно ждать. Каждый виджет обрастает состоянием «а вдруг локаль ещё не подгрузилась».

Для студии с сотнями тысяч слов всё это оправдано. Для инди это налог без услуг.

Как у меня: файл, который читается глазами

Один источник, один JSON, одна строка на ключ:

{ "Key": "MenuBestiary", "Values": ["Bestiary", "Бестиарий", "Bestiarium", "Bestiario", "Bestiaire", "Bestiariusz", "Bestiyari"] }

Позиция в массиве равна языку. Никаких GUID, никаких Addressables, диффы в гите построчные, конфликт мержится глазами за секунду. API синхронный: попросил строку, получил строку.

А болезнь опечаток лечится атрибутом. Поле с ключом в инспекторе помечается так:

[LocalizationKey("UICommon")] string Key;

И вместо текстового поля Unity рисует выпадающий список существующих ключей с живым превью текста. Если ключ в источнике отсутствует, инспектор пишет NOT IN SOURCE красным. Опечатка стала видимой в момент, когда её делают, а не через месяц в турецкой версии.

Главное: с этим может работать нейросеть

Теперь то, ради чего всё затевалось. Правки локализации у меня делает агент, и для него в проекте есть набор операций с защитой от его же ошибок.

Операция index сваливает весь корпус в один файл: 1050 ключей со всеми переводами. Модель читает его целиком вместо того, чтобы открывать 470 файлов по одному. Вопрос «есть ли уже такой ключ и как это сказано по-польски» решается одним чтением.

Операция find ищет строку по всем источникам до того, как агент заведёт дубль. Реальный случай: попросил добавить ключ «Leave», система ответила, что таких уже восемь, выбирай любой. Дубль номер девять не родился.

Операция add_key принимает переводы по именам языков, а не по позициям в массиве. Пропустил язык, значит туда встанет английский, и строка никогда не окажется короче положенного. Перепутать колонку немецкого с испанским машина физически не может.

Операция usages находит все сцены, префабы и скрипты, где ключ используется. А операции удаления нет вообще, сознательно: ключи торчат из сцен, и безопасно удалять их автоматом нельзя.

И одна деталь, которой я горжусь. Первая версия записи переформатировала файл под себя, и добавление одного ключа давало дифф в 289 строк. Ровно та болезнь, за которую я ругал таблицы Unity, только теперь моя собственная. Переписал: файл сохраняется в том стиле, в котором был написан. Дифф на один ключ стал две строки.

Итог по рабочему циклу: вечером кидаю агенту список новых строк, он раскладывает их по источникам на семи языках, валидатор проверяет корпус, утром я вычитываю русский и английский. Вычитка носителями остальных пяти впереди, но черновик уже стоит в игре и работает.

Бонус, которого нет даже у больших: честные склонения

Классика жанра: «Перезарядка: {0} ход(ов)». Эти скобочки пишет каждый, чей движок не умеет склонять. У меня формы лежат в самом значении через разделитель:

"Осталось: {0} ход|Осталось: {0} хода|Осталось: {0} ходов"

Правила настоящие, с граничными случаями: 11 ходов, но 21 ход. Польский на 21 уходит в третью форму, в отличие от русского. Французский считает ноль единственным числом. Турецкий после числительного множественное не употребляет вовсе. Ключ без разделителя работает как раньше, поэтому мигрировать 470 старых файлов не пришлось: склонения включаются только там, где число стоит рядом со словом.

Валидатор это тоже караулит: если хоть один язык завёл формы, он проверит, что русский и польский дали три, а остальные две.

Мою игру на 7 языков переводит нейросеть

Где Unity Localization честно сильнее

Чтобы пост не выглядел рекламой самому себе, перечислю слабые места без скидок.

Пер-локальные ассеты. Пакет умеет подменять по локали шрифты, спрайты и озвучку. У меня конвейер чисто текстовый, шрифт один на всех. Покрытие букв, правда, уже проверяет отдельный инструмент: он собирает каждый символ из всех 470 файлов и спрашивает у каждого шрифта, отрисует ли тот его. Именно он нашёл, что в моём декоративном шрифте нет польской диакритики и польская версия рисовала квадраты. Квадраты я вылечил фолбэком, но факт остаётся: у пакета эта зона продумана лучше.

Память. Пакет грузит только активный язык. У меня все семь языков живут в памяти всегда. На моих объёмах это копейки, но растёт линейно.

Обмен с переводчиками. У пакета из коробки XLIFF. У меня только CSV туда и обратно: весь корпус выгружается одной таблицей на 1470 строк, агентство правит ячейки, импорт кладёт изменения по файлам и отбрасывает строки с несуществующими ключами. Для агентства этого хватает, для интеграции с TMS-системами перевода нет.

Формат позиционный. Вставить новый язык в середину списка нельзя, только дописать в конец: иначе все переводы съедут на колонку. Валидатор эту катастрофу ловит первой же проверкой, но само ограничение никуда не девается.

Smart Strings у пакета богаче моего разделителя: род, падежи, вложенные условия. Мне хватает форм множественного числа, но потолок у меня ниже.

Вердикт

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

И главное, это решение без замка на двери. String Tables пакета импортируются из CSV штатно, кнопкой в редакторе. Моя выгрузка выдаёт ровно такую таблицу: строка на ключ, колонка на язык, 1470 строк. Если проект дорастёт до агентства и озвучки, переезд на пакет займёт один экспорт и вечер раскладывания по таблицам. Обратное неверно: вытащить тексты ИЗ пакета в простые файлы сильно больнее, чем занести.

Поэтому максимальный выигрыш ровно на маленьком масштабе, и риск этого выбора почти нулевой. Начинаешь на JSON, получаешь все удобства маленькой команды, а дверь в пакет держишь открытой одной кнопкой экспорта. С ростом проекта чаша весов сползает к пакету, и это нормально: к тому моменту у тебя будут деньги на агентство и человек, который нажмёт эту кнопку.ww

4
3
2