Skip to main content
Table of Contents

Getting started with TMS FNC Localization

TMS FNC Localization translates a Delphi or C++Builder application without rewriting its UI. You design your forms in one language as usual. The product collects the translatable text, you translate it in an editor (by hand or with AI help), and at run time the active language switches with one call. This page walks you through the smallest setup that works: two non-visual components, one base-form change, and one line of startup code. Use it the first time you add localization to a project. Once that loop works, the component guides cover the details: collection rules, translation files, string catalogs, snippets, and the editor itself.

Requirements

Requirement Details
IDE Delphi 11.1 Alexandria or C++ Builder 11.1 Alexandria or later
Frameworks VCL Win32/Win64 · FMX Win32/Win64, macOS, iOS, Android, Linux · WEB Core (Chrome, Edge, Firefox, Safari)
Dependency TMS FNC Core must be installed before Localization

The pieces you need

A minimal localized application uses three building blocks. The other components are optional.

Piece Role
TTMSFNCLocalizationCollector Finds the translatable text in your forms and owns the translation file (translations.json). Double-click it at design time to open the editor.
TTMSFNCLocalizationLocalizer The runtime engine. It loads the translation file and switches the active language.
TTMSFNCLocalizationForm A base form class. A form derived from it translates itself when it is created and again on every language change.

Optionally, a TTMSFNCLocalizationComboBox gives the user a ready-made language picker. A String Catalog or snippets hold text that does not live on a control.

Step 1 — Add the collector and the localizer

Place one TTMSFNCLocalizationCollector and one TTMSFNCLocalizationLocalizer in your project. They are application-wide, so you need only one of each. A data module that is created before your forms is the natural home, and the product demos use one. Set LocalizationFolder to the same value on both components so they read and write the same file. The demos use .. A relative folder resolves against the executable's folder, and the design-time editor uses the project's output folder, so design time and run time find the same translations.json.

Tip

If the application will save translations on the user's machine, use a writable token such as {HOME} or {DOC} instead of a path under the install folder. See translation folders for the token list.

Step 2 — Derive your forms from TTMSFNCLocalizationForm

A form only shows translated text once the localizer has been applied to it, and it has to be applied again whenever the language changes. TTMSFNCLocalizationForm does both for you. A form derived from it is translated into the selected language as soon as it is created, and it re-translates itself every time the language changes. You never write a localization call for that form.

There are two ways to get automatic translation:

Approach How it works Where it works
Derive from TTMSFNCLocalizationForm (recommended) Each form translates itself on creation and on every language change. VCL, FMX and TMS WEB Core, for every form, independent of the localizer's start mode.
Hook start modes (lsmAutoHook, lsmIdleHook) Set LocalizationStartMode on the localizer, and it picks up new forms through an application-level hook. Your forms keep inheriting from TForm. VCL and FMX only, and only for forms created after startup. Forms created in the .dpr are not caught.

The hooks are handy when you cannot change a form's ancestor, for example a form from a third-party library. The base form is the most universal approach, because it works the same in every framework and for every form, however and whenever the form is created. That is why this guide uses it. See choosing when localization runs for the hook modes.

To use the base form, change the ancestor of every form you want translated from TForm (or TWebForm) to TTMSFNCLocalizationForm, and add the framework unit to the uses clause. The form designer is unaffected: the class only adds the translate-on-create and translate-on-language-change behavior.

Derive a form from TTMSFNCLocalizationFormPascalC++
unit UCustomer;

interface

uses
  System.SysUtils, System.Classes, FMX.Types, FMX.Controls, FMX.Forms,
  FMX.StdCtrls, FMX.Controls.Presentation,
  FMX.TMSFNCLocalizationForm; { VCL: VCL.TMSFNCLocalizationForm, WEB: WEBLib.TMSFNCLocalizationForm }

type
  { Was: TFormCustomer = class(TForm) }
  TFormCustomer = class(TTMSFNCLocalizationForm)
    lblName: TLabel;
    btnSave: TButton;
  end;

implementation

{$R *.fmx}

end.
// UCustomer.h - FMX; VCL: VCL.TMSFNCLocalizationForm.hpp
// Was: class TFormCustomer : public TForm
class TFormCustomer : public TTMSFNCLocalizationForm
{
__published:
  TLabel *lblName;
  TButton *btnSave;
public:
  __fastcall TFormCustomer(TComponent *Owner) : TTMSFNCLocalizationForm(Owner) {}
};

Use the unit for your framework: FMX.TMSFNCLocalizationForm, VCL.TMSFNCLocalizationForm, or WEBLib.TMSFNCLocalizationForm. The base form works the same way in all three.

Step 3 — Translate in the editor

At design time, double-click the collector, or use its context menu, to open the Localization Editor. The editor scans the project's forms, so the tree lists every form with its translatable properties. Then:

  1. On the Languages tab, add the languages you want to ship.
  2. On the Translations tab, select a target language and type the translations. Alternatively, configure an AI service on the AI Settings tab and let it propose them.
  3. Approve the translations. Only Approved translations are applied at run time. Proposed ones stay in the file until a human signs them off.
  4. Close the editor. It writes translations.json to the collector's folder.

The editor's filters help you find a form or a string quickly once the project grows.

Step 4 — Localize at startup and switch languages

The localizer loads translations.json from its folder automatically. The main form needs one call to apply the starting language to everything that already exists. That language is the one the user picked on a previous run, or the default. After that, a TTMSFNCLocalizationComboBox on the form switches the language with no code. You can also call TrySetLocale from your own UI:

Localize at startup and switch the languagePascalC++
procedure TFormMain.FormCreate(Sender: TObject);
begin
  { Apply the starting language (the persisted selection, or the default)
    to every form that already exists. Forms derived from
    TTMSFNCLocalizationForm handle this themselves. }
  DM.Localizer.PerformLocalization;
end;

procedure TFormMain.btnGermanClick(Sender: TObject);
begin
  { Switch the whole application; returns False when the language is not
    in the translation file. A TTMSFNCLocalizationComboBox does this for you. }
  if not DM.Localizer.TrySetLocale('de') then
    ShowMessage('German is not available.');
end;
void __fastcall TFormMain::FormCreate(TObject *Sender)
{
  // Apply the starting language (the persisted selection, or the default)
  // to every form that already exists. Forms derived from
  // TTMSFNCLocalizationForm handle this themselves.
  DM->Localizer->PerformLocalization();
}

void __fastcall TFormMain::btnGermanClick(TObject *Sender)
{
  // Switch the whole application; returns false when the language is not
  // in the translation file. A TTMSFNCLocalizationComboBox does this for you.
  if (!DM->Localizer->TrySetLocale("de"))
    ShowMessage("German is not available.");
}

TrySetLocale returns False when the translation file has no such language, so you can fall back gracefully. With PersistLanguageSelection left at its default (True), the chosen language is restored on the next start.

Putting it together

The complete wiring is short. The project file creates the data module first, so the localizer has loaded the translations before any form is constructed. Every form derives from TTMSFNCLocalizationForm, and a form that builds text in code refreshes it in LanguageChange:

Minimal localized applicationPascalC++
{ Demo.dpr - the data module holding the collector and the localizer is
  created first, so the translations are loaded before any form exists. }
begin
  Application.Initialize;
  Application.CreateForm(TDM, DM);
  Application.CreateForm(TFormMain, FormMain);
  Application.Run;
end.

{ UMain.pas - uses TMS.TMSFNCLocalizationSnippets for the status text. }
const
  SnippetStatusReady: TTMSFNCSnippetRec = (Name: 'Main.StatusReady'; DefaultText: 'Ready');

