spiegel-keyman/common/windows/delphi/tools/devtools/Keyman.System.DevTools.BuildLocaleIndex.pas
Marc Durdin ad9ab53c72 feat(windows): improve startup time by caching locale names
This was flagged years ago as a potential performance hotspot: each
locale is loaded at process start for any kmcomapi-involved process,
which takes quite a long time. This refactor moves the locale
enumeration out of kmcomapi and into the build process, so we have a
static list of locales put into locale/index.xml.

This includes a minor breaking change to Keyman Engine API on Windows:
`MessageFromID(id, locale)` now only allows the three ids
SKUILanguageName, SKUILanguageNameWithEnglish, and SKLanguageCode, for
performance reasons. At this point, only SKUILanguageNameWithEnglish is
actually used anywhere in Keyman, and it is unlikely that any other
consumers are requesting alternate IDs.

Fixes: #14787
Build-bot: skip release:windows
2025-09-19 14:11:37 +02:00

118 lines
3 KiB
ObjectPascal

(*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Build index.xml from individual locale.xml files
*)
unit Keyman.System.DevTools.BuildLocaleIndex;
interface
uses
System.Classes,
System.SysUtils,
Xml.XMLIntf,
Xml.XMLDoc;
type
TBuildLocaleIndex = class
private
class function ProcessFile(const SourceFile: string; LocalesNode: IXmlNode;
const BCP47: string): Boolean; static;
public
class function Run(SourcePath, Index: string): Boolean; static;
end;
implementation
uses
Xml.Win.msxmldom,
Winapi.ActiveX,
System.StrUtils,
System.TypInfo,
System.Variants;
{ TBuildSetupStringTranslations }
class function TBuildLocaleIndex.ProcessFile(const SourceFile: string; LocalesNode: IXmlNode;
const BCP47: string): Boolean;
var
doc: IXmlDocument;
node, locale: IXMLNode;
uiLanguageName, uiLanguageNameWithEnglish: string;
begin
doc := TXMLDocument.Create(nil);
doc.ParseOptions := [poResolveExternals]; // I902 - resolve externals when loading XML files
doc.LoadFromFile(SourceFile);
node := doc.DocumentElement.ChildNodes[0];
while Assigned(node) do
begin
if (node.NodeName = 'string') and (node.Attributes['name'] = 'SKUILanguageName') then
uiLanguageName := node.NodeValue
else if (node.NodeName = 'string') and (node.Attributes['name'] = 'SKUILanguageNameWithEnglish') then
uiLanguageNameWithEnglish := node.NodeValue;
node := node.NextSibling;
end;
if (uiLanguageName = '') or (uiLanguageNameWithEnglish = '') then
begin
writeln('ERROR: '+SourceFile+' is missing SKUILanguageName or SKUILanguageNameWithEnglish strings');
Exit(False);
end;
locale := LocalesNode.AddChild('locale');
locale.Attributes['SKLanguageCode'] := BCP47;
locale.Attributes['SKUILanguageName'] := uiLanguageName;
locale.Attributes['SKUILanguageNameWithEnglish'] := uiLanguageNameWithEnglish;
Result := True;
end;
class function TBuildLocaleIndex.Run(SourcePath, Index: string): Boolean;
var
f: TSearchRec;
SourceFile, BCP47: string;
OutputDoc: IXmlDocument;
LocalesNode: IXmlNode;
begin
SourcePath := IncludeTrailingPathDelimiter(SourcePath);
// Iterate through SourcePath/*/strings.xml and generate index.xml
CoInitializeEx(nil, COINIT_APARTMENTTHREADED);
try
Xml.Win.msxmldom.MSXMLDOMDocumentFactory.AddDOMProperty('ProhibitDTD', False);
OutputDoc := NewXMLDocument;
OutputDoc.Encoding := 'UTF-8';
OutputDoc.Active := True;
LocalesNode := OutputDoc.AddChild('locales');
if FindFirst(SourcePath+'*', faDirectory, f) = 0 then
begin
repeat
BCP47 := f.Name;
SourceFile := SourcePath+BCP47+'\strings.xml';
if (BCP47 <> '..') and (BCP47 <> '.') and FileExists(SourcePath+BCP47+'\strings.xml') then
begin
if not ProcessFile(SourceFile, LocalesNode, BCP47) then
Exit(False);
end;
until FindNext(f) <> 0;
FindClose(f);
end;
LocalesNode := nil;
OutputDoc.SaveToFile(Index);
OutputDoc := nil;
finally
CoUninitialize;
end;
Result := True;
end;
end.