BSOne.SFC/Tocsg.Lib/VCL/Other/KDL.Localizer.pas

697 lines
21 KiB
Plaintext

{ Kryvich's Delphi Localizer Class
Copyright (C) 2006 - 2018 Kryvich, Belarusian Linguistic Software team.
}
unit KDL.Localizer;
//{$I NoRTTI}
{$IFDEF UNICODE}
{$WEAKLINKRTTI ON}
{$RTTI EXPLICIT METHODS([]) PROPERTIES([]) FIELDS([])}
{$ENDIF}
interface
uses
Classes, KDL.Detours;
{$IFDEF _HE_}
{$DEFINE VCL}
{$ENDIF}
const
KDL_PLATFORM =
{$IF DEFINED(VCL)} 'VCL'
{$ELSEIF DEFINED(FMX)} 'FMX'
{$ELSEIF DEFINED(NOGUI)} 'NOGUI'
{$ELSE}
{$Message Fatal 'One of the following symbols must be defined: VCL, FMX or NOGUI'}
{$IFEND};
type
// Method of error processing
TErrorProcessing = (
epSilent, // Just skip errors (default) - use for public releases
epMessage, // Show message to an user - use for beta testing
epException, // Raise exception - use while develop and debug
epDebug, // Use DebugOutputString
epErrors // Append all messages to a string list
);
// Translated form properties
TResForm = class
public
Name: string; // Form name
Props: TStringList; // Property names
Values: TStringList; // Translated property values
end;
// Events of Localizer
TBeforeLanguageLoadEvent = procedure(Sender: TObject; const OldLanguageFile,
NewLanguageFile: string) of object;
TAfterLanguageLoadEvent = procedure(Sender: TObject;
const LanguageFile: string) of object;
TFreeLocalizer = class
private
fLanguageFile: string; // Loaded language file
ResForms: array of TResForm; // List of all localized forms
fAutoTranslate: Boolean;
fBeforeLanguageLoadEvent: TBeforeLanguageLoadEvent;
fAfterLanguageLoadEvent: TAfterLanguageLoadEvent;
fErrors: TStrings;
{$IF NOT DEFINED(NOGUI)}
InitInheritedRepl: TFuncReplacement; // InitInheritedComponent replacement
{$IFEND}
// Get Humanize settings of a language file
procedure GetEncoding(sl: TStringList; var Humanize: Boolean;
var HumanizedCR, HumanizedCRLF, HumanizedLF: string);
// Delete old translations in ResForms
procedure ClearResForms;
// Load translations from file
procedure LoadLanguageFile(const aLanguageFile: string);
{$IF NOT DEFINED(NOGUI)}
// Set value PropValue for property PropName in Component RootComp
procedure TranslateProp(RootComp: TComponent; const PropName, PropValue: string);
// Translate component (form) as component of class CompClassType
procedure TranslateAs(Comp: TComponent; const CompClassType: TClass);
{$IFEND}
// Enable/disable autotranslation feature
procedure SetAutoTranslate(aAutoTranslate: Boolean);
// Enable/disable translation of resource strings
procedure EnableResStringer(DoEnable: Boolean);
// Called when error encountered
procedure Error(const Mess: string);
// Get error messages from fErrors
function GetErrors: string;
public
LanguageDir: string; // Directory with language files (optional)
ErrorProcessing: TErrorProcessing;
constructor Create;
destructor Destroy; override;
{$IF NOT DEFINED(NOGUI)}
// Translate component (form)
procedure Translate(Comp: TComponent);
// Translate all forms on Screen
procedure TranslateScreen;
{$IFEND}
// Clear error messages in fErrors
procedure ClearErrors;
// Error messages (set ErrorProcessing to epErrors)
property Errors: string read GetErrors;
// Language file name. Set it to load a new translation
property LanguageFile: string read fLanguageFile write LoadLanguageFile;
// Enable/disable translation of resource strings
property TranslateResourceStrings: Boolean write EnableResStringer;
// Auto translate a form after creating
property AutoTranslate: Boolean read fAutoTranslate write SetAutoTranslate;
// Occurs exactly before loading new language file.
// You can call the silent exception (Abort) to abort the operation
property BeforeLanguageLoad: TBeforeLanguageLoadEvent
read fBeforeLanguageLoadEvent write fBeforeLanguageLoadEvent;
// Occurs exactly after a new language was loaded.
// Do here necessary operations such as calling TranslateScreen
// (if AutoTranslate is disabled) and updating of controls state
property AfterLanguageLoad: TAfterLanguageLoadEvent
read fAfterLanguageLoadEvent write fAfterLanguageLoadEvent;
end;
var
FreeLocalizer: TFreeLocalizer;
resourcestring
rsKdlMark = '*KDL*Mark*';
implementation
uses
Windows, SysUtils, TypInfo, KDL.StringUtils, StrUtils, Tocsg.Trace
{$IF DEFINED(VCL)}
, Vcl.Forms
{$ELSEIF DEFINED(FMX)}
, FMX.Forms, System.UITypes, FMX.DialogService.Sync;
{$ELSEIF DEFINED(NOGUI)}
// No GUI framework used
{$IFEND};
const
LngHeader = '; Kryvich''s Delphi Localizer Language File.';
sNewMark = '(!)';
sDelMark = '(x)';
{$region 'EKdlError'}
type
EKdlError = class (Exception)
constructor Create(AMessage: string);
end;
EKdlSilentError = class (EKdlError)
constructor Create;
end;
constructor EKdlError.Create(AMessage: string);
begin
inherited Create(AMessage);
end;
constructor EKdlSilentError.Create;
begin
inherited Create('');
end;
{$endregion}
{$region 'TResStringer'}
type
TResStringer = class
private
LoadResRepl: TFuncReplacement; // LoadResString replacement
ResStrings: TStringList; // Translated resource strings
fEnabled: Boolean; // Do translations of resource strings
fSelfTestMode: Boolean;
// Get resource string
function GetString(Id: Integer; var s: string): Boolean;
// Set translation status
procedure SetEnabled(aEnabled: Boolean);
public
constructor Create;
destructor Destroy; override;
// Read resource strings from sl into ResStrings
procedure LoadResStrings(sl: TStringList; var i: Integer;
Humanize: Boolean; const HumanizedCR, HumanizedCRLF, HumanizedLF: string);
property Enabled: Boolean read fEnabled write SetEnabled;
end;
var
ResStringer: TResStringer;
function MyLoadResString(ResStringRec: PResStringRec): string;
function GetNotTranslated: string;
begin
ResStringer.Enabled := False;
try
Result := System.LoadResString(ResStringRec);
finally
ResStringer.Enabled := True;
end;
end;
begin
if ResStringRec = nil then
Exit;
if Assigned(ResStringer) and ResStringer.Enabled then begin
if ResStringRec.Identifier >= 64*1024 then
Result := PChar(ResStringRec.Identifier)
else if not ResStringer.GetString(ResStringRec.Identifier, Result) then
if ResStringer.fSelfTestMode then
Result := ''
else
Result := GetNotTranslated;
end else
Result := System.LoadResString(ResStringRec);
end;
{ TResStringer }
constructor TResStringer.Create;
begin
LoadResRepl := TFuncReplacement.Create(@System.LoadResString, @MyLoadResString);
end;
destructor TResStringer.Destroy;
begin
Enabled := False;
FreeAndNil(ResStrings);
LoadResRepl.Free;
inherited;
end;
procedure TResStringer.SetEnabled(aEnabled: Boolean);
begin
LoadResRepl.Replaced := aEnabled;
fEnabled := aEnabled;
end;
procedure TResStringer.LoadResStrings(sl: TStringList; var i: Integer;
Humanize: Boolean; const HumanizedCR, HumanizedCRLF, HumanizedLF: string);
const
KdlMarkStringName = 'KDL_Localizer_rsKdlMark';
var
s, el: string;
id: Integer;
oEnabled: Boolean;
kdlMarkFound: Boolean;
begin
oEnabled := Enabled;
Enabled := False;
if ResStrings <> nil then
ResStrings.Clear
else
ResStrings := TStringList.Create;
kdlMarkFound := False;
while i < sl.Count do begin
s := sl[i];
if (s <> '') and (s[1] <> ';') then begin
if s[1] = '(' then begin
if Copy(s, 1, Length(sDelMark)) = sDelMark then
FreeLocalizer.Error('Obsolete line in language file:'#13#10'"' +
sl[i] + '"'#13#10'You have to delete it!')
else if Copy(s, 1, Length(sNewMark)) = sNewMark then
FreeLocalizer.Error('Untranslated line in language file:'#13#10'"' +
sl[i] + '"'#13#10'You have to translate it!');
end else begin
if s[1] = '[' then
Break;
// 65167_ComConst_SOleError='OLE error %.8x'
SplitBy(s, '_', el);
if not TryStrToInt(el, id) then
FreeLocalizer.Error('Bad resource ID in language file: "' + el + '"');
SplitBy(s, '=', el);
kdlMarkFound := kdlMarkFound or (el = KdlMarkStringName);
s := LngToString(s, Humanize, HumanizedCR, HumanizedCRLF, HumanizedLF,
sLineBreak);
ResStrings.Add(s);
ResStrings.Objects[ResStrings.Count-1] := Pointer(id);
end;
end;
Inc(i);
end;
if not kdlMarkFound then begin
FreeLocalizer.Error('Can''t find the special string ' + KdlMarkStringName
+ ' in the loaded language file. This language file is corrupted.');
ResStrings.Clear;
end else begin
try
fSelfTestMode := True;
Enabled := True;
if rsKdlMark <> '*KDL*Mark*' then begin
ResStrings.Clear;
FreeLocalizer.Error(
'Strings section in the loaded language file is outdated.'#13#10
+ 'Messages of this application will not be translated.');
end;
finally
Enabled := False;
fSelfTestMode := False;
end;
end;
Enabled := oEnabled and (ResStrings.Count > 0);
end;
function TResStringer.GetString(Id: Integer; var s: string): Boolean;
var
i0, i1, i2: Integer;
begin
if ResStrings = nil then
Result := False
else begin
i0 := 0;
i2 := ResStrings.Count-1;
while i0 < i2 do begin
i1 := (i0+i2) div 2;
if Id > Integer(ResStrings.Objects[i1]) then
i0 := i1+1
else
i2 := i1;
end;
Result := (Id = Integer(ResStrings.Objects[i0]));
if Result then
s := ResStrings[i0];
end;
end;
{$endregion}
{$region 'TFreeLocalizer'}
procedure TFreeLocalizer.ClearErrors;
begin
fErrors.Clear;
end;
procedure TFreeLocalizer.ClearResForms;
var
i: Integer;
begin
for i := 0 to Length(ResForms) - 1 do begin
ResForms[i].Props.Free;
ResForms[i].Values.Free;
ResForms[i].Free;
end;
SetLength(ResForms, 0);
end;
constructor TFreeLocalizer.Create;
begin
fErrors := TStringList.Create;
ResStringer := TResStringer.Create;
ResStringer.Enabled := True;
end;
destructor TFreeLocalizer.Destroy;
begin
SetAutoTranslate(False);
ResStringer.Free;
ClearResForms;
fErrors.Free;
inherited;
end;
procedure TFreeLocalizer.Error(const Mess: string);
begin
case ErrorProcessing of
epMessage:
{$IF DEFINED(VCL)}
Application.MessageBox(pChar(Mess), 'K.D.L. Error',
MB_ICONERROR+MB_OK+MB_DEFBUTTON1+MB_APPLMODAL);
{$ELSEIF DEFINED(FMX)}
TDialogServiceSync.MessageDialog(Mess, TMsgDlgType.mtError,
[TMsgDlgBtn.mbOK], TMsgDlgBtn.mbOK, 0);
{$ELSE}
Writeln('K.D.L. Error: ', Mess);
{$IFEND}
epException: raise EKdlError.Create(Mess);
epDebug: OutputDebugString(pChar(Mess));
epErrors: fErrors.Append(Mess);
end;
end;
procedure TFreeLocalizer.GetEncoding(sl: TStringList; var Humanize: Boolean;
var HumanizedCR, HumanizedCRLF, HumanizedLF: string);
var
i: Integer;
s: string;
begin
Humanize := False;
HumanizedCR := defHumanizeDivider;
HumanizedCRLF := defHumanizeDivider;
HumanizedLF := defHumanizeDivider;
i := sl.IndexOfName('Humanize');
if i >= 0 then begin
Humanize := (sl.ValueFromIndex[i] = '1');
i := sl.IndexOfName('HumanizeDivider');
if i >= 0 then begin // For backward compatibility
s := sl.ValueFromIndex[i];
HumanizedCR := s;
HumanizedCRLF := HumanizedCR;
end;
i := sl.IndexOfName('HumanizedCR');
if i >= 0 then begin
s := sl.ValueFromIndex[i];
HumanizedCR := s;
end;
i := sl.IndexOfName('HumanizedCRLF');
if i >= 0 then begin
s := sl.ValueFromIndex[i];
HumanizedCRLF := s;
end;
i := sl.IndexOfName('HumanizedLF');
if i >= 0 then begin
s := sl.ValueFromIndex[i];
HumanizedLF := s;
end;
end;
end;
function TFreeLocalizer.GetErrors: string;
begin
Result := fErrors.Text;
end;
procedure TFreeLocalizer.LoadLanguageFile(const aLanguageFile: string);
const
LngExt = '.dat'; // '.lng';
var
FullLangFile: string;
sl: TStringList;
i, iResForms: Integer;
s, el: string;
Humanize: Boolean;
HumanizedCR, HumanizedCRLF, HumanizedLF: string;
begin
if Assigned(fBeforeLanguageLoadEvent) then
fBeforeLanguageLoadEvent(Self, LanguageFile, aLanguageFile);
try
ClearResForms;
// Build Full name of language file
FullLangFile := LanguageDir;
if (FullLangFile <> '')
and not CharInSet(FullLangFile[Length(FullLangFile)], ['/', '\'])
then
FullLangFile := FullLangFile + '\';
FullLangFile := FullLangFile + aLanguageFile;
if not AnsiEndsText(LngExt, FullLangFile) then
FullLangFile := FullLangFile + LngExt;
sl := TStringList.Create;
try
sl.LoadFromFile(FullLangFile, TEncoding.UTF8);
// 헤드 체크 삭제 22_0810 09:49:39 kku
// if (sl.Count <= 0)
// or (sl[0] <> LngHeader)
// then begin
// Error('Bad signature in language file "' + FullLangFile + '"');
// Exit;
// end;
if sl.Count <= 0 then
exit;
GetEncoding(sl, Humanize, HumanizedCR, HumanizedCRLF, HumanizedLF);
iResForms := -1;
i := 1;
while i < sl.Count do begin
s := sl[i];
if (s <> '') and (s[1] <> ';') then begin
if s[1] = '[' then begin
if UpperCase(s) = '[RESOURCESTRINGS]' then begin
Inc(i);
ResStringer.LoadResStrings(sl, i, Humanize, HumanizedCR,
HumanizedCRLF, HumanizedLF);
Continue;
end else begin
if Copy(s, 2, Length(sDelMark)) = sDelMark then begin
Error('Deleted component in language file:'#13#10'"' + sl[i] +
'"'#13#10'You have to remove it!');
Exit;
end;
Inc(iResForms);
SetLength(ResForms, iResForms+1);
ResForms[iResForms] := TResForm.Create;
ResForms[iResForms].Name := Copy(s, 2, Length(s)-2);
ResForms[iResForms].Props := TStringList.Create;
ResForms[iResForms].Values := TStringList.Create;
end;
end else if iResForms >= 0 then begin
if s[1] = '(' then begin
if Copy(s, 1, Length(sDelMark)) = sDelMark then
Error('Obsolete line in language file:'#13#10'"' + sl[i] +
'"'#13#10'You have to remove it!')
else if Copy(s, 1, Length(sNewMark)) = sNewMark then
Error('Untranslated line in language file:'#13#10'"' + sl[i] +
'"'#13#10'You have to translate it!');
end else begin
SplitBy(s, '=', el);
s := LngToString(s, Humanize, HumanizedCR, HumanizedCRLF,
HumanizedLF, #13);
ResForms[iResForms].Values.Add(s);
// btnNewForm.Caption{1} -> drop version #
SplitBy(el, '{', s);
if s = '' then begin
Error('Bad line in language file: "' + sl[i] + '"');
Exit;
end;
ResForms[iResForms].Props.Add(s);
end;
end;
end;
Inc(i);
end;
finally
sl.Free;
end;
fLanguageFile := aLanguageFile;
{$IF NOT DEFINED(NOGUI)}
if AutoTranslate then
TranslateScreen;
{$IFEND}
if Assigned(fAfterLanguageLoadEvent) then
fAfterLanguageLoadEvent(Self, fLanguageFile);
except
on E: Exception do
begin
// 여기서 왜 에러가 나는지 모르겠다...
// 디버깅 모드에서는 재현이 안되고 릴리즈 시에만 발생하는데...
// 정상동작은 함 그래서 오류 표시만 숨기고 로그로만 남기도록 기능 보완 22_1213 15:55:07 kku
TTgTrace.T('Error while loading language file "%s"', [FullLangFile]);
// Error('Error while loading language file "' + FullLangFile + '"'#13#10
// + E.Message);
end;
end;
end;
{$IF NOT DEFINED(NOGUI)}
procedure TFreeLocalizer.TranslateAs(Comp: TComponent;
const CompClassType: TClass);
var
ResForm: TResForm;
ParentClassType: TClass;
i: Integer;
begin
// Whether the component's ancestor can contain localizable controls?
ParentClassType := CompClassType.ClassParent;
if (ParentClassType <> TForm)
and (ParentClassType <> TDataModule)
and (ParentClassType <> TObject)
then
TranslateAs(Comp, ParentClassType)
else begin
// Translate nested frames
for i := 0 to Comp.ComponentCount - 1 do
if Comp.Components[i] is TFrame then
FreeLocalizer.Translate(Comp.Components[i]);
end;
ResForm := Nil;
for i := 0 to Length(ResForms)-1 do
if CompClassType.ClassName = ResForms[i].Name then begin
ResForm := ResForms[i];
Break;
end;
if ResForm = Nil then Exit; // This component not translated
for i := 0 to ResForm.Props.Count - 1 do
TranslateProp(Comp, ResForm.Props[i], ResForm.Values[i]);
end;
procedure TFreeLocalizer.Translate(Comp: TComponent);
begin
TranslateAs(Comp, Comp.ClassType);
end;
procedure TFreeLocalizer.TranslateProp(RootComp: TComponent; const PropName,
PropValue: string);
procedure SetStringsProp(st: TStrings);
var
i: Integer;
s, el: string;
begin
s := PropValue;
i := 0;
st.BeginUpdate;
try
while s <> '' do begin
SplitBy(s, ListDivider, el);
if i < st.Count then
st[i] := el
else
st.Add(el);
Inc(i);
end;
while st.Count > i do
st.Delete(st.Count-1);
finally
st.EndUpdate;
end;
end;
procedure SetProp(Obj: TObject; const pName: string);
var
PropInfo: PPropInfo;
begin
if Obj is TStrings then
SetStringsProp(Obj as TStrings)
else begin
PropInfo := GetPropInfo(Obj.ClassInfo, pName);
if PropInfo <> Nil then // Property exists
SetPropValue(Obj, PropInfo, PropValue)
else
raise EKdlSilentError.Create;
end;
end;
label
CheckComp, CheckClass;
var
s, el: string;
Comp, cmp, OwnerComp: TComponent;
obj: TObject;
PropInfo: PPropInfo;
i: Integer;
begin
try
OwnerComp := RootComp;
Comp := RootComp;
s := PropName;
repeat
SplitBy(s, '.', el);
CheckComp:
if s = '' then begin // el is property name
SetProp(Comp, el);
Exit;
end;
cmp := Comp.FindComponent(el);
if cmp = Nil then
Break;
Comp := cmp;
if Comp is TFrame then
OwnerComp := Comp;
until False;
// TVirtualStringTree에서 번역 리소스 추출 시 컬럼의 경우 Header 정보까지 가져오지 않는다.
// 그래서 아래처럼 처리함 22_1212 16:23:12 kku
if (Comp <> nil) and (Comp.ClassName = 'TVirtualStringTree') and
(el = 'Columns') then
begin
el := 'Header';
s := 'Columns.' + s;
end;
// Check for nested classes
obj := Comp;
while Obj is TPersistent do begin
PropInfo := GetPropInfo(obj.ClassInfo, el);
if (PropInfo = Nil) or (PropInfo.PropType^.Kind <> tkClass) then
Break; // Such class property not exists
obj := Pointer(NativeUInt(GetPropValue(Obj, PropInfo)));
CheckClass:
SplitBy(s, '.', el);
if s = '' then begin // el is property name
SetProp(obj, el);
Exit;
end;
if Obj is TCollection then
Break;
end;
// Check for nested TCollection
if (obj is TCollection)
and (Length(el) >= 3)
and (el[1] = '(')
and (el[Length(el)] = ')')
and TryStrToInt(Copy(el, 2, Length(el)-2), i)
then begin
// el = '(0)' s = ...rest of nested classes and properties
obj := (obj as TCollection).Items[i];
goto CheckClass;
end;
// Try to find out el among components of OwnerComp
if Comp <> OwnerComp then begin
Comp := OwnerComp;
goto CheckComp;
end;
// yet untranslated...
raise EKdlSilentError.Create;
except
on E: EKdlSilentError do
begin
// 사라진 컨트롤 무시 22_1213 14:20:55 kku
// s := 'Unknown property "%s" found in component "%s".'#13#10
// + 'Remove it from language file';
// Error(Format(s, [PropName, RootComp.Name]));
end;
on E: Exception do
begin
s := 'Translation error of property "%s" in component "%s"'#13#10
+ E.Message;
Error(Format(s, [PropName, RootComp.Name]));
end;
end;
end;
procedure TFreeLocalizer.TranslateScreen;
var
i: Integer;
begin
for i := 0 to Screen.FormCount - 1 do Translate(Screen.Forms[i]);
end;
function MyInitInheritedComponent(Instance: TComponent;
RootAncestor: TClass): Boolean;
begin
FreeLocalizer.InitInheritedRepl.Replaced := False;
try
Result := InitInheritedComponent(Instance, RootAncestor);
FreeLocalizer.Translate(Instance);
finally
FreeLocalizer.InitInheritedRepl.Replaced := True;
end;
end;
procedure TFreeLocalizer.SetAutoTranslate(aAutoTranslate: Boolean);
begin
if aAutoTranslate = fAutoTranslate then
Exit;
if aAutoTranslate then begin
InitInheritedRepl := TFuncReplacement.Create(
@Classes.InitInheritedComponent,
@MyInitInheritedComponent);
InitInheritedRepl.Replaced := True;
end else begin
InitInheritedRepl.Free;
end;
fAutoTranslate := aAutoTranslate;
end;
{$ELSE}
procedure TFreeLocalizer.SetAutoTranslate(aAutoTranslate: Boolean);
begin
// 이거 선언 만으로 리소스 추출 가능하도록 추가 22_0810 16:13:34 kku
fAutoTranslate := False; // Auto translation is for GUI only
end;
{$IFEND}
procedure TFreeLocalizer.EnableResStringer(DoEnable: Boolean);
begin
ResStringer.Enabled := DoEnable;
end;
{$endregion}
Initialization
FreeLocalizer := TFreeLocalizer.Create;
Finalization
FreeLocalizer.Free;
end.