[erledigt] i18n

Für alles, was in den übrigen Lazarusthemen keinen Platz, aber mit Lazarus zutun hat.
Antworten
Scotty
Beiträge: 768
Registriert: Mo 4. Mai 2009, 13:24
OS, Lazarus, FPC: Arch Linux, Lazarus 1.3 r44426M FPC 2.6.4
CPU-Target: x86_64-linux-qt/gtk2
Kontaktdaten:

[erledigt] i18n

Beitrag von Scotty »

Per DefaultTranslator kann man recht einfach eine Übersetzung einbauen. Ich habe unter languages/(de|en|fr|it|es) die po-Files abgelegt, fände es aber schöner, wenn diese Dateien als Ressource eingebettet wären. Gibt es dafür eine fertige Routine?
Zuletzt geändert von Scotty am Do 26. Aug 2010, 19:12, insgesamt 1-mal geändert.

Scotty
Beiträge: 768
Registriert: Mo 4. Mai 2009, 13:24
OS, Lazarus, FPC: Arch Linux, Lazarus 1.3 r44426M FPC 2.6.4
CPU-Target: x86_64-linux-qt/gtk2
Kontaktdaten:

Re: i18n

Beitrag von Scotty »

Gleich noch eine zweite Frage hinterher: Wenn ich den DefaultTranslator benutze, wie kann ich die Sprache im Betrieb wechseln? Einfach per LRSTranslator:=TPoTranslator.Create('/Pfad/zu/der/po/Datei'); überschreiben und potentielle Speicherlecks ignorieren?

pluto
Lazarusforum e. V.
Beiträge: 7192
Registriert: So 19. Nov 2006, 12:06
OS, Lazarus, FPC: Linux Mint 19.3
CPU-Target: AMD
Wohnort: Oldenburg(Oldenburg)

Re: i18n

Beitrag von pluto »

ich würde vorher Prüfen ob "LRSTranslator" zugewiesen ist wenn ja Frei geben....
Aber die meisten Klassen haben eine Entsprechende LoadFrom Methode. Schau dir mal in Create bei TPoTranslator an, was da genau passiert.
MFG
Michael Springwald

Scotty
Beiträge: 768
Registriert: Mo 4. Mai 2009, 13:24
OS, Lazarus, FPC: Arch Linux, Lazarus 1.3 r44426M FPC 2.6.4
CPU-Target: x86_64-linux-qt/gtk2
Kontaktdaten:

Re: i18n

Beitrag von Scotty »

Ich habe mich gegen po-Dateien entschieden, weil mir da Flexibilität fehlt, ich während der Laufzeit die Sprache nicht ändern kann und die po-Dateien etwas unübersichtlicher sind als bei meiner Lösung.
Meine Lösung benutzt ini-Dateien, die sind IMHO übersichtlich und einfach. Wählt man eine <CurrentLanguge>, die noch nicht definiert ist, wird diese Datei mit den gerade aktuellen Einstellungen aufgefüllt. Die IDs sind im Gegensatz zu po nicht die Texte selbst sondern die Variablenbezeichner. Ich achte beim Programmieren auf ordentliche Namen inklusive Variablentyp, z.B. miFile für TMenuItem 'File' oder tbSave für Toolbutton 'Save'. Alle zur Laufzeit benutzen Strings müssen unter resourcestring angegeben werden. Zum Beispiel für Form1.OnShow(): Showmessage(rHello); rHello='Hello World'; Natürlich muss ulanguage dann in dieser unit eingebunden werden.

Code: Alles auswählen

unit ulanguage;
 
{$mode objfpc}{$H+}
 
interface
 
uses
  Classes, SysUtils, Forms, Inifiles, ExtCtrls, Typinfo, CheckLst, Grids, Buttons;
 
type
 
  TLanguage=class
    private
      FCurrentLanguage : string;
      FAuthor, FDate   : string;
      FVersion         : integer;
      function GetInfo(index: byte): string;
      procedure SetLanguage(const aValue: string);
      function HasProperty(comp:TComponent;prop:string):boolean;
      function GetProp(Comp:TComponent;Prop:string):string;
      procedure SetProp(comp:TComponent;const prop,value:string);
    public
      constructor Create;
      destructor Destroy;override;
      property CurrentLanguage:string read FCurrentLanguage write SetLanguage;
      property Info[index:byte]:string read GetInfo;
    end;
 
var Language : TLanguage;
 
resourcestring
//alle resourcestrings kommen hier hin
 
implementation
 
constructor TLanguage.Create;
begin
  inherited Create;
  FCurrentLanguage:='';
end;
 
