Translating resourcestrings
Most of an application's text lives on its forms, and the collector finds that by walking components. A good part does not: the confirmation in a message box, the text of an exception, a status line assembled in code. In Delphi those strings are usually declared in a resourcestring section, and a resourcestring is not a component property, so no form scan can ever see it. Resourcestring support closes that gap without touching your source: the compiler already lists every resourcestring it links into the binary in a .drc file, the collector reads that list into the translation project, and at run time the translations are substituted when the resourcestring is read. Reach for it when existing code already uses resourcestrings, or when you want message texts translated with the same editor, files, and language switch as the forms. For new code that needs plural forms or named, reusable strings, consider snippets instead; both can be used in one application.
How resourcestrings reach the translation project
Three stages, each handled by a different part of the product:
| Stage | Who does it | What happens |
|---|---|---|
| Discover | The compiler and the collector | The compiler writes a .drc file listing every resourcestring with its unit, identifier, and source text. The collector reads it and adds the application's own entries under a ResourceStrings root, one element per unit. |
| Translate | The Localization Editor | The entries appear in the tree next to the forms and are translated like any other text. They are saved in the translation data and travel in the language files. |
| Apply | The Localizer | Once the localizer has loaded translation data that contains resourcestrings, every read of a resourcestring returns the text for the active language. |
No source code is parsed, and nothing in your units has to change. A resourcestring declared and used the ordinary way is all the product needs:
unit uOrderMessages;
interface
resourcestring
SOrderSaved = 'Order %d was saved.';
SOrderNotFound = 'Order %d could not be found.';
SOrderDeleteConfirm = 'Delete order %d?';
function OrderSavedText(AOrderId: Integer): string;
function OrderNotFoundText(AOrderId: Integer): string;
implementation
uses
System.SysUtils;
function OrderSavedText(AOrderId: Integer): string;
begin
// Every read of a resourcestring goes through the RTL's resource lookup,
// which is where the translation is substituted. Reading it at the moment
// the text is needed therefore always yields the active language.
Result := Format(SOrderSaved, [AOrderId]);
end;
function OrderNotFoundText(AOrderId: Integer): string;
begin
Result := Format(SOrderNotFound, [AOrderId]);
end;
end.
// C++Builder has no resourcestring keyword: keep the resourcestring section in a
// Delphi unit that is part of the project, and use it from C++ through the
// header the compiler generates for that unit.
#include "uOrderMessages.hpp"
void __fastcall TForm1::ReportSaved(int AOrderId)
{
// A function of the Delphi unit that formats the resourcestring.
ShowMessage(Uordermessages::OrderSavedText(AOrderId));
// A resourcestring read directly: LoadResourceString goes through the same
// RTL lookup, so it returns the translation as well.
ShowMessage(Format(LoadResourceString(&Uordermessages::_SOrderDeleteConfirm),
ARRAYOFCONST((AOrderId))));
}
The shipped application needs no collector for this. The resourcestring entries arrive in the translation files like the rest of the text, and the localizer picks them up when it loads them.
Enabling the .drc output
The .drc file is the only source of resourcestrings, so the compiler has to be asked for it. In Project > Options > Building > Delphi Compiler > Linking, turn on Output resource string .drc file (setting Map file to Detailed produces it as well), then build. The compiler writes <project>.drc beside the executable, for Win32, Win64, and macOS targets.
The Localization Editor's Info tab checks this for you: it reports when the option is off, when the .drc file cannot be found, and when it is older than the project's source files. Nothing is reported as an error when the option is simply off; the project just has no resourcestrings in it, which looks exactly like a project that does not use any. Check the Info tab first whenever resourcestrings are missing from the tree.
The file is read from the folder the IDE builds the executable into, for the platform and configuration selected in the IDE. When a build script moves it elsewhere, name it with ResourceStringsFile, and use LocalizeResourceStrings (on by default) to keep resourcestrings out of the project entirely:
procedure TDataModule1.DataModuleCreate(Sender: TObject);
begin
{ On by default. Turn it off to keep resourcestrings out of the
translation project altogether. }
Collector.LocalizeResourceStrings := True;
{ Empty - the normal case - means the .drc the compiler writes beside the
executable. Set it only when a build script moves the file, and use an
absolute path: a relative one is resolved against the current directory. }
Collector.ResourceStringsFile := 'C:\Builds\OrderDesk\Win64\Release\OrderDesk.drc';
end;
void __fastcall TDataModule1::DataModuleCreate(TObject *Sender)
{
// On by default. Turn it off to keep resourcestrings out of the
// translation project altogether.
Collector->LocalizeResourceStrings = true;
// Empty - the normal case - means the .drc the compiler writes beside the
// executable. Set it only when a build script moves the file, and use an
// absolute path: a relative one is resolved against the current directory.
Collector->ResourceStringsFile = "C:\\Builds\\OrderDesk\\Win64\\Release\\OrderDesk.drc";
}
What the editor brings in
Every time the Localization Editor opens for a project, the collector re-reads the .drc file. Two rules decide what ends up in the tree:
- Only the project's own units. The
.drclists the resourcestrings of the RTL and the frameworks too, thousands of them. The import keeps only the units the project compiles, including plain units without a form, so the translation project holds your messages and nothing else. - Removal only from a fresh file. A resourcestring that is no longer in the
.drcis retired, together with its translations, but only when the.drcis newer than every source file in the project. Against an older file, the import only adds: during development the sources are usually newer than the last build, and the last build's list is not proof that a resourcestring was removed.
So the routine is: build, then open the editor. A resourcestring you just added appears after the next build; one you deleted disappears after the next build.
The VCL and FMX message dialog texts (SMsgDlgOK, SMsgDlgCancel, SMsgDlgConfirm, and the rest) are a special case. They are RTL resourcestrings, so they are not imported, but the product ties each of them to one of its built-in dialog snippets. Translate the snippet, and MessageDlg buttons and titles follow the active language without any resourcestring import.
Importing at run time
ImportResourceStrings is the run-time counterpart of the editor's import. A collection pass in a running application finds every caption on every form but no resourcestrings, so call it whenever the translation project is built or refreshed in code: a custom collection tool, a build step, or an in-house translation mode.
The parameterless form reads ResourceStringsFile, or the .drc next to the executable when that is empty, and returns how many entries it read; 0 means there was no file or LocalizeResourceStrings is False. It decides which units belong to the application from the model itself: units already under ResourceStrings, and the units the collected forms, frames, and data modules came from. That is why it runs after AnalyzeApplication. Resourcestrings in units without a form are reached through the overload that takes the unit names:
procedure TForm1.CollectTranslatableText;
var
vCount: Integer;
begin
{ Forms first: the units they were collected from tell the parameterless
ImportResourceStrings which .drc entries belong to the application. }
Collector.AnalyzeApplication;
{ Reads ResourceStringsFile, or <executable>.drc when that is empty.
Returns 0 when there is no file or LocalizeResourceStrings is False. }
vCount := Collector.ImportResourceStrings;
{ Units without a form are unknown to the model - name them explicitly,
spelled as the .drc spells them. }
vCount := vCount + Collector.ImportResourceStrings('', ['uOrderMessages', 'uReportTexts']);
if vCount = 0 then
ShowMessage('No .drc found - enable "Output resource string .drc file" and rebuild.')
else
Collector.SaveSettings;
end;
void __fastcall TForm1::CollectTranslatableText()
{
// Forms first: the units they were collected from tell the parameterless
// ImportResourceStrings which .drc entries belong to the application.
Collector->AnalyzeApplication();
// Reads ResourceStringsFile, or <executable>.drc when that is empty.
// Returns 0 when there is no file or LocalizeResourceStrings is false.
int vCount = Collector->ImportResourceStrings();
// Units without a form are unknown to the model - name them explicitly,
// spelled as the .drc spells them.
System::TArray<System::UnicodeString> vUnits;
vUnits.Length = 2;
vUnits[0] = "uOrderMessages";
vUnits[1] = "uReportTexts";
vCount += Collector->ImportResourceStrings("", vUnits);
if (vCount == 0)
ShowMessage("No .drc found - enable \"Output resource string .drc file\" and rebuild.");
else
Collector->SaveSettings();
}
Unit names are matched as the .drc spells them, which is the unit name as declared, with the dots of a dotted unit name restored.
Retiring removed resourcestrings in a build step
Both overloads take ARetireMissing, False by default. With True, every resourcestring the file does not list is marked obsolete, which drops it and its translations on the next save. That is correct only when the .drc is known to come from the build being processed, so reserve it for a build step that runs straight after compiling:
procedure TForm1.RefreshResourceStrings(const AProjectFolder, ADrcFile: string;
const AUnitNames: TArray<string>);
var
vCollector: TTMSFNCLocalizationCollector;
vCount: Integer;
begin
vCollector := TTMSFNCLocalizationCollector.Create(nil);
try
{ A collector created in code is not streamed, so nothing is loaded for
it automatically. }
vCollector.LocalizationFolder := AProjectFolder;
vCollector.LoadSettings;
{ True retires every resourcestring the file no longer lists. Only pass it
for a .drc that comes from the build being processed: against a stale
file it would mark live entries obsolete. }
vCount := vCollector.ImportResourceStrings(ADrcFile, AUnitNames, True);
if vCount > 0 then
vCollector.SaveSettings;
finally
vCollector.Free;
end;
end;
void __fastcall TForm1::RefreshResourceStrings(const System::UnicodeString AProjectFolder,
const System::UnicodeString ADrcFile, const System::TArray<System::UnicodeString> AUnitNames)
{
TTMSFNCLocalizationCollector *vCollector = new TTMSFNCLocalizationCollector(nullptr);
try
{
// A collector created in code is not streamed, so nothing is loaded for
// it automatically.
vCollector->LocalizationFolder = AProjectFolder;
vCollector->LoadSettings();
// true retires every resourcestring the file no longer lists. Only pass it
// for a .drc that comes from the build being processed: against a stale
// file it would mark live entries obsolete.
int vCount = vCollector->ImportResourceStrings(ADrcFile, AUnitNames, true);
if (vCount > 0)
vCollector->SaveSettings();
}
__finally
{
delete vCollector;
}
}
What happens at run time
The shipped application needs nothing beyond the localizer. Once it has loaded translation data that contains resourcestrings, every resourcestring your code reads returns the text for the current language. Only translations with a status in the localizer's AcceptedStatuses are used; any other entry shows the source text. After TrySetLocale the next read returns the new language, with nothing to reload.
What does not change is text that was read before the switch: a caption assigned from a resourcestring in FormCreate, a message cached in a string variable, a list filled at start-up. Those are copies and keep the language they were read in. Read the resourcestring again when the language changes, as the combination example below does.
Putting it together: resourcestrings, scoped files, and language switching
A larger application often splits its translations into scope files so each form's text is read only when the form is used. Resourcestrings fit into that layout without extra configuration: like snippets, the ResourceStrings root is pinned to the shared translations.json and cannot be assigned to a scope, so resourcestring translations are available from start-up on, before any scope file is read. The example builds such a project in code, collecting forms and resourcestrings in one pass, and refreshes a status line built from a resourcestring whenever the user switches language:
procedure TForm1.BuildTranslationProject;
var
vFile: TTMSFNCLocalizationFile;
begin
Collector.LocalizationFiles.Clear;
{ The order forms get their own file, read the first time one of them is
localized. Resourcestrings cannot be scoped: they always stay in the
shared translations.json, so they are available from start-up on. }
vFile := Collector.LocalizationFiles.Add;
vFile.Name := 'Orders';
vFile.FileName := 'orders.json';
vFile.Roots.Add('Forms.frmOrders');
vFile.LoadMode := flmAutomatic;
{ Collect the forms, then the resourcestrings of their units, then write
the shared file and the scope file in one pass. }
Collector.AnalyzeApplication;
Collector.ImportResourceStrings;
Collector.SaveSettings;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
Localizer.OnLanguageChange := HandleLanguageChange;
UpdateStatus;
end;
procedure TForm1.HandleLanguageChange(Sender: TObject);
begin
{ The localizer retranslates component properties itself. Text that code
built from a resourcestring is only a copy, so build it again. }
UpdateStatus;
end;
procedure TForm1.UpdateStatus;
begin
{ Read at this moment, the resourcestring returns the active language. }
StatusLabel.Text := Format(SStatusReady, [FOpenOrders]);
end;
void __fastcall TForm1::BuildTranslationProject()
{
Collector->LocalizationFiles->Clear();
// The order forms get their own file, read the first time one of them is
// localized. Resourcestrings cannot be scoped: they always stay in the
// shared translations.json, so they are available from start-up on.
TTMSFNCLocalizationFile *vFile = Collector->LocalizationFiles->Add();
vFile->Name = "Orders";
vFile->FileName = "orders.json";
vFile->Roots->Add("Forms.frmOrders");
vFile->LoadMode = TTMSFNCLocalizationFileLoadMode::flmAutomatic;
// Collect the forms, then the resourcestrings of their units, then write
// the shared file and the scope file in one pass.
Collector->AnalyzeApplication();
Collector->ImportResourceStrings();
Collector->SaveSettings();
}
void __fastcall TForm1::FormCreate(TObject *Sender)
{
Localizer->OnLanguageChange = HandleLanguageChange;
UpdateStatus();
}
void __fastcall TForm1::HandleLanguageChange(TObject *Sender)
{
// The localizer retranslates component properties itself. Text that code
// built from a resourcestring is only a copy, so build it again.
UpdateStatus();
}
void __fastcall TForm1::UpdateStatus()
{
// StatusReadyText reads the resourcestring from its Delphi unit
// (LoadResourceString), so it returns the active language at this moment.
StatusLabel->Text = Format(StatusReadyText(), ARRAYOFCONST((FOpenOrders)));
}
The two halves usually live in different builds: the collection half in a development or build tool, the language-switching half in the shipped application, which only needs the localizer.
Platform support
| Platform | Discovery (.drc) |
Translation at run time |
|---|---|---|
| Windows (VCL, FMX) | Yes | Yes |
| macOS (FMX) | Yes | Yes |
| iOS, Android, Linux (FMX) | No .drc is produced; translate from a Windows or macOS build of the same sources |
Yes |
| TMS WEB Core | No | No |
On TMS WEB Core resourcestrings are not translated and ImportResourceStrings is not available. Use snippets for text in code there.
Common pitfalls
- No
.drc, no resourcestrings. With Output resource string .drc file off, the import finds nothing and says nothing. Turn the option on, build, and check the editor's Info tab. - A stale
.drconly adds. Removed resourcestrings stay in the project until a build produces a.drcnewer than the sources. Build before you open the editor to tidy up. - Text read before the switch keeps its language. Captions, variables, and lists filled from a resourcestring are copies. Re-read the resourcestring in the localizer's
OnLanguageChange. - Text read before the translations are loaded stays in the source language. A resourcestring read before the localizer has loaded its translation data, during unit initialization for example, is not translated.
- Identical source texts can share one translation. When two resourcestrings have the same source text, the translation of the first one may be used for both, especially after a rebuild. Give strings that need different translations different source texts, and re-open the editor after building. The editor's Info tab lists the affected texts.
- The parameterless
ImportResourceStringsneeds known units. It filters the.drcby the units the model already knows. On an empty model there is nothing to filter by, so the whole file is imported, including the RTL's and frameworks' resourcestrings. Collect the forms first, or pass the unit names explicitly. - Retire only against a matching build.
ARetireMissing = Trueagainst a.drcfrom another build marks live entries obsolete, and the next save drops them with their translations. - Use an absolute
ResourceStringsFile. A relative path is resolved against the process's current directory, which in the IDE is rarely the project folder.
Related API
TTMSFNCLocalizationCollector.ImportResourceStringsTTMSFNCLocalizationCollector.LocalizeResourceStringsTTMSFNCLocalizationCollector.ResourceStringsFileTTMSFNCLocalizationLocalizer