OJET: Internationalization - Localization

In this post I will bring my thoughs and first experience by making an Oracle JET Application ready for Multilanguage. Probably, not the best practices yet, but it is an initial step


My application (ojet-internationalization) can be found in:
https://github.com/DanielMerchan/ojet-examples


How to make your Oracle JET Application ready for Multilanguage?

This is well explained in the official documentation: https://docs.oracle.com/en/middleware/jet/6.1/develop/internationalizing-and-localizing-applications.html

 But, in this post I will bring what I did and my feelings.
  1. I created a Factory utility Module called Languages.js in js/utils. This factory contains an array of the supported languages in Oracle JET and some wrapper methods I used / un-used meanwhile my app was changing.

    setLocale: (newLocale, langCallback) => {
                console.log(`Old locale: ${Languages.getCurrentLocale()} and New Locale: ${newLocale}`);
                oj.Config.setLocale(newLocale, () => {
                    $('html').attr('lang', newLocale);
                    if (newLocale.startsWith('ar')) {
                        $('html').attr('dir', 'rtl');
                    } else {
                        $('html').attr('dir', 'ltr');
                    }
                    langCallback();
                    // Dispatch a JS Event to allow other modules to subscribe and change translations accordingly.
                    document.dispatchEvent(new CustomEvent('localeChangedEvent'));
                });
            },
  2. I added the utils/Languages to the define and function blocks of the appController.js to be able to use the Factory I just defined.

    define(['ojs/ojcore', 'knockout', 'ojs/ojmodule-element-utils', 'utils/Languages', 'ojs/ojarraydataprovider', 'ojs/ojlistdataproviderview', 'ojs/ojmodule-element', 'ojs/ojrouter', 'ojs/ojknockout', 'ojs/ojarraytabledatasource',
      'ojs/ojoffcanvas', 'ojs/ojselectcombobox'],
      function(oj, ko, moduleUtils, Languages, ArrayDataProvider, ListDataProviderView) {
  3. Following best practices, I created my Resource Bundle files under /resources/nls folder and updated the main.js to merge the Out-of-the-Box OJET Translations with my Custom Translations. I was dissapointed you can only register 1 Resource Bundle as clearly said in the official documentation. So, I recommend you structure your Resource Bundle by blocks as I did. Have a look into menu, footer, nav sections in mytranslations.js
    requirejs.config(
    {
      baseUrl: 'js',
    
      // Path mappings for the logical module names
      // Update the main-release-paths.json for release mode when updating the mappings
      paths:
      //injector:mainReleasePaths
      {
        'knockout': 'libs/knockout/knockout-3.4.2.debug',
        'jquery': 'libs/jquery/jquery-3.3.1',
        'jqueryui-amd': 'libs/jquery/jqueryui-amd-1.12.1',
        'promise': 'libs/es6-promise/es6-promise',
        'hammerjs': 'libs/hammer/hammer-2.0.8',
        'ojdnd': 'libs/dnd-polyfill/dnd-polyfill-1.0.0',
        'ojs': 'libs/oj/v6.1.0/debug',
        'ojL10n': 'libs/oj/v6.1.0/ojL10n',
        'ojtranslations': 'libs/oj/v6.1.0/resources',
        'text': 'libs/require/text',
        'signals': 'libs/js-signals/signals',
        'customElements': 'libs/webcomponents/custom-elements.min',
        'proj4': 'libs/proj4js/dist/proj4-src',
        'css': 'libs/require-css/css',
        'touchr': 'libs/touchr/touchr'    
      }
      //endinjector
      ,config: {
        ojL10n: {
          merge: {
            'ojtranslations/nls/ojtranslations': 'resources/nls/mytranslations'
          }
        }
      }


    define({
        "root": {
            "app": {
                "appName": "OJET Internationalization Example"
            },
            "nav": {
                "dashboard": "Dashboard",
                "incidents": "Incidents",
                "customers": "Customers",
                "about": "About"
            },
            "menu": {
                "languages": "Languages",
                "preferences": "Preferences",
                "help": "Help",
                "about": "About",
                "signOut": "Sign Out"
            },
            "footer": {
                "aboutOracle": "About Oracle",
                "contactUs": "Contact Us",
                "legalNotices": 'Legal Notices',
                "termsOfUse": "Terms of Use",
                "yourPrivacyRights": "Your Privacy Rights"
            }
        },
        "es": true
    });
  4. I added two functions in appController.js which are called initTranslations and refreshTranslations
    1.  The initTranslations is a function I always add in the modules (xx.js) to initate the initial value of our custom translations.
      self.initTranslations = () => {
              // Header
              // Application Name used in Branding Area
              self.appName = ko.observable(oj.Translations.getTranslatedString("app.appName"));
              // Navigation Labels
              self.dashboardMenuLabel = ko.observable(oj.Translations.getTranslatedString('nav.dashboard'));
              self.incidentsMenuLabel = ko.observable(oj.Translations.getTranslatedString('nav.incidents'));
              self.customersMenuLabel = ko.observable(oj.Translations.getTranslatedString('nav.customers'));
              self.aboutMenuLabel = ko.observable(oj.Translations.getTranslatedString('nav.about'));
              // Menu Labels
              self.preferencesLabel = ko.observable(oj.Translations.getTranslatedString('menu.preferences'));
              self.laguangesLabel = ko.observable(oj.Translations.getTranslatedString('menu.languages'));
              self.helpLabel = ko.observable(oj.Translations.getTranslatedString('menu.help'));
              self.aboutLabel = ko.observable(oj.Translations.getTranslatedString('menu.about'));
              self.signOutLabel = ko.observable(oj.Translations.getTranslatedString('menu.signOut'));
              // Footer Labels
              self.aboutOracleLabel = ko.observable(oj.Translations.getTranslatedString('footer.aboutOracle'));
              self.contactUsLabel = ko.observable(oj.Translations.getTranslatedString('footer.contactUs'));
              self.legalNoticesLabel = ko.observable(oj.Translations.getTranslatedString('footer.legalNotices'));
              self.termsofUseLabel = ko.observable(oj.Translations.getTranslatedString('footer.termsOfUse'));
              self.yourPrivacyRightsLabel = ko.observable(oj.Translations.getTranslatedString('footer.yourPrivacyRights'));
              console.log("AppController Translations initiated!");
            }
    2. The refreshTranslations is a function I always add in the modules (xx.js) to refresh the custom translations and add custom logic if necessary when a language changes.

      self.refreshTranslations = () => {
              // Header
              // Application Name used in Branding Area
              self.appName(oj.Translations.getTranslatedString("app.appName"));
              // Refresh Navigation Labels
              self.dashboardMenuLabel(oj.Translations.getTranslatedString('nav.dashboard'));
              self.incidentsMenuLabel(oj.Translations.getTranslatedString('nav.incidents'));
              self.customersMenuLabel(oj.Translations.getTranslatedString('nav.customers'));
              self.aboutMenuLabel(oj.Translations.getTranslatedString('nav.about'));
              // Refresh Labels
              self.preferencesLabel(oj.Translations.getTranslatedString('menu.preferences'));
              self.laguangesLabel(oj.Translations.getTranslatedString('menu.languages'));
              self.helpLabel(oj.Translations.getTranslatedString('menu.help'));
              self.aboutLabel(oj.Translations.getTranslatedString('menu.about'));
              self.signOutLabel(oj.Translations.getTranslatedString('menu.signOut'));
              // Refreshc Labels
              self.aboutOracleLabel(oj.Translations.getTranslatedString('footer.aboutOracle'));
              self.contactUsLabel(oj.Translations.getTranslatedString('footer.contactUs'));
              self.legalNoticesLabel(oj.Translations.getTranslatedString('footer.legalNotices'));
              self.termsofUseLabel(oj.Translations.getTranslatedString('footer.termsOfUse'));
              self.yourPrivacyRightsLabel(oj.Translations.getTranslatedString('footer.yourPrivacyRights'));
              console.log("AppController Translations refreshed");
            }
  5.  I added a oj-select as the language selector, but you can add whatever control you want in the index.html for managing the Language. The logic of the Localisation can be found in the setLangAction function of the appController.js
    // Change Language
          self.setLangAction = event => {    // Change Language Event
            const newLocale = event.target.value;
            Languages.setLocale(newLocale, self.refreshTranslations);
          }
  6. The code of setLangAction changes the Locale by using the utility wrapper of the Languages module (wraps the oj.Config.setLocale and other code) and also throws a custom event called localeChangedEvent. Other modules like dashboard.js, customers.js etc.. will need to listen to this event to invoke their owns refreshTranslations functions when the locale has changed. Remember that the Oracle JET components changes its language without any other extra code, but our custom translations needs to be refreshed.

    Languages.js
    setLocale: (newLocale, langCallback) => {
                console.log(`Old locale: ${Languages.getCurrentLocale()} and New Locale: ${newLocale}`);
                oj.Config.setLocale(newLocale, () => {
                    $('html').attr('lang', newLocale);
                    if (newLocale.startsWith('ar')) {
                        $('html').attr('dir', 'rtl');
                    } else {
                        $('html').attr('dir', 'ltr');
                    }
                    langCallback();
                    // Dispatch a JS Event to allow other modules to subscribe and change translations accordingly.
                    document.dispatchEvent(new CustomEvent('localeChangedEvent'));
                });
            },

    dashboard.js
    // Localisation
          self.initTranslations = () => {
            self.dashboardTitle = ko.observable(oj.Translations.getTranslatedString('dashboard.dashboardTitle'));
          };
    
          self.refreshTranslations = () => {
            self.dashboardTitle(oj.Translations.getTranslatedString('dashboard.dashboardTitle'));
          };
    
          self.initTranslations();
    
          // Listen to a Locale Change
          document.addEventListener("localeChangedEvent", function () {
            self.refreshTranslations();
          });
    

What was my feeling compared to Oracle ADF or just Java Server Faces?

I think is "too much" dirty code compared to a regular J2EE - JSF - ADF Application. In JSF / Oracle ADF you an easily split the Resource Bundle in multiple smaller files more maintainable and also requires less work on the side of the change of Localization.

Summary

This diagram describes how Multilanguage more or less works in Oracle JET.

Comments

Popular posts from this blog

OJET: Inter-Module communication in TypeScript Template

OJET: Build and Deploy in an Application Server

OJET: Select All options using only Checkboxset