destructor TLanguage.Destroy;
begin
  inherited Destroy;
end;
 
function TLanguage.HasProperty(comp: TComponent; prop: string): boolean;
begin
  Result:=(GetPropInfo(Comp.classInfo,Prop)<>nil) and (Comp.Name<>'');
end;
 
function TLanguage.GetProp(Comp: TComponent; Prop: string): string;
var pi:PPropInfo;
begin
  pi:=GetPropInfo(Comp.ClassInfo,Prop);
  if pi<>nil then Result:=GetStrProp(Comp,pi)
             else Result:='';
end;
 
procedure TLanguage.SetProp(comp: TComponent; const prop, value: string);
var pi:PPropInfo;
begin
  if Value<>'' then
  begin
    pi:=GetPropInfo(Comp.ClassInfo,Prop);
    if pi<>nil then SetStrProp(Comp,pi,Value);
  end;
end;
 
function TranslateResource(Name,Value : AnsiString; Hash : Longint; arg:pointer) : String;
var s:string;
begin
  s:=copy(Name,11,length(Name)); //remove ulanguage. from ulanguage.<resourcestring>
  Result:=TInifile(arg).ReadString('Translation',s,'');
  if Result='' then
  begin
    TInifile(arg).WriteString('Translation',s,Value);
    Result:=Value;
  end;
end;
 
procedure TLanguage.SetLanguage(const aValue: string);
var i,j,k : integer;
    ParentComp,Comp : TComponent;
    s : string;
    IniFile : TIniFile;
begin
  if (aValue<>FCurrentLanguage) and (aValue<>'') and (aValue<>' ') then
  begin
    Inifile:=TIniFile.Create(ExtractFilePath(Application.ExeName)+aValue);
    try
      FCurrentLanguage:=aValue;
      FAuthor:=Inifile.ReadString('Header','Author','');
      FVersion:=Inifile.ReadInteger('Header','Version',0);
      FDate:=Inifile.ReadString('Header','Date','');
      for i:=0 to Application.ComponentCount-1 do
      begin
        ParentComp:=Application.Components[i];
        if HasProperty(ParentComp,'Caption') then
        begin
          s:=Inifile.ReadString('Translation',ParentComp.Name+'.Caption','');
          if s='' then
          begin
            s:=GetProp(ParentComp,'Caption');
            Inifile.WriteString('Translation',ParentComp.Name+'.Caption',s);
          end;
          SetProp(ParentComp,'Caption',s);
        end;
        for j:=0 to ParentComp.ComponentCount-1 do
        begin
          Comp:=ParentComp.Components[j];
          if HasProperty(Comp,'Caption') and
             not (getProp(Comp,'Caption')='-') and
             not (getProp(Comp,'Caption')='') then
          begin
            s:=Inifile.ReadString('Translation',Comp.Name+'.Caption', '');
            if s='' then
            begin
              s:=GetProp(Comp,'Caption');
              Inifile.WriteString('Translation',Comp.Name+'.Caption', s);
            end;
            SetProp(Comp,'Caption',s);
          end;
 
          if HasProperty(Comp,'Hint') and (GetProp(Comp,'Hint')<>'') then
          begin
            s:=Inifile.ReadString('Translation',Comp.Name+'.Hint','');
            if s='' then
            begin
              s:=GetProp(Comp,'Hint');
              Inifile.WriteString('Translation',Comp.Name+'.Hint',s);
            end;
            SetProp(Comp,'Hint',s);
          end;
 
          if (Comp is TRadioGroup) then
           for k:=0 to TRadioGroup(Comp).Items.Count-1 do
           begin
             s:=Inifile.ReadString('Translation',Comp.Name+'.Items['+IntToStr(k)+']','');
             if s='' then
             begin
               s:=TRadioGroup(Comp).Items[k];
               Inifile.WriteString('Translation',Comp.Name+'.Items['+IntToStr(k)+']',s);
             end;
             (Comp as TRadioGroup).Items[k]:=s;
           end;
 
          if (Comp is TCheckListBox) then
           for k:=0 to TCheckListBox(Comp).Items.Count-1 do
           begin
             s:=Inifile.ReadString('Translation',Comp.Name+'.Items['+IntToStr(k)+']','');
             if s='' then
             begin
               s:=TCheckListBox(Comp).Items[k];
               Inifile.WriteString('Translation',Comp.Name+'.Items['+IntToStr(k)+']',s);
             end;
             (Comp as TCheckListBox).Items[k]:=s;
           end;
 
          if (Comp is TStringGrid) then
           for k:=0 to TStringGrid(Comp).Columns.Count-1 do
           begin
             s:=Inifile.ReadString('Translation',Comp.Name+'.Column['+IntToStr(k)+']','');
             if s='' then
             begin
               s:=TStringGrid(Comp).Columns[k].Title.Caption;
               Inifile.WriteString('Translation',Comp.Name+'.Column['+IntToStr(k)+']',s);
             end;
             (Comp as TStringGrid).Columns[k].Title.Caption:=s;
           end;
        end; //for j
       end;//for i
      //translate resourcestrings
      SetUnitResourceStrings('ulanguage',@TranslateResource,IniFile);
    finally
      IniFile.Free;
    end;
  end;
