уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.

Помнится в прошлом (теперь уже) году публиковал я статью-обзор «23 решения для локализации и интернационализации приложений» в котором я рассмотрел различные инструменты для перевода интерфейса приложения на разные языки. Сегодня же речь пойдет про старый, заезженный всеми кому не лень способ локализации/интернационализации приложений — с использованием ресурса STRINGTABLE. Казалось бы, про STRINGTABLE уже рассказано все, что только можно и практически на каждом более менее крупном форуме по программированию можно встретить десятки топиков с примерами по использованию такой функции как LoadString() и ей подобных. Что ещё можно добавить по использованию этого ресурса? Как оказалось — добавить есть что.

Типичный пример использования STRINGTABLE

Итак, чему нас учат на форумах при работе со STRINGTABLE? Вот самый типичнейший пример использования ресурсов для локализации приложения: 1. Создаем ресурсный файл, например, такой

StringTableLanguage.rc 

STRINGTABLE
{
1000, "English"
1001, "Display selected"
1002, "Yes"
1003, "No"
1004, "Maybe"

2000, "Русский"
2001, "Выбор отображения"
2002, "Да"
2003, "Нет"
2004, "Возможно"
}

2. Подключаем этот файл в проекте:

{$R StringTableLanguage.RES}

3. Таскаем строки из ресурса:

function GetString(const Index: integer) : string;
var
  buffer : array[0..255] of char;
  ls : integer;
begin
  Result := '';
  ls := LoadString(hInstance, 
                   Index, 
                   buffer, 
                   sizeof(buffer));
  if ls <> 0 then Result := buffer;
end;

4. Радуемся — все работает, все классно и вроде бы удобно. А теперь представим, что строк у нас будет не пять, а скажем 1000 и перевести нам нашу программку надо не на 2 языка, а на 20 и при этом учесть всякие там диалекты, письмо справа-налево и всё такое прочее. Очевидно, что получившийся ресурсный файл будет, мягко говоря, не совсем удобным в использовании. С примерно похожей задачей пришлось и мне недавно столкнуться. Языков пока, конечно, не 20, а всего 10,  но строк примерно под 1000 и список языков содержит китайский, корейский, арабский и прочие. Каким «макаром» все это дело красиво и удобно упаковать да так, чтобы в любой момент можно было быстро вносить коррективы в нужные строки? Полез в MSDN в поисках решения.

Что говорит MSDN?

И обнаружил довольно занятную статью под названием «STRINGTABLE resource (Windows)«. Там, конечно, тоже посылают изучать функцию LoadString(), но в этой статье кратко и толково рассказано именно про запись STRINGTABLE. Первый вариант записи STRINGTABLE полностью совпадает с рассмотренным выше примером — все строки лежат в одной большой куче, каждая строка имеет свой уникальный ID. Однако же никто нам не запрещает записать те же самые строки так:

StringTableLanguage.rc 

STRINGTABLE
LANGUAGE 0x09, 0x01
{
1000, "English"
1001, "Display selected"
1002, "Yes"
1003, "No"
1004, "Maybe"}

STRINGTABLE
LANGUAGE 0x19, 0x01
{
1000, "Русский"
1001, "Выбор отображения"
1002, "Да"
1003, "Нет"
1004, "Возможно"
}

И так:

STRINGTABLE 
LANGUAGE 0x09, 0x01 
{ 1000, "English" 
  1001, "Display selected" 
  1002, "Yes" 
  1003, "No" 
  1004, "Maybe"}

STRINGTABLE
 LANGUAGE 0x19, 0x01
 {
 2000, "Русский"
 2001, "Выбор отображения"
 2002, "Да"
 2003, "Нет"
 2004, "Возможно"
 }

И вот так:

STRINGTABLE
LANGUAGE 0x19, 0x01
{
1000, L"\x0421\x043f\x0440\x0430\x0432\x043a\x0430"
....
}

А также размещать эти STRINGTABLE в разных res-файлах. Конечно, вариант, в котором у строк пересекаются идентификаторы создает проблемку — LoadString() уже просто так не используешь, но и это дело можно поправить, если залезть в работу со STRINGTABLE немного по-глубже (об этом поговорим чуть ниже). Как эти знания можно использовать на практике? Тут, конечно, все зависит от конкретных потребностей, но можно предложить такой вариант работы:

  1. В проекте держим отдельный pas-файл со всеми строками для локализации. Этот файл всегда содержит самые свежие варианты строк. Тут же можно определить и, например, константы с идентификаторами. Этот файл содержит строки на языке по умолчанию, например, на русском
  2. Для всех остальных языков создается по отдельному STRINGTABLE в котором определена секция LANGUAGE.
  3. По необходимости цепляем к проекту нужный res-файл (или несколькоres-файлов) и ищем строки по двум параметрам — ID и Язык.

