Dans mon ouvrage “Programmation visuelle”, j’ai abordé en détail les problèmes de localisation et d’internationalisation, il est temps d’appliquer ces connaissances dans un nouveau projet.
Par défaut, My Visual Database détecte le paramètre local du système d’exploitation et s’y adapte. Mais, premièrement, MVDB ne prend en charge que deux langues (russe et anglais), deuxièmement, la langue de localisation du système d’exploitation n’est pas toujours la langue souhaitée pour l’utilisateur, et troisièmement, l’application comporte des composants créés par l’utilisateur, ainsi que toutes sortes de test données ajoutées par les scripts. Par conséquent, pour les applications qui doivent être utilisées partout dans le monde, il est nécessaire de fournir un mécanisme de localisation pratique qui ne nécessite pas de réécrire le code de l’application elle-même
Pour la localisation, des fichiers texte seront utilisés pour stocker les paires clé=valeur. Mais pour plus de souplesse, deux types de clés seront utilisées :
- Static (valeur de propriété de formulaire ou ressource de texte système)
- Dynamic (ressource de texte personnalisée)
Clé statique
Ces clés sont traitées une seule fois, au démarrage du programme. Une fois la valeur attribuée, la paire est supprimée du stockage.
Une clé statique possède trois types :
- R.<Nom de forme>.<Propriété>
- R.<Nom de forme>.<Nom du composant>.<Свойство>
- T.<Identificateur de ressource de texte>
Le préfixe R ou T est utilisé pour désigner une clé dynamique. La procédure Resource_L10App() est utilisée pour traiter les clés. La procédure est lourde, car dans le processus de traitement, il est nécessaire d’apporter
procedure Resource_L10App;
// localisation de tous les formulaires de l’application
var
i: integer;
tmpWords: array of string;
tmpForm: TForm;
tmpComponent: TComponent;
tmpValue: string;
// procédure d'assistance qui rend le code plus 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) , '.');
// traduction de mots et d'expressions intégrés dans 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
// traduction des propriétés statiques
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]);
// doit être casté dans une classe spécifique
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;
Langage du code : Delphi (delphi)
Clé dynamique
Les clés dynamiques se trouvent en mémoire pour la durée de l’application, car elles peuvent être nécessaires à tout moment.
La fonction R() est utilisée pour travailler avec des clés dynamiques. La concision de son nom est due au fait qu’il devra être utilisé dans tous les scripts qui définissent par programme les propriétés des composants d’interface.
function R(AName:string; ADefValue:string='???'):string;
// On récupère les ressources
begin
if (StringRes <> nil) and (StringRes.Lines.IndexOfName(AName) < 0) then
Result := ADefValue
else
Result := StringRes.Lines.Values(AName);
end;
Langage du code : Delphi (delphi)
Habituellement, la valeur textuelle n’est pas stockée dans le code, mais placée dans une constante :
const
RESOURCE_CHANGE_LANGUAGE = 'Switch language';
begin
ShowMessage(RESOURCE_CHANGE_LANGUAGE);
Langage du code : Delphi (delphi)
Pour traduire un tel morceau de code destiné à utiliser une ressource linguistique, un petit changement sûr est nécessaire :
ShowMessage(R('RESOURCE_CHANGE_LANGUAGE',RESOURCE_CHANGE_LANGUAGE));
Langage du code : Delphi (delphi)
La sécurité du changement réside dans le fait que si la ressource spécifiée n’est pas trouvée ou si le système de ressources n’est pas impliqué, alors la fonction R() renverra la valeur par défaut, qui est contenue dans une constante. Cette astuce est également utilisée pour créer un fichier de localisation “par défaut” – il peut être vide, puisque toutes les valeurs nécessaires seront prises à partir de constantes. Mais si vous le souhaitez, vous pouvez corriger l’inscription sans modifier le code.
Si vous décidez de stocker toutes les ressources de texte dans un fichier séparé et de ne pas utiliser de constantes de chaîne pour stocker les valeurs par défaut dans le code, il est alors acceptable d’appeler la fonction R() sans second paramètre.
ShowMessage(R('RESOURCE_CHANGE_LANGUAGE'));
Langage du code : Delphi (delphi)
Cela réduit la taille du code source, et au cas où vous auriez oublié de décrire la ressource texte correspondante, trois points d’interrogation apparaîtront à l’écran.
Initialisation des resources
Lors de l’initialisation, qui se produit dans la procédure Resourse_Init(), les données sont chargées dans le composant de classe TdbMemo, qui peut fonctionner correctement avec des fichiers au format UTF-8 avec BOM.
Important ! Seul le format spécifié doit être utilisé : UTF-8 avec BOM
procedure Resource_Init();
// initialisation
var
tmpFileList: array of string;
i: integer;
s: string;
tmpDir: string;
begin
tmpDir := ExtractFilePath(Application.ExeName)+RESOURCE_DIR;
if DirectoryExists(tmpDir) then
begin
// langues - par le nombre de fichiers. Chaque fichier pour sa propre langue
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;
// langue courante lue à partir des paramètres
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;
// localisation statique
Resource_L10App;
end;
end;
Langage du code : PHP (php)
Un exemple de fichier ressource :
APP_ABOUT_CAPTION=About
APP_CONFIRM_RESTART= Un redémarrage du programme est nécessaire pour appliquer les modifications. Exécuter maintenant?
APP_COPYRIGHT=2023 Konstantin Pankov
APP_NAME=Guide du développeur
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
Langage du code : SQL (Structured Query Language) (sql)
Changement de langue
Pour changer la langue, nous avons besoin d’un élément de menu. Pour le créer, la procédure Resource_CreateMenu() est utilisée et la procédure de définition de la langue sélectionnée Resource_SetLanguage() est appelée dans le gestionnaire de clic
procedure Resource_CreateMenu( AForm:TForm );
// création de l'élément de menu "Langue"
var
i: integer;
tmpItem: TMenuItem;
tmpTopItem: TMenuItem;
begin
// On crée un élément de menu de niveau supérieur
tmpTopItem := TMenuItem.Create( AForm );
tmpTopItem.Name := 'mniLanguage';
tmpTopItem.Caption := R('RESOURCE_MENU_LANGUAGE',RESOURCE_MENU_LANGUAGE);
AForm.Menu.Items.Insert(1,tmpTopItem);
// On ajoute les elements du menu pour changer de langue
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;
// On met en surbrillance la langue courante
if Lаnguages.Strings(i) = currentLаnguage then
tmpItem.Checked := True;
tmpTopItem.Insert(i,tmpItem);
end;
end;
procedure Resource_MenuItem_OnClick (Sender: TObject; );
// On clique sur l'élément de menu de sélection de la langue
begin
// On supprime les caractères spéciaux qui ont été ajoutés automatiquement
Resource_SetLanguage( ReplaceStr(TMenuItem(Sender).Caption,'&','') );
end;
Langage du code : PHP (php)
En raison de la présence de ressources textuelles changeant dynamiquement, le changement de langue nécessite un redémarrage de l’application. Un redémarrage peut être nécessaire dans d’autres cas, il est donc judicieux de lui fournir une procédure Restart() distincte, qui affiche un formulaire de confirmation avant d’effectuer un redémarrage.
procedure Resource_SetLanguage(AName:string);
// Défintiion de la langue
// les modifications prendront effet après le redémarrage
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; // On ferme l'application en cours. En fait, cette commande ne crée qu'un message, en fait, la fermeture interviendra après la fin de la procédure en cours.
OpenFile( Application.ExeName ); // On lance l’application
end;
end;
Langage du code : Delphi (delphi)
Problèmes connus
Cette localisation ne s’arrête pas là. Il existe une petite liste de composants et de propriétés avec un moyen non trivial d’y accéder, qui doit également être traitée. Cela s’applique principalement aux en-têtes de tableau et aux listes déroulantes des formulaires de service. Vous devez également localiser les menus contextuels des tables, arborescences et autres composants. L’opération la plus délicate est la localisation de la traduction du message d’erreur d’authentification sur le formulaire de mot de passe et de connexion, ce qui nécessitera le remplacement du bouton standard. Le sujet reste donc ouvert.
Bonus
L’application utilise largement les formulaires dynamiques du module DTF.pas. Ces formulaires ont été créés dans la procédure UserApp_InitForm(), mais en raison de l’uniformité des paramètres de chaîne, j’ai décidé d’ajouter la procédure DTF_CreateForms(), qui effectue tout le travail de création de formulaires basés sur le fichier texte dforms.ini, et à la utilise en même temps le système de localisation nouvellement créé.
procedure DTF_CreateForms;
// création de formulaires dynamiques selon la liste située dans le fichier dforms.ini
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;
Langage du code : Delphi (delphi)
La structure du fichier dforms.ini est suffisamment simple pour ajouter de nouveaux formulaires, si nécessaire, directement dans un éditeur de texte.
[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
Langage du code : SQL (Structured Query Language) (sql)
Et FindCF() a été ajouté à la collection de fonctions utiles, qui peuvent trouver des composants sur le formulaire par leur chemin complet <Nom de forme>.<Nom du composant>
function FindCF(AName:string): TComponent;
// On trouve le composant par son nom complet
var
tmpForm:TForm;
tmpWords: array of string;
begin
tmpWords := SplitString(AName,'.');
if Length(tmpWords) < 2 then
RaiseException('FindCF('+AName+') argument incorrect');
tmpForm := GetFormByName( tmpWords[0] );
if tmpForm = nil then
RaiseException('FindCF('+AName+') Form non trouvée '+tmpWords[0]);
Result := tmpForm.FindComponent(tmpWords[1]);
if Result = nil then
RaiseException('FindCF('+AName+') Composant non trouvé '+tmpWords[1]);
end;
Langage du code : Delphi (delphi)
Liens
Traduction : Yann Yvnec