Entry tags:
Локализация cpp-шного проекта
(интересно только программистам)
Вот, встала тут такая задачка. В очередной раз, не знаю, уже, в какой. Дан проект средних размеров - 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, мне будет лениво :)
Re: От лица мазохиста :)
потому что программеров вообще никто не спрашвает обычно, на чём будет проект сделан. во-вторых, потому что большинство так же падко на рекламу, как и румяные менеджеры. им на лбу не выжигали фразы «there's no silver bullet». к сожалению.
>В-третьих — почему бы другим языкам не освоить линковку с C?
давно освоено. заголовочные файлы тоже освоить? и вообще, научиться си компилировать? так вот c++ и научился.
>конструкции вида extern «C» берутся не с потолка
ну и ничего сложного в них тоже нет.
>а подключить к C-программе C++-код
а этого никто не гарантировал. снизу вверх, а не сверху вниз.
>Исходя из этой логики давно должны были победить (Visual) Basic и Java
>:).
а так и есть. побеждали и рулили, пока на пеар цэрешётки не был кинут звёздолёт с деньгами. пеар — страшная сила.
Re: От лица мазохиста :)
Еще раз: у С++ рекламы не было, и быть не могло - первые 5-7 лет своего существования этот язык был НЕПРИГОДЕН для серьезного коммерческого использования. Кроме того серьезные менеджеры проектов (в отличие от программистов) - как раз люди очень консервативные. Переходить на новую технологию просто потому что "это круто" они не будут.
давно освоено. заголовочные файлы тоже освоить?
Естественно. Там же нет ничего сложного.
ну и ничего сложного в них тоже нет.
А Вы понимаете, что за ними стоит, нет? Для программиста-то они действительно несложные, но на самом деле за ними стоит довольно хитрая конструкция, созданная исключительно для обеспечения линковки C++-кода с C. Почему аналогичные конструкции нельзя было сделать в других языках - я не знаю.
а так и есть. побеждали и рулили, пока на пеар цэрешётки не был кинут звёздолёт с деньгами. пеар — страшная сила.
Мне смешно :D. Поберждали, рулили :D... Ну-ну, щаззз.