Skip to main content
Table of Contents

Snippets

The collector translates text that lives on components: captions, hints, list items. Much of an application's text is not on a component, though. A confirmation message, a status line, an error string passed to ShowMessage, or "3 files selected" is built in code at run time, and no form scan can find it. Snippets close that gap. You declare the string once in code, under a stable name and with its source-language text. The collector registers it, the editor lists it for translation next to the form strings, and at run time you ask for the translated text by that declaration. Use snippets for text you produce in code. In particular, use number snippets for counts, because their wording depends on the number and the right plural form differs per language. For a fixed set of strings you would rather manage at design time without code, use a String Catalog instead.

Snippets or a String Catalog?

Both hold text that is not on a control, but they suit different jobs.

Snippets String Catalog
Declared In code, as typed constants or by name At design time, in the component's item list
Plural forms Yes, with number snippets No
Appears in the editor under The Snippets and NumberSnippets roots The form or data module that holds the catalog
Best for Messages and counts used from anywhere in code Named strings owned by one module or by anywhere if placed on a TDataModule

You can mix the two in one application.

Declaring a plain snippet

A plain snippet is a name and a default text. You can declare it in two ways: as a record, shown here, or by name only. As a record, it is a TTMSFNCSnippetRec with a Name and a DefaultText. Declare it as a typed constant so the code that uses it and the code that registers it share one definition. Then call CollectSnippet from the unit's initialization section so the snippet is known as soon as the application starts:

Declare and register plain snippetsPascalC++
unit UAppStrings;

interface

uses
  TMS.TMSFNCLocalizationSnippets;

const
  SnippetOrderSaved: TTMSFNCSnippetRec = (
    Name: 'Orders.Saved';
    DefaultText: 'The order has been saved.'
  );

  SnippetOrderDeleteConfirm: TTMSFNCSnippetRec = (
    Name: 'Orders.DeleteConfirm';
    DefaultText: 'Delete order %s?'
  );

implementation

initialization
  { Registering makes the snippets known to the collector, so they appear
    in the editor under the Snippets root. }
  CollectSnippet(SnippetOrderSaved);
  CollectSnippet(SnippetOrderDeleteConfirm);

end.
// UAppStrings.h / UAppStrings.cpp
const TTMSFNCSnippetRec SnippetOrderSaved = {
  "Orders.Saved",
  "The order has been saved."
};

const TTMSFNCSnippetRec SnippetOrderDeleteConfirm = {
  "Orders.DeleteConfirm",
  "Delete order %s?"
};

// Registering makes the snippets known to the collector, so they appear
// in the editor under the Snippets root. Run it once at startup (the Pascal
// initialization section).
static void RegisterAppStrings()
{
  CollectSnippet(SnippetOrderSaved);
  CollectSnippet(SnippetOrderDeleteConfirm);
}
#pragma startup RegisterAppStrings

The name is the snippet's identity in the translation file. Keep it stable: renaming it creates a new, untranslated entry. Use dotted names such as Orders.Saved to group related snippets so they stay findable in the editor. The default text is the source-language string, and it remains the source of truth. If you later change it in code, the existing translations are marked Outdated so a translator reviews them.

Translating a snippet at run time

Call TranslateSnippet wherever you need the text. It returns the translation for the active language. When there is no approved translation yet, it returns the declared default text, so the application never shows an empty string:

Use snippets at run timePascalC++
procedure TFormOrders.btnSaveClick(Sender: TObject);
begin
  SaveOrder;
  { Translation for the active language, or the declared default text. }
  ShowMessage(TranslateSnippet(SnippetOrderSaved));
end;

procedure TFormOrders.btnDeleteClick(Sender: TObject);
begin
  { The translated text keeps its format specifiers; validation rejects a
    translation that would break them. }
  if MessageDlg(Format(TranslateSnippet(SnippetOrderDeleteConfirm), [CurrentOrderNumber]),
    TMsgDlgType.mtConfirmation, [TMsgDlgBtn.mbYes, TMsgDlgBtn.mbNo], 0) = mrYes then
    DeleteOrder;
end;

function TFormOrders.StatusText(const AStatusKey: string): string;
begin
  { Look a snippet up by name when the name is computed, e.g. 'Orders.Status.Shipped'. }
  Result := TranslateSnippet('Orders.Status.' + AStatusKey);
end;
void __fastcall TFormOrders::btnSaveClick(TObject *Sender)
{
  SaveOrder();
  // Translation for the active language, or the declared default text.
  ShowMessage(TranslateSnippet(SnippetOrderSaved));
}

