Как перевести игры на Unreal Engine
Игры на Unreal поставляют текст в скомпилированных таблицах .locres внутри контейнеров .pak или IoStore. Как их прочитать и почему на выходе — крошечный переопределяющий pak, а не пересобранная игра.
Игра на Unreal Engine выглядит как самое трудное для перевода, что есть у вас на диске. Никакой папки data, никаких файлов скриптов, никакого .json, который можно открыть, — только исполняемый файл, каталог Content/Paks с одним-двумя огромными файлами .pak и, может быть, пара контейнеров .utoc/.ucas. Хорошая новость в том, что нужный вам текст обычно спрятан внутри этих файлов не нарочно. У Unreal есть первоклассная система локализации, и большинство выпущенных игр ею пользуется. Ваша задача — найти таблицу, заменить строки и подсунуть движку небольшой файл, который имеет приоритет над оригиналом.
Эта последняя часть и делает Unreal приятным, как только в нём разберёшься. Вы не пересобираете игру на 60 ГБ. Вы делаете переопределяющий архив, измеряемый килобайтами, и кладёте его рядом с оригиналами.
Одна вещь перед шагами: поддержка Unreal в RuneTranslate — best-effort и новее, чем движки вроде RPG Maker или Ren’Py. Она подтверждена играбельной на реальных тайтлах UE4, но поверхность формата огромна, и каждая студия упаковывает по-своему. Проверьте экспортированную сборку в игре, прежде чем вкладываться в долгий прогон перевода.
Что на самом деле поставляет игра на Unreal
Unreal держит текст для игрока в файлах `.locres` — скомпилированной форме FTextLocalizationResource. По одному на культуру, по пути вида <Game>/Content/Localization/Game/ja/Game.locres, с соседями для en, zh-Hans и всего прочего, что выпустила студия. Даже у игры вовсе без переводов такой файл обычно есть — для той культуры, на которой её писали.
.locres — не текстовый файл и не простой список ключ/значение. Это бинарная таблица, устроенная как пространства имён, в каждом из которых лежат ключи, а каждый ключ указывает на строку. В современных версиях пространство имён и ключ хранятся как хеши (CRC32 в версии 2, CityHash64 поверх UTF-16 в версии 3), а не как читаемые имена, и сами строки живут в общей таблице поиска с дедупликацией и счётчиками ссылок. Среда выполнения никогда не ищет строку по её английскому тексту — она ищет её по паре пространство имён/ключ, которая вкомпилирована в Blueprints и C++ игры на этапе сборки.
Эта структура диктует единственный безопасный способ такой файл править. RuneTranslate сохраняет каждый хеш пространства имён, хеш ключа и хеш исходной строки байт в байт и меняет только значение, после чего пересобирает дедуплицированную таблицу строк и её счётчики ссылок. Ни один хеш не пересчитывается, поэтому и ключ, которого игра не найдёт, получиться не может. Переводы вольны быть длиннее или короче оригинала.
Эти файлы .locres затем упакованы в архивы .pak — их выдаёт магическое значение 0x5A6F12E1 в хвостовике в самом конце файла — или, в современных тайтлах на UE5, в контейнеры IoStore .utoc/.ucas.
Проверьте охват, прежде чем тратить час
.locres покрывает интерфейс, меню, системные сообщения, обучающие подсказки, названия предметов и навыков и субтитры — считайте, примерно 90% видимого текста типичной игры. Это та часть, которую RuneTranslate читает и записывает.
Чего он не покрывает: текст, вкомпилированный в DataTables или запечённый в приготовленные пакеты `.uasset`. Некоторые студии — особенно на небольших или сильно завязанных на Blueprint проектах — держат диалоги там, а не в системе локализации. Такие строки адресуются хешами внутри приготовленных пакетов, без читаемого оглавления, и чтобы прочитать их как следует, нужен файл соответствий .usmap, которого в выпущенной игре нет. Если ваша игра держит сценарий там, извлечение вернёт меню и кнопки и ни одного диалога, и RuneTranslate сообщит об этом как о явном результате «вне области», а не сделает вид, что всё получилось.
Полезно знать заранее: быстрее всего проверить — прогнать создание проекта и посмотреть, что вышло. Пара сотен коротких строк, все в духе Continue, Options, Are you sure?, означает, что локализован интерфейс, а сценарий — нет. Несколько тысяч строк, среди которых полные предложения, — значит, всё в порядке.
Ключ AES и почему это не стена
Изрядная доля выпущенных паков зашифрована AES-256 — а конкретно в режиме ECB без IV, поверх оглавления архива, а нередко и поверх отдельных записей. Без ключа архив непрозрачен: вы не сможете даже перечислить, что внутри.
Ключ восстанавливается по простой причине. Игра должна читать собственные архивы во время работы, на машине без сети и без сервера лицензий, — значит, ключ присутствует в поставляемом бинарнике как обычные данные. RuneTranslate сканирует исполняемый файл и его DLL на 32-байтовые окна, отсеивает их энтропийным фильтром (у настоящего ключа AES не меньше 25 различных байт из 32), делает дешёвую проверку на вменяемость — расшифровывает первый блок оглавления и смотрит, читается ли ведущее значение как правдоподобная длина строки, — а затем проверяет выживших по-настоящему: расшифровывает оглавление целиком и сравнивает его SHA-1 с хешем, записанным в собственном хвостовике пака. Этот последний шаг и отличает всё это от искателей ключей по ранжированию энтропии, которых вокруг полно: кандидат либо воспроизводит записанный самим архивом хеш, либо отвергается. Неверный ключ принят быть не может.
Некоторые игры хранят ключ текстом, а не сырыми байтами, некоторые разносят его по отдельным инструкциям в бинарнике; оба случая обрабатываются. Если ничего не сработало, диалог New project принимает вставленный ключ в hex (0x… или 64 шестнадцатеричных символа) или в base64, а восстановленный ключ кешируется на проект, чтобы вы не пересканировали 85-мегабайтный исполняемый файл при каждом экспорте.
UE5 IoStore устроен иначе, и извлечение точечное
Тайтлы на UE5 всё чаще поставляют контейнеры IoStore вместо обычных паков: оглавление .utoc в паре с блобом данных .ucas, со сжатием Oodle и собственным шифрованием. Наивный подход здесь — распаковать всё целиком, что на игре в 105 ГБ и подходом-то не назвать.
RuneTranslate встраивает retoc (под лицензией MIT) и просит его сконвертировать контейнеры в устаревший pak, отфильтровав только файлы локализации — эквивалент to-legacy -f locres --no-shaders --no-script-objects. Тайтл IoStore на 105 ГБ ужимается до нескольких килобайт .locres, за секунды, а не за часы. retoc берёт на себя и собственные AES с Oodle контейнера — поэтому игры на IoStore обходят стороной проблему Oodle, на которую могут наткнуться россыпные паки.
Важная половина всего этого в том, что IoStore усложняет только чтение. Запись универсальна, и о ней — следующий раздел.
На выходе — переопределяющий pak, а не пересобранная игра
Это та часть, которую ни одно конкурирующее руководство не объясняет как следует, и именно она делает перевод Unreal дешёвым, как только он вообще заработал.
Unreal монтирует pak-файлы в определённом порядке, и любой архив, чьё имя оканчивается на `_P`, монтируется последним. Побеждают поздние монтирования. Поэтому небольшой архив, не содержащий ничего, кроме вашего изменённого .locres, по тому же виртуальному пути, что использует базовая игра, просто затеняет оригинал — менеджер локализации грузит ваш и поставляемого не видит никогда.
- Ничего не надо переподписывать. Подпись паков — совершенно отдельный механизм на RSA, и в выпущенных потребительских тайтлах она проверяется принудительно редко; переопределение её не трогает.
- Ключ AES для вывода не нужен. Переопределение пишется без шифрования и без сжатия. Ключ нужен был только для того, чтобы прочитать оригинал.
- Никакой переупаковки игры. Базовые архивы не тронуты. Установка на 60 ГБ остаётся на 60 ГБ; ваш перевод — файл, измеряемый килобайтами.
- Это работает и для игр на IoStore.
.locresчитается через файловую систему pak, а не через загрузчик пакетов Zen, поэтому обычный россыпной_P.pakподхватывается даже игрой на UE5, все ресурсы которой лежат в.utoc/.ucas.
Устанавливается он копированием полученного _P.pak в <Game>/Content/Paks/ — ту же папку, где лежат оригиналы. Некоторые игры монтируют ещё и подпапку ~mods/, что работает не хуже. Убрать перевод — значит удалить этот один файл, поэтому откат тривиален, а тестировать безопасно с самого начала.
Шаги
- Установите RuneTranslate со страницы загрузки и войдите. При первом запуске нужен бесплатный вход через Patreon; бесплатный тариф открывает каждый движок и каждого провайдера — он ограничивает скорость и держит один проект за раз.
- Создайте новый проект и укажите его на корневую папку игры — ту, где лежат исполняемый файл и каталог
<Game>. Определение ищетContent/Paksи магическое значение в хвостовике пака либо.utoc/.ucas. - Если архивы зашифрованы, дайте отработать восстановлению ключа. Оно сканирует поставляемые бинарники и проверяет каждого кандидата по собственному хешу оглавления пака. Если ничего не нашлось, вставьте ключ в диалог.
- Дайте извлечению закончиться. Вы получаете записи
.locresдля исходной культуры, сгруппированные по цели локализации и готовые к просмотру. - Выберите провайдера и переведите. Короткие строки интерфейса и длинные диалоги можно направить разным провайдерам, если хотите тратить деньги только там, где это важно.
- Просмотрите, что вернулось. Строки меню — самое слабое место машинного перевода: у голого
SaveилиLoadнет контекста, а кнопка из двух слов, вернувшаяся предложением, вылезет за свой виджет. - Экспортируйте. RuneTranslate пишет один незашифрованный
_P.pakс вашим изменённым.locresпо правильному пути относительно движка. - Скопируйте этот файл в
<Game>/Content/Paks/и запустите игру.
Протестируйте после восьмого шага с горсткой переведённых строк, прежде чем гнать через провайдера всю игру. Если переопределение окажется проигнорированным — не та папка, не та культура, необычная схема монтирования, — узнать об этом лучше за пять минут, а не после оплаты 9 000 строк.
Плейсхолдеры и форматированный текст
Форматирование FText в Unreal использует {0}, {1} для позиционных аргументов и {PlayerName}, {Count} для именованных, плюс разметку <RichText> с закрывающими </> для встроенного оформления. Если провайдер перевода потеряет или испортит что-то из этого, игра покажет не слегка неправильное предложение — она покажет сломанную форматную строку, или аргумент молча исчезнет из фразы.
RuneTranslate маскирует каждое из них нейтральным плейсхолдером до того, как строку увидит провайдер, и восстанавливает после. Провайдер переводит вокруг маркеров, а не сквозь них, поэтому модель, решившая, что фигурные скобки похожи на опечатку, не сможет удалить аргумент. Если хочется пойти дальше и вовсе удержать конкретные строки от перевода, regex-фильтры исключения работают на любом движке, а глоссарий удерживает имена собственные одинаковыми по всей игре.
Ловушка с диакритикой — это баг кодировки, а не шрифт
Этот случай стоит задокументировать отдельно, потому что симптом указывает ровно не туда. Unreal сериализует строку с префиксом длины: положительная длина означает один байт на символ, отрицательная — UTF-16. Узкую форму движок выбирает, только когда каждый символ — 7-битный ASCII: его проверка отвергает всё выше 0x7F. Загрузчик устроен так же, поэтому, получив узкую строку со старшим байтом, он подставляет вместо каждого такого байта литеральный ?.
Практическое следствие: перевод, содержащий poción mágica и записанный в узкой форме, доходит до игрока как poci?n m?gica. А затронутая полоса — U+0080–U+00FF, то есть ровно буквы с диакритикой испанского, французского, немецкого, португальского, итальянского и скандинавских языков. Польский, турецкий, чешский, греческий, кириллица, японский и китайский лежат выше U+00FF, автоматически уходят в широкую ветку и не задеты никогда.
Значит, отпечаток такой: диакритика ломается в испанском, а с русским всё в порядке. Если вы это видите — это баг кодировки в том, что записывало файл, а не отсутствующий глиф в шрифте игры: отсутствующий глиф рисуется квадратиком или ничем, но никогда вопросительным знаком. В RuneTranslate был ровно этот баг, и он исправлен в 0.49.4; кодировщик теперь живёт в одном месте и пишет широкую форму для всего, что за пределами ASCII. Знать об этом стоит, потому что другие инструменты в этой области до сих пор ошибаются здесь.
Выбор провайдера
Трём из девяти провайдеров API-ключ не нужен вообще — Google, бесплатный DeepL и модели DeepL Classic/Next-gen, — и бесплатный тариф даёт вам их все. Для игры, состоящей в основном из интерфейса, бесплатного провайдера действительно хватает. Для тайтлов, насыщенных диалогами, LLM справляется с контекстом куда лучше; доступны OpenAI, Anthropic, DeepSeek, любая OpenAI-совместимая точка и локальная модель через Ollama или LM Studio. Сравнение провайдеров разбирает, кто в чём хорош и в чём плох, с реальными цифрами затрат.
Устранение неполадок
Игра запускается, но ничего не переведено
Почти всегда одно из трёх. Файл не в <Game>/Content/Paks/ — положить его рядом с исполняемым файлом не даёт ничего. Имя файла потеряло суффикс _P — тогда он монтируется в обычном алфавитном порядке и базовый pak может победить. Или игра работает в культуре, которую вы не переводили: она грузит .locres для той культуры, которую определила при запуске, поэтому игра, форсирующая en, проигнорирует переопределение, написанное только для ja. Сначала проверьте языковую настройку игры.
Извлечение падает на россыпном паке со сжатием Oodle
Россыпному .pak, сжатому Oodle, нужен распаковщик, который нельзя поставлять в комплекте. Если игра поставляет ещё и контейнеры IoStore, путь через retoc берёт Oodle на себя и всё в порядке. Если это только россыпные паки — пока это жёсткий стоп, о котором сообщается явно, а не тихим пустым результатом.
В игре нигде нет .locres
Значит, её текст вкомпилирован в приготовленные ресурсы, и этот путь до него не достаёт. Самый наглядный пример — крупный live-service тайтл на UE5, у которого оглавление IoStore сведено к хешам содержимого вообще без имён файлов: сотни тысяч чанков и ноль .locres. RuneTranslate так и говорит, вместо того чтобы перемалывать весь контейнер.
Определение вообще не срабатывает
Указывайте на папку, где лежит исполняемый файл, а не на сам Content/Paks и не на родительскую папку, полную других игр. Если и тогда не определяется — упаковка нестандартная, и о ней стоит сообщить: у формата pak есть версионно-зависимые раскладки хвостовика, и диалект, которого ещё никто не видел, — ровно то, что потом добавляют.
Честные ограничения
Поддержка Unreal — best-effort. .locres внутри россыпных и зашифрованных паков и внутри UE5 IoStore — проверенный путь, подтверждённый играбельным в игре на тайтле UE4 на испанском. Текст DataTable и приготовленных .uasset не покрыт. Россыпные паки со сжатием Oodle без пары в виде IoStore не поддерживаются. Паки с принудительной проверкой подписи переопределить нельзя, хотя на практике они редки. И поскольку формат так сильно меняется от версии движка к версии и от студии к студии, относитесь к первому экспорту как к тесту, а не как к результату.
В итоге у вас — играбельная переведённая сборка, которую вы держите у себя: оригинальная игра нетронута плюс один небольшой файл, удалив который, можно всё отменить. Если вы сравниваете движки или проверяете, покрыта ли ваша игра вообще, страница движка Unreal содержит технические детали, а все движки перечисляют семнадцать поддерживаемых форматов, включая Unity и RPG Maker. Если вы новичок во всём этом процессе, начните с общего руководства и вернитесь сюда за частями, специфичными для Unreal.
Готовы попробовать RuneTranslate?
Бесплатный тариф открывает все движки и всех провайдеров перевода. Supporter ($3/mo) открывает полную скорость.
Скачать для Windows
