In my book “Visual Programming” I discussed in detail the issues of localization and internationalization, it is time to apply this knowledge in a new project.
By default, My Visual Database detects the locale of the operating system and adapts to it. But, firstly, MVDB only supports two languages (Russian and English), secondly, the OS localization language is not always the desired language for the user, and thirdly, the application has components created by the user, as well as all kinds of test data added by scripts . Therefore, for applications that should be used all over the world, it is necessary to provide a convenient localization mechanism that does not require rewriting the code of the application itself.
For localization, text files will be used that store key=value pairs. But for more flexibility, two types of keys will be used:
- Static (form property value or system text resource)
- Dynamic (custom text resource)
Static key
These keys are processed once, when the program is started. After the value is assigned, the pair is removed from storage.
A static key has three types:
- R.<Form name>.<Property>
- R.<Form name>.<Component name.Property>
- T.<Text resource identifier>
The R or T prefix is used to designate a dynamic key. The Resource_L10App() procedure is used to process keys. The procedure is cumbersome, since in the process of processing it is necessary to bring
procedure Resource_L10App;
// localization of all application forms
var
i: integer;
tmpWords: array of string;
tmpForm: TForm;
tmpComponent: TComponent;
tmpValue: string;
// helper procedure that makes the code more compact
procedure RE;
begin
RaiseException('Resource_L10Form() Property not found '+tmpWords[RESOURCE_I_FORM]+'.'+tmpWords[RESOURCE_I_COMPONENT]+'.'+tmpWords[RESOURCE_I_PROPERTY]);
end;
begin
for i:=StringRes.Lines.Count - 1 downto 0 do
begin
tmpWords := SplitString( StringRes.Lines.Names(i) , '.');
// translation of words and phrases built into MVDB
if tmpWords[RESOURCE_I_PREFIX] = RESOURCE_TRANSLATE then
begin
tmpValue := StringRes.Lines.Strings(i);
Delete(tmpValue,1,Pos('=',tmpValue));
Translate( tmpWords[RESOURCE_I_IDENTIFIER],tmpValue );
StringRes.Lines.Delete(i);
end
else
// translation of static properties
if tmpWords[RESOURCE_I_PREFIX] = RESOURCE_PREFIX then
begin
if length(tmpWords)<3 then
RaiseException('Resource_L10Form() invalid data format: '+StringRes.Lines.Strings(i) );
//
tmpValue := StringRes.Lines.Strings(i);
Delete(tmpValue,1,Pos('=',tmpValue));
//
tmpForm := GetFormByName(tmpWords[RESOURCE_I_FORM]);
if tmpForm = nil then
RaiseException('Resource_L10Form() Form not found '+tmpWords[RESOURCE_I_FORM]);
//
if length(tmpWords)=3 then
begin
case UpperCase( tmpWords[RESOURCE_I_COMPONENT] ) of
'CAPTION': tmpForm.Caption := tmpValue;
else RaiseException('Resource_L10Form() Form property not found '+tmpWords[RESOURCE_I_FORM]+'.'+tmpWords[RESOURCE_I_COMPONENT]);
end;
end
else
begin
FindC(tmpForm,tmpWords[RESOURCE_I_COMPONENT],tmpComponent,False);
if tmpComponent = nil then
RaiseException('Resource_L10Form() Component not found '+tmpWords[RESOURCE_I_FORM]+'.'+tmpWords[RESOURCE_I_COMPONENT]);
// must be cast to a specific class
if (tmpComponent is TLabel) or (tmpComponent is TdbLabel) then
case UpperCase( tmpWords[RESOURCE_I_PROPERTY] ) of
'CAPTION': TLabel(tmpComponent).Caption := tmpValue;
'HINT': TLabel(tmpComponent).Hint := tmpValue;
else RE;
end
else
if (tmpComponent is TdbCheckBox) or (tmpComponent is TCheckBox) then
case UpperCase( tmpWords[RESOURCE_I_PROPERTY] ) of
'CAPTION': TCheckBox(tmpComponent).Caption := tmpValue;
'HINT': TCheckBox(tmpComponent).Hint := tmpValue;
else RE;
end
else
if (tmpComponent is TTabSheet) or (tmpComponent is TdbTabSheet) then
case UpperCase( tmpWords[RESOURCE_I_PROPERTY] ) of
'CAPTION': TTabSheet(tmpComponent).Caption := tmpValue;
'HINT': TTabSheet(tmpComponent).Hint := tmpValue;
else RE;
end
else
if tmpComponent is TGroupBox then
case UpperCase( tmpWords[RESOURCE_I_PROPERTY] ) of
'CAPTION': TGroupBox(tmpComponent).Caption := tmpValue;
else RE;
end
else
if tmpComponent is TRadioButton then
case UpperCase( tmpWords[RESOURCE_I_PROPERTY] ) of
'CAPTION': TRadioButton(tmpComponent).Caption := tmpValue;
else RE;
end
else
if (tmpComponent is TButton) or (tmpComponent is TdbButton) then
case UpperCase( tmpWords[RESOURCE_I_PROPERTY] ) of
'CAPTION': TButton(tmpComponent).Caption := tmpValue;
'HINT': TButton(tmpComponent).Hint := tmpValue;
else RE;
end
else
if tmpComponent is TMenuItem then
case UpperCase( tmpWords[RESOURCE_I_PROPERTY] ) of
'CAPTION': TMenuItem(tmpComponent).Caption := tmpValue;
else RE;
end
else
if tmpComponent is TdbEdit then
case UpperCase( tmpWords[RESOURCE_I_PROPERTY] ) of
'HINT': TdbEdit(tmpComponent).Hint := tmpValue;
'TEXTHINT': TdbEdit(tmpComponent).TextHint := tmpValue;
else RE;
end
else
RaiseException('Resource_L10Form() Unsupported class '+tmpWords[RESOURCE_I_FORM]+'.'+tmpWords[RESOURCE_I_COMPONENT]+' - '+tmpComponent.ClassName);
end;
StringRes.Lines.Delete(i);
end;
end;
end;
Code language: Delphi (delphi)
Dynamic key
Dynamic keys are in memory for the duration of the application, as they may be needed at any time.
The R() function is used to work with dynamic keys. The conciseness of its name is due to the fact that it will have to be used in all scripts that programmatically set properties to interface components.
function R(AName:string; ADefValue:string='???'):string;
// извлечение ресурсов
begin
if (StringRes <> nil) and (StringRes.Lines.IndexOfName(AName) < 0) then
Result := ADefValue
else
Result := StringRes.Lines.Values(AName);
end;
Code language: Delphi (delphi)
Usually the text value is not stored in the code, but placed in a constant:
const
RESOURCE_CHANGE_LANGUAGE = 'Switch language';
begin
Restart(True, RESOURCE_CHANGE_LANGUAGE);
Code language: Delphi (delphi)
To translate such a piece of code to use a language resource, a small and safe change is required:
ShowMessage(R('RESOURCE_CHANGE_LANGUAGE',RESOURCE_CHANGE_LANGUAGE));
Code language: Delphi (delphi)
The safety of the change is that if the specified resource is not found or the resource system is not involved, then the R() function will return the default value, which is in a constant. This trick is also used to create a “default” localization file – it can be empty, since all the necessary values will be taken from constants. But if you wish, you can fix the inscription without changing the code.
If you decide to store all text resources in a separate file and not use string constants to store default values inside the code, then it is acceptable to call the R() function without a second parameter.
ShowMessage(R('RESOURCE_CHANGE_LANGUAGE'));
Code language: Delphi (delphi)
This reduces the size of the source code, and in case you forgot to describe the corresponding text resource, three question marks will appear on the screen.
Resource initialization
During initialization, which occurs in the Resourse_Init() procedure, data is loaded into the TdbMemo class component, which can correctly work with files in UTF-8 format with BOM.
Important! Only the specified format must be used: UTF-8 with BOM
procedure Resource_Init();
// initialization
var
tmpFileList: array of string;
i: integer;
s: string;
tmpDir: string;
begin
tmpDir := ExtractFilePath(Application.ExeName)+RESOURCE_DIR;
if DirectoryExists(tmpDir) then
begin
// languages - by the number of files. Each file for its own language
Lаnguages := TStringList.Create;
StringRes := TdbMemo.Create(MainForm);
StringRes.Parent := MainForm;
StringRes.Visible := False;
StringRes.WordWrap := False;
tmpFileList := SplitString( GetFilesList(tmpDir,'*'+RESOURCE_EXT, False), chr(13) );
for i:=0 to Length(tmpFileList) -1 do
begin
s := Trim(ExtractFileName(tmpFileList[i]));
if s<>'' then
begin
delete(s,length(s)-3,4);
Lаnguages.Add(s);
end;
end;
// current language - read from settings
currentLаnguage := IniFile_Read( RESOURCE_INI_SECTION, RESOURCE_INI_CUR_LANG, '' );
i := Lаnguages.IndexOf(currentLаnguage);
if i = -1 then
i := 0;
if i = -1 then
RaiseException('Resurce_Init() - language not found: '+currentLаnguage)
else
begin
StringRes.Lines.LoadFromFile(tmpDir + '\'+ Lаnguages.Strings(i) + RESOURCE_EXT);
end;
// static localization
Resource_L10App;
end;
end;
Code language: PHP (php)
An example resource file:
APP_ABOUT_CAPTION=About
APP_CONFIRM_RESTART=A program restart is required to apply the changes. Execute now?
APP_COPYRIGHT=2023 Konstantin Pankov
APP_NAME=Developer's guide
DTF_CLASSEVENT_CAPTIONS=Name,Description
DTF_CLASSTYPE_TREE_CAPTIONS=Name
DTF_OPERATOR_TREE_CAPTIONS=Name,Description
DTF_PROJECT_TREE_CAPTIONS=Name,Path,Status,Description
DTF_PROPERTY_CAPTIONS=Name,Type,Description
DTF_PROPERTY_VARIABLELIST_CAPTIONS=Name,Type,Description
DTF_TASK_TREE_CAPTIONS=Name
DTF_TYPECONST_CAPTIONS=Name,Description
R.efmClassEvent.Caption=Event
R.efmClassEvent.btnCancel.Caption=Cancel
R.efmClassEvent.btnNew_Example.Hint=Add...
R.efmClassEvent.btnSave.Caption=Save
R.efmClassEvent.labDescription.Caption=Description
R.efmClassEvent.labExample.Caption=Example
R.efmClassType.btnSave.Caption=Save
R.efmClassType.labDescription.Caption=Description
Code language: SQL (Structured Query Language) (sql)
Language switching
To change the language, we need a menu item. To create it, the Resource_CreateMenu() procedure is used, and the procedure for setting the selected language Resource_SetLanguage() is called in the click handler
procedure Resource_CreateMenu( AForm:TForm );
// creating menu item "Language"
var
i: integer;
tmpItem: TMenuItem;
tmpTopItem: TMenuItem;
begin
// create a top level menu item
tmpTopItem := TMenuItem.Create( AForm );
tmpTopItem.Name := 'mniLanguage';
tmpTopItem.Caption := R('RESOURCE_MENU_LANGUAGE',RESOURCE_MENU_LANGUAGE);
AForm.Menu.Items.Insert(1,tmpTopItem);
// add menu items to change language
for i := 0 to Lаnguages.Count - 1 do
begin
tmpItem := TMenuItem.Create( AForm );
tmpItem.Name := 'mniSetLanguage_'+IntToStr(i);
tmpItem.Caption := Lаnguages.Strings(i);
tmpItem.OnClick := @Resource_MenuItem_OnClick;
tmpItem.RadioItem := True;
tmpItem.GroupIndex := 1;
tmpItem.Autocheck := True;
// highlight current language
if Lаnguages.Strings(i) = currentLаnguage then
tmpItem.Checked := True;
tmpTopItem.Insert(i,tmpItem);
end;
end;
procedure Resource_MenuItem_OnClick (Sender: TObject; );
// Click on the language selection menu item
begin
// remove special characters that were added automatically
Resource_SetLanguage( ReplaceStr(TMenuItem(Sender).Caption,'&','') );
end;
Code language: PHP (php)
Due to the presence of dynamically changing text resources, changing the language requires an application restart. Restart may be needed in other cases, so it is wise to provide a separate Restart() procedure for it, which displays a confirmation form before performing a restart.
procedure Resource_SetLanguage(AName:string);
// set language
// changes will take effect after reboot
begin
IniFile_Write( RESOURCE_INI_SECTION, RESOURCE_INI_CUR_LANG, AName );
Restart(True, R('RESOURCE_CHANGE_LANGUAGE',RESOURCE_CHANGE_LANGUAGE))
end;
procedure Restart(AConfirm:boolean = False; AReason:string);
begin
if (not AConfirm) or (MessageBox( R('APP_CONFIRM_RESTART',APP_CONFIRM_RESTART) ,AReason,MB_OKCANCEL ) = mrOK) then
begin
frmMain.Close; // close the current application. In fact, this command only creates a message, in fact, the closing will occur after the completion of the current procedure.
OpenFile( Application.ExeName ); // application launch
end;
end;
Code language: Delphi (delphi)

Known Issues
This localization does not end there. There is a small list of components and properties with a non-trivial way to access them, which also needs to be processed. This mainly applies to table headers and drop-down lists on service forms. You also need to localize context menus for tables, trees, and other components. The most difficult operation is localizing the translation of the authentication error message on the password and login form, which will require replacing the standard button. So the topic remains open.
Bonus
The application makes extensive use of dynamic forms from the DTF.pas module. These forms were created in the UserApp_InitForm() procedure, but due to the uniformity of string parameters, I decided to add the DTF_CreateForms() procedure, which does all the work of creating forms based on the dforms.ini text file, and at the same time uses the newly created localization system.
procedure DTF_CreateForms;
// creation of dynamic forms according to the list located in the dforms.ini file
var
tmpIniFile: TIniFile;
tmpFormList:TStringList;
i:integer;
tmpForm: TForm;
tmpSName: string;
function S(AName:string):string;
begin
Result := tmpIniFile.ReadString( tmpFormList.Strings(i), AName, '' );
end;
begin
tmpIniFile := TIniFile.Create( ExtractFilePath(Application.ExeName)+'dforms.ini' );
tmpFormList := TStringList.Create;
tmpIniFile.ReadSections(tmpFormList);
for i:=0 to tmpFormList.count - 1 do
begin
tmpSName := 'DTF_'+UpperCase(tmpFormList.Strings(i))+'_CAPTIONS';
tmpForm := DTF_Create(tmpFormList.Strings(i),S('table'),S('fields'), R( tmpSName,S('captions')),S('sort'),S('parentField'),S('filter'),S('isDetail')='True');
Form_ShowOnWinControl( tmpForm, TWinControl( FindCF( S('parentControl') ) ) );
end;
tmpFormList.Free;
tmpIniFile.Free;
end;
Code language: Delphi (delphi)
The structure of the dforms.ini file is simple enough to add new forms, if necessary, directly in a text editor.
[Keyword]
table=keyword
fields=keyword.name,keyword.Description
captions= Name, Description
sort=keyword.name
parentControl=frmMain.tshKeyword
[Operator_Tree]
table=operator
fields=operator.name,operator.Description
captions= Name, Description
sort=ParentID,name
parentField=parentID
parentControl=frmMain.tshOperator
[ClassType_TypeList]
table=classType
fields=classType.name,classType.Description
captions= Name, Description
sort=classType.name
filter=isType = 1
parentControl=frmMain.panType
Code language: SQL (Structured Query Language) (sql)
And FindCF() has been added to the collection of useful functions, which can find components on the form by their full path <Form name>.<Component name>
function FindCF(AName:string): TComponent;
// finds the component by full name
var
tmpForm:TForm;
tmpWords: array of string;
begin
tmpWords := SplitString(AName,'.');
if Length(tmpWords) < 2 then
RaiseException('FindCF('+AName+') invalid argument');
tmpForm := GetFormByName( tmpWords[0] );
if tmpForm = nil then
RaiseException('FindCF('+AName+') Form not found '+tmpWords[0]);
Result := tmpForm.FindComponent(tmpWords[1]);
if Result = nil then
RaiseException('FindCF('+AName+') Component not found '+tmpWords[1]);
end;
Code language: Delphi (delphi)