lazarus/lcl/translations.pas
maxim 45daca322f IDE: Add ability to exclude components from i18n by identifier or original text:
1) Added methods to remove entries from TPOFile by identifier and by original text.
2) Implemented storage of excluded identifiers and originals via TProject.
3) Updated translation routines to pass over project's excluded identifiers and originals when updating PO files.
4) Editing of excluded identifiers and originals in i18n project options.
5) Added option to "Force update PO files on next compile" in i18n project options (auto reset, non-persistent). To force update PO files after changing excluded identifiers and originals.

Patch by Denis Kozlov, bug #29627.

git-svn-id: trunk@51589 -
2016-02-10 22:46:00 +00:00

1828 lines
50 KiB
ObjectPascal

{
*****************************************************************************
This file is part of the Lazarus Component Library (LCL)
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}
{$include include/lcl_defines.inc}
interface
uses
Classes, SysUtils, LCLProc, FileUtil, LazFileUtils, StringHashList, AvgLvlTree,
LConvEncoding, LazUTF8, LazUTF8Classes,
{$IF FPC_FULLVERSION>=30101}jsonscanner,{$ENDIF} jsonparser, fpjson;
type
TStringsType = (
stLrt, // Lazarus resource string table
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;
constructor Create(const TheIdentifierLow, TheOriginal, TheTranslated: string);
procedure ModifyFlag(const AFlag: string; Check: boolean);
property Identifier: string read IdentifierLow; deprecated;
end;
{ TPOFile }
TPOFile = class
protected
FItems: TFPList;// list of TPOFileItem
FIdentifierLowToItem: TStringToPointerTree; // lowercase identifier to TPOFileItem
FIdentLowVarToItem: TStringHashList; // of TPOFileItem
FOriginalToItem: TStringHashList; // of TPOFileItem
FCharSet: String;
FHeader: TPOFileItem;
FAllEntries: boolean;
FTag: Integer;
FModified: boolean;
FHelperList: TStringList;
FModuleList: TStringList;
// New fields
FPoName: string;
FNrTranslated: Integer;
FNrUntranslated: Integer;
FNrFuzzy: Integer;
FNrErrors: Integer;
FFormatChecked: Boolean;
procedure RemoveTaggedItems(aTag: Integer);
procedure RemoveUntaggedModules;
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);
constructor Create(AStream: TStream; Full:boolean=false);
destructor Destroy; override;
procedure ReadPOText(const Txt: string);
procedure Add(const Identifier, OriginalValue, TranslatedValue, Comments,
Context, Flags, PreviousID: string; SetFuzzy: boolean = false; LineNr: Integer = -1);
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 UpdateTranslation(BasePOFile: TPOFile);
procedure ClearModuleList;
procedure AddToModuleList(Identifier: string);
procedure UntagAll;
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 CheckFormatArguments;
procedure CleanUp; { removes previous ID from non-fuzzy entries
and badformat flags if appropriate }
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 OriginalList: TStringHashList read FOriginalToItem;
property PoItems[Index: Integer]: TPoFileItem read GetPoItem;
property Count: Integer read GetCount;
property Header: TPOFileItem read FHeader;
property FormatChecked: boolean read FFormatChecked;
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
tgHasDup = $01;
sFuzzyFlag = 'fuzzy';
sBadFormatFlag = 'badformat';
implementation
{$DEFINE CHECK_FORMAT}
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)
or (CompareFilenames(LeftStr(FileInfo.Name,length(NameOnly)),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;
// Update po file with lrt,rst/rsj of RSTFiles
for i:=0 to RSTFiles.Count-1 do begin
Filename:=RSTFiles[i];
if (CompareFileExt(Filename,'.lrt')=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,'.lrt')=0 then
BasePOFile.UpdateStrings(InputLines, stLrt)
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;
BasePOFile.SaveToFile(POFilename);
Result := BasePOFile.Modified;
UpdatePoFileTranslations(POFilename,BasePoFile);
finally
InputLines.Free;
BasePOFile.Free;
end;
end;
function Translate (Name,Value : AnsiString; 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) ResUnitName,'" 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 }
procedure TPOFile.RemoveUntaggedModules;
var
Module: string;
Item,VItem: TPOFileItem;
i, p: Integer;
VarName: String;
begin
if FModuleList=nil then
exit;
// remove all module references that were not tagged
for i:=FItems.Count-1 downto 0 do begin
Item := TPOFileItem(FItems[i]);
p := pos('.',Item.IdentifierLow);
if P=0 then
continue; // module not found (?)
Module :=LeftStr(Item.IdentifierLow, p-1);
if (FModuleList.IndexOf(Module)<0) then
continue; // module was not modified this time
if Item.Tag=FTag then
continue; // PO item was updated
// this item is not more in updated modules, delete it
FIdentifierLowToItem.Remove(Item.IdentifierLow);
// delete it also from VarToItem
VarName := RightStr(Item.IdentifierLow, Length(Item.IdentifierLow)-P);
VItem := TPoFileItem(FIdentLowVarToItem.Data[VarName]);
if (VItem=Item) then
FIdentLowVarToItem.Remove(VarName);
FOriginalToItem.Remove(Item.Original, Item);
FItems.Delete(i);
Item.Free;
end;
end;
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;
FItems:=TFPList.Create;
FIdentifierLowToItem:=TStringToPointerTree.Create(true);
FIdentLowVarToItem:=TStringHashList.Create(true);
FOriginalToItem:=TStringHashList.Create(true);
end;
constructor TPOFile.Create(const AFilename: String; Full:boolean=False);
var
f: TStream;
begin
FPoName := AFilename;
f := TFileStreamUTF8.Create(AFilename, fmOpenRead or fmShareDenyNone);
try
Create(f, Full);
if FHeader=nil then
CreateHeader;
finally
f.Free;
end;
end;
constructor TPOFile.Create(AStream: TStream; Full:boolean=false);
begin
Create;
FAllEntries := Full;
ReadPOText(AStream);
{$IFDEF CHECK_FORMAT}
CheckFormatArguments; // Verify that translation will not generate crashes
CleanUp; // Remove leftover badformat flags and keep only real ones
{$ENDIF}
end;
destructor TPOFile.Destroy;
var
i: Integer;
begin
if FModuleList<>nil then
FModuleList.Free;
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;
FIdentLowVarToItem.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,
mctx
);
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[mctx]:='';
Identifier := '';
Comments := '';
Flags := '';
PrevMsgID := '';
MsgStrFlag := false;
end;
procedure AddEntry (LineNr: Integer);
var
Item: TPOFileItem;
SetFuzzy: boolean;
begin
if Identifier<>'' then begin
SetFuzzy:=false;
// check for unresolved duplicates in po file
Item := TPOFileItem(FOriginalToItem.Data[Msg[mid]]);
if (Item<>nil) then begin
// fix old duplicate context
if Item.Context='' then
Item.Context:=Item.IdentifierLow;
// set context of new duplicate
if Msg[mctx]='' then
Msg[mctx] := Identifier;
// if old duplicate was translated and new one is not,
// provide an initial translation and set a flag to mark it fuzzy
if (Msg[mstr]='') and (Item.Translation<>'') then begin
// copy flags to new duplicate
if Flags='' then
Flags := Item.Flags;
Msg[mstr] := Item.Translation;
// if old item is fuzzy, copy PreviousID too
if pos('fuzzy', Item.Flags)<>0 then
PrevMsgID := Item.PreviousID;
// mark newly translated item fuzzy
SetFuzzy:=true;
end;
end;
Add(Identifier,Msg[mid],Msg[mstr],Comments,Msg[mctx],Flags,PrevMsgID,SetFuzzy,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:=mctx;
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;
var
P: Integer;
begin
Result := TPOFileItem(FItems[Index]);
FOriginalToItem.Remove(Result.Original, Result);
FIdentifierLowToItem.Remove(Result.IdentifierLow);
P := Pos('.', Result.IdentifierLow);
if P>0 then
FIdentLowVarToItem.Remove(Copy(Result.IdentifierLow, P+1, Length(Result.IdentifierLow)));
FItems.Delete(Index);
UpdateCounters(Result, True);
end;
procedure TPOFile.Add(const Identifier, OriginalValue, TranslatedValue,
Comments, Context, Flags, PreviousID: string; SetFuzzy: boolean = false; LineNr: Integer = -1);
var
Item: TPOFileItem;
p: Integer;
begin
if (not FAllEntries) and (TranslatedValue='') then exit;
Item:=TPOFileItem.Create(lowercase(Identifier),OriginalValue,TranslatedValue);
Item.Comments:=Comments;
Item.Context:=Context;
Item.Flags:=Flags;
if SetFuzzy = true then
Item.ModifyFlag(sFuzzyFlag, true);
Item.PreviousID:=PreviousID;
Item.Tag:=FTag;
Item.LineNr := LineNr;
UpdateCounters(Item, False);
FItems.Add(Item);
//debugln(['TPOFile.Add Identifier=',Identifier,' Orig="',dbgstr(OriginalValue),'" Transl="',dbgstr(TranslatedValue),'"']);
FIdentifierLowToItem[Item.IdentifierLow]:=Item;
P := Pos('.', Identifier);
if P>0 then
FIdentLowVarToItem.Add(copy(Item.IdentifierLow, P+1, Length(Item.IdentifierLow)), Item);
if OriginalValue<>'' then
FOriginalToItem.Add(OriginalValue,Item);
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;
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, lowercase(Item.Flags))=0)
{$IFDEF CHECK_FORMAT}
//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, lowercase(Item.Flags))=0)
{$ENDIF}
then begin
Result:=Item.Translation;
if Result='' then RaiseGDBException('TPOFile.Translate Inconsistency');
end else
Result:=OriginalValue;
end;
procedure TPOFile.Report;
var
Item: TPOFileItem;
i: Integer;
begin
DebugLn('Header:');
DebugLn('---------------------------------------------');
if FHeader=nil then
DebugLn('No header found in po file')
else begin
DebugLn('Comments=',FHeader.Comments);
DebugLn('Identifier=',FHeader.IdentifierLow);
DebugLn('msgid=',FHeader.Original);
DebugLn('msgstr=', FHeader.Translation);
end;
DebugLn;
DebugLn('Entries:');
DebugLn('---------------------------------------------');
for i:=0 to FItems.Count-1 do begin
DebugLn('#',dbgs(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 [',DbgS(StartIndex),'..',Dbgs(StopIndex),']:');
DebugLn('---------------------------------------------');
for i := StartIndex to StopIndex do begin
DebugLn('#',dbgs(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('Entries ['+DbgS(StartIndex)+'..'+Dbgs(StopIndex)+']:');
Log.Add('---------------------------------------------');
for i := StartIndex to StopIndex do begin
Log.Add('#'+dbgs(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 := UTF8CharacterLength(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>=30101},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
ClearModuleList;
UntagAll;
if SType = stRsj then
// .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
if SType=stLrt then begin
// .lrt file
p:=Pos('=',Line);
Value :=copy(Line,p+1,n-p); //if p=0, that's OK, all the string
Identifier:=copy(Line,1,p-1);
UpdateItem(Identifier, Value);
end 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;
RemoveUntaggedModules;
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;
AContext,AComment,ATranslation,AFlags,APrevStr: string;
SetFuzzy: boolean;
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
AddToModuleList(Identifier);
if CompareMultilinedStrings(Item.Original, Original)<>0 then begin
FModified := True;
if Item.Translation<>'' then begin
Item.ModifyFlag(sFuzzyFlag, true);
Item.PreviousID:=Item.Original;
end;
end;
Item.Original:=Original;
Item.Tag:=FTag;
exit;
end;
// try to find po entry based only on it's value
SetFuzzy := false;
AContext := '';
AComment := '';
ATranslation := '';
AFlags := '';
APrevStr := '';
Item := TPOFileItem(FOriginalToItem.Data[Original]);
if Item<>nil then begin
// old item don't have context, add one
if Item.Context='' then
Item.Context := Item.IdentifierLow;
// if old item is already translated use translation
if Item.Translation<>'' then begin
ATranslation := Item.Translation;
// if old item is fuzzy, copy PreviousID too
if pos(sFuzzyFlag, Item.Flags)<>0 then
APrevStr := Item.PreviousID;
// set a flag to mark item fuzzy if it is not already
SetFuzzy := true;
end;
AFlags := Item.Flags;
// update identifier list
AContext := Identifier;
end;
// this appear to be a new item
FModified := true;
Add(Identifier, Original, ATranslation, AComment, AContext, AFlags, APrevStr, SetFuzzy);
end;
procedure TPOFile.UpdateTranslation(BasePOFile: TPOFile);
var
Item: TPOFileItem;
i: Integer;
begin
UntagAll;
ClearModuleList;
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.ClearModuleList;
begin
if FModuleList<>nil then
FModuleList.Clear;
end;
procedure TPOFile.AddToModuleList(Identifier: string);
var
p: Integer;
begin
if FModuleList=nil then begin
FModuleList := TStringList.Create;
FModuleList.Duplicates:=dupIgnore;
end;
p := pos('.', Identifier);
if p>0 then
FModuleList.Add(LeftStr(Identifier, P-1));
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.CheckFormatArguments;
var
I: Integer;
aPoItem: TPOFileItem;
isFuzzy: boolean;
isBadFormat: boolean;
begin
FNrErrors := 0;
for I := 0 to FItems.Count -1 do begin
aPoItem := TPOFileItem(FItems.Items[I]);
if aPoItem.Translation = '' then Continue;
isFuzzy := pos(sFuzzyFlag,aPoItem.Flags) <> 0;
isBadFormat := pos(sBadFormatFlag,aPoItem.Flags) <> 0;
if (pos('%',aPoItem.Original) <> 0) or (pos('%',aPoItem.Translation) <> 0) then begin
if not CompareFormatArgs(aPoItem.Original,aPoItem.Translation) then begin
inc(FNrErrors);
if not isFuzzy then begin
aPoItem.ModifyFlag(sFuzzyFlag,true);
inc(FNrFuzzy);
dec(FNrTranslated);
FModified := true;
end;
if not isBadFormat then begin
aPoItem.ModifyFlag(sBadFormatFlag,true);
FModified := true;
end;
end;
end
else begin // possibly an offending string has been removed
if isBadFormat then begin
aPoItem.ModifyFlag(sBadFormatFlag,False);
FModified := true;
end;
end;
end;
FFormatChecked := true;
end;
procedure TPOFile.CleanUp;
var
I: Integer;
aPoItem: TPOFileItem;
isFuzzy: boolean;
isBadFormat: boolean;
begin
for I := 0 to FItems.Count -1 do begin
aPoItem := TPOFileItem(FItems.Items[I]);
isFuzzy := pos(sFuzzyFlag,aPoItem.Flags) <> 0;
isBadFormat := pos(sBadFormatFlag,aPoItem.Flags) <> 0;
if not isFuzzy then begin
// remove PreviousID from non-fuzzy Items
if (aPoItem.PreviousID <> '') then begin
aPoItem.PreviousID := '';
FModified := true;
end;
// remove badformat flag from non-fuzzy Items
if isBadFormat and FFormatChecked then begin
aPoItem.ModifyFlag(sBadFormatFlag,false);
FModified := true;
end;
end;
// is Context of some use ?
{if aPoItem.Context = '' then begin
aPoItem.Context := aPoItem.IdentifierLow;
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
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.