psilogic: (ioda)
psilogic ([personal profile] psilogic) wrote2008-05-05 11:59 am

Локализация cpp-шного проекта

(интересно только программистам)

Вот, встала тут такая задачка. В очередной раз, не знаю, уже, в какой. Дан проект средних размеров - 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: От лица мазохиста :)

[identity profile] ex-ketmar.livejournal.com 2008-05-07 02:55 pm (UTC)(link)
>Во-первых — почему бы кодерам не остаться на C :)?
потому что программеров вообще никто не спрашвает обычно, на чём будет проект сделан. во-вторых, потому что большинство так же падко на рекламу, как и румяные менеджеры. им на лбу не выжигали фразы «there's no silver bullet». к сожалению.

>В-третьих — почему бы другим языкам не освоить линковку с C?
давно освоено. заголовочные файлы тоже освоить? и вообще, научиться си компилировать? так вот c++ и научился.

>конструкции вида extern «C» берутся не с потолка
ну и ничего сложного в них тоже нет.

>а подключить к C-программе C++-код
а этого никто не гарантировал. снизу вверх, а не сверху вниз.

>Исходя из этой логики давно должны были победить (Visual) Basic и Java
>:).

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

Re: От лица мазохиста :)

[identity profile] 0serg.livejournal.com 2008-05-08 07:22 am (UTC)(link)
во-вторых, потому что большинство так же падко на рекламу, как и румяные менеджеры. им на лбу не выжигали фразы «there's no silver bullet». к сожалению.

Еще раз: у С++ рекламы не было, и быть не могло - первые 5-7 лет своего существования этот язык был НЕПРИГОДЕН для серьезного коммерческого использования. Кроме того серьезные менеджеры проектов (в отличие от программистов) - как раз люди очень консервативные. Переходить на новую технологию просто потому что "это круто" они не будут.

давно освоено. заголовочные файлы тоже освоить?

Естественно. Там же нет ничего сложного.

ну и ничего сложного в них тоже нет.

А Вы понимаете, что за ними стоит, нет? Для программиста-то они действительно несложные, но на самом деле за ними стоит довольно хитрая конструкция, созданная исключительно для обеспечения линковки C++-кода с C. Почему аналогичные конструкции нельзя было сделать в других языках - я не знаю.

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

Мне смешно :D. Поберждали, рулили :D... Ну-ну, щаззз.