Table of Contents

Core concepts of TMS FNC Localization

How the model is organised

The model is a tree. Below the root sit a small number of fixed categories:

  • Forms: one entry per form in the project
  • Frames: one entry per frame class, shared by every instance of that frame
  • DataModules: one entry per data module
  • Snippets: strings declared in code rather than found on a component

Under each root sits the component tree of that form, frame or data module: an entry per named component, and under each of those, an entry per translatable property. A property that is itself an object — a Font, a TStrings, a collection — becomes a child entry of its own rather than a dotted name, so what you see in the tree mirrors what you see in the form designer.

Each property carries the source text (the value as it exists in the form) and a translation per language. The source text is the anchor: when it changes in the designer, every translation of it is marked outdated, because a translation of text that no longer exists is worse than no translation at all.

Two kinds of state

This is the single concept most worth understanding before you start.

Item state answers should this string be translated at all? It belongs to the string, not to any language. A string can be left at its default, explicitly included, or manually ignored. Ignoring is how you tell the tool that a caption is a product name, a debug label, or something that must stay in English everywhere.

Translation status answers how good is this particular translation, in this particular language? Every translation carries one:

Status Meaning
New item Needs a translation
Proposed A translation exists but nobody has checked it
Approved Quality-checked and safe to ship
Rejected A translation exists and is not good enough
Excluded Do not use this translation
Outdated The source text changed after this was translated

Machine output always lands as Proposed, never Approved. Approved is a statement by a human that the string is safe to ship, and nothing automatic can make that statement on your behalf.

The workflow

Design-time

  1. Drop a collector on a form in the project.
  2. Set where the translation file lives via the LocalizationFolder property.
  3. Open the forms you want covered and open the editor; the collector picks them up and fills the tree.
  4. On the Languages tab modify the language list so that it matches the desired languages to translate to.
  5. On the Translations tab, select the language you want translations for and work through the strings. Filter by status to find what still needs attention. Translate by hand in the grid, with the AI assistance, or by exporting XLIFF for an outside translator and importing their result.
  6. Approve what you have checked. The grid supports multi-select, and Approve selected on its context menu sets the whole selection at once — so the usual move after an AI batch is to filter the status column to Proposed, read through, select all, and approve in one go.
  7. Ship the file with your application and let the localizer load it.

Coverage answers the question "Is what I see in the tree actually everything the project has?" The editor reads the project’s own list of modules and compares it against the model. Every root is then marked:

  • Collected: the project declares it and we have it
  • Not collected: the project declares it but it has never been scanned; it appears in the tree as a greyed placeholder so you can see the gap
  • Orphaned: the project still declares the unit, but no longer declares that form; a rename or a deletion
  • Not in project: nothing in the project declares its unit, so it may come from a shared package and we say only that we do not know

These placeholders are never saved and never exported. They exist purely to show you what is missing.

Two actions sit on the tree’s filter bar. Delete removes an orphaned root together with its translations, after a confirmation. Relink handles the rename case: it pairs a root that has disappeared with a newly declared module by comparing their property sets, and moves the translations across rather than making you retranslate. It never overwrites a translation the target already has, and it leaves the old root in place so you can check the result before deleting it.

What “Not in project” does and does not mean

The design-time scan only looks at modules that belong to the project. A form open in the IDE that belongs to a different project, or to no project at all, is skipped before anything is collected from it. It does not become a root, and it therefore never gets marked as anything.

So Not in project is never caused by a form you happen to have open. It only ever describes a root that is already in the model, which means it arrived from the file. In practice that is one of:

  • a form that was collected while it was part of the project, whose unit has since been removed from the project entirely — if the unit is still there and only the form name changed, you get Orphaned instead
  • a model file shared between projects, holding roots that belong to another one
  • a form that lives in a shared package rather than in the project

If the editor cannot work out which project it is looking at, coverage switches itself off rather than guessing: every root is reported as Collected and no placeholders appear. You get no coverage information, but you never get false alarms.

Forms that are not in the project file

A form reached through the search path (not listed in the project, but used in a unit’s uses clause and created at run time) is invisible to the design-time scan, for exactly the reason above. The project does not declare it, so it is not scanned, and no placeholder appears for it either, because placeholders are generated from the project’s declared modules.

That form is not lost, though. It is collected by the second route below, and once collected it is stored in the file like any other root. Coverage will then mark it Not in project: its unit is not declared, so the tool cannot tell a search-path form from a form in a shared package.

Runtime

Besides the design-time scan, the collector can gather strings from a running application. Left in the application and asked to analyse, it walks every form, popup form, frame and data module that currently exists and collects from the live instances, then writes the result to the file.

This route sees things the design-time scan cannot, because it works from what actually exists rather than from what the project declares:

  • forms reached through the search path rather than listed in the project
  • anything created dynamically that nonetheless has a design-time counterpart, including snippets and number snippets

Its limitation is that it only sees what has been created by the moment it runs. A dialog nobody opened during that session contributes nothing. In practice the two routes complement each other — collect at design time for coverage of the project, and run the application through its main screens once to pick up the rest.

Frames

A frame is not just another root. Forms and data modules are keyed by their instance — one form, one entry — but a frame is designed once and dropped onto many forms, and translating the same captions once per host form would be tedious and inconsistent. So a frame root is keyed by its class instead, with the leading T dropped: TFrameOrderLines is filed under Frames as FrameOrderLines, and every instance of it in the application shares that one root. Translate the frame once and every host form gets the translation. Frames work the same way in VCL, FMX and TMS WEB Core.

That leaves the question of what happens when a host form does change something on its own copy of a frame — a different caption on one screen. The model answers it with a base and an overlay:

  • The base is the frame as designed, collected from the frame's own module. It holds the frame's text.
  • Under a host form, only the overrides are stored: the properties that form changed relative to the base. A frame a form did not customise leaves no trace on that form at all.

At run time the Localizer applies the two in that order — base first, then the host form's overrides — so a property a form deliberately changed still wins over the frame as designed.

Because everything else is measured against the base, where the base comes from matters. When a frame is first met as an instance on a host form, the collector seeds the base from that instance, so the frame is usable without anyone having opened its module. That is a guess: the instance may carry that one form's customisations. An existing base is never rewritten from another instance — otherwise it would flip back and forth as different forms were scanned — so the guess is corrected in one place only, by collecting the frame's own module with AnalyzeFrameModule. The design-time editor does this for the frames the project declares.

Snippets

Not every string lives on a component. Messages, prompts and anything assembled at run time need somewhere to live, and that is the Snippets root. There are two kinds, and they are declared in different ways.

Plain snippets are a name and a default text in the source language and they are declared in code. An alternative to Snippets is the TTMSFNCLocalizationStringCatalog component. It holds the name-value entries you add at design time, and will appear in the editor under the containing form / data module, which makes it easier to use with less code.

Number snippets handle plurals, and similarly to Snippets, they are declared in code. Instead of one string they carry a slot per plural form, and the language decides which slot applies to a given count. This matters because languages disagree about how many forms exist — English has two, Polish has three, Arabic has six — so a single “1 item / n items” string cannot be translated correctly for all of them. You declare a number snippet as a record, and helper constructors cover the common shapes: a singular and a plural, a zero form as well, or the full six slots for languages that need them. The collector keeps only the slots the included languages actually use and asks the translator only for those; a slot that already holds work is never removed, even if the language that needed it has since been excluded.

AI translation

The AI assistance translates in batches, in the target language you select. Three things are worth knowing:

  1. Everything it produces lands as Proposed. Manual approval is still needed!
  2. Before a translation is accepted it is validated. A translation that would break a format specifier or lose an accelerator is stored as Rejected rather than applied, and you are notified if there were any rejected entries during the translation.
  3. It only offers strings that need work: new, rejected and outdated items, so re-running the AI translations does not re-translate what is already done.

Validation

Translations are checked against their source for a set of things that break applications rather than merely read badly:

  • Format specifiers: %s becoming %d, a specifier added or dropped, an argument index changed.
  • Brace placeholders: {name} style substitutions, for applications that use them
  • Accelerators: a lost or duplicated & gives you a dead keyboard shortcut
  • Line breaks: a multi-line label collapsed into one line
  • Whitespace: leading and trailing space that matters when strings are concatenated