B4J Tutorial [ABMaterial] 1.09 Localization using dynamic pages

This is the second part of the tutorial on some big changes I'm making to ABMaterial in 1.09. It should be fully backwards compatible so no (or very, very little) changes will be needed from your side on your existing projects if you stay working the pre-1.09 way.

Read Part 1 as it gives a introduction on dynamic pages:
https://www.b4x.com/android/forum/threads/abmaterial-1-09-all-about-flexibility.65819/

PART 2: Localization
-------------------
If you live in a country like me (Belgium), when developing an app you are immidiately confronted with the fact you have to write your app for different languages. This topic was raised in the forum too.

I've created a framework within ABMaterial to cope with this and some helpful methods to do your translation.

As described in Part 1, dynamic pages was a must-have if you want to do localization. You can of course run 3 versions of your app, but that is not the ABMaterial way ;).

So let's go over the code steps we need to do to localize an ABMaterial web app.

Steps while you are programming:
1. For every term you want to be translatable, you'll use the page.XTR("Code", "DefaultText") method.

"code": I've found it useful to use a number, like '0001', '0002' etc. This maps the value between languages. Note that you can re-use codes on a per/page base. Meaning you can use a '0001' code in a page and a '0001' code in a module, having a different value. This makes it easier to code as you don't have to remember what the last code was you used throught the whole program, just per class, module etc...

"defaulttext": This has a double purpose. First, it makes your code still 'readable' as a coder. It looks very similar as if you used the string itself. Second it handles as a backup if it does not find a translation for this code.

Examples:
B4X:
page.NavigationBar.AddSideBarItem("Contacten", page.XTR("0001" , "Contacten" ), "mdi-action-dashboard", "../StartPage")

combo1.AddItem("combo1S0", page.XTR("0005","Alles"), BuildSimpleItem("S0", "mdi-action-account-circle", "{NBSP}{NBSP}" & page.XTR("0005","Alles")))

page.ShowToast("toast" & myToastId, "toastred", page.XTR("0014","Geen referenties gevonden"), 5000)

2. Done! :) Now, every time you run your WebApp in the IDE, your B4J code will be analysed and a couple of files will be generated:

a. BaseTranslations.lng
This file contains all the terms that need a translation. Here is an example of part of an extracted file for one of my own projects:
B4X:
abmshared_0001;Contacten
abmshared_0001;Referenties
abmshared_0003;Agenda
abmshared_0004;Prospecten in de buurt
agendapage_0001;Agenda
agendapage_0002;Ontgrendeld
agendapage_0003;Vergrendeld
prospectpage_0001;Prospecten
prospectpage_0002;Rond huidige locatie
prospectpage_0003;Rond adres
prospectpage_0004;Volledig adres
prospectpage_0061;Kies een sector
prospectpage_0005;Alles
prospectpage_0006;Hout
prospectpage_0007;Groen
prospectpage_0008;All Energy
prospectpage_0009;Andere
prospectpage_0010;Bouw
prospectpage_0011;Inrichting
prospectpage_0012;Productie
prospectpage_0013;Zoek
prospectpage_0014;Geen referenties gevonden
prospectpage_0062;Contact
prospectpage_0015;Adres
prospectpage_0016;Sector
prospectpage_0017;Tel
prospectpage_0018;GSM
prospectpage_0019;Email
prospectpage_0022;Verkoper
prospectpage_0020;Geen referenties gevonden (GPS?)
prospectpage_0021;Connectie verloren. Herlaad de lijst
...

Pretty nice,no?

The observant reader will have noticed there are two abmshared_0001 codes. This is where the second file comes in:

b. BaseTranslations.optimizable
This file contains an error check and some hints on how you can optimize your translations.

The frist part shows you some optimizations per/page. Meaning it will not report if you've used two times 'ja' accross pages. I had to find a balance between speedy coding and cross page term use. I choose for coding speed. In the example, codes 0028 and 0024 both say 'Onderwerp' in the prospectpage, so we can actually re-use 0024 instead of 0028.

The second part will show where you have made an error. You're using the same code for two different terms.

