Tuesday 28 May 2013

Part 5: Adding multi lingual support

This is a repost of my old tutorial I wrote in late 2007. I'm presenting them as they where.

Now that our application is still small its easy to add multi language support into it. I always suggest doing this when you start building your application, not later down the line.
For multi lingual support Omnis internally uses string tables. String table translate a label to the current language in the background so you can build your application using the labels. As the string table is stored separately from your library you can have someone translate the labels while you continue working on your application.
Well be using Omnis string table editor to create our string table. This is a fine tool to use when youre a single developer but when youre working in a team you may want to take a different approach. Ive build my own version of the string table editor that uses a database backend so a team of developers can maintain a single collection of translations and build string tables from there. However I would probably not go down that route today but instead let each developer maintain their own IDs and merge string tables together.
For our tutorial well use a single string table file, that is enough to show you how it works. If you have a big application, or think your application may become big in the future, I do advise you to create multiple string table files. What I do and what works fine for me is that I place all the string tables I need in a subfolder and when my library loads, I load all string tables within that subfolder into memory using the string table file name as a guide.
The reason I suggest using multiple string tables is two fold:
1) very very big string tables are hard to maintain and can have a performance penalty
2) you can add string tables with translations that only apply for a certain customer or module
You can also choose not to use string table files but store the translations separately. For instance the string table module contains functions from loading translations from a list that you can build from a database. But Ill leave that up to your own imagination.
You can find the string table editor by going to Tools->Add Ons->String Table Editor. Each string table has an "ID" column and one or more language columns. You can open my example string table and take a look at its contents. There are translations in there for 3 languages at the moment.
The ID column is whats important to our application. If you use a single string table you can use this ID field directly but if you have multiple string tables loaded, its smart to prefix the ID with the string table name when using it in your application. So a ID called "Username" you would use as "demolib.Username" in the application. I always prefix it because I may be using a single string table today, but maybe tomorrow I will add a second or third.
Now that we have created our string table we need to load it into memory to be able to use it. Add the following code to your startup_task:

Startup_Task.$reloadStringTable()
----
;  Get the location of our library, our string table should be in the same directory
Split path name (sys(10),tmpDrive,tmpDir) 

;  Get the current selected language (if we are reloading)
Calculate tmpColumn as StringTable.$getcolumnnumber(demolib)
If tmpColumn=-22     ;; No columns set?
Calculate tmpColumn as 2     ;; 2 is the first language!
End If

;  Unload string table just in case
Do StringTable.$unloadstringtable(demolib)

;  Now load our string table
Do StringTable.$loadstringtable(demolib,con(tmpDrive,tmpDir,demolib.stb))

;  Make the language current, note that if you have multiple string tables you need to do this for each one!
Do StringTable.$setcolumn(con(demolib.,tmpColumn))

We need to call this method from our startup tasks $construct. Note that its called $reloadStringTable, not $loadStringTable and that the code allows you to call it at a later time again. As youre developing youll be altering the string table and you will want to load the changes into memory without restarting your whole application. Adding a menu item or toolbar button that is only available during development and calls $ctask.$reloadStringTable will give you the ability to quickly load the updated version of the string table.
Most of the time you will be using the string table to translate labels on your window. For this Raining Data has created a nifty little background object called a string label field. Its in the last pane of your component store. Well start by making our logon window multi lingual and replace all our label fields with string label fields.
Remove the username label from the logon window and drag a string label field onto the logon window and place it where our username used to be. Then rightclick on the string label and select properties, the property manager should popup.
In the property manager there is a new tab called "Custom". In this tab there is a property called rowid. In this you should put the ID of the string table, type in "demolib.Username".
Note that the text isnt translated until runtime.
Also assign CtrlLabel to its $fieldstyle (note, in early versions of the string label $fieldstyle didnt work and you had to set all the properties manually!).
Now do the same for the password and hostname fields.
We also need to do the logon button. Now there is no string table version of the pushbutton, instead well need to handle the translation ourselves. This is easy. Select the $text property of the logon pushbutton and clear it, now press F9.
Select the StringTable pane in the catalog and doubleclick the logon label. Omnis will insert the correct text, all we need to do is place this text between brackets so $text becomes: [StringTable.$gettext("demolib.Logon")]
We will do the same with the window title. I have a standard that I always name my label the same as my window, so wLogon.$title becomes: [StringTable.$gettext("demolib.wLogon")].
Now when you open the logon window it should show all the labels in Dutch. Since Dutch is my first column in my StringTable, it has become my default.
However we want the user to select a different language. You might want to make this part of your applications configuration or maybe store it along with the user details and not set it until after the user logs on. For our tutorial well make the language selectable by the user on the logon window as that is the first window the user sees. Well add a dropdown list field to our logon window and add the following two methods to it:

wLogon.iLanguageList kList

wLogon.iLanguageList.$construct
----
;  the stringtable framework should always be installed so we can use that..
Calculate tmpCurrentColumn as StringTable.$getcolumnnumber(demolib)

Do iLanguageList.$define()
Do iLanguageList.$cols.$add("Description",kCharacter,0,250)

For tmpColNumber from 2 to StringTable.$colcnt(demolib) step 1
  ;  Make the column current so we can get the column name!
  Do StringTable.$setcolumn(con(demolib.,tmpColNumber))
  Calculate tmpColName as StringTable.$getcolumnname(demolib)
  Do iLanguageList.$add(tmpColName)
End For

;  Change it back to what it was
Do StringTable.$setcolumn(con(demolib.,tmpCurrentColumn))
Calculate iLanguageList.$line as tmpCurrentColumn-1

wLogon.iLanguageList.$event
-----
On evClick     ;; Event Parameters - pRow ( Itemreference )
  Do StringTable.$setcolumn(con(demolib.,iLanguageList.$line+1))
  Do StringTable.$redraw($cwind.$hwnd)
  Do $cinst.$redraw(kTrue,kTrue)
  ;  Title is not reset automatically....
  Calculate $cinst.$title as StringTable.$gettext(demolib.wLogon)

Now when you open the logon window you should be able to select from the 3 languages and the interface should change itself.
So resume:
  • Use string labels for your labels and they will automatically translate themselves
  • Use the method stringtable.$gettext(stringtable.label), between [ and ] where needed, to translate on the fly
  • Always prefix your labels with the name of your stringtable.
Two last things that remain to be said:
  • The standard $gettext is fun, but I always implement a Startup_task.$gettext(pLabel, p1, p2, p3, ... p9) method that translates the label pLabel and then does a replaceall function for each specified parameter. This is great for translating error messages. My error label "mandatoryError" translates to "%1 is a mandatory field". Not I can simply do a $ctask.$gettext("mandatoryError", $ctask.$gettext("Username")) and it will come back with a nicely translated "Username is a mandatory field". Ill leave it up to you to make a nice one for yourself.
  • I dont particularly like the placement of some of my routines. Proper OO programming, if there is such a thing, would dictate that I would subclass the StringTable object and implement a $reloadStringTable, $buildLangList, $setLanguage, $getLanguage and $gettext (overridden) method in there with my logic. But I can not subclass from the StringTable object in the way that I want and so I generally dont bother because its only the $gettext that I frequently use. Still it may be nicer to create a single object that encapsulates this logic so its in one place and then call the logic from the places its needed.
Thanks to Rainer R. Greim for the German translations.
----
Disclaimer: Im sure some of the listers will recognize their ideas in this tutorial in one form or another. Some ideas are truly mine, some are inspired by what Ive learned in over 10 years of conferencing and reading the list. It would be an impossible task for me to list the many many people who I owe thanks to. Im sure you know who you are my friends. I take only credit for taking the time to put this series together. Use the code you find in this tutorials and the library/libraries attached freely and as you see fit. I take no responsibility and will not be liable for any damages resulting directly or indirectly from using the information I present here.

No comments:

Post a Comment