В отличие от других типов ресурсов, где идентификатор ресурса совпадает с указаным в *.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 нужного ресурсного модуля.
Привет!
ОтветитьУдалитьХотелось бы узнать такую вещь: что лучше использовать при разработке не программы, а, например, компонента Delphi - resourcestring в модуле, чтоб потом этот модуль переводили на другие языки или всё-таки подключать где-нибудь res-файл и распространять с модулем rc-файлик с таблицей строк? Чтоб разраб сам делал res и ложил его рядом с модулем, а в модуле хранить только индексы строк.
По сути ведь получается, что различие между res-файлом с таблицей строк и resourcestring в том, что в первом случае мы сами определяем идентификаторы строк в таблице, а во втором все проходит на автомате и индексы могут быть хз какими.
Какой вариант более правильный, грамотный, и т.д., включая и вероятность возникновения каких-то скрытых ошибок?
>>> По сути ведь получается, что различие между res-файлом с таблицей строк и resourcestring в том, что в первом случае мы сами определяем идентификаторы строк в таблице, а во втором все проходит на автомате и индексы могут быть хз какими.
ОтветитьУдалитьДа, верно.
>>> Какой вариант более правильный, грамотный, и т.д., включая и вероятность возникновения каких-то скрытых ошибок?
Единственный правильный вариант (ИМХО, конечно) - хранить локализуемые данные в ресурсах. В частности, для строк в коде это будет resourcestring. Это - стратегия по-умолчанию, которой следует большинство (Delphi, крупные поставщики компонент, библиотек кода и т.п.), за исключением самоделкиных, которым надо написать свой вариант с блек-джеком и... В частности, любое нормальное решение по локализации поддерживает resourcestring.
Насчёт собственно переводов на другие языки - сильно зависит от конкретного решения, которое будет использовать конечная программа. К примеру, если пользователь вашего компонента будет использовать ITE - вы можете добавить в свой пакет словарик (ITE-шный tmx-файл). Пользователь просто подключит словарь и скажет: загрузить перевод из словаря. Если же будет использоваться стороннее решение - то и перевод надо бы поставлять в формате, поддерживаемом этим стороннем решением. В идеале, конечно, это стороннее решение должно уметь импортировать tmx-словарь, но у меня есть сильные сомнения, что с народной нелюбовью к ITE, это мало кто умеет. Поэтому, возможно, вам также нужно будет сконвертировать словарик в другой подходящий формат.
Кстати, в новых Delphi (2010 и выше) появилась новая возможность локализации, о которой я скажу позднее отдельным постом.
Ну то, что в ресурсах это понятно.
ОтветитьУдалитьЯ сейчас храню строки в res-файле, а в модуле Delphi - только идентификаторы строк в таблице и в нужный момент вызываю LoadStr(идентификатор), чтоб показать строку пользователю.
Идея была такова - давать будущим переводчикам rc-файл. Переводят, делают Res, кидают рядом с проектом и делают build - компонент соответственно выдает строки на нужном языке.
Такой подход нормальный или то финт ушами с переподвыпердышем? :)
Что если два компонента сделают это и оба будут использовать пересекающиеся идентификаторы?
ОтветитьУдалитьЯ не думаю, что это удачная идея для компонента общего назначения. Для домашнего использования - вполне.
Так что всё зависит от цели. Попробуйте и нам потом расскажете, что получилось :)
Если же идея в том, чтобы давать переводчикам текстовый файл для перевода - вероятно, более лучшим решением было бы написание конвертера tmx <-> txt.
Вообще, TMX - это открытый формат, а не изобретение Borland-а, так что хранение словаря в нём - обычно хорошая идея. С другой стороны, несколько странно выглядит отсутствие поддержки такого стандартного решения во многих "утилитах локализации для Delphi".
ОтветитьУдалитьИ если вас не устраивает предлагаемый Delphi ITM/ETM - вы можете использовать и сторонние редакторы. Например. Это редактор для TMX-файлов. К Delphi не имеет ни малейшего отношения. (я НЕ пробовал его юзать)
Вообще, по tmx converter поискать если в гугле/вики - всякое можно найти. Например, xls/cvs < - > tmx.
Отчёт в QC по вопросу формата TMX :)
ОтветитьУдалить