In that post B4Multilingual I have started to describe a Windows program (B4Multingual) that I have been developing to maintain multilingual B4A and B4J applications. As that post is mainly about the tool itself and is bound to grow bigger as documentation gets gradually added to it, I thought that a different post, which deals exclusively with the localization aspect and the B4(A/J) library was warranted. You may use the library (source code will be released at a later date when comments and source have been tidied up) without the tool, but B4Multingual offers some real advantages.
There are a few excellent implementations of localization developed by fellows BA coders on this forum, in particular:
RiverRaid's AHATranslate class and corwin42's AHLocale library. Warwound offers a library too and a short and concise tutorial you can find here.
You might actually be using one of them and be very happy with it. I was. So why did I create a new library?
I wanted, as part of my toolchain, a solution that would allow me to work on multilingual applications with the following criteria in mind:
Points 3, 4 & 5 are taken care of by the code generated by the tool and the set of classes (library for now) that wrap some of the Android classes.
Point 6 is yet to be implemented, but the foundations are already in place in the Windows app.
Point 7 will be addressed as I keep developing this post. The no java rule has not been put forward out of dislike for java, but because I find it quite a chore to have to keep switching editors (one for java and one for B4(A/J)) when developing. Not only that, but the hassle of compiling libraries outside the BA editor is a pain. I wished for in-situ java, but I don't see that happening anytime soon. The consequence of this choice is that the BAJLocale library is 99% reflection based.
The BAJLocale library wraps the following Android classes:
AJLocale -> Locale
AJCurrency -> Currency
AJDateFormat -> DateFormat
AJDate -> Date
AJCalendar -> Calendar
AJSimpleDateFormat -> SimpleDateFormat
AJDateFormatSymbols -> DateFormatSymbols
AJSimpleDateFormat -> SimpleDateFormat
And adds 2 helper classes AJResources and AJHelper
It aims at providing localization to B4A and B4J applications.
If you are still reading this post after this long preamble, here's some meatier stuff.
I have attached a BA demo that showcases some of the possibilities of this library. Before running it, you'll need to deploy the library (jar + xml) into your shared library folder. The demo itself is in essence based, with permission, on corwin42's own demo. Though the demo GUI is essentially the same and his, I have added a few views and the code and library are original.
The localization business is encapsulated in the class generated by B4Multingual and the attached BAJLocale library.
First declare an object of type Tr (or whatever you will call the class).
Then initialize it and assign values to the relevant views:
As you can tell, it's extremely simple and easy on the eye and the fingers. You can shorten the object name further (T for example) if you wish.
NB: The translations have been generated with Bing Web Service (using B4Multingual) and may not be accurate. Actually, some are definitely wrong, but I decided to leave them in as is, as a reminder that automated translation lacks context and needs to be manually checked.
If you are dealing with arrays of strings, say planets, the syntax is as concise and easy to follow:
A few comments about the generated class (Tr) which is shown further below. The Class_Globals sub declares a few objects that map the Android java classes as faithfully as possible. These objects are not really used in the class itself, but are there as a convenient way of getting localization related stuff in one class. So for instance, if you wish to display all localized months in a spinner for the current locale say, you could do it this way:
It looks a bit of a mouthful. It's on purpose as I like to know which Android function I am calling from b4(A/J) code. If you don't like this, you can edit the generated code (Tr class) and add a new sub that'll hide the hideous details:
And the call from the activity would look like this:
Which might be more palatable.
I will (at a later date) add an option in BAJMultilingual to do just that automatically.
Another feature is the presence of multiple Initializers. One of the disadvantages of developing classes in B4(A/J) instead of Java, is the restriction to only one Initializer, as per the documentation and confirmed by Erel's statement. Sure the initializer may accept arguments as well, but at the end, there can be only one initializer per class. Fortunately, there are various ways of lifting that restriction. One is demonstrated in the code below:
Most of the time you'll only want to initialize a Tr object by calling Initialize as it will keep in synch with the device current chosen locale and your chosen default language. The default language is the one which is used when no translations are available. However, you may want to decide to use a different default language, such as French instead of English say. In this particular case you would initialize the object this way:
To allow this to happen in a pure BA class you must ensure that your extra initializers call B4A's private method innerInitialize.This is taken care of by the sub InternalInitialize that makes a reflection call to do just that.
Therefore by calling InternalInitialize at the beginning of your extra initializers you get the benefit of multiple initializers without having to create your libraries in Java. (It's possible to call your own class Initializer as well and therefore benefit from multiple initializers. I'll describe a method later on.)
In a future post, there will be further commenting on the generated and the the AJLocale class will be described in detail.
Generated code
Edit: 7/12/2015
This library is not maintained here anymore. Go there for the latest version.
There are a few excellent implementations of localization developed by fellows BA coders on this forum, in particular:
RiverRaid's AHATranslate class and corwin42's AHLocale library. Warwound offers a library too and a short and concise tutorial you can find here.
You might actually be using one of them and be very happy with it. I was. So why did I create a new library?
I wanted, as part of my toolchain, a solution that would allow me to work on multilingual applications with the following criteria in mind:
- It should necessitate little or no coding.
- The translations should be easy to maintain.
- The text assignments should be as clear and concise as possible
- The implementation should be able to cope with ad hoc translations and localization (Java i18n)
- The implementation should cope with B4A and B4J applications transparently.
- Allow for scaffolding (i.e. to a quickly set up skeleton for an app.)
- The code should all be written in BA (no java) and the source available freely.
Points 3, 4 & 5 are taken care of by the code generated by the tool and the set of classes (library for now) that wrap some of the Android classes.
Point 6 is yet to be implemented, but the foundations are already in place in the Windows app.
Point 7 will be addressed as I keep developing this post. The no java rule has not been put forward out of dislike for java, but because I find it quite a chore to have to keep switching editors (one for java and one for B4(A/J)) when developing. Not only that, but the hassle of compiling libraries outside the BA editor is a pain. I wished for in-situ java, but I don't see that happening anytime soon. The consequence of this choice is that the BAJLocale library is 99% reflection based.
The BAJLocale library wraps the following Android classes:
AJLocale -> Locale
AJCurrency -> Currency
AJDateFormat -> DateFormat
AJDate -> Date
AJCalendar -> Calendar
AJSimpleDateFormat -> SimpleDateFormat
AJDateFormatSymbols -> DateFormatSymbols
AJSimpleDateFormat -> SimpleDateFormat
And adds 2 helper classes AJResources and AJHelper
It aims at providing localization to B4A and B4J applications.
If you are still reading this post after this long preamble, here's some meatier stuff.
I have attached a BA demo that showcases some of the possibilities of this library. Before running it, you'll need to deploy the library (jar + xml) into your shared library folder. The demo itself is in essence based, with permission, on corwin42's own demo. Though the demo GUI is essentially the same and his, I have added a few views and the code and library are original.
The localization business is encapsulated in the class generated by B4Multingual and the attached BAJLocale library.
First declare an object of type Tr (or whatever you will call the class).
B4X:
Sub Globals
Private oTr As Tr
'...
End Sub
Then initialize it and assign values to the relevant views:
B4X:
Sub Activity_Create(FirstTime As Boolean)
oTr.Initialize
'...
Activity.Title = oTr.title
lbCurrency.Text = oTr.currency
lbFirstDayOfWeek.Text = oTr.first_week_day
lbCurrentLanguage.Text = oTr.current_language
btParameter.Text = oTr.trans_with_var_values
End Sub
NB: The translations have been generated with Bing Web Service (using B4Multingual) and may not be accurate. Actually, some are definitely wrong, but I decided to leave them in as is, as a reminder that automated translation lacks context and needs to be manually checked.
If you are dealing with arrays of strings, say planets, the syntax is as concise and easy to follow:
B4X:
'Fill spinner view with localized planet values
spPlanets.Clear
'
For Each planet In oTr.planets_Map.Values
spPlanets.Add(planet)
Next
A few comments about the generated class (Tr) which is shown further below. The Class_Globals sub declares a few objects that map the Android java classes as faithfully as possible. These objects are not really used in the class itself, but are there as a convenient way of getting localization related stuff in one class. So for instance, if you wish to display all localized months in a spinner for the current locale say, you could do it this way:
B4X:
'Fill spinner view with localized month values
spMonths.Clear
For Each month In oTr.AJCalendar.GetDisplayNames(oTr.AJCalendar.MONTH, oTr.AJCalendar.LONG, oTr.AJLocale.Locale)
spMonths.Add(month)
Next
It looks a bit of a mouthful. It's on purpose as I like to know which Android function I am calling from b4(A/J) code. If you don't like this, you can edit the generated code (Tr class) and add a new sub that'll hide the hideous details:
B4X:
Public Sub getLongMonths As List
Return oAJCalendar.GetDisplayNames(oAJCalendar.MONTH, oAJCalendar.LONG, oAJLocale.Locale)
End Sub
And the call from the activity would look like this:
B4X:
For Each month In oTr.LongMonths
spMonths.Add(month)
Next
I will (at a later date) add an option in BAJMultilingual to do just that automatically.
Another feature is the presence of multiple Initializers. One of the disadvantages of developing classes in B4(A/J) instead of Java, is the restriction to only one Initializer, as per the documentation and confirmed by Erel's statement. Sure the initializer may accept arguments as well, but at the end, there can be only one initializer per class. Fortunately, there are various ways of lifting that restriction. One is demonstrated in the code below:
Most of the time you'll only want to initialize a Tr object by calling Initialize as it will keep in synch with the device current chosen locale and your chosen default language. The default language is the one which is used when no translations are available. However, you may want to decide to use a different default language, such as French instead of English say. In this particular case you would initialize the object this way:
B4X:
oTr.Initialize2("fr")
B4X:
Private Sub InternalInitialize
Dim r As Reflector
r.Target = Me
r.Runmethod4("innerInitialize", Array As Object(r.GetProcessBA("main")), Array As String("anywheresoftware.b4a.BA"))
End Sub
B4X:
Public Sub Initialize
PopulateTranslate
oAJLocale.InitializeDefault
SetHelperClasses(oAJLocale.Locale)
default_language_translation_ = EN
End Sub
Public Sub Initialize2(default_language_translation As String)
InternalInitialize
'
PopulateTranslate
oAJLocale.Initialize(default_language_translation)
SetHelperClasses(oAJLocale.Locale)
default_language_translation_ = default_language_translation
End Sub
In a future post, there will be further commenting on the generated and the the AJLocale class will be described in detail.
Generated code
B4X:
'B4Multingual B4A/B4J Generated Code
Private Sub Class_Globals
Private translationMap As Map
Private oAJLocale As AJLocale
Private oAJDateFormatSymbols As AJDateFormatSymbols
Private oAJCalendar As AJCalendar
Private oAJCurrency As AJCurrency
Private oAJDateFormat As AJDateFormat
Private default_language_translation_ As String
'
#Region varSYMBOLS
Private const clubs_ As Int = 4
Private const currency_ As Int = 5
Private const currency_symbol_ As Int = 6
Private const current_language_ As Int = 7
Private const diamonds_ As Int = 10
Private const first_week_day_ As Int = 12
Private const hearts_ As Int = 13
Private const planets_ As Int = 23
Private const select_lang_ As Int = 29
Private const select_month_ As Int = 30
Private const select_weekday_ As Int = 31
Private const spades_ As Int = 39
Private const suits_ As Int = 42
Private const title_ As Int = 46
Private const trans_with_var_values_ As Int = 47
Private const variable_message_ As Int = 48
Private const car_ As Int = 49
Private const planets__ As Int = 51
#End Region varSYMBOLS
#Region varLOCALES
Private const EN As String = "en" 'English:Default Language
Private const FR As String = "fr" 'French
Private const IT As String = "it" 'Italian
#End Region varLOCALES
End Sub
#Region Initialize Methods
Private Sub InternalInitialize
Dim r As Reflector
r.Target = Me
r.Runmethod4("innerInitialize", Array As Object(r.GetProcessBA("main")), Array As String("anywheresoftware.b4a.BA"))
End Sub
Public Sub Initialize
PopulateTranslate
oAJLocale.InitializeDefault
SetHelperClasses(oAJLocale.Locale)
default_language_translation_ = EN
End Sub
Public Sub Initialize2(default_language_translation As String)
InternalInitialize
'
PopulateTranslate
oAJLocale.Initialize(default_language_translation)
SetHelperClasses(oAJLocale.Locale)
default_language_translation_ = default_language_translation
End Sub
Private Sub SetHelperClasses(Locale As JavaObject)
oAJDateFormatSymbols.Initialize2(Locale)
oAJCalendar.GetInstance2(Locale)
oAJDateFormat.Initialize
oAJDateFormat.GetDateInstance3(oAJDateFormat.DEFAULT, Locale)
oAJDateFormat.SetCalendar(oAJCalendar.Calendar)
Try
oAJCurrency.GetInstance(Locale)
Catch
Log(LastException)
End Try
End Sub
#End Region Initialize Methods
#Region LOCALES
Public Sub getEnglish As String
Return EN
End Sub
Public Sub getFrench As String
Return FR
End Sub
Public Sub getItalian As String
Return IT
End Sub
#End Region LOCALES
#Region KEYS
Public Sub getclubs As String
Return Get_(clubs_)
End Sub
Public Sub getcurrency As String
Return Get_(currency_)
End Sub
Public Sub getcurrency_symbol As String
Return Get_(currency_symbol_)
End Sub
Public Sub getcurrent_language As String
Return Get_(current_language_)
End Sub
Public Sub getdiamonds As String
Return Get_(diamonds_)
End Sub
Public Sub getfirst_week_day As String
Return Get_(first_week_day_)
End Sub
Public Sub gethearts As String
Return Get_(hearts_)
End Sub
Public Sub getplanets As String
Return Get_(planets_)
End Sub
Public Sub getselect_lang As String
Return Get_(select_lang_)
End Sub
Public Sub getselect_month As String
Return Get_(select_month_)
End Sub
Public Sub getselect_weekday As String
Return Get_(select_weekday_)
End Sub
Public Sub getspades As String
Return Get_(spades_)
End Sub
Public Sub getsuits As String
Return Get_(suits_)
End Sub
Public Sub gettitle As String
Return Get_(title_)
End Sub
Public Sub gettrans_with_var_values As String
Return Get_(trans_with_var_values_)
End Sub
Public Sub getvariable_message As String
Return Get_(variable_message_)
End Sub
Public Sub getcarMap As Map
Return GetMap_(car_)
End Sub
Public Sub getplanets_Map As Map
Return GetMap_(planets__)
End Sub
#End Region KEYS
#Region Public Methods
Public Sub getAJLocale As AJLocale
Return oAJLocale
End Sub
Public Sub setAJLocale(AJLocale As AJLocale)
oAJLocale = AJLocale
SetHelperClasses(oAJLocale.Locale)
End Sub
Public Sub getAJDateFormatSymbols As AJDateFormatSymbols
Return oAJDateFormatSymbols
End Sub
Public Sub getAJCalendar As AJCalendar
Return oAJCalendar
End Sub
Public Sub getAJCurrency As AJCurrency
Return oAJCurrency
End Sub
Public Sub setAJCurrency(AJCurrency As AJCurrency)
oAJCurrency = AJCurrency
End Sub
Public Sub getAJDateFormat As AJDateFormat
Return oAJDateFormat
End Sub
Private Sub Get_(symbol As Int) As String
Return Get(symbol, oAJLocale.GetLanguage)
End Sub
Public Sub Get(symbol As Int, lang As String) As String
Dim line_map As Map
line_map = translationMap.Get(symbol)
Return(line_map.GetDefault(lang, line_map.Get(default_language_translation_)))
End Sub
Private Sub GetMap_(symbol As Int) As Map
Return GetMap(symbol, oAJLocale.GetLanguage)
End Sub
Public Sub GetMap(symbol As Int, lang As String) As Map
Dim lines_map As Map
lines_map = translationMap.Get(symbol)
Return lines_map.GetDefault(lang, lines_map.Get(default_language_translation_))
End Sub
#End Region Public Methods
Private Sub PopulateTranslate
Dim line_map As Map
Dim lines_map As Map
translationMap.Initialize
'4: CLUBS
line_map.Initialize
line_map.Put(EN,"Clubs")
line_map.Put(FR,"Trèfle")
line_map.Put(IT,"Club")
translationMap.Put(clubs_, line_map)
'5: CURRENCY
line_map.Initialize
line_map.Put(EN,"Currency")
line_map.Put(FR,"Devise")
line_map.Put(IT,"Valuta")
translationMap.Put(currency_, line_map)
'6: CURRENCY_SYMBOL
line_map.Initialize
line_map.Put(EN,"Currency Symbol")
line_map.Put(FR,"Symbole monétaire")
line_map.Put(IT,"Simbolo di valuta")
translationMap.Put(currency_symbol_, line_map)
'7: CURRENT_LANGUAGE
line_map.Initialize
line_map.Put(EN,"Current Language")
line_map.Put(FR,"Langage courant")
line_map.Put(IT,"Lingua corrente")
translationMap.Put(current_language_, line_map)
'10: DIAMONDS
line_map.Initialize
line_map.Put(EN,"Diamonds")
line_map.Put(FR,"Diamants")
line_map.Put(IT,"Diamanti")
translationMap.Put(diamonds_, line_map)
'12: FIRST_WEEK_DAY
line_map.Initialize
line_map.Put(EN,"First day of week")
line_map.Put(FR,"Premier jour de la semaine")
line_map.Put(IT,"Primo giorno della settimana")
translationMap.Put(first_week_day_, line_map)
'13: HEARTS
line_map.Initialize
line_map.Put(EN,"Hearts")
line_map.Put(FR,"Coeurs")
line_map.Put(IT,"Cuori")
translationMap.Put(hearts_, line_map)
'23: PLANETS
line_map.Initialize
line_map.Put(EN,"Planets")
line_map.Put(FR,"Planètes")
line_map.Put(IT,"Pianeti")
translationMap.Put(planets_, line_map)
'29: SELECT_LANG
line_map.Initialize
line_map.Put(EN,"Select Language")
line_map.Put(FR,"Sélectionner une langue")
line_map.Put(IT,"Seleziona lingua")
translationMap.Put(select_lang_, line_map)
'30: SELECT_MONTH
line_map.Initialize
line_map.Put(EN,"Select Month")
line_map.Put(FR,"Sélectionnez le mois")
line_map.Put(IT,"Selezionare il mese")
translationMap.Put(select_month_, line_map)
'31: SELECT_WEEKDAY
line_map.Initialize
line_map.Put(EN,"Select Weekday")
line_map.Put(FR,"Sélectionnez le jour")
line_map.Put(IT,"Selezionare il giorno")
translationMap.Put(select_weekday_, line_map)
'39: SPADES
line_map.Initialize
line_map.Put(EN,"Spades")
line_map.Put(FR,"Pique")
line_map.Put(IT,"Picche")
translationMap.Put(spades_, line_map)
'42: SUITS
line_map.Initialize
line_map.Put(EN,"Pick a Suit:")
line_map.Put(FR,"Choisir une couleur:")
line_map.Put(IT,"Scegliere un colore:")
translationMap.Put(suits_, line_map)
'46: TITLE
line_map.Initialize
line_map.Put(EN,"Title")
line_map.Put(FR,"Titre")
line_map.Put(IT,"Titolo")
translationMap.Put(title_, line_map)
'47: TRANS_WITH_VAR_VALUES
line_map.Initialize
line_map.Put(EN,"Translation with variable values")
line_map.Put(FR,"Traduction avec les valeurs des variables")
line_map.Put(IT,"Traduzione con i valori delle variabili")
translationMap.Put(trans_with_var_values_, line_map)
'48: VARIABLE_MESSAGE
line_map.Initialize
line_map.Put(EN,"You are in %1$s and your Currency is %2$s")
line_map.Put(FR,"Vous êtes à %1$s et votre monnaie est %2$s")
line_map.Put(IT,"Sei in %1$s e la valuta è %2$s")
translationMap.Put(variable_message_, line_map)
'49: CARDS (string-srray)
lines_map.Initialize
line_map.Initialize
line_map.Put(0, "Ace")
line_map.Put(1, "King")
line_map.Put(2, "Queen")
line_map.Put(3, "Jack")
lines_map.Put(EN, line_map)
line_map.Initialize
line_map.Put(0, "ACE")
line_map.Put(1, "King")
line_map.Put(2, "Reine")
line_map.Put(3, "Jack")
lines_map.Put(FR, line_map)
'50: PLANETS_ (string-srray)
lines_map.Initialize
line_map.Initialize
line_map.Put(0, "Mercury")
line_map.Put(1, "Venus")
line_map.Put(2, "Earth")
line_map.Put(3, "Mars")
line_map.Put(4, "Jupiter")
line_map.Put(5, "Saturn")
line_map.Put(6, "Uranus")
line_map.Put(7, "Neptune")
lines_map.Put(EN, line_map)
line_map.Initialize
line_map.Put(0, "Mercure")
line_map.Put(1, "Venus")
line_map.Put(2, "Terre")
line_map.Put(3, "Mars")
line_map.Put(4, "Jupiter")
line_map.Put(5, "Saturn")
line_map.Put(6, "Uranus")
line_map.Put(7, "Neptune")
lines_map.Put(FR, line_map)
line_map.Initialize
line_map.Put(0, "Mercurio")
line_map.Put(1, "Venus")
line_map.Put(2, "Terra")
line_map.Put(3, "Marzo")
line_map.Put(4, "Giove")
line_map.Put(5, "Saturno")
line_map.Put(6, "Urano")
line_map.Put(7, "Nettuno")
lines_map.Put(IT, line_map)
translationMap.Put(planets__, lines_map)
'51: CAR (plurals-array)
lines_map.Initialize
line_map.Initialize
line_map.Put("one", "car")
line_map.Put("other", "cars")
lines_map.Put(EN, line_map)
line_map.Initialize
line_map.Put("one", "voiture")
line_map.Put("other", "voitures")
lines_map.Put(FR, line_map)
line_map.Initialize
line_map.Put("one", "auto")
line_map.Put("other", "Automobili")
lines_map.Put(IT, line_map)
translationMap.Put(car_, lines_map)
End Sub
This library is not maintained here anymore. Go there for the latest version.
Last edited: