Using scoped files
The collected model can be kept in a single file, or split across several. Roots — a form, a frame, a data module — are assigned to scope files, and anything not claimed by a scope lands in the shared file. Splitting is worth doing on a large application where different people own different areas: it keeps two translators out of the same file, makes merges tractable, and lets the running application read only the translations it actually needs. On a small project one file is simpler and there is no reason to split. This guide covers how the layout is stored, how to declare it at design time or in code, when each file is read, and the mistakes that cost you translations.
How the layout is stored
There is always a shared file, translations.json, in the folder named by
LocalizationFolder. It holds two things:
- the layout itself — the list of scope files, their roots and their load modes, so a scoped project needs no configuration beyond pointing at the folder
- every root that is not assigned to a scope file
Each scope file sits next to it and holds only its own roots. Marking one scope file Default moves the catch-all role to that file; the shared file then carries the layout alone.
Snippets and NumberSnippets are pinned to the shared file and cannot be assigned to a scope. Code-declared strings are typically used from anywhere in the application, so scoping them would mean loading most scopes anyway.
Naming roots
A root is addressed by its category and module name:
| Root path | Refers to |
|---|---|
Forms.frmOrders |
the form frmOrders |
Frames.FrameOrderLines |
every instance of the frame class TFrameOrderLines |
DataModules.dmSecurity |
the data module dmSecurity |
A frame root is named after its class with the leading T dropped, not after any instance of it, because one root is shared by every instance — see Frames.
Only roots that actually carry collected content can be assigned; an empty root has nothing to write.
Assigning roots to files
At design time, open the collector's files editor by clicking the LocalizationFiles property. It lists the configured files on the left and, for the selected file, its name, file name, load mode and the Default file option. The two lists at the bottom move roots between Unassigned roots and the selected file with >> and <<, so the layout is built from the roots that were actually collected rather than from typed strings.
The editor warns about the two file names that lose work: pointing two scope files at the same file (only one of them is saved), and reusing the shared translations.json as a scope file name (the shared save overwrites it).
The same layout can be built in code — useful when the split follows a naming convention rather than a hand-picked list:
procedure TForm1.ConfigureScopedFiles;
var
vSales, vAdmin: TTMSFNCLocalizationFile;
begin
{ Everything is written under this folder. }
Collector.LocalizationFolder := '{APP}\Translations';
Collector.LocalizationFiles.Clear;
{ One file per functional area. Roots are named <Category>.<ModuleName>. }
vSales := Collector.LocalizationFiles.Add;
vSales.Name := 'Sales';
vSales.FileName := 'sales.json';
vSales.Roots.Add('Forms.frmOrders');
vSales.Roots.Add('Forms.frmInvoice');
vSales.LoadMode := flmAutomatic;
vAdmin := Collector.LocalizationFiles.Add;
vAdmin.Name := 'Administration';
vAdmin.FileName := 'admin.json';
vAdmin.Roots.Add('Forms.frmUsers');
vAdmin.Roots.Add('DataModules.dmSecurity');
vAdmin.LoadMode := flmAutomatic;
{ Writes the layout plus every unassigned root to the shared translations.json,
and each scope file next to it. }
Collector.SaveSettings;
end;
SaveSettings writes the shared file and every scope file in one pass. A root that is assigned to a file that has no FileName is treated as unassigned.
Load modes
The load mode decides when a scope file is merged into the model. It is set per file, and the run-time side honours it:
| Mode | The file is read |
|---|---|
flmAutomatic |
lazily, the first time a root it owns is localized. The default, and the reason splitting reduces start-up work |
flmStartup |
immediately after the shared file, before anything is translated |
flmManual |
never on its own |
Use flmStartup for anything translated before its own form exists: the main menu, splash text, strings applied during start-up. Use flmAutomatic for everything reached through a form the user opens.
procedure TForm1.ApplyLoadModes;
var
I: Integer;
vFile: TTMSFNCLocalizationFile;
begin
for I := 0 to Collector.LocalizationFiles.Count - 1 do
begin
vFile := Collector.LocalizationFiles[I];
{ Anything translated before its own form exists must be there from the start. }
if SameText(vFile.Name, 'Shell') then
vFile.LoadMode := flmStartup
else
vFile.LoadMode := flmAutomatic;
end;
{ A scope the application merges itself, for example an add-on pack. }
vFile := Collector.LocalizationFiles.FindByName('Plugins');
if Assigned(vFile) then
vFile.LoadMode := flmManual;
Collector.SaveSettings;
end;
Reading the layout at run time
In TTMSFNCLocalizationLocalizer, pointing its
LocalizationFolder at the shipped folder is enough, because the layout travels in the shared file: reading translations.json adopts the embedded file list. A collection configured on the component before that read stays authoritative and is not replaced.
Pitfalls
- Two files with the same
FileName. Only one of them is saved and the other file's roots are lost. The design-time editor flags this; a layout built in code does not. - A scope file named
translations.json. The shared save writes that name last and overwrites the scope content. - Renaming a form after the split. The root path changes, so the old entry stays in its scope file and the renamed form falls back to the catch-all. Fix the assignment, or relink the root from the editor's tree.
- Shipping the scope files but not the shared file. Without
translations.jsonthere is no layout and no catch-all content, so nothing loads.