I18n

From Dreamtsoft Wiki
Jump to: navigation, search

Internationalization

The internationalization support has the following goals:

  1. Allow messages and text that is presented to the user to be shown as translated text based on the user's current language setting
  2. Allow messages and text to be modified on a specific system/site/space
  3. Provide a mechanism to gather all messages and text that needs to be translated into a single place so that it can be easily exported and imported in support of translating to a different language

Where

There are several places that messages are used that require translation:

  • Bucket labels
  • Slot labels
  • Widgets
    • Input attribute labels
    • Javascript code that displays messages to the user
  • Components
    • Input attribute labels
    • Javascript code that displays messages to the user
  • Templates
  • Choice slots that show static labels
  • Script Mappers
    • Javascript code that displays messages to the user
  • Actions
    • Action button labels and tooltips
    • Input attribute labels
  • Action Flows
    • Activities that show messages to the user
  • "i18n_string" slots
  • Javascript client code that contains UI strings

Internally, this is also supported:

  • Java code that returns messages to the UI

Handling Translations in the UI

Most of the places that require translations are automatically handled by the UI. For example, all of the labels are translated to the user's selected language without any action on your part. Also, any strings that are stored in records that are translatable (such as the button labels in Browser Actions) are specified with a slot type of I18n String.

translate()

In the case of javascript code, you need to indicate when you want to use a string that needs to be translated. This requires a call to the translate API. For the javascript code that is part of any of the following:

  • Widget
  • Component (both client and server)
  • ScriptMapper
  • Activity
  • Javascript Module (both client and server)

the TRANSLATE API is called as:

  TRANSLATE(message, [replacements], [options]);
/**
 * Translate the key into the current user's language and then perform any replacements if they are specified
 *
 * @param messageKey (Object|String) if String, message to be translated and used for replacements.  If Object,
 *    may be one of the following:
 *    
 *       [
 *          "My text message with {1} and {2},
 *          "%{data.replacement1}"
 *          "%{data.replacement2}",
 *       ]
 *    
 *    or
 *    
 *       {
 *          message: "text message here {1} {2}",
 *          plural: "plural version of message here {1} {2}",     // optional
 *          replacements: [                                       // optional
 *             "%{data.replacement1}",
 *             "%{data.replacement2}"
 *          ]
 *       }
 *
 * @param replacements (Object) an optional object used to provide replacements in the translated string. The
 *    message string may contain replacements, specified as '%{name}' within the string.  By default, the
 *    replacements are HTML escaped.  To not escape the replacement value, use '%{==name}'
 *
 * @param options (Object) an optional object that contains the following properties:
 *    escaping - if 'none', no escaping is done on the replacements or the translated string (use with caution as
 *       this may introduce security issues)
 *
 *    count - if specified, indicates that the message might need to use the 'plural' version from the messageKey.
 *       If count = 1 or options.count is not specified, the normal message is used from the key.  
 *       If count != 1 and the messageKey specifies the 'plural' version of the message, the plural is used.
 *
 * Examples:
 *    message                 replacements            returns
 *    ---------------------   --------------------    ----------------------------------
 *    My name is %{name}.     {name: 'Joe'}           My name is Joe.
 *    My name is %{name}.     {name: 'Joe'}    My name is <b>Joe</b>.
 *    My name is %{==name}.   {name: 'Joe'}    My name is Joe.
 * 
 * The process to translate a message is:
 *    1. If object specified as key, get message from the key
 *    2. Replace any occurrance of %{...} with {index} in message
 *    3. Lookup translation for message, if found, use translated message
 *    4. Replace each {index} with original %{...} replacement or replacement strings from messageKey
 *    5. Perform variable replacements
 *    6. return translated and replaced message
 */

I18N_MESSAGES

In some cases, you might want to reuse messages, or just put all messages to be replaced in the same place. In this case, you have to use the following variable defined in a javascript module so that the messages to be translated may be found by the messages mapper.

To use this format, instead of the simpler TRANSLATE() function format shown above, specify the messages as:

  var I18N_MESSAGES = {
     "message_1": "message_1",
     "message_2": "",
     "message_3": "translated message is different than the key",
     "message_4": {
       "message": "This is the actual message"
     }
  }

Then, the call to TRANSLATE becomes something like:

  TRANSLATE(I18N_MESSAGES['message_1']);

Using this special variable

The values in I18N_MESSAGES may be defined in several different ways as shown above. The value may be either a string or an object and matches the key parameter for the TRANSLATE function.