void __fastcall TFormOrders::btnDeleteClick(TObject *Sender)
{
  // The translated text keeps its format specifiers; validation rejects a
  // translation that would break them.
  if (MessageDlg(Format(TranslateSnippet(SnippetOrderDeleteConfirm), ARRAYOFCONST((CurrentOrderNumber))),
    TMsgDlgType::mtConfirmation, TMsgDlgButtons() << TMsgDlgBtn::mbYes << TMsgDlgBtn::mbNo, 0) == mrYes)
    DeleteOrder();
}

System::UnicodeString __fastcall TFormOrders::StatusText(const System::UnicodeString AStatusKey)
{
  // Look a snippet up by name when the name is computed, e.g. "Orders.Status.Shipped".
  return TranslateSnippet("Orders.Status." + AStatusKey);
}

Two overloads exist. TranslateSnippet(SnippetRec) takes the declaration and falls back to its DefaultText. TranslateSnippet('Name') looks the snippet up by name. It is covered in the next section. A snippet that contains format specifiers is passed to Format as usual. The validation step protects those specifiers: a translation that drops or changes a %s is rejected instead of applied.

Declaring by name, without a record

A record per string is not required. CollectSnippet and TranslateSnippet both have overloads that take the snippet's name as a plain string. CollectSnippet('Name', 'Default text') registers the name and its source text in one call, and TranslateSnippet('Name') returns the translation for the active language. That is the quickest way to make a one-off message translatable:

Snippets by name, without a recordPascalC++
procedure TFormOrders.btnArchiveClick(Sender: TObject);
begin
  ArchiveOrder;
  { The name is the only key: use the same string as in the registration. }
  ShowMessage(TranslateSnippet('Orders.Archived'));
end;

initialization
  { Name + default text in one call; no TTMSFNCSnippetRec constant needed. }
  CollectSnippet('Orders.Archived', 'The order has been archived.');
  CollectSnippet('Orders.Status.Open', 'Open');
  CollectSnippet('Orders.Status.Shipped', 'Shipped');
void __fastcall TFormOrders::btnArchiveClick(TObject *Sender)
{
  ArchiveOrder();
  // The name is the only key: use the same string as in the registration.
  ShowMessage(TranslateSnippet("Orders.Archived"));
}

// Name + default text in one call; no TTMSFNCSnippetRec constant needed.
// Run it once at startup (the Pascal initialization section).
static void RegisterOrderSnippets()
{
  CollectSnippet("Orders.Archived", "The order has been archived.");
  CollectSnippet("Orders.Status.Open", "Open");
  CollectSnippet("Orders.Status.Shipped", "Shipped");
}
#pragma startup RegisterOrderSnippets

The two forms are interchangeable: the record and the name-based call register the same entry, so a snippet registered by name can be read with a record of the same name, and the other way around. Names are compared case-insensitively. Choose by how often the text is used:

  • By name suits a string used in one place, or names you compute at run time, such as 'Orders.Status.' + StatusKey.
  • A record suits a string used in several places. The name and default text live in one declaration, so a typo in the name is caught by the compiler instead of at run time.

When no translation exists for the active language, TranslateSnippet('Name') falls back to the default text that was registered under that name. A name that was never registered and is not in the translation file returns an empty string, while the record overload always falls back to its own DefaultText.

To build a record at run time instead of declaring a constant, use the helper TTMSFNCSnippetRec.CreateSnippet('Name', 'Default text').

Number snippets and plurals

"1 item" / "5 items" looks like two strings, but that only holds in English. Polish uses three forms, and Arabic six. A number snippet, a TTMSFNCNumberSnippetRec, carries one text per plural slot: Zero, One, Two, Few, Many and Other. Each language's plural rule picks the slot for a given count. You fill in only the slots your source language uses. For English, that is One and Other:

Plural-aware counts with number snippetsPascalC++
const
  { English uses the One and Other slots; slots needed by the target
    languages are added by the collector, not declared here. }
  SnippetItemCount: TTMSFNCNumberSnippetRec = (
    Name: 'Sales.ItemCount';
    One: '%d item in the order.';
    Other: '%d items in the order.'
  );

var
  { Helpers are functions, so they initialize a variable, not a constant. }
  SnippetSelectedFiles: TTMSFNCNumberSnippetRec;

procedure TFormMain.UpdateCounts(AItems, AFiles: Integer);
begin
  { Picks the slot for AItems in the active language and formats it in. }
  lblItems.Text := TranslateNumberSnippetFormat(SnippetItemCount, AItems);

  { The array-of-const overload passes extra arguments to the text. }
  lblFiles.Text := TranslateNumberSnippetFormat(SnippetSelectedFiles, AFiles, [AFiles, FolderName]);
end;

initialization
  SnippetSelectedFiles := TTMSFNCNumberSnippetRec.CreateOneOther('Files.Selected',
    '%0:d file selected in %1:s.', '%0:d files selected in %1:s.');

  CollectNumberSnippet(SnippetItemCount);
  CollectNumberSnippet(SnippetSelectedFiles);
// English uses the One and Other slots; slots needed by the target
// languages are added by the collector, not declared here.
const TTMSFNCNumberSnippetRec SnippetItemCount = {
  "Sales.ItemCount", "", "%d item in the order.", "", "", "", "%d items in the order."};

// Initialized at startup, not as a constant.
TTMSFNCNumberSnippetRec SnippetSelectedFiles;

void __fastcall TFormMain::UpdateCounts(int AItems, int AFiles)
{
  // Picks the slot for AItems in the active language and formats it in.
  lblItems->Text = TranslateNumberSnippetFormat(SnippetItemCount, AItems);

  // The array-of-const overload passes extra arguments to the text.
  lblFiles->Text = TranslateNumberSnippetFormat(SnippetSelectedFiles, AFiles,
    ARRAYOFCONST((AFiles, FolderName)));
}

static void RegisterNumberSnippets()
{
  SnippetSelectedFiles.Name = "Files.Selected";
  SnippetSelectedFiles.One = "%0:d file selected in %1:s.";
  SnippetSelectedFiles.Other = "%0:d files selected in %1:s.";

  CollectNumberSnippet(SnippetItemCount);
  CollectNumberSnippet(SnippetSelectedFiles);
}
#pragma startup RegisterNumberSnippets

Register it with CollectNumberSnippet. The collector then gives every included language exactly the slots its plural rule needs. Adding Polish makes its Few and Many slots appear for translation, even though you declared only two. TranslateNumberSnippetFormat(Snippet, Count) picks the slot for Count in the active language and formats the count into it. The overload with an array of const lets the text use other arguments as well. TranslateNumberSnippet returns the unformatted slot text.

The record helpers build the common shapes in code: CreateOneOther, CreateZeroOneOther for a source language that has a zero category, and CreateSix for all slots explicitly. Because they are functions, use them to initialize a variable rather than a constant. The slot numbers follow the CLDR plural categories, and a language only ever uses the categories its rule defines. English, for example, maps a count of 0 to Other ("0 items"), so a Zero text is never shown for English.

Getting snippets into the editor

Snippets are registered when the application runs, because that is when initialization sections execute. The IDE never runs your units. To translate snippets, first get them into the translation file. Two ways work:

  • Run the editor in the application. Call Execute on a TTMSFNCLocalizationEditor from your running app, as the demos do from a button. The snippets appear under the Snippets and NumberSnippets roots next to your forms, and closing the editor saves them.
  • Let the collector save on exit. Set the collector's AutoSaveOnDestroy to True during development, run the application once, and close it. The registered snippets are written to translations.json, and the design-time editor shows them from then on.

The editor's list filter matches snippet names as well as their source and translated text, so typing Orders. narrows the list to one group.

Message dialog buttons (FMX)

FMX message dialogs build their button captions (Yes, No, Cancel, …) and titles from framework resource strings, not from a form you designed. TMS FNC Localization ships built-in snippets for them, named GlobalMsgDlgYes, GlobalMsgDlgNo, GlobalMsgDlgCancel and so on. They appear under Snippets without any declaration on your part. To apply them, wrap the dialog call in BeginLocalizedInterception / EndLocalizedInterception:

Translate FMX message dialog buttonsPascalC++
procedure TFormMain.ConfirmClose;
begin
  { While the interception is active, FMX dialog titles and button captions
    (Yes, No, Cancel, ...) use the GlobalMsgDlg* snippets. }
  BeginLocalizedInterception;
  try
    if MessageDlg(TranslateSnippet(SnippetCloseConfirm), TMsgDlgType.mtConfirmation,
      [TMsgDlgBtn.mbYes, TMsgDlgBtn.mbNo], 0) = mrYes then
      Close;
  finally
    EndLocalizedInterception;
  end;
end;
void __fastcall TFormMain::ConfirmClose()
{
  // While the interception is active, FMX dialog titles and button captions
  // (Yes, No, Cancel, ...) use the GlobalMsgDlg* snippets.
  BeginLocalizedInterception();
  try
  {
    if (MessageDlg(TranslateSnippet(SnippetCloseConfirm), TMsgDlgType::mtConfirmation,
      TMsgDlgButtons() << TMsgDlgBtn::mbYes << TMsgDlgBtn::mbNo, 0) == mrYes)
      Close();
  }
  __finally
  {
    EndLocalizedInterception();
  }
}

