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 RequireJS Translation Bundles to an Oracle JET App.

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/19.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 displays in the specified language. In this example, the error displays 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:

Oracle 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 that 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 the location.reload() method.

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 translation bundle, see Add RequireJS Translation Bundles to an Oracle JET App or Add ICU Translation Bundles to an Oracle JET Virtual DOM App, depending on the type of translation bundle that you use.

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 RequireJS Translation Bundles

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

Note:

If you are creating a translation bundle for the first time in your Oracle JET app, we recommend that you use the other type of translation bundle (ICU translation bundle) that Oracle JET supports. For more information, see Work with ICU Translation Bundles in an Oracle JET Virtual DOM App.

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/v19.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.

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 the Translations.applyParameters() method 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 code that calls the applyParameters() and getTranslatedString() methods 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() method 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
"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>

Caution:

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 RequireJS Translation Bundles to an Oracle JET App

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>
    

    You can run an Oracle JET app that includes these code snippets 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.

Work with ICU Translation Bundles in an Oracle JET Virtual DOM App

Oracle JET supports ICU message format translation bundles that enable Oracle JET apps to provide localized UI strings and support runtime substitution using placeholders.

International Components for Unicode (ICU) is a set of libraries that provides support for the internationalization of text, numbers, dates, times, currencies, and other locale-sensitive data in formatting user-visible strings (messages). To read more about how ICU formats messages, see its documentation.

To use Oracle JET’s support for ICU message format translation bundles, run the Oracle JET CLI’s add translation command in the root directory of your Oracle JET project. This installs the NPM package (@oracle/oraclejet-icu-l10n) that you need to generate the runtime ICU translation bundles. It also creates a resources directory with ICU translation bundles in your project’s src directory with the following files to help you get started:

appRootDir\src\resources
\---nls
    |   translationBundle.json
    |
    \---de
    translationBundle.json

Finally, it updates your project’s oraclejetconfig.json file with the following properties to facilitate generation of the runtime ICU translation bundles when you run the Oracle JET CLI’s build or serve commands:

. . .
"translationIcuLibraries": "@oracle/oraclejet-icu-l10n",
. . .
"buildICUTranslationsBundle": true,
  "translation": {
    "type": "icu",
    "options": {
      "rootDir": "./src/resources/nls",
      . . .
      "supportedLocales": "de"
    }
  }
. . .

When you build your Oracle JET project, @oracle/oraclejet-icu-l10n parses the ICU translation bundles (for example, translationBundle.json) and converts them to TypeScript modules (translationBundle.ts using our example). These TypeScript modules are the runtime ICU translation bundles. They return an object with functions to get the localized string for a message key. The function takes parameters to format the message. For plurals, the parameter is always a number. Note that the same message can have multiple parameters (for example, a number for the plural rule and a string for a text placeholder). The parameter types will be described by TypeScript.

As you’ll notice from the example ICU translation bundle that the add translation command creates, translation bundle files must have a .JSON file extension and follow the JSON format. Keys for translation bundle resources should always appear at the top level. Creating sub-objects for translated resources is not supported. Component authors may use some convention-based character like '_ ' in the key name (for example, "input_messages_error") to indicate the intended usage of the resource.

"input_message_error": "Error",
"input_message_warning": "Warning"

The value is a string that may contain placeholders.

Note:

The examples that follow are formatted as multi-line strings for readability. Actual values in an ICU translation bundle must be single-line strings, as required by the JSON specification. Enable word wrapping in your editor to view them conveniently.

Keys starting with the @ character specify metadata. If a metadata key starts with @@, the metadata is considered global. Otherwise, the metadata is associated with the key whose name follows the @ character:

"input_message_error": "Error: {MESSAGE}",
"@input_message_error": 
   "placeholders": {
      "MESSAGE": 
         "type": "text",
         "description": "translated error message"
      }
   },
   "description": "Error with an embedded message"
}

Metadata is optional. Supply it in the following cases:

  1. The usage of the text placeholder is going to be unclear to the translator, and supplying an extra description in the metadata is going to help with translating the rest of the message.

  2. A particular placeholder is not being used in the message included in the root/development bundle, but the corresponding parameter should be settable for some other locales.

    ICU translation bundle's build process derives parameter type information from the message in the root/development bundle. Type information for any parameters used only in other locales will have to come from metadata.

The reserved special characters within the translated resource are pound # and curly braces ({, }). Use the ASCII apostrophe (', U+0027) around these characters so that they appear in the message. Use double ASCII apostrophe if a single ASCII apostrophe should appear in the message, but only when it is immediately followed by a pound character or a curly brace. We recommend that you use the real apostrophe (single quote) character (U+2019) for human-readable text, and use the ASCII apostrophe ' (U+0027) only as an escape character. This avoids having to use the double ASCII apostrophe.

