Monday, May 18, 2015

ADF BC i18n - Text Translation

A common request on web applications is internationalization (i18n), specially the translation of text and data. In this post we are going to see how you can work with text translation within the ADF BC module of an ADF application and how to avoid common pitfalls.

The samples showed in this post are based on the HR schema, specifically, on the Regions table.

In order to provide text translation we first configure the ADF BC project Resource Bundle: Right click on project -> Project Properties



From the above image:
  • Automatically Synchronize Bundle: Select to automatically create text resources in the resource bundle when editing UI components in the visual editor.
  • Warn About Hard-coded Translatable Strings: Select to display a message when a UI component being edited in the visual editor has an associated translatable string.
  • One Bundle Per Project or Per File: This configuration depends on your project. I like one bundle per project because most of my projects are developed in modules and every module is small, so one bundle is enough.
  • Resource Bundle Type: Properties Bundle is perhaps the most common type. List Resource Bundle may be difficult to edit and may cause problems when you have a lot of translatable text. Xliff is only important if you are using the same type in other Oracle applications.
After you are done with the previous configuration, you can create your Entities, View Objects, Properties Sets... etc. And when you edit a translatable text, it will be added automatically to the resource bundle.

Lets create an Entity based on the Regions table and add a new column called Description of type String:



Lets modify the Control Hints of each attribute, specifically, lets change the Label Text property. Here we are using the Spanish translation for the name attribute of a region:



After doing this for each attribute, we can inspect the entity's XML source code and notice the following:

1. The attribute definition now has a Properties section where the key identifier for the attribute in the resource bundle is defined:



2. Also, at the end of the XML file, the Entity's resource bundle definition is defined:



If we open the above mentioned resource bundle file, we'll see something similar to this (we have added the Description_TXT key for later use):



Now that we have a base resource bundle, lets create the English translation. Right click on the container package of our resource bundle and select New, in the popup under General, select File. Enter the same base name of our resource bundle and append an underscore and the code of the target language, in this case English is en. Copy the same keys but provide the text translated to English:



Recommendation: If you are going to edit the resource bundle files, try to do it using JDeveloper option Edit Resource Bundles... under Application menu, since some characters are written to the file in UNICODE. For example, you can see that in ModelBundle.properties Spanish accents and special characters are written in UNICODE.



We can be more specific, for example, we can have a file for US English (ModelBundle_en_US.properties) and a file for UK English (ModelBundle_en_UK.properties) but the general one works for now.

Lets test what we have done so far. Create a default view object for the Regions entity and add it to an Application Module, then run the Application Module. Follow my previous post about the ADF BC Tester Locale Configuration if you want to be able to change the language from the ADF BC Tester application.





You can notice that when we change the locale, the Label Text property of each attribute changes properly.

Now lets try something more programmatic. Lets return the text for the Description attribute from our resource bundle. Open the Regions entity and under the Java section, click on the pencil icon and select the Generate Entity Object Class option and the Accessors option:



This will create the RegionsImpl class where we can modify the text for the Description attribute. In the RegionsImpl class, modify the getDescription method to one of the following


1. Use the oracle.jbo.common.StringManager to access to a hardcoded resource bundle:
...
//In RegionsImpl

    /**
     * Gets the attribute value for Description, using the alias name Description.
     * @return the Description
     */
    public String getDescription() {
        Locale locale = getDBTransaction().getSession().getLocale();
        return StringManager.getLocalizedString("com.javanme.model.res.ModelBundle",
                                                "Description_TXT", 
                                                null,
                                                locale,
                                                null, 
                                                false);
    }
...


2. Use the oracle.jbo.common.StringManager class using the resource bundle definition of the Entity/ViewObject:
...
//In RegionsImpl

    /**
     * Gets the attribute value for Description, using the alias name Description.
     * @return the Description
     */
    public String getDescription() {
        Locale locale = getDBTransaction().getSession().getLocale();
        return StringManager.
               getLocalizedStringFromResourceDef(getResourceBundleDef(),
                                                 "Description_TXT",
                                                 null,
                                                 locale,
                                                 null, 
                                                 false);
    }
...


