Table of Contents

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.
ITextLocalizer​Factory 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 fr can match fr-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-CN prefers Simplified/Hans, zh-TW prefers 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.​Finalize​Current​Thread​Language 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 ITextLocalizer​Factory. 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>.json files relative to the application URL.
  • DOM translation — the Translate procedure scans the DOM for elements with the CSS class translatable and 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"