end;
 
function TLanguage.GetInfo(index: byte): string;
begin
  case index of
   0 : Result:=FAuthor;
   1 : Result:=FVersion;
   2 : Result:=FDate;
   else Result:='';
  end;
end;
 
initialization
  Language:=TLanguage.Create;
finalization
  Language.Free;
end.

Benutzeravatar
corpsman
Lazarusforum e. V.
Beiträge: 1641
Registriert: Sa 28. Feb 2009, 08:54
OS, Lazarus, FPC: Linux Mint Mate, Lazarus GIT Head, FPC 3.0
CPU-Target: 64Bit
Wohnort: Stuttgart
Kontaktdaten:

Re: [erledigt] i18n

Beitrag von corpsman »

hi scotty,

Ich habe mich auch gerade an den po Files versucht, und festgestellt, dass es irgendwie nicht so das ist, was ich will.

Dein Ansatz ist Einfach, wie genial, und gefällt mir sehr ( Mal abgesehen von einigen kleinen Bugs [ unterstützung von Mehrzeielstrings, ungültiger Typ von Version ], welche ich geschwind korrigiert habe ).

Wie siehts denn Lizenztechnisch aus, darf ich die Unit in einer Kommerziellen Anwendung verwenden ? Oder darf man das immer mit Source der hier veröffentlicht wird ?

Eine Frage bleibt auch noch offen, wie kriegen wir die Standard sprache des Systemes raus, ? der Po Ansatz kann das ja anscheinend..

Hier noch die reparierte Version

Code: Alles auswählen

Unit ulanguage;
 
{$MODE objfpc}{$H+}
 
Interface
 
Uses
  Classes, SysUtils, Forms, Inifiles, ExtCtrls, Typinfo, CheckLst, Grids, Buttons;
 
Type
 
  TLanguage = Class
  private
    FCurrentLanguage: String;
    FAuthor, FDate: String;
    FVersion: integer;
    Function GetInfo(index: byte): String;
    Procedure SetLanguage(Const aValue: String);
    Function HasProperty(comp: TComponent; prop: String): boolean;
    Function GetProp(Comp: TComponent; Prop: String): String;
    Procedure SetProp(comp: TComponent; Const prop, value: String);
  public
    Constructor Create;
    Destructor Destroy; override;
    Property CurrentLanguage: String read FCurrentLanguage write SetLanguage;
    Property Info[index: byte]: String read GetInfo;
  End;
 
Var
  Language: TLanguage;
 
Resourcestring
//alle resourcestrings kommen hier hin
 
Implementation
 
(*
 * Decodes a single line string to a multiline string
 *)
 
Function decode(value: String): String;
Var
  i: integer;
Begin
  i := 1;
  While i <= length(value) Do Begin
    If value[i] = '~' Then Begin
      Case value[i + 1] Of
        '~': Begin // Es passiert nicht weil der Doppelt index einfach gelöscht wird
          End;
        '3': value[i] := #13;
        '0': value[i] := #10;
      End;
      delete(value, i + 1, 1);
    End;
    inc(i);
  End;
  result := value;
End;
 
(*
 * Encodes a multiline string to a single line string
 *)
 
Function encode(value: String): String;
Var
  i: integer;
Begin
  i := 1;
  While i <= length(value) Do Begin
    // Das Steuerzeichentoken wird verdoppelt
    If value[i] = '~' Then Begin
      value := copy(value, 1, i) + '~' + copy(value, i + 1, length(value));
      inc(i);
    End;
    // CR in ~3
    If value[i] = #13 Then Begin
      value := copy(value, 1, i - 1) + '~3' + copy(value, i + 1, length(value));
    End;
    // RT in ~0
    If value[i] = #10 Then Begin
      value := copy(value, 1, i - 1) + '~0' + copy(value, i + 1, length(value));
    End;
    inc(i);
  End;
  result := value;
End;
 
Constructor TLanguage.Create;
Begin
  Inherited Create;
  FCurrentLanguage := '';
End;
 
Destructor TLanguage.Destroy;
Begin
  Inherited Destroy;
End;
 
Function TLanguage.HasProperty(comp: TComponent; prop: String): boolean;
Begin
  Result := (GetPropInfo(Comp.classInfo, Prop) <> Nil) And (Comp.Name <> '');
End;
 
Function TLanguage.GetProp(Comp: TComponent; Prop: String): String;
Var
  pi: PPropInfo;
Begin
  pi := GetPropInfo(Comp.ClassInfo, Prop);
  If pi <> Nil Then
    Result := GetStrProp(Comp, pi)
  Else
    Result := '';
End;
 
Procedure TLanguage.SetProp(comp: TComponent; Const prop, value: String);
Var
  pi: PPropInfo;
Begin
  If Value <> '' Then Begin
    pi := GetPropInfo(Comp.ClassInfo, Prop);
    If pi <> Nil Then SetStrProp(Comp, pi, Value);
  End;
End;
 
Function TranslateResource(Name, Value: AnsiString; Hash: Longint; arg: pointer): String;
Var
  s: String;
Begin
  s := copy(Name, 11, length(Name)); //remove ulanguage. from ulanguage.<resourcestring>
  Result := decode(TInifile(arg).ReadString('Translation', s, ''));
  If Result = '' Then Begin
    TInifile(arg).WriteString('Translation', s, encode(Value));
    Result := Value;
  End;
End;
 
Procedure TLanguage.SetLanguage(Const aValue: String);
Var
  i, j, k: integer;
  ParentComp, Comp: TComponent;
  s: String;
  IniFile: TIniFile;
Begin
  If (aValue <> FCurrentLanguage) And (aValue <> '') And (aValue <> ' ') Then Begin
    Inifile := TIniFile.Create(ExtractFilePath(Application.ExeName) + aValue);
    Try
      FCurrentLanguage := aValue;
      FAuthor := decode(Inifile.ReadString('Header', 'Author', ''));
      FVersion := Inifile.ReadInteger('Header', 'Version', 0);
      FDate := decode(Inifile.ReadString('Header', 'Date', ''));
      For i := 0 To Application.ComponentCount - 1 Do Begin
        ParentComp := Application.Components[i];
        If HasProperty(ParentComp, 'Caption') Then Begin
          s := decode(Inifile.ReadString('Translation', ParentComp.Name + '.Caption', ''));
          If s = '' Then Begin
            s := GetProp(ParentComp, 'Caption');
            Inifile.WriteString('Translation', ParentComp.Name + '.Caption', encode(s));
          End;
          SetProp(ParentComp, 'Caption', s);
        End;
        For j := 0 To ParentComp.ComponentCount - 1 Do Begin
          Comp := ParentComp.Components[j];
          If HasProperty(Comp, 'Caption') And
            Not (getProp(Comp, 'Caption') = '-') And
            Not (getProp(Comp, 'Caption') = '') Then Begin
            s := decode(Inifile.ReadString('Translation', Comp.Name + '.Caption', ''));
            If s = '' Then Begin
              s := GetProp(Comp, 'Caption');
              Inifile.WriteString('Translation', Comp.Name + '.Caption', encode(s));
            End;
            SetProp(Comp, 'Caption', s);
          End;
 
          If HasProperty(Comp, 'Hint') And (GetProp(Comp, 'Hint') <> '') Then Begin
            s := decode(Inifile.ReadString('Translation', Comp.Name + '.Hint', ''));
            If s = '' Then Begin
              s := GetProp(Comp, 'Hint');
              Inifile.WriteString('Translation', Comp.Name + '.Hint', encode(s));
            End;
            SetProp(Comp, 'Hint', s);
          End;
 
          If (Comp Is TRadioGroup) Then
            For k := 0 To TRadioGroup(Comp).Items.Count - 1 Do Begin
              s := decode(Inifile.ReadString('Translation', Comp.Name + '.Items[' + IntToStr(k) + ']', ''));
              If s = '' Then Begin
                s := TRadioGroup(Comp).Items[k];
                Inifile.WriteString('Translation', Comp.Name + '.Items[' + IntToStr(k) + ']', encode(s));
              End;
              (Comp As TRadioGroup).Items[k] := s;
            End;
 
          If (Comp Is TCheckListBox) Then
            For k := 0 To TCheckListBox(Comp).Items.Count - 1 Do Begin
              s := decode(Inifile.ReadString('Translation', Comp.Name + '.Items[' + IntToStr(k) + ']', ''));
              If s = '' Then Begin
                s := TCheckListBox(Comp).Items[k];
                Inifile.WriteString('Translation', Comp.Name + '.Items[' + IntToStr(k) + ']', encode(s));
              End;
              (Comp As TCheckListBox).Items[k] := s;
            End;
 
          If (Comp Is TStringGrid) Then
            For k := 0 To TStringGrid(Comp).Columns.Count - 1 Do Begin
              s := decode(Inifile.ReadString('Translation', Comp.Name + '.Column[' + IntToStr(k) + ']', ''));
              If s = '' Then Begin
                s := TStringGrid(Comp).Columns[k].Title.Caption;
                Inifile.WriteString('Translation', Comp.Name + '.Column[' + IntToStr(k) + ']', encode(s));
              End;
              (Comp As TStringGrid).Columns[k].Title.Caption := s;
            End;
        End; //for j
      End; //for i
      //translate resourcestrings
      SetUnitResourceStrings('ulanguage', @TranslateResource, IniFile);
    Finally
      IniFile.Free;
    End;
  End;