3. Use the java.util.ResourceBundle class to access to a hardcoded resource bundle:
...
//In RegionsImpl

    /**
     * Gets the attribute value for Description, using the alias name Description.
     * @return the Description
     */
    public String getDescription() {
        
        ResourceBundle res =
            ResourceBundle.getBundle("com.javanme.model.res.ModelBundle",
                                     getDBTransaction().getSession().getLocale());
        return res.getString("Description_TXT");
        
    }
...


Using any of the above, we'll get the translated text for the Description attribute:





Following are my notes about the above strategies:

Strategy 1
Uses the locale of the current DB Transaction. This is how you access the current locale in ADF BC without breaking the MVC architecture. Then uses the class oracle.jbo.common.StringManager, which is a singleton, in order to get a localized String from a Resource Bundle. Notice that this strategy uses a hardcoded resource bundle. I'd use this strategy if the Entity/ViewObject does not have defined a resource bundle in its XML file.
You can learn more about the StringManager class in the API docs.

Strategy 2
Uses the locale of the current DB Transaction. This is how you access the current locale in ADF BC without breaking the MVC architecture. Then uses a class oracle.jbo.common.StringManager, which is a singleton, in order to get a localized String from the resource bundle defined in the XML file of the Entity/ViewObject. I'd use this strategy if the Entity/ViewObject does have defined a resource bundle in its XML file. This is my preferred strategy.
You can learn more about the StringManager class in the API docs.

Strategy 3
Uses the locale of the current DB Transaction. This is how you access the current locale in ADF BC without breaking the MVC architecture. Then uses the class java.util.ResourceBundle in order to get a localized String. Notice that this strategy uses a hardcoded resource bundle. I would not use this strategy in ADF BC since it requires an extra configuration step which will be discussed in a moment.

Strategies 1 and 2 are almost the same, the only difference is that one of them (2) uses the definition of the Entity's or ViewObject's resource bundle. 
They both make use of the oracle.jbo.common.StringManager class, a singleton, for accessing localized Strings. Don't be afraid to use this class in your accessors methods, it won't create more than one instance and it will take care of the default locale of the JVM (this is really important! as we'll see next). Being part of the ADF framework, this is what the framework uses and recommends.

Strategy 3 is a tricky one and it will look OK if we test it from the ADF BC Tester, but it may not work properly (I mean it won't localize our text properly) if, for example, we run an ADF Faces page that access our Regions Entity/ViewObject.
If a resource bundle for the specified Locale does not exist, getBundle tries to find the closest match. In our example, if we are looking for the English translation (ModelBundle_en.properties) and the default locale (Locale.getDefault()) is es_CO getBundle will look for the resources in the following order:
  • ModelBundle_en.properties -> Because this is what we are looking for -> Found!!
  • ModelBundle_es_CO.properties -> Because this is Locale.getDefault()
  • ModelBundle_es.properties -> Variant of Locale.getDefault()
  • ModelBundle.properties -> When none of the above are found

The above will work properly and the correct ModelBundle_en.properties file will be read. Now let's try this: We need the Spanish translation (ModelBundle.properties in our example) and Locale.getDefault() returns en_US, getBundle will look for the resources in the following order:
  • ModelBundle_es.properties  -> Because this is what we are looking for -> Not Found!
  • ModelBundle_en_US.properties -> Because this is Locale.getDefault() -> Not Found!
  • ModelBundle_en.properties -> Variant of Locale.getDefault() -> Found!!
  • ModelBundle.properties -> When none of the above are found

You can notice that the java.util.ResourceBundle class won't get my ModelBundle.properties file but the ModelBundle_en.properties file, because the latter is found first.

This is why it is so tricky, if you don't have your resources named accordingly to their languages or if the JVM default locale (Locale.getDefault()) is different than your default language, it may not work properly. This is a common pitfall...

So after all this, I hope you consider using the oracle.jbo.common.StringManager class over the java.util.ResourceBundle class when working with resource bundles in your ADF applications.

see ya!


References


About the ResourceBundle Class. Oracle [online].
Available on Internet: https://docs.oracle.com/javase/tutorial/i18n/resbundle/concept.html
[accessed on May 17 2015].

Class StringManager. Oracle [online].
Available on Internet: https://docs.oracle.com/cd/E28389_01/apirefs.1111/e10653/oracle/jbo/common/StringManager.html
[accessed on May 16 2015].

Binaries. Jobinesh's blog [online].
Available on Internet: http://www.jobinesh.com/2010/06/accessing-resource-bundle-from.html
[accessed on May 14 2015].