lazarus/components/pochecker/simplepofiles.pp

1302 lines
32 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.
*****************************************************************************
This file is based upon the translations.pas file by Mattias Gaertner
Author: Bart Broersma
Year: 2011
Abstract:
Methods and classes for loading and checking validity of po-files.
Note:
Most references to unneeded methods/functions/procedures are commented out,
if we later need them, we can easily uncomment the relevant parts
* For the moment I left out all character encoding stuff: all the relevant
strings I need to investigate can be investigated without knowing the encoding
If a program needs to know the encoding, it can read the CharSet property of TSimplePoFile
* Change the implementation of ReadPoText to use Strings instead of PChars, this resulted in
a speed-up with a factor 20
(ifdef-ed the old method)
* Added LineNr to TPoFileItem
}
{ $define DebugSimplePoFiles}
unit SimplePoFiles;
{$mode objfpc}{$H+}{$INLINE ON}
{ $include include/lcl_defines.inc}
interface
uses
Classes, SysUtils, LCLProc, FileUtil, StringHashList
{, LConvEncoding}
//{$IFDEF UNIX}{$IFNDEF DisableCWString}, cwstring{$ENDIF}{$ENDIF}
;
{
type
TStringsType = (stLrt, stRst);
TTranslateUnitResult = (turOK, turNoLang, turNoFBLang, turEmptyParam);
}
type
{ TPOFileItem }
TPOFileItem = class
public
LineNr: Integer;
Tag: Integer;
Comments: string;
Identifier: string;
Original: string;
Translation: string;
Flags: string;
PreviousID: string;
Context: string;
constructor Create(const TheIdentifier, TheOriginal, TheTranslated: string);
procedure ModifyFlag(const AFlag: string; Check: boolean);
end;
{ TSimplePOFile }
TSimplePOFile = class
protected
FItems: TFPList;// list of TPOFileItem
FIdentifierToItem: TStringHashList;
//FIdentVarToItem: TStringHashList;
FOriginalToItem: TStringHashList;
FCharSet: String;
FHeader: TPOFileItem;
FAllEntries: boolean;
FTag: Integer;
//FModified: boolean;
FHelperList: TStringList;
FModuleList: TStringList;
//procedure RemoveTaggedItems(aTag: Integer);
//procedure RemoveUntaggedModules;
function GetCount: Integer;
procedure SetCharSet(const AValue: String);
procedure ReadPOText(AStream: TStream);
function GetPoItem(Index: Integer): TPoFileItem;
protected
property Items: TFPList read FItems;
public
property OriginalList: TStringHashList read FOriginalToItem;
public
constructor Create(const AFilename: String; const Full: Boolean = True);
constructor Create(AStream: TStream; const Full: Boolean = True);
destructor Destroy; override;
procedure Add(const Identifier, OriginalValue, TranslatedValue, Comments,
Context, Flags, PreviousID: string; LineNr: Integer);
//function Translate(const Identifier, OriginalValue: String): String;
procedure Report;
procedure Report(StartIndex, StopIndex: Integer; const DisplayHeader: Boolean);
procedure Report(Log: TStrings; StartIndex, StopIndex: Integer; const DisplayHeader: Boolean);
procedure CreateHeader;
//procedure UpdateStrings(InputLines:TStrings; SType: TStringsType);
//procedure SaveToFile(const AFilename: string);
//procedure UpdateItem(const Identifier: string; Original: string);
//procedure UpdateTranslation(BasePOFile: TSimplePOFile);
//procedure ClearModuleList;
//procedure AddToModuleList(Identifier: string);
//procedure UntagAll;
function FindPoItem(const Identifier: String): TPoFileItem;
function OriginalToItem(Data: String): TPoFileItem;
property CharSet: String read FCharSet;
property Tag: integer read FTag write FTag;
//property Modified: boolean read FModified;
property PoItems[Index: Integer]: TPoFileItem read GetPoItem;
property Count: Integer read GetCount;
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 UTF8ToSystemCharSet(const s: string): string; inline;
//function UpdatePoFile(Files: TStrings; const POFilename: string): boolean;
const
tgHasDup = $01;
implementation
{$ifdef DebugSimplePoFiles}
var
T0, T1: DWord; function GetTickCount: DWord;
var
HH, MM, SS, MS: Word;
begin
DecodeTime(Now, HH, MM, SS, MS);
Result := DWord(MS) + (DWord(SS) * 1000) + (DWord(MM) * 1000 * 60) + (DWord(HH) * 1000 * 60 * 24);
end;
{$endif}
function UTF8ToSystemCharSet(const s: string): string; inline;
begin
if SystemCharSetIsUTF8 then
exit(s);
{$IFDEF NoUTF8Translations}
Result:=s;
{$ELSE}
Result:=UTF8ToSys(s);
{$ENDIF}
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 UpdatePOFile(Files: TStrings; const POFilename: string): boolean;
var
InputLines: TStringList;
Filename: string;
BasePoFile, POFile: TSimplePOFile;
i: Integer;
E: EPOFileError;
procedure UpdatePoFilesTranslation;
var
j: Integer;
Lines: TStringList;
begin
// Update translated PO files
Lines := FindAllTranslatedPoFiles(POFilename);
try
for j:=0 to Lines.Count-1 do begin
POFile := TSimplePOFile.Create(Lines[j], true);
try
POFile.Tag:=1;
POFile.UpdateTranslation(BasePOFile);
try
POFile.SaveToFile(Lines[j]);
except
on Ex: Exception do begin
E := EPOFileError.Create(Ex.Message);
E.ResFileName:=Lines[j];
E.POFileName:=POFileName;
raise E;
end;
end;
finally
POFile.Free;
end;
end;
finally
Lines.Free;
end;
end;
begin
Result := false;
if (Files=nil) or (Files.Count=0) then begin
if FileExistsUTF8(POFilename) then begin
// just update translated po files
BasePOFile := TSimplePOFile.Create(POFilename, true);
try
UpdatePoFilesTranslation;
finally
BasePOFile.Free;
end;
end;
exit;
end;
InputLines := TStringList.Create;
try
// Read base po items
if FileExistsUTF8(POFilename) then
BasePOFile := TSimplePOFile.Create(POFilename, true)
else
BasePOFile := TSimplePOFile.Create;
BasePOFile.Tag:=1;
// Update po file with lrt or/and rst files
for i:=0 to Files.Count-1 do begin
Filename:=Files[i];
if (CompareFileExt(Filename,'.lrt')=0)
or (CompareFileExt(Filename,'.rst')=0) then
try
//DebugLn('');
//DebugLn(['AddFiles2Po Filename="',Filename,'"']);
InputLines.Clear;
InputLines.LoadFromFile(UTF8ToSys(FileName));
if CompareFileExt(Filename,'.lrt')=0 then
BasePOFile.UpdateStrings(InputLines, stLrt)
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;
UpdatePOFilesTranslation;
finally
InputLines.Free;
BasePOFile.Free;
end;
end;
}
{
function Translate (Name,Value : AnsiString; Hash : Longint; arg:pointer) : AnsiString;
var
po: TSimplePOFile;
begin
po:=TSimplePOFile(arg);
// get UTF8 string
result := po.Translate(Name,Value);
// convert UTF8 to current local
if result<>'' then
result:=UTF8ToSystemCharSet(result);
end;
}
{ TSimplePOFile }
{
procedure TSimplePOFile.RemoveUntaggedModules;
var
Module: string;
Item,VItem: TPOFileItem;
i, p: Integer;
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.Identifier);
if P=0 then
continue; // module not found (?)
Module :=LeftStr(Item.Identifier, 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
FIdentifierToItem.Remove(Item.Identifier);
// delete it also from VarToItem
Module := RightStr(Item.Identifier, Length(Item.Identifier)-P);
VItem := TPoFileItem(FIdentVarToItem.Data[Module]);
if (VItem=Item) then
FIdentVarToItem.Remove(Module);
FOriginalToItem.Remove(Item.Original); // isn't this tricky?
FItems.Delete(i);
Item.Free;
end;
end;
}
function TSimplePOFile.GetCount: Integer;
begin
Result := FItems.Count;
end;
procedure TSimplePOFile.SetCharSet(const AValue: String);
begin
if (CompareText(FCharSet, AValue) = 0) then Exit;
if (AValue = '') then FCharSet := 'UTF-8'
else FCharSet := AValue;
end;
constructor TSimplePOFile.Create(const AFilename: String; const Full: Boolean = True);
var
f: TStream;
begin
f := TFileStream.Create(UTF8ToSys(AFilename), fmOpenRead or fmShareDenyNone);
try
Create(f, Full);
if FHeader=nil then CreateHeader;
finally
f.Free;
end;
end;
constructor TSimplePOFile.Create(AStream: TStream; const Full: Boolean = True);
var
Size: Integer;
begin
inherited Create;
FAllEntries:=true;
FItems:=TFPList.Create;
FIdentifierToItem:=TStringHashList.Create(false);
//FIdentVarToItem:=TStringHashList.Create(false);
FOriginalToItem:=TStringHashList.Create(true);
FAllEntries := Full;
Size:=AStream.Size-AStream.Position;
if Size<=0 then exit;
ReadPoText(AStream);
end;
destructor TSimplePOFile.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;
//FIdentVarToItem.Free;
FIdentifierToItem.Free;
FOriginalToItem.Free;
inherited Destroy;
end;
function SliceToStr(SourceStart: PChar; SourceLen: PtrInt) : string;
//converts PChar (can be in the middle of some larger string) to a string
var
Dest: PChar;
begin
SetLength(Result, SourceLen);
Dest := PChar(Result);
System.Move(SourceStart^, Dest^, SourceLen);
end;
procedure TSimplePOFile.ReadPOText(AStream: TStream);
{ Read a .po file. Structure:
Example
#: lazarusidestrconsts:lisdonotshowsplashscreen
msgid "Do not show splash screen"
msgstr ""
}
const
sCommentIdentifier = '#: ';
lCommentIdentifier = 3;
sCharSetIdentifier = '"Content-Type: text/plain; charset=';
lCharSetIdentifier = 35;
sMsgID = 'msgid "';
//lMsgId = 7;
sMsgStr = 'msgstr "';
//lMsgStr = 8;
sMsgCtxt = 'msgctxt "';
lMsgCtxt = 9;
sFlags = '#, ';
lFlags = 3;
sPrevMsgID = '#| msgid "';
//lPrevMsgId = 11;
sPrevStr = '#| "';
lPrevStr = 4;
const
ciNone = 0;
ciMsgID = 1;
ciMsgStr = 2;
ciPrevMsgID = 3;
var
SL: TStringList;
//l: Integer;
Cnt: Integer;
LineLen: Integer;
LineNr: Integer;
//LineStart: PChar;
//LineEnd: PChar;
Identifier: String;
MsgID,MsgStr,PrevMsgID: String;
Line: String;
Comments: String;
Context: string;
Flags: string;
//TextEnd: PChar;
i, CollectedIndex: Integer;
//OldLineStartPos: PtrUInt;
//NewSrc: String;
//s: String;
CurLine: String;
function HasPrefix(const Prefix, aStr: string): boolean;
var
k: Integer;
begin
Result:=false;
if length(aStr)<length(Prefix) then exit;
for k:=1 to length(Prefix) do
if Prefix[k]<>aStr[k] then exit;
Result:=true;
end;
procedure ResetVars;
begin
MsgId := '';
MsgStr := '';
Line := '';
Identifier := '';
Comments := '';
Context := '';
Flags := '';
PrevMsgID := '';
CollectedIndex := ciNone;
LineNr := 0;
end;
procedure StoreCollectedLine;
begin
case CollectedIndex of
ciMsgID: MsgID := Line;
ciMsgStr: MsgStr := Line;
ciPrevMsgID: PrevMsgID := Line;
end;
CollectedIndex := ciNone;
end;
procedure AddEntry(const LineNr: Integer);
//var
//Item: TPOFileItem;
begin
StoreCollectedLine;
if Identifier<>'' then
begin
// check for unresolved duplicates in po file
{
Item := TPOFileItem(FOriginalToItem.Data[MsgID]);
if (Item<>nil) then begin
// fix old duplicate context
if Item.Context='' then
Item.Context:=Item.Identifier;
// set context of new duplicate
if Context='' then
Context := Identifier;
// if old duplicate was translated and
// new one is not, provide a initial translation
if MsgStr='' then
MsgStr := Item.Translation;
end;
}
Add(Identifier,MsgID,MsgStr,Comments,Context,Flags,PrevMsgID, LineNr);
ResetVars;
end else
if (Line<>'') and (FHeader=nil) then
begin
FHeader := TPOFileItem.Create('',MsgID,Line);
FHeader.Comments:=Comments;
ResetVars;
end
end;
function TestPrefixStr(AIndex: Integer): boolean;
var
s: string;
l: Integer;
begin
case aIndex of
ciMsgID: s:=sMsgId;
ciMsgStr: s:=sMsgStr;
ciPrevMsgId: s:=sPrevMsgId;
end;
L := Length(s);
result := HasPrefix(S, CurLine);
if Result then
begin
StoreCollectedLine;
CollectedIndex := AIndex;
Line := Copy(CurLine,L+1,LineLen-L-1);
end;
end;
begin
{$ifdef DebugSimplePoFiles}
T0 := GetTickCount;
{$endif}
SL := TStringList.Create;
SL.LoadFromStream(AStream);
try
//if SL.Count > 0 then AdjustLinebreaks(SL.Text);
Identifier:='';
Comments:='';
Line:='';
Flags:='';
CollectedIndex := ciNone;
LineNr := 0;
for Cnt := 0 to SL.Count - 1 do
begin
CurLine := Sl.Strings[Cnt];
LineLen := Length(CurLine);
if (LineLen > 0) then
begin
if HasPrefix (sCommentIdentifier,CurLine) then
begin
//Add the Entry collected before this line (not the current line)
AddEntry(LineNr);
LineNr := Cnt + 1;
Identifier:=copy(CurLine,lCommentIdentifier+1,LineLen-lCommentIdentifier);
// 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]:='.';
end
else if TestPrefixStr(ciMsgId) then
begin
end
else if TestPrefixStr(ciMsgStr) then
begin
end
else if TestPrefixStr(ciPrevMsgId) then
begin
end else if HasPrefix(sMsgCtxt, CurLine) then
begin
Context:= Copy(CurLine,lMsgCtxt+1,LineLen - lMsgCtxt - 1);
end
else if HasPrefix(SFlags, CurLine) then
begin
Flags := Copy(CurLine, lFlags + 1, LineLen - lFlags);
end
else if (CurLine[1] = '"') then
begin
if (MsgID='') and HasPrefix(sCharSetIdentifier,CurLine) then
begin
SetCharSet(copy(CurLine,lCharSetIdentifier+1,LineLen-lCharSetIdentifier-3));
{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;
Line := Line + Copy(CurLine,2,LineLen-2);
end
else if HasPrefix(sPrevStr,CurLine) then
begin
Line := Line + Copy(CurLine,lPrevStr + 1,LineLen - lPrevStr - 1);
end
else if CurLine[1] = '#' then
begin
if Comments<>'' then Comments := Comments + LineEnding;
Comments := Comments + CurLine;
end
else
begin
AddEntry(LineNr);
end;
end;//LineLen > 0
end;
//debugln('Last entry:');
//debugln('Identifier = ',Identifier);
//debugln('LineNr = ',DbgS(LineNr));
//debugln('Cnt = ',DbgS(Cnt));
AddEntry(LineNr);
finally
SL.Free;
end;
{$ifdef DebugSimplePoFiles}
T1 := gettickcount;
debugln('T1 = ',dbgs(t1-t0));
debugln('Count = ',DbgS(Count));
{$endif}
end;
procedure TSimplePOFile.Add(const Identifier, OriginalValue, TranslatedValue,
Comments, Context, Flags, PreviousID: string; LineNr: Integer);
var
Item, OItem: TPOFileItem;
OIndex: Integer;
//p: Integer;
begin
if (not FAllEntries) and (TranslatedValue='') then exit;
Item:=TPOFileItem.Create(Identifier,OriginalValue,TranslatedValue);
Item.Comments:=Comments;
Item.Context:=Context;
Item.Flags:=Flags;
Item.PreviousID:=PreviousID;
Item.Tag:=0;
Item.LineNr := LineNr;
FItems.Add(Item);
//debugln('TPOFile.Add %8x Tag=%d Id="%s" Org="%s" Trn="%s"',
// [ptrint(Item),FTag,Identifier,dbgstr(OriginalValue),dbgstr(TranslatedValue)]);
FIdentifierToItem.Add(Identifier,Item);
{
P := Pos('.', Identifier);
if P>0 then
FIdentVarToItem.Add(copy(Identifier, P+1, Length(IDentifier)), Item);
}
//if FIdentifierToItem.Data[UpperCase(Identifier)]=nil then raise Exception.Create('');
OIndex := FOriginalToItem.Find(OriginalValue);
if (OIndex > -1) then
begin
//TPoFileItem(FOriginalToItem.List[OIndex]^.Data).Tag := TPoFileItem(FOriginalToItem.List[OIndex]^.Data).Tag or tgHasDup;
OItem := TPoFileItem(FOriginalToItem.List[OIndex]^.Data);
OItem.Tag := OItem.Tag or tgHasDup;
Item.Tag := Item.Tag or tgHasDup;
end;
FOriginalToItem.Add(OriginalValue,Item);
//if FOriginalToItem.Data[OriginalValue]=nil then raise Exception.Create('');
end;
{
function TSimplePOFile.Translate(const Identifier, OriginalValue: String): String;
var
Item: TPOFileItem;
begin
Item:=TPOFileItem(FIdentifierToItem.Data[Identifier]);
if Item=nil then
Item:=TPOFileItem(FOriginalToItem.Data[OriginalValue]);
if Item<>nil then begin
Result:=Item.Translation;
if Result='' then RaiseGDBException('TPOFile.Translate Inconsistency');
end else
Result:=OriginalValue;
end;
}
procedure TSimplePOFile.Report;
begin
Report(0, Count - 1, True);
end;
procedure TSimplePOFile.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.Identifier);
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.Identifier);
DebugLn('msgid=',Item.Original);
DebugLn('msgstr=', Item.Translation);
DebugLn('Comments=',Item.Comments);
DebugLn;
end;
end;
procedure TSimplePOFile.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.Identifier);
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.Identifier);
Log.Add('msgid='+Item.Original);
Log.Add('msgstr='+ Item.Translation);
Log.Add('Comments='+Item.Comments);
Log.Add('');
end;
end;
procedure TSimplePOFile.CreateHeader;
begin
if FHeader=nil then
FHeader := TPOFileItem.Create('','','');
FHeader.Translation:='Content-Type: text/plain; charset=UTF-8';
FHeader.Comments:='';
end;
{
procedure TSimplePOFile.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;
begin
ClearModuleList;
UntagAll;
// for each string in lrt/rst list check if it's already
// in PO if not add it
Value := '';
Identifier := '';
i := 0;
while i < InputLines.Count do begin
Line := InputLines[i];
n := Length(Line);
if n=0 then
// empty line
else
if SType=stLrt then begin
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
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;
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;
UpdateItem(Identifier, Value);
end;
end; // if p>0 then begin
end;
end;
inc(i);
end;
RemoveUntaggedModules;
end;
}
{
procedure TSimplePOFile.RemoveTaggedItems(aTag: Integer);
var
Item: TPOFileItem;
i: Integer;
begin
// get rid of all entries that have Tag=aTag
for i:=FItems.Count-1 downto 0 do begin
Item := TPOFileItem(FItems[i]);
if Item.Tag<>aTag then
Continue;
FIdentifierToItem.Remove(Item.Identifier);
FOriginalToItem.Remove(Item.Original); // isn't this tricky?
FItems.Delete(i);
Item.Free;
end;
end;
}
function ComparePOItems(Item1, Item2: Pointer): Integer;
begin
result := CompareText(TPOFileItem(Item1).Identifier,
TPOFileItem(Item2).Identifier);
end;
{
procedure TSimplePOFile.SaveToFile(const AFilename: string);
var
OutLst: TStringList;
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 OutLst.Add(AProp+' "'+FHelperList[0]+'"');
end else begin
if AProp<>'' then
OutLst.Add(AProp+' ""');
for i:=0 to FHelperList.Count-1 do begin
s := FHelperList[i];
if AProp<>'' then begin
s := '"' + s + '\n"';
if AProp='#| msgid' then
s := '#| ' + s;
end;
OutLst.Add(s)
end;
end;
end;
procedure WriteItem(Item: TPOFileItem);
begin
WriteLst('',Item.Comments);
if Item.Identifier<>'' then
OutLst.Add('#: '+Item.Identifier);
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;
OutLst := TStringList.Create;
try
// 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]));
OutLst.SaveToFile(UTF8ToSys(AFilename));
finally
OutLst.Free;
end;
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 (P1^<>#0) do begin
Inc(P1); Inc(P2);
Dec(L1); Dec(L2);
if P1^<>P2^ 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;
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;
{
procedure TSimplePOFile.UpdateItem(const Identifier: string; Original: string);
var
Item: TPOFileItem;
AContext,AComment,ATranslation,AFlags,APrevStr: string;
begin
if FHelperList=nil then
FHelperList := TStringList.Create;
// try to find PO entry by identifier
Item:=TPOFileItem(FIdentifierToItem.Data[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('fuzzy', 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
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.Identifier;
// if old item it's already translated use translation
if Item.Translation<>'' then
ATranslation := Item.Translation;
AFlags := Item.Flags;
// if old item was fuzzy, new should be fuzzy too.
if (ATranslation<>'') and (pos('fuzzy', AFlags)<>0) then
APrevStr := Item.PreviousID;
// update identifier list
AContext := Identifier;
end;
// this appear to be a new item
FModified := true;
Add(Identifier, Original, ATranslation, AComment, AContext, AFlags, APrevStr);
end;
}
{
procedure TSimplePOFile.UpdateTranslation(BasePOFile: TSimplePOFile);
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.Identifier, Item.Original);
end;
RemoveTaggedItems(0); // get rid of any item not existing in BasePOFile
end;
}
{
procedure TSimplePOFile.ClearModuleList;
begin
if FModuleList<>nil then
FModuleList.Clear;
end;
}
{
procedure TSimplePOFile.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 TSimplePOFile.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;
}
function TSimplePOFile.FindPoItem(const Identifier: String): TPoFileItem;
begin
Result := TPOFileItem(FIdentifierToItem.Data[Identifier]);
end;
function TSimplePOFile.GetPoItem(Index: Integer): TPoFileItem;
begin
Result := TPoFileItem(FItems.Items[Index]);
end;
function TSimplePOFile.OriginalToItem(Data: String): TPoFileItem;
begin
Result := TPOFileItem(FOriginalToItem.Data[Data]);
end;
{ TPOFileItem }
constructor TPOFileItem.Create(const TheIdentifier, TheOriginal,
TheTranslated: string);
begin
Identifier:=TheIdentifier;
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.