{ *************************************************************************** * * * This source is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This code is distributed in the hope that it will be useful, but * * WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * * General Public License for more details. * * * * A copy of the GNU General Public License is available on the World * * Wide Web at . You can also * * obtain it by writing to the Free Software Foundation, * * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * * * *************************************************************************** Author: Mattias Gaertner Name: updatepofiles - updates po files. Synopsis: updatepofiles filename1.po [filename2.po ... filenameN.po] Description: updatepofiles deletes doubles in the po file and merges new strings into all translated po files (filename1.po.xx) Modifications: Graeme Geldenhuys (2008-03-17): Removed all dependencies to Lazarus units. } program UpdatePoFiles; {$mode objfpc}{$H+} {$ifdef Windows} {$define CaseInsensitiveFilenames} {$endif} uses Classes, SysUtils, AvL_Tree; const UTF8FileHeader = #$ef#$bb#$bf; type TMsgItem = record Comment: string; ID: string; Str: string; end; PMsgItem = ^TMsgItem; function CompareMsgItems(Data1, Data2: pointer): integer; var MsgItem1: PMsgItem; MsgItem2: PMsgItem; begin MsgItem1:=PMsgItem(Data1); MsgItem2:=PMsgItem(Data2); Result:=CompareStr(MsgItem1^.ID,MsgItem2^.ID); end; procedure DisposeMsgTree(var Tree: TAVLTree); var Node: TAVLTreeNode; MsgItem: PMsgItem; begin Node:=Tree.FindLowest; while Node<>nil do begin MsgItem:=PMsgItem(Node.Data); Dispose(MsgItem); Node:=Tree.FindSuccessor(Node); end; Tree.Free; Tree:=nil; end; function GetAllFilesMask: string; begin {$IFDEF WINDOWS} Result:='*.*'; {$ELSE} Result:='*'; {$ENDIF} end; function CompareFilenames(const Filename1, Filename2: string): integer; begin {$IFDEF CaseInsensitiveFilenames} Result:=AnsiCompareText(Filename1, Filename2); {$ELSE} Result:=CompareStr(Filename1, Filename2); {$ENDIF} end; type TPoFile = class public Tree: TAVLTree; Header: TStringList; UTF8Header: string; constructor Create; destructor Destroy; override; end; { TPoFile } constructor TPoFile.Create; begin Tree:=TAVLTree.Create(@CompareMsgItems); Header:=TStringList.Create; end; destructor TPoFile.Destroy; begin DisposeMsgTree(Tree); Header.Free; inherited Destroy; end; //============================================================================== var Files: TStringList; Prefix: string; procedure IncPrefix; begin Prefix:=Prefix+' '; end; procedure DecPrefix; begin Prefix:=LeftStr(Prefix,length(Prefix)-2); end; function ParamsValid: boolean; var i: Integer; Filename: String; Ext: String; Name: string; begin Result:=false; if ParamCount<1 then exit; for i:=1 to ParamCount do begin Filename:=ParamStr(1); if not FileExists(Filename) then begin writeln('ERROR: file not found: ',FileName); exit; end; Ext:=ExtractFileExt(Filename); if (Ext<>'.po') then begin writeln('ERROR: invalid extension: ',Filename); exit; end; Name:=ExtractFileName(Filename); Name:=LeftStr(Name,length(Name)-length(Ext)); if Pos('.',Name)>0 then begin writeln('ERROR: invalid unitname: ',Name); exit; end; if Files=nil then Files:=TStringList.Create; Files.Add(Filename); end; Result:=true; end; function ReadMessageItem(SrcFile: TStringList; var Line: integer): PMsgItem; var s: string; begin New(Result); while Line'') and (s[1]='#') then begin Result^.Comment:=Result^.Comment+copy(s,2,length(s)); end else if (LeftStr(s,7)='msgid "') then begin // read ID Result^.ID:=copy(s,8,length(s)-8); inc(Line); while Line'') and (s[1]='"') then begin Result^.ID:=Result^.ID+#10+copy(s,2,length(s)-2); inc(Line); end else break; end; // read Str if Line'') and (s[1]='"') then begin Result^.Str:=Result^.Str+#10+copy(s,2,length(s)-2); inc(Line); end else break; end; end; end; exit; end; inc(Line); end; end; procedure WriteMessageItem(MsgItem: PMsgItem; DestFile: TStringList); procedure WriteItem(const Prefix: string; Str: string); var s: String; p: Integer; begin s:=Prefix+' "'; p:=1; while (p<=length(Str)) do begin if Str[p]=#10 then begin // a new line s:=s+copy(Str,1,p-1)+'"'; DestFile.Add(s); Str:=copy(Str,p+1,length(Str)); p:=1; // start new line s:='"'; end else inc(p); end; if (Str<>'') or (s<>'"') then begin s:=s+Str+'"'; DestFile.Add(s); end; end; begin if MsgItem^.Comment<>'' then DestFile.Add('#'+MsgItem^.Comment); WriteItem('msgid',MsgItem^.ID); WriteItem('msgstr',MsgItem^.Str); DestFile.Add(''); end; function ReadPoFile(const Filename: string): TPoFile; var SrcFile: TStringList; MsgItem: PMsgItem; Line: Integer; begin Result:=TPoFile.Create; // read source .po file //writeln(Prefix,'Loading ',Filename,' ...'); SrcFile:=TStringList.Create; SrcFile.LoadFromFile(Filename); if (SrcFile.Count>0) and (copy(SrcFile[0],1,3)=UTF8FileHeader) then begin Result.UTF8Header:=copy(SrcFile[0],1,3); SrcFile[0]:=copy(SrcFile[0],4,length(SrcFile[0])); end; Line:=0; while Linenil) then begin Dispose(MsgItem); continue; end; // add message Result.Tree.Add(MsgItem); end; end; SrcFile.Free; end; procedure WritePoFile(PoFile: TPoFile; const Filename: string); var DestFile: TStringList; Node: TAVLTreeNode; MsgItem: PMsgItem; Save: Boolean; OldDestFile: TStringList; begin //writeln(Prefix,'Saving ',Filename,' ...'); DestFile:=TStringList.Create; if (PoFile.Header.Count>0) then begin DestFile.Add('msgid ""'); DestFile.Add('msgstr ""'); DestFile.AddStrings(PoFile.Header); DestFile.Add(''); end; Node:=PoFile.Tree.FindLowest; while Node<>nil do begin MsgItem:=PMsgItem(Node.Data); WriteMessageItem(MsgItem,DestFile); Node:=PoFile.Tree.FindSuccessor(Node); end; if (PoFile.UTF8Header<>'') and (DestFile.Count>0) then DestFile[0]:=PoFile.UTF8Header+DestFile[0]; Save:=true; if FileExists(Filename) then begin OldDestFile:=TStringList.Create; OldDestFile.LoadFromFile(Filename); if OldDestFile.Text=DestFile.Text then Save:=false; OldDestFile.Free; end; if Save then DestFile.SaveToFile(Filename); DestFile.Free; 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 SysUtils.FindFirst(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 SysUtils.FindNext(FileInfo)<>0; end; SysUtils.FindClose(FileInfo); end; procedure MergePoTrees(SrcTree, DestTree: TAVLTree); var SrcNode, DestNode: TAVLTreeNode; SrcMsgItem, DestMsgItem: PMsgItem; OldNode: TAVLTreeNode; begin // add all message items from SrcTree into DestTree SrcNode:=SrcTree.FindLowest; while SrcNode<>nil do begin SrcMsgItem:=PMsgItem(SrcNode.Data); DestNode:=DestTree.FindKey(SrcMsgItem,@CompareMsgItems); if DestNode<>nil then begin // ID already exists -> update comment DestMsgItem:=PMsgItem(DestNode.Data); DestMsgItem^.Comment:=SrcMsgItem^.Comment; end else begin // new ID -> add new message item to DestTree New(DestMsgItem); DestMsgItem^.Comment:=SrcMsgItem^.Comment; DestMsgItem^.ID:=SrcMsgItem^.ID; DestMsgItem^.Str:=SrcMsgItem^.Str; DestTree.Add(DestMsgItem); end; SrcNode:=SrcTree.FindSuccessor(SrcNode); end; // remove all old messages in DestTree DestNode:=DestTree.FindLowest; while DestNode<>nil do begin DestMsgItem:=PMsgItem(DestNode.Data); OldNode:=DestNode; DestNode:=DestTree.FindSuccessor(DestNode); if (DestMsgItem^.ID<>'') and (SrcTree.FindKey(DestMsgItem,@CompareMsgItems)=nil) then begin // unused message -> delete it writeln('Deleting unused message "',DestMsgItem^.ID,'"'); Dispose(DestMsgItem); DestTree.Delete(OldNode); end; end; end; procedure UpdatePoFile(const Filename: string); var SrcFile, DestFile: TPoFile; DestFiles: TStringList; i: Integer; begin writeln('Loading ',Filename,' ...'); SrcFile:=ReadPoFile(Filename); DestFiles:=FindAllTranslatedPoFiles(Filename); IncPrefix; for i:=0 to DestFiles.Count-1 do begin writeln(Prefix,'Updating ',DestFiles[i]); IncPrefix; DestFile:=ReadPoFile(DestFiles[i]); MergePoTrees(SrcFile.Tree,DestFile.Tree); WritePoFile(DestFile,DestFiles[i]); DestFile.Free; DecPrefix; end; DecPrefix; DestFiles.Free; SrcFile.Free; end; procedure UpdateAllPoFiles; var i: Integer; begin for i:=0 to Files.Count-1 do begin UpdatePoFile(Files[i]); end; end; begin Prefix:=''; Files:=nil; if not ParamsValid then begin writeln('Usage: ',ExtractFileName(ParamStr(0)) ,' filename1.po [filename2.po ... filenameN.po]'); exit; end else begin UpdateAllPoFiles; end; Files.Free; end.