The interception routes the framework's dialog strings through the translated snippets for as long as it is active. Keep it tight around the dialog call.

Putting it together: a localized status bar

A typical use combines both snippet kinds with the form's language-change hook. A status label shows a plain message and a plural-aware count. Both are built in code, so the form re-renders them whenever the language changes:

A localized status barPascalC++
const
  SnippetStatusReady: TTMSFNCSnippetRec = (Name: 'Status.Ready'; DefaultText: 'Ready');
  SnippetStatusOrders: TTMSFNCNumberSnippetRec = (
    Name: 'Status.Orders';
    One: '%d open order';
    Other: '%d open orders'
  );

type
  TFormMain = class(TTMSFNCLocalizationForm)
    lblStatus: TLabel;
  private
    FOpenOrders: Integer;
    procedure UpdateStatus;
  protected
    procedure LanguageChange(Sender: TObject); override;
  public
    procedure SetOpenOrders(ACount: Integer);
  end;

procedure TFormMain.UpdateStatus;
begin
  { One place builds every code-generated string from snippets. }
  lblStatus.Text := TranslateSnippet(SnippetStatusReady) + ' - ' +
    TranslateNumberSnippetFormat(SnippetStatusOrders, FOpenOrders);
end;

procedure TFormMain.SetOpenOrders(ACount: Integer);
begin
  FOpenOrders := ACount;
  UpdateStatus;   { the data changed }
end;

procedure TFormMain.LanguageChange(Sender: TObject);
begin
  inherited;
  UpdateStatus;   { the language changed }
end;

initialization
  CollectSnippet(SnippetStatusReady);
  CollectNumberSnippet(SnippetStatusOrders);
const TTMSFNCSnippetRec SnippetStatusReady = {"Status.Ready", "Ready"};
const TTMSFNCNumberSnippetRec SnippetStatusOrders = {
  "Status.Orders", "", "%d open order", "", "", "", "%d open orders"};

class TFormMain : public TTMSFNCLocalizationForm
{
__published:
  TLabel *lblStatus;
private:
  int FOpenOrders;
  void __fastcall UpdateStatus();
protected:
  virtual void __fastcall LanguageChange(TObject *Sender);
public:
  void __fastcall SetOpenOrders(int ACount);
};

void __fastcall TFormMain::UpdateStatus()
{
  // One place builds every code-generated string from snippets.
  lblStatus->Text = TranslateSnippet(SnippetStatusReady) + " - " +
    TranslateNumberSnippetFormat(SnippetStatusOrders, FOpenOrders);
}

void __fastcall TFormMain::SetOpenOrders(int ACount)
{
  FOpenOrders = ACount;
  UpdateStatus();   // the data changed
}

void __fastcall TFormMain::LanguageChange(TObject *Sender)
{
  TTMSFNCLocalizationForm::LanguageChange(Sender);
  UpdateStatus();   // the language changed
}

static void RegisterSnippets()
{
  CollectSnippet(SnippetStatusReady);
  CollectNumberSnippet(SnippetStatusOrders);
}
#pragma startup RegisterSnippets

The pattern generalizes: keep one method that builds every code-generated string from snippets, call it after you change the underlying data, and call it again from LanguageChange.

Common pitfalls

  • Check the spelling of name-based snippets. TranslateSnippet('Orders.Archvied') compiles and returns an empty string. If a name is used in more than one place, declare a record constant so the compiler checks it.
  • Register before you translate. A snippet that is never passed to CollectSnippet / CollectNumberSnippet still returns its default text, but it never appears in the editor, so it cannot be translated. Register in the initialization section of the unit that declares it.
  • Snippets do not reach the design-time editor on their own. Run the application once with AutoSaveOnDestroy enabled, or open the editor from the running application, so the snippets are saved to translations.json.
  • Only approved translations are used. Like every other string, a snippet translation that is still Proposed is ignored at run time, and the default text is returned.
  • Refresh code-built text on a language change. TranslateSnippet returns the text for the language that is active when you call it. A label set at startup keeps the old language until you set it again. Override LanguageChange on a TTMSFNCLocalizationForm, or use the localizer's OnLanguageChange event.
  • Do not build sentences from fragments. Concatenating "Saved " + count + " orders" cannot be translated correctly, because word order differs between languages. Put the whole sentence in one snippet with format specifiers.
  • A "nothing here" message is not a plural form. In English (and most languages) 0 uses the Other slot. To show "No open orders" instead of "0 open orders", test for zero in code and use a separate plain snippet.
  • Declare every slot your source language uses. For English, fill both One and Other. A missing source slot leaves that count with no text to fall back on.

See also