Here is an example of such a file:
B4X:
[Suggestions where you can re-use existing translations]
prospectpage_0028 'Onderwerp' -> prospectpage_0024
prospectpage_0029 'Commentaar' -> prospectpage_0025
prospectpage_0036 'Sluiten' -> prospectpage_0026
prospectpage_0047 'Sluiten' -> prospectpage_0026
prospectpage_0048 'Onderwerp' -> prospectpage_0024
prospectpage_0050 'Commentaar' -> prospectpage_0025
prospectpage_0051 'Annuleren' -> prospectpage_0030
prospectpage_0052 'Bewaren' -> prospectpage_0031
prospectpage_0054 'Ja' -> prospectpage_0033
prospectpage_0055 'Nee' -> prospectpage_0034
prospectpage_0056 'Gelieve eerst alle velden in te vullen!' -> prospectpage_0035
prospectpage_0057 'Sluiten' -> prospectpage_0026
prospectpage_0059 'Ja' -> prospectpage_0033
prospectpage_0060 'Nee' -> prospectpage_0034
[Duplicate use of the same translation code withing a B4J file]
abmshared_0001 used for both: 'Contacten' AND 'Referenties'

'Looks nice Alain, but how the hell do we use it?', I hear you ask...

Well, once you have created your program for one language, you can start thinking about translating it.

You can give BaseTranslations.lng to a translator. e.g. Dutch->English. Once he returns it, create a folder 'translations' in your /www/appName/ folder en put them there. Use ISO language codes. In our case we'll have two files:

'BaseTranslations.lng' becomes 'nl.lng'
'TranslatorsFile.lng' becomes 'en.lng'

Time for some coding again:

1. In each Page_Create() put the following code BEFORE the ConnectPage() method (see part 1 of the tutorial):
B4X:
' NEW 1.09 has to be done on every page
    page.SetAcceptedLanguages(Array As String("en", "fr", "de", "nl"), "en")
    Dim ActiveFoundLanguage As String = page.DetectLanguage(ws.UpgradeRequest.GetHeader("Accept-Language"))
    ABM.LoadTranslations(File.DirApp & "/www/" & AppName & "/translations/")
    page.SetActiveLanguage(ActiveFoundLanguage,  "" )

The first line tells ABMaterial the languages you have translations for, and a default language to use if no translation file was found for that language.

Page.DetectLanguage will try to find the language of the browser of the user. If it is not one of the AcceptedLanguages, it will return the default one.

The third line loads all your translations

The last line sets the app in a language. You can use the one returned from DetectLanguage, or use one depending on e.g. the user that logged in.

Notes:
a.
You'll notice a second empty param in the page.SetActiveLanguage(ActiveFoundLanguage, "" ). This is usefull if you want to have seperate translations for e.g. different clients. Some clients call an person 'Person', others 'Employee'. Using this extra param you can use a different translation.

I give an example:
en.lng:
abmshared_0001;Contacts
abmshared_0002;References
abmshared_0003;Agenda
abmshared_0004;Neighborhood Prospects

You can create a file en_clientx.lng that contains:
abmshared_0003;Calendar

The 'clientx' part is the param you can pass into SetActiveLanguage().

How it works:
If the extra param is used, ABMaterial will first look into this file. If found it will use it. If not it will fall back to the default value.

example:
page.XTR("0001", "Contacts") will return 'Contacts' in both cases.
page.XTR("0003", "Agenda") will return "Agenda" if no extra param was passed, but "Calendar" if 'clientx' was passed.

This has a lot of potential use if you create a WebApp for different clients with different needs. It looks like you made a customized app especially for them, although all you had to do was create a new .lng file.

b. You may want to put this code in Websocket_Connected in case of the ABMApplication (where you login). That way you can present the login page in the language of the users browser. In all cases, it has to come BEFORE the ConnectPage() method where you create your components.

And basically this is it. You can see how little code is needed to localize you WebApp with ABMaterial!

And the best part: I've created the same system for defaults!
e.g. one client wants articleX as default value for a combo, clientY wants another default value for a combo. It works very similar: you use page.XDF("code", "DefaultValue"). It generates 'BaseDefaults.def' and 'BaseDefaults.optimizable'. You put them in /www/defaults/, load them with LoadDefaults(). Use page.SetActiveDefaults("defcode", "extraparam" ). Exactly the same story... :)

EXTRA FOR DONATORS ONLY
I've created a couple of very helpful methods to test your app in a different language using machine translation, but I'll talk about this more in a seperate article as this tutorial is already long enough.

