Описывается методика автоматизированного перевода интерфейса на другой язык(читать далее)Как правило, необходимость перевода интерфейса возникает, когда не ждёшь. Если вы в своей программе каким-либо образом это уже предусмотрели и её использовали, вам будет просто. А вот даже если вы предусмотрели, но использовали только "кое-где", начнётся геморрой. И чем больше программа, тем больше проблем.
Некоторые среды разработки предоставляют свои функции для этого. Можно ещё воспользоваться, например, gettext, но когда надо было мне, я про неё забыл. Хотя помнил основные принципы. Из чего и родилось нечто.
Итак, у нас есть программа под винду на C++. Часть строк хранится в тексте программы, часть в файле ресурсов. Вообще, ресурсы имеют код языка и могут быть размещены в dll, поэтому, если бы разработчик думал в самом начале, можно было бы свести перевод к переводу текста ресурсов. Но разработчик не подумал и часть строк для перевода в файле ресурсов, а часть в исходном тексте.
Итак, задачи:
1. Обработать файл ресурсов (ну, это легко)
а) Выдрать строки из файла ресурсов в отдельный файл
б) переведённый строки запихать на свои места
2. Обработать все файлы с исходными текстами
Можно было бы сделать точно так же, но тогда количество файлов для перевода будет равно числу файлов с исходными текстами. А даже если сливать всё вместе, то будет трудно понять, какие строки откуда.
Сделаем следующим образом:
а) каждой строке сопоставим некий идентификатор
б) идентификатор положим как #define в специальный заголовочный файл (который потом будет включён во все файлы с исходными текстами)
в) все текстовые строки заменяем на tr(ID)
г) сами строки складываем в файл языка, где содержатся строки вида ID="string"
д) создаём файл с исходным текстом, в котором содержатся три функции -- загрузить язык, выгрузить язык и выдать фразу по идентификатору ( tr(ID) ).
Как сформировать имя идентификатора, чтобы было хоть что-нибудь понятно? В исходной строке все латинские буквы становятся заглавными, цифры остаются без изменений, все остальные символы заменяются на нижнее подчёркивание ("_"). Чтобы не думать по поводу первого символа, который не может быть цифрой, "_" можно добавлять в начало автоматом (я просто сделал, что кавычки участвуют в формировании названия, поэтому мои идентификаторы и начинаются и заканчиваются на "_".
Итак:
В исходном тексте:
"string" -> tr(_STRING_1_)
В заголовочном файле:
#define _STRING_1_ 1
В файле языка:
_STRING_1_="string"
Тут можно переводить уже.
Пример реализации:
pastebin.com/Uw2FRVcm p_trans.cpp -- программа автовыдирания и автозамены строк (см. примечания)
pastebin.com/A3CKqi5b translation.cpp -- "движок", лол (см. примечания)
pastebin.com/W2JYRZ3k translation.h -- заголовочный файл, надо включить во все файлы проекта
pastebin.com/EBWX3qiW tl.h -- пример файла с определениями строковых констант
Примечания:
1. Данный пример реализации очень сырой. Многое не работает или неизвестно, работает или нет.
2. В начале работы программы (чем раньше, тем лучше) следует вызывать LoadLanguage, а в конце (или при переключении языков) -- UnloadLanguage.
3. translation.h включается в StdAfx.h, а если его нет (отключены pre-compiled headers или вообще не студия, то надо раскомментировать в p_trans.cpp строки, добавляющие включение этого файла во все файлы исходников.
4. После работы программы p_trans исходники будут безвозвратно изменены. Необходимо сделать бэкап.
5. Если в текстовых строках больше двух обратных слэшей подряд, они заменяться не будут (т.е. "\\" заменяется на "\", "\\\\" заменяется на "\\", а остальное нет).
6. p_trans затрагивает инициализацию массивов символов строкой-константой и заменяет на инициализацию при помощи функции. Это не работает. Если у вас есть места с инициализацией массивов строкой-константой, замените в этих местах вызовы на оригинальные строки вручную, на вызов strcpy или на подобное:
#define SOME "some"
char t[10]=SOME;
Такое заменяться не будет, т.к. все директивы препроцессора пропускаются.
7. Вообще это поделка, используйте gettext.
Пожалуйста, ознакомьтесь с комментариями!
@темы:
Программирование,
Говнокод,
Статьи
12.04.2012 в 10:52
Вторая проблема сложнее. Автоматический фокус, который ты описываешь, плох по ряду причин; его можно доводить до ума, но я считаю, что он всё равно никогда не будет достаточно хорош.
Во-первых, строки в коде программы дублирются. Автоматика никогда не будет точно определять, какая пара строк - дубль одной, а какая - одинаковые разные (по смыслу).
Во-вторых, автоматически сгенерированные идентификаторы всегда будут хуже даже корявых корейских "IsShouldFileOverwrite". А ведь со всеми этими __TEXT_SHOULD_OVERWRITE_FILE__1_ придётся жить, переносить их с места на место, придумывать (уже самому, а не автоматом) новые такие же... Нет, это слишком некрасиво.
В-третьих, строки не всегда генерируются простым образом. Строка может состоять из нескольких кусков, например: "Info: "+IntToStr(sprCount)+"sprites"\
"were drawn on"\
"the" + canvasName + "canvas".
Очевидно, что тупой метод вынесет даже кусочек "the" в отдельный ресурс, и даже умный метод всё равно не заменит это чудовище на удобный sprintf.
В чётвёртых, отличия всегда существуют не только в языке. Валюта пишется слева, а не справа, летоисчисление ведётся с рождения пророка Мухаммеда, строки пишутся справа налево, календарь считается с воскресенья, и ещё тысячи, тысячи мелочей. Я переводил программу на арабский и знаю.
В общем, моё решительное мнение - вводить в программу поддержку нескольких языков можно ТОЛЬКО вручную. Всё остальное - это как книгу промптом переводить, а потом руками дорабатывать. Ничего хорошего из этого не получится никогда.
Теперь что касается правильных методов. Это тема очень интересная. Хотелось бы найти такой способ локализации, чтобы и код читался нормально (без __TEXT_SHOULD_OVERWRITE_FILE__1_), и локализовывать было просто.
В винде стандартные механизмы локализации, на самом деле, очень неплохие. Главное, чтобы работу с ними поддерживал компилятор, тогда у нас не просто торт, а торт с вишенкой. Например, в Delphi можно сделать так:
resourcestring
sShouldOverwriteFile = "File already exists, should I overwrite it?"
...
MessageBox(..., sShouldOverwriteFile, ...);
Это, однако, не всегда возможно и не всегда удобно, а в некоторых языках не поддерживается. Поэтому часто делают вот так:
MessageBox(..., _s("ShouldOverwriteFile"), ...);
Где функция _s ищет строку с указанным текстовым идентификатором в файле перевода. Преимущества: файл строк с самого начала текстовый, его легко переводить и автоматическими средствами сравнивать, что во всех переводах один и тот же набор строк. Кроме того, устраняется лишний слой "номеров ресурсов" с дефайнами, а поиск по строковому идентификатору, в общем-то, лишь в пару раз медленней числового (несущественно).
Недостаток всех этих схем: что, если вообще все ресурсы пропали? И арабские, и английсские, и русские. Что показывать?
Функция _s() может, в конце концов, показать идентификатор "ShouldOverwriteFile" - это лучше, чем ничего, ведь он как-то похож на обозначаемую им строку. Конечно, пользователь ничего не поймёт, но программист сориентируется, где искать ошибку.
Интереснее сделали в Вордпрессе. Там рассудили: раз всё равно используется текстовый идентификатор, почему не сделать этим идентификатором саму строку в базовой локали?
MessageBox(..., _s("File already exists. Should I overwrite it?"), );
И если подумать, это очень неплохое решение. Скорость сравнения строк - затрата копеешная, зато в программе, во-первых, всегда будет базовая локаль, и во-вторых, практически не падает качество чтения кода. Плохо, конечно, то, что случайный лишний пробел в строке незаметно лишает её всех переводов.
12.04.2012 в 11:10
> Я переводил программу на арабский и знаю.
Жеесть!
Часто о возможности локализации в начале не задумываются, а потом приходится всё разгребать. При этом не задумываются одни, а разгребают другие, иногда.
Хочу отметить, что при исходной английской локали читаемость __TEXT_SHOULD_OVERWRITE_FILE__1_ ненулевая.
В моём варианте файл перевода тоже текстовый, но с ресурсами приходится разбираться отдельно, так что придётся вручную добавить для каждой кнопки SetWindowText, если будет необходимость полного переключения на лету без перекомпиляции.
Да, что при использовании gettext при изменении исходных строк могут пропадать переводы я не подумал. Спасибо за указание на подводный камень в речке, к которой я только подошёл (ибо у меня стояла одноразовая задача; но грядёт многоразовая, и там я уже подумаю получше и подольше).