xtool/contrib/mORMot/SQLite3/mORMoti18n.pas

2599 lines
95 KiB
ObjectPascal
Raw Permalink Blame History

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