The string value may be the same as the key or different. This allows the code to be written with generic message keys so that the values may be changed without having to change the javascript code.

Templates

To support translations within a template, a special template tag is used to indicate a translatable string:

 <%~ My translation string goes here %>
 <%~ My translation string with %{data.item1} and %{data.item2} goes here %>

By default, the replacement items are HTML escaped, but the rest of the string is not translated. This allows complete control over creating the HTML within the template. There are cases however, where the translated string might need to contain HTML tags that should not be escaped. You could just define these within the translation string:

 <%~ My translation string with %{data.item1} and %{data.item2} goes here %>

But that means that someone responsible for doing the translation needs to ensure that they also include the HTML tags in their translated string and make sure they are in the right place. This becomes difficult, so a more complex format for specifying the translation string within a template is available:

 <%~ 
   [
     "My translation string with {1} and {2} goes here",
     "%{data.item1}",
     "%{data.item2}"
   ]
 %>

This format uses an array where the first item of the array is the translatable string. The subsequent elements of the array are the replacements for each of the {INDEX} identifiers. This allows very precise control over the translation string as well as the replacement values. In the case above, the data.item1 and data.item2 values would be HTML escaped but and would not, which is what is desired in this case.

An additional format is supported as well:

  <%~
   {
      message: "text message here {1} {2}",
      plural: "plural version of message here {1} {2}",
      count: "data.count",
      replacements: [
         "%{data.replacement1}",
         "%{data.replacement2}"
      ]
   }
  %>

For this format, the count, if specified, uses the value to determine what the count is so that either the message or plural string should be used. The replacements are the elements of the replacements array, in order (first element is replaced for {1}, etc.)

Test mode

When at least one language is defined and set as Active, the user menu (accessed by clicking on the user name in the upper right of the page) displays a list of Languages that you can change between and a Language test mode checkbox. When the Language test mode is checked, anywhere in the system that supports translation is displayed with a I18N: prefix. This allows you to quickly review the UI to see where translations are being used.

Translating to another language

In order to translate a system to another language, we must first gather all of the strings that need to be translated. As mentioned above, most of these strings are discoverable by the platform as they are defined in records that we know need to be translated (Labels, I18n Strings). In addition, the messages specified in calls to TRANSLATE() and within the I18N_MESSAGES variable of each of the user-developed objects are discoverable. To translate to a new language, the following steps are used:

  1. Within a bundle, click on the gears in the upper-right of the page
  2. Select Messages in the menu

This will display a list of all of the messages that are translatable. You will note that any messages that contain replacements (%{...}) are modified to show a simpler replacement identifier to make it easier to translate the messages without having to keep track of the exact replacement string. For example:

  My name is %{first_name} %{last_name}

becomes

  My name is {1} {2}

A translation might need to reorder the replacements by putting {2} before {1} based on language structure.

After the list of messages is displayed, you can click on the Create Translation File in order to generate a CSV file containing the list of messages to be translated. When you click this button, a dialog asking if you want to include only messages that do not already have a translation for a selected language. The CSV file contains the following columns:

  Language,Original Message,Translation(if it exists already)

This file is then used to define the translations for each messages. When that is complete,

  1. Within a bundle, click on the gears in the upper-right of the page
  2. If the language is not yet defined, click Languages
    1. Define the new language
  3. Return to the IMessages menu item and click the Load Translations button

You are then asked to specify the language being loaded and the file to be uploaded. This will load the translations into the system and make them available for use by user's when the select that language.

The details

messages.json

A catchall exists for a bundle if you have a translatable message that cannot be found using the techniques above. In this case, a file may be defined in the bundle directory as i18n/messages.json. This file contains a JSON object of messages that are included in the list of translatable messages when the Messages list is generated. This file is used by the internal ds.base bundle for all of the translatable messages generated by the Java code since they are not discoverable in the ways described above.


messages.LANGUAGE.json

The messages for a specific language are stored within each bundle directory as:

  i18n/messages_LANGUAGE.json

and contains a simple JSON object of message_string to traslation_string pairs.

messages_default.json

A special messages file named 'messages_default.json' may be used to override the default messages when no language is specified for the user. For example, if the messages_default.json file contained the following:

  {
     "OK": "Okay"
  }

Then anywhere in the UI that a message of "OK" was used, it would show up as "Okay" instead (unless the user has selected a specific language, as that language's translation would be used).