Перевод «Легенды о Дряньке»

Переводы

Около 18 ноября 2015 года я узнал о том, что переводу «Легенду о Дряньке» нужна помощь. Спустя неделю, я написал на коленке небольшую программу, чтобы извлечь неизвлекаемое оставшиеся неизвлечёнными тексты из игры, и теперь у нас (команды перевода) на руках всё, что необходимо перевести.

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

Сама игра сделана тоже более или менее на коленке, но, как по мне, это придаёт ей только свой шарм. Игры, которые мне не нравятся, меня переводить не тянет. Так что...

Этот пост существует для двух целей:

  1. Если вы владеете английским языком на достаточном уровне и игра вам интересна (скачать полную бесплатную русскую версию можно в разделе «Игры» на официальном сайте студии «Локатор»), я приглашаю вас принять в нём участие. С тем, как именно это технически происходит, разбираться не нужно — мы найдём, как наиболее удобно предоставить текст, и заберём его обратно для вставки.
  2. Сюда я буду писать технические детали перевода. Не организационные (темы есть, но обсуждать это очень долго), и не собственно переводные. Под катом — оно самое.

Сама игра сделана в RPG Maker 2003, и тексты соответственно хранятся в обычной однобайтовой кодировке КОИ-8. На само начало этого перевода Тенну сподвигла такая находка как Dream Maker — программа для извлечения и зашивания обратно текста проектов RPG Maker'а. Исходники программы открыты, и даже есть документация, но, по иронии судьбы, всё это на португальском. Эта программа смогла извлечь из проекта ЛоД всё, кроме двух вещей — текстов локации Озера Желаний и текстов базы данных с умениями, предметами, и прочими важными вещами. Выдаёт какую-то ошибку от фонаря про деление на ноль, и вот поди разбери там этот Руби.

Для извлечения этих текстов я написал экстрактор, который знает коды всех кириллических символов, находит цепочки таковых символов длиной более трёх, и выводит это всё в файл.

Для того чтобы потом зашить переведённые тексты в файл, я вскоре напишу другую программу, которой на вход подаётся файл1 — с исходным текстом, и файл2 — с переведенным текстом. Далее программа читает оба файла в словарь (благо в C# есть встроенный удобнейший класс Dictionary), а вот дальше есть одна проблема, которую я как раз придумал как решить:

Если просто последовательно (побайтово) заменять все тексты слева (оригинальные) на тексты справа (переведённые), то может попасться такая ситуация:

НетNo
ИнтерНетInterNet

Простой алгоритм заменит Нет на No, а цепочки байт соответствующей слову «ИнтерНет» он на следующем шаге уже вовсе не найдёт — это будет «ИнтерNo»! Пример из головы, но аналогичную ситуацию в ЛоД представить нетрудно (например, имя «Катя» и обращение «Катя» внутри диалога — в диалоге заменится только это имя, если «Катя» будет в списке раньше). Разбираться с последствиями подобных замен я не собираюсь — ведь я могу предусмотреть это заранее.

И вот, после раздумий над этим, возникла мысль — список слов на замену следует упорядочить по длине строк, начиная с наибольшей длины. В таком случае сначала будет заменяться, условно говоря, «абракадабра», потом «кадабра», и только потом «абра».

Вообще, в простом ROM-хакинге используют оффсеты, то есть смещения закодированных символов относительно начала файла. Но я рассматриваю ситуацию перевода произвольного исходника, который менялся сколько угодно раз.

Пока это всё, как напишу новое — покажу.

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

Похожие статьи

  • Что делает инди-игру хитом? Как выбрать верный дизайн (by Ryan Clark)
    Кто ты, черт побери, такой и в чем компетентен, чтобы что-то мне рассказывать? В этой индустрии трудно уйти далеко, если не учиться у других. Но у кого нам лучше учиться? Думаю,...
  • The Scratchware Manifesto
    Попал я как-то раз на некий текст под названием «The Scratchware Manifesto». Не помню в каком году — может в 2010, может в 2012. И очень он уж меня зацепил. Хотелось бы мне его ...
  • GET LAMP — документальный фильм об Interactive fiction
        Фильм отличный. Colossal Cave Adventure, Don Woods, Zork, Infocom, Скотт Адамс, Стив Мерецки, извечные лабиринты, дизайн загадок, проблемы парсера, сообщество хоббиистов,...
  • Восход и Падение Одинокого Разработчика Игр
    Это перевод статьи с официального разрешения автора, кое я получил сегодня ночью. Вообще говоря где-то на Хабре есть перевод этой статьи, но когда я пригляделся к переводчику,...
17 комментариев
AnnTenna

Внезапно спасибо)) Заодно узнала некоторые технические подробности, о которых не догадывалась) 