If a particular part of a message should not be translated, provide it as a parameter for a text placeholder.

Format Placeholders Tokens

Placeholder tokens enable dynamic content generation by substituting values into translated messages.

Text

Use text placeholders to perform simple token substitution. Avoid the use of numeric placeholder names (0, 1, and so on).

"input_message_error": "Error: {MESSAGE}"

In the previous example, MESSAGE identifies a named parameter to insert into the translated string.

Plural

Plural placeholders define translated messages intended for either certain groups of numbers or for exact numbers. You can think of them as switch statements.

"shopping_card_items": "You have {item_count, plural, offset:0
    =0 {no items}
    one {# item}
    other {# items}
} in your cart"

To understand the previous example, consider the following:

  • item_count is the name of the parameter whose numeric value is used to for matching the message.
  • plural is the type of the placeholder.
  • =0 will match exactly 0.
  • one is one of the language-specific categories that are matched against the category of the parameter's value. For example, 1, 21 and 31 will all be in the category one for Ukrainian. Other categories are zero, two, few, many. Note that the match for an exact number (=1) has a higher precedence than a category-based match.
  • offset is the number that will be subtracted from the parameter's value before a category-based rule is applied. The default is 0. Note that offset is not used in exact matches (=1, and so on).
  • other is just like a default label in a switch statement. That is, it is a catch all category. Note that other is required in every plural placeholder.
  • # will be substituted with the actual numeric value after the offset is subtracted.

Select

Select placeholders specify translated messages that will be chosen based on the value of a string parameter. Just like with the plural selector, you can think of them as a switch statement.

"response_message": "{gender, select,
    male {He}
    female {She}
    other {They}
    } will respond shortly."

In the example above, gender is the name of the parameter whose string value is used to match the translated message. Possible parameter values are male and female. Similar to the plural placeholder, other is a special category acting like a default label in a switch statement. Note that other is required in every select placeholder.

Nesting Select and Plural Placeholders

"invite_message": "{gender_of_host, select,
  female {
    {num_guests, plural, offset:1
      =0 {{host} does not give a party.}
      =1 {{host} invites {guest} to her party.}
      =2 {{host} invites {guest} and one other person to her party.}
      other {{host} invites {guest} and # other people to her party.}}}
  male {
    {num_guests, plural, offset:1
      =0 {{host} does not give a party.}
      =1 {{host} invites {guest} to his party.}
      =2 {{host} invites {guest} and one other person to his party.}
      other {{host} invites {guest} and # other people to his party.}}}
  other {
    {num_guests, plural, offset:1
      =0 {{host} does not give a party.}
      =1 {{host} invites {guest} to their party.}
      =2 {{host} invites {guest} and one other person to their party.}
      other {{host} invites {guest} and # other people to their party.}}}}"

In the example above, plural placeholders are nested in a select placeholder. The result is that plural rules are applied for each gender-specific group of messages. Note that the value of the offset in plural rules is used only for printing the number of guests, not counting the main guest identified by the guest parameter (substituting #).

Set Up an Oracle JET Virtual DOM App to Create ICU Translation Bundles

To set up an Oracle JET virtual DOM app to create and use ICU translation bundles, you run the add translation command and edit the properties that this command inserts in your app's oraclejetconfig.json file.

To set up your Oracle JET app to support ICU translation bundles:

  1. Change to your app's top-level directory, open a terminal window, and enter the following command:

    npx ojet add translation

  2. In an editor, open the oraclejetconfig.json file to review and edit the properties that the add translation command inserted.

    Leave the value of the type property in the translation section unchanged at icu. This is the only supported value at present. Similarly, leave the buildICUTranslationsBundle property unchanged at true, unless you do not want to generate runtime ICU translation bundles. In that case, set it to false.

    The following table describes the child properties of options:

    Property Description
    rootDir The root directory for ICU translation bundles. The value that the add translation command inserts is "./src/resources/nls".
    bundleName The bundle name from which to generate runtime ICU translation bundles when you run the ojet build or serve commands. The value that the add translation command inserts is "translationBundle.json".
    locale The locale of the ICU translation bundle in the root directory. The value that the add translation command inserts is "en-US".
    outDir The output directory for runtime ICU translation bundles. The value that the add translation command inserts is "./src/resources/nls". Change this to a value of your choice. For example, "./src/resources-dist".
    supportedLocales A comma-separated list of additional locales to build. If you specify a locale that does not have a corresponding entry in the nls directory under the rootDir directory, then the runtime ICU translation bundle is built from the ICU translation bundle in the rootDir directory. The value that the add translation command inserts is "de".
    componentBundleName

    Specifies the naming pattern for the ICU translation bundle associated with a component.

    The add translation command inserts the value "${componentName}-strings.json" where {componentName} references the name of the component.

    If you create a component named my-component, then the file name for the ICU translation bundle must be my-component-strings.json.

Add ICU Translation Bundles to an Oracle JET Virtual DOM App

Add ICU translation bundles to your Oracle JET virtual DOM app with the custom strings that your app UI needs and the translations that you want your app to support.

To add ICU translation bundles to your Oracle JET app:

  1. Define the UI strings and translations in .JSON files with a flat key-value structure.

    Each key must be at the top level. Do not nest objects for grouping.

    For example, the following entry, generated by default when you run the add translation command, defines a greeting message.

    {
      "greeting": "Hello! How are you doing?"
    }

    Keys prefixed with @ provide optional metadata, such as descriptions and parameter types. This metadata helps when placeholders are not self-explanatory or when required only for certain locales.

  2. Include each definition in a .JSON file located in a directory named nls.
    For example, the translation in the previous step is placed in a file named translationBundle.json in the appRootDir/src/resources/nls directory. The supported translations are in files named translationBundle.json in child sub-directories that use the name of the locale:
    appRootDir/src/resources/nls
    |   translationBundle.json
    |
    /---de
        translationBundle.json

Generate Runtime ICU Translation Bundles

Once you have created the .JSON files with the UI strings for the locales that your Oracle JET virtual DOM app supports, you need to build the runtime ICU translation bundles to a location from where your app retrieves the UI strings at runtime.

Change to your app's top-level directory, open a terminal window, and enter the following command:

npx ojet build

This creates the runtime ICU translation bundles (.TS files) and the supportedLocales.ts file in the directory specified by the outDir property in the oraclejetconfig.json file. By default, it is ./src/resources/nls.

appRootDir/src/resources
\---nls
    |   supportedLocales.ts
    |   translationBundle.json
    |   translationBundle.ts
    |
    +---de
    |       translationBundle.json
    |       translationBundle.ts

Now that you have generated the runtime ICU translation bundles, you can use the UI strings that they contain in your Oracle JET virtual DOM app and in your VComponents.

Use an ICU Translation Bundle in an Oracle JET Virtual DOM App Component

Once you generate the runtime ICU translation bundles, you use the UI strings from the generated bundles.

A runtime ICU translation bundle defines an object with function(s) to get the UI string value for the specified message key. The function can take parameters to format the message. For plurals, the parameter is always a number.

Note:

The same message can have multiple parameters. For example, a number for the plural rule and a string for a text placeholder. TypeScript describes the parameter types.

Your app dynamically loads the translation bundles like any other Typescript module. You first determine what locale your app uses. Once you know the desired locale, you find the best match out of the supported locales that the ./appRootDir/src/resources-dist/supportedLocales.ts file lists. For example, the add translation command adds support for de by default:

export default ["de"];

The following code sample demonstrates how to load the UI string for the label-hint attribute value of the oj-input-text element in the appRootDir/src/components/content/index.tsx file of a virtual DOM app. It first imports the runtime ICU translation bundle and the supported locales. A Preact useEffect hook loads the imported ICU translation bundles and if one is found to match the preferred locale, it is used. If not, the ICU translation bundle for the German locale is used (de).

import { h } from "preact";
import { BundleType as ICUBundleType } from "../../resources/nls/translationBundle";
import supportedLocales from "../../resources/nls/supportedLocales";
import { useState, useEffect } from "preact/hooks";
import "ojs/ojlabel";
import "ojs/ojinputtext";
import "ojs/ojformlayout";

export function Content() {
  const [ICUBundle, setICUBundle] = useState<ICUBundleType>();
  // Load runtime ICU translation bundle in Preact's useEffect hook.
  useEffect(() => {
    _loadTranslationBundle().then((bundle) => setICUBundle(bundle));
  }, []);

  return (
    <div class="oj-web-applayout-max-width oj-web-applayout-content">
      <div id="icu-app">
      <p>
      Render UI string from the <code>./src/resources/nls/de</code> runtime
      ICU translation bundle.{" "}
      </p>
        {ICUBundle && (
          <oj-form-layout id="ofl1" max-columns="1" direction="column">
            <oj-input-text
              id="input"
              value=" "
              labelHint={ICUBundle.greeting()}
              labelEdge="inside"
            ></oj-input-text>
          </oj-form-layout>
        )}
      </div>
    </div>
  );
}

async function _loadTranslationBundle(): Promise<ICUBundleType> {
  const preferredLocale = navigator.languages[0];
  const localeToLoad = _matchTranslationBundle(
    preferredLocale,
    supportedLocales
  );

 // The ICU translation bundle name (translationBundle in this example) derives 
 // its name from the value that you specify for the bundleName property 
 // in the oraclejetconfig.json file. 
  const module = await import(
    `../../resources/nls/${localeToLoad}/translationBundle`
  );
  return module.default;
}

function _matchTranslationBundle(
  preferredLocale: string,
  supportedLocales: string[]
) {
  let match = null;
  const supportedLocaleSet = new Set(supportedLocales);

  if (supportedLocaleSet.has(preferredLocale)) {
    match = preferredLocale;
  } else {
    match = _findPartialMatch(preferredLocale, supportedLocaleSet);
  }
  // Normally, the fallback locale would be 'en-US', but for 
  // this example, we're using 'de'.
  return match || 'de';
}

function _findPartialMatch(locale: string, supportedLocales: Set<string>) {
  let match = null;
  const sep = "-";
  const parts = locale.split(sep);

  while (match === null && parts.length > 1) {
    parts.pop();
    const partial = parts.join(sep);
    if (supportedLocales.has(partial)) {
      match = partial;
    }
  }
  return match;
}

Use an ICU Translation Bundle in an Oracle JET VComponent

The steps to create and use ICU translation bundles in a VComponent are the same as those for a virtual DOM app. That is, you create ICU translation bundle source files (.JSON) in a directory location that matches the pattern specified by the rootDir property in the oraclejetconfig.json file. By default, this is "./src/resources/nls". When you build the Oracle JET project that contains the VComponent, the runtime ICU translation bundles are generated to the location specified by the outDir property. Again, the default is "./src/resources/nls". One additional property in the oraclejetconfig.json file to be aware of is componentBundleName. This specifies the naming pattern for the VComponent’s ICU translation bundle so that Oracle JET can generate the runtime ICU translation bundle. Its default value is "${componentName}-strings.json".

So, assume for example, that you create a VComponent named translation-icu in your Oracle JET virtual DOM app:

npx ojet create component translation-icu --vcomponent

This leads to the creation of a translation-icu directory within your virtual DOM app’s project file and this directory includes resources and nls sub-directories as follows:

appRootDir/src/components/translation-icu/resources/nls

Within the nls sub-directory, you create the ICU translation bundles as needed for the locales that you are going to support:

appRootDir/src/components/translation-icu/resources/nls
|   translation-icu-strings.json
+---de
|       translation-icu-strings.json

Once you have generated the runtime ICU translation bundles by building the Oracle JET virtual DOM app that contains the VComponent, you can use UI strings from the runtime ICU translation bundle in your VComponent. The following code snippets demonstrate how you import and reference a UI string from the ICU translation bundle. For brevity, the full VComponent code and helper functions that are unchanged from the code sample in the previous section for the virtual DOM app have been omitted.

. . .
import "css!./translation-icu-styles.css";
import { useState, useEffect } from "preact/hooks";
import "ojs/ojinputtext";
import "ojs/ojformlayout";
import { BundleType as ICUBundleType } from "./resources/nls/translation-icu-strings";
import supportedLocales from "./resources/nls/supportedLocales";

. . .

function TranslationIcuImpl() {

  const [ICUBundle, setICUBundle] = useState<ICUBundleType>();

  // Load ICU translation bundle in Preact's useEffect hook.
  useEffect(() => {
    _loadTranslationBundle().then((bundle) => setICUBundle(bundle));
  }, []);

  return (
    <div>
      <p>Render the string from the ICU translation bundle</p>
      {ICUBundle ? (
        <oj-form-layout id="ofl1" max-columns="1" direction="column">
          <oj-input-text
            id="input"
            value=" "
            labelHint={ICUBundle.greeting()}
            labelEdge="inside"
          ></oj-input-text>
        </oj-form-layout>
      ) : (
        <p>ICU translation bundle did not load</p>
      )}
    </div>
  );
}

// Helper functions for ICU translation bundle
async function _loadTranslationBundle(): Promise<ICUBundleType> {
  const preferredLocale = navigator.languages[0];
  const localeToLoad = _matchTranslationBundle(
    preferredLocale,
    supportedLocales
  );

// The component ICU translation bundle name 
// (translation-icu-strings in this example) derives 
// its name from the VComponent name and other values
// that you specify for the componentBundleName property 
// in the oraclejetconfig.json file.
  const module = await import(
    `./resources/nls/${localeToLoad}/translation-icu-strings`
  );
  return module.default;
}

function _matchTranslationBundle(
. . .
// Omitted for brevity. See previous section that includes full code
// for this function

function _findPartialMatch(locale: string, supportedLocales: Set<string>) {
. . .
// Omitted for brevity. See previous section that includes full code
// for this function

export const TranslationIcu: ComponentType<ExtendGlobalProps<
  ComponentProps<typeof TranslationIcuImpl>
  >> = registerCustomElement("translation-icu", TranslationIcuImpl);