lazarus/components/lazutils/translations.pas

1748 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;
FNrErrors: 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;
property NrErrors: Integer read FNrErrors;
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.
// 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;
FNrErrors := 0;
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
inc(FNrErrors);
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.