mirror of
https://gitlab.com/freepascal.org/lazarus/lazarus.git
synced 2025-07-15 09:16:13 +02:00

Reasons: 1) NrErrors was added for POChecker, but it does not use this property. 2) NrErrors count reflected only formatting errors, but this count has little value without list of strings with these errors. 3) PO file errors/problems are not limited by formatting errors (e.g. there can be master/child PO file string mismatches). Detecting them is out of scope of TPOFile class, so NrErrors count in many cases could be misleading. git-svn-id: trunk@57948 -
1745 lines
49 KiB
ObjectPascal
1745 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.
|
|
*****************************************************************************
|
|
|
|
Author: Mattias Gaertner
|
|
|
|
Author of SimplePoFiles: Bart Broersma
|
|
|
|
Merge by: Giuliano Colla
|
|
|
|
Abstract:
|
|
Methods and classes for loading translations/localizations from po files.
|
|
|
|
This unit is a merge of the Translations unit by Mattias Gaertner and the
|
|
SimplePoFiles unit by Bart Broersma. Its purpose is to provide a single unit
|
|
for easier maintenance.
|
|
In addition the traditional functions, it provides facilities for checking and
|
|
maintaining translations.
|
|
|
|
A number of new properties and methods have been introduced, or exposed, namely:
|
|
|
|
in TPOFileItem - Property LineNr
|
|
Property Identifier (deprecated but left in for compatibility)
|
|
|
|
in TPOFile - Method CheckFormatArguments
|
|
Method CleanUp
|
|
Method FindPoItem
|
|
Property PoName
|
|
Property FormatChecked
|
|
and many more - see the type declaration for details
|
|
|
|
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, LazLogger,
|
|
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);
|
|
|
|
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);
|
|
procedure ModifyFlag(const AFlag: string; Check: boolean);
|
|
property Identifier: string read IdentifierLow; deprecated;
|
|
end;
|
|
|
|
{ TPOFile }
|
|
|
|
TPOFile = class
|
|
private
|
|
FAllowChangeFuzzyFlag: boolean;
|
|
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;
|
|
FNrTranslated: Integer;
|
|
FNrUntranslated: Integer;
|
|
FNrFuzzy: Integer;
|
|
function Remove(Index: Integer): TPOFileItem;
|
|
procedure UpdateCounters(Item: TPOFileItem; Removed: Boolean);
|
|
// 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: string; Original: string);
|
|
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 NrTranslated: Integer read FNrTranslated;
|
|
property NrUntranslated: Integer read FNrUntranslated;
|
|
property NrFuzzy: Integer read FNrFuzzy;
|
|
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 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';
|
|
|
|
|
|
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 ['"','\'] 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;
|
|
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 FindAllTranslatedPoFiles(const Filename: string): TStringList;
|
|
var
|
|
Path: String;
|
|
Name: String;
|
|
NameOnly: String;
|
|
Ext: String;
|
|
FileInfo: TSearchRec;
|
|
CurExt: String;
|
|
begin
|
|
Result:=TStringList.Create;
|
|
Path:=ExtractFilePath(Filename);
|
|
Name:=ExtractFilename(Filename);
|
|
Ext:=ExtractFileExt(Filename);
|
|
NameOnly:=LeftStr(Name,length(Name)-length(Ext));
|
|
if FindFirstUTF8(Path+GetAllFilesMask,faAnyFile,FileInfo)=0 then begin
|
|
repeat
|
|
if (FileInfo.Name='.') or (FileInfo.Name='..') or (FileInfo.Name='')
|
|
or (CompareFilenames(FileInfo.Name,Name)=0) then continue;
|
|
CurExt:=ExtractFileExt(FileInfo.Name);
|
|
if (CompareFilenames(CurExt,'.po')<>0)
|
|
//skip files which names don't have form 'nameonly.foo.po', e.g. 'nameonlyfoo.po'
|
|
or (CompareFilenames(LeftStr(FileInfo.Name,length(NameOnly)+1),NameOnly+'.')<>0)
|
|
then
|
|
continue;
|
|
Result.Add(Path+FileInfo.Name);
|
|
until FindNextUTF8(FileInfo)<>0;
|
|
end;
|
|
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)
|
|
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);
|
|
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.Remove(Index: Integer): TPOFileItem;
|
|
begin
|
|
Result := TPOFileItem(FItems[Index]);
|
|
FOriginalToItem.Remove(Result.Original, Result);
|
|
FIdentifierLowToItem.Remove(Result.IdentifierLow);
|
|
FItems.Delete(Index);
|
|
UpdateCounters(Result, True);
|
|
end;
|
|
|
|
procedure TPOFile.UpdateCounters(Item: TPOFileItem; Removed: Boolean);
|
|
var
|
|
IncrementBy: Integer;
|
|
begin
|
|
if Removed then
|
|
IncrementBy := -1
|
|
else
|
|
IncrementBy := 1;
|
|
if Item.Translation = '' then
|
|
Inc(FNrUntranslated, IncrementBy)
|
|
else if Pos(sFuzzyFlag, Item.Flags)<>0 then
|
|
Inc(FNrFuzzy, IncrementBy)
|
|
else
|
|
Inc(FNrTranslated, IncrementBy);
|
|
end;
|
|
|
|
function TPOFile.Translate(const Identifier, OriginalValue: String): String;
|
|
var
|
|
Item: TPOFileItem;
|
|
l: Integer;
|
|
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
|
|
Result:=Item.Translation;
|
|
if Result='' then
|
|
Raise Exception.Create('TPOFile.Translate Inconsistency');
|
|
end else
|
|
Result:=OriginalValue;
|
|
//Remove lineending at the end of the string if present.
|
|
//This is the case e.g. for multiline strings and not desired when assigning e.g. to
|
|
//Caption property (can negatively affect form layout). In other cases it should not matter.
|
|
l:=Length(Result);
|
|
if l>1 then
|
|
begin
|
|
//Every string with #13 and/or #10 character at the end was treated as multiline, this means that
|
|
//extra lineending could have been added to it.
|
|
if RightStr(Result,2)=#13#10 then
|
|
begin
|
|
if l>2 then //do not leave the string empty
|
|
SetLength(Result,l-2);
|
|
end
|
|
else
|
|
if (Result[l]=#13) or (Result[l]=#10) then
|
|
SetLength(Result,l-1);
|
|
end;
|
|
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;
|
|
MultiLinedValue: boolean;
|
|
|
|
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
|
|
if MultiLinedValue then begin
|
|
// check that we end on lineending, multilined
|
|
// resource strings from rst usually do not end
|
|
// in lineending, fix here.
|
|
if not (Value[Length(Value)] in [#13,#10]) then
|
|
Value := Value + LineEnding;
|
|
|
|
//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);
|
|
end;
|
|
// po requires special characters as #number
|
|
p:=1;
|
|
while p<=length(Value) do begin
|
|
j := UTF8CodepointSize(pchar(@Value[p]));
|
|
if (j=1) and (Value[p] in [#0..#9,#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
|
|
MultiLinedValue := false;
|
|
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 begin
|
|
Value[L] := chr(SourceBytes.Integers[L-1]);
|
|
if Value[L] in [#13,#10] then
|
|
MultilinedValue := True;
|
|
end;
|
|
end else begin
|
|
Value:=JsonItem.Get('value');
|
|
// check if the value we got is multilined
|
|
L := 1;
|
|
while (L<=Length(Value)) and (MultiLinedValue = false) do begin
|
|
if Value[L] in [#13,#10] then
|
|
MultilinedValue := True;
|
|
inc(L);
|
|
end;
|
|
end;
|
|
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
|
|
MultilinedValue := false;
|
|
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 := '';
|
|
MultilinedValue := false;
|
|
|
|
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 Ch in [#13,#10] then
|
|
MultilinedValue := True;
|
|
|
|
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;
|
|
begin
|
|
if (AValue='') and (AProp='') then
|
|
exit;
|
|
|
|
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
|
|
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
|
|
s := '"' + s + '\n"';
|
|
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: string; Original: string);
|
|
var
|
|
Item: TPOFileItem;
|
|
begin
|
|
if FHelperList=nil then
|
|
FHelperList := TStringList.Create;
|
|
|
|
// 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;
|
|
end;
|
|
Item.Original:=Original;
|
|
end
|
|
else // in this case new item will be added
|
|
FModified := true;
|
|
FillItem(Item, Identifier, Original, '', '', '', '', '');
|
|
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;
|
|
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
|
|
inc(FNrFuzzy);
|
|
dec(FNrTranslated);
|
|
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;
|
|
end;
|
|
|
|
var
|
|
FoundItem: TPOFileItem;
|
|
NewItem: boolean;
|
|
begin
|
|
NewItem := false;
|
|
|
|
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;
|
|
NewItem := true;
|
|
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 NewItem = true then
|
|
begin
|
|
UpdateCounters(CurrentItem, False);
|
|
|
|
FItems.Add(CurrentItem);
|
|
|
|
//debugln(['TPOFile.FillItem Identifier=',Identifier,' Orig="',dbgstr(OriginalValue),'" Transl="',dbgstr(TranslatedValue),'"']);
|
|
FIdentifierLowToItem[CurrentItem.IdentifierLow]:=CurrentItem;
|
|
end;
|
|
|
|
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);
|
|
end;
|
|
RemoveTaggedItems(0); // get rid of any item not existing in BasePOFile
|
|
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;
|
|
|
|
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;
|
|
|
|
procedure TPOFileItem.ModifyFlag(const AFlag: string; Check: boolean);
|
|
var
|
|
i: Integer;
|
|
F: TStringList;
|
|
begin
|
|
F := TStringList.Create;
|
|
try
|
|
F.CommaText := Flags;
|
|
i := F.IndexOf(AFlag);
|
|
if (i<0) and Check then
|
|
F.Add(AFlag)
|
|
else
|
|
if (i>=0) and (not Check) then
|
|
F.Delete(i);
|
|
Flags := F.CommaText;
|
|
finally
|
|
F.Free;
|
|
end;
|
|
end;
|
|
|
|
end.
|
|
|