Локализация cpp-шного проекта
May. 5th, 2008 11:59 am![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
(интересно только программистам)
Вот, встала тут такая задачка. В очередной раз, не знаю, уже, в какой. Дан проект средних размеров - 200 с лишним cpp-шников. В нем - куча строковых литералов (не Unicode) на английском. Требуется сделать по крайней мере двуязычие (rus+eng).
Задачку я уже решил, но так получилось, что "на выходе" образовались кое-какие полезные идеи, которыми хочу поделиться. Вдруг кому-нибудь пригодится.
Upd:
Все плюшки здесь
Пришлось напейсать программу-локализатор, которая автоматизирует большую часть процесса.
"Ручной труд" применялся, в основном, на начальном этапе - когда надо было определиться с тем, какие строки подлежат локализации, а какие - нет. Я просто пометил все такие строки уникальным идентификатором LS. Например, вместо "&Cancel" стало LS"&Cancel". Потом определил в проекте
- чтобы программа с этими пометками нормально компилировалась.
Думаю, и эту часть работы можно было частично автоматизировать - отметить префиксами вообще все строки, содержащие латинские буквы, а потом только убирать ненужные префиксы. Это дает меньший объем работы, чем обратная операция - расстановка префиксов. Наверное, я добавлю в локализатор и такую фишку - ради достижения гештальта :)
Дальше я возился с локализатором, который потом выполнил всю грязную работу:
1. Вытащил список исходников из проекта Visual Studio.
2. Просканировал их на предмет помеченных строк.
3. Собрал все эти строки в один "файл локализации", имеющий такой формат:
Строки, которые оказались одинаковыми, объединяются в одну.
В этом месте были две "приятности", которой не было в прежних вариантах локализации, с которыми я имел дело.
Во-первых, "строка" пишется as is - со всеми переводами строк, табуляциями и даже нулевыми символами. Никаких escape-последовательностей не требуется, что очень удобно. Само собой, в исходниках escape-последовательности были (вроде \r\n), но их локализатор "раскрыл". Признаком конца строки является магическая последовательность "/END/". Да, я в курсе, что магические последовательности - это как бы нехорошо, но тут, похоже, исключение из правила.
Во-вторых, идентификатор_строки не надо мучительно выдумывать, он генерится сам по первым буквам содержимого строки. Получается что-то типа:
Могут получиться одинаковые идентификаторы, если две строки начинаются похоже. Тогда добавляются (автоматически) суффиксы __2, __3, __4... Пример:
4. Потом все строки в исходниках были заменены на идентификаторы по принципу:
LS__Amplitude__6 вместо LS"Amplitude: %g"
Тут еще одна полезная фишка: LS__Amplitude__6 - это есть просто const char *, которой значение присваивается на runtime, при старте программы. В результате программу после локализации почти не пришлось переделывать. Единственный узкий момент - это инициализация статических переменных литералами.
Возникли два основных случая:
- превращается в:
- и это compile error.
Для исправления эта строка просто убирается, и везде вместо mystring пишется LS__mystring.
Другой случай:
- превращается в:
- и это неприятно, поскольку указатель LS__blin инициализируется слишком поздно.
Это исправляется так:
- и, соответственно, везде вместо pups.text используется *(pups.text).
5. Потом были сгенерированы объявления локализованных строк через extern.
И вот тут очередная "вкусность". Во всех прежних проектах подобные идентификаторы были объявлены все сразу в одном хэдере. Этот хэдер #include-ился повсеместно. Малейшее изменение в локализации - и получите перекомпиляцию всех файлов проекта. Неудобно.
Теперь же все выглядит так:
Объявления генерятся и исправляются автоматически, и даже местечко для них подбирается автоматом: пропускаются все комментарии и директивы в начале файла и блок вставляется перед первым объявлением переменной или первой функцией. Потом блок можно переставить, и локализатор впредь будет совать свои объявления в новое место. Хотя мне ничего переставлять не пришлось - все сработало сразу как надо.
Изменения в локализации теперь затрагивают только те файлы, где действительно что-то необходимо изменить.
6. Ну дальше все просто. На основе "файла локализации" сгенерил cpp-шный модуль, где определены (а не объявлены) все эти указатели вроде LS__Search_complete. Файл добавил в проект. Потом сгенерил бинарный файл, который содержит сами строки и добавил его загрузку в функцию main. Сам загрузчик получился крошечный (естественно, это тоже модуль проекта), а его вызов - и вовсе несколько строк кода:
LS_all - это... эээ... массив указателей на указатели. То есть, указатель на указатель на указатель на char :) Но заморачиваться не стоит, поскольку он тоже генерится сам.
7. После этого мне осталось только сделать вариант "файла локализации" на русском языке и сгенерировать другой бинарник greenwin_loc.bin. Заменяя бинарники и перезапуская программу, я меняю язык:


Там еще много мелких плюшек вроде автоматического "merge" двух "файлов локализации" на разных языках, но это уже мелочи. Основное рассказал.
Программой-локализатором, исходником загрузчика и help-ом к ним могу поделиться, но сразу скажу: переделывать это дело под чьи-то нужды, например, под Unicode, мне будет лениво :)
Вот, встала тут такая задачка. В очередной раз, не знаю, уже, в какой. Дан проект средних размеров - 200 с лишним cpp-шников. В нем - куча строковых литералов (не Unicode) на английском. Требуется сделать по крайней мере двуязычие (rus+eng).
Задачку я уже решил, но так получилось, что "на выходе" образовались кое-какие полезные идеи, которыми хочу поделиться. Вдруг кому-нибудь пригодится.
Upd:
Все плюшки здесь
Пришлось напейсать программу-локализатор, которая автоматизирует большую часть процесса.
"Ручной труд" применялся, в основном, на начальном этапе - когда надо было определиться с тем, какие строки подлежат локализации, а какие - нет. Я просто пометил все такие строки уникальным идентификатором LS. Например, вместо "&Cancel" стало LS"&Cancel". Потом определил в проекте
#define LS
- чтобы программа с этими пометками нормально компилировалась.
Думаю, и эту часть работы можно было частично автоматизировать - отметить префиксами вообще все строки, содержащие латинские буквы, а потом только убирать ненужные префиксы. Это дает меньший объем работы, чем обратная операция - расстановка префиксов. Наверное, я добавлю в локализатор и такую фишку - ради достижения гештальта :)
Дальше я возился с локализатором, который потом выполнил всю грязную работу:
1. Вытащил список исходников из проекта Visual Studio.
2. Просканировал их на предмет помеченных строк.
3. Собрал все эти строки в один "файл локализации", имеющий такой формат:
идентификатор_строки строка /END/ идентификатор_строки строка /END/ ...
Строки, которые оказались одинаковыми, объединяются в одну.
В этом месте были две "приятности", которой не было в прежних вариантах локализации, с которыми я имел дело.
Во-первых, "строка" пишется as is - со всеми переводами строк, табуляциями и даже нулевыми символами. Никаких escape-последовательностей не требуется, что очень удобно. Само собой, в исходниках escape-последовательности были (вроде \r\n), но их локализатор "раскрыл". Признаком конца строки является магическая последовательность "/END/". Да, я в курсе, что магические последовательности - это как бы нехорошо, но тут, похоже, исключение из правила.
Во-вторых, идентификатор_строки не надо мучительно выдумывать, он генерится сам по первым буквам содержимого строки. Получается что-то типа:
Any_changes_will_be_reflected Any changes will be reflected after program restart. Do it now? /END/
Могут получиться одинаковые идентификаторы, если две строки начинаются похоже. Тогда добавляются (автоматически) суффиксы __2, __3, __4... Пример:
Amplitude A&mplitude: /END/ Amplitude__2 Amplitude: /END/ Amplitude__3 Amplitude: - /END/ Amplitude__4 &Amplitude: /END/ Amplitude__5 Amplitude /END/ Amplitude__6 Amplitude: %g /END/
4. Потом все строки в исходниках были заменены на идентификаторы по принципу:
LS__Amplitude__6 вместо LS"Amplitude: %g"
Тут еще одна полезная фишка: LS__Amplitude__6 - это есть просто const char *, которой значение присваивается на runtime, при старте программы. В результате программу после локализации почти не пришлось переделывать. Единственный узкий момент - это инициализация статических переменных литералами.
Возникли два основных случая:
static char mystring[]= LS"mystring";
- превращается в:
static char mystring[]= LS__mystring;
- и это compile error.
Для исправления эта строка просто убирается, и везде вместо mystring пишется LS__mystring.
Другой случай:
struct Pupseg { char *text; int id; }; static Pupseg pups = {LS"blin", 12 };
- превращается в:
static Pupseg pups = {LS__blin, 12 };
- и это неприятно, поскольку указатель LS__blin инициализируется слишком поздно.
Это исправляется так:
struct Pupseg { char **text; int id; }; static Pupseg pups = {&LS__blin, 12 };
- и, соответственно, везде вместо pups.text используется *(pups.text).
5. Потом были сгенерированы объявления локализованных строк через extern.
И вот тут очередная "вкусность". Во всех прежних проектах подобные идентификаторы были объявлены все сразу в одном хэдере. Этот хэдер #include-ился повсеместно. Малейшее изменение в локализации - и получите перекомпиляцию всех файлов проекта. Неудобно.
Теперь же все выглядит так:
/* ...blablabla */ #include "gwbase.h" #include "leakon.h" #include "grichedit.h" ...blablabla /*--LOCALIZER DECLARATIONS-- Localizer: LS --BEGIN-- */ extern const char *LS__Search_complete, *LS__Replace_complete, *LS__d_replacement_s_are_made; /*--LOCALIZER DECLARATIONS-- Localizer: LS --END-- */
Объявления генерятся и исправляются автоматически, и даже местечко для них подбирается автоматом: пропускаются все комментарии и директивы в начале файла и блок вставляется перед первым объявлением переменной или первой функцией. Потом блок можно переставить, и локализатор впредь будет совать свои объявления в новое место. Хотя мне ничего переставлять не пришлось - все сработало сразу как надо.
Изменения в локализации теперь затрагивают только те файлы, где действительно что-то необходимо изменить.
6. Ну дальше все просто. На основе "файла локализации" сгенерил cpp-шный модуль, где определены (а не объявлены) все эти указатели вроде LS__Search_complete. Файл добавил в проект. Потом сгенерил бинарный файл, который содержит сами строки и добавил его загрузку в функцию main. Сам загрузчик получился крошечный (естественно, это тоже модуль проекта), а его вызов - и вовсе несколько строк кода:
extern const char **LS_all[]; return sys.localizer.load("greenwin_loc.bin", LS_all);
LS_all - это... эээ... массив указателей на указатели. То есть, указатель на указатель на указатель на char :) Но заморачиваться не стоит, поскольку он тоже генерится сам.
7. После этого мне осталось только сделать вариант "файла локализации" на русском языке и сгенерировать другой бинарник greenwin_loc.bin. Заменяя бинарники и перезапуская программу, я меняю язык:


Там еще много мелких плюшек вроде автоматического "merge" двух "файлов локализации" на разных языках, но это уже мелочи. Основное рассказал.
Программой-локализатором, исходником загрузчика и help-ом к ним могу поделиться, но сразу скажу: переделывать это дело под чьи-то нужды, например, под Unicode, мне будет лениво :)
no subject
Date: 2008-05-05 02:15 pm (UTC)А здесь просто набрал LS перед первой кавычкой - и действительно можно забыть об этом на какое-то время. Потом один раз натравил утилиту - и строки ушли в ресурсы сами, дубли убрались, "говорящие" идентификаторы тоже сгенерились сами и вписались куда надо, про include тоже не надо думать.
Вот только вбивать текст на другом языке придется ручками :) Но переводить удобнее как раз сразу все, а не по одной строке.