Bill
Here is what I did. I'm not saying it's better or worse, it's just the
way I did it.
There is no need for separate builds for each language. First of all,
the application needs to read the default language upon startup. Each
language dictionary is stored in a separate table, containing two
columns: StringId (primary key) and Translation (Memo / Text). All the
dictionary tables have the same number of rows and the same content in
StringId. A typical string Id looks like this: "CancelWithHotkey" and
the corresponding memo field contains "\<Cancel". In other words, the
English language is considered to be a translation as well. The
application requests "CancelWithHotkey" string and the MessagingMgr
provides the string localized in whatever language is currently
selected. I'll come back to this later.
The Languages table definition:
Create Table classframeworkmetadata.languages(
Id Char (36),
LanguageId Varchar (10) Not Null,
Name Varchar (50) Not Null,
LanguageTable Varchar (50) Not Null,
CharSet Int (11) Not Null,
MessageBoxTable Varchar (50) Not Null,
RTL Tinyint (1) Not Null Default 0,
IsDefault Tinyint (1) Not Null Default 0,
IsTemplate Tinyint (1) Not Null Default 0,
Common Tinyint (1) Not Null Default 0,
Primary Key (Id)
)
Engine = Innodb
Avg_Row_Length = 8192
Character Set latin1
Collate latin1_swedish_ci;
Some records in this table:
4C5443D1-FDCB-4159-BBE6-F3B1B4FB47FF RO Românã messages_ro 238
messageboxes 0 0 0 1
92D7077E-F210-42CE-A49C-5527956C4CFB EN English messages_en 1
messageboxes 0 1 1 1
=====================================================
messages_en table:
Create Table classframeworkmetadata.messages_en(
Id Char (36),
StringId Varchar (150) Not Null,
Translation Text Not Null,
Common Tinyint (1) Not Null Default 0,
Primary Key (Id)
)
Engine = Innodb
Avg_Row_Length = 185
Character Set latin1
Collate latin1_swedish_ci;
Some records in this table:
00FD677F-5E34-4D13-BF83-15D65EE9AA55 AccessRightsActions Access rights
on actions 1
01D0E3C9-343B-477B-BDBB-C36EEF8C3582 CreateUpdate Create update 1
01EBAF3A-F1E2-4AA8-A34F-06A7124BAB32 ApplicationKey Application's key:
1
02394C54-FD98-4700-9FB6-F22371D42525 HashAlgorythm Hash algorythm: 1
0282C16F-F8B5-42EB-9DAB-462546075FBB CancelCaptionHotkey \<Cancel
1
033A80EC-A617-405C-AC57-674AB83B2F51 E-Mail E-mail: 1
033F669E-D8E9-499F-AB84-AD45E97F35BE RoleDescription Role description:
1
03BB2C1A-DB7D-4D03-82FE-6B00AA14521A LanguageInfoCouldNotBeSaved Language
info could not be saved. 1
043D422E-2F75-4F4F-A462-71D5F782CC74 NoItemFormDefined The form
'<<par1>>' could not be found. Please check the .ItemForm property of
the form. 1
04DADE1A-CF46-47EA-8673-05E59049FA95 Compare Compare 0
04F3407B-76B7-44ED-AB5C-F0193D02261D ValidConectionHandlesNeeded In
order to move to next step, please create the connections by clicking
"Connect" buttons. 0
050CDA9C-C9EA-4E9E-AA00-E9CE8E5BA4EF EditAction Edit action 1
05C6B467-7681-4F19-A6F8-71A700AE4A9F DockableHotKey Doc\<kable 1
068446C5-2FCC-453B-B798-84684B09950F SchemaComparison Schema
comparison 0
07140319-EA55-4446-BDA9-62EEF8132C7D FormsMgrInitialization FORMSMGR
INITIALIZATION 1
09D591A4-07F3-45BB-812F-2A6156E844DA TryExecuteSPT Trying executing
the SPT command named <<par1>>. DataController.Index = <<par2>>. 1
=================================================
messageboxes table:
Create Table classframeworkmetadata.messageboxes(
Id Char (36),
MessageBoxId Varchar (150) Not Null,
MessageTextId Varchar (150) Not Null,
DialogType Int (11) Not Null,
HeaderTextId Varchar (150) Not Null,
TimeOut Int (11) Not Null Default 0,
MessageBoxClass Varchar (100) Not Null,
MessageBoxClassLib Varchar (100) Not Null,
Common Tinyint (1) Not Null Default 0,
Primary Key (Id)
)
Engine = Innodb
Avg_Row_Length = 528
Character Set latin1
Collate latin1_swedish_ci;
Some records:
06886CAD-E9D2-439B-A72E-C756D3F255FB NoCommandForDatamgr
NoCommandForDatamgr 48 =_Screen.Caption 0 A_Message
A_MessageMgr 1
06D4AED1-4BBF-40C7-9748-41225149291D UserNotFoundInDatabase
UserNotFoundInDatabase 16 =_Screen.Caption 0 A_Message
A_MessageMgr 1
0977EACF-B462-40DA-A38C-89190A91996D ConnectionError
ErrorConnectingToDataSource 16 =_Screen.Caption 0
A_Message A_MessageMgr 1
0D467D90-2A68-4782-BCFD-1B0AAFEDFD2C PasswordChanged PasswordChanged 64
=_Screen.Caption 0 A_Message A_MessageMgr 1
0ED8F865-8A3F-486D-9B57-DB87B719EE4E UserCouldNotBeDeleted
UserCouldNotBeDeleted 48 =_Screen.Caption 0 A_Message
A_MessageMgr 1
===========================================
Ignore the "Common" field in above definitions. It is used internally
by my framework and doesn't affect the subject of this message.
The application reads the Languages table upon startup, then loads the
dictionary for the default language and the messageboxes definitions
in 2 collections. Also memorizes the charset and the RTL of the
language because it needs them later. There's one catch here though.
Tooltips don't have FontCharset and RTL properties, but there are two
SYS functiions: SYS(3007) and SYS(3009).
The messages collection is loaded with the following code:
.AddObject("Messages","Collection")
*-- Now add all the records found in .MessagesORM.Alias to the
collection:
Select (.MessagesORM.Alias)
Scan
Scatter Name oMessage Memo
.Messages.Add(oMessage, Upper(Alltrim(StringId)))
EndScan
llSuccess = .T.
Use In Select(.MessagesORM.Alias)
This way, MessageMgr.Messages["CancelHotKey"] is an object containing
both the StringId and the actual translation.
MessageMgr class exposes two methods: one returns a string and the
other one shows a messagebox. I am gonna paste them here:
**********************************************************************
* Program....: B-MessageMgr.GetMessage
* Version....:
* Author.....: Grigore Dolghin
* Date.......: 08 November 2008, 22:44:51
* Notice.....: Copyright © 2008, Class Software.
* Compiler...: Visual FoxPro 09.00.0000.5815 for Windows
* Abstract...:
* Changes....: Grigore Dolghin, Created 08 November 2008 / 22:44:51
* Parameters.:
* Called by..:
* Purpose....: Returns a message from .Messages collection.
* ...........: TextMerge() is used to insert the parameters (if any).
**********************************************************************
Lparameters tcMessageId As String, ;
Par1 As Variant, ;
Par2 As Variant, ;
Par3 As Variant, ;
Par4 As Variant, ;
Par5 As Variant, ;
Par6 As Variant, ;
Par7 As Variant, ;
Par8 As Variant, ;
Par9 As Variant, ;
Par10 As Variant
Local lcTranslatedString As String, ;
lcReturnValue As String
With This
Try
lcTranslatedString = .Messages(Upper(tcMessageId)).Translation
lcReturnValue = Textmerge(lcTranslatedString, .T.)
Catch To loError When loError.ErrorNo = 2061
lcReturnValue = "!!! String not found: " + tcMessageId
Catch To loError When loError.ErrorNo = 1943
lcReturnValue = "NoMessagesLoaded"
Catch To loError
lcReturnValue = loError.Message
EndTry
EndWith
Return lcReturnValue
A typical call of this is: ?
App.MessageMgr.GetMessage("CancelHotKey"). It will return the
localized string for that StringId from the currently loaded
dictionary.
Messageboxes collection is loaded with a similar mechanism. A typical
call is App.MessageMgr.Messagebox("UserNotFoundInDatabase"). The
Messagebox method will extract the corresponding item from collection,
will call GetMessage() method passing as parameter the MessageTextId
value, then show the messagebox. This allows the application to show
different messageboxes for each language, containing different
strings, if required. I haven't had yet the need for that, but anyway,
I'm ready for that if needed.
==========================================================
About the actual translation of the interface:
Each class has a number of custom properties, named like this:
CaptionStringId, ToolTipTextStringId, StatusbarTextStringId (these
mirror the real properties. If a class doesn't have a caption, it
doesn't have the CaptionStringId property either). If these are empty,
no localization is applied to that specific instance of the class. If
they contain something, the object tries to localize itself upon
instantiation.
This is the code in Label.Init() class:
With This
.SetAccessLevel()
.SetVistaFonts()
.Localize()
EndWith
The .Localize() method:
With This
If .Localizable && that's right, you can disable the localization for
a specific instance, if needed
._SetFontCharset()
._SetCaption()
._SetToolTipText()
._SetStatusBarText()
EndIf
EndWith
The _Set######## methods:
*_SetFontCharSet
With This
Try
*-- A TRY...CATCH is required because not all fonts support
charsets.
*-- The App.LanguageMgr.Language.Record.Charset contains the
charset
of the currently selected language. 1 = Western; 238 = Central
European; and so on.
.FontCharSet = App.LanguageMgr.Language.Record.Charset
Catch
EndTry
EndWith
* _SetCaption
Local lcString As String
With This
If .Localizable And Not Empty(.CaptionStringId)
lcString =
App.MessageMgr.GetMessage(Transform(.CaptionStringId))
If lcString <> "NoMessagesLoaded" && something bad happened to
Messages collection. Fall back to value set in design mode.
.Caption = lcString
EndIf
EndIf
EndWith
*_SetTooltipText
Local lcString As String
With This
If .Localizable And Not Empty(.ToolTipTextStringId)
lcString =
App.MessageMgr.GetMessage(Transform(.ToolTipTextStringId))
If lcString <> "NoMessagesLoaded"
.ToolTipText = lcString
EndIf
EndIf
EndWith
*_SetStatusbarText
Local lcString As String
With This
If .Localizable And Not Empty(.StatusBarTextStringId)
lcString =
App.MessageMgr.GetMessage(Transform(.StatusBarTextStringId))
If lcString <> "NoMessagesLoaded"
.StatusBarText = lcString
EndIf
EndIf
EndWith
-----------------------------------------------
Bottom line: you drop two buttons on a form. The caption is set in
design mode to "\<Save" and "\<Cancel". Run the thing, it works. The
CaptionStringId is set to "SaveHotKey" and "CancelHotKey", and
.Localizable property is set to .T. Run the app, select English, run
the form, the buttons will call .GetMessage() to obtain the
translation. Change the language, they'll do the same thing. If you
change the language while the form is running, the form class provides
a mechanism that iterates through all controls and calls their
Localize() method.
No need for builds at all - everything is in memory. And if soemthing
goes wrong, you still have the old design mode strings.
On Tue, Feb 2, 2010 at 5:52 PM, Bill Arnold
<[email protected]> wrote:
> Grigore,
>
> I would like to create a Spanish version of an application at some point, so
> I appreciate hearing of your experience.
>
> Trying to summarize the overall approach, it seems that the process should
> inventory all visible text used in an application's UI in a table with it's
> translated counterpart in 'x' number of target languages (columns). I'd
> imagine running the text through a standard dictionary translator would
> catch easy cases and save translator time, and then let the translator go
> through each screen and override cases in need of change, updating the
> translations table for builds to use going forward.
>
> At build time, steps of the process would create working folders for each
> different language build, apply the translations, run the compiles, maybe
> make code page changes, and generate a set of builds, one for each language.
>
> Is this basically consistant with your approach?
>
>
> Bill
>
_______________________________________________
Post Messages to: [email protected]
Subscription Maintenance: http://leafe.com/mailman/listinfo/profox
OT-free version of this list: http://leafe.com/mailman/listinfo/profoxtech
Searchable Archive: http://leafe.com/archives/search/profox
This message:
http://leafe.com/archives/byMID/profox/[email protected]
** All postings, unless explicitly stated otherwise, are the opinions of the
author, and do not constitute legal or medical advice. This statement is added
to the messages for those lawyers who are too stupid to see the obvious.