Internationalization
BCL includes a lightweight internationalization (i18n) framework for translating application strings at runtime. The design is inspired by GNU gettext: you wrap user-facing strings in a function call, provide translations in simple JSON files, and untranslated strings pass through unchanged. The entire framework lives in the Bcl.Lang unit.
uses {...}, Bcl.Lang;
begin
TLang.Init;
TLocalizer.SetGlobalLanguage('fr-FR');
ShowMessage(_('Hello')); // Shows 'Bonjour' if translated
end;
Core Concepts
The framework is built around a few interfaces and classes:
| Type | Role |
|---|---|
| ITextLocalizer | Looks up a translated string by message ID. Returns the original string if no translation exists. |
| ITextLocalizerFactory | Creates or retrieves ITextLocalizer instances for a given locale name. |
| TLocalizer | Manages the active localizer at two levels: global (process-wide) and per-thread. |
| TLang | Entry point for initializing the i18n system and setting the localizer factory. |
| ILocale | Represents a parsed BCP 47 locale identifier with language, region, and script components. |
Quick Start
1. Initialize the System
Call TLang.Init once at application startup. This installs the default localizer factory, which loads translations from JSON files and embedded resources:
TLang.Init;
2. Provide Translation Files
Create a languages folder next to your executable. Add one JSON file per locale, named with the locale code (e.g., fr-FR.json):
{
"Hello": "Bonjour",
"Save": "Enregistrer",
"Cancel": "Annuler"
}
Each file is a flat object mapping message IDs (the original strings) to their translations.
3. Set the Language
Choose the active language for the application:
TLocalizer.SetGlobalLanguage('fr-FR');
4. Retrieve Translations
Use the GetText function or its shorthand alias _ to retrieve translated strings anywhere in your code:
procedure I18nQuickStart;
begin
// Initialize the localization system
TLang.Init;
// Set the application language
TLocalizer.SetGlobalLanguage('fr-FR');
// Retrieve translated strings
WriteLn(GetText('Hello'));
WriteLn(_('Save')); // _() is shorthand for GetText
end;
If no localizer is active, or if no translation exists for a given message ID, both functions return the original string unchanged. This means your application always works, even without translation files.
Translation Sources
By default, the framework loads translations from two sources, merged together:
JSON Files on Disk
Place per-locale JSON files in a languages folder next to the executable. Each file is named <locale>.json and contains a flat key-value object:
myapp.exe
languages/
fr-FR.json
de-DE.json
pt-BR.json
Embedded Win32 Resources
You can also embed translations as an RT_RCDATA resource named BIZ_LANGUAGES. The resource contains a single JSON object where each top-level key is a locale name:
{
"fr-FR": {
"Hello": "Bonjour",
"Save": "Enregistrer"
},
"de-DE": {
"Hello": "Hallo",
"Save": "Speichern"
}
}
Both sources are loaded when the factory is created. Translations from the folder are merged into (and can override) those from the resource.
You can customize the resource name and folder path by creating the factory manually:
TLang.SetLocalizerFactory(
TDefaultLocalizerFactory.Create('MY_TRANSLATIONS', 'C:\App\Lang')
);
Locale Identifiers
Locale names follow the BCP 47 standard. The TLocale class parses a locale string into its components:
| Component | Property | Example |
|---|---|---|
| Language | Language |
en, fr, zh (ISO 639, lowercase) |
| Script | Script |
Hans, Hant (ISO 15924, title case) |
| Region | Region |
US, FR, CN (ISO 3166, uppercase) |
Both hyphens and underscores are accepted as separators, so fr-FR and fr_FR are equivalent. The Name property returns the normalized form (e.g., zh-Hans-CN).
Locale Negotiation
When the application requests a locale, the factory does not require an exact match. The MatchLocale function uses a scoring algorithm to find the best available candidate:
- Language must match — candidates with a different language are rejected.
- Exact region match scores highest, followed by a language-only fallback (e.g., requesting
frcan matchfr-FR). - Script is considered for languages that have multiple writing systems. For Chinese, the region implicitly selects a script when none is specified (e.g.,
zh-CNprefers Simplified/Hans,zh-TWprefers Traditional/Hant).
This means you can request fr and receive translations from fr-FR, or request zh-CN and receive translations tagged as zh-Hans.
If no candidate shares the same language, the function returns nil and GetText falls back to returning the original message ID.
Thread-Level Localization
In server applications that handle requests from users with different languages, you can set a per-thread localizer that overrides the global one. TLocalizer.Current always prefers the thread-specific localizer when set:
procedure I18nThreadLocal;
begin
TLocalizer.SetCurrentThreadLanguage('de-DE');
try
// All GetText calls in this thread now return German translations
WriteLn(_('Hello'));
finally
TLocalizer.FinalizeCurrentThreadLanguage;
end;
end;
This pattern is particularly useful in HTTP server middleware. For example, TMS Sparkle provides a locale middleware that parses the Accept-Language header, selects the best matching locale, and sets the thread language for the duration of each request. See the Sparkle Locale Middleware documentation for details.
Note
Always call TLocalizer.FinalizeCurrentThreadLanguage in a finally block to ensure the thread-local localizer is cleared after processing.
Custom Localizer Factories
You can replace the default factory with your own implementation of ITextLocalizerFactory. This is useful when translations come from a database, a web service, or any other source:
type
TMyLocalizerFactory = class(TInterfacedObject, ITextLocalizerFactory)
private
function GetLocalizer(const LocaleName: string): ITextLocalizer;
end;
function TMyLocalizerFactory.GetLocalizer(const LocaleName: string): ITextLocalizer;
var
Localizer: TDictionaryLocalizer;
begin
Localizer := TDictionaryLocalizer.Create(TLocale.Create(LocaleName));
// Populate translations programmatically
Localizer.AddText('Hello', 'Hola');
Localizer.AddText('Save', 'Guardar');
Result := Localizer;
end;
procedure I18nCustomFactoryExample;
begin
TLang.SetLocalizerFactory(TMyLocalizerFactory.Create);
TLocalizer.SetGlobalLanguage('es');
WriteLn(_('Hello')); // Returns 'Hola'
end;
TMS Web Core Support
The i18n framework also works in TMS Web Core (pas2js) applications. The web implementation differs in two ways:
- Async loading — translations are fetched asynchronously via XHR from
languages/<locale>.jsonfiles relative to the application URL. - DOM translation — the
Translateprocedure scans the DOM for elements with the CSS classtranslatableand replaces their text content with the translated value.
Usage in a web application follows the same pattern:
TLang.Init; // Installs the XHR-based factory
await(TLocalizer.SetGlobalLanguage('fr-FR'));
Translate(nil); // Translates all elements with class "translatable"