type
  TFormMain = class(TTMSFNCLocalizationForm)
    lblStatus: TLabel;
    cbLanguage: TTMSFNCLocalizationComboBox; { switches the language, no code needed }
    procedure FormCreate(Sender: TObject);
  private
    procedure UpdateStatus;
  protected
    procedure LanguageChange(Sender: TObject); override;
  end;

procedure TFormMain.FormCreate(Sender: TObject);
begin
  DM.Localizer.PerformLocalization;
  UpdateStatus;
end;

procedure TFormMain.UpdateStatus;
begin
  { Text built in code is not part of the designed form, so it is refreshed
    by hand. TranslateSnippet returns the default text until a translation
    is approved. }
  lblStatus.Text := TranslateSnippet(SnippetStatusReady);
end;

procedure TFormMain.LanguageChange(Sender: TObject);
begin
  inherited;     { the designed properties are re-translated first }
  UpdateStatus;  { then refresh the text this form sets itself }
end;

initialization
  CollectSnippet(SnippetStatusReady); { register it so the editor lists it }
// Demo.cpp - the data module holding the collector and the localizer is
// created first, so the translations are loaded before any form exists.
int WINAPI _tWinMain(HINSTANCE, HINSTANCE, LPTSTR, int)
{
  Application->Initialize();
  Application->CreateForm(__classid(TDM), &DM);
  Application->CreateForm(__classid(TFormMain), &FormMain);
  Application->Run();
  return 0;
}

// UMain.cpp - uses TMS.TMSFNCLocalizationSnippets for the status text.
const TTMSFNCSnippetRec SnippetStatusReady = {"Main.StatusReady", "Ready"};

class TFormMain : public TTMSFNCLocalizationForm
{
__published:
  TLabel *lblStatus;
  TTMSFNCLocalizationComboBox *cbLanguage;  // switches the language, no code needed
  void __fastcall FormCreate(TObject *Sender);
private:
  void __fastcall UpdateStatus();
protected:
  virtual void __fastcall LanguageChange(TObject *Sender);
};

void __fastcall TFormMain::FormCreate(TObject *Sender)
{
  DM->Localizer->PerformLocalization();
  UpdateStatus();
}

void __fastcall TFormMain::UpdateStatus()
{
  // Text built in code is not part of the designed form, so it is refreshed
  // by hand. TranslateSnippet returns the default text until a translation
  // is approved.
  lblStatus->Text = TranslateSnippet(SnippetStatusReady);
}

void __fastcall TFormMain::LanguageChange(TObject *Sender)
{
  TTMSFNCLocalizationForm::LanguageChange(Sender);  // the designed properties are re-translated first
  UpdateStatus();                                   // then refresh the text this form sets itself
}

// Register the snippet so the editor lists it (the Pascal initialization section).
static void RegisterSnippets()
{
  CollectSnippet(SnippetStatusReady);
}
#pragma startup RegisterSnippets

LanguageChange is the one hook you override. The base class has already re-translated the designed properties when it runs. Use it for text you set from code, such as a status line, a computed caption, or a count. Calling inherited first keeps the built-in behavior (on TMS WEB Core it also restores the size of a popup form).

Common pitfalls

  • Create the data module before the forms. If the main form is created first, it is constructed before the localizer has loaded translations.json, and there is nothing to apply. Keep the data module first in the .dpr.
  • Keep the localizer on lsmManual when you use TTMSFNCLocalizationForm. That is the default. The base form already localizes itself, and a hook start mode would add a second, redundant pass.
  • Approve translations before you test them. A translation that is still Proposed, for example straight from the AI, is not applied at run time, so the UI stays in the source language.
  • Ship translations.json with the application. The localizer reads it from LocalizationFolder at run time. Deploy the file to that folder.
  • Text set from code is not translated automatically. A caption you assign in FormCreate overwrites the translation. Move such text into a snippet or a String Catalog entry, and refresh it in LanguageChange.

Next steps