Collecting translatable text
Before anything can be translated it has to be found. The collector is the component that finds it: it walks the application's forms and components, decides which properties are worth translating, and builds the set of strings the Localization Editor shows and the Localizer applies. Most of the time you drive it from the editor at design time, but the same API is available in code for build steps, custom tooling, or apps that assemble their UI dynamically. This guide covers running a scan, narrowing what is collected, persisting the result, choosing where it is stored, and reacting when the collected set changes. Text declared in resourcestring sections is collected differently, from the compiler's output; see Translating resourcestrings. If you have not added the component yet, start with Getting started.
Analyzing the application
AnalyzeApplication gathers the translatable properties from the forms and data modules currently registered with the running application, including any frames those forms host. It enumerates the application's global form list, so it only sees forms that have already been created — forms not yet instantiated are skipped. Re-run it after creating additional forms to pick them up, or scan a form explicitly with AnalyzeFormModule the moment you create it. Call ReApplyRules to re-evaluate the existing set against the current rules without rescanning.
When you only need part of the application, scan a single form with AnalyzeFormModule, or one component (with a category label) using AnalyzeComponent.
AnalyzeApplicationis a VCL/FMX capability. It relies on the application's global list of forms, which TMS WEB Core does not provide — on the web only the main form is scanned. For web applications (and for any form you want localized regardless of when it is created), derive the form fromTTMSFNCLocalizationForm, which localizes each form on its own, instead of relying onAnalyzeApplication. See Automatic per-form translation.
Collecting while a translation is active
A scan can run at any moment, including after the user switched the application to another language. That raises a question the collector has to answer carefully: a control that the localizer has translated now shows the translation, and taking that text as the source would corrupt the entry. So at run time the collector reads each property's source text once per session. A property it has already collected, or one the localizer has already translated, keeps the source value it has; only properties met for the first time are read from the control. Re-running AnalyzeApplication after a language switch, or after creating a new form, is therefore safe: new controls are added, translated ones are left alone.
The flip side is that changing a control's text in code after it was collected does not update its source value during that session. The design-time collection in the Localization Editor always reads every property again, so it is the authoritative source for text that changes.
Collecting a frame's own module
Frames get their own root under Frames, keyed by class so that every instance of a frame shares one set of translations — see Frames for the model. When AnalyzeApplication or AnalyzeFormModule meets a frame it has not seen before, it seeds that shared root from the instance it happened to find, which makes the frame usable straight away but bakes in whatever that one host form customised.
AnalyzeFrameModule replaces the guess with the real thing: hand it the frame as it exists in its own designer and it collects that as the authoritative base. Do this before scanning the forms that host the frame, so instance text is measured against the frame as designed and only genuine overrides are stored under each form:
procedure TDataModule1.CollectWithFrames;
var
vFrame: TFrameOrderLines;
begin
{ The frame as designed, not as customised by any host form. Create it on its
own and hand it to the collector before the forms that use it are scanned. }
vFrame := TFrameOrderLines.Create(nil);
try
Collector.AnalyzeFrameModule(vFrame);
finally
vFrame.Free;
end;
{ Host forms are scanned afterwards: each one now contributes only the
properties it actually overrides on its own copy of the frame. }
Collector.AnalyzeApplication;
Collector.SaveToFile('translations.json');
end;
void __fastcall TDataModule1::CollectWithFrames()
{
// The frame as designed, not as customised by any host form. Create it on its
// own and hand it to the collector before the forms that use it are scanned.
TFrameOrderLines *vFrame = new TFrameOrderLines(nullptr);
try
{
Collector->AnalyzeFrameModule(vFrame);
}
__finally
{
delete vFrame;
}
// Host forms are scanned afterwards: each one now contributes only the
// properties it actually overrides on its own copy of the frame.
Collector->AnalyzeApplication();
Collector->SaveToFile("translations.json");
}
An existing base is never overwritten from an instance, so scanning the host forms afterwards cannot undo it.
Narrowing what is collected
Not everything on a form should be translated — brand names, product codes, and format strings usually should not. Use Exclude to keep an object out of the collected set and UnExclude to bring it back. Combine a targeted AnalyzeFormModule with a few exclusions to control exactly what ends up in the translation set:
procedure TForm1.ConfigureCollection;
begin
{ Scan just this form instead of the whole application. }
Collector.AnalyzeFormModule(Self);
{ Keep specific controls out of translation (for example a brand name). }
Collector.Exclude(lblBrandName);
Collector.Exclude(edtProductCode);
{ Bring one back into scope later if needed. }
Collector.UnExclude(edtProductCode);
{ Persist the collected set. }
Collector.SaveToFile('translations.json');
end;
void __fastcall TForm1::ConfigureCollection()
{
// Scan just this form instead of the whole application.
Collector->AnalyzeFormModule(this);
// Keep specific controls out of translation (for example a brand name).
Collector->Exclude(lblBrandName);
Collector->Exclude(edtProductCode);
// Bring one back into scope later if needed.
Collector->UnExclude(edtProductCode);
// Persist the collected set.
Collector->SaveToFile("translations.json");
}
Saving and loading the set
Persist the collected set with SaveToFile (or SaveToStream) and reload it with LoadFromFile / LoadFromStream; the default format is JSON. SaveSettings and LoadSettings do the same against the folder named by LocalizationFolder: the shared translations.json plus any scope files. A collector placed on a form or data module calls LoadSettings by itself when the application starts; one created in code does not, so call it before working with the data. Set AutoSaveOnDestroy := True to have the collected set written automatically when the component is destroyed, so an interactive collection session is never lost.
procedure TDataModule1.PersistCollection;
begin
{ Save the collected set (JSON by default). }
Collector.SaveToFile('translations.json');
end;
procedure TDataModule1.RestoreCollection;
begin
{ Reload a previously-saved set. }
if TFile.Exists('translations.json') then
Collector.LoadFromFile('translations.json');
end;
void __fastcall TDataModule1::PersistCollection()
{
// Save the collected set (JSON by default).
Collector->SaveToFile("translations.json");
}
void __fastcall TDataModule1::RestoreCollection()
{
// Reload a previously-saved set.
if (TFile::Exists("translations.json"))
Collector->LoadFromFile("translations.json");
}
Choosing a writable translation folder
LocalizationFolder decides where the collector reads and writes the translation data. It accepts a prefix token so the path resolves per machine and per user rather than being hard-coded:
| Token | Expands to |
|---|---|
{DEPLOY} |
The folder that files deployed with the application end up in. This is also what an empty LocalizationFolder means. |
{APP} |
The folder holding the executable. |
{DOC} |
The current user's documents folder. |
{HOME} |
The current user's application-support folder. |
{PF} |
The Program Files folder (Windows only). |
Token names are not case-sensitive, and a token that is not recognized is left in the path unchanged. After expansion, a path that is still relative is resolved against {DEPLOY}; an absolute path is used as-is.
On Windows {DEPLOY} and {APP} are the same folder. They differ on macOS, iOS, and Android, where the IDE's deployment places added files inside the application bundle: {DEPLOY} points at them there (Contents/Resources/StartUp on macOS, the bundle root on iOS, the documents path on Android), while {APP} keeps pointing beside the bundle. Use {DEPLOY}, or leave the value empty, for translation files that ship with the application, and one setting works on every target. In TMS WEB Core applications the tokens are not expanded; the value is used as given.
The collector writes here, so the folder must be writable.
{APP}for an application installed under Program Files, and{PF}itself, are read-only for a standard user — saving the collected set there fails. Use{HOME}or{DOC}for translation data that is edited or saved at run time, and reserve{DEPLOY}or{APP}for translations that ship read-only with the application.
Set the same folder on the localizer that reads the data back:
procedure TDataModule1.DataModuleCreate(Sender: TObject);
begin
// The HOME token expands to the current user's application-support folder,
// which a standard user can write to. The collector saves the translation
// data, so its folder has to be writable - Program Files is not.
Collector.LocalizationFolder := '{HOME}\Translations';
// The localizer only reads, but it has to look where the collector wrote,
// so give it the same value.
Localizer.LocalizationFolder := '{HOME}\Translations';
// The localizer read its design-time folder before OnCreate ran, so read
// the new folder now.
Localizer.RescanLanguages;
// Translations that ship with the application and are never written back
// can live next to the executable instead:
// Localizer.LocalizationFolder := '{APP}\Translations';
end;
void __fastcall TDataModule1::DataModuleCreate(TObject *Sender)
{
// The HOME token expands to the current user's application-support folder,
// which a standard user can write to. The collector saves the translation
// data, so its folder has to be writable - Program Files is not.
Collector->LocalizationFolder = "{HOME}\\Translations";
// The localizer only reads, but it has to look where the collector wrote,
// so give it the same value.
Localizer->LocalizationFolder = "{HOME}\\Translations";
// The localizer read its design-time folder before OnCreate ran, so read
// the new folder now.
Localizer->RescanLanguages();
// Translations that ship with the application and are never written back
// can live next to the executable instead:
// Localizer->LocalizationFolder = "{APP}\\Translations";
}
Running from the IDE
While you develop, the translation data lives in the project folder: that is where the Localization Editor reads and saves it at design time. On Windows, once the editor has been opened for a project, an executable started from the IDE uses the same files. It reads and writes the translation data in the project folder (the folder of the .dproj), and the separate language files in its runtime language files subfolder, instead of resolving LocalizationFolder. Each platform and configuration builds into its own output folder, so this keeps one set of files instead of a stale copy beside every executable, and a translation edited in the editor is what the next run shows.
The same executable started any other way, from Explorer, an installer, or a deployment, resolves LocalizationFolder as described above. Deploy the translation files with the application, and test a release from its installed location rather than only from the IDE.
Reacting to changes
The OnChange event fires whenever the collected set changes — a scan added items, a rule was re-applied, or an object was excluded. Use it to persist automatically or to update tooling that reflects the collection.
Putting it together: scan, react, and persist
A self-maintaining collection setup analyzes the application, saves whenever the set changes, and needs no further intervention:
procedure TDataModule1.DataModuleCreate(Sender: TObject);
begin
Collector.LocalizationFolder := 'Translations';
Collector.OnChange := HandleCollectionChange;
{ Gather the translatable strings; OnChange fires as the set is built. }
Collector.AnalyzeApplication;
end;
procedure TDataModule1.HandleCollectionChange(ASender: TObject;
AChanges: TTMSFNCLocalizationChanges);
begin
{ The collected set changed - persist it. }
Collector.SaveToFile('translations.json');
end;
void __fastcall TDataModule1::DataModuleCreate(TObject *Sender)
{
Collector->LocalizationFolder = "Translations";
Collector->OnChange = HandleCollectionChange;
// Gather the translatable strings; OnChange fires as the set is built.
Collector->AnalyzeApplication();
}
void __fastcall TDataModule1::HandleCollectionChange(TObject *ASender,
TTMSFNCLocalizationChanges AChanges)
{
// The collected set changed - persist it.
Collector->SaveToFile("translations.json");
}
Common pitfalls
- Collected text does not follow run-time changes. At run time each property is read once per session, so a scan after a language switch never mistakes a translation for source text, but text you change in code after the first scan is not picked up either. Collect text that changes at design time.
- The IDE run is not the deployed run. An executable started from the IDE uses the translation files in the project folder; started anywhere else it uses
LocalizationFolder. Deploy the files and test outside the IDE before shipping. - Analyze after the UI exists.
AnalyzeApplicationcollects only the forms already registered with the application when it runs; call it after forms/components are created, and re-run it when the UI changes. Forms that have not been instantiated yet are not scanned. AnalyzeApplicationdoes not cover TMS WEB Core. The web runtime has no global form list, so only the main form is scanned there — useTTMSFNCLocalizationFormfor the rest.- Collect a frame's own module before its host forms. A frame first met as an instance seeds its shared base from that instance, so one form's customisations can become the baseline every other form is measured against. Call
AnalyzeFrameModuleon the frame itself to set the authoritative base. - Exclude what must not be translated. Brand names, codes, and format strings should be excluded so a translator cannot accidentally break them.
- Persist your work. An interactive collection session is only kept if it is saved — use
SaveToFileorAutoSaveOnDestroy. - Point
LocalizationFoldersomewhere writable. The collector saves into this folder, so resolving it to a read-only install location ({DEPLOY}or{APP}under Program Files, or{PF}) loses the work. Prefer{HOME}or{DOC}. - Forms do not contain resourcestrings. Messages declared in
resourcestringsections are not component properties, so no scan finds them. Import them from the compiler's.drcfile; see Translating resourcestrings.