{ 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.