Cheers,

Alain



 

killiak

Member
Licensed User
Longtime User
I not sure i quite follow....but look promising... Thx
 

clarionero

Active Member
Licensed User
Longtime User
Hi. It's working very well :)

What is the correct codification (UTF-8, ANSI,...) for the traduction files?. In spanish the especial characters are bad showed. For example:

"Nuestras Imágenes" <==> "Nuestras Imágenes"

Notepad++ said me the codificacion file is UTF-8

Thank you


Rubén Sánchez Peña
 

alwaysbusy

Expert
Licensed User
Longtime User
My encoding is:
upload_2017-1-4_9-20-25.png


It showed ok:

upload_2017-1-4_9-23-22.png
 

clarionero

Active Member
Licensed User
Longtime User

Thank you Allain. I will check my server configuration. All looks fine in my W10 laptop, but the issue is in a english Windows 2003 server when i show spanish text. The issue is only with the traslated texts. The special characters in normal text are right displayed.

caracter.jpg


Edit: Another issue. In mobile devices Google Chrome identify the language page like English ever. Maybe because the HTML generated for pages begin with <html lang="en">.

Thank you for your excellent suport.


Rubén Sánchez Peña
 
Last edited:

mindful

Active Member
Licensed User
' NEW 1.09 has to be done on every page
page.SetAcceptedLanguages(Array As String("en", "fr", "de", "nl"), "en")
Dim ActiveFoundLanguage As String = page.DetectLanguage(ws.UpgradeRequest.GetHeader("Accept-Language"))
ABM.LoadTranslations(
File.DirApp & "/www/" & AppName & "/translations/")
page.SetActiveLanguage(ActiveFoundLanguage, "" )

Do we really need to call ABM.LoadTranslations in every page ? I am calling it from the Initialize method in ABMApplication and I see the same result - my pages are translated...
 

mindful

Active Member
Licensed User
Have you defined ABM in a shared module, or private in the page?
I have defined ABM as private in ABMShared and also all of my pages, including ABMApplication. I forgot to place ABM.LoadTranslations in one page of my project and when I ran it it was translated. Then I removed all ABM.LoadTranslations from all my pages and add it only in Initialize method of ABMApplication and my pages translate correctly. This is why I asked if we really need to call ABM.LoadTranslations in every page before page.SetActiveLanguage ...
 

Cableguy

Expert
Licensed User
Longtime User
A word to the ones less aware like me...
when run from the IDE (under Windows) this line is incorrect:
B4X:
 ABM.LoadTranslations(File.DirApp & "/www/" & AppName & "/translations/")
and should be
B4X:
 ABM.LoadTranslations(File.DirApp & "\www\" & AppName & "\translations\")

The best is to put in a conditional #If debug for testing purposes
 

mindful

Active Member
Licensed User
@Cableguy you should use File.Combine and let java decide on which platform you run ...
B4X:
ABM.LoadTranslations(File.Combine(File.Combine(File.Combine(File.DirApp, "www"), AppName), "translations"))
 

Cableguy

Expert
Licensed User
Longtime User
@Cableguy you should use File.Combine and let java decide on which platform you run ...
B4X:
ABM.LoadTranslations(File.Combine(File.Combine(File.Combine(File.DirApp, "www"), AppName), "translations"))
this should/could have been discussed earlier, lol
 

billyrudi

Active Member
Licensed User
Longtime User
Hi Alain,
there is a method to set language by code?
do you have a small example?
regards Paolo
 

Cableguy

Expert
Licensed User
Longtime User
B4X:
    #Region Check the Browser Language
    page.SetAcceptedLanguages(Array As String("en", "de", "it", "fr", "pt"), "en")
    BrowserBaseLanguage = page.DetectLanguage(ws.UpgradeRequest.GetHeader("Accept-Language"))
    #End Region
   
    #Region Load the translations files
    ABM.LoadTranslations(File.Combine(File.Combine(File.Combine(File.DirApp, "www"), "web"), "translations"))
    page.SetActiveLanguage(BrowserBaseLanguage, "" )
    Log("BrowserBaseLanguage : " & BrowserBaseLanguage)
    #End Region

In the code above, if you use page.SetActiveLanguage("es","") the page will load and show all text from the es.lng file
 
Top