13 Internationalize and Localize Oracle JET Apps

Oracle JET includes support for internationalization (I18N), localization (L10N), and use of Oracle National Language Support (NLS) translation bundles in Oracle JET apps. Configure your Oracle JET app so that the app can be used in a variety of locales and international user environments.

About Internationalizing and Localizing Oracle JET Apps

Internationalization (I18N) is the process of designing software so that it can be adapted to various languages and regions easily, cost effectively, and, in particular, without engineering changes to the software. Localization (L10N) is the use of locale-specific language and constructs at runtime.

Oracle has adopted the industry standards for I18N and L10N such as World Wide Web Consortium (W3C) recommendations, Unicode technologies, and Internet Engineering Task Force (IETF) specifications to enable support for the various languages, writing systems, and regional conventions of the world. Languages and locales are identified with a standard language tag and processed as defined in BCP 47. Oracle JET includes Oracle National Language Support (NLS) translation support for the languages listed in the following table.

Language Language Tag

Arabic

ar

Brazilian Portuguese

pt

Bulgarian bg-BG

Canadian French

fr-CA

Chinese (Simplified)

zh-Hans (or zh-CN)

Chinese (Traditional)

zh-Hant (or zh-TW)

Croatian

hr

Czech cs

Danish

da

Dutch

nl

Estonian

et

Finnish

fi

French

fr

German

de

Greek

el

Hebrew

he

Hungarian

hu

Icelandic is

Italian

it

Japanese

ja

Korean

ko

Latin_Serbian sr-Latn

Latvian

lv

Lithuanian

lt

Malay ms-MY

Norwegian

no

Polish

pl

Portuguese

pt-PT

Romania

ro

Russian

ru

Serbian sr

Slovak

sk

Slovenian sl

Spanish

es

Swedish

sv

Thai

th

Turkish

tr

Ukrainian uk-UA
Vietnamese vi

Oracle JET translations are stored in a resource bundle. You can add your own translations to the bundles. For additional information see Add Translation Bundles to Oracle JET.

Oracle JET also includes formatting support for over 196 locales. Oracle JET locale elements are based upon the Unicode Common Locale Data Repository (CLDR) and are stored in locale bundles. For additional information about Unicode CLDR, see http://cldr.unicode.org. You can find the supported locale bundles in the Oracle JET distribution:

js/libs/oj/15.0.0/resources/nls

It is the app's responsibility to determine the locale used on the page. Typically the app determines the locale by calculating it on the server side from the browser locale setting or by using the user locale preference stored in an identity store and the supported translation languages of the app.

Once the locale is determined, your app must communicate this locale to Oracle JET for its locale sensitive operations, such as loading resource bundles and formatting date-time data. Oracle JET determines the locale for locale sensitive operations in the following order:

  1. Locale specification in the ojL10n plugin of the RequireJS configuration.

  2. lang attribute of the html tag.

  3. navigator.language browser property.

If your app will not provide an option for users to change the locale dynamically, then setting the lang attribute on the html tag is the recommended practice because, in addition to setting the locale for Oracle JET, it sets the locale for all HTML elements as well. Oracle JET automatically loads the translations bundle for the current language and the locale bundle for the locale that was set. If you don't set a locale, Oracle JET will default to the browser property. If, however, your app provides an option to change the locale dynamically, we recommend that you set the locale specification in the ojL10n plugin if your app uses RequireJS. Oracle JET loads the locale and resource bundles automatically when your app initializes.

If you use Webpack rather than RequireJS as the module bundler for your Oracle JET app, we recommend that you generate one code bundle for each locale that you want to support, and deploy each bundle to a different URL for the locale that you want to support. If, for example, your app URL is https://www.oracle.com/index.html and you want to support the French and Spanish locales, deploy the bundles for these locales to https://www.oracle.com/fr/index.html and https://www.oracle.com/es/index.html respectively.

Finally, Oracle JET includes validators and converters that use the locale bundles. When you change the locale on the page, an Oracle JET component has built-in support for displaying content in the new locale. For additional information about Oracle JET's validators and converters, see Validate and Convert Input.

Internationalize and Localize Oracle JET Apps

Configure your app to use Oracle JET's built-in support for internationalization and localization.

Use Oracle JET's Internationalization and Localization Support

To use Oracle JET's built-in internationalization and localization support, you can specify one of the supported languages or locales on the lang attribute of the html element on your page. For example, the following setting will set the language to the French (France) locale.

<html lang="fr-FR">

If you want to specify the French (Canada) locale, you would specify the following instead:

<html lang="fr-CA">

Tip:

The locale specification isn’t case sensitive. Oracle JET will accept FR-FR, fr-fr, and so on, and map it to the correct resource bundle directory.

When you specify the locale in this manner, any Oracle JET component on your page will display in the specified language and use locale constructs appropriate for the locale.

If the locale doesn’t have an associated resource bundle, Oracle JET will load the lesser significant language bundle. If Oracle JET doesn’t have a bundle for the lesser significant language, it will use the default root bundle. For example, if Oracle JET doesn’t have a translation bundle for fr-CA, it will look for the fr resource bundle. If the fr bundle doesn’t exist, Oracle JET will use the default root bundle and display the strings in English.

In the image below, the page is configured with the oj-input-date-time component. The image shows the effect of changing the lang attribute to fr-FR.

If you type an incorrect value in the oj-input-date-time field, the error text is displayed in the specified language. In this example, the error is displayed in French.

Enable Bidirectional (BiDi) Support in Oracle JET

If the language you specify uses a right-to-left (RTL) direction instead of the default left-to-right (LTR) direction, such as Arabic and Hebrew, you must specify the dir attribute on the html tag.

<html dir="rtl">

The image below shows the oj-input-date-time field that displays if you specify the Arabic (Egypt) language code and change the dir attribute to rtl.

Once you have enabled BiDi support in your Oracle JET app, you must still ensure that your app displays properly in the desired layout and renders strings as expected.

Note:

JET does not support the setting of the dir attribute on individual HTML elements which would cause a page to show mixed directions. Also, if you programmatically change the dir attribute after the page has been initialized, you must reload the page or refresh each JET component.

Set the Locale and Direction Dynamically

You can configure your app to change its locale and direction dynamically by setting a key value pair in the app’s local storage that your app’s RequireJS ojL10n plugin reads when you reload the app URL.

The image below shows an Oracle JET app configured to display a menu that displays a department list when clicked and a date picker. By default, the app is set to the en-US locale. Both the menu and date picker display in English.

You can run the Oracle JET app shown in the image if you download the JET-Localization.zip and run the Oracle JET CLI ojet restore and ojet serve commands in the directory where you extract the ZIP file.

The app also includes a button set which shows the United States of America, France, Czech Republic, and Egypt flags. When the user clicks one of the flags, the app locale is set to the locale represented by the flag: en-US, fr-FR, cs-CZ or ar-EG.

Note:

The flags used in this example are for illustrative use only. Using national flags to select a UI language is strongly discouraged because multiple languages are spoken in one country, and a language may be spoken in multiple countries as well. In a real app, you can use clickable text instead that indicates the preferred language to replace the flag icons.

The image below shows the updated page after the user clicks the Egyptian flag.

Implementing this behavior requires you to make changes in the view, the viewModel, and the appRootDir/src/main.js file of your app. In the view code of the app, the on-value-changed property change listener attribute specifies a setLang function that is called when a user changes the selected button.

<oj-buttonset-one . . . on-value-changed="[[setLang]]">

In the viewModel code of the app, this setLang function determines what locale the user selected and sets entries in Window.localStorage so that a user’s selection persists across browser sessions. The final step in the function is to reload the current URL using location.reload().

setLang = (evt) => {
    let newLocale = "";
    let lang = evt.detail.value;
    switch (lang) {
      case "Čeština":
        newLocale = "cs-CZ";
        break;
      case "Français":  
        newLocale = "fr-FR";
        break;
      case "عربي":
          newLocale = "ar-EG";
          break;        
      default:
        newLocale = "en-US";
    }
    window.localStorage.setItem('mylocale',newLocale);
    window.localStorage.setItem('mylang',lang);
    location.reload();
  };

To set the newly-selected locale in the ojL10n plugin of our app’s appRootDir/src/main.js file, we write the following entries that read the updated locale value from local storage, and set it on the locale specification in the ojL10n plugin. We also include a check that sets the direction to rtl if the specified locale is Egyptian Arabic (ar-EG).

(function () {
  ...
const localeOverride = window.localStorage.getItem("mylocale");
  if (localeOverride) {  
    // Set dir attribute on <html> element.
    // Note that other Arabic locales and Hebrew also use the rtl direction. 
    // Include a check here for other locales that your app must support.
    if(localeOverride === "ar-EG"){
      document.getElementsByTagName('html')[0].setAttribute('dir','rtl');
    } else {
      document.getElementsByTagName('html')[0].setAttribute('dir','ltr');
    }
    requirejs.config({
      config: {
        ojL10n: {
          locale: localeOverride,
        },
      },
    });
  }
})();
...

For information about defining your own translation strings and adding them to the Oracle JET resource bundle, see Add Translation Bundles to Oracle JET.

When you use this approach to internationalize and localize your app, you must consider every component and element on your page and provide translation strings where needed. If your page includes a large number of translation strings, the page can take a performance hit.

Also, if SEO (Search Engine Optimization) is important for your app, be aware that search engines normally do not run JavaScript and access static text only.

Tip:

To work around issues with performance or SEO, you can add pages to your app that are already translated in the desired language. When you use pages that are already translated, the Knockout bindings are executed only for truly dynamic pieces.

Work with Currency, Dates, Time, and Numbers

When you use the converters included with Oracle JET, dates, times, numbers, and currency are automatically converted based on the locale settings. You can also provide custom converters if the Oracle JET converters are not sufficient for your app. For additional information about Oracle JET converters, see About Oracle JET Converters. For information about adding custom converters to your app, see Use Custom Converters in Oracle JET.

Work with Oracle JET Translation Bundles

Oracle JET includes a translation bundle that translates strings generated by Oracle JET components into all supported languages. Add your own translation bundle by merging it with the Oracle JET bundle.

About Oracle JET Translation Bundles

Oracle JET includes a translation bundle that translates strings generated by Oracle JET components into all supported languages. You can add your own translation bundle following the same format used in Oracle JET.

The Oracle JET translation bundle follows a specified format for the content and directory layout but also allows some leniency regarding case and certain characters.

Translation Bundle Location

The location of the Oracle JET translation bundle, which is named ojtranslations.js, is in the following directory:

libs/oj/v15.0.0/resources/nls/ojtranslations

Each supported language is contained in a directory under the nls directory. The directory names use the following convention:

  • lowercase for the language sub-tag (zh, sr, and so on.)

  • title case for the script sub-tag (Hant, Latn, and so on.)

  • uppercase for the region sub-tag (HK, BA, and so on.)

The language, script, and region sub-tags are separated by hyphens (-). The following image shows a portion of the directory structure.

Top Level Module

The ojtranslations.js file contains the strings that Oracle JET translates and lists the languages that have translations. This is the top-level module or root bundle. In the root bundle, the strings are in English and are the runtime default values when a translation isn’t available for the user’s preferred language.

Translation Bundle Format

Oracle JET expects the top-level root bundle and translations to follow a specified format. The root bundle contains the Oracle JET strings with default translations and the list of locales that have translations. // indicates a comment.

define({
// root bundle
  root: {
    "oj-message":{
        fatal:"Fatal",
        error:"Error",
        warning:"Warning",
        info:"Info",
        confirmation:"Confirmation",
        "compact-type-summary":"{0}: {1}"
     },
     // ... contents omitted
  },

// supported locales.       
  "fr-CA":1,
   ar:1,
   ro:1,
   "zh-Hant":1,
   nl:1,
   it:1,
   fr:1,
   //  ... contents omitted
   tr:1,fi:1
});

The strings are defined in nested JSON objects so that each string is referenced by a name with a prefix: oj-message.fatal, oj-message.error, and so on.

The language translation resource bundles contain the Oracle JET string definitions with the translated strings. For example, the following code sample shows a portion of the French (Canada) translation resource bundle, contained in nls/fr-CA/ojtranslations.js.

define({
  "oj-message":{
     fatal:"Fatale",
     error:"Erreur",
     warning:"Avertissement",
     info:"Infos",
     confirmation:"Confirmation",
     "compact-type-summary":"{0}: {1}"
     },
    //  ... contents omitted
});

When there is no translation available for the user's dialect, the strings in the base language bundle will be displayed. If there are no translations for the user's preferred language, the root language bundle, English, will be displayed.

Named Message Tokens

Some messages may contain values that aren't known until runtime. For example, in the message "User foo was not found in group bar", the foo user and bar group is determined at runtime. In this case, you can define {username} and {groupname} as named message tokens as shown in the following code.

"MyUserKey":"User {username} was not found in group {groupname}."

At runtime, the actual values are replaced into the message at the position of the tokens by calling Translations.applyParameters() with the key of the message as the first argument and the parameters to be inserted into the translated pattern as the second argument.

let parMyUserKey = { 'username': 'Foo', 'groupname': 'Test' };
let tmpString = Translations.applyParameters(MenuBundle.MyUserKey, parMyUserKey);
this.MyUserKey = Translations.getTranslatedString(tmpString);

Numeric Message Tokens

Alternatively, you can define numeric tokens instead of named tokens. For example, in the message "This item will be available in 5 days", the number 5 is determined at runtime. In this case, you can define the message with a message token of {0} as shown in the following code.

"MyKey": "This item will be available in {0} days."

A message can have up to 10 numeric tokens. For example, the message "Sales order {0} has {1} items" contains two numeric tokens. When translated, the tokens can be reordered so that message token {1} appears before message token {0} in the translated string, if required by the target language grammar. The JavaScript code that calls applyParameters() and getTranslatedString() remains the same no matter how the tokens are reordered in the translated string.

Tip:

Use named tokens instead of numeric tokens to improve readability and reuse.

Escape Characters in Resource Bundle Strings

The dollar sign, curly braces and square brackets require escaping if you want them to show up in the output. Add a dollar sign ($) before the characters as shown in the following table.

Escaped Form Output
$$ $
${ {
$} }
$[ [
$] ]

For example, if you want your output to display [Date: {01/02/2020}, Time: {01:02 PM}, Cost: $38.99, Book Name: JET developer's guide], enter the following in your resource bundle string:

"productDetail": "$[Date: ${01/02/2020$}, Time: ${01:02 PM$}, Cost: $$38.99, Book Name: {bookName}$]"

You then use the Translations.applyParameters() to return the string with the escaped characters and substituted tokens, if any, to display in the UI, as shown in the following example:

let parProductDetail = { bookName: "JET developer's guide"};
this.productDetail = Translations.applyParameters(MenuBundle.productDetail, parProductDetail);

Format Translated Strings

In some situations, you may want to apply formatting to strings in the resource bundle to appear in the UI. Take, for example, a book title to which we may want to apply the <i> tag so that the book title renders using italics in the HTML output. In this scenario, you might define the following entry in the resource bundle(s):

// root bundle
// appRootDir/src/ts/resources/nls/menu.js
"FormatTranslatedString": "The <i>{booktitle}</i> describes how to develop Oracle JET apps"

And then use Oracle JET’s oj-bind-dom element to render the string in the UI, as in the following example:

<p><span>
  <oj-bind-dom config="{{ formatTranslatedString() }}"></oj-bind-dom>
</span></p>

Note:

The oj-bind-dom element does not validate HTML input provided by an app for integrity or security violations. It is the app's responsibility to sanitize the input to prevent unsafe content from being added to the page.

In our viewModel, we use Oracle JET’s HtmlUtils utility class to parse the string from the resource bundle.

. . .
import * as HtmlUtils from "ojs/ojhtmlutils";
import "ojs/ojbinddom";
. . .
class DashboardViewModel {
 . . .
  FormatTranslatedString: String;
 . . .
  formatTranslatedString = () => {

    var parBookTitle = { 'booktitle': 'Oracle JET Developer Guide' };
    let strTitle = Translations.applyParameters(MenuBundle.FormatTranslatedString, parBookTitle);
    this.FormatTranslatedString = Translations.getTranslatedString(strTitle);
   
    return {
      view: HtmlUtils.stringToNodeArray(
        `<span>${this.FormatTranslatedString}</span>`,
      ),
    };
   };
. . .

Add Translation Bundles to Oracle JET

You can add a translation bundle to your Oracle JET app with the custom strings that your app UI needs and the translations that you want your app to support.

To add translation bundles to Oracle JET:

  1. Define the translations.

    For example, the following code defines a translation set for a menu containing a button label and three menu items. The default language is set to English, and the default label and menu items will be displayed in English. The root object in the file is the default resource bundle. The other properties list the supported locales, fr, cs, and ar.

    define({
      "root": {
        "label": "Select a department",
        "menu1": "Sales",
        "menu2": "Human Resources",
        "menu3": "Transportation"
    },
      "fr": true,
      "cs": true,
      "ar": true
    });

    To add a prefix to the resource names (for example, department.label, department.menu1, and so on), add it to your bundles as shown below.

    define({
      "root": {
        "department": {
          "label": "Select a department",
          "menu1": "Sales",
          "menu2": "Human Resources",
          "menu3": "Transportation"
        }
      }
    },
      "fr": true,
      "cs": true,
      "ar": true
    });

    When the locale is set to a French locale, the French translation bundle is loaded. The code below shows the definition for the label and menu items in French.

    define({
      "label": "Sélectionnez un département",
      "menu1": "Ventes",
      "menu2": "Ressources humaines",
      "menu3": "Transports"
    })

    You can also provide regional dialects for your base language bundle by just defining what you need for that dialect.

    define({
        "label": "Canadian French message here"
    });

    When there is no translation available for the user's dialect, the strings in the base language bundle will be displayed. In this example, the menu items will be displayed using the French translations. If there are no translations for the user's preferred language, the root language bundle, whatever language it is, will be displayed.

  2. Include each definition in a file located in a directory named nls.

    For example, the default translation in the previous step is placed in a file named menu.js in the appRootDir/src/ts/resources/nls directory. The supported translations are located in a file named menu.js in child sub-directories that use the name of the locale:

    appRootDir/src/ts/resources/nls
    |   menu.js
    |
    +---ar
    |     menu.js
    |
    +---cs
    |     menu.js
    |
    +---fr
          menu.js

    The directory name examples use ts as the examples assume that the Oracle JET app is a TypeScript-based app. If the app is JavaScript-based, then the directory name is js, as in appRootDir/src/js/resources/nls.

  3. You need to configure changes in the view and viewModel code of your app to make sure that it retrieves the appropriate translation.

    If, for example, you want to implement the following UI where the menu labels change to English or French depending on the locale, you need to reference btnLocaleLabel and menuNames Knockout variables in the view code, as follows:

    <oj-menu-button id="menuButton1">
      <span>
        <oj-bind-text value="[[btnLocaleLabel]]"></oj-bind-text>
      </span>
    <oj-menu id="myMenu1" slot="menu" on-oj-action="[[changeLabel]]">
      <oj-bind-for-each data="[[menuNames]]">
        <template>
          <oj-option value="[[$current.data.itemName]]" :id="[[$current.data.id]]">
    	<span>
    	  <oj-bind-text value="[[$current.data.itemName]]"></oj-bind-text>
    	</span>
          </oj-option>
      . . .
    
    The image shows translated strings in English and French from the imported resource bundles
  4. In the viewModel code, you first import the translation bundles where you defined the default and translated strings:
    import * as MenuBundle from "ojL10n!../resources/nls/menu";
  5. You then declare the Knockout observables that reference the imported translation bundle (MenuBundle) for the appropriate value to use, as demonstrated by the following excerpts from the viewModel file:
    import * as MenuBundle from "ojL10n!../resources/nls/menu";
    
    class DashboardViewModel {
      btnLocaleLabel: ko.Observable<string>;
      menuNames: ko.ObservableArray<object>;
    
      constructor() {
        // setting up knockout observables for the 
        // button label and the menu items
        this.btnLocaleLabel = ko.observable();
        this.menuNames = ko.observableArray([{}]);
          . . .
      }
    
      loadMenu = () => {
        // These lines pull the translated values for the menu items 
        // from the appropriate resource file in the /resources/nls directory
        this.menuNames([
          { itemName: MenuBundle.menu1, id:'menu1' },
          { itemName: MenuBundle.menu2, id: 'menu2' },
          { itemName: MenuBundle.menu3, id: 'menu3' },
        ]);
        this.btnLocaleLabel(MenuBundle.label);
      };
      
      /**
       * Optional ViewModel method invoked after transition to the new View is complete.
       * That includes any possible animation between the old and the new View.
       */
      transitionCompleted(): void {
        // Call the function that pulls the translated values.
        this.loadMenu();
      }
    }
    
    export = DashboardViewModel;
  6. If the strings from your resource bundle include message tokens or reserved characters ($, {, }, [, ]) that need to be escaped, you must use Oracle JET's Translation.applyParameters API.

    The following code demonstrates how to render characters that must be escaped and a string that requires a parameter value (also referred to as a token value).

    // // App UI is going to render the following strings:
    $ { } [ ]
    [The Oracle JET Developer's Guide costs $38.99]
    
    // To accomplish this, we enter the following entries in the app's resource bundle(s): 
    (appRootDir/src/ts/resources/nls/menu.js)
    . . .
            "EscapeChar": "$$ ${ $} $[ $]",
            "EscapeCharToken": "$[The {bookName} costs $$38.99$]"
        },
    . . .
    
    // appRoot/src/ts/viewModels/dashboard.ts includes the following entries:
    // Import the Translations module that includes the applyParameters() API
    import * as Translations from "ojs/ojtranslation";
    
    // Define types:
    . . .
      EscapeChar: String;
      EscapeCharToken: String;
    
    constructor() {
    
    . . .
    
    // Escape characters in resource bundle strings
    let parEscapeChar = { };
    this.EscapeChar = Translations.applyParameters(MenuBundle.EscapeChar, parEscapeChar)
    
    // Substitute a token and escape a character
    let parEscapeCharToken = { bookName: "Oracle JET Developer's Guide"};
    this.EscapeCharToken = Translations.applyParameters(MenuBundle.EscapeCharToken, parEscapeCharToken)
    
    // appRoot/src/ts/views/dashboard.html includes the following entries to render the final string
    . . .
    <oj-bind-text value="[[EscapeChar]]"></oj-bind-text>
    . . .
    <oj-bind-text value="[[EscapeCharToken]]"></oj-bind-text>