Теперь посмотрим как этот простой алгоритм можно реализовать в Delphi. Если есть необходимость записывать RC-файл автоматически на основании каких-либо данных, то, наверняка может возникнуть вопрос по записи LANGUAGE — где брать параметры для этой секции. С этого и начнем.

Идентификаторы языков в Windows

На том же MSDN есть замечательная табличка «Language Identifier Constants and Strings» где расписаны все возможные константы для различных языков. Например, возьмем такой идентификатор локали 0x1004. Судя по таблице этому идентификатору соответствует китайский язык c Primary Language ID равным 0х04 и SubLanguage ID = 0x04. То есть, судя все по той же таблице, на этом китайском разговаривают и пишут в Сингапуре. А локаль 0x3009 — это английский язык в Зимбабве. Откуда брать эти значения в Delphi? Вариантов есть несколько. Первый вариант — использовать класс Languages из модуля SysUtils. В этом классе определены следующие свойства:

property Count: Integer read GetCount; //количество языков
property Name[Index: Integer]: string read GetName;//имя, например, "Китайский (традиционное письмо, Гонконг)"
property ID[Index: Integer]: string read GetID; //идентификатор языка, например, "$00001004" (StrToInt($00001004)=4100)
property LocaleName[Index: Integer]: string read GetLocaleName;//имя локали, например, zh-SG
property LocaleID[Index: Integer]: TLocaleID read GetLocaleID; //идентификатор локали, например, 4100
property Ext[Index: Integer]: string read GetExt;//стандартное расширения для файла локализации, например, ZHI

Здесь стоит отметить следующий момент: значение ID (идентификатор языка, Language ID), переведенное в Integer не обязательно совпадает с LocaleID. Хоть я и проверил перед написанием этой статьи все записи в TLanguages на предмет равенства ID и LocaleID и ни одного расхождения в этих значениях не нашел, но сам MSDN говорит об этих идентификаторах следующее:

Language ID (идентификатор языка) — 16-битное число, определяющее язык в стране или регионе. В этом числе биты с 0 по 9 определяют Primary Language ID, а с 10 по 15 — SubLanguageID. Locale ID (идентификатор локали) — 32-битное число, которое содержит в себе Language ID (c 0 по 15 бит), порядок сортировки (с 16 по 19 бит) и зарезервированные биты (с 20 по 31).

Используя свойства TLanguages можно получить и Primary Language ID и SubLanguage ID. Для этого воспользуемся следующими функциями из модуля Windows:

function PRIMARYLANGID(lgid: WORD): WORD; inline;
function SUBLANGID(lgid: WORD): WORD; inline;

Здесь в качестве входного параметра необходимо указать идентификатор языка. К примеру,

var PrimID, SubID:integer;
PrimID:=PRIMARYLANGID(1049);
SubID:=SUBLANGID(1049)
//PrimID = 25 или в HEX 0x19
//SubID = 1 или в HEX 0x01

Соответственно, никто не мешает нам воспользоваться и обратной функцией, которая также определена в модуле Windows:

var LocaleID: integer;
LocaleID:=MAKELANGID(25,1);
//LocaleID = 1049

Таким образом, используя приведенные выше функции и класс TLanguages мы можем довольно легко организовать поиск необходимых идентификаторов языка для записи в секцию LANGUAGES у STRINGTABLE. Кстати, если Вам не требуется или Вы не хотите лишний раз рассчитывать Primary ID или SubLanguage ID, то эти же самые константы уже определены все в том же модуле Windows:

[...]
LANG_ENGLISH = $09;
LANG_ESTONIAN = $25;
LANG_FAEROESE = $38;
LANG_FARSI = $29;
LANG_FINNISH = $0b;
LANG_FRENCH = $0c;
LANG_GERMAN = $07;
[...]
SUBLANG_DUTCH_BELGIAN = $02; { Dutch (Belgian) }
SUBLANG_ENGLISH_US = $01; { English (USA) }
SUBLANG_ENGLISH_UK = $02; { English (UK) }
SUBLANG_ENGLISH_AUS = $03; { English (Australian) }
SUBLANG_ENGLISH_CAN = $04; { English (Canadian) }
SUBLANG_ENGLISH_NZ = $05; { English (New Zealand) }
SUBLANG_ENGLISH_EIRE = $06; { English (Irish) }
[...]

