вторник, 9 декабря 2008 г.

Формат строковых ресурсов

Это перевод The format of string resources. Автор: Реймонд Чен. Примечание: в отличие от других постов, этот пост сильно отличается от оригинала. Произведено множество замен от C к Delphi.

В отличие от других типов ресурсов, где идентификатор ресурса совпадает с указаным в *.rc файле, строковые ресурсы упаковываются в "пачки" ("bundles"). В статье Knowledge Base Q196774 это описанно довольно лаконично. Сегодня мы расширим это сжатое описание в работающий код.

Строки, объявленные в *.rc файле, группируются вместе в пачку по 16-ть штук. Так, первая пачка содержит строки с номерами от 0 до 15, вторая - с 16 до 31, и т.д. В общем случае пачка N содержит строки от (N - 1) * 16 до(N - 1) * 16 + 15.

Строки в каждой пачке хранятся как UNICODE-строки с заданной длиной (не как нуль-терминированные строки). Если в нумерации строк есть пробелы, то для заполнения пропусков используются пустые строки (null strings). Так, например, если ваша таблица строк содержит только строки с номерами 16 и 31, то у вас будет одна пачка (номер 2), которая состоит из 16-й строки, четырнадцати пустых строк и строка номер 31.

(Заметим, что это означает, что нет никакого способа узнать разницу между "строка 20 - это строка с нулевой длиной" и "строки 20 в пачке нет").

При этом функция LoadString имеет некоторые ограничения:
- Вы не можете задать ей идентификатор языка (language ID). Это значит, что если ваша программа имеет строковые ресурсы на нескольких языках, то вы можете загрузить только вариант с языком по-умолчанию.
- Вы не можете отдельно запросить длину строки.

Давайте напишем несколько функций, которые снимают эти ограничения:
function FindStringResourceEx(AInstance: HINST; AStringID: UINT; ALangID: UINT): PWideChar;
var
  Res: HRSRC;
  LoadedRes: HGLOBAL;
  I: Integer;
begin
  Result := nil;

  // Конвертируем ID строки в номер пачки
  Res := FindResourceEx(AInstance, RT_STRING, MAKEINTRESOURCE(AStringID div 16 + 1), ALangID);
  if Res <> 0 then
  begin
    LoadedRes := LoadResource(AInstance, Res);
    if LoadedRes <> 0 then
    try
      Result := PWideChar(LockResource(LoadedRes));
      if Assigned(Result) then
      try
        // Окей, теперь проходимся по таблице строк
        for I := 0 to (AStringID and 15) - 1 do
          Inc(Result, PWord(Result)^ + 1);
      finally
        UnlockResource(THandle(Result));
      end;
    finally
      FreeResource(LoadedRes);
    end;
  end;
end;
После конвертирования ID строки в номер пачки, мы находим пачку строк, загружаем и блокируем её (да, ужасно много работы для того, чтобы просто получить доступ к ресурсу. Это вам привет из времён Windows 3.1; подробнее об этом - в другой раз).

Затем мы проходимся по пачке строк, пропуская нужное количество записей, пока не найдём нужную строку. Первый WideChar в каждой записи строки содержит длину строки, поэтому добавляя 1, мы пропускаем поля с размером, а добавляя число в первом символе, мы пропускаем саму строку.

Когда мы заканчиваем проход, то в Result лежит указатель на строку с длиной (в первом символе).

На основе этой функции мы можем создавать другие, более интересные функции.

Функция FindStringResource - это простая оболочка (wrapper), которая загружает строку с языком по-умолчанию для потока:
function MAKELANGID(PrimaryLang, SubLang: Word): Word;
begin
  Result := (SubLang shl 10) or PrimaryLang;
end;

function FindStringResource(AInstance: HINST; AStringID: UINT): PWideChar;
begin
  Result := FindStringResourceEx(AInstance, AStringID, MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL));
end;
Функция GetResourceStringLengthEx возвращает длину соответствующей строки, включая нуль-терминатор:
function GetStringResourceLengthEx(AInstance: HINST; AStringID: UINT; ALangID: UINT): UINT;
var
  PStr: PWideChar;
begin
  PStr := FindStringResourceEx(AInstance, AStringID, ALangID);
  Result := 1;
  if Assigned(PStr) then
    Result := Result + PWord(PStr)^;
end;
А функция AllocStringFromResourceEx загружает всю строку целиком в родную строку Delphi:
function AllocStringFromResourceEx(AInstance: HINST; AStringID: UINT; ALangID: UINT): String;
const
  EmptyStr = #0;
var
  PStr: PWideChar;
  WS: WideString;
begin
  PStr := FindStringResourceEx(AInstance, AStringID, ALangID);
  if not Assigned(PStr) then
    PStr := EmptyStr;
  SetLength(WS, PWord(PStr)^);
  Inc(PStr);
  if Length(WS) > 0 then
    Move(PStr^, Pointer(WS)^, Length(WS) * SizeOf(WideChar));
  Result := WS;
end;
(Написание не-Ex вариантов функций GetStringResourceLength и AllocStringFromResource оставляется вам в качестве упражнения).

Упражнение: объясните, как флаг /n для rc.exe влияет на эти функции.

Примечание переводчика: вам навряд ли придётся использовать функции, аналогичные приведённым здесь, в Delphi, поскольку для задания ресурсных строк обычно используются resourcestring (*), которые управляются автоматически: вам нужно просто указывать идентификатор константы - а о загрузке позаботится RTL. Для мультиязычных приложений также обычно используется ITE или сторонние утилиты.

(*) Обращение к resourcestring приводит к вызову LoadResString из модуля System, которая просто вызывает стандартный LoadString. Но при этом модуль, содержащий строку, может отличаться от текущего - за это отвечает вызываемая оттуда LoadResourceModule. LoadResourceModule загружает модуль с локализованными версиями строк (если они есть, конечно). При этом эта функция пытается загрузить модуль, соответствующий текущему языку потока (если только в ключе реестра HKCU\Software\Borland\Locales не указана форсированная локаль).

Таким образом, философия Delphi несколько расходится со схемой, принятой в Windows: вместо того, чтобы иметь в одной строковой таблице несколько вариантов строк на разных языках, в Delphi отдаётся предпочтение схеме, при которой в главном модуле хранится только язык по-умолчанию. А все прочие языки располагаются в отдельных ресурсных файлах, содержащих только локализации. Разумеется, идентификаторы ресурсов совпадают между главным и ресурсными модулями. Таким образом, вместо указания идентификатора языка, указывается HInstance нужного ресурсного модуля.

6 комментариев:

  1. Привет!
    Хотелось бы узнать такую вещь: что лучше использовать при разработке не программы, а, например, компонента Delphi - resourcestring в модуле, чтоб потом этот модуль переводили на другие языки или всё-таки подключать где-нибудь res-файл и распространять с модулем rc-файлик с таблицей строк? Чтоб разраб сам делал res и ложил его рядом с модулем, а в модуле хранить только индексы строк.
    По сути ведь получается, что различие между res-файлом с таблицей строк и resourcestring в том, что в первом случае мы сами определяем идентификаторы строк в таблице, а во втором все проходит на автомате и индексы могут быть хз какими.
    Какой вариант более правильный, грамотный, и т.д., включая и вероятность возникновения каких-то скрытых ошибок?

    ОтветитьУдалить
  2. >>> По сути ведь получается, что различие между res-файлом с таблицей строк и resourcestring в том, что в первом случае мы сами определяем идентификаторы строк в таблице, а во втором все проходит на автомате и индексы могут быть хз какими.

    Да, верно.

    >>> Какой вариант более правильный, грамотный, и т.д., включая и вероятность возникновения каких-то скрытых ошибок?

    Единственный правильный вариант (ИМХО, конечно) - хранить локализуемые данные в ресурсах. В частности, для строк в коде это будет resourcestring. Это - стратегия по-умолчанию, которой следует большинство (Delphi, крупные поставщики компонент, библиотек кода и т.п.), за исключением самоделкиных, которым надо написать свой вариант с блек-джеком и... В частности, любое нормальное решение по локализации поддерживает resourcestring.

    Насчёт собственно переводов на другие языки - сильно зависит от конкретного решения, которое будет использовать конечная программа. К примеру, если пользователь вашего компонента будет использовать ITE - вы можете добавить в свой пакет словарик (ITE-шный tmx-файл). Пользователь просто подключит словарь и скажет: загрузить перевод из словаря. Если же будет использоваться стороннее решение - то и перевод надо бы поставлять в формате, поддерживаемом этим стороннем решением. В идеале, конечно, это стороннее решение должно уметь импортировать tmx-словарь, но у меня есть сильные сомнения, что с народной нелюбовью к ITE, это мало кто умеет. Поэтому, возможно, вам также нужно будет сконвертировать словарик в другой подходящий формат.

    Кстати, в новых Delphi (2010 и выше) появилась новая возможность локализации, о которой я скажу позднее отдельным постом.

    ОтветитьУдалить
  3. Ну то, что в ресурсах это понятно.
    Я сейчас храню строки в res-файле, а в модуле Delphi - только идентификаторы строк в таблице и в нужный момент вызываю LoadStr(идентификатор), чтоб показать строку пользователю.
    Идея была такова - давать будущим переводчикам rc-файл. Переводят, делают Res, кидают рядом с проектом и делают build - компонент соответственно выдает строки на нужном языке.
    Такой подход нормальный или то финт ушами с переподвыпердышем? :)

    ОтветитьУдалить
  4. Что если два компонента сделают это и оба будут использовать пересекающиеся идентификаторы?

    Я не думаю, что это удачная идея для компонента общего назначения. Для домашнего использования - вполне.

    Так что всё зависит от цели. Попробуйте и нам потом расскажете, что получилось :)

    Если же идея в том, чтобы давать переводчикам текстовый файл для перевода - вероятно, более лучшим решением было бы написание конвертера tmx <-> txt.

    ОтветитьУдалить
  5. Вообще, TMX - это открытый формат, а не изобретение Borland-а, так что хранение словаря в нём - обычно хорошая идея. С другой стороны, несколько странно выглядит отсутствие поддержки такого стандартного решения во многих "утилитах локализации для Delphi".

    И если вас не устраивает предлагаемый Delphi ITM/ETM - вы можете использовать и сторонние редакторы. Например. Это редактор для TMX-файлов. К Delphi не имеет ни малейшего отношения. (я НЕ пробовал его юзать)

    Вообще, по tmx converter поискать если в гугле/вики - всякое можно найти. Например, xls/cvs < - > tmx.

    ОтветитьУдалить

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку (поддерживается OpenID).

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.