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

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *