mirror of
https://gitlab.com/freepascal.org/lazarus/lazarus.git
synced 2025-06-01 09:53:20 +02:00

In base .pot file (template): - Newly created item has no user flags. - Existing updated item has its user flags kept intact. In translation .po file: - Newly created item gets user flags copied from base file (all flags missing in base file are removed). - Existing updated item gets user flags copied from base file (all flags missing in base file are removed except fuzzy flag, whose state is kept). After copying user flags, object-pascal-format, badformat and fuzzy flags are set automatically for each item (if needed). This makes flags in translations (.po files) follow the ones from base file (.pot file) when they are added or removed in all cases (even when an item in .pot file gets its last flag deleted). git-svn-id: trunk@61231 -
1784 lines
49 KiB
ObjectPascal
1784 lines
49 KiB
ObjectPascal
{
|
|
*****************************************************************************
|
|
This file is part of LazUtils.
|
|
|
|
See the file COPYING.modifiedLGPL.txt, included in this distribution,
|
|
for details about the license.
|
|
*****************************************************************************
|
|
|
|
Initial authors: Mattias Gaertner, Bart Broersma, Giuliano Colla
|
|
|
|
Abstract:
|
|
Methods and classes for loading/checking/maintaining translations from po files.
|
|
|
|
Example 1: Load a specific .po file:
|
|
|
|
procedure TForm1.FormCreate(Sender: TObject);
|
|
var
|
|
PODirectory: String;
|
|
begin
|
|
PODirectory:='/path/to/lazarus/lcl/languages/';
|
|
TranslateUnitResourceStrings('LCLStrConsts',PODirectory+'lcl.%s.po',
|
|
'nl','');
|
|
MessageDlg('Title','Text',mtInformation,[mbOk,mbCancel,mbYes],0);
|
|
end;
|
|
|
|
|
|
Example 2: Load the current language file using the GetLanguageIDs function
|
|
of the gettext unit in the project lpr file:
|
|
|
|
uses
|
|
...
|
|
Translations, LCLProc;
|
|
|
|
procedure TranslateLCL;
|
|
var
|
|
PODirectory, Lang, FallbackLang: String;
|
|
begin
|
|
PODirectory:='/path/to/lazarus/lcl/languages/';
|
|
Lang:='';
|
|
FallbackLang:='';
|
|
LCLGetLanguageIDs(Lang,FallbackLang); // in unit LCLProc
|
|
Translations.TranslateUnitResourceStrings('LCLStrConsts',
|
|
PODirectory+'lclstrconsts.%s.po',Lang,FallbackLang);
|
|
end;
|
|
|
|
begin
|
|
TranslateLCL;
|
|
Application.Initialize;
|
|
Application.CreateForm(TForm1, Form1);
|
|
Application.Run;
|
|
end.
|
|
|
|
Note for Mac OS X:
|
|
The supported language IDs should be added into the application
|
|
bundle property list to CFBundleLocalizations key, see
|
|
lazarus.app/Contents/Info.plist for example.
|
|
}
|
|
unit Translations;
|
|
|
|
{$mode objfpc}{$H+}{$INLINE ON}
|
|
|
|
interface
|
|
|
|
uses
|
|
Classes, SysUtils,
|
|
{$IF FPC_FULLVERSION>=30001}jsonscanner,{$ENDIF} jsonparser, fpjson,
|
|
// LazUtils
|
|
FileUtil, LazFileUtils, LazUTF8, LazUTF8Classes, LConvEncoding, LazLoggerBase,
|
|
AvgLvlTree, StringHashList;
|
|
|
|
type
|
|
TStringsType = (
|
|
stLrj, // Lazarus resource string table in JSON format
|
|
stRst, // FPC resource string table (before FPC 2.7.1)
|
|
stRsj // FPC resource string table in JSON format (since FPC 2.7.1)
|
|
);
|
|
TTranslateUnitResult = (turOK, turNoLang, turNoFBLang, turEmptyParam);
|
|
|
|
TTranslationStatistics = record
|
|
Translated: Integer;
|
|
Untranslated: Integer;
|
|
Fuzzy: Integer;
|
|
end;
|
|
|
|
type
|
|
{ TPOFileItem }
|
|
|
|
TPOFileItem = class
|
|
public
|
|
Tag: Integer;
|
|
LineNr: Integer; // required by pochecker
|
|
Comments: string;
|
|
IdentifierLow: string; // lowercase
|
|
Original: string;
|
|
Translation: string;
|
|
Flags: string;
|
|
PreviousID: string;
|
|
Context: string;
|
|
Duplicate: boolean;
|
|
constructor Create(const TheIdentifierLow, TheOriginal, TheTranslated: string);
|
|
// Can accept the comma separated list of flags
|
|
// Returns true if the Flags property has been modified
|
|
function ModifyFlag(const AFlags: string; Check: boolean): boolean;
|
|
end;
|
|
|
|
{ TPOFile }
|
|
|
|
TPOFile = class
|
|
private
|
|
FAllowChangeFuzzyFlag: boolean;
|
|
FStatisticsUpdated: boolean;
|
|
FStatistics: TTranslationStatistics;
|
|
function GetStatistics: TTranslationStatistics;
|
|
protected
|
|
FItems: TFPList;// list of TPOFileItem
|
|
FIdentifierLowToItem: TStringToPointerTree; // lowercase identifier to TPOFileItem
|
|
FOriginalToItem: TStringHashList; // of TPOFileItem
|
|
FCharSet: String;
|
|
FHeader: TPOFileItem;
|
|
FAllEntries: boolean;
|
|
FTag: Integer;
|
|
FModified: boolean;
|
|
FHelperList: TStringList;
|
|
// New fields
|
|
FPoName: string;
|
|
function Remove(Index: Integer): TPOFileItem;
|
|
// used by pochecker
|
|
function GetCount: Integer;
|
|
procedure SetCharSet(const AValue: String);
|
|
function GetPoItem(Index: Integer): TPoFileItem;
|
|
procedure ReadPOText(AStream: TStream);
|
|
public
|
|
constructor Create(Full:Boolean=True); //when loading from internal resource Full needs to be False
|
|
constructor Create(const AFilename: String; Full: boolean=false; AllowChangeFuzzyFlag: boolean=true);
|
|
constructor Create(AStream: TStream; Full: boolean=false; AllowChangeFuzzyFlag: boolean=true);
|
|
destructor Destroy; override;
|
|
procedure ReadPOText(const Txt: string);
|
|
function Translate(const Identifier, OriginalValue: String): String;
|
|
Property CharSet: String read FCharSet;
|
|
procedure Report;
|
|
procedure Report(StartIndex, StopIndex: Integer; const DisplayHeader: Boolean); //pochecker
|
|
procedure Report(Log: TStrings; StartIndex, StopIndex: Integer; const DisplayHeader: Boolean); //pochecker
|
|
procedure CreateHeader;
|
|
procedure UpdateStrings(InputLines:TStrings; SType: TStringsType);
|
|
procedure SaveToStrings(OutLst: TStrings);
|
|
procedure SaveToFile(const AFilename: string);
|
|
procedure UpdateItem(const Identifier, Original: string; const Flags: string = '';
|
|
const ProcessingTranslation: boolean = false);
|
|
procedure FillItem(var CurrentItem: TPOFileItem; Identifier, Original,
|
|
Translation, Comments, Context, Flags, PreviousID: string; LineNr: Integer = -1);
|
|
procedure UpdateTranslation(BasePOFile: TPOFile);
|
|
|
|
procedure UntagAll;
|
|
procedure RemoveTaggedItems(aTag: Integer);
|
|
|
|
procedure RemoveIdentifier(const AIdentifier: string);
|
|
procedure RemoveOriginal(const AOriginal: string);
|
|
procedure RemoveIdentifiers(AIdentifiers: TStrings);
|
|
procedure RemoveOriginals(AOriginals: TStrings);
|
|
|
|
property Tag: integer read FTag write FTag;
|
|
property Modified: boolean read FModified;
|
|
property Items: TFPList read FItems;
|
|
// used by pochecker /pohelper
|
|
public
|
|
procedure CleanUp; // removes previous ID from non-fuzzy entries
|
|
property PoName: String read FPoName;
|
|
property PoRename: String write FPoName;
|
|
property Statistics: TTranslationStatistics read GetStatistics;
|
|
procedure InvalidateStatistics;
|
|
function FindPoItem(const Identifier: String): TPoFileItem;
|
|
function OriginalToItem(const Data: String): TPoFileItem;
|
|
property PoItems[Index: Integer]: TPoFileItem read GetPoItem;
|
|
property Count: Integer read GetCount;
|
|
property Header: TPOFileItem read FHeader;
|
|
end;
|
|
|
|
EPOFileError = class(Exception)
|
|
public
|
|
ResFileName: string;
|
|
POFileName: string;
|
|
end;
|
|
|
|
var
|
|
SystemCharSetIsUTF8: Boolean = true;// the LCL interfaces expect UTF-8 as default
|
|
// if you don't use UTF-8, install a proper widestring manager and set this
|
|
// to false.
|
|
|
|
function GetPOFilenameParts(const Filename: string; out AUnitName, Language: string): boolean;
|
|
function FindAllTranslatedPoFiles(const Filename: string): TStringList;
|
|
|
|
// translate resource strings for one unit
|
|
function TranslateUnitResourceStrings(const ResUnitName, BaseFilename,
|
|
Lang, FallbackLang: string):TTranslateUnitResult; overload;
|
|
function TranslateUnitResourceStrings(const ResUnitName, AFilename: string
|
|
): boolean; overload;
|
|
function TranslateUnitResourceStrings(const ResUnitName:string; po: TPOFile): boolean; overload;
|
|
|
|
// translate all resource strings
|
|
function TranslateResourceStrings(po: TPOFile): boolean;
|
|
function TranslateResourceStrings(const AFilename: string): boolean;
|
|
procedure TranslateResourceStrings(const BaseFilename, Lang, FallbackLang: string);
|
|
|
|
function UTF8ToSystemCharSet(const s: string): string; inline;
|
|
|
|
function UpdatePoFile(RSTFiles: TStrings; const POFilename: string): boolean;
|
|
procedure UpdatePoFileTranslations(const BasePOFilename: string; BasePOFile: TPOFile = nil);
|
|
|
|
const
|
|
sFuzzyFlag = 'fuzzy';
|
|
sBadFormatFlag = 'badformat';
|
|
sFormatFlag = 'object-pascal-format';
|
|
sNoFormatFlag = 'no-object-pascal-format';
|
|
|
|
|
|
implementation
|
|
|
|
function IsKey(Txt, Key: PChar): boolean;
|
|
begin
|
|
if Txt=nil then exit(false);
|
|
if Key=nil then exit(true);
|
|
repeat
|
|
if Key^=#0 then exit(true);
|
|
if Txt^<>Key^ then exit(false);
|
|
inc(Key);
|
|
inc(Txt);
|
|
until false;
|
|
end;
|
|
|
|
function GetUTF8String(TxtStart, TxtEnd: PChar): string; inline;
|
|
begin
|
|
Result:=UTF8CStringToUTF8String(TxtStart,TxtEnd-TxtStart);
|
|
end;
|
|
|
|
function ComparePOItems(Item1, Item2: Pointer): Integer;
|
|
begin
|
|
Result := CompareText(TPOFileItem(Item1).IdentifierLow,
|
|
TPOFileItem(Item2).IdentifierLow);
|
|
end;
|
|
|
|
function UTF8ToSystemCharSet(const s: string): string; inline;
|
|
begin
|
|
if SystemCharSetIsUTF8 then
|
|
exit(s);
|
|
{$IFDEF NoUTF8Translations}
|
|
Result:=s;
|
|
{$ELSE}
|
|
Result:=UTF8ToSys(s);
|
|
{$ENDIF}
|
|
end;
|
|
|
|
function SkipLineEndings(var P: PChar; var DecCount: Integer): Integer;
|
|
procedure Skip;
|
|
begin
|
|
Dec(DecCount);
|
|
Inc(P);
|
|
end;
|
|
begin
|
|
Result := 0;
|
|
while (P^ in [#10,#13]) do begin
|
|
Inc(Result);
|
|
if (P^=#13) then begin
|
|
Skip;
|
|
if P^=#10 then
|
|
Skip;
|
|
end else
|
|
Skip;
|
|
end;
|
|
end;
|
|
|
|
function CompareMultilinedStrings(const S1,S2: string): Integer;
|
|
var
|
|
C1,C2,L1,L2: Integer;
|
|
P1,P2: PChar;
|
|
begin
|
|
L1 := Length(S1);
|
|
L2 := Length(S2);
|
|
P1 := pchar(S1);
|
|
P2 := pchar(S2);
|
|
Result := ord(P1^) - ord(P2^);
|
|
|
|
while (Result=0) and (L1>0) and (L2>0) and (P1^<>#0) do begin
|
|
if (P1^<>P2^) or (P1^ in [#10,#13]) then begin
|
|
C1 := SkipLineEndings(P1, L1);
|
|
C2 := SkipLineEndings(P2, L2);
|
|
if (C1<>C2) then
|
|
// different amount of lineendings
|
|
result := C1-C2
|
|
else
|
|
if (C1=0) then
|
|
// there are no lineendings at all, will end loop
|
|
result := Ord(P1^)-Ord(P2^);
|
|
end;
|
|
Inc(P1); Inc(P2);
|
|
Dec(L1); Dec(L2);
|
|
end;
|
|
|
|
// if strings are the same, check that all chars have been consumed
|
|
// just in case there are unexpected chars in between, in this case
|
|
// L1=L2=0;
|
|
if Result=0 then
|
|
Result := L1-L2;
|
|
end;
|
|
|
|
function StrToPoStr(const s:string):string;
|
|
var
|
|
SrcPos, DestPos: Integer;
|
|
NewLength: Integer;
|
|
begin
|
|
NewLength:=length(s);
|
|
for SrcPos:=1 to length(s) do
|
|
if s[SrcPos] in ['"','\',#9] then inc(NewLength);
|
|
if NewLength=length(s) then begin
|
|
Result:=s;
|
|
end else begin
|
|
SetLength(Result,NewLength);
|
|
DestPos:=1;
|
|
for SrcPos:=1 to length(s) do begin
|
|
case s[SrcPos] of
|
|
'"','\':
|
|
begin
|
|
Result[DestPos]:='\';
|
|
inc(DestPos);
|
|
Result[DestPos]:=s[SrcPos];
|
|
inc(DestPos);
|
|
end;
|
|
#9:
|
|
begin
|
|
Result[DestPos]:='\';
|
|
inc(DestPos);
|
|
Result[DestPos]:='t';
|
|
inc(DestPos);
|
|
end;
|
|
else
|
|
Result[DestPos]:=s[SrcPos];
|
|
inc(DestPos);
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
function ExtractFormatArgs(S: String; out ArgumentError: Integer): String;
|
|
const
|
|
FormatArgs = 'DEFGMNPSUX';
|
|
FormatChar = '%';
|
|
FormatSpecs = ':-.0123456789';
|
|
var
|
|
p: PtrInt;
|
|
NewStr, Symb: String;
|
|
begin
|
|
NewStr := '';
|
|
ArgumentError := 0;
|
|
p := UTF8Pos(FormatChar, S);
|
|
while (Length(S)>0) and (p>0) and (ArgumentError=0) do
|
|
begin
|
|
UTF8Delete(S, 1, p);
|
|
if Length(S)>0 then
|
|
begin
|
|
Symb := UTF8UpperCase(UTF8Copy(S, 1, 1));
|
|
while (Length(S)>1) and (UTF8Pos(Symb, FormatSpecs)>0) do
|
|
begin
|
|
//weak syntax check for formatting options, skip them if found
|
|
UTF8Delete(S, 1, 1);
|
|
Symb := UTF8UpperCase(UTF8Copy(S, 1, 1));
|
|
end;
|
|
if Symb <> FormatChar then
|
|
begin
|
|
NewStr := NewStr+Symb;
|
|
if UTF8Pos(Symb, FormatArgs)=0 then
|
|
ArgumentError := Utf8Length(NewStr);
|
|
end;
|
|
//removing processed symbol
|
|
UTF8Delete(S, 1, 1);
|
|
//searching for next argument
|
|
p := UTF8Pos(FormatChar, S);
|
|
end
|
|
else
|
|
//in this case formatting symbol doesn't have its argument
|
|
ArgumentError := Utf8Length(NewStr) + 1;
|
|
end;
|
|
Result := NewStr;
|
|
end;
|
|
|
|
function CompareFormatArgs(S1, S2: String): Boolean;
|
|
var
|
|
Extr1, Extr2: String;
|
|
ArgErr1, ArgErr2: Integer;
|
|
begin
|
|
Result := true;
|
|
//do not check arguments if strings are equal to save time and avoid some
|
|
//false positives, e.g. for '{%Region}' string in lazarusidestrconsts
|
|
if S1 <> S2 then
|
|
begin
|
|
Extr1 := ExtractFormatArgs(S1, ArgErr1);
|
|
Extr2 := ExtractFormatArgs(S2, ArgErr2);
|
|
//writeln('Extr1 = ',Extr1,' ArgErr1 = ',ArgErr1);
|
|
//writeln('Extr2 = ',Extr1,' ArgErr2 = ',ArgErr2);
|
|
if (ArgErr1 = 0) then
|
|
begin
|
|
if (ArgErr2 = 0) then
|
|
begin
|
|
Result := Utf8CompareText(Extr1, Extr2) = 0;
|
|
end
|
|
else
|
|
begin
|
|
//Extr2 can have dangling %'s
|
|
//e.g. Extr1 = "%s %d" Extr2 = "%s %d {%H}", it does not make sense, but it's not illegal
|
|
if (ArgErr2 = Utf8Length(Extr1)+1) and not (ArgErr2 > Utf8Length(Extr2)) then Extr2 := Utf8Copy(Extr2,1,ArgErr2-1);
|
|
Result := Utf8CompareText(Extr1, Extr2) = 0;
|
|
end;
|
|
end
|
|
else
|
|
begin //ArgErr1 <> 0
|
|
//Assume Extr1 is always legal, otherwise the IDE would crash in it's default language...
|
|
//Only compare until the last valid argument in Extr1
|
|
if (ArgErr1 = Utf8Length(Extr1)) then Utf8Delete(Extr1, ArgErr1, 1);
|
|
if Utf8Length(Extr2) > Utf8Length(Extr1) then Extr2 := Utf8Copy(Extr2, 1, Utf8Length(Extr1));
|
|
Result := Utf8CompareText(Extr1, Extr2) = 0;
|
|
end;
|
|
//writeln('CompareFormatArgs: Result = ',Result);
|
|
end;
|
|
end;
|
|
|
|
function GetPOFilenameParts(const Filename: string; out AUnitName, Language: string): boolean;
|
|
var
|
|
NameWithoutExt, Ext: string;
|
|
ExtLength: Integer;
|
|
begin
|
|
Result:=false;
|
|
AUnitName:='';
|
|
Language:='';
|
|
if CompareFileExt(Filename, '.po', false)=0 then
|
|
begin
|
|
NameWithoutExt:=ExtractFileNameWithoutExt(Filename);
|
|
Ext:=ExtractFileExt(NameWithoutExt);
|
|
ExtLength:=Length(Ext);
|
|
if ExtLength>1 then
|
|
begin
|
|
AUnitName:=copy(NameWithoutExt, 1, Length(NameWithoutExt)-ExtLength);
|
|
Language:=copy(Ext, 2, ExtLength-1);
|
|
Result:=true;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
function FindAllTranslatedPoFiles(const Filename: string): TStringList;
|
|
var
|
|
Path: String;
|
|
NameOnly: String;
|
|
FileInfo: TSearchRec;
|
|
CurUnitName: String;
|
|
CurLang: String;
|
|
begin
|
|
Result:=TStringList.Create;
|
|
Path:=ExtractFilePath(Filename);
|
|
NameOnly:=ExtractFileNameOnly(Filename);
|
|
if FindFirstUTF8(Path+GetAllFilesMask,faAnyFile,FileInfo)=0 then
|
|
repeat
|
|
if GetPOFilenameParts(FileInfo.Name, CurUnitName, CurLang) and (NameOnly=CurUnitName) then
|
|
Result.Add(Path+FileInfo.Name);
|
|
until FindNextUTF8(FileInfo)<>0;
|
|
FindCloseUTF8(FileInfo);
|
|
end;
|
|
|
|
procedure UpdatePoFileTranslations(const BasePOFilename: string; BasePOFile: TPOFile);
|
|
var
|
|
j: Integer;
|
|
Lines: TStringList;
|
|
FreeBasePOFile: Boolean;
|
|
TranslatedPOFile: TPOFile;
|
|
E: EPOFileError;
|
|
begin
|
|
// Update translated PO files
|
|
FreeBasePOFile := false;
|
|
Lines := FindAllTranslatedPoFiles(BasePOFilename);
|
|
try
|
|
for j:=0 to Lines.Count-1 do begin
|
|
TranslatedPOFile := TPOFile.Create(Lines[j], true);
|
|
try
|
|
TranslatedPOFile.Tag:=1;
|
|
if BasePOFile=nil then begin
|
|
BasePOFile := TPOFile.Create(BasePOFilename, true);
|
|
FreeBasePOFile := true;
|
|
end;
|
|
TranslatedPOFile.UpdateTranslation(BasePOFile);
|
|
try
|
|
TranslatedPOFile.SaveToFile(Lines[j]);
|
|
except
|
|
on Ex: Exception do begin
|
|
E := EPOFileError.Create(Ex.Message);
|
|
E.ResFileName:=Lines[j];
|
|
E.POFileName:=BasePOFileName;
|
|
raise E;
|
|
end;
|
|
end;
|
|
finally
|
|
TranslatedPOFile.Free;
|
|
end;
|
|
end;
|
|
finally
|
|
if FreeBasePOFile then
|
|
BasePOFile.Free;
|
|
Lines.Free;
|
|
end;
|
|
end;
|
|
|
|
function UpdatePOFile(RSTFiles: TStrings; const POFilename: string): boolean;
|
|
var
|
|
InputLines: TStringListUTF8;
|
|
Filename: string;
|
|
BasePoFile: TPoFile;
|
|
i: Integer;
|
|
E: EPOFileError;
|
|
begin
|
|
Result := false;
|
|
|
|
if (RSTFiles=nil) or (RSTFiles.Count=0) then begin
|
|
if FileExistsUTF8(POFilename) then begin
|
|
// just update translated po RSTFiles
|
|
UpdatePoFileTranslations(POFilename);
|
|
end;
|
|
exit;
|
|
end;
|
|
|
|
InputLines := TStringListUTF8.Create;
|
|
try
|
|
// Read base po items
|
|
if FileExistsUTF8(POFilename) then
|
|
BasePOFile := TPOFile.Create(POFilename, true)
|
|
else
|
|
BasePOFile := TPOFile.Create;
|
|
BasePOFile.Tag:=1;
|
|
// untagging is done only once for BasePoFile
|
|
BasePOFile.UntagAll;
|
|
|
|
// Update po file with lrj, rst/rsj of RSTFiles
|
|
for i:=0 to RSTFiles.Count-1 do begin
|
|
Filename:=RSTFiles[i];
|
|
if (CompareFileExt(Filename,'.lrj')=0) or
|
|
(CompareFileExt(Filename,'.rst')=0) or
|
|
(CompareFileExt(Filename,'.rsj')=0) then
|
|
try
|
|
//DebugLn('');
|
|
//DebugLn(['AddFiles2Po Filename="',Filename,'"']);
|
|
InputLines.Clear;
|
|
InputLines.LoadFromFile(FileName);
|
|
|
|
if CompareFileExt(Filename,'.lrj')=0 then
|
|
BasePOFile.UpdateStrings(InputLines, stLrj)
|
|
else
|
|
if CompareFileExt(Filename,'.rsj')=0 then
|
|
BasePOFile.UpdateStrings(InputLines, stRsj)
|
|
else
|
|
BasePOFile.UpdateStrings(InputLines, stRst);
|
|
except
|
|
on Ex: Exception do begin
|
|
E := EPOFileError.Create(Ex.Message);
|
|
E.ResFileName:=FileName;
|
|
E.POFileName:=POFileName;
|
|
raise E;
|
|
end;
|
|
end;
|
|
end;
|
|
// once all rst/rsj/lrj files are processed, remove all unneeded (missing in them) items
|
|
BasePOFile.RemoveTaggedItems(0);
|
|
|
|
BasePOFile.SaveToFile(POFilename);
|
|
Result := BasePOFile.Modified;
|
|
|
|
UpdatePoFileTranslations(POFilename,BasePoFile);
|
|
|
|
finally
|
|
InputLines.Free;
|
|
BasePOFile.Free;
|
|
end;
|
|
end;
|
|
|
|
function Translate (Name,Value : AnsiString; {%H-}Hash : Longint; arg:pointer) : AnsiString;
|
|
var
|
|
po: TPOFile;
|
|
begin
|
|
po:=TPOFile(arg);
|
|
// get UTF8 string
|
|
result := po.Translate(Name,Value);
|
|
// convert UTF8 to current local
|
|
if result<>'' then
|
|
result:=UTF8ToSystemCharSet(result);
|
|
end;
|
|
|
|
function TranslateUnitResourceStrings(const ResUnitName, AFilename: string
|
|
): boolean;
|
|
var po: TPOFile;
|
|
begin
|
|
//debugln('TranslateUnitResourceStrings) ResUnitName="',ResUnitName,'" AFilename="',AFilename,'"');
|
|
if (ResUnitName='') or (AFilename='') or (not FileExistsUTF8(AFilename)) then
|
|
exit;
|
|
result:=false;
|
|
po:=nil;
|
|
try
|
|
po:=TPOFile.Create(AFilename);
|
|
result:=TranslateUnitResourceStrings(ResUnitName,po);
|
|
finally
|
|
po.free;
|
|
end;
|
|
end;
|
|
|
|
function TranslateUnitResourceStrings(const ResUnitName: string; po: TPOFile): boolean;
|
|
begin
|
|
Result:=false;
|
|
try
|
|
SetUnitResourceStrings(ResUnitName,@Translate,po);
|
|
Result:=true;
|
|
except
|
|
on e: Exception do begin
|
|
{$IFnDEF DisableChecks}
|
|
DebugLn('Exception while translating ', ResUnitName);
|
|
DebugLn(e.Message);
|
|
DumpExceptionBackTrace;
|
|
{$ENDIF}
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
function TranslateUnitResourceStrings(const ResUnitName, BaseFilename,
|
|
Lang, FallbackLang: string):TTranslateUnitResult;
|
|
begin
|
|
Result:=turOK; //Result: OK
|
|
if (ResUnitName='') or (BaseFilename='') then
|
|
Result:=turEmptyParam //Result: empty Parameter
|
|
else begin
|
|
if (FallbackLang<>'') and FileExistsUTF8(Format(BaseFilename,[FallbackLang])) then
|
|
TranslateUnitResourceStrings(ResUnitName,Format(BaseFilename,[FallbackLang]))
|
|
else
|
|
Result:=turNoFBLang; //Result: missing FallbackLang file
|
|
if (Lang<>'') and FileExistsUTF8(Format(BaseFilename,[Lang])) then
|
|
TranslateUnitResourceStrings(ResUnitName,Format(BaseFilename,[Lang]))
|
|
else
|
|
Result:=turNoLang; //Result: missing Lang file
|
|
end;
|
|
end;
|
|
|
|
function TranslateResourceStrings(po: TPOFile): boolean;
|
|
begin
|
|
Result:=false;
|
|
try
|
|
SetResourceStrings(@Translate,po);
|
|
Result:=true;
|
|
except
|
|
on e: Exception do begin
|
|
{$IFnDEF DisableChecks}
|
|
DebugLn('Exception while translating:');
|
|
DebugLn(e.Message);
|
|
DumpExceptionBackTrace;
|
|
{$ENDIF}
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
function TranslateResourceStrings(const AFilename: string): boolean;
|
|
var
|
|
po: TPOFile;
|
|
begin
|
|
//debugln('TranslateResourceStrings) AFilename="',AFilename,'"');
|
|
if (AFilename='') or (not FileExistsUTF8(AFilename)) then
|
|
exit;
|
|
Result:=false;
|
|
po:=nil;
|
|
try
|
|
po:=TPOFile.Create(AFilename);
|
|
Result:=TranslateResourceStrings(po);
|
|
finally
|
|
po.free;
|
|
end;
|
|
end;
|
|
|
|
procedure TranslateResourceStrings(const BaseFilename, Lang, FallbackLang: string);
|
|
begin
|
|
if (BaseFilename='') then exit;
|
|
|
|
//debugln('TranslateResourceStrings BaseFilename="',BaseFilename,'"');
|
|
if (FallbackLang<>'') then
|
|
TranslateResourceStrings(Format(BaseFilename,[FallbackLang]));
|
|
if (Lang<>'') then
|
|
TranslateResourceStrings(Format(BaseFilename,[Lang]));
|
|
end;
|
|
|
|
{ TPOFile }
|
|
|
|
function TPOFile.GetCount: Integer;
|
|
begin
|
|
Result := FItems.Count;
|
|
end;
|
|
|
|
procedure TPOFile.SetCharSet(const AValue: String);
|
|
begin
|
|
if (CompareText(FCharSet, AValue) = 0) then Exit;
|
|
if (AValue = '') then FCharSet := 'UTF-8'
|
|
else FCharSet := AValue;
|
|
end;
|
|
|
|
function TPOFile.GetPoItem(Index: Integer): TPoFileItem;
|
|
begin
|
|
Result := TPoFileItem(FItems.Items[Index]);
|
|
end;
|
|
|
|
procedure TPOFile.ReadPOText(AStream: TStream);
|
|
var
|
|
Size: Integer;
|
|
s: string;
|
|
begin
|
|
Size:=AStream.Size-AStream.Position;
|
|
if Size<=0 then exit;
|
|
SetLength(s,Size);
|
|
AStream.Read(s[1],Size);
|
|
ReadPOText(s);
|
|
end;
|
|
|
|
constructor TPOFile.Create(Full:Boolean=True);
|
|
begin
|
|
inherited Create;
|
|
FAllEntries:=Full;
|
|
// changing 'fuzzy' flag is allowed by default
|
|
FAllowChangeFuzzyFlag:=true;
|
|
FItems:=TFPList.Create;
|
|
FIdentifierLowToItem:=TStringToPointerTree.Create(true);
|
|
FOriginalToItem:=TStringHashList.Create(true);
|
|
end;
|
|
|
|
constructor TPOFile.Create(const AFilename: String; Full: boolean=false; AllowChangeFuzzyFlag: boolean=true);
|
|
var
|
|
f: TStream;
|
|
begin
|
|
FPoName := AFilename;
|
|
f := TFileStreamUTF8.Create(AFilename, fmOpenRead or fmShareDenyNone);
|
|
try
|
|
Create(f, Full, AllowChangeFuzzyFlag);
|
|
if FHeader=nil then
|
|
CreateHeader;
|
|
finally
|
|
f.Free;
|
|
end;
|
|
end;
|
|
|
|
constructor TPOFile.Create(AStream: TStream; Full: boolean=false; AllowChangeFuzzyFlag: boolean=true);
|
|
begin
|
|
Create;
|
|
|
|
FAllEntries := Full;
|
|
//AllowChangeFuzzyFlag allows not to change fuzzy flag for items with bad format arguments,
|
|
//so there can be arguments with only badformat flag set. This is needed for POChecker.
|
|
FAllowChangeFuzzyFlag := AllowChangeFuzzyFlag;
|
|
|
|
ReadPOText(AStream);
|
|
|
|
if AllowChangeFuzzyFlag then
|
|
CleanUp; // Removes previous ID from non-fuzzy entries (not needed for POChecker)
|
|
InvalidateStatistics;
|
|
end;
|
|
|
|
destructor TPOFile.Destroy;
|
|
var
|
|
i: Integer;
|
|
begin
|
|
if FHelperList<>nil then
|
|
FHelperList.Free;
|
|
if FHeader<>nil then
|
|
FHeader.Free;
|
|
for i:=0 to FItems.Count-1 do
|
|
TObject(FItems[i]).Free;
|
|
FItems.Free;
|
|
FIdentifierLowToItem.Free;
|
|
FOriginalToItem.Free;
|
|
inherited Destroy;
|
|
end;
|
|
|
|
procedure TPOFile.ReadPOText(const Txt: string);
|
|
{ Read a .po file. Structure:
|
|
|
|
Example
|
|
#: lazarusidestrconsts:lisdonotshowsplashscreen
|
|
msgid " Do not show splash screen"
|
|
msgstr ""
|
|
|
|
}
|
|
type
|
|
TMsg = (
|
|
mid,
|
|
mstr,
|
|
mctxt
|
|
);
|
|
var
|
|
l: Integer;
|
|
LineLen: Integer;
|
|
p: PChar;
|
|
LineStart: PChar;
|
|
LineEnd: PChar;
|
|
Cnt: Integer;
|
|
LineNr: Integer;
|
|
Identifier: String;
|
|
PrevMsgID: String;
|
|
Comments: String;
|
|
Flags: string;
|
|
TextEnd: PChar;
|
|
i: Integer;
|
|
OldLineStartPos: PtrUInt;
|
|
NewSrc: String;
|
|
s: String;
|
|
Handled: Boolean;
|
|
CurMsg: TMsg;
|
|
Msg: array[TMsg] of string;
|
|
MsgStrFlag: boolean;
|
|
|
|
procedure ResetVars;
|
|
begin
|
|
CurMsg:=mid;
|
|
Msg[mid]:='';
|
|
Msg[mstr]:='';
|
|
Msg[mctxt]:='';
|
|
Identifier := '';
|
|
Comments := '';
|
|
Flags := '';
|
|
PrevMsgID := '';
|
|
MsgStrFlag := false;
|
|
end;
|
|
|
|
procedure AddEntry (LineNr: Integer);
|
|
var
|
|
Item: TPOFileItem;
|
|
begin
|
|
Item := nil;
|
|
if Identifier<>'' then begin
|
|
FillItem(Item,Identifier,Msg[mid],Msg[mstr],Comments,Msg[mctxt],Flags,PrevMsgID,LineNr);
|
|
ResetVars;
|
|
end
|
|
else if (Msg[CurMsg]<>'') and (FHeader=nil) then begin
|
|
FHeader := TPOFileItem.Create('',Msg[mid],Msg[CurMsg]);
|
|
FHeader.Comments:=Comments;
|
|
ResetVars;
|
|
end;
|
|
end;
|
|
|
|
begin
|
|
if Txt='' then exit;
|
|
s:=Txt;
|
|
l:=length(s);
|
|
p:=PChar(s);
|
|
LineStart:=p;
|
|
TextEnd:=p+l;
|
|
Cnt := 0;
|
|
LineNr := 0;
|
|
ResetVars;
|
|
|
|
while LineStart<TextEnd do begin
|
|
LineEnd:=LineStart;
|
|
while (not (LineEnd^ in [#0,#10,#13])) do inc(LineEnd);
|
|
LineLen:=LineEnd-LineStart;
|
|
Inc(Cnt); // we must count also empty lines
|
|
if LineLen>0 then begin
|
|
Handled:=false;
|
|
case LineStart^ of
|
|
'#':
|
|
begin
|
|
if MsgStrFlag=true then begin
|
|
//we detected comments after previous MsgStr. Consider it as start of new entry
|
|
AddEntry(LineNr);
|
|
inc(Cnt); // for empty line before comment
|
|
LineNr := Cnt; // the comment line is the line number for this entry
|
|
end;
|
|
case LineStart[1] of
|
|
':':
|
|
if LineStart[2]=' ' then begin
|
|
// '#: '
|
|
Identifier:=copy(s,LineStart-p+4,LineLen-3);
|
|
// the RTL creates identifier paths with point instead of colons
|
|
// fix it:
|
|
for i:=1 to length(Identifier) do
|
|
if Identifier[i]=':' then
|
|
Identifier[i]:='.';
|
|
Handled:=true;
|
|
end;
|
|
'|':
|
|
if IsKey(LineStart,'#| msgid "') then begin
|
|
PrevMsgID:=PrevMsgID+GetUTF8String(LineStart+length('#| msgid "'),LineEnd-1);
|
|
Handled:=true;
|
|
end else if IsKey(LineStart, '#| "') then begin
|
|
PrevMsgID := PrevMsgID + GetUTF8String(LineStart+length('#| "'),LineEnd-1);
|
|
Handled:=true;
|
|
end;
|
|
',':
|
|
if LineStart[2]=' ' then begin
|
|
// '#, '
|
|
Flags := GetUTF8String(LineStart+3,LineEnd);
|
|
Handled:=true;
|
|
end;
|
|
end;
|
|
if not Handled then begin
|
|
// '#'
|
|
if Comments<>'' then
|
|
Comments := Comments + LineEnding;
|
|
// if comment is valid then store it, otherwise omit it
|
|
if (LineStart[1]=' ') or (LineStart[1]='.') then
|
|
Comments := Comments + GetUTF8String(LineStart+1,LineEnd)
|
|
else
|
|
GetUTF8String(LineStart+1,LineEnd);
|
|
Handled:=true;
|
|
end;
|
|
end;
|
|
'm':
|
|
if (LineStart[1]='s') and (LineStart[2]='g') then begin
|
|
case LineStart[3] of
|
|
'i':
|
|
if IsKey(LineStart,'msgid "') then begin
|
|
CurMsg:=mid;
|
|
Msg[CurMsg]:=Msg[CurMsg]+GetUTF8String(LineStart+length('msgid "'),LineEnd-1);
|
|
Handled:=true;
|
|
end;
|
|
's':
|
|
if IsKey(LineStart,'msgstr "') then begin
|
|
MsgStrFlag:=true;
|
|
CurMsg:=mstr;
|
|
Msg[CurMsg]:=Msg[CurMsg]+GetUTF8String(LineStart+length('msgstr "'),LineEnd-1);
|
|
Handled:=true;
|
|
end;
|
|
'c':
|
|
if IsKey(LineStart, 'msgctxt "') then begin
|
|
CurMsg:=mctxt;
|
|
Msg[CurMsg]:=Msg[CurMsg]+GetUTF8String(LineStart+length('msgctxt "'), LineEnd-1);
|
|
Handled:=true;
|
|
end;
|
|
end;
|
|
end;
|
|
'"':
|
|
begin
|
|
if (Msg[mid]='')
|
|
and IsKey(LineStart,'"Content-Type: text/plain; charset=') then
|
|
begin
|
|
FCharSet:=GetUTF8String(LineStart+length('"Content-Type: text/plain; charset='),LineEnd);
|
|
if SysUtils.CompareText(FCharSet,'UTF-8')<>0 then begin
|
|
// convert encoding to UTF-8
|
|
OldLineStartPos:=PtrUInt(LineStart-PChar(s))+1;
|
|
NewSrc:=ConvertEncoding(copy(s,OldLineStartPos,length(s)),
|
|
FCharSet,EncodingUTF8);
|
|
// replace text and update all pointers
|
|
s:=copy(s,1,OldLineStartPos-1)+NewSrc;
|
|
l:=length(s);
|
|
p:=PChar(s);
|
|
TextEnd:=p+l;
|
|
LineStart:=p+(OldLineStartPos-1);
|
|
LineEnd:=LineStart;
|
|
while (not (LineEnd^ in [#0,#10,#13])) do inc(LineEnd);
|
|
LineLen:=LineEnd-LineStart;
|
|
end;
|
|
end;
|
|
// continuation
|
|
Msg[CurMsg]:=Msg[CurMsg]+GetUTF8String(LineStart+1,LineEnd-1);
|
|
Handled:=true;
|
|
end;
|
|
end;
|
|
if not Handled then
|
|
AddEntry(LineNr);
|
|
end;
|
|
LineStart:=LineEnd+1;
|
|
while (LineStart^ in [#10,#13]) do inc(LineStart);
|
|
end;
|
|
AddEntry(LineNr);
|
|
FModified := false;
|
|
end;
|
|
|
|
procedure TPOFile.RemoveIdentifiers(AIdentifiers: TStrings);
|
|
var
|
|
I: Integer;
|
|
begin
|
|
for I := 0 to AIdentifiers.Count - 1 do
|
|
RemoveIdentifier(AIdentifiers[I]);
|
|
end;
|
|
|
|
procedure TPOFile.RemoveOriginals(AOriginals: TStrings);
|
|
var
|
|
I: Integer;
|
|
begin
|
|
for I := 0 to AOriginals.Count - 1 do
|
|
RemoveOriginal(AOriginals[I]);
|
|
end;
|
|
|
|
procedure TPOFile.RemoveIdentifier(const AIdentifier: string);
|
|
var
|
|
Index: Integer;
|
|
Item: TPOFileItem;
|
|
begin
|
|
if Length(AIdentifier) > 0 then
|
|
begin
|
|
Item := TPOFileItem(FIdentifierLowToItem[LowerCase(AIdentifier)]);
|
|
if Item <> nil then
|
|
begin
|
|
Index := FItems.IndexOf(Item);
|
|
// We should always find our item, unless there is data corruption.
|
|
if Index >= 0 then
|
|
begin
|
|
Remove(Index);
|
|
Item.Free;
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure TPOFile.RemoveOriginal(const AOriginal: string);
|
|
var
|
|
Index: Integer;
|
|
Item: TPOFileItem;
|
|
begin
|
|
if Length(AOriginal) > 0 then
|
|
// This search is expensive, it could be reimplemented using
|
|
// yet another hash map which maps to items by "original" value
|
|
// with stripped line ending characters.
|
|
for Index := FItems.Count - 1 downto 0 do
|
|
begin
|
|
Item := TPOFileItem(FItems[Index]);
|
|
if CompareMultilinedStrings(Item.Original, AOriginal) = 0 then
|
|
begin
|
|
Remove(Index);
|
|
Item.Free;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
function TPOFile.GetStatistics: TTranslationStatistics;
|
|
var
|
|
Item: TPOFileItem;
|
|
i: Integer;
|
|
begin
|
|
if FStatisticsUpdated = false then
|
|
begin
|
|
FStatistics.Translated := 0;
|
|
FStatistics.Untranslated := 0;
|
|
FStatistics.Fuzzy := 0;
|
|
for i:=0 to Items.Count-1 do
|
|
begin
|
|
Item := TPOFileItem(FItems[i]);
|
|
if Item.Translation = '' then
|
|
Inc(FStatistics.Untranslated)
|
|
else
|
|
if Pos(sFuzzyFlag, Item.Flags)<>0 then
|
|
Inc(FStatistics.Fuzzy)
|
|
else
|
|
Inc(FStatistics.Translated);
|
|
end;
|
|
FStatisticsUpdated := true;
|
|
end;
|
|
Result.Translated := FStatistics.Translated;
|
|
Result.Untranslated := FStatistics.Untranslated;
|
|
Result.Fuzzy := FStatistics.Fuzzy;
|
|
end;
|
|
|
|
function TPOFile.Remove(Index: Integer): TPOFileItem;
|
|
begin
|
|
Result := TPOFileItem(FItems[Index]);
|
|
FOriginalToItem.Remove(Result.Original, Result);
|
|
FIdentifierLowToItem.Remove(Result.IdentifierLow);
|
|
FItems.Delete(Index);
|
|
end;
|
|
|
|
function TPOFile.Translate(const Identifier, OriginalValue: String): String;
|
|
var
|
|
Item: TPOFileItem;
|
|
begin
|
|
Item:=TPOFileItem(FIdentifierLowToItem[lowercase(Identifier)]);
|
|
if Item=nil then
|
|
Item:=TPOFileItem(FOriginalToItem.Data[OriginalValue]);
|
|
//Load translation only if it exists and is NOT fuzzy.
|
|
//This matches gettext behaviour and allows to avoid a lot of crashes related
|
|
//to formatting arguments mismatches.
|
|
if (Item<>nil) and (pos(sFuzzyFlag, Item.Flags)=0)
|
|
//Load translation only if it is not flagged as badformat.
|
|
//This allows to avoid even more crashes related
|
|
//to formatting arguments mismatches.
|
|
and (pos(sBadFormatFlag, Item.Flags)=0) then
|
|
begin
|
|
if Item.Translation<>'' then
|
|
Result:=Item.Translation
|
|
else
|
|
Result:=Item.Original;
|
|
if Result='' then
|
|
Raise Exception.Create('TPOFile.Translate Inconsistency');
|
|
end else
|
|
Result:=OriginalValue;
|
|
end;
|
|
|
|
procedure TPOFile.Report;
|
|
var
|
|
Item: TPOFileItem;
|
|
i: Integer;
|
|
begin
|
|
DebugLn('Header:');
|
|
DebugLn('---------------------------------------------');
|
|
|
|
if FHeader=nil then
|
|
DebugLn('No header found in po file')
|
|
else begin
|
|
DebugLn('Comments=',FHeader.Comments);
|
|
DebugLn('Identifier=',FHeader.IdentifierLow);
|
|
DebugLn('msgid=',FHeader.Original);
|
|
DebugLn('msgstr=', FHeader.Translation);
|
|
end;
|
|
DebugLn;
|
|
|
|
DebugLn('Entries:');
|
|
DebugLn('---------------------------------------------');
|
|
for i:=0 to FItems.Count-1 do begin
|
|
DebugLn(['#', i ,': ']);
|
|
Item := TPOFileItem(FItems[i]);
|
|
DebugLn('Comments=',Item.Comments);
|
|
DebugLn('Identifier=',Item.IdentifierLow);
|
|
DebugLn('msgid=',Item.Original);
|
|
DebugLn('msgstr=', Item.Translation);
|
|
DebugLn;
|
|
end;
|
|
|
|
end;
|
|
|
|
procedure TPOFile.Report(StartIndex, StopIndex: Integer;
|
|
const DisplayHeader: Boolean);
|
|
var
|
|
Item: TPOFileItem;
|
|
i: Integer;
|
|
begin
|
|
if DisplayHeader then
|
|
begin
|
|
DebugLn('Header:');
|
|
DebugLn('---------------------------------------------');
|
|
|
|
if FHeader=nil then
|
|
DebugLn('No header found in po file')
|
|
else begin
|
|
DebugLn('Comments=',FHeader.Comments);
|
|
DebugLn('Identifier=',FHeader.IdentifierLow);
|
|
DebugLn('msgid=',FHeader.Original);
|
|
DebugLn('msgstr=', FHeader.Translation);
|
|
end;
|
|
DebugLn;
|
|
end;
|
|
|
|
if (StartIndex > StopIndex) then
|
|
begin
|
|
i := StopIndex;
|
|
StopIndex := StartIndex;
|
|
StartIndex := i;
|
|
end;
|
|
if (StopIndex > Count - 1) then StopIndex := Count - 1;
|
|
if (StartIndex < 0) then StartIndex := 0;
|
|
|
|
DebugLn(['Entries [', StartIndex, '..', StopIndex, ']:']);
|
|
DebugLn('---------------------------------------------');
|
|
for i := StartIndex to StopIndex do begin
|
|
DebugLn(['#', i, ': ']);
|
|
Item := TPOFileItem(FItems[i]);
|
|
DebugLn('Identifier=',Item.IdentifierLow);
|
|
DebugLn('msgid=',Item.Original);
|
|
DebugLn('msgstr=', Item.Translation);
|
|
DebugLn('Comments=',Item.Comments);
|
|
DebugLn;
|
|
end;
|
|
end;
|
|
|
|
procedure TPOFile.Report(Log: TStrings; StartIndex, StopIndex: Integer;
|
|
const DisplayHeader: Boolean);
|
|
var
|
|
Item: TPOFileItem;
|
|
i: Integer;
|
|
begin
|
|
if DisplayHeader then
|
|
begin
|
|
Log.Add('Header:');
|
|
Log.Add('---------------------------------------------');
|
|
|
|
if FHeader=nil then
|
|
Log.Add('No header found in po file')
|
|
else begin
|
|
Log.Add('Comments='+FHeader.Comments);
|
|
Log.Add('Identifier='+FHeader.IdentifierLow);
|
|
Log.Add('msgid='+FHeader.Original);
|
|
Log.Add('msgstr='+ FHeader.Translation);
|
|
end;
|
|
Log.Add('');
|
|
end;
|
|
|
|
if (StartIndex > StopIndex) then
|
|
begin
|
|
i := StopIndex;
|
|
StopIndex := StartIndex;
|
|
StartIndex := i;
|
|
end;
|
|
if (StopIndex > Count - 1) then StopIndex := Count - 1;
|
|
if (StartIndex < 0) then StartIndex := 0;
|
|
|
|
Log.Add(Format('Entries [%d..%d]:', [StartIndex, StopIndex]));
|
|
Log.Add('---------------------------------------------');
|
|
for i := StartIndex to StopIndex do begin
|
|
Log.Add(Format('#%d: ', [i]));
|
|
Item := TPOFileItem(FItems[i]);
|
|
Log.Add('Identifier='+Item.IdentifierLow);
|
|
Log.Add('msgid='+Item.Original);
|
|
Log.Add('msgstr='+ Item.Translation);
|
|
Log.Add('Comments='+Item.Comments);
|
|
Log.Add('');
|
|
end;
|
|
end;
|
|
|
|
procedure TPOFile.CreateHeader;
|
|
begin
|
|
if FHeader=nil then
|
|
FHeader := TPOFileItem.Create('','','');
|
|
FHeader.Translation:='Content-Type: text/plain; charset=UTF-8';
|
|
FHeader.Comments:='';
|
|
end;
|
|
|
|
procedure TPOFile.UpdateStrings(InputLines: TStrings; SType: TStringsType);
|
|
var
|
|
i, j, n: integer;
|
|
p: LongInt;
|
|
Identifier, Value, Line: string;
|
|
Ch: Char;
|
|
|
|
procedure NextLine;
|
|
begin
|
|
if i<InputLines.Count then
|
|
inc(i);
|
|
if i<InputLines.Count then
|
|
Line := InputLines[i]
|
|
else
|
|
Line := '';
|
|
n := Length(Line);
|
|
p := 1;
|
|
end;
|
|
|
|
procedure NormalizeValue;
|
|
begin
|
|
//treat #10#13 sequences as #13#10 for consistency,
|
|
//e.g. #10#13#13#13#10#13#10 should become #13#10#13#13#10#13#10
|
|
p:=2;
|
|
while p<=Length(Value) do begin
|
|
if (Value[p]=#13) and (Value[p-1]=#10) then begin
|
|
Value[p]:=#10;
|
|
Value[p-1]:=#13;
|
|
end;
|
|
// further analysis shouldn't affect found #13#10 pair
|
|
if (Value[p]=#10) and (Value[p-1]=#13) then
|
|
inc(p);
|
|
inc(p);
|
|
end;
|
|
Value := AdjustLineBreaks(Value);
|
|
|
|
// escape special characters as #number, do not confuse translators
|
|
p:=1;
|
|
while p<=length(Value) do begin
|
|
j := UTF8CodepointSize(pchar(@Value[p]));
|
|
if (j=1) and (Value[p] in [#0..#8,#11,#12,#14..#31,#127..#255]) then
|
|
Value := copy(Value,1,p-1)+'#'+IntToStr(ord(Value[p]))+copy(Value,p+1,length(Value))
|
|
else
|
|
inc(p,j);
|
|
end;
|
|
end;
|
|
|
|
procedure UpdateFromRSJ;
|
|
var
|
|
Parser: TJSONParser;
|
|
JsonItems, SourceBytes: TJSONArray;
|
|
JsonData, JsonItem: TJSONObject;
|
|
K, L: Integer;
|
|
Data: TJSONData;
|
|
begin
|
|
Parser := TJSONParser.Create(InputLines.Text{$IF FPC_FULLVERSION>=30001},jsonscanner.DefaultOptions{$ENDIF});
|
|
try
|
|
JsonData := Parser.Parse as TJSONObject;
|
|
try
|
|
JsonItems := JsonData.Arrays['strings'];
|
|
for K := 0 to JsonItems.Count - 1 do
|
|
begin
|
|
JsonItem := JsonItems.Items[K] as TJSONObject;
|
|
Data:=JsonItem.Find('sourcebytes');
|
|
if Data is TJSONArray then begin
|
|
// fpc 3.1.1 writes the bytes of the source without encoding change
|
|
// while 'value' contains the string encoded as UTF16 with \u hexcodes.
|
|
SourceBytes := TJSONArray(Data);
|
|
SetLength(Value,SourceBytes.Count);
|
|
for L := 1 to length(Value) do
|
|
Value[L] := chr(SourceBytes.Integers[L-1]);
|
|
end else
|
|
Value:=JsonItem.Get('value');
|
|
if Value<>'' then begin
|
|
NormalizeValue;
|
|
UpdateItem(JsonItem.Get('name'), Value);
|
|
end;
|
|
end;
|
|
finally
|
|
JsonData.Free;
|
|
end;
|
|
finally
|
|
Parser.Free;
|
|
end;
|
|
end;
|
|
|
|
begin
|
|
if (SType = stLrj) or (SType = stRsj) then
|
|
// .lrj/.rsj file
|
|
UpdateFromRSJ
|
|
else
|
|
begin
|
|
// for each string in lrt/rst/rsj list check if it's already in PO
|
|
// if not add it
|
|
Value := '';
|
|
Identifier := '';
|
|
i := 0;
|
|
while i < InputLines.Count do begin
|
|
|
|
Line := InputLines[i];
|
|
n := Length(Line);
|
|
|
|
if n=0 then
|
|
// empty line
|
|
else begin
|
|
// .rst file
|
|
if Line[1]='#' then begin
|
|
// rst file: comment
|
|
|
|
Value := '';
|
|
Identifier := '';
|
|
|
|
end else begin
|
|
|
|
p:=Pos('=',Line);
|
|
if P>0 then begin
|
|
|
|
Identifier := copy(Line,1,p-1);
|
|
inc(p); // points to ' after =
|
|
|
|
Value := '';
|
|
while p<=n do begin
|
|
|
|
if Line[p]='''' then begin
|
|
inc(p);
|
|
j:=p;
|
|
while (p<=n)and(Line[p]<>'''') do
|
|
inc(p);
|
|
Value := Value + copy(Line, j, P-j);
|
|
inc(p);
|
|
continue;
|
|
end else
|
|
if Line[p] = '#' then begin
|
|
// a #decimal
|
|
repeat
|
|
inc(p);
|
|
j:=p;
|
|
while (p<=n)and(Line[p] in ['0'..'9']) do
|
|
inc(p);
|
|
|
|
Ch := Chr(StrToInt(copy(Line, j, p-j)));
|
|
Value := Value + Ch;
|
|
|
|
if (p=n) and (Line[p]='+') then
|
|
NextLine;
|
|
|
|
until (p>n) or (Line[p]<>'#');
|
|
end else
|
|
if Line[p]='+' then
|
|
NextLine
|
|
else
|
|
inc(p); // this is an unexpected string
|
|
end;
|
|
|
|
if Value<>'' then begin
|
|
NormalizeValue;
|
|
UpdateItem(Identifier, Value);
|
|
end;
|
|
|
|
end; // if p>0 then begin
|
|
end;
|
|
end;
|
|
|
|
inc(i);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure TPOFile.SaveToStrings(OutLst: TStrings);
|
|
var
|
|
j: Integer;
|
|
|
|
procedure WriteLst(const AProp, AValue: string );
|
|
var
|
|
i: Integer;
|
|
s: string;
|
|
ValueHasTrailingLineEnding: boolean;
|
|
begin
|
|
if (AValue='') and (AProp='') then
|
|
exit;
|
|
|
|
if AValue<>'' then
|
|
ValueHasTrailingLineEnding:=AValue[Length(AValue)] in [#13,#10]
|
|
else
|
|
ValueHasTrailingLineEnding:=false;
|
|
|
|
FHelperList.Text:=AValue;
|
|
if FHelperList.Count=1 then begin
|
|
if AProp='' then
|
|
OutLst.Add(FHelperList[0])
|
|
else begin
|
|
if AProp='#' then
|
|
//comments are not quoted
|
|
OutLst.Add(AProp+FHelperList[0])
|
|
else
|
|
if ValueHasTrailingLineEnding then
|
|
OutLst.Add(AProp+' "'+FHelperList[0]+'\n"')
|
|
else
|
|
OutLst.Add(AProp+' "'+FHelperList[0]+'"');
|
|
end;
|
|
end else begin
|
|
//comments are not quoted, instead prepend each line with '#'
|
|
if (AProp<>'') and (AProp<>'#') then
|
|
OutLst.Add(AProp+' ""');
|
|
for i:=0 to FHelperList.Count-1 do begin
|
|
s := FHelperList[i];
|
|
if (AProp<>'') and (AProp<>'#') then begin
|
|
if (i<FHelperList.Count-1) or ValueHasTrailingLineEnding then
|
|
s := '"' + s + '\n"'
|
|
else
|
|
s := '"' + s + '"';
|
|
if AProp='#| msgid' then
|
|
s := '#| ' + s;
|
|
end else
|
|
if AProp='#' then
|
|
s := AProp + s;
|
|
OutLst.Add(s)
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure WriteItem(Item: TPOFileItem);
|
|
begin
|
|
if Item.Comments<>'' then
|
|
WriteLst('#', Item.Comments);
|
|
if Item.IdentifierLow<>'' then
|
|
OutLst.Add('#: '+Item.IdentifierLow);
|
|
if Trim(Item.Flags)<>'' then
|
|
OutLst.Add('#, '+Trim(Item.Flags));
|
|
if Item.PreviousID<>'' then
|
|
WriteLst('#| msgid', strToPoStr(Item.PreviousID));
|
|
if Item.Context<>'' then
|
|
WriteLst('msgctxt', Item.Context);
|
|
WriteLst('msgid', StrToPoStr(Item.Original));
|
|
WriteLst('msgstr', StrToPoStr(Item.Translation));
|
|
OutLst.Add('');
|
|
end;
|
|
|
|
begin
|
|
if FHeader=nil then
|
|
CreateHeader;
|
|
|
|
if FHelperList=nil then
|
|
FHelperList:=TStringList.Create;
|
|
|
|
// write header
|
|
WriteItem(FHeader);
|
|
|
|
// Sort list of items by identifier
|
|
FItems.Sort(@ComparePOItems);
|
|
|
|
for j:=0 to Fitems.Count-1 do
|
|
WriteItem(TPOFileItem(FItems[j]));
|
|
end;
|
|
|
|
// Remove all entries that have Tag=aTag
|
|
procedure TPOFile.RemoveTaggedItems(aTag: Integer);
|
|
var
|
|
Item: TPOFileItem;
|
|
i: Integer;
|
|
begin
|
|
for i:=FItems.Count-1 downto 0 do
|
|
begin
|
|
Item := TPOFileItem(FItems[i]);
|
|
if Item.Tag = aTag then
|
|
begin
|
|
Remove(i);
|
|
Item.Free;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure TPOFile.SaveToFile(const AFilename: string);
|
|
var
|
|
OutLst: TStringListUTF8;
|
|
begin
|
|
OutLst := TStringListUTF8.Create;
|
|
try
|
|
SaveToStrings(OutLst);
|
|
OutLst.SaveToFile(AFilename);
|
|
finally
|
|
OutLst.Free;
|
|
end;
|
|
end;
|
|
|
|
procedure TPOFile.UpdateItem(const Identifier, Original: string; const Flags: string; const ProcessingTranslation: boolean);
|
|
var
|
|
Item: TPOFileItem;
|
|
ItemHasFuzzyFlag: boolean;
|
|
ItemOldFlags: string;
|
|
begin
|
|
// try to find PO entry by identifier
|
|
Item:=TPOFileItem(FIdentifierLowToItem[lowercase(Identifier)]);
|
|
if Item<>nil then begin
|
|
// found, update item value
|
|
if CompareMultilinedStrings(Item.Original, Original)<>0 then begin
|
|
FModified := True;
|
|
if Item.Translation <> '' then begin
|
|
if (Item.PreviousID = '') or (pos(sFuzzyFlag, Item.Flags) = 0) then
|
|
Item.PreviousID:=Item.Original;
|
|
Item.ModifyFlag(sFuzzyFlag, true);
|
|
end;
|
|
Item.Original:=Original;
|
|
end;
|
|
if ProcessingTranslation then
|
|
begin
|
|
// synchronize translation flags with base .po file, but keep fuzzy flag state
|
|
ItemOldFlags := Item.Flags;
|
|
ItemHasFuzzyFlag := pos(sFuzzyFlag, Item.Flags) <> 0;
|
|
Item.Flags := lowercase(Flags);
|
|
Item.ModifyFlag(sFuzzyFlag, ItemHasFuzzyFlag);
|
|
if ItemOldFlags <> Item.Flags then
|
|
FModified := True;
|
|
end;
|
|
end
|
|
else // in this case new item will be added
|
|
FModified := true;
|
|
FillItem(Item, Identifier, Original, '', '', '', Flags, '');
|
|
end;
|
|
|
|
procedure TPOFile.FillItem(var CurrentItem: TPOFileItem; Identifier, Original,
|
|
Translation, Comments, Context, Flags, PreviousID: string; LineNr: Integer = -1);
|
|
|
|
function VerifyItemFormatting(var Item: TPOFileItem): boolean;
|
|
var
|
|
HasBadFormatFlag: boolean;
|
|
i: integer;
|
|
begin
|
|
// this function verifies item formatting and sets its flags if the formatting is bad
|
|
Result := true;
|
|
if Item.Translation <> '' then
|
|
begin
|
|
Result := CompareFormatArgs(Item.Original,Item.Translation);
|
|
if not Result then
|
|
begin
|
|
if pos(sFuzzyFlag, Item.Flags) = 0 then
|
|
begin
|
|
if FAllowChangeFuzzyFlag = true then
|
|
begin
|
|
Item.ModifyFlag(sFuzzyFlag, true);
|
|
FModified := true;
|
|
end;
|
|
end;
|
|
end;
|
|
HasBadFormatFlag := pos(sBadFormatFlag, Item.Flags) <> 0;
|
|
if HasBadFormatFlag <> not Result then
|
|
begin
|
|
Item.ModifyFlag(sBadFormatFlag, not Result);
|
|
FModified := true;
|
|
end;
|
|
end
|
|
else
|
|
begin
|
|
if pos(sFuzzyFlag, Item.Flags)<>0 then
|
|
begin
|
|
Item.ModifyFlag(sFuzzyFlag, false);
|
|
FModified := true;
|
|
end;
|
|
if pos(sBadFormatFlag, Item.Flags) <> 0 then
|
|
begin
|
|
Item.ModifyFlag(sBadFormatFlag, false);
|
|
FModified := true;
|
|
end;
|
|
end;
|
|
|
|
if Item.Original <> '' then
|
|
begin
|
|
i:=0;
|
|
if (ExtractFormatArgs(Item.Original, i) <> '') and (i = 0) then
|
|
begin
|
|
if Item.ModifyFlag(sFormatFlag, pos(sNoFormatFlag, Item.Flags) = 0) then
|
|
FModified := true;
|
|
end
|
|
else
|
|
if Item.ModifyFlag(sFormatFlag, false) then
|
|
FModified := true;
|
|
end;
|
|
end;
|
|
|
|
var
|
|
FoundItem: TPOFileItem;
|
|
begin
|
|
FoundItem := TPOFileItem(FOriginalToItem.Data[Original]);
|
|
|
|
if CurrentItem = nil then
|
|
begin
|
|
if (not FAllEntries) and (((FoundItem=nil) or (FoundItem.Translation='')) and (Translation='')) then
|
|
exit;
|
|
CurrentItem:=TPOFileItem.Create(lowercase(Identifier), Original, Translation);
|
|
CurrentItem.Comments := Comments;
|
|
CurrentItem.Context := Context;
|
|
CurrentItem.Flags := lowercase(Flags);
|
|
CurrentItem.PreviousID := PreviousID;
|
|
CurrentItem.LineNr := LineNr;
|
|
FItems.Add(CurrentItem);
|
|
//debugln(['TPOFile.FillItem Identifier=',Identifier,' Orig="',dbgstr(OriginalValue),'" Transl="',dbgstr(TranslatedValue),'"']);
|
|
FIdentifierLowToItem[CurrentItem.IdentifierLow]:=CurrentItem;
|
|
end;
|
|
|
|
CurrentItem.Tag := FTag;
|
|
|
|
if FoundItem <> nil then
|
|
begin
|
|
if FoundItem.IdentifierLow<>CurrentItem.IdentifierLow then
|
|
begin
|
|
// if old item doesn't have context, add one
|
|
if FoundItem.Context='' then
|
|
FoundItem.Context := FoundItem.IdentifierLow;
|
|
// if current item doesn't have context, add one
|
|
if CurrentItem.Context='' then
|
|
CurrentItem.Context := CurrentItem.IdentifierLow;
|
|
// marking items as duplicate (needed only by POChecker)
|
|
FoundItem.Duplicate := true;
|
|
CurrentItem.Duplicate := true;
|
|
// if old item is already translated and current item not, use translation
|
|
// note, that we do not copy fuzzy translations in order not to potentially mislead translators
|
|
if (CurrentItem.Translation='') and (FoundItem.Translation<>'') and (pos(sFuzzyFlag, FoundItem.Flags) = 0) then
|
|
begin
|
|
CurrentItem.Translation := FoundItem.Translation;
|
|
if CurrentItem.Flags='' then
|
|
CurrentItem.Flags := FoundItem.Flags;
|
|
CurrentItem.ModifyFlag(sFuzzyFlag, true);
|
|
FModified := True;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
VerifyItemFormatting(CurrentItem);
|
|
|
|
if Original <> '' then
|
|
begin
|
|
if (FoundItem = nil) or ((FoundItem.Translation = '') and (CurrentItem.Translation <> '')) or
|
|
((FoundItem.Translation <> '') and (CurrentItem.Translation <> '') and
|
|
(pos(sFuzzyFlag, FoundItem.Flags) <> 0) and (pos(sFuzzyFlag, CurrentItem.Flags) = 0)) then
|
|
begin
|
|
if FoundItem <> nil then
|
|
FOriginalToItem.Remove(Original);
|
|
FOriginalToItem.Add(Original,CurrentItem);
|
|
end;
|
|
end;
|
|
|
|
end;
|
|
|
|
procedure TPOFile.UpdateTranslation(BasePOFile: TPOFile);
|
|
var
|
|
Item: TPOFileItem;
|
|
i: Integer;
|
|
begin
|
|
UntagAll;
|
|
for i:=0 to BasePOFile.Items.Count-1 do begin
|
|
Item := TPOFileItem(BasePOFile.Items[i]);
|
|
UpdateItem(Item.IdentifierLow, Item.Original, Item.Flags, true);
|
|
end;
|
|
RemoveTaggedItems(0); // get rid of any item not existing in BasePOFile
|
|
InvalidateStatistics;
|
|
end;
|
|
|
|
procedure TPOFile.UntagAll;
|
|
var
|
|
Item: TPOFileItem;
|
|
i: Integer;
|
|
begin
|
|
for i:=0 to Items.Count-1 do begin
|
|
Item := TPOFileItem(Items[i]);
|
|
Item.Tag:=0;
|
|
end;
|
|
end;
|
|
|
|
procedure TPOFile.CleanUp;
|
|
var
|
|
i: Integer;
|
|
aPoItem: TPOFileItem;
|
|
isFuzzy: boolean;
|
|
begin
|
|
for i := 0 to FItems.Count -1 do begin
|
|
aPoItem := TPOFileItem(FItems.Items[i]);
|
|
isFuzzy := pos(sFuzzyFlag,aPoItem.Flags) <> 0;
|
|
if not isFuzzy then
|
|
// remove PreviousID from non-fuzzy Items
|
|
if aPoItem.PreviousID <> '' then begin
|
|
aPoItem.PreviousID := '';
|
|
FModified := true;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure TPOFile.InvalidateStatistics;
|
|
begin
|
|
FStatisticsUpdated := false;
|
|
end;
|
|
|
|
function TPOFile.FindPoItem(const Identifier: String): TPoFileItem;
|
|
begin
|
|
Result := TPOFileItem(FIdentifierLowToItem[lowercase(Identifier)]);
|
|
end;
|
|
|
|
function TPOFile.OriginalToItem(const Data: String): TPoFileItem;
|
|
begin
|
|
// TODO: Should we take into account CompareMultilinedStrings ?
|
|
Result := TPOFileItem(FOriginalToItem.Data[Data]);
|
|
end;
|
|
|
|
{ TPOFileItem }
|
|
|
|
constructor TPOFileItem.Create(const TheIdentifierLow, TheOriginal,
|
|
TheTranslated: string);
|
|
begin
|
|
Duplicate:=false;
|
|
IdentifierLow:=TheIdentifierLow;
|
|
Original:=TheOriginal;
|
|
Translation:=TheTranslated;
|
|
end;
|
|
|
|
function TPOFileItem.ModifyFlag(const AFlags: string; Check: boolean): boolean;
|
|
var
|
|
F, MF: TStringList;
|
|
|
|
procedure ProcessFlag(const AFlag: string);
|
|
var
|
|
i: Integer;
|
|
begin
|
|
i := F.IndexOf(AFlag);
|
|
if (i<0) and Check then
|
|
begin
|
|
F.Add(AFlag);
|
|
Result := true;
|
|
end
|
|
else
|
|
begin
|
|
if (i>=0) and (not Check) then
|
|
begin
|
|
F.Delete(i);
|
|
Result := true;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
var
|
|
i: Integer;
|
|
begin
|
|
Result := false;
|
|
MF := nil;
|
|
F := TStringList.Create;
|
|
try
|
|
F.CommaText := Flags;
|
|
|
|
if Pos(',', AFlags) = 0 then
|
|
ProcessFlag(AFlags)
|
|
else
|
|
begin
|
|
MF := TStringList.Create;
|
|
MF.CommaText := AFlags;
|
|
for i := 0 to MF.Count - 1 do
|
|
ProcessFlag(MF[i]);
|
|
end;
|
|
|
|
if not Result then
|
|
exit;
|
|
Flags := F.CommaText;
|
|
Flags := StringReplace(Flags, ',', ', ', [rfReplaceAll]);
|
|
finally
|
|
F.Free;
|
|
MF.Free;
|
|
end;
|
|
end;
|
|
|
|
end.
|
|
|