End;
 
Function TLanguage.GetInfo(index: byte): String;
Begin
  Case index Of
    0: Result := FAuthor;
    1: Result := inttostr(FVersion);
    2: Result := FDate;
  Else
    Result := '';
  End;
End;
 
Initialization
  Language := TLanguage.Create;
 
Finalization
  Language.Free;
End.
--
Just try it

Scotty
Beiträge: 768
Registriert: Mo 4. Mai 2009, 13:24
OS, Lazarus, FPC: Arch Linux, Lazarus 1.3 r44426M FPC 2.6.4
CPU-Target: x86_64-linux-qt/gtk2
Kontaktdaten:

Re: [erledigt] i18n

Beitrag von Scotty »

corpsman hat geschrieben:Dein Ansatz ist einfach, wie genial, und gefällt mir sehr ( Mal abgesehen von einigen kleinen Bugs [ unterstützung von Mehrzeielstrings, ungültiger Typ von Version ], welche ich geschwind korrigiert habe ).
Danke. Mehrzeilig geht per \\n (der aktuelle Code liegt auf Sourceforge oder Lazforge).

Code: Alles auswählen

Result:=StringReplace(TInifile(arg).ReadString('Translation',s,''),'\\n', LineBreak, [rfIgnoreCase, rfReplaceAll]);
Es fehlt allerdings noch ein variabler Plural (à la Gnu ngettext), eine einfachere Bearbeitung für Übersetzer (für Po's gibt es genug Tools), überflüssige Strings bleiben ewig drin usw.
Einfach ist meine Variante aber hoffentlich schon ;-). Du kannst den Code benutzen wie du magst. Ich habe GPLv3 als Lizenz gewählt.
PS: Vielleicht nimmst du besser UTF8Copy.

Benutzeravatar
corpsman
Lazarusforum e. V.
Beiträge: 1641
Registriert: Sa 28. Feb 2009, 08:54
OS, Lazarus, FPC: Linux Mint Mate, Lazarus GIT Head, FPC 3.0
CPU-Target: 64Bit
Wohnort: Stuttgart
Kontaktdaten:

Re: [erledigt] i18n

Beitrag von corpsman »

*g*

ja habe deine Version mittlerweile auch schon ganz schön aufgebort.

- Auslagern der Ressourcenstrings in Include files, zur Unterstützung meiner Library Philosophie

Code: Alles auswählen

uses lclproc;
Function Get_System_Default_Language(): String;
Var
  lang, def: String;
Begin
  lang := '';
  def := '';
  LCLGetLanguageIDs(lang, def);
  result := lang;
End;
Einfügen einiger Konstanten und ein Versuch das ganze zu kommentieren ...

Einen Editor muss ich sowieso schreiben, wenn ich ihn fertig hab, kann ich das Ding eigentlich auch hier posten ...
Die Option des "Finden von veralteten Einträgen" ist eigentlich auch keine Dumme Idee. In Klab hatte ich auch so was eingebaut. Mal schaun was sich da machen läst.
--
Just try it

Benutzeravatar
corpsman
Lazarusforum e. V.
Beiträge: 1641
Registriert: Sa 28. Feb 2009, 08:54
OS, Lazarus, FPC: Linux Mint Mate, Lazarus GIT Head, FPC 3.0
CPU-Target: 64Bit
Wohnort: Stuttgart
Kontaktdaten:

Re: [erledigt] i18n

Beitrag von corpsman »

So meine Anpassungen sind fertig, und hier verfügbar.

Neben dem Editor, ist auch das automatische löschen von nicht mehr benötigten einträgen enthalten.
--
Just try it

Antworten