Так что, вариантов «выудить» необходимые значения идентификаторов для ресурсного файла масса. Отдельным вопросом работы с языками в Delphi можно выделить определение аббревиатур языков и других значений, например, вывод имени не на родном языке пользователя, а на английском. Для этой цели удобно воспользоваться ещё одним методом из WinAPI — GetLocaleInfo. Метод GetLocaleInfo возвращает различную информацию о локали и выглядит следующим образом:

function GetLocaleInfo(Locale:LCID;LType:LCTYPE;lpLCDATA:PWideChar;cchData:integer):integer;

Параметр LCType указывает какую информацию необходимо получить для локали (Locale). Все возможные значения этого параметра указаны в статье «Locale Information Constants«. Рассмотрим несколько примеров использования этой функции. Для начала немного упростим использование GetLocaleInfo и напишем такую обертку:

function GetLocaleData(ID: LCID; Flag: DWORD): string;
var
  Buffer: array[0..1023] of WideChar;
begin
  Buffer[0] := #0;
  GetLocaleInfo(ID, Flag, Buffer, Length(Buffer));
  Result := Buffer;
end;

Теперь примеры:

{Язык в формате ISO 639}
GetLocaleData(3076,LOCALE_SISO639LANGNAME);//zh
{Английское название языка}
GetLocaleData(Languages.LocaleID[i],LOCALE_SENGLANGUAGE);//Chinese (Traditional)
{Название страны по-английски}
GetLocaleData(Languages.LocaleID[i],LOCALE_SENGCOUNTRY);//Hong Kong S.A.R.
{Аббревиатура языка}
GetLocaleData(Languages.LocaleID[i],LOCALE_SABBREVLANGNAME);//ZHH

Также можно получить ещё много какой информации — названия месяцев, дней недели и т.д. и т.п., но это уже имеет мало отношения к нашей теме. В заключение этой части я написал маленькое приложение, которое выводит вот такую таблицу со всеми необходимыми нам идентификаторами языков и локалей в десятичном и hex-форматах:
LanguagesСкачать программку можно со страницы исходников (раздел «Вспомогательные программы»).

С идентификаторами вроде бы немного разобрались — переходим к следующему шагу — создаем необходимые ресурсные файлы.

Работа со STRINGTABLE

Для начала создадим простенькое приложение с несколькими кнопками и метками текст которых мы и будем переводить. В проекте определим такой модуль:

unit DemoStrings;
 
interface
 
const
Russian = 1049;
ChineseTraditional = 3076;
 
strVersion = 'version ';
idxVersion = 1;
strContacts = 'Contacts';
idxContacts = 2;
strSettings = 'Settings';
idxSettings = 3;
strLanguage = 'Language';
idxLanguage = 4;
 
implementation
 
end.

Первые две константы — это идентификаторы локалей в десятичном формате, остальные — строки и их идентификаторы в ресурсных файлах.
Теперь определимся как будем хранить наши локализованные строки. Для примера я решил, что каждая локализация будет находиться в отдельном файле, а сами строки будут записаны в UNICODE-формате:

L"\x0421\x043f\x0440\x0430\x0432\x043a\x0430"

Для начала напишем небольшую функцию формирования таких строк в Delphi (нет, в Delphi 7 она работать не будет):

function MaskString(const AValue: String): string;
var Bytes:TBytes;
I: Integer;
begin
  Result:=EmptyStr;
  Bytes:=WideBytesOf(AValue);
  for I := 1 to Length(AValue) do
    Result:=Result+'\x'+IntToHex(TCharacter.ConvertToUtf32(AValue,i),4);
  Result:='L"'+Result+'"';
end;

Теперь переводим строки с помощью какого-нибудь переводчика и создаем RC-файлы. Например для Китайского языка у меня получился вот такой файл:

STRINGTABLE
LANGUAGE 0x04,0х03
{
1, L"\x7248\x672C"
2, L"\x9023\x7D61\x4EBA"
3, L"\x8A2D\x7F6E"
6, L"\x8A9E\x8A00"
}

Для русского языка запись файла будет аналогична — идентификаторы оставим теми же. Теперь собираем RES-файлы. Для сборки RES-файла я использовал утилиту rc.exe.

Полученные файлы цепляем в наш проект, например, в главный модуль:

{$R ChineseTraditional.RES}
{$R Russian.RES}

Теперь проблема — есть два файла с ресурсами, в каждом STRINGTABLE строки имеют одинаковые идентификаторы: как загрузить из ресурсов строку на необходимом нам языке?

Для решения этой задачи пришлось немного «попотеть», т.к. большинство ссылок ведущих на страницы с примерами в MSDN уже не работают — странички поудаляли/переместили и т.д. В итого обнаружил два ещё живых примера: первый в блоге на том же msdn где кратко, но доступно объясняется как хранятся строки в ресурсах и приводятся несколько примеров на C++ для загрузки строк. И второй пример на Delphi уже по-свежее, на сайте VR-Online. В итоге прочитав и переварив всю полученную из этих примеров информацию я немного подправил пример с VR-Online и получил вот такую небольшую функцию для загрузки строки из STRINGTABLE на нужном мне языке:

function GetStringRes(idx: DWORD; wLang: Word): WideString;
var
  hFindRes: Cardinal;
  hLoadRes: Cardinal;
  nBlockID, nItemID: DWORD;
  pRes: Pointer;
  i, j: Integer;
  dwSize: Cardinal;
  nLen: Integer;
  iStr: Cardinal;
const
  NO_OF_STRINGS_PER_BLOCK = 16;
begin
  Result := EmptyStr;
  nBlockID := (idx shr 4) + 1;
  nItemID := 16 - (nBlockID shl 4 - idx);
  hFindRes := FindResourceEx(HInstance, RT_STRING, MAKEINTRESOURCEW(nBlockID), wLang);
  if hFindRes = 0 then Exit;
  hLoadRes := LoadResource(HInstance, hFindRes);
  if hLoadRes = 0 then Exit;
  pRes := LockResource(hLoadRes);
  if pRes = nil then Exit;
  dwSize := SizeofResource(HInstance, hFindRes);
  if dwSize = 0 then Exit;
  iStr := 0;
  for i := 0 to NO_OF_STRINGS_PER_BLOCK - 1 do
  begin
    nLen := PWord(pRes)^;
    Inc(DWord(pRes), 2);
    if pRes = nil then
      Exit;
    if iStr = nItemID then
    begin
      SetLength(Result, nLen);
      for j := 1 to nLen do
      begin
        Result[j] := PWideChar(pRes)^;
        Inc(DWord(pRes), 2);
      end;
      UnlockResource(hLoadRes);
      Exit;
    end
    else
      Inc(DWord(pRes), nLen * 2);
    inc(iStr);
  end;
end;

Так как я точно знаю, что все мои ресурсы будут 100% содержаться в моей программе, не будет никаких dll с ресурсами и т.д., то здесь я позволил себе использовать HInstance проекта для работы с ресурсами. Ну, а если у вас ресурсы будут хранится в dll, то используйте дескриптор своей библиотеки.
И теперь осталось только показать пример загрузки строк, но здесь все довольно просто. Например, так можно перевести все надписи кнопок на китайский язык:

<a href="http://webdelphi.ru/wp-content/uploads/2013/01/chenese_form.png"><img class="aligncenter size-full wp-image-7569" alt="chenese_form" src="http://webdelphi.ru/wp-content/uploads/2013/01/chenese_form.png" width="171" height="216" /></a> Button1.Caption:=GetStringRes(idxVersion,ChineseTraditional);
 Button2.Caption:=GetStringRes(idxContacts,ChineseTraditional);
 Button3.Caption:=GetStringRes(idxSettings,ChineseTraditional);
 Button4.Caption:=GetStringRes(idxLanguage,ChineseTraditional);

И получить такую форму (надпись первой кнопки не переводилась):

chenese_form

 

Вот, собственно, и все, что хотел сказать про STRINGTABLE и работу с идентификаторами языков и локалей. Конечно, этот пост не претендует на оригинальность, т.к. я сказал в самом начале — тема использования STRINGTABLE довольно популярная и рассматривалась со всевозможных сторон и позиций. Скорее это небольшое дополнение к тому, что уже было сказано ранее.

Ну, а если Вы не сторонник локализации приложений с использованием ресурсов типа STRINGTABLE, то вариантов работы масса — про 23 решения для локализации и интернационализации Ваших приложений Вы можете прочитать в блоге.

0 0 голоса
Рейтинг статьи
уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.
Подписаться
Уведомить о
6 Комментарий
Межтекстовые Отзывы
Посмотреть все комментарии
Наиль
Наиль
09/01/2013 17:27

Шрифты азиатских шрифтов заработали сразу, без шаманства?

Torbins
Torbins
09/01/2013 21:42

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

trackback

[…] STRINGTABLE and work with the language identifier in Delphi. | Delphi in Internet (this is a Russian article, but Google translate does a pretty good job) […]

Артем
Артем
15/03/2015 04:05

Здравствуйте.

Подскажите пожалуйста как изменить данные в stringtables. Я понимаю, что нужно с помщью UpdateResource, но не могу понять как.

например , заменить 1 на 0

STRINGTABLE
LANGUAGE 25, 1
{
101, «1»
102, «2»
103, «3»
}

Спасибо