А вообще присоединяюсь — помогите кто может с переводом, пожалуйста!

buntarsky
файл1 — с исходным текстом, и файл2 — с переведенным текстом. Далее программа читает оба файла в словарь
Простой алгоритм заменит Нет на No, а цепочки байт соответствующей слову «ИнтерНет» он на следующем шаге уже вовсе не найдёт — это будет «ИнтерNo»!

список слов на замену следует упорядочить по длине строк, начиная с наибольшей длины

То есть ключом будет слово? Пословно сопоставить два предложения на разных языках практически невозможно. «How are you?» — «Как есть ты?». Значит, ключами будут целые предложения. Поэтому сортировать по длине бессмысленно. И не только поэтому. Ведь ключи в словарях ищутся с полным соответствием (а-ля «whole word»+«case sensitive»). То есть проблема МеждуНет-а не возникает вовсе. Не морочьте людям голову.

Xitilon

Ключами будут цепочки байт, соответствующие фрагментам текста — в одних случаях это будут слова, в других — что-то большее. Там я под списком слов имел в виду список цепочек байт нужной кодировки, это я ошибся чуть, да.

Программа по моему алгоритму проходит файл с текстами от начала до конца. Как только в потоке будет обнаружена последовательность байт, существующая в левой части словаря, она будет заменена на правую часть словаря. Проблема с МеждуНетами будет, потому что на каждом конкретном шаге проверяется, есть ли в словаре текущий набор байт=символов. Таковой может быть найден в словаре, хотя алгоритм ещё не дочитал непрерывную цепочку нужных символов до конца. Вот в этом месте мне нужно ввести флаг конца чтения, и отбрасывать все совпадения, которые получены не на конце цепочки. Тогда действительно не нужно ничего пересортировывать, но моя ошибка была не в том, что я якобы не знаю как работает словарь, а в том, что я не придумал более адекватный алгоритм на тот момент.

Вот теперь придумал.

buntarsky
на каждом конкретном шаге проверяется, есть ли в словаре текущий набор байт=символов. Таковой может быть найден в словаре, хотя алгоритм ещё не дочитал непрерывную цепочку нужных символов до конца. Вот в этом месте мне нужно ввести флаг конца чтения, и отбрасывать все совпадения, которые получены не на конце цепочки.
Старею. Мне бы и в голову не пришло, искать в словаре недочитанный ключ после каждого считанного символа. Ведь в качестве «флага конца чтения» можно использовать естественный разделитель слов в предложении — пробел. А этого достаточно, чтобы проблема «не-whole word» не возникла. Но возникла бы упомянутая проблема невозможности точного сопоставления слов. И, кстати, еще дублирующихся ключей. Но ок, мы оба согласились, что слова-ключи не годятся, что нужны строки-ключи. Которые, к тому ж, должны разделяться неким специальным разделителем.
И здесь мы плавно подходим к
моя ошибка была не в том, что я якобы не знаю как работает словарь
Без обид, вы сами так сформулировали. Вот ответьте себе на вопрос, как собираетесь заполнять словарь переводом из второго файла? То есть как определите какой строке оригинала какая сторока перевода соответствует? Уверен, будете считать разделители. Ну пусть все будет даже в одном файле — оригинал и перевод в шахматном порядке. Очевидно, как ни крути, а связь эта позиционная. И зачем тогда такие сложности как словарь? Почему не массив?
Если (не)понятно о чем я, алгоритм такой:
1. экспортируем оригинальный текст с разделителями, без которых все равно не обойтись, в текстовый файл;
2. переводим, разделителей не трогая;
3. читаем полученный файл в массив: каждый элемент есть строка до следующего разделителя;
4. импортируем с заменой переведенный текст из массива поэлементно («разделители» в оригинале берутся по тому же принципу, что и в пункте 1).
Не за что.
Xitilon

Словарь я собираюсь заполнять очень просто.

Читаю строку из левого файла. Читаю строку из правого файла. Создаю новую пару ключ+значение из левого+правого.

Перевод строки и есть разделитель, и считать их не требуется.

А почему словарь — потому что в C# словари сопоставляют между собой произвольные типы данных, а строки по стандарту в Юникоде, а не в КОИ-8. Я мог бы написать dictionary[«Vasya»] и получить «Вася», но так оно не сработает. Мне нужно отображение множества ключей типа byte[] во множество значений такого же типа. И тогда я смогу получать перевод для заданного фрагмента, подставляя dictionary[массив_считанных_байтов]. Без словаря, с массивами (ступенчатыми или прочими) мне пришлось бы писать собственную функцию поиска подпоследовательности байт в каждой последовательности существующих исходных текстов в виде байтов. Сигнатуры короче уже, а не строки.

buntarsky
Читаю строку из левого файла. Читаю строку из правого файла. Создаю новую пару ключ+значение из левого+правого.
Что я и сказал, только другими словами. Еще раз. Строки между всеми файлами с текстом связаны позиционно — последовательностью их чтения/записи (порядком нахождения некого «разделителя»). Первая по счету строка из экзешника — первая в файле с оригинальным экспортированным текстом — первая в файле с переводом — первая итерация в цикле читающем/пишущем строки в любой из файлов. А порядок чтения строк (разделителей) соответствует положению (индексу) в массиве. Последовательно обходим массив — последовательно «смещаемся» в файле относительно текущей итерации цикла (индекса массива). Будь это чтение или запись. И не нужно сто раз искать по словарю, бояться дублей ключей и заморачиваться с сопоставлениями текстов со всеми вытекающими проблемами с их кодировками и, бог весть, чем еще. Положение в массиве — это и есть наш безымянный ключ.
Без словаря, с массивами (ступенчатыми или прочими) мне пришлось бы писать собственную функцию поиска подпоследовательности байт в каждой последовательности существующих исходных текстов в виде байтов. Сигнатуры короче уже, а не строки.
Как вы экспортируете/импортируете данные из исполняемого файла не зависит от того, в каком виде вы храните их в памяти программы (от типа элемента массива). Будь это хоть строки, хоть байты, хоть массивы символов, как душе угодно, массив всего лишь служебное место в памяти для промежуточного хранения читаемых/пишущихся данных между файлами. Вы в программе не выполняете никаких действий по модификации этих данных, кроме простой замены (что не страшно для однотипных данных), а только их пишете/читаете. Значит, программе фиолетово какой у них там «формат».
Xitilon
Что я и сказал, только другими словами.

В общем-то это то, что я задумал, ещё до того, как это было сказано в первый раз (не мной). Просто в посте я этого не озвучил.

Значит, программе фиолетово какой у них там «формат».

Всё так, совершенно верно. Но вот мне — не фиолетово, писать длинный код, или короткий. С парой массивов массивов работать сложнее, чем со словарём, сопоставляющим 1-мерные массивы 1-мерным массивам. Возможно, это проще в других языках другим людям. Мне, в моём привычном Си Пошарпанном под мастдайной платформой .NET — проще так.

Я знаю, что делаю, и всё как раз так, как должно быть. Но вообще, приятно общаться с разбирающимся человеком.

buntarsky
С парой массивов массивов работать сложнее, чем со словарём..
Массив будет один. Так как вместо сопоставления мы используем индекс, то хранить в памяти перед записью потребуется только перевод. Оригинал текста нужен только переводчику и, как я понял, он получается сторонней утилитой.
Вряд ли есть большая разница в сложности работы со стандартными для языка контейнерами, но в этой конкретной задаче массив будет проще словаря алгоритмически. Чтение/запись файла и обход массива — это все последовательные операции. Словари же хранят данные позиционно-независимо (оптимизированное под поиск дерево), и не обходятся поэлементно. Это потенциально усложнит код.
Я знаю, что делаю, и всё как раз так, как должно быть. Но вообще, приятно общаться с разбирающимся человеком.
Это такая вежливая форма слова «заколебал ты своим очень важным и нужным мнением»? Ну, ок. :)
Xitilon

Да нет. Просто люди с таким уровнем понимания на Коленке исчисляются очень малым количеством. Может, 3.

Так как вместо сопоставления мы используем индекс, то хранить в памяти перед записью потребуется только перевод.

Это как так?

Словари же хранят данные позиционно-независимо (оптимизированное под поиск дерево), и не обходятся поэлементно. Это потенциально усложнит код.

Чем усложнит, если код самих словарей уже написан до (за) меня? Если словарь содержит текущую нужную нам цепочку, то заменить на значение по такому ключу. Вот и весь код замены. Или о каком коде речь?

В любом случае, я пока спать пошёл.

buntarsky
Это как так?
Мой первый алгоритм описывает как это можно сделать в четыре шага. В зависимости от того, как реализован импорт/экспорт (например, он не хранит никакой мета-информации) может, теоретически, понадобится дополнительный массив для сопоставления позиции в текстовом файле и позиции (смещения от начала) в исполняемом файле (все по тому же принципу), но в этом случае один словарь тоже бессилен, он тоже не знает смещений, по которым писать.
Чем усложнит, если код самих словарей уже написан до (за) меня?
Код массивов тоже написан. Ничего больше не потребуется. В общем случае, словарь ни легче, ни сложнее.
Но при заполнении словаря возможно придется решать проблему с ключами-дублями. Массив же возвращает строки в том же порядке, что и взял их — ему безразличны дубли.
Плюс, это быстрее (взять элемент массива, а не искать его по ключу в словаре-дереве), экономнее к памяти (нам не нужно хранить ключи, ими служит положение элемента в массиве, то есть в памяти). И наконец, это просто логичнее. Последовательно читаем/пишем файл — последовательно берем/кладем строки. Используем то, что больше подходит к задаче и не создает вредных побочных эффектов.

Про побочные эффекты. Ключи-дубли маловероятны, учитывая, что для наиболее «опасных» коротких слов перевод тоже будет обычно совпадать, но не исключены. Вот пример. У нас где-то в тексте разбросаны три строки с текстом «OK» (надписи на кнопках). В одном случае переводчик перевел это как «Ок», в другом — «Да», в третьем — «Хорошо». Программа читает из словаря по ключу «OK», что там окажется? Правильно, последний вариант. Первый и второй просто «исчезнут» — будут перезаписаны третьим. В финальном тексте в некотором месте переводчик ожидает «Да», а там «Хорошо». Магия. Тут программист может сразу не догадаться, что произошло, что уж говорить о переводчике, для которого это будет полной неожиданностью. И так для любых ключей — одинаковых строк оригинала. Особенно для коротких — однословных.
buntarsky
Xitilon
Да, схема по делу. Я даже сначала хотел возмутиться, что строки в формате БД RPGM вовсе не нуль-терминированные, но сумрачный гений обозначил через /0 просто любой делимитер, чем лишил меня этого права! А вот теперь я заметил \n, но и там всё верно
buntarsky

Ну я и слэши развернул, мол, это не «нуль-терминатор» и «перевод строки», а некая абстракция разделителей.

Только заметил, что при экспорте в png потерялось: «w — write operation». Но думаю, это и так понятно.

И конечно, не обязательно писать перевод в оригинал, можно и в его копию. Алгоритм никаких ограничений не накладывает, так как открывает бинарный файл (или файлы) дважды: отдельно при экспорте (чтение), отдельно при импорте (запись). Это верно и для текстового файла, их может быть два: один с оригинальным текстом и другой с переведенным.

Xitilon

Код массивов тоже написан. Ничего больше не потребуется. В общем случае, словарь ни легче, ни сложнее.

В общем — возможно, хотя я бы так не говорил. Уместней было бы разделить ЯП на два класса — в которых эта задача более удобна, и остальные. Но я-то не от того танцевать начинаю, у меня есть и алгоритм и выбранный язык.

По поводу экономии памяти и процессорного времени — сам C# прожорлив по обоим параметрам по сравнению с плюсами, и я это знаю. Но в 2015 году, для небольшой задачи (ну 10000 замен, вряд ли жто прям «много») такая оптимизация больше напрягает меня при разработке чем конечного юзера при использовании — время работы проги даже если и будет 10 секунд, то это куда быстрее чем мне переписывать и тестировать логику. А так-то верно.

Про дубли — да, это момент для раздумий. И хотя что-то мне подсказывает, что «ОК» с разным переводом это скорее вырожденный случай чем то чего стоит ожидать, особенность конкретно этого перевода в том что он коллективный и (по крайней мере сейчас) не очень контролируемый (ох, дойдут ещё туда мои руки, дойдут). Но есть ещё одна беда, если считать бедой трудность сохранения разного перевода одного и того же слова/фрагмента, что само по себе смахивает на искажение, а не на перевод… Суть моего подхода в том, что импорт текста должен быть «портируемым» — не зависеть от порядка последовательности строк! Соответственно, я планирую проходить весь файл для каждой пары заменяемых строк.

buntarsky
смахивает на искажение, а не на перевод
Некоторые переводчики называют это «адаптацией». И они по своему правы. В любом случае, лучше перебдеть, чем недобдеть.
Суть моего подхода в том, что импорт текста должен быть «портируемым» — не зависеть от порядка последовательности строк!
«Портируемым» быть не получится. Не зависеть от порядка строк — зависеть от их содержимого, что еще и черевато побочными эффектами.
К тому ж, когда игру переводят, она уже обычно готова и не меняется. Но если и изменится уже после экспорта оригинального текста, то мы легко можем повторно этот текст экспотрировать, а затем подифать-смержить со старым переводом. То есть со стороны переводчика рабочий процесс не меняется.
Xitilon

подифать-смержить со старым переводом

Вот это действительно что-то, что мне в голову не приходило, спасибо.

А какие побочные эффекты, кроме того самого ИнтерНета (и эту проблему я уже решил)?

buntarsky
Дубли еще.
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.