2599 lines
95 KiB
ObjectPascal
2599 lines
95 KiB
ObjectPascal
/// internationalization (i18n) routines and classes for mORMot
|
||
// - this unit is a part of the freeware Synopse mORMot framework,
|
||
// licensed under a MPL/GPL/LGPL tri-license; version 1.18
|
||
unit mORMoti18n;
|
||
|
||
(*
|
||
This file is part of Synopse mORMot framework.
|
||
|
||
Synopse mORMot framework. Copyright (C) 2022 Arnaud Bouchez
|
||
Synopse Informatique - https://synopse.info
|
||
|
||
*** BEGIN LICENSE BLOCK *****
|
||
Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
||
|
||
The contents of this file are subject to the Mozilla Public License Version
|
||
1.1 (the "License"); you may not use this file except in compliance with
|
||
the License. You may obtain a copy of the License at
|
||
http://www.mozilla.org/MPL
|
||
|
||
Software distributed under the License is distributed on an "AS IS" basis,
|
||
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||
for the specific language governing rights and limitations under the License.
|
||
|
||
The Original Code is Synopse mORMot framework.
|
||
|
||
The Initial Developer of the Original Code is Arnaud Bouchez.
|
||
|
||
Portions created by the Initial Developer are Copyright (C) 2022
|
||
the Initial Developer. All Rights Reserved.
|
||
|
||
Contributor(s):
|
||
- lagodny
|
||
|
||
Alternatively, the contents of this file may be used under the terms of
|
||
either the GNU General Public License Version 2 or later (the "GPL"), or
|
||
the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
||
in which case the provisions of the GPL or the LGPL are applicable instead
|
||
of those above. If you wish to allow use of your version of this file only
|
||
under the terms of either the GPL or the LGPL, and not to allow others to
|
||
use your version of this file under the terms of the MPL, indicate your
|
||
decision by deleting the provisions above and replace them with the notice
|
||
and other provisions required by the GPL or the LGPL. If you do not delete
|
||
the provisions above, a recipient may use your version of this file under
|
||
the terms of any one of the MPL, the GPL or the LGPL.
|
||
|
||
***** END LICENSE BLOCK *****
|
||
|
||
|
||
i18n routines for the Synopse mORMot framework
|
||
************************************************
|
||
|
||
- works internaly with the string type, that is AnsiString with code pages
|
||
and charsets for compiler versions earlier than Delphi 2009, and
|
||
UnicodeString since Delphi 2009 and up -> so it's 100% VCL compatible
|
||
- can load language definition files encoded in Unicode or UTF8
|
||
- auto-call SetThreadLocale() for full application i18n
|
||
- update default locale settings values (date,currency..)
|
||
- force english locale settings on non english system (consistent with UI)
|
||
- handle multiple charsets (standard or custom VCL components compatible)
|
||
- resourcestring on-the-fly translation
|
||
- resourcestring access using fast cache, even without translation; use this
|
||
feature already included in our custom System.pas and in this unit
|
||
(if the ENHANCEDRTL conditional is not defined - see above)
|
||
- CR/LF consistent replacement (#13->'|', #10->'~')
|
||
|
||
uses ini-compatible plain text *.msg messages language file format:
|
||
- Ansi, UTF8 or Unicode .msg files are translated into appropriate CharSet
|
||
- messages translations before first [FormName] section of .msg file
|
||
used for resourcestring and _()
|
||
- forms translations in [FormName] sections of .msg file:
|
||
Label1.Caption=First &label
|
||
Edit.Hint=Enter some text here
|
||
Label1.Hint=& -> taken from Label1.Caption, without the & = 'First label'
|
||
BtnOK.Caption=_78124567 -> taken from messages: 78124567=OK
|
||
BtnTwo.Caption=%MainForm.LabelTwo.Caption -> taken from another translation
|
||
- global language parameters can be saved in [.Params.] section
|
||
- standard Dialogs messages (Ok,Cancel..) are also translated (resourcestring)
|
||
- very fast, and thread safe
|
||
|
||
Two way of use:
|
||
|
||
1. manual translation of every form
|
||
+ can change languages on the fly (no need to restart app)
|
||
- must modify code
|
||
. use SetCurrentLanguage() to set Language object and Delphi locale settings
|
||
. add FormTranslate([MainForm, FormTwo, FormAbout]) in TMainForm.FormShow
|
||
-> this forms will be translated again by any SetCurrentLanguage() call
|
||
warning: none of this form must be free after it - use FormTranslateOne()
|
||
. temporary forms must call FormTranslateOne(self) in their FormShow event
|
||
|
||
2. TCustomForm.OnCreate hook
|
||
+ no code modification
|
||
- can't change languages on the fly (need to restart app) and the language
|
||
must be set in registry (because must be available before any form is
|
||
created)
|
||
. just define USEFORMCREATEHOOK
|
||
. autocall FormTranslateOne() just before an OnCreate handler would be called
|
||
. translate also TFrame
|
||
. initialization.LangInit will call SetCurrentLanguage() once at startup:
|
||
RegisterIO.Reg: CreateKey('User prefs') + WriteString('Language','FR')
|
||
no Reg specified -> will use Win32 user locale
|
||
|
||
Revision history:
|
||
0.1.0 Initial code maintenance
|
||
0.2.0 unit refactoring:
|
||
- new enhanced System.pas now contains a global LoadResStringTranslate()
|
||
procedure, and allow resourcestring caching;
|
||
- new SysUtils uses NormToUpper[] for Ansi*() functions ->
|
||
i18nCompareStr/Text() is to be used instead
|
||
0.2.1 bug found in i18nInnerCompareText()
|
||
1.0 First public release of the Synopse SQLite3 Framework
|
||
|
||
Version 1.1 - 14 January 2009:
|
||
- allow to get rid of our Enhanced Runtime Library dependency if not available
|
||
(e.g. for FPC or on cross-platform, or on Delphi version newer than Delphi 7)
|
||
- attempt to reach Delphi 2009 and up compilation (string=UnicodeString):
|
||
the UNICODE conditional will adapt the framework to these compilers
|
||
(you shouldn't have to change any conditinal define below
|
||
- generic string type is now used for all i18n of text: in Delphi 2009 and up,
|
||
it will be an UnicodeString, but with earlier version of Delphi,
|
||
string is an AnsiString with the codepage of the current selected language
|
||
- attempt to reach Free Pascal Compiler 2.4.0 compatibility
|
||
- LoadResStringTranslate() and resourcestring caching are defined in the
|
||
SQLite3Commons unit, if our Enhanced Run Time Library (or LVCL) is not used
|
||
|
||
Version 1.2 - 18 January 2010
|
||
- Delphi 2009 testing and code proofing
|
||
|
||
Version 1.3 - January 22, 2010
|
||
- some small fixes (e.g. i18nInnerCompareStr) and multi-compiler enhancements
|
||
- compiler conditional renamed ENHANCEDRTL instead of ENHANCEDTRTL
|
||
|
||
Version 1.4 - February 8, 2010
|
||
- whole Synopse SQLite3 database framework released under the GNU Lesser
|
||
General Public License version 3, instead of generic "Public Domain"
|
||
|
||
Version 1.5 - February 14, 2010
|
||
- bug on compilation without our enhanced RTL
|
||
- .PO import/export features (to use an external translation tool)
|
||
|
||
Version 1.9
|
||
- new TLanguageFile.BooleanToString method, returning 'No' or 'Yes'
|
||
- new TLanguageFile.PropToString method to convert a TSQLRecord published
|
||
property value into ready to be displayed text
|
||
|
||
Version 1.13
|
||
- fix URW699 issue when compiling with Delphi 6
|
||
|
||
Version 1.15
|
||
- compatibility with Delphi XE2
|
||
- fix endless recursion loop in ExtractAllResources for nested classes
|
||
- several changes in ExtractAllResources implementation
|
||
- handle TModTime published property / sftModTime SQL field
|
||
- handle TCreateTime published property / sftCreateTime SQL field
|
||
|
||
Version 1.16
|
||
- fixed some compilation warnings with Delphi XE and XE2
|
||
- uses new generic TSynAnsiConvert classes for code page process: since
|
||
TLanguageFile.Create() will set CurrentAnsiConvert global instance,
|
||
applications should use CurrentAnsiConvert instead of previous
|
||
TLanguageFile methods
|
||
- now Iso2S() - i.e. overridden i18nDateText global function pointer - will
|
||
handle a date-only or time-only supplied value as expected
|
||
|
||
Version 1.17
|
||
- some refactoring about process in-memory code patching
|
||
|
||
Version 1.18
|
||
- renamed SQLite3i18n.pas to mORMoti18n.pas
|
||
- BREAKING CHANGE: changed '<27>' into '~' character, for better ASCII support
|
||
in text file - ALL EXISTING .MSG FILES SHALL BE MODIFIED IN CONSEQUENCE
|
||
(by an automated search/replace in your favorite text editor)
|
||
- introducing TSQLPropInfo* classes to decouple ORM definitions from RTTI
|
||
- fixed EXTRACTALLRESOURCES process for multi-platform Delphi versions
|
||
- fixed Win64 compilation and execution issues (Delphi XE2+)
|
||
- fixed Unicode issue in function i18nLanguageToRegistry()
|
||
- added aForceEnglishIfNoMsgFile optional parameter for SetCurrentLanguage()
|
||
|
||
*)
|
||
|
||
{$I Synopse.inc} // define HASINLINE CPU32 CPU64 OWNNORMTOUPPER
|
||
|
||
interface
|
||
|
||
{.$D-,L-}
|
||
|
||
{.$define EXTRACTALLRESOURCES}
|
||
// must be set globally for the whole application
|
||
|
||
{.$define ENHANCEDRTL}
|
||
{ define this if you DID install our Enhanced Runtime library: it has already
|
||
hacked the "legacy" LoadResString() procedure and added a LoadResStringTranslate()
|
||
- it will be unset automaticaly (see below) for Delphi 2009 and up, since
|
||
no version of our Enhanced RTL exists for these compilers
|
||
- this conditional must be defined in both mORMot.pas and mORmoti18n.pas units,
|
||
or (even better) globally in the Project options }
|
||
|
||
{.$define USESHARP}
|
||
// if defined, $$,$$$,$$$ are replaced with some globals in _()
|
||
|
||
{$ifndef NOI18N}
|
||
// with this global define, you can use the unit procs, without the UI i18n
|
||
{$define USEFORMCREATEHOOK}
|
||
// if defined, all forms will be auto-translated, even 3rd party ones
|
||
// just before an OnCreate handler would be called
|
||
{$endif}
|
||
|
||
{$ifdef LVCL}
|
||
// the LVCL don't have TForm.DoCreate and such
|
||
// -> it's easier to explicitely change the captions from code in LVCL
|
||
{$undef USEFORMCREATEHOOK}
|
||
{$endif}
|
||
|
||
uses
|
||
Windows,
|
||
SysUtils,
|
||
Classes,
|
||
{$ifdef USEFORMCREATEHOOK}
|
||
{$ifndef LVCL}
|
||
Menus,
|
||
{$endif}
|
||
{$endif USEFORMCREATEHOOK}
|
||
StdCtrls,
|
||
Forms,
|
||
SynCommons, // some basic types and functions
|
||
mORmot; // need extended RTTI information
|
||
|
||
|
||
{$ifdef UNICODE}
|
||
{$undef ENHANCEDRTL} // no version of our Enhanced RTL exists for Delphi 2009 and up
|
||
{$endif}
|
||
|
||
{$ifdef LVCL}
|
||
// LVCL system.pas doesn't implement LoadResStringTranslate() and won't need it
|
||
{$define ENHANCEDRTL}
|
||
{$endif}
|
||
|
||
|
||
type
|
||
/// languages handled by this mORMoti18n unit
|
||
// - include all languages known by WinXP SP2 without some unicode-only very
|
||
// rare languages; total count is 60
|
||
// - some languages (Japanase, Chinese, Arabic) may need specific language
|
||
// pack installed on western/latin version of windows
|
||
// - lngEnglish is the default language of the executable, used as reference
|
||
// for all other translation, and included into executable (no EN.msg file
|
||
// will never be loaded)
|
||
TLanguages = (
|
||
lngHebrew, lngGreek, lngLatin, lngDari, lngBosnian, lngCatalan,
|
||
lngCorsican, lngCzech, lngCoptic, lngSlavic, lngWelsh, lngDanish,
|
||
lngGerman, lngArabic, lngEnglish, lngSpanish, lngFarsi, lngFinnish,
|
||
lngFrench, lngIrish, lngGaelic, lngAramaic, lngCroatian, lngHungarian,
|
||
lngArmenian, lngIndonesian, lngInterlingue, lngIcelandic, lngItalian,
|
||
lngJapanese, lngKorean, lngTibetan, lngLituanian, lngMalgash, lngNorwegian,
|
||
lngOccitan, lngPortuguese, lngPolish, lngRomanian, lngRussian, lngSanskrit,
|
||
lngSlovak, lngSlovenian, lngAlbanian, lngSerbian, lngSwedish, lngSyriac,
|
||
lngTurkish, lngTahitian, lngUkrainian, lngVietnamese, lngChinese, lngDutch,
|
||
lngThai, lngBulgarian, lngBelarusian, lngEstonian, lngLatvian, lngMacedonian,
|
||
lngPashtol );
|
||
|
||
const
|
||
/// value stored into a TLanguages enumerate to mark no language selected yet
|
||
LANGUAGE_NONE = TLanguages(255);
|
||
|
||
/// ISO 639-1 compatible abbreviations (not to be translated):
|
||
LanguageAbr: packed array[TLanguages] of RawByteString { 4bytes-aligned }
|
||
= ('he','gr','la','ad','bs','ca','co','cs','cp','cu','cy','da','de','ar',
|
||
'en','es','fa','fi','fr','ga','gd','am','hr','hu','hy','id','ie','is',
|
||
'it','ja','ko','bo','lt','mg','no','oc','pt','pl','ro','ru','sa','sk',
|
||
'sl','sq','sr','sv','sy','tr','ty','uk','vi','zh','nl', { end of Ictus3 values }
|
||
'th','bg','be','et','lv','mk','ap');
|
||
|
||
/// to sort in alphabetic order : LanguageAbr[TLanguages(LanguageAlpha[lng])]
|
||
// - recreate these table with ModifiedLanguageAbr if LanguageAbr[] changed
|
||
LanguageAlpha: packed array[TLanguages] of byte =
|
||
(3, 21, 59, 13, 55, 54, 31, 4, 5, 6, 8, 7, 9, 10, 11, 12, 14, 15, 56, 16, 17,
|
||
18, 19, 20, 1, 0, 22, 23, 24, 25, 26, 27, 28, 29, 30, 2, 32, 57, 33, 58, 52,
|
||
34, 35, 37, 36, 38, 39, 40, 41, 42, 43, 44, 45, 46, 53, 47, 48, 49, 50, 51);
|
||
|
||
/// US English Windows LCID, i.e. standard international settings
|
||
LCID_US = $0409;
|
||
|
||
{$ifdef WITHUXTHEME}
|
||
var
|
||
/// international settings from US English $0409
|
||
// - useful in any software, if you want to save some content
|
||
// with the default english encoding (e.g. floating point values with '.')
|
||
SettingsUS: TFormatSettings;
|
||
{$endif}
|
||
|
||
var
|
||
/// true if this program is running on Windows Vista (tm)
|
||
// - used to customize on the fly any TTreeView component, to meet Vista
|
||
// and Seven expectations
|
||
isVista: boolean = false;
|
||
|
||
type
|
||
/// a common record to identify a language
|
||
{$ifdef USERECORDWITHMETHODS}TLanguage = record
|
||
{$else}TLanguage = object{$endif}
|
||
public
|
||
/// as in LanguageAbr[index], LANGUAGE_NONE before first SetLanguageLocal()
|
||
Index: TLanguages;
|
||
/// the corresponding Char Set
|
||
CharSet: integer;
|
||
/// the corresponding Code Page
|
||
CodePage: cardinal;
|
||
/// the corresponding Windows LCID
|
||
LCID: cardinal;
|
||
/// initializes all TLanguage object fields for a specific language
|
||
procedure Fill(Language: TLanguages);
|
||
/// returns two-chars long language abreviation ('HE' e.g.)
|
||
function Abr: RawByteString;
|
||
/// returns fully qualified language name ('Hebrew' e.g.),
|
||
// using current UI language
|
||
// - return "string" type, i.e. UnicodeString for Delphi 2009 and up
|
||
function Name: string;
|
||
end;
|
||
|
||
var
|
||
/// the global Language used by the User Interface,
|
||
// as updated by the last SetCurrentLanguage() call
|
||
CurrentLanguage: TLanguage = (
|
||
Index: LANGUAGE_NONE;
|
||
CharSet: DEFAULT_CHARSET;
|
||
CodePage: CODEPAGE_US;
|
||
LCID: LCID_US
|
||
);
|
||
|
||
{$ifndef NOI18N}
|
||
{$ifdef USEFORMCREATEHOOK} // language is read from registry once at startup:
|
||
const
|
||
/// the sub-entry used to store the i18n settings in the registry;
|
||
// change this value to your company's name, with a trailing backslash
|
||
// ('WorldCompany\' e.g.). the key is
|
||
// HKEY_CURRENT_USER\Software\[RegistryCompanyName]i18n\programname
|
||
RegistryCompanyName = '';
|
||
|
||
/// add strings items, for all available languages on disk
|
||
// - it will search in MsgPath for all *.msg available
|
||
// - if MsgPath is not set, the current executable directory will be used for searching
|
||
// - new items are added to List: Strings[] will contain a caption text, ready
|
||
// to be displayed, and PtrInt(Objects[]) will be the corresponding language ID
|
||
// - return the current language index in List.Items[]
|
||
function i18nAddLanguageItems(MsgPath: TFileName; List: TStrings): integer;
|
||
|
||
{$ifndef LVCL}
|
||
/// add sub-menu items to the Menu, for all available languages on disk
|
||
// - uses internaly i18nAddLanguageItems() function above
|
||
// - current language is checked
|
||
// - all created MenuItem.OnClick event will launch Language.LanguageClick to
|
||
// change the current language in the registry
|
||
procedure i18nAddLanguageMenu(const MsgPath: TFileName; Menu: TMenuItem);
|
||
|
||
/// add combo-box items, for all available languages on disk
|
||
// - uses internaly i18nAddLanguageItems() function above
|
||
// - current language is selected by default
|
||
// - the OnClick event will launch Language.LanguageClick to
|
||
// change the current language in the registry
|
||
procedure i18nAddLanguageCombo(const MsgPath: TFileName; Combo: TComboBox);
|
||
{$endif}
|
||
|
||
/// save the default language to the registry
|
||
// - language will be changed at next startup
|
||
// - return a message ready to be displayed on the screen
|
||
// - return "string" type, i.e. UnicodeString for Delphi 2009 and up
|
||
function i18nLanguageToRegistry(const Language: TLanguages): string;
|
||
|
||
/// get the default language from the registry
|
||
function i18nRegistryToLanguage: TLanguages;
|
||
|
||
resourcestring
|
||
/// this message will be displayed on the screen when the user change the
|
||
// current language, together with its english version
|
||
SHaveToRestart = 'You have to restart the application to apply these language changes.';
|
||
|
||
{$else} { only called once in Initialization.LangInit: }
|
||
|
||
/// resets all translation and locale-specific variables via SetThreadLocale()
|
||
// - by default, if the supplied language does not have a corrrespondig .msg
|
||
// local file, it will fallback to lngEnlish for the whole application
|
||
// - you may set aForceEnglishIfNoMsgFile=false to change the application
|
||
// localization code, even if there is no matching .msg file
|
||
procedure SetCurrentLanguage(aLanguage: TLanguages; aForceEnglishIfNoMsgFile: boolean=true); overload;
|
||
|
||
/// resets all translation and locale-specific variables via SetThreadLocale()
|
||
// - by default, if the supplied language does not have a corrrespondig .msg
|
||
// local file, it will fallback to lngEnlish for the whole application
|
||
// - you may set aForceEnglishIfNoMsgFile=false to change the application
|
||
// localization code, even if there is no matching .msg file
|
||
procedure SetCurrentLanguage(const value: RawUTF8; aForceEnglishIfNoMsgFile: boolean=true); overload;
|
||
{$endif}
|
||
{$endif}
|
||
|
||
/// Return the language text, ready to be displayed (after translation if
|
||
// necessary)
|
||
// - e.g. LanguageName(lngEnglish)='English'
|
||
// - return "string" type, i.e. UnicodeString for Delphi 2009 and up
|
||
function LanguageName(aLanguage: TLanguages): string;
|
||
|
||
/// LanguageAbrToIndex('GR')=1, e.g.
|
||
// - return LANGUAGE_NONE if not found
|
||
function LanguageAbrToIndex(const value: RawUTF8): TLanguages; overload;
|
||
|
||
/// LanguageAbrToIndex('GR')=1, e.g.
|
||
// - return LANGUAGE_NONE if not found
|
||
function LanguageAbrToIndex(p: pAnsiChar): TLanguages; overload;
|
||
|
||
/// convert a i18n language index into a Windows LCID
|
||
function LanguageToLCID(Language: TLanguages): integer;
|
||
|
||
/// convert a Windows LCID into a i18n language
|
||
function LCIDToLanguage(LCID: integer): TLanguages;
|
||
|
||
{$ifdef USESHARP}
|
||
var
|
||
/// customize this 3 values for $$ $$$ and $$$$ replacement
|
||
sharp: string = '';
|
||
sharp2: string = '';
|
||
sharp3: string = '';
|
||
{$endif}
|
||
|
||
/// translate the 'Text' term into current language, with no || nor $$[$[$]]
|
||
// - LoadResStringTranslate of our customized system.pas points to this procedure
|
||
// - therefore, direct use of LoadResStringTranslate() is better in apps
|
||
// - expect "string" type, i.e. UnicodeString for Delphi 2009 and up
|
||
procedure GetText(var Text: string);
|
||
|
||
/// translate the 'English' term into current language
|
||
// - you should use resourcestring instead of this function
|
||
// - call interenaly GetText() procedure, i.e. LoadResStringTranslate()
|
||
function _(const English: WinAnsiString): string;
|
||
|
||
var
|
||
/// a table used for fast conversion to lowercase, according to the current language
|
||
// - can NOT be used for MBCS strings (with such code pages, you should use windows
|
||
// slow but accurate API)
|
||
i18nToLower,
|
||
/// a table used for fast conversion to uppercase, according to the current language
|
||
// - can NOT be used for MBCS strings (with such code pages, you should use windows
|
||
// slow but accurate API)
|
||
i18nToUpper: TNormTable;
|
||
/// a table used for fast conversion to lowercase, according to the current language
|
||
// - can NOT be used for MBCS strings (with such code pages, you should use windows
|
||
// slow but accurate API)
|
||
i18nToLowerByte: TNormTableByte absolute i18nToLower;
|
||
/// a table used for fast conversion to uppercase, according to the current language
|
||
// - can NOT be used for MBCS strings (with such code pages, you should use windows
|
||
// slow but accurate API)
|
||
i18nToUpperByte: TNormTableByte absolute i18nToUpper;
|
||
|
||
type
|
||
/// function prototype for comparing two Ansi strings
|
||
// - used for comparison within the current selected language
|
||
TCompareFunction = function(const S1, S2: AnsiString): Integer;
|
||
|
||
var
|
||
/// use this function to compare string with case sensitivity for the UI
|
||
// - use current language for comparison
|
||
// - can be used for MBCS strings (with such code pages, it will use windows
|
||
// slow but accurate API)
|
||
i18nCompareStr: TCompareFunction = nil;
|
||
/// use this function to compare string with no case sensitivity for the UI
|
||
// - use current language for comparison
|
||
// - can be used for MBCS strings (with such code pages, it will use windows
|
||
// slow but accurate API)
|
||
i18nCompareText: TCompareFunction = nil;
|
||
|
||
|
||
type
|
||
TCustomFormDynArray = array of TCustomForm;
|
||
|
||
/// class to load and handle translation files (fr.msg, de.msg, ja.msg.. e.g.)
|
||
// - This standard .msg text file contains all the program resources translated
|
||
// into any language.
|
||
// - Unicode characters (Chinese or Japanese) can be used.
|
||
// - The most important part of this file is the [Messages] section, which
|
||
// contain all the text to be displayed in NumericValue=Text pairs.
|
||
// The numeric value is a hash (i.e. unique identifier) of the Text.
|
||
// To make a new translation, the "Text" part of these pairs must be translated,
|
||
// but the NumericValue must remain the same.
|
||
// - In the "Text" part, translator must be aware of some important characters,
|
||
// which must NOT be modified, and appears in exactly the same place inside
|
||
// the translated text:\line
|
||
// 1. | indicates a CR (carriage return) character;\line
|
||
// 2. ~ indicates a LF (line feed) character;\line
|
||
// 3. , sometimes is a comma inside a sentence, but is also used some other times
|
||
// as a delimiter between sentences; \line
|
||
// 4. %s will be replaced by a textual value before display;\line
|
||
// 5. %d will be replaced by a numerical value before display;\line
|
||
// some HTML code may appear (e.g. <br><font color="clnavy">...) and all text
|
||
// between < and > must NOT be modified;\line
|
||
// 6. no line feed or word wrap is to be used inside the "Text" part; the whole
|
||
// NumericValue=Text pair must be contained in an unique line, even if it is huge.
|
||
// - Some other sections appears before the [Messages] part, and does apply to
|
||
// windows as they are displayed on screen. By default, the text is replaced by
|
||
// a _ with a numerical value pointing to a text inside the [Messages] section.
|
||
// On some rare occasion, this default translation may be customized: in such
|
||
// cases, the exact new text to be displayed can be used instead of the
|
||
// _1928321 part. At the end of every line, the original text (never used,
|
||
// only put there for translator convenience) was added.
|
||
// - In order to add a new language, the steps are to be performed:\line
|
||
// 0. Extract all english message into a .txt ansi file, by calling the
|
||
// ExtractAllResources() procedure in the main program\line
|
||
// 1. Use the latest .txt original file, containing the original English messages\line
|
||
// 2. Open this file into a text editor (not Microsoft Word, but a real text editor,
|
||
// like the Windows notepad)\line
|
||
// 3. Translate the English text into a new language; some Unicode characters may be used\line
|
||
// 4. Save this new file, with the ISO two chars corresponding to the new language
|
||
// as file name, and .msg as file extension (e.g. FR.msg for French or RU.msg for Russian).\line
|
||
// 5. By adding this .msg file into the PhD.exe folder, the PC User software
|
||
// will automatically find and use it to translate the User Interface on the fly.
|
||
// Each user is able to select its own preferred translation.\line
|
||
// 6. The translator can perform the steps 3 to 5 more than once, to see in real
|
||
// time its modifications: he/she just has to restart the PC software to
|
||
// reload the updated translations.
|
||
TLanguageFile = class
|
||
protected
|
||
/// the content of the .msg file, translated into generic VCL string
|
||
// - [Messages] section is expanded into Messages TStringList (see below)
|
||
// - for Forms translations: [FormName] sections, with Properties=UI Text pairs
|
||
// - is either an AnsiString in the current code page, or an UnicodeString
|
||
// (in case of Delphi 2009 and up, that is a UNICODE compiler)
|
||
Text: string;
|
||
/// copy of translated strings from [Messages] section
|
||
// - Objects[] contain pointer(Hash32(WinAnsiEncodedMessage))
|
||
// - Strings[] contain Message text, in UnicodeString for Delphi 2009 and up
|
||
Messages: TStringList;
|
||
{$ifndef LVCL} { LVCL will use always the ISO 8601 generic text format }
|
||
/// format string used to convert a date value to a text
|
||
// - the expected format is the one used by the FormatDateTime() function
|
||
// - the current system format, depending on the current language, is used,
|
||
// then overridden by a DateFmt= entry in the .msg file content
|
||
DateFmt: string;
|
||
/// format string used to convert a time value to a text
|
||
// - the expected format is the one used by the FormatDateTime() function
|
||
// - the current system format, depending on the current language, is used,
|
||
// then overridden by a TimeFmt= entry in the .msg file content
|
||
TimeFmt: string;
|
||
/// format string used to convert a date and time value to a text
|
||
// - the expected format is the one used by the FormatDateTime() function
|
||
// - the current system format, depending on the current language, is used,
|
||
// then overridden by a DateTimeFmt= entry in the .msg file content
|
||
DateTimeFmt: string;
|
||
/// string used for displaying boolean values
|
||
fBooleanToString: array[boolean] of string;
|
||
{$endif}
|
||
{$ifndef USEFORMCREATEHOOK}
|
||
/// list of TForm sent to FormTranslate([....])
|
||
AlreadyTranslated: TCustomFormDynArray;
|
||
{$else}
|
||
/// set a language ID to change the UI into the registry
|
||
// - TComboBox(Sender).Items.Objects[TComboBox(Sender).ItemIndex] is the
|
||
// language ID
|
||
// - TMenuItem(Sender).Tag is the language ID
|
||
procedure LanguageClick(Sender: TObject);
|
||
{$endif USEFORMCREATEHOOK}
|
||
/// get corresponding *.msg translation text file name from current exe directory
|
||
// - e.g. return 'C:\Program Files\MyApplication\FR.msg'
|
||
class function FileName(aLanguageLocale: TLanguages): TFileName;
|
||
/// return a translated text from a Hash32(WinAnsiString) value
|
||
// - search is very fast (use binary search algorithm)
|
||
// - return a generic VCL string (i.e. UnicodeString for Delphi 2009 and up)
|
||
function FindMessage(Hash: cardinal): string;
|
||
public
|
||
/// identify the current language
|
||
Language: TLanguage;
|
||
/// specify a text file containing the translation messages for a language
|
||
constructor Create(const aFileName: TFileName; aLanguageLocale: TLanguages); overload;
|
||
/// load corresponding *.msg translation text file from the current exe directory
|
||
constructor Create(aLanguageLocale: TLanguages); overload;
|
||
/// free translation tables memory
|
||
destructor Destroy; override;
|
||
/// fill translation tables from text file containing the translation messages
|
||
// - handle on the fly UTF-8 and UNICODE decode into the corresponding ANSI
|
||
// CHARSET, or into UnicodeString for Delphi 2009 and up (checking UTF-8 or
|
||
// Unicode BOM marker is available)
|
||
procedure LoadFromFile(const aFileName: TFileName);
|
||
/// translate an English string into a localized string
|
||
// - English is case-sensitive (same as standard gettext)
|
||
// - translations are stored in Messages[] and Text properties
|
||
// - expect parameter as generic VCL string (i.e. UnicodeString for Delphi 2009 and up)
|
||
procedure Translate(var English: string);
|
||
/// translate the english captions of a TForm into the current UI language
|
||
// - must be called once with english captions
|
||
// - call automaticaly if conditional USEFORMCREATEHOOK is defined
|
||
procedure FormTranslateOne(aForm: TComponent);
|
||
{$ifndef USEFORMCREATEHOOK}
|
||
procedure FormTranslate(Forms: array of TCustomForm);
|
||
{$endif USEFORMCREATEHOOK}
|
||
/// read a parameter, stored in the .msg file before any [Section]
|
||
function ReadParam(const ParamName: RawUTF8): string;
|
||
/// convert the supplied boolean constant into ready to be displayed text
|
||
// - by default, returns 'No' for false, and 'Yes' for true
|
||
// - returns the text as generic string type, ready to be used in the VCL
|
||
function BooleanToString(Value: boolean): string;
|
||
/// convert a TSQLRecord published property value into ready to be displayed text
|
||
// - will convert any sftUTF8Text/sftAnsiText into ready to be displayed text
|
||
// - will convert any sftInteger/sftFloat/sftCurrency into its textual value
|
||
// - will convert any sftBoolean, sftEnumerate, sftDateTime, sftUnixTime or
|
||
// sftTimeLog/sftModTime/sftCreateTime into the corresponding text, depending
|
||
// on the current language
|
||
// - will convert a sftSet property value to a list of all set enumerates,
|
||
// separated by #13#10
|
||
// - will convert any sftID to 'Record Name', i.e. the value of the main
|
||
// property (mostly 'Name') of the referenced record
|
||
// - will convert any sftRecord to 'Table Name: Record Name'
|
||
// - will ignore sftBlob field
|
||
// - returns the text as generic string type, ready to be used in the VCL
|
||
function PropToString(Prop: TSQLPropInfo; Instance: TSQLRecord; Client: TSQLRest): string;
|
||
/// convert a date into a ready to be displayed text on the screen
|
||
function DateToText(const DateTime: TDateTime): string; overload; {$ifdef HASINLINE}inline;{$endif}
|
||
/// convert a date into a ready to be displayed text on the screen
|
||
function DateToText(const Time: TTimeLogBits): string; overload; {$ifdef HASINLINE}inline;{$endif}
|
||
/// convert a date into a ready to be displayed text on the screen
|
||
function DateToText(const Time: TTimeLog): string; overload; {$ifdef HASINLINE}inline;{$endif}
|
||
/// convert a date and time into a ready to be displayed text on the screen
|
||
function DateTimeToText(const DateTime: TDateTime): string;
|
||
overload; {$ifdef HASINLINE}inline;{$endif}
|
||
/// convert a date and time into a ready to be displayed text on the screen
|
||
function DateTimeToText(const Time: TTimeLogBits): string; overload; {$ifdef HASINLINE}inline;{$endif}
|
||
/// convert a date and time into a ready to be displayed text on the screen
|
||
function DateTimeToText(const Time: TTimeLog): string; overload; {$ifdef HASINLINE}inline;{$endif}
|
||
/// convert a time into a ready to be displayed text on the screen
|
||
function TimeToText(const DateTime: TDateTime): string;
|
||
overload; {$ifdef HASINLINE}inline;{$endif}
|
||
/// convert a time into a ready to be displayed text on the screen
|
||
function TimeToText(const Time: TTimeLogBits): string; overload; {$ifdef HASINLINE}inline;{$endif}
|
||
/// convert a time into a ready to be displayed text on the screen
|
||
function TimeToText(const Time: TTimeLog): string; overload; {$ifdef HASINLINE}inline;{$endif}
|
||
end;
|
||
|
||
{
|
||
/// export the translation file into a .PO format
|
||
// - the .PO format is used by the GNU gettext tool, and allow to use some
|
||
// very useful translation tools
|
||
// (see @http://www.gnu.org/software/hello/manual/gettext/PO-Files.html
|
||
// for documentation about the .PO format itself)
|
||
// - the .PO is created from two .msg files, both contained in the SourceMsgPath
|
||
// directory: the original EN.msg file and the specified SourceLanguage.msg
|
||
// translated file; the resulting POFileName will be created for this language
|
||
// - if not SourceMsgPath is supplied, the current directory is used (not
|
||
// necessary the executable directory)
|
||
procedure POExport(const SourceMsgPath, POFileName: TFileName; SourceLanguage: TLanguages);
|
||
}
|
||
|
||
/// generic US/English date/time to VCL text conversion
|
||
// - not to be used in your programs: it's just here to allow inlining of
|
||
// TLanguageFile.DateTimeToText/DateToText/TimeToText
|
||
function DateTimeToIso(const DateTime: TDateTime; DateOnly: boolean): string;
|
||
|
||
var
|
||
/// global variable set by SetCurrentLanguage(), used for translation
|
||
// - use this object, and its Language property, to retrieve current UI settings
|
||
Language: TLanguageFile = nil;
|
||
|
||
/// global event to be assigned for component translation override
|
||
// - the method implementing this event must return true if the
|
||
// translation was handled, or false if the translation must be done
|
||
// by the framework
|
||
OnTranslateComponent: function(C: TComponent): boolean of object = nil;
|
||
|
||
|
||
{$ifdef EXTRACTALLRESOURCES}
|
||
/// save all forms and resourcestring of the current exe to a .messages file
|
||
// following the .msg format (WinAnsi text file, since it should be in english)
|
||
// call this procedure once in your program in order to create a template
|
||
// to be used later for translation (don't call it in release executable)
|
||
// - only parameter is the used enumerations to be displayed (after uncamel)
|
||
procedure ExtractAllResources(const EnumTypeInfo: array of pointer;
|
||
const Objects: array of TObject; const Records: array of TClass;
|
||
const CustomCaptions: array of WinAnsiString);
|
||
{$endif}
|
||
|
||
{$ifndef ENHANCEDRTL}
|
||
/// our hooked procedure for reading a string resource
|
||
// - the default one in System.pas unit is replaced by this one
|
||
// - this function add caching and on the fly translation (if LoadResStringTranslate
|
||
// is defined in SynCommons.pas unit)
|
||
// - use "string" type, i.e. UnicodeString for Delphi 2009 and up
|
||
function LoadResString(ResStringRec: PResStringRec): string;
|
||
{$endif}
|
||
|
||
|
||
/// convert any generic VCL Text into an UTF-8 encoded String
|
||
// - same as SynCommons.StringToUTF8()
|
||
function S2U(const Text: string): RawUTF8;
|
||
{$ifdef HASINLINE}inline;{$endif}
|
||
|
||
/// convert an UTF-8 encoded text into a VCL-ready string
|
||
// - same as SynCommons.UTF8ToString()
|
||
function U2S(const Text: RawUTF8): string;
|
||
{$ifdef HASINLINE}inline;{$endif}
|
||
|
||
/// convert a custom date/time into a VCL-ready string
|
||
// - this function must be assigned to i18nDateText global var of SynCommons.pas
|
||
// - wrapper to Language.DateTimeToText(Iso) method
|
||
function Iso2S(const Iso: TTimeLog): string;
|
||
|
||
/// convert a custom date/time into a VCL-ready string
|
||
// - this function must be assigned to i18nDateTimeText global var of SynCommons.pas
|
||
// - wrapper to Language.DateTimeToText(DateTime) method
|
||
function DateTime2S(const DateTime: TDateTime): string;
|
||
|
||
|
||
implementation
|
||
|
||
uses
|
||
{$ifndef LVCL}
|
||
ComCtrls,
|
||
{$ifdef WITHUXTHEME}
|
||
UxTheme,
|
||
{$endif}
|
||
{$endif}
|
||
Controls,
|
||
ExtCtrls,
|
||
Graphics;
|
||
|
||
var
|
||
// to speed up search in LanguageAbrToIndex():
|
||
LanguageAbrWord: array[TLanguages] of word; // copy of LanguageAbr[]
|
||
|
||
const
|
||
LANG_MACEDONIAN = $2f;
|
||
LANG_DARI = $8c;
|
||
LANG_PASHTO = $63;
|
||
sPriLang: array[TLanguages] of byte =
|
||
(LANG_HEBREW,LANG_GREEK,0,LANG_DARI,0,LANG_CATALAN,0,LANG_CZECH,0,0,0,
|
||
LANG_DANISH,LANG_GERMAN,LANG_ARABIC,LANG_ENGLISH,LANG_SPANISH,LANG_FARSI,
|
||
LANG_FINNISH,LANG_FRENCH,0,0,0,0,LANG_HUNGARIAN,0,LANG_INDONESIAN,0,
|
||
LANG_ICELANDIC,LANG_ITALIAN,LANG_JAPANESE,LANG_KOREAN,0,LANG_LITHUANIAN,0,
|
||
LANG_NORWEGIAN,0,LANG_PORTUGUESE,LANG_POLISH,LANG_ROMANIAN,LANG_RUSSIAN,0,
|
||
LANG_SLOVAK,LANG_SLOVENIAN,LANG_ALBANIAN,LANG_SERBIAN,LANG_SWEDISH,0,
|
||
LANG_TURKISH,0,LANG_UKRAINIAN,LANG_VIETNAMESE,LANG_CHINESE,LANG_DUTCH,
|
||
LANG_THAI,LANG_BULGARIAN,LANG_BELARUSIAN,LANG_ESTONIAN,LANG_LATVIAN,
|
||
LANG_MACEDONIAN,LANG_PASHTO);
|
||
|
||
function LanguageToLCID(Language: TLanguages): integer;
|
||
begin
|
||
if Language=LANGUAGE_NONE then
|
||
result := LCID_US else
|
||
case sPriLang[Language] of
|
||
LANG_CHINESE: result := $0804; // Chinese (PRC) if not $0404
|
||
else
|
||
result := LANG_USER_DEFAULT or sPriLang[Language]; // Process Default Language ($0400)
|
||
end; // leave Sort order to $0 = default
|
||
end;
|
||
|
||
var LastLCID: integer;
|
||
LastLCIDLanguage: TLanguages = LANGUAGE_NONE;
|
||
|
||
function LCIDToLanguage(LCID: integer): TLanguages;
|
||
// compares sPriLang[] values with sysLocale.PriLangID to set current language
|
||
// return LanguageUS if this LCID is not known
|
||
var b: byte;
|
||
lng: TLanguages;
|
||
begin
|
||
if LCID=LastLCID then begin
|
||
result := LastLCIDLanguage;
|
||
exit;
|
||
end;
|
||
b := LCID and 255;
|
||
case b of
|
||
$1A: // ambigious PriLangID -> get it by full LCID
|
||
case LCID of
|
||
$141a, $201a: result := lngBosnian;
|
||
$041a, $101a: result := lngCroatian;
|
||
else result := lngSerbian; // by default - don't call UN again
|
||
end; // case SysLocale
|
||
else begin
|
||
result := lngEnglish;
|
||
for lng := low(lng) to high(lng) do
|
||
if b=sPriLang[lng] then begin
|
||
result := lng;
|
||
break;
|
||
end;
|
||
end;
|
||
end;
|
||
LastLCID := LCID;
|
||
LastLCIDLanguage := Result;
|
||
end;
|
||
|
||
|
||
function LanguageAbrToIndex(const value: RawUTF8): TLanguages;
|
||
// LanguageAbrToIndex('GR')=1
|
||
begin
|
||
if length(value)>=2 then
|
||
result := LanguageAbrToIndex(pointer(Value)) else
|
||
result := LANGUAGE_NONE;
|
||
end;
|
||
|
||
function LanguageAbrToIndex(P: PAnsiChar): TLanguages; overload;
|
||
var ndx: integer;
|
||
begin
|
||
if P=nil then
|
||
ndx := -1 else
|
||
ndx := WordScanIndex(@LanguageAbrWord,Length(LanguageAbrWord),
|
||
NormToLowerByte[ord(P[0])]+NormToLowerByte[ord(P[1])] shl 8);
|
||
if ndx<0 then
|
||
result := LANGUAGE_NONE else
|
||
result := TLanguages(ndx);
|
||
end;
|
||
|
||
|
||
const
|
||
// default character set for a specific language (for GUI i18n)
|
||
// list taken from http://www.webheadstart.org/xhtml/encoding
|
||
// see also http://msdn2.microsoft.com/en-us/library/ms776260.aspx
|
||
// DEFAULT_CHARSET is set if not known -> Win32 will take care as default locale
|
||
// ANSI_CHARSET is iso-8859-1, windows-1252
|
||
LanguageCharSet: packed array[TLanguages] of byte // byte-aligned
|
||
= (HEBREW_CHARSET, // 'he' CP1255 iso-8859-8
|
||
GREEK_CHARSET, // 'gr' CP1253 iso-8859-7
|
||
ANSI_CHARSET, // 'la' Latin
|
||
ARABIC_CHARSET, // 'ad' Dari (Afghanistan)
|
||
EASTEUROPE_CHARSET, // 'bs' bosnian CP1250 iso-8859-2
|
||
ANSI_CHARSET, // 'ca' catalan
|
||
ANSI_CHARSET, // 'co' corsican
|
||
EASTEUROPE_CHARSET, // 'cs' czech CP1250 iso-8859-2
|
||
DEFAULT_CHARSET, // 'cp' Coptic is Unicode-UTF8 only
|
||
EASTEUROPE_CHARSET, // 'cu' Slavic
|
||
ANSI_CHARSET, // 'cy' Welsh (gallois)
|
||
ANSI_CHARSET, // 'da' Danish
|
||
ANSI_CHARSET, // 'de' German
|
||
ARABIC_CHARSET, // 'ar' Arabic CP1256, iso-8859-6
|
||
ANSI_CHARSET, // 'en' English (GB+US)
|
||
ANSI_CHARSET, // 'es' Spanish
|
||
ARABIC_CHARSET, // 'fa' Farsi CP1256, iso-8859-6
|
||
ANSI_CHARSET, // 'fi' Finish
|
||
ANSI_CHARSET, // 'fr' French
|
||
ANSI_CHARSET, // 'ga' Irish
|
||
ANSI_CHARSET, // 'gd' Gaelic
|
||
HEBREW_CHARSET, // 'am' Aramaic CP1255, iso-8859-8
|
||
EASTEUROPE_CHARSET, // 'hr' Croatian CP1250 iso-8859-2
|
||
EASTEUROPE_CHARSET, // 'hu' Hungarian CP1250 iso-8859-2
|
||
DEFAULT_CHARSET, // 'hy' Armenian is Unicode-UTF8 only
|
||
ANSI_CHARSET, // 'id' Indonesian
|
||
ANSI_CHARSET, // 'ie' Interlingue
|
||
ANSI_CHARSET, // 'is' Icelandic
|
||
ANSI_CHARSET, // 'it' Italian
|
||
SHIFTJIS_CHARSET, // 'ja' Japanese CP932
|
||
HANGEUL_CHARSET, // 'ko' Korean CP949 (JOHAB is for old Win95+NT4)
|
||
DEFAULT_CHARSET, // 'bo' Tibetan is Unicode-UTF8 only
|
||
BALTIC_CHARSET, // 'lt' Lituanian CP1257, iso-8859-13
|
||
ANSI_CHARSET, // 'mg' Malgash uses latin alphabet
|
||
ANSI_CHARSET, // 'no' Norwegian
|
||
ANSI_CHARSET, // 'oc' Occitan
|
||
ANSI_CHARSET, // 'pt' Portuguese
|
||
EASTEUROPE_CHARSET, // 'pl' Polish CP1250 iso-8859-2
|
||
EASTEUROPE_CHARSET, // 'ro' Romanian CP1250 iso-8859-2
|
||
RUSSIAN_CHARSET, // 'ru' Russian CP1251, iso-8859-5
|
||
DEFAULT_CHARSET, // 'sa' Sanskrit is unicode only
|
||
EASTEUROPE_CHARSET, // 'sk' Slovak CP1250 iso-8859-2
|
||
EASTEUROPE_CHARSET, // 'sl' Slovenian CP1250 iso-8859-2
|
||
ANSI_CHARSET, // 'sq' Albanian
|
||
EASTEUROPE_CHARSET, // 'sr' Serbian - latin alphabet CP1250 iso-8859-2
|
||
ANSI_CHARSET, // 'sv' Swedish
|
||
DEFAULT_CHARSET, // 'sy' Syriac ISO 639-3 is Unicode-UTF8 only
|
||
TURKISH_CHARSET, // 'tr' Turkish iso-8859-9, windows-1254
|
||
ANSI_CHARSET, // 'ty' Tahitian
|
||
RUSSIAN_CHARSET, // 'uk' Ukrainian iso-8859-5 CP1251
|
||
VIETNAMESE_CHARSET, // 'vi' Vietnamese CP1258
|
||
GB2312_CHARSET, // 'zh' Chinese EUC-CN CP936, gb2312.1980-0 = simplified
|
||
ANSI_CHARSET, // 'nl' Dutch
|
||
THAI_CHARSET, // 'th' Thai CP874 iso-8859-11 tis620
|
||
RUSSIAN_CHARSET, // 'bg' Bulgarian CP1251, iso-8859-5
|
||
RUSSIAN_CHARSET, // 'be' Byelorussian CP1251, iso-8859-5
|
||
BALTIC_CHARSET, // 'et' Estonian CP1257 iso-8859-15
|
||
BALTIC_CHARSET, // 'lv' Latvian CP1257 iso-8859-15
|
||
RUSSIAN_CHARSET, // 'mk' Macedonian CP1251, iso-8859-5
|
||
ARABIC_CHARSET // 'ap' Pashto (Afghanistan)
|
||
);
|
||
|
||
|
||
{$ifndef ENHANCEDRTL}
|
||
// code below is extracted from our Extended System.pas unit, and
|
||
// use the generic string type (i.e. UnicodeString for Delphi 2009 and up)
|
||
|
||
const
|
||
// cache makes it faster, even more when using on the fly translations
|
||
// 512 is a reasonnable value, never reached in practice
|
||
LoadResStringCacheSize = 512;
|
||
|
||
var CacheRes: array[0..LoadResStringCacheSize-1] of PResStringRec;
|
||
CacheResValue: array of string;
|
||
CacheResLast: PResStringRec = nil;
|
||
CacheResLastIndex: integer = -1;
|
||
CacheResCriticalSection: TRTLCriticalSection;
|
||
LastResModule,
|
||
LastResModuleInst: cardinal;
|
||
BackupLoadResString: TPatchCode;
|
||
|
||
function LoadResString(ResStringRec: PResStringRec): string;
|
||
var Buffer: array [0..4095] of Char; // char = use the generic string type
|
||
i: integer;
|
||
begin
|
||
if ResStringRec=nil then begin
|
||
result := '';
|
||
Exit;
|
||
end;
|
||
if ResStringRec.Identifier<64*1024 then begin
|
||
if CacheResCount<0 then begin // before initialization or after finalization
|
||
SetString(Result, Buffer, LoadString(FindResourceHInstance(ResStringRec.Module^),
|
||
ResStringRec.Identifier, Buffer, SizeOf(Buffer))); // direct API call
|
||
exit;
|
||
end;
|
||
EnterCriticalSection(CacheResCriticalSection); // thread-safe and mostly fast
|
||
if (ResStringRec=CacheResLast) and
|
||
(CacheRes[CacheResLastIndex].Identifier=ResStringRec.Identifier) and
|
||
(pointer(CacheResValue)<>nil) then begin
|
||
result := CacheResValue[CacheResLastIndex]; // smart cache of values
|
||
LeaveCriticalSection(CacheResCriticalSection); // manual try..finally = faster
|
||
exit;
|
||
end;
|
||
i := PtrUIntScanIndex(@CacheRes,CacheResCount,PtrUInt(ResStringRec));
|
||
if i>=0 then
|
||
repeat
|
||
if (CacheRes[i].Identifier=ResStringRec.Identifier) and
|
||
(pointer(CacheResValue)<>nil) then begin
|
||
CacheResLast := ResStringRec;
|
||
CacheResLastIndex := i;
|
||
result := CacheResValue[i]; // smart cache of values
|
||
LeaveCriticalSection(CacheResCriticalSection); // manual try..finally = faster
|
||
exit;
|
||
end;
|
||
inc(i); // wrong module -> continue search of this Identifier
|
||
if i>=CacheResCount then break;
|
||
i := PtrUIntScanIndex(@CacheRes[i],(CacheResCount-i),PtrUInt(ResStringRec));
|
||
until i<0;
|
||
if ResStringRec.Module^<>LastResModule then begin
|
||
LastResModule := ResStringRec.Module^;
|
||
LastResModuleInst := FindResourceHInstance(ResStringRec.Module^);
|
||
end;
|
||
SetString(Result, Buffer,
|
||
LoadString(LastResModuleInst, ResStringRec.Identifier, Buffer, SizeOf(Buffer)));
|
||
if Assigned(LoadResStringTranslate) then
|
||
LoadResStringTranslate(Result);
|
||
if CacheResCount<LoadResStringCacheSize then begin
|
||
if pointer(CacheResValue)=nil then
|
||
SetLength(CacheResValue,LoadResStringCacheSize);
|
||
CacheResValue[CacheResCount] := Result;
|
||
CacheRes[CacheResCount] := ResStringRec;
|
||
CacheResLast := ResStringRec;
|
||
CacheResLastIndex := CacheResCount;
|
||
inc(CacheResCount);
|
||
end;
|
||
LeaveCriticalSection(CacheResCriticalSection);
|
||
end else begin
|
||
Result := PChar(ResStringRec.Identifier);
|
||
if Assigned(LoadResStringTranslate) then
|
||
LoadResStringTranslate(Result);
|
||
end;
|
||
end;
|
||
|
||
{$endif ENHANCEDRTL}
|
||
|
||
|
||
{$ifdef USEFORMCREATEHOOK}
|
||
type
|
||
THookedForm = class(TCustomForm)
|
||
procedure HookedDoCreate;
|
||
end;
|
||
|
||
THookedFrame = class(TCustomFrame)
|
||
constructor Create(AOwner: TComponent); override;
|
||
end;
|
||
|
||
var
|
||
OriginalForm, OriginalFrame: TPatchCode;
|
||
|
||
procedure PatchCreate;
|
||
begin
|
||
if OriginalForm[0]<>0 then
|
||
exit; // patch once
|
||
RedirectCode(@THookedForm.DoCreate,@THookedForm.HookedDoCreate,@OriginalForm);
|
||
RedirectCode(@THookedFrame.Create,@THookedFrame.Create,@OriginalFrame);
|
||
end;
|
||
|
||
|
||
// hook logic was inspired from GetText()
|
||
|
||
{ THookedForm }
|
||
|
||
procedure THookedForm.HookedDoCreate;
|
||
// translate form contents just before an OnCreate handler would be called
|
||
begin
|
||
try
|
||
try
|
||
if Language<>nil then begin
|
||
DisableAlign;
|
||
DisableAutoRange;
|
||
try
|
||
Language.FormTranslateOne(self); // translate form
|
||
finally
|
||
EnableAlign;
|
||
EnableAutoRange;
|
||
end;
|
||
end;
|
||
finally
|
||
RedirectCodeRestore(@THookedForm.DoCreate,OriginalForm); // disable Hook
|
||
try
|
||
DoCreate; // call normal DoCreate event
|
||
finally
|
||
RedirectCode(@THookedForm.DoCreate,@THookedForm.HookedDoCreate);
|
||
end;
|
||
end;
|
||
except
|
||
on Exception do; // ignore all raised exception
|
||
end;
|
||
end;
|
||
|
||
{ THookedFrame }
|
||
|
||
constructor THookedFrame.Create(AOwner: TComponent);
|
||
// translate frame contents just after constructor has been called
|
||
begin
|
||
RedirectCodeRestore(@THookedFrame.Create,OriginalFrame); // disable Hook
|
||
try
|
||
inherited Create(AOwner); // call normal constructor
|
||
finally
|
||
RedirectCode(@THookedFrame.Create,@THookedFrame.Create);
|
||
end;
|
||
if Language=nil then exit;
|
||
DisableAlign;
|
||
DisableAutoRange;
|
||
try
|
||
Language.FormTranslateOne(self); // translate frame
|
||
finally
|
||
EnableAlign;
|
||
EnableAutoRange;
|
||
end;
|
||
end;
|
||
|
||
{$endif USEFORMCREATEHOOK}
|
||
|
||
{$ifdef PUREPASCAL}
|
||
|
||
function i18nInnerCompareStr(const S1, S2: AnsiString): Integer;
|
||
var Str1, Str2: PByte;
|
||
begin
|
||
Str1 := pointer(S1);
|
||
Str2 := pointer(S2);
|
||
if Str1<>Str2 then
|
||
if Str1<>nil then
|
||
if Str2<>nil then begin
|
||
if Str1^=Str2^ then
|
||
repeat
|
||
if (Str1^=0) or (Str2^=0) then break;
|
||
inc(Str1);
|
||
inc(Str2);
|
||
until Str1^<>Str2^;
|
||
result := Str1^-Str2^;
|
||
end else
|
||
result := 1 else // Str2=''
|
||
result := -1 else // Str1=''
|
||
result := 0; // Str1=Str2
|
||
end;
|
||
|
||
function i18nInnerCompareText(const S1, S2: AnsiString): Integer;
|
||
var Str1, Str2: PByte;
|
||
C1, C2: byte;
|
||
table: {$ifdef CPUX86NOTPIC}TNormTableByte absolute i18nToUpperByte{$else}PNormTableByte{$endif};
|
||
begin
|
||
Str1 := pointer(S1);
|
||
Str2 := pointer(S2);
|
||
if Str1<>Str2 then
|
||
if Str1<>nil then
|
||
if Str2<>nil then begin
|
||
{$ifndef CPUX86NOTPIC}table := @i18nToUpperByte;{$endif}
|
||
repeat
|
||
C1 := table[Str1^];
|
||
C2 := table[Str2^];
|
||
if (C1<>C2) or (C1=0) then
|
||
break;
|
||
Inc(Str1);
|
||
Inc(Str2);
|
||
until false;
|
||
Result := C1-C2;
|
||
end else
|
||
result := 1 else // Str2=''
|
||
result := -1 else // Str1=''
|
||
result := 0; // Str1=Str2
|
||
end;
|
||
|
||
{$else PUREPASCAL}
|
||
|
||
{$ifndef ENHANCEDRTL}
|
||
function i18nInnerCompareStr(const S1, S2: AnsiString): Integer;
|
||
{$ifdef FPC} nostackframe; assembler; {$endif}
|
||
// original name: CompareStr_PLR_IA32_14
|
||
asm
|
||
cmp eax, edx
|
||
je @SameString
|
||
test eax, edx // Is either of the strings perhaps nil?
|
||
jz @PossibleNilString
|
||
{ Compare the first four characters (there has to be a trailing #0). In random
|
||
AnsiString compares (quicksort, e.g.) this can save a lot of CPU time. }
|
||
@BothNonNil: // Compare the first character
|
||
mov ecx, [edx]
|
||
cmp cl, [eax]
|
||
je @FirstCharacterSame
|
||
movzx eax, byte ptr [eax] // First character differs
|
||
movzx ecx, cl
|
||
sub eax, ecx
|
||
ret
|
||
nop
|
||
@FirstCharacterSame:
|
||
push ebx
|
||
mov ebx, [eax] // Get first four characters
|
||
cmp ebx, ecx
|
||
je @FirstFourSame
|
||
mov eax, [eax - 4] // Get the AnsiString lengths in eax and edx
|
||
mov edx, [edx - 4]
|
||
cmp ch, bh // Is the second character the same?
|
||
je @FirstTwoCharactersMatch
|
||
test eax, eax // Second character differs: Are any of the strings non-nil but zero length?
|
||
jz @ReturnLengthDifference
|
||
test edx, edx
|
||
jz @ReturnLengthDifference
|
||
movzx eax, bh
|
||
movzx edx, ch
|
||
@ReturnLengthDifference:
|
||
sub eax, edx
|
||
pop ebx
|
||
ret
|
||
@FirstTwoCharactersMatch:
|
||
cmp eax, 2
|
||
jna @ReturnLengthDifference
|
||
cmp edx, 2
|
||
jna @ReturnLengthDifference
|
||
mov eax, ebx // Swap the bytes into the correct order
|
||
bswap eax
|
||
bswap ecx
|
||
sub eax, ecx
|
||
pop ebx
|
||
ret
|
||
@SameString:
|
||
xor eax, eax
|
||
ret
|
||
@PossibleNilString: // Good possibility that at least one of the strings are nil
|
||
test eax, eax
|
||
jz @FirstStringNil
|
||
test edx, edx
|
||
jnz @BothNonNil
|
||
mov eax, [eax - 4] // Return first AnsiString length: second AnsiString is nil
|
||
ret
|
||
@FirstStringNil: // Return 0 - length(S2): first AnsiString is nil
|
||
sub eax, [edx - 4]
|
||
ret
|
||
nop; nop
|
||
@FirstFourSame: // The first four characters are identical
|
||
mov ebx, [eax - 4] // set ebx = length(S1)
|
||
xor ecx, ecx
|
||
sub ebx, [edx - 4] // set ebx = length(S1) - length(S2)
|
||
push ebx // Save the length difference on the stack
|
||
adc ecx, -1 // set esi = 0 if length(S1) < length(S2), $ffffffff otherwise
|
||
and ecx, ebx // set esi = - min(length(s1), length(s2))
|
||
sub ecx, [eax - 4] // Adjust the pointers to be negative based
|
||
sub eax, ecx
|
||
sub edx, ecx
|
||
nop; nop; nop
|
||
@CompareLoop:
|
||
add ecx, 4
|
||
jns @MatchUpToLength
|
||
mov ebx, [eax + ecx]
|
||
xor ebx, [edx + ecx]
|
||
jz @CompareLoop
|
||
@Mismatch:
|
||
bsf ebx, ebx
|
||
shr ebx, 3
|
||
add ecx, ebx
|
||
jns @MatchUpToLength
|
||
movzx eax, byte ptr [eax + ecx]
|
||
movzx edx, byte ptr [edx + ecx]
|
||
sub eax, edx
|
||
pop ebx
|
||
pop ebx
|
||
ret
|
||
@MatchUpToLength: // All characters match - return the difference in length
|
||
pop eax
|
||
pop ebx
|
||
end;
|
||
|
||
{$endif}
|
||
|
||
function i18nInnerCompareText(const S1, S2: AnsiString): Integer;
|
||
{$ifdef FPC} nostackframe; assembler; {$endif}
|
||
asm // fast CompareText() version using i18nToUpper[] instead of NormToUpper[]
|
||
cmp eax,edx
|
||
je @2
|
||
test eax,edx // Is either of the strings perhaps nil?
|
||
jz @3
|
||
@0: push ebx // Compare the first character (faster quicksort)
|
||
movzx ebx,byte ptr [eax] // ebx=S1[1]
|
||
movzx ecx,byte ptr [edx] // ecx=S2[1]
|
||
cmp ebx,ecx
|
||
je @1
|
||
mov bl,byte ptr [i18nToUpper+ebx]
|
||
mov cl,byte ptr [i18nToUpper+ecx]
|
||
cmp ebx,ecx
|
||
je @1
|
||
mov eax,ebx
|
||
pop ebx
|
||
sub eax,ecx // return S1[1]-S2[1]
|
||
ret
|
||
@2: xor eax, eax
|
||
ret
|
||
@3: test eax,eax
|
||
jz @4
|
||
test edx,edx
|
||
jnz @0
|
||
mov eax,[eax-4] // Return length(S1): second AnsiString is nil
|
||
ret
|
||
@4: sub eax,[edx-4] // Return 0 - length(S2): first AnsiString is nil
|
||
ret
|
||
@1: // here, the first character was the same: test the others
|
||
push edx
|
||
push eax // save S1 and S2 for returning length(S1)-length(S2)
|
||
@s: inc eax
|
||
inc edx
|
||
mov bl,[eax] // ebx=S1[i]
|
||
mov cl,[edx] // ecx=S2[i]
|
||
or ebx,ebx
|
||
je @z // end of S1
|
||
cmp ebx,ecx
|
||
je @s
|
||
mov bl,byte ptr [i18nToUpper+ebx]
|
||
mov cl,byte ptr [i18nToUpper+ecx]
|
||
cmp ebx,ecx
|
||
je @s
|
||
mov eax,ebx
|
||
pop ebx
|
||
pop ebx // ignore S1+S2 on stack
|
||
pop ebx
|
||
sub eax,ecx // return S1[i]-S2[i]
|
||
ret
|
||
@z: pop eax
|
||
mov eax,[eax-4]
|
||
pop edx
|
||
mov edx,[edx-4]
|
||
pop ebx
|
||
sub eax,edx // return length(S1)-length(S2)
|
||
end;
|
||
|
||
{$endif PUREPASCAL}
|
||
|
||
function Win32CompareStr(const S1, S2: AnsiString): Integer;
|
||
// AnsiCompareStr() replacement using CurrentLanguage.LCID
|
||
// (used for Arabic, Japan, Chinese and Korean)
|
||
begin
|
||
Result := CompareStringA(CurrentLanguage.LCID, 0, PAnsiChar(pointer(S1)), Length(S1),
|
||
PAnsiChar(pointer(S2)), Length(S2)) - 2;
|
||
end;
|
||
|
||
function Win32CompareText(const S1, S2: AnsiString): Integer;
|
||
// AnsiCompareText() replacement using CurrentLanguage.LCID
|
||
// (used for Arabic, Japan, Chinese and Korean)
|
||
begin
|
||
Result := CompareStringA(CurrentLanguage.LCID, NORM_IGNORECASE, PAnsiChar(pointer(S1)),
|
||
Length(S1), PAnsiChar(pointer(S2)), Length(S2)) - 2;
|
||
end;
|
||
|
||
function LanguageName(aLanguage: TLanguages): string;
|
||
begin
|
||
if aLanguage=LANGUAGE_NONE then
|
||
result := '' else
|
||
result := PTypeInfo(TypeInfo(TLanguages))^.EnumBaseType^.GetCaption(aLanguage);
|
||
end;
|
||
|
||
{$ifndef NOI18N}
|
||
procedure SetCurrentLanguage(aLanguage: TLanguages; aForceEnglishIfNoMsgFile: boolean); overload;
|
||
{$ifndef USEFORMCREATEHOOK}
|
||
var i: integer;
|
||
Already: TCustomFormDynArray; // to re-translate all forms
|
||
{$endif USEFORMCREATEHOOK}
|
||
var c: AnsiChar;
|
||
LanguageForLanguageFile: TLanguages;
|
||
begin
|
||
// 1. not already set to this value?
|
||
if CurrentLanguage.Index=aLanguage then
|
||
exit;
|
||
// default CurrentLanguage.Index=LANGUAGE_NONE -> force updated english locale if necessary
|
||
{$ifdef USEFORMCREATEHOOK}
|
||
if CurrentLanguage.Index<>LANGUAGE_NONE then
|
||
raise Exception.Create('lang unit: language must be set only once');
|
||
{$endif USEFORMCREATEHOOK}
|
||
|
||
// 2. handle missing .msg file
|
||
LanguageForLanguageFile := aLanguage;
|
||
if LanguageForLanguageFile<>lngEnglish then
|
||
if not FileExists(TLanguageFile.FileName(LanguageForLanguageFile)) then begin
|
||
if aForceEnglishIfNoMsgFile then
|
||
if CurrentLanguage.Index=lngEnglish then
|
||
exit else
|
||
aLanguage := lngEnglish;
|
||
LanguageForLanguageFile := lngEnglish; // no .msg -> no translation
|
||
end;
|
||
|
||
// 3. reset all Locale settings + AnsiCompare*() functions
|
||
with CurrentLanguage do begin
|
||
Fill(aLanguage); // init all CurrentLanguage fields for this language
|
||
{$ifndef LVCL}
|
||
if GetThreadLocale<>LCID then // force locale settings if different
|
||
if SetThreadLocale(LCID) then
|
||
GetFormatSettings; // resets all locale-specific variables
|
||
{$ifdef UNICODE}
|
||
SetMultiByteConversionCodePage(CodePage); // for default AnsiString handling
|
||
{$endif}
|
||
{$endif}
|
||
CurrentAnsiConvert := TSynAnsiConvert.Engine(CodePage); // redefine from GetACP
|
||
for c := #0 to #255 do begin
|
||
i18nToUpper[c] := c;
|
||
i18nToLower[c] := c;
|
||
end;
|
||
CharUpperBuffA(i18nToUpper,256); // get values from current user locale
|
||
CharLowerBuffA(i18nToLower,256);
|
||
if not((CharSet in [GB2312_CHARSET,SHIFTJIS_CHARSET,HANGEUL_CHARSET,ARABIC_CHARSET])
|
||
{$ifndef LVCL} or SysLocale.FarEast{$endif}) and
|
||
(LanguageCharSet[LCIDToLanguage(GetUserDefaultLCID)]=CharSet) then begin
|
||
// NormToUpper/Lower[] was filled with LOCALE_USER_DEFAULT values
|
||
// -> OK if same CHARSET, and not multi-byte
|
||
i18nCompareStr := // not MBCS strict comparison is always valid
|
||
{$ifdef ENHANCEDRTL}CompareStr{$else}i18nInnerCompareStr{$endif};
|
||
// CompareText in SysUtils.pas uses NormToUpper[], this uses i18nToUpper[]:
|
||
i18nCompareText := i18nInnerCompareText;
|
||
end else begin
|
||
// AnsiCompareStr/Text() replacements using CurrentLanguage.LCID
|
||
i18nCompareStr := Win32CompareStr; // calls Win32 API for MBCS
|
||
i18nCompareText := Win32CompareText;
|
||
end;
|
||
// AnsiUpper/LowerCase use CharUpper/LowerBuff() = NormToUpper/Lower[] values
|
||
end;
|
||
|
||
// 4. create Language object from exe directory if not english
|
||
{$ifdef USEFORMCREATEHOOK}
|
||
FreeAndNil(Language);
|
||
if LanguageForLanguageFile<>lngEnglish then
|
||
Language := TLanguageFile.Create(LanguageForLanguageFile);
|
||
{$else}
|
||
if Language<>nil then begin // save AlreadyTranslated[] forms for translation
|
||
Already := Language.AlreadyTranslated;
|
||
FreeAndNil(Language);
|
||
end;
|
||
if LanguageForLanguageFile<>lngEnglish then
|
||
Language := TLanguageFile.Create(LanguageForLanguageFile);
|
||
for i := 0 to high(Already) do // translate available forms again
|
||
try
|
||
Language.FormTranslateOne(Already[i]);
|
||
except // ignore any exception -> form.Free -> acces violation e.g.
|
||
on Exception do;
|
||
end;
|
||
{$endif USEFORMCREATEHOOK}
|
||
// we use our custom system.pas unit, which contains already resourcestring caching
|
||
// (we don't have to use critical section here, since call is thread safe)
|
||
{$ifndef LVCL}
|
||
LoadResStringTranslate := GetText; // just set translation function
|
||
CacheResCount := 0; // flush LoadResString() cache
|
||
{$endif}
|
||
end;
|
||
|
||
procedure SetCurrentLanguage(const value: RawUTF8; aForceEnglishIfNoMsgFile: boolean); overload;
|
||
begin
|
||
SetCurrentLanguage(LanguageAbrToIndex(value),aForceEnglishIfNoMsgFile);
|
||
end;
|
||
|
||
{$ifdef USEFORMCREATEHOOK}
|
||
|
||
function i18nAddLanguageItems(MsgPath: TFileName; List: TStrings): integer;
|
||
var SR: TSearchRec;
|
||
lng, index: TLanguages;
|
||
Exists: set of TLanguages;
|
||
begin
|
||
if MsgPath='' then
|
||
MsgPath := ExeVersion.ProgramFilePath;
|
||
result := -1; // no language selection if no language available
|
||
fillchar(Exists,sizeof(Exists),0);
|
||
include(Exists,lngEnglish); // English is always present (default built in EXE)
|
||
if FindFirst(MsgPath+'*.msg', faAnyFile, SR)<>0 then
|
||
exit;
|
||
repeat
|
||
lng := LanguageAbrToIndex(
|
||
{$ifdef UNICODE}RawUTF8(SR.Name){$else}pointer(SR.Name){$endif});
|
||
if lng<>LANGUAGE_NONE then
|
||
include(Exists,lng);
|
||
until FindNext(SR)<>0;
|
||
FindClose(SR);
|
||
for lng := low(lng) to high(lng) do begin
|
||
index := TLanguages(LanguageAlpha[lng]); // add languages by LanguageAbr[] alpha order
|
||
if not (index in Exists) then
|
||
continue;
|
||
if index=CurrentLanguage.Index then
|
||
result := List.Count; // current language selection
|
||
List.AddObject(FormatString('% (%)',[LanguageName(index),LanguageAbr[index]]),
|
||
pointer(index));
|
||
end;
|
||
end;
|
||
|
||
procedure i18nAddLanguageCombo(const MsgPath: TFileName; Combo: TComboBox);
|
||
var i, index: integer;
|
||
List: TStringList;
|
||
begin
|
||
List := TStringList.Create;
|
||
try
|
||
index := i18nAddLanguageItems(MsgPath,List);
|
||
Combo.Items.BeginUpdate;
|
||
Combo.Clear;
|
||
for i := 0 to List.Count-1 do
|
||
Combo.AddItem(List[i],List.Objects[i]);
|
||
Combo.ItemIndex := index;
|
||
Combo.Items.EndUpdate;
|
||
Combo.OnClick := Language.LanguageClick;
|
||
finally
|
||
List.Free;
|
||
end;
|
||
end;
|
||
|
||
procedure i18nAddLanguageMenu(const MsgPath: TFileName; Menu: TMenuItem);
|
||
var i, index: integer;
|
||
MenuItem: TMenuItem;
|
||
List: TStringList;
|
||
begin
|
||
List := TStringList.Create;
|
||
try
|
||
index := i18nAddLanguageItems(MsgPath,List);
|
||
for i := 0 to List.Count-1 do begin
|
||
MenuItem := TMenuItem.Create(Menu.Owner);
|
||
MenuItem.Caption := List[i];
|
||
MenuItem.Tag := PtrInt(List.Objects[i]);
|
||
MenuItem.OnClick := Language.LanguageClick;
|
||
if i=index then
|
||
MenuItem.Checked := true; // mark current language selection
|
||
Menu.Add(MenuItem);
|
||
end;
|
||
finally
|
||
List.Free;
|
||
end;
|
||
Menu.Visible := true;
|
||
end;
|
||
|
||
function ReadRegString(Key: HKEY; const Path, Value: string): string;
|
||
// this version is UNICODE ready, and will call appropriate *W() or *A() Win32API
|
||
var l, t: DWORD;
|
||
z: array[byte] of char;
|
||
k: HKey;
|
||
begin
|
||
Result := '';
|
||
if RegOpenKeyEx(Key, pointer(Path), 0, KEY_QUERY_VALUE, k)=ERROR_SUCCESS then
|
||
try
|
||
l := sizeof(z);
|
||
t := REG_SZ;
|
||
if RegQueryValueEx(K, pointer(Value), nil, @t, @z, @l)=ERROR_SUCCESS then
|
||
Result := z;
|
||
finally
|
||
RegCloseKey(k);
|
||
end;
|
||
end;
|
||
|
||
function CreateRegKey(RootKey: HKEY; const Key, ValueName, Value: string): boolean;
|
||
// this version is UNICODE ready, and will call appropriate *W() or *A() Win32API
|
||
var Handle: HKey;
|
||
Disposition: Integer;
|
||
begin
|
||
Result := RegCreateKeyEx(RootKey, pointer(Key), 0, '',
|
||
REG_OPTION_NON_VOLATILE, KEY_READ or KEY_WRITE, nil, Handle,
|
||
@Disposition)=0;
|
||
if Result then begin
|
||
Result := RegSetValueEx(Handle, pointer(ValueName), 0, REG_SZ,
|
||
pointer(Value), (Length(Value)+1)*sizeof(char))=0;
|
||
RegCloseKey(Handle);
|
||
end;
|
||
end;
|
||
|
||
|
||
function i18nLanguageToRegistry(const Language: TLanguages): string;
|
||
// write to HKEY_CURRENT_USER\Software\[CompanyName]i18n\paramstr(0)
|
||
begin
|
||
result := '';
|
||
if Language=LANGUAGE_NONE then
|
||
exit;
|
||
CreateRegKey(HKEY_CURRENT_USER,'Software\'+RegistryCompanyName+'i18n',
|
||
SysUtils.lowercase(ExtractFileName(paramstr(0))),string(LanguageAbr[Language]));
|
||
result := SHaveToRestart; // show it in english + current language
|
||
if CurrentLanguage.Index<>lngEnglish then
|
||
result := 'You have to restart the application to apply these language changes.'#13#10+
|
||
result;
|
||
end;
|
||
|
||
function i18nRegistryToLanguage: TLanguages;
|
||
// read from HKEY_CURRENT_USER\Software\[CompanyName]i18n\paramstr(0)
|
||
begin
|
||
result := LanguageAbrToIndex(RawUTF8(ReadRegString(HKEY_CURRENT_USER,
|
||
'Software\'+RegistryCompanyName+'i18n',
|
||
SysUtils.lowercase(ExtractFileName(paramstr(0))))));
|
||
end;
|
||
{$endif}
|
||
|
||
procedure LangInit;
|
||
// do redirection + init user default locale (from Win32 or registry)
|
||
var i: TLanguages;
|
||
hKernel32: HMODULE;
|
||
begin
|
||
// LanguageAbrInteger[]: to use fast IntegerScanIndex() in LanguageAbrToIndex()
|
||
for i := low(i) to high(i) do
|
||
LanguageAbrWord[i] := PWord(pointer(LanguageAbr[i]))^;
|
||
assert(LanguageAbrToIndex('En')=lngEnglish);
|
||
assert(LanguageAbrToIndex('fR')=lngFrench);
|
||
assert(LanguageAbrToIndex('xx')=LANGUAGE_NONE);
|
||
{$ifndef EXTRACTALLRESOURCES}
|
||
{$ifdef USEFORMCREATEHOOK}
|
||
// get language from registry, if USEFORMCREATEHOOK
|
||
i := i18nRegistryToLanguage; // from \Software\CompanyName\i18n\paramstr(0)
|
||
//i := LanguageAbrToIndex('FR'); // DEBUG: load FR.MSG
|
||
if i<>LANGUAGE_NONE then
|
||
SetCurrentLanguage(i,false) else
|
||
{$endif}
|
||
{$endif}
|
||
{$ifndef LVCL} // LVCL doesn't have any SysLocale defined
|
||
SetCurrentLanguage(LCIDToLanguage(SysLocale.DefaultLCID),false);
|
||
{$endif}
|
||
// LCID_US = $0409 US English = international settings
|
||
hKernel32 := GetModuleHandle('kernel32');
|
||
if (hKernel32 > 0) then
|
||
isVista := GetProcAddress(hKernel32, 'GetLocaleInfoEx')<>nil;
|
||
{$ifdef USEFORMCREATEHOOK}
|
||
if Language<>nil then
|
||
PatchCreate; // only patch TForm and TFrame if not english
|
||
{$endif USEFORMCREATEHOOK}
|
||
end;
|
||
{$endif}
|
||
|
||
|
||
{ TLanguageFile }
|
||
|
||
constructor TLanguageFile.Create(aLanguageLocale: TLanguages);
|
||
// FR.msg, DE.msg, JP.msg files must be in the .exe directory
|
||
begin
|
||
Create(FileName(aLanguageLocale),aLanguageLocale);
|
||
end;
|
||
|
||
constructor TLanguageFile.Create(const aFileName: TFileName; aLanguageLocale: TLanguages);
|
||
begin
|
||
Language.Fill(aLanguageLocale);
|
||
CurrentAnsiConvert := TSynAnsiConvert.Engine(Language.CodePage);
|
||
LoadFromFile(aFileName);
|
||
end;
|
||
|
||
destructor TLanguageFile.Destroy;
|
||
begin
|
||
FreeAndNil(Messages);
|
||
inherited;
|
||
end;
|
||
|
||
class function TLanguageFile.FileName(aLanguageLocale: TLanguages): TFileName;
|
||
begin
|
||
if aLanguageLocale<>LANGUAGE_NONE then
|
||
result := ExeVersion.ProgramFilePath+
|
||
Ansi7ToString(LanguageAbr[aLanguageLocale])+'.msg' else
|
||
result := '';
|
||
end;
|
||
|
||
{$ifndef USEFORMCREATEHOOK}
|
||
procedure TLanguageFile.FormTranslate(Forms: array of TCustomForm);
|
||
var f: integer;
|
||
begin
|
||
SetLength(AlreadyTranslated,length(Forms));
|
||
for f := 0 to high(Forms) do begin
|
||
AlreadyTranslated[f] := Forms[f];
|
||
FormTranslateOne(Forms[f]);
|
||
end;
|
||
end;
|
||
{$endif USEFORMCREATEHOOK}
|
||
|
||
procedure TLanguageFile.FormTranslateOne(aForm: TComponent);
|
||
{$ifndef LVCL}
|
||
var DefCharSet: integer;
|
||
{$endif}
|
||
{$ifdef UNICODE} // beginning of the [aForm.Name] section in Text
|
||
var Section: PWideChar; {$else}
|
||
var Section: PUTF8Char; {$endif}
|
||
|
||
procedure DoAll(Comp: TComponent; const ParentName: RawUTF8);
|
||
|
||
function ReadString(const CompName, Name: RawUTF8): string;
|
||
var UpperName: array[byte] of AnsiChar;
|
||
begin
|
||
if Section=nil then // no [aForm.Name] available -> translate from Hash
|
||
result := '' else begin
|
||
PWord(UpperCopy(UpperCopy(UpperName,CompName),Name))^ := ord('=');
|
||
{$ifdef UNICODE}
|
||
result := FindIniNameValueW(Section,UpperName); {$else}
|
||
result := FindIniNameValue(Section,UpperName);
|
||
{$endif}
|
||
end;
|
||
end;
|
||
function TranslateOne(const CompName, PropName: RawUTF8): string;
|
||
var i: integer;
|
||
begin
|
||
result := ReadString(CompName,PropName);
|
||
if result='' then
|
||
exit;
|
||
if result[1]='_' then // btnOK.Caption=_78124567 -> from messages: 78124567=OK
|
||
{$ifdef UNICODE}
|
||
result := FindMessage(GetCardinalW(@result[2])) else
|
||
if result[1]='%' then begin // from another [FormName] translation
|
||
i := pos('.',result); // DocEdit.Caption=%MainForm.MenuEditor.Caption
|
||
result := FindIniEntryW(Text,RawUTF8(copy(result,2,i-2)),
|
||
RawUTF8(copy(result,i+1,maxInt)));
|
||
end;
|
||
{$else}
|
||
result := FindMessage(GetCardinal(@result[2])) else
|
||
if result[1]='%' then begin // from another [FormName] translation
|
||
i := pos('.',result); // DocEdit.Caption=%MainForm.MenuEditor.Caption
|
||
result := FindIniEntry(Text,copy(result,2,i-2),copy(result,i+1,maxInt));
|
||
end;
|
||
{$endif}
|
||
end;
|
||
procedure TranslateOneProp(ppi: PPropInfo; comp: TPersistent; const CompName: RawUTF8);
|
||
var old: string;
|
||
text: string;
|
||
begin
|
||
old := ppi^.GetGenericStringValue(comp);
|
||
if old='' then exit;
|
||
if Section<>nil then
|
||
text := TranslateOne(CompName,ppi^.Name);
|
||
if text='' then // if not defined in [aForm.Name] section -> direct translate
|
||
text := FindMessage(Hash32(
|
||
// resourcestring are expected to be in English, that is WinAnsi encoded
|
||
{$ifdef UNICODE}UnicodeStringToWinAnsi{$endif}(old)));
|
||
if (text<>'') and (old<>text) then
|
||
ppi^.SetGenericStringValue(comp,text);
|
||
end;
|
||
procedure TranslateObj(O: TPersistent; const CName: RawUTF8);
|
||
var j,k: integer;
|
||
Obj: TObject;
|
||
P: PPropInfo;
|
||
CL: TClass;
|
||
s: string;
|
||
{$ifndef LVCL} // doesn't allow to change Font during the run
|
||
procedure DoFont(Font: TFont);
|
||
var s: string;
|
||
CharSet: integer;
|
||
siz: integer;
|
||
begin
|
||
s := ReadString(CName,'Font.Name');
|
||
if s<>'' then
|
||
Font.Name := s;
|
||
siz := {$ifdef UNICODE}GetCardinalW{$else}GetCardinal{$endif}
|
||
(pointer(ReadString(CName,'Font.Size')));
|
||
if siz<>0 then
|
||
Font.size := siz;
|
||
s := ReadString(CName,'Font.Charset');
|
||
if s<>'' then // Font.Charset=ANSI_CHARSET to force a charset
|
||
if IdentToCharset(s,Charset) then begin
|
||
Font.Charset := CharSet;
|
||
exit;
|
||
end;
|
||
CharSet := Font.Charset;
|
||
if (CharSet=DEFAULT_CHARSET) and (Language.CharSet<>DEFAULT_CHARSET) then
|
||
CharSet := DefCharSet; // calc real CharSet: don't change good DEFAULT_CHARSET
|
||
if CharSet<>Language.CharSet then
|
||
Font.Charset := Language.CharSet;
|
||
end;
|
||
{$endif}
|
||
begin
|
||
CL := PPointer(O)^;
|
||
while (CL<>nil) and (CL<>TComponent) and (CL<>TObject) do begin
|
||
for k := 1 to InternalClassPropInfo(CL,P) do begin
|
||
// standard properties
|
||
if (P^.Name='Caption') or (P^.Name='Hint') or
|
||
(P^.Name='Title') or (P^.Name='DisplayLabel') then
|
||
TranslateOneProp(P,O,CName) else
|
||
// class properties
|
||
if P^.PropType^^.Kind=tkClass then begin
|
||
Obj := P^.GetObjProp(O);
|
||
if Obj<>nil then
|
||
{$ifndef LVCL} // doesn't allow to change Font during the run
|
||
if Obj.InheritsFrom(TFont) then
|
||
// TFont
|
||
DoFont(TFont(Obj)) else
|
||
{$endif}if Obj.InheritsFrom(TStrings) then
|
||
if P^.Name='Lines' then begin
|
||
// TMemo, TRichEdit
|
||
s := TranslateOne(CName,'Lines.Text');
|
||
if s='' then
|
||
s := FindMessage(Hash32(
|
||
// resourcestring are expected to be in English, that is WinAnsi encoded
|
||
{$ifdef UNICODE}UnicodeStringToWinAnsi{$endif}(TStrings(Obj).Text)));
|
||
if s<>'' then
|
||
TStrings(Obj).Text := s;
|
||
end else
|
||
// TListBox, TComboBox, TRadioGroup
|
||
for j := 0 to TStrings(Obj).Count-1 do begin
|
||
s := TranslateOne(CName,ShortStringToUTF8(P^.Name)+'['+Int32ToUtf8(j)+']');
|
||
if s='' then
|
||
s := FindMessage(Hash32(
|
||
// resourcestring are expected to be in English, that is WinAnsi encoded
|
||
{$ifdef UNICODE}UnicodeStringToWinAnsi{$endif}(TStrings(Obj).Strings[j])));
|
||
if s<>'' then
|
||
TStrings(Obj).Strings[j] := s;
|
||
end else
|
||
{$ifndef LVCL} // LVCL doesn't have any TCollection
|
||
// TCollection descendents
|
||
if Obj.InheritsFrom(TCollection) then
|
||
with TCollection(Obj) do begin
|
||
for j := 0 to Count-1 do
|
||
TranslateObj(Items[j],CName+ShortStringToUTF8(P^.Name)+'['+Int32ToUtf8(j)+'].');
|
||
end else
|
||
{$endif}// TComponent descendents
|
||
if Obj.InheritsFrom(TComponent) then
|
||
DoAll(TComponent(Obj),CName+ShortStringToUTF8(P^.Name)+'.');
|
||
end;
|
||
P := P^.Next;
|
||
end;
|
||
CL := GetClassParent(CL); // translate parent published properties
|
||
end;
|
||
end;
|
||
|
||
var i: integer;
|
||
C: TComponent;
|
||
begin
|
||
if Comp=nil then
|
||
exit;
|
||
|
||
// TForm: not done in the following loop
|
||
if ParentName='' then
|
||
TranslateObj(Comp,''); // Caption,Hint and all
|
||
|
||
// all components of this Form / Component collection
|
||
for i := 0 to Comp.ComponentCount-1 do begin
|
||
// 1. deal with subcomponents, if any
|
||
C := Comp.Components[i];
|
||
if (C.ComponentCount>0)
|
||
{$ifndef LVCL}and not C.InheritsFrom(TRadioGroup){$endif} then
|
||
DoAll(C,ParentName+RawUTF8(C.Name)+'.');
|
||
{$ifdef WITHUXTHEME}
|
||
// 2. Vista
|
||
if isVista and C.InheritsFrom(TTreeView) then
|
||
SetWindowTheme(TTreeView(C).Handle, 'explorer', nil);
|
||
{$endif}
|
||
// 3. user-defined translation
|
||
if Assigned(OnTranslateComponent) then
|
||
if OnTranslateComponent(C) then
|
||
exit; // user method returned true, that is already translated
|
||
// 4. ignore components with no name or which names begin with '_'
|
||
if (C.Name='') or (C.Name[1]='_') then
|
||
continue;
|
||
// 5. Translate properties (Caption,Hint,Title,Lines,Items,Font..)
|
||
TranslateObj(C,ParentName+RawUTF8(C.Name)+'.');
|
||
end;
|
||
end;
|
||
|
||
var UpperSection: array[byte] of AnsiChar;
|
||
begin
|
||
if (Self=nil) or (Text='') or (aForm=nil) then
|
||
exit;
|
||
{$ifndef LVCL}
|
||
DefCharSet := GetDefFontCharSet;
|
||
DefFontData.Charset := Language.CharSet;
|
||
{$endif}
|
||
Section := pointer(Text);
|
||
PWord(UpperCopy(UpperSection,RawUTF8(aForm.ClassName)))^ := ord(']');
|
||
{$ifdef UNICODE}
|
||
if not FindSectionFirstLineW(Section,UpperSection) then {$else}
|
||
if not FindSectionFirstLine(Section,UpperSection) then
|
||
{$endif}
|
||
Section := nil; // no [aForm.Name] section -> use Hash32() translation
|
||
DoAll(aForm,'');
|
||
if aForm.InheritsFrom(TCustomForm) then // can be called with TCustomFrame
|
||
if TCustomForm(aForm).Visible then
|
||
TCustomForm(aForm).Refresh;
|
||
Application.ProcessMessages;
|
||
end;
|
||
|
||
{$ifdef USEFORMCREATEHOOK}
|
||
procedure TLanguageFile.LanguageClick(Sender: TObject);
|
||
// called with MenuItem.Tag = language ID
|
||
var LangIndex: TLanguages;
|
||
begin
|
||
if Sender.InheritsFrom(TMenuItem) then
|
||
LangIndex := TLanguages(TMenuItem(Sender).Tag) else
|
||
if Sender.InheritsFrom(TComboBox) then
|
||
with TComboBox(Sender) do
|
||
if ItemIndex<0 then
|
||
exit else
|
||
LangIndex := TLanguages(Items.Objects[ItemIndex]) else
|
||
exit;
|
||
if (LangIndex=LANGUAGE_NONE) or (LangIndex=CurrentLanguage.Index) then
|
||
exit;
|
||
// Registry Values for i18n unit
|
||
MessageBox(Application.Handle,pointer(i18nLanguageToRegistry(LangIndex)),
|
||
nil,MB_OK or MB_ICONINFORMATION);
|
||
end;
|
||
{$endif}
|
||
|
||
function StringListCompareStrings(List: TStringList; Index1, Index2: integer): Integer;
|
||
begin // we need this integer<->cardinal trick to avoid comparison overflow
|
||
Index1 := PtrInt(List.Objects[Index1]);
|
||
Index2 := PtrInt(List.Objects[Index2]);
|
||
if PtrUInt(Index1)<PtrUInt(Index2) then
|
||
result := -1 else
|
||
if Index1=Index2 then
|
||
result := 0 else
|
||
result := 1;
|
||
assert((Index1=Index2) or (Result<>0)); // debug: no hash collision allowed
|
||
end;
|
||
|
||
function TLanguageFile.FindMessage(Hash: cardinal): string;
|
||
var L, H, I: Integer;
|
||
V: cardinal; // trick to avoid comparison overflow
|
||
begin // finding is very fast, even if Objects[] is called
|
||
if (self<>nil) and (Hash<>0) and (Messages<>nil) then begin
|
||
{ for i := 0 to Count-1 do // slower version
|
||
if cardinal(Objects[I])=Hash then begin result := Strings[i]; break; end;
|
||
exit; }
|
||
L := 0;
|
||
H := Messages.Count - 1;
|
||
while L <= H do begin // use fast binary search algorithm
|
||
I := (L + H) shr 1;
|
||
V := cardinal(Messages.Objects[I]); // our custom Classes.pas unit is fast enough
|
||
if V<Hash then
|
||
L := I+1 else
|
||
if V=Hash then begin
|
||
result := Messages.Strings[I]; // UnicodeString on Delphi 2009 and up
|
||
exit;
|
||
end else
|
||
H := I-1;
|
||
end;
|
||
end;
|
||
result := '';
|
||
end;
|
||
|
||
const
|
||
B2SW: array[boolean] of WinAnsiString = ('No','Yes');
|
||
B2SS: array[boolean] of string = ('No','Yes');
|
||
|
||
procedure TLanguageFile.LoadFromFile(const aFileName: TFileName);
|
||
var s: string; // either AnsiString either UnicodeString
|
||
{$ifdef UNICODE}
|
||
P: PWideChar; {$else}
|
||
P: PUTF8Char; {$endif}
|
||
H: cardinal;
|
||
i: integer;
|
||
tmp: string;
|
||
B: boolean;
|
||
begin
|
||
FreeAndNil(Messages);
|
||
fBooleanToString[false] := B2SS[false];
|
||
fBooleanToString[true] := B2SS[true];
|
||
Text := '';
|
||
if not FileExists(aFileName) then
|
||
exit;
|
||
// 1. read .msg file with appropriate UTF8 or Unicode conversion
|
||
Text := AnyTextFileToString(aFileName); // appropriate conversion
|
||
// 2. fill Translation[] and Messages[]
|
||
Messages := TStringList.Create;
|
||
P := pointer(Text);
|
||
{$ifdef UNICODE}
|
||
if FindSectionFirstLineW(P,'MESSAGES]') then
|
||
while (P<>nil) and (P^<>'[') do begin
|
||
H := GetNextItemCardinalW(P,'=');
|
||
s := GetNextLineW(P,P);
|
||
{$else}
|
||
if FindSectionFirstLine(P,'MESSAGES]') then
|
||
while (P<>nil) and (P^<>'[') do begin
|
||
H := GetNextItemCardinal(P,'=');
|
||
s := GetNextLine(P,P);
|
||
{$endif}
|
||
if H<>0 then begin
|
||
for i := 1 to length(s) do
|
||
case s[i] of
|
||
'|': s[i] := #13;
|
||
'~': s[i] := #10;
|
||
end;
|
||
Messages.AddObject(s,pointer(H));
|
||
end;
|
||
end;
|
||
Messages.CustomSort(StringListCompareStrings); // sort by Hash32() values
|
||
{$ifndef LVCL}
|
||
tmp := ReadParam('DateFmt');
|
||
if tmp<>'' then
|
||
DateFmt := tmp else
|
||
DateFmt := {$ifdef ISDELPHIXE}FormatSettings.{$endif}
|
||
ShortDateFormat; // get default value from current locale
|
||
tmp := ReadParam('TimeFmt');
|
||
if tmp<>'' then
|
||
TimeFmt := tmp else
|
||
TimeFmt := 'hh:mm'; // default value for time is 24 hours display
|
||
tmp := ReadParam('DateTimeFmt');
|
||
if tmp<>'' then
|
||
DateTimeFmt := tmp else
|
||
DateTimeFmt := DateFmt+' '+TimeFmt; // default value from current locale
|
||
{$endif}
|
||
for B := false to true do begin
|
||
tmp := FindMessage(Hash32(B2SW[B]));
|
||
if tmp<>'' then
|
||
fBooleanToString[B] := tmp;
|
||
end;
|
||
end;
|
||
|
||
function TLanguageFile.ReadParam(const ParamName: RawUTF8): string;
|
||
begin
|
||
if self=nil then
|
||
result := '' else
|
||
{$ifdef UNICODE}
|
||
result := FindIniEntryW(Text,'',ParamName); {$else}
|
||
result := FindIniEntry(Text,'',ParamName);
|
||
{$endif}
|
||
end;
|
||
|
||
procedure TLanguageFile.Translate(var English: string);
|
||
// case-sensitive (same as standard gettext)
|
||
var result: string;
|
||
begin
|
||
result := FindMessage(Hash32(
|
||
// resourcestring are expected to be in English, that is WinAnsi encoded
|
||
{$ifdef UNICODE}StringToWinAnsi{$endif}(English)));
|
||
if result<>'' then
|
||
English := result;
|
||
end;
|
||
|
||
procedure GetText(var Text: string);
|
||
// used for System.LoadResStringTranslate case-sensitive (same as standard gettext)
|
||
begin
|
||
if Language<>nil then
|
||
Language.Translate(Text);
|
||
end;
|
||
|
||
function _(const English: WinAnsiString): string;
|
||
begin
|
||
if Language<>nil then begin
|
||
result := Language.FindMessage(Hash32(English));
|
||
if result<>'' then
|
||
exit;
|
||
end;
|
||
{$ifdef UNICODE}
|
||
result := WinAnsiToUnicodeString(English);
|
||
{$else}
|
||
result := CurrentAnsiConvert.AnsiToAnsi(WinAnsiConvert,English);
|
||
{$endif}
|
||
end;
|
||
|
||
function S2U(const Text: string): RawUTF8;
|
||
begin
|
||
{$ifdef UNICODE}
|
||
RawUnicodeToUtf8(PWideChar(pointer(Text)),length(Text),result);
|
||
{$else}
|
||
result := CurrentAnsiConvert.AnsiBufferToRawUTF8(pointer(Text),length(Text));
|
||
{$endif}
|
||
end;
|
||
|
||
function U2S(const Text: RawUTF8): string;
|
||
begin
|
||
{$ifdef UNICODE}
|
||
UTF8DecodeToUnicodeString(pointer(Text),length(Text),result);
|
||
{$else}
|
||
result := CurrentAnsiConvert.UTF8BufferToAnsi(pointer(Text),length(Text));
|
||
{$endif}
|
||
end;
|
||
|
||
function Iso2S(const Iso: TTimeLog): string;
|
||
begin
|
||
if Iso=0 then
|
||
result := '' else
|
||
if Iso and (1 shl (6+6+5)-1)=0 then
|
||
result := Language.DateToText(Iso) else
|
||
if Iso shr (6+6+5)=0 then
|
||
result := Language.TimeToText(Iso) else
|
||
result := Language.DateTimeToText(Iso);
|
||
end;
|
||
|
||
function DateTime2S(const DateTime: TDateTime): string;
|
||
begin
|
||
if DateTime=0 then
|
||
result := '' else
|
||
result := Language.DateTimeToText(DateTime);
|
||
end;
|
||
|
||
function TLanguageFile.BooleanToString(Value: boolean): string;
|
||
begin
|
||
if self=nil then
|
||
result := B2SS[Value] else begin
|
||
result := fBooleanToString[Value];
|
||
if result='' then
|
||
result := B2SS[Value];
|
||
end;
|
||
end;
|
||
|
||
function TLanguageFile.PropToString(Prop: TSQLPropInfo; Instance: TSQLRecord;
|
||
Client: TSQLRest): string;
|
||
var Value: RawUTF8;
|
||
Time: TTimeLogBits;
|
||
ref: RecordRef;
|
||
begin
|
||
Result := '';
|
||
if (Prop=nil) or (Instance=nil) then
|
||
exit;
|
||
Value := Prop.GetValue(Instance,false);
|
||
case Prop.SQLFieldType of
|
||
sftInteger, sftCurrency, sftFloat, sftUTF8Text, sftAnsiText:
|
||
result := UTF8ToString(Value);
|
||
sftDateTime, sftDateTimeMS:
|
||
result := DateTimeToText(Iso8601ToDateTime(Value));
|
||
sftTimeLog, sftModTime, sftCreateTime: begin
|
||
// need temp Iso to avoid URW699 with Delphi 6
|
||
Time.Value := GetInt64(pointer(Value));
|
||
result := DateTimeToText(Time);
|
||
end;
|
||
sftUnixTime: begin
|
||
Time.FromUnixTime(GetInt64(pointer(Value)));
|
||
result := DateTimeToText(Time);
|
||
end;
|
||
sftUnixMSTime:
|
||
result := DateTimeToText(UnixMSTimeToDateTime(GetInt64(pointer(Value))));
|
||
sftBoolean:
|
||
result := BooleanToString(boolean(GetInteger(pointer(Value))));
|
||
sftEnumerate:
|
||
result := (Prop as TSQLPropInfoRTTIEnum).EnumType^.GetCaption(Value);
|
||
sftSet:
|
||
result := (Prop as TSQLPropInfoRTTISet).SetEnumType^.GetCaptionStrings(@Value);
|
||
sftID:
|
||
if Client<>nil then
|
||
result := UTF8ToString(Client.MainFieldValue(
|
||
TSQLRecordClass((Prop as TSQLPropInfoRTTIID).ObjectClass),
|
||
GetInt64(pointer(Value)),true));
|
||
sftRecord: if Client<>nil then begin
|
||
SetID(pointer(Value),ref.Value);
|
||
result := UTF8ToString(Client.MainFieldValue(ref.Table(Client.Model),ref.ID,true));
|
||
if result='' then
|
||
result := Instance.CaptionName else
|
||
result := Instance.CaptionName+': '+result;
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
{$ifdef LVCL}
|
||
function DateTimeToIso(const DateTime: TDateTime; DateOnly: boolean): string;
|
||
var Time: TTimeLogBits;
|
||
begin // generic ISO date/time to text conversion
|
||
Iso.From(DateTime);
|
||
if DateOnly then
|
||
Int64Rec(Iso).Lo := Int64Rec(Iso).Lo and not(1 shl (6+6+5)-1);
|
||
result := Iso.Text(true,' ');
|
||
end;
|
||
{$else}
|
||
function DateTimeToIso(const DateTime: TDateTime; DateOnly: boolean): string;
|
||
const DATEFMT: array[boolean] of string = ('mmm dd, yyyy hh:mm am/pm','mmm dd, yyyy');
|
||
begin // generic US/English date/time to text conversion
|
||
DateTimeToString(Result, DATEFMT[DateOnly], DateTime);
|
||
end;
|
||
{$endif}
|
||
|
||
function TLanguageFile.DateToText(const DateTime: TDateTime): string;
|
||
begin
|
||
{$ifndef LVCL}if Self=nil then{$endif}
|
||
result := DateTimeToIso(DateTime,true)
|
||
{$ifndef LVCL} else
|
||
DateTimeToString(Result,DateFmt,DateTime);
|
||
{$endif}
|
||
end;
|
||
|
||
function TLanguageFile.DateToText(const Time: TTimeLogBits): string;
|
||
begin
|
||
{$ifndef LVCL}if Self=nil then{$endif}
|
||
result := DateTimeToIso(Time.ToDate,true)
|
||
{$ifndef LVCL} else
|
||
DateTimeToString(Result,DateFmt,Time.ToDate);
|
||
{$endif}
|
||
end;
|
||
|
||
function TLanguageFile.DateToText(const Time: TTimeLog): string;
|
||
begin
|
||
{$ifndef LVCL}if Self=nil then{$endif}
|
||
result := DateTimeToIso(TTimeLogBits(Time).ToDate,true)
|
||
{$ifndef LVCL} else
|
||
DateTimeToString(result,DateFmt,TTimeLogBits(Time).ToDate);
|
||
{$endif}
|
||
end;
|
||
|
||
function TLanguageFile.DateTimeToText(const DateTime: TDateTime): string;
|
||
begin
|
||
{$ifndef LVCL}if Self=nil then{$endif}
|
||
result := DateTimeToIso(DateTime,false)
|
||
{$ifndef LVCL} else
|
||
DateTimeToString(result, DateTimeFmt, DateTime);
|
||
{$endif}
|
||
end;
|
||
|
||
function TLanguageFile.DateTimeToText(const Time: TTimeLogBits): string;
|
||
begin
|
||
{$ifndef LVCL}if Self=nil then{$endif}
|
||
result := DateTimeToIso(Time.ToDateTime,false)
|
||
{$ifndef LVCL} else
|
||
DateTimeToString(result,DateTimeFmt,Time.ToDateTime);
|
||
{$endif}
|
||
end;
|
||
|
||
function TLanguageFile.DateTimeToText(const Time: TTimeLog): string;
|
||
begin
|
||
{$ifndef LVCL}if Self=nil then{$endif}
|
||
result := DateTimeToIso(TTimeLogBits(Time).ToDateTime,false)
|
||
{$ifndef LVCL} else
|
||
DateTimeToString(Result,DateTimeFmt,TTimeLogBits(Time).ToDateTime);
|
||
{$endif}
|
||
end;
|
||
|
||
function TLanguageFile.TimeToText(const DateTime: TDateTime): string;
|
||
begin
|
||
{$ifndef LVCL}if Self=nil then{$endif}
|
||
result := DateTimeToIso(DateTime,false)
|
||
{$ifndef LVCL} else
|
||
DateTimeToString(Result, TimeFmt, DateTime);
|
||
{$endif}
|
||
end;
|
||
|
||
function TLanguageFile.TimeToText(const Time: TTimeLogBits): string;
|
||
begin
|
||
{$ifndef LVCL}if Self=nil then{$endif}
|
||
result := DateTimeToIso(Time.ToTime,false)
|
||
{$ifndef LVCL} else
|
||
DateTimeToString(Result,TimeFmt,Time.ToTime);
|
||
{$endif}
|
||
end;
|
||
|
||
function TLanguageFile.TimeToText(const Time: TTimeLog): string;
|
||
begin
|
||
{$ifndef LVCL}if Self=nil then{$endif}
|
||
result := DateTimeToIso(TTimeLogBits(Time).ToTime,false)
|
||
{$ifndef LVCL} else
|
||
DateTimeToString(Result,TimeFmt,TTimeLogBits(Time).ToTime);
|
||
{$endif}
|
||
end;
|
||
{
|
||
procedure POExport(const SourceMsgPath, POFileName: TFileName; SourceLanguage: TLanguages);
|
||
var English, Source: TLanguageFile;
|
||
SourceDir: TFileName;
|
||
Dest: TFileStream;
|
||
W: TTextWriter;
|
||
i: integer;
|
||
E: string;
|
||
begin
|
||
SourceDir := SourceMsgPath;
|
||
if SourceDir='' then exit;
|
||
if SourceDir[length(SourceDir)]<>'\' then
|
||
SourceDir := SourceDir+'\';
|
||
Dest := TFileStream.Create(POFileName,fmCreate);
|
||
English := TLanguageFile.Create(SourceDir+TFileName(LanguageAbr[lngEnglish]+'.msg'),lngEnglish);
|
||
Source := TLanguageFile.Create(SourceDir+TFileName(LanguageAbr[SourceLanguage]+'.msg'),SourceLanguage);
|
||
W := TTextWriter.Create(Dest);
|
||
try
|
||
W.AddLine('"Content-Type: text/plain; charset=UTF-8\n"'#13#10+
|
||
'"Content-Transfer-Encoding: 8bit\n"'#13#10);
|
||
for i := 0 to English.Messages.Count - 1 do begin
|
||
E := English.Messages[i];
|
||
Source.Translate(E);
|
||
W.Add('msgid "%"'#13'msgstr"'#13#13, // #13 will be written as #13#10
|
||
[WinAnsiConvert.StringToUTF8(StringReplace(English.Messages[i],#13#10,'"'#13#10'"',[rfReplaceAll])),
|
||
Source.StringToUTF8(StringReplace(E,#13#10,'"'#13#10'"',[rfReplaceAll]))]);
|
||
end;
|
||
finally
|
||
W.Free;
|
||
Source.Free;
|
||
English.Free;
|
||
Dest.Free;
|
||
end;
|
||
end;
|
||
}
|
||
|
||
|
||
{ TLanguage }
|
||
|
||
function TLanguage.Abr: RawByteString;
|
||
begin
|
||
if Index=LANGUAGE_NONE then
|
||
result := '' else
|
||
result := LanguageAbr[Index];
|
||
end;
|
||
|
||
procedure TLanguage.Fill(Language: TLanguages);
|
||
begin
|
||
if Language=LANGUAGE_NONE then begin
|
||
Index := lngEnglish; // default language = english
|
||
CharSet := ANSI_CHARSET;
|
||
CodePage := CODEPAGE_US;
|
||
LCID := LCID_US;
|
||
end else begin
|
||
Index := Language;
|
||
CharSet := LanguageCharSet[Language];
|
||
CodePage := CharSetToCodePage(CharSet);
|
||
LCID := LanguageToLCID(Language);
|
||
end;
|
||
end;
|
||
|
||
function TLanguage.Name: string;
|
||
begin
|
||
result := LanguageName(Index);
|
||
end;
|
||
|
||
{$ifdef EXTRACTALLRESOURCES}
|
||
var
|
||
// expect english text, converted into WinAnsi before Hash32()
|
||
// - Delphi 2009 and up will do the implicit codepage conversion
|
||
// (useful for chars with unicode value >255, e.g. '<27>')
|
||
CB_EnumStrings: TWinAnsiDynArray;
|
||
/// number of items in CB_EnumStrings[]
|
||
CB_EnumStringsCount: integer;
|
||
// store the curently identified Hash32() of each english text
|
||
CB_Enum: TDynArrayHashed;
|
||
|
||
function Hash32Str(crc: cardinal; buf: PAnsiChar; len: cardinal): cardinal;
|
||
begin
|
||
result := Hash32(pointer(buf),len);
|
||
end;
|
||
|
||
function AddOnceDynArray(const S: WinAnsiString): integer;
|
||
var added: boolean;
|
||
begin
|
||
if (S='') or (S[1] in ['_','@']) then
|
||
// ignore text beginning with '_' or '@'
|
||
result := -1 else begin
|
||
result := CB_Enum.FindHashedForAdding(S,added);
|
||
if added then
|
||
CB_EnumStrings[result] := S else
|
||
if CB_EnumStrings[result]<>S then
|
||
assert(false,'Hash colision for "'+S+'" and "'+CB_EnumStrings[result]+'"');
|
||
end;
|
||
end;
|
||
|
||
{$I-}
|
||
// called within *A() Win32 API -> only english=Ansi text is expected here
|
||
function CB_EnumStringProc(hModule: THandle; lpszType, lpszName: PAnsiChar;
|
||
lParam: PtrInt): Boolean; stdcall;
|
||
var buf: array[0..4095] of AnsiChar;
|
||
s: WinAnsiString;
|
||
i: PtrInt;
|
||
begin
|
||
result := true;
|
||
if (PtrInt(lpszType)<>PtrInt(RT_STRING)) then exit;
|
||
i := (PtrInt(lpszName)-1)shl 4;
|
||
for i := i to i+15 do begin // resourcestrings are stored by groups of 16
|
||
SetString(s,buf,LoadStringA(hInstance,i,buf,sizeof(buf)));
|
||
if s='' then exit; // we reach the end
|
||
AddOnceDynArray(s);
|
||
end;
|
||
end;
|
||
|
||
// called within *A() Win32 API -> only english=Ansi text is expected here
|
||
function CB_EnumDFMProc(hModule: THandle; lpszType, lpszName: PAnsiChar;
|
||
lParam: PtrInt): Boolean; stdcall;
|
||
// code below use the string generic type, which is prefered for the RTTI
|
||
var F: ^Text absolute lparam;
|
||
Reader: TReader;
|
||
|
||
procedure ConvertObject(const ParentName, ObjectName: string);
|
||
procedure ConvertValue(const PropName, LastPropName: string);
|
||
procedure WriteProperty(const Value: WinAnsiString);
|
||
// for Delphi 2009 and up, Value: string was converted into a WinAnsiString
|
||
begin
|
||
// ignore components which names begin with '_'
|
||
if (PropName<>LastPropName) and (PropName<>'') then // PropName=Label1.Caption
|
||
if PropName[1]='_' then // ignore _Copyright.Caption
|
||
exit;
|
||
// write value
|
||
if (LastPropName='Caption') or (LastPropName='EditLabel.Caption') or
|
||
(LastPropName='Hint') or (LastPropName='EditLabel.Hint') or
|
||
(LastPropName='Title') or (LastPropName='Items') or
|
||
(LastPropName='DisplayLabel') then begin
|
||
Writeln(F^,PropName,'=_',Hash32(CB_EnumStrings[AddOnceDynArray(Value)]),
|
||
' ',Value); // add original caption for custom form translation
|
||
end;
|
||
end;
|
||
var I, Count: Integer;
|
||
aPropName, aSubPropName: string;
|
||
begin
|
||
case Reader.NextValue of
|
||
vaList:
|
||
begin
|
||
Reader.ReadValue;
|
||
I := 0;
|
||
while not Reader.EndOfList do begin
|
||
ConvertValue(PropName+'['+IntToStr(I)+']',LastPropName);
|
||
inc(I);
|
||
end;
|
||
Reader.ReadListEnd;
|
||
end;
|
||
vaInt8, vaInt16, vaInt32:
|
||
Reader.ReadInteger;
|
||
vaInt64:
|
||
Reader.ReadInt64;
|
||
vaExtended:
|
||
Reader.ReadFloat;
|
||
vaSingle:
|
||
Reader.ReadSingle;
|
||
vaCurrency:
|
||
Reader.ReadCurrency;
|
||
vaDate:
|
||
Reader.ReadDate;
|
||
{$ifdef UNICODE}
|
||
vaDouble:
|
||
Reader.ReadDouble;
|
||
vaWString, vaUTF8String:
|
||
WriteProperty(StringToWinAnsi(Reader.ReadString));
|
||
{$else}
|
||
vaWString, vaUTF8String:
|
||
WriteProperty(WideStringToWinAnsi(Reader.ReadWideString));
|
||
{$endif}
|
||
vaString, vaLString:
|
||
WriteProperty(StringToWinAnsi(Reader.ReadString));
|
||
vaIdent, vaFalse, vaTrue, vaNil, vaNull:
|
||
{ if (LastPropName='Font.Charset') then begin
|
||
s := Reader.ReadIdent;
|
||
if (s<>'DEFAULT_CHARSET') and (s<>'ANSI_CHARSET') then
|
||
Writeln(F^,PropName,'=',s);
|
||
end else}
|
||
Reader.ReadIdent;
|
||
vaBinary: begin
|
||
Reader.ReadValue;
|
||
Reader.Read(Count, SizeOf(Count));
|
||
Reader.Position := Reader.Position+Count;
|
||
end;
|
||
vaSet: begin
|
||
Reader.ReadValue;
|
||
repeat until Reader.ReadStr=''; // each ReadStr = one Set
|
||
end;
|
||
vaCollection:begin // same as TReader.ReadCollection()
|
||
Reader.ReadValue;
|
||
I := 0;
|
||
while not Reader.EndOfList do begin
|
||
if Reader.NextValue in [vaInt8, vaInt16, vaInt32] then
|
||
Reader.ReadInteger;
|
||
aPropName := PropName+'['+IntToStr(I)+'].';
|
||
inc(I);
|
||
Reader.ReadListBegin;
|
||
while not Reader.EndOfList do begin
|
||
aSubPropName := Reader.ReadStr;
|
||
ConvertValue(aPropName+aSubPropName,aSubPropName);
|
||
end;
|
||
Reader.ReadListEnd;
|
||
end;
|
||
Reader.ReadListEnd;
|
||
end;
|
||
else
|
||
assert(false,IntToStr(PtrInt(Reader.NextValue)));
|
||
end;
|
||
end;
|
||
var
|
||
Flags: TFilerFlags;
|
||
Position: Integer;
|
||
aObjectName, aClassName, aPropName: string;
|
||
begin // ConvertObject()
|
||
Reader.ReadPrefix(Flags, Position);
|
||
aClassName := Reader.ReadStr;
|
||
aObjectName := Reader.ReadStr;
|
||
if ObjectName='' then begin // first object = new TForm:
|
||
Writeln(F^,#13#10'[',aClassName,']');
|
||
while not Reader.EndOfList do begin
|
||
aPropName := Reader.ReadStr;
|
||
ConvertValue(aPropName,aPropName);
|
||
end;
|
||
Reader.ReadListEnd;
|
||
while not Reader.EndOfList do
|
||
ConvertObject('',aObjectName);
|
||
end else begin // not TForm components:
|
||
while not Reader.EndOfList do begin
|
||
aPropName := Reader.ReadStr;
|
||
if ((aPropName='Lines.Strings') {or (aPropName='Title.Text.Strings')}) and
|
||
(Reader.NextValue=vaList) then begin // TMemo, TRichEdit
|
||
Reader.ReadValue;
|
||
if aObjectName[1]='_' then begin // ignore _CompName component
|
||
while not Reader.EndOfList do
|
||
case Reader.NextValue of
|
||
{$ifdef UNICODE}
|
||
vaWString, vaUTF8String,
|
||
{$else}
|
||
vaWString, vaUTF8String: Reader.ReadWideString;
|
||
{$endif}
|
||
vaString, vaLString: Reader.ReadString;
|
||
else assert(false);
|
||
end;
|
||
end else begin
|
||
Write(F^,ParentName,aObjectName,'.Lines.Text=');
|
||
if not Reader.EndOfList then
|
||
repeat
|
||
case Reader.NextValue of
|
||
{$ifdef UNICODE}
|
||
vaWString, vaUTF8String,
|
||
{$else}
|
||
vaWString, vaUTF8String:
|
||
Write(F^,Reader.ReadWideString); // will do conversion into Ansi
|
||
{$endif}
|
||
vaString, vaLString:
|
||
Write(F^,Reader.ReadString);
|
||
else assert(false);
|
||
end;
|
||
if Reader.EndOfList then break;
|
||
Write(F^,'|~'); // = CRLF
|
||
until false;
|
||
Writeln(F^);
|
||
end;
|
||
Reader.ReadListEnd;
|
||
end else
|
||
if aPropName='Items.Strings' then // TRadioGroup, TComboBox, TlistBox
|
||
ConvertValue(ParentName+aObjectName+'.Items','Items') else
|
||
ConvertValue(ParentName+aObjectName+'.'+aPropName,aPropName);
|
||
end;
|
||
Reader.ReadListEnd;
|
||
if ffInline in Flags then
|
||
while not Reader.EndOfList do // TFrame: include Parent
|
||
ConvertObject(ParentName+aObjectName+'.',aObjectName) else
|
||
while not Reader.EndOfList do // normal objects are root (as TMenuItem)
|
||
ConvertObject('',aObjectName);
|
||
end;
|
||
Reader.ReadListEnd;
|
||
end;
|
||
|
||
var RS: TResourceStream;
|
||
Signature: cardinal;
|
||
begin
|
||
result := true;
|
||
if PtrInt(lpszType)<>PtrInt(RT_RCDATA) then
|
||
exit;
|
||
RS := TResourceStream.Create(HInstance, string(lpszName), RT_RCDATA);
|
||
try
|
||
if RS.Size<4 then
|
||
exit;
|
||
Reader := TReader.Create(RS, 4096);
|
||
try
|
||
Signature := 0;
|
||
Reader.Read(Signature,4);
|
||
if Signature=$30465054 then // 'TPF0' = DFM resources only
|
||
ConvertObject('','');
|
||
finally
|
||
Reader.Free;
|
||
end;
|
||
finally
|
||
RS.Free;
|
||
end;
|
||
end;
|
||
|
||
procedure ExtractAllResources(const EnumTypeInfo: array of pointer;
|
||
const Objects: array of TObject; const Records: array of TClass;
|
||
const CustomCaptions: array of WinAnsiString);
|
||
// save all forms and resourcestring of the current exe to a .messages file
|
||
// following the .msg format (winAnsi text file, since it should be in english)
|
||
var F: Text;
|
||
buf: RawByteString;
|
||
i, index, j: integer;
|
||
P: PPropInfo;
|
||
s: WinAnsiString;
|
||
ClassList: TList;
|
||
CT: TClass;
|
||
|
||
procedure AddEnum(T: PEnumType);
|
||
var index: integer;
|
||
begin
|
||
for index := T^.MinValue to T^.MaxValue do
|
||
AddOnceDynArray(StringToWinAnsi(T^.GetCaption(index)));
|
||
// for Delphi 2009 and up/XE: CaptionName converted into a WinAnsiString
|
||
end;
|
||
procedure AddClass(C: TClass);
|
||
var i: integer;
|
||
P: PPropInfo;
|
||
begin
|
||
if (C=nil) or (ClassList.IndexOf(C)>=0) then
|
||
exit; // already done or no RTTI information (e.g. reached TObject level)
|
||
ClassList.Add(C);
|
||
AddClass(GetClassParent(C)); // add parent properties first
|
||
for i := 1 to InternalClassPropInfo(C,P) do begin // add all field names
|
||
AddOnceDynArray(StringToWinAnsi(TSQLRecord.CaptionNameFromRTTI(@P^.Name)));
|
||
// for Delphi 2009 and up/XE: CaptionName converted into a WinAnsiString
|
||
with P^.PropType^^ do
|
||
case Kind of
|
||
tkClass: // add contained objects
|
||
AddClass(ClassType^.ClassType);
|
||
tkEnumeration: // add enumeration values
|
||
AddEnum(EnumBaseType);
|
||
tkSet:
|
||
AddEnum(SetEnumType);
|
||
end;
|
||
P := P^.Next;
|
||
end;
|
||
end;
|
||
|
||
begin
|
||
// all code below use *A() Win32 API -> only english=Ansi text is expected here
|
||
CB_Enum.Init(TypeInfo(TWinAnsiDynArray),CB_EnumStrings,nil,nil,Hash32Str,@CB_EnumStringsCount);
|
||
ClassList := TList.Create;
|
||
try
|
||
assign(F,ChangeFileExt(ExeVersion.ProgramFileName,'.messages'));
|
||
SetLength(buf,65536);
|
||
settextbuf(F,buf[1],length(buf));
|
||
Rewrite(F);
|
||
// add all resourcestring values
|
||
EnumResourceNamesA(HInstance,PAnsiChar(RT_STRING),@CB_EnumStringProc,0);
|
||
// add all enumerates captions
|
||
for i := 0 to high(EnumTypeInfo) do
|
||
AddEnum(PTypeInfo(EnumTypeInfo[i])^.EnumBaseType);
|
||
// add object instance captions
|
||
for i := 0 to high(Objects) do
|
||
if Objects[i].InheritsFrom(TSQLModel) then begin
|
||
AddOnceDynArray('ID'); // ID property is never published, but always here
|
||
// add custom captions for all tables of a database model
|
||
with TSQLModel(Objects[i]) do
|
||
for index := 0 to high(Tables) do
|
||
with Tables[index] do begin // TSQLRecord.CaptionName() may be overridden
|
||
AddOnceDynArray(StringToWinAnsi(CaptionName(nil))); // add table name
|
||
CT := Tables[index];
|
||
repeat
|
||
for j := 1 to InternalClassPropInfo(CT,P) do begin
|
||
// for Delphi 2009 and up, CaptionName(): string for safety
|
||
AddOnceDynArray(StringToWinAnsi(CaptionNameFromRTTI(@P^.Name)));
|
||
P := P^.Next;
|
||
end;
|
||
CT := GetClassParent(CT);
|
||
until CT=nil;
|
||
end;
|
||
end else
|
||
// add standard captions for all TPersistent published fields
|
||
if Objects[i].InheritsFrom(TPersistent) then
|
||
AddClass(Objects[i].ClassType);
|
||
// add standard captions for all published fields of these classes
|
||
for i := 0 to high(Records) do
|
||
AddClass(Records[i]);
|
||
// add custom captions
|
||
for i := 0 to high(CustomCaptions) do
|
||
AddOnceDynArray(CustomCaptions[i]);
|
||
// add form properties to be translated, with Property=Hash pairs
|
||
EnumResourceNamesA(HInstance, PAnsiChar(RT_RCDATA), @CB_EnumDFMProc, PtrInt(@F));
|
||
// create message list, with hash=value pairs
|
||
Writeln(F,#13#10'[Messages]');
|
||
for i := 0 to CB_EnumStringsCount-1 do begin
|
||
// CR/LF consistent replace
|
||
s := CB_EnumStrings[i];
|
||
for j := 1 to length(s) do
|
||
case s[j] of
|
||
#13: s[j] := '|';
|
||
#10: s[j] := '~'
|
||
end;
|
||
Writeln(F,Hash32(CB_EnumStrings[i]),'=',s);
|
||
end;
|
||
Close(F);
|
||
finally
|
||
ioresult;
|
||
ClassList.Free;
|
||
end;
|
||
end;
|
||
{$I+}
|
||
|
||
{$endif}
|
||
|
||
{var L,index: TLanguages;
|
||
initialization
|
||
AllocConsole;
|
||
for L := low(L) to high(L) do begin
|
||
index := TLanguages(LanguageAlpha[L]);
|
||
writeln(format('|%s|%s|%d|%s',
|
||
[LanguageName(index),LanguageAbr[index],CharSetToCodePage(LanguageCharSet[index]),
|
||
LanguageAbr[index]+'.msg']));
|
||
end;
|
||
readln;
|
||
//}
|
||
initialization
|
||
{$ifdef WITHUXTHEME}
|
||
// standard FormatSettings (US)
|
||
{$WARN SYMBOL_DEPRECATED OFF}
|
||
GetLocaleFormatSettings(LCID_US,SettingsUS);
|
||
{$endif}
|
||
// avoid call nil functions -> set default function to point to
|
||
i18nCompareStr := {$ifdef ENHANCEDRTL}CompareStr{$else}i18nInnerCompareStr{$endif};
|
||
move(NormToUpper,i18nToUpper,sizeof(NormToUpper));
|
||
move(NormToLower,i18nToLower,sizeof(NormToUpper));
|
||
i18nCompareText := i18nInnerCompareText;
|
||
{$ifndef ENHANCEDRTL}
|
||
RedirectCode(@System.LoadResString,@mORmoti18n.LoadResString,@BackupLoadResString);
|
||
InitializeCriticalSection(CacheResCriticalSection);
|
||
{$endif}
|
||
{$ifndef NOI18N}
|
||
LangInit; // do redirection + init user default locale (from Win32 or registry)
|
||
i18nDateText := Iso2S; // for SynCommons.pas unit
|
||
i18nDateTimeText := DateTime2S;
|
||
{$endif}
|
||
|
||
finalization
|
||
{$ifndef NOI18N}
|
||
FreeAndNil(Language);
|
||
{$ifdef USEFORMCREATEHOOK}
|
||
if OriginalForm[0]<>0 then begin
|
||
RedirectCodeRestore(@THookedForm.DoCreate,OriginalForm);
|
||
RedirectCodeRestore(@THookedFrame.Create,OriginalFrame);
|
||
end;
|
||
{$endif}
|
||
{$endif}
|
||
{$ifndef ENHANCEDRTL}
|
||
RedirectCodeRestore(@System.LoadResString,BackupLoadResString);
|
||
DeleteCriticalSection(CacheResCriticalSection);
|
||
{$endif}
|
||
end.
|
||
|
||
|