X

Technical Articles relating to Oracle Development Tools and Frameworks

  • JET
    February 14, 2017

JET Custom Components XV - Translations

Duncan Mills
Architect

Last Updated Jan 2020 for JET 8

Introduction

Once you start to get serious about building truly reusable components, the issue of language support will come to the fore. The JET components that you use within your custom components will of course do the right thing based on the language and locale settings of the application, however, what about your own resources?

Background on Translatable Strings in JET

In the context of a JET application, an application developer will usually define translatable resources using the following steps:

  1. Create a /resources/nls folder under /js
  2. Create one subfolder under this nls folder for each supported language / region
  3. Define the default bundle with a particular name (e.g. myappstrings.js) in the /nls folder
  4. Define corresponding translated versions of myappstrings.js in each language subdirectory
  5. Configure the requirejs ojL10n plugin to merge the custom bundle (myappstrings.js) with the built-in JET bundles
This is all covered in detail in the documentation. Reading this, however, you might notice two particular issues which have a direct bearing on custom components.
 
  1. The configuration of the application bundle merging is done within the main application code (usually main.js)
  2. You can actually only merge one custom bundle into the base JET bundle

Both of these restrictions make it impossible for a custom component to magically merge its strings into the default JET bundle for access via oj.Translations.getTranslatedString() and oj.Translations.gerResource(). So what should you do?

Steps to Apply Translations to Your Custom Components

Well the good news here is that we've automated pretty much all of this if you use the ojet create component command to scaffold up your new components.  If you need to do this manually, then here's what it has done for you

Definition of the Base Bundle(s)

When the component is created you'll notice in the component folder structure includes a /resources folder, you might have a structure a bit like this (using our standard example component ccdemo-name-badge.)

\ccdemo-name-badge
  \resources
    \nls
      ccdemo-name-badge-strings.js
  ccdemo-name-badge-styles.css
  ccdemo-name-badge-view.html
  ccdemo-name-badge-viewModel.js
  component.json
  loader.js
  README.md  

If you then take a peek at that generated ccdemo-name-badge-strings.js file you see it will look like this:

define({
  "root": {
    "ccdemo-name-badge" : {
      "sampleString": "The strings file can be used to manage translatable resources"
    }
  }
});  

So it contains just one sample string in just the root (aka default) locale. The available strings are name-spaced within an object with the same name as the component. This is important and prevents resource key naming collisions if more than one bundle is being used, so stick to that standard.

You can then update this file to add further strings and locales as required e.g. Assuming that my root language is US English I might have a resource bundle file like this:

define({
  "root": {
    "ccdemo-name-badge" : {
      "color-error": "Selected color is not a valid CSS color code"
    }
  },
  "en-gb": {
    "ccdemo-name-badge" : {
      "color-error": "Selected colour is not a valid CSS colour code"
    }
  },
  "fr": {
    "ccdemo-name-badge" : {
      "color-error": "La couleur sélectionnée n'est pas un code de couleur CSS valide"
    }
  }
});  

Now my component supports two flavours of English (US and British) and French. You can also split this bundle up to make it easier to manage the translation files. To do that, we simplify this base ccdemo-name-badge-strings.js to read as follows:

define({
  "root": true,
  "en-gb": true,
  "fr": true
});

In this version, the base bundle just declares the supported languages. Then we have individual bundle files in subdirectories, one for each supported locale, so the folder structure changes to:

\ccdemo-name-badge
  \resources
    \nls
      ccdemo-name-badge-strings.js
      /root
        ccdemo-name-badge-strings.js
      /en-gb
        ccdemo-name-badge-strings.js
      /fr
        ccdemo-name-badge-strings.js
  ccdemo-name-badge-styles.css
  ccdemo-name-badge-view.html
  ccdemo-name-badge-viewModel.js
  component.json
  loader.js
  README.md  

Each of the locale specific bundles will then contain just the resources for that locale. For example the french version (/resources/nls/fr/ccdemo-name-badge-strings.js) would contain:

define({
  "ccdemo-name-badge" : {
    "color-error": "La couleur sélectionnée n'est pas un code de couleur CSS valide"
  }
});  

Note that there is no need for the "fr" locale object to be mentioned as that is implied by the path.

So that's how the string bundles are defined, now let's look at how they are picked up by the ViewModel. 

Translations and the Component ViewModel

If you look at the define() block of a scaffolded component you will pretty easily see the way that translatable strings are accessed.  Here's an extract from the generated code:

define(
  ['knockout',
    'ojL10n!./resources/nls/ccdemo-name-badge-strings',
    'ojs/ojcontext',
    'ojs/ojknockout'],
  function (ko, componentStrings, Context) {
      var self = this;
      ...
      self.res = componentStrings['ccdemo-name-badge'];
      ...
  }
  ...
});

So what's happening here?

First of all you see the reference to ojL10n!./resources/nls/ccdemo-name-badge-strings in the define block.  This uses a special requireJS plugin (ojL10n) to automatically resolve the correct language bundle based on the locale of the browser (generally set via the HTML lang value).  That plugin does all the hard work and understands either of the two disk layouts shown above. It will return just the language subset required into that componentStrings parameter that the constructor has access to. 

Second the strings that are specific to this component are extracted into a variable stored on the component instance called res

About Sparse Translations

A quick side note here about the bundle that is returned to you by the ojL10n plugin. The process is not as simple as selecting the correct bundle and returning that.  What it actually does is merge the required locale bundle with the root bundle and return the combined results (in fact it may do a three layer merge e.g. fr-ca -> fr ->root ) The purpose of this merging is to provide a fallback if a given locale bundle does not include values for all the keys.  It goes without saying therefore that your root should be the superset of all keys and then the locale specific bundles will override as many of those keys as is required. 

Using the Translated Strings 

At this point the ViewModel now has an instance variable "res" which provides access to the translated strings.  This can be used from your JavaScript code or even directly from the custom component view html.  For example you might have in input text:

<oj-input-text value="{{$properties.myValue}}" label-hint="[[res.labelKey]]">

Managing Strings with Substitution

You'll note that because you have direct access to the string bundle via the injected res reference, you don't need to access the string through the Translations.getTranslatedString() API. That's good because your code is somewhat less verbose, however, what about the token substitution feature of that API? Fortunately, this feature is available separately through the Translations.applyParameters(pattern, parameters) API. So you can still manage your token substitutions without having to write a separate function to do so.

Allowing Overrides

In some cases you might want your component user to be able to provide their own value for one of your build-in translations. For example, in a search type component, they might want to override the "No Data Found" message. The standard way to achieve this is to expose a single property on your component called translations. This property in turn then has sub-properties to expose override points for each of the strings that you want to be customizable on a per-instance basis. So in your component.json:

"translations": {
  "displayName": "Message Overrides",
  "type": "object",
  "description": "Allows the customization of hints and error messages used by the component.",
  "properties": {
    "noDataFound": {
      "description": "Override the no results message.",
      "type": "string"
    },
    "tooManyHits": {
      "description": "Override of the too-wide-a-search message.",
      "type": "string"
    }
    ...
  }
}

Of course you do not have to offer overrides for all of the strings used by the component, the exact list is up to you.

If you allow overrides then in the constructor and propertyChanged callback of your component you should simply update the self.res object with the injected overrides.


JET Custom Component Series

If you've just arrived at Custom JET Components and would like to learn more, then you can access the whole series of articles on the topic from the Custom JET Component Learning Path

Be the first to comment

Comments ( 0 )
Please enter your name.Please provide a valid email address.Please enter a comment.CAPTCHA challenge response provided was incorrect. Please try again.