Test Internationalization API

Overview

The purpose of test internationalization (i18n) API is to have localized text on UI of each pytest, and user can choose between different languages on UI.

Quick start

A typical workflow to add or modify text in test:

  1. Depends on where the text is used, use language-specific method to mark the text:

  • Python: Use cros.factory.test.i18n._():

    from cros.factory.test.i18n import _
    from cros.factory.test import test_ui
    
    class SomeTest(unittest.TestCaseWithUI):
      def setUp(self):
        self.ui.SetState(_('Some displayed text and should be translated.'))
    
  • JavaScript: Use window._() (This is automatically injected to test iframe by Goofy):

    const label = _('Some displayed text and should be translated.')
    window.template.appendChild(cros.factory.i18n.i18nLabelNode(label))
    
  • Static HTML: Use <i18n-label>:

    <test-template>
      <i18n-label>Some displayed text and should be translated.</i18n-label>
    </test-template>
    
  • JSON test list items: Prefix the text with "i18n! ":

    {
      "definitions": {
        "SomeTest": {
          "pytest_name": "some_test",
          "args": {
            "html": "i18n! Some displayed text and show be translated."
          }
        }
      }
    }
    
  1. In chroot under ~/trunk/src/platform/factory/po, run:

    make update
    

    Please refer to Manage translation files for detail if code in board overlay is modified, or translation of new locale should be added.

  2. Edit the translation files ~/trunk/src/platform/factory/po/*.po. Currently there’s only one file zh-CN.po that needs to be edited.

    The format of these files is in GNU gettext PO Files.

    Find the line similar to lines below, and add translation to it:

    #: ../py/test/pytests/my_awesome_test.py
    msgid "Some displayed text and should be translated."
    msgstr "The translated text, in zh-CN.po this should be Simplified Chinese"
    

    Also remove all #, fuzzy lines, and remove all unused lines at the end of file started with #~.

    The po file would be bundled together with the code when factory toolkit is made.

  3. Run make update again to make sure the po file is formatted correctly.

  4. Refer to language specific section on how to use the i18n API to display the translated text: I18n in Python pytest, I18n in JavaScript, I18n in static HTML and I18n in JSON test list. (Actually, this should already be done together with step 1.)

I18n in Python pytest

All literal strings that need to be translated should be passed as the first argument of _().

The return value of _() is a translation dict, that is, a plain python dict with locale name as key, and translated string as value.

For example, the value of _('Cancel') is {'en-US': 'Cancel', 'zh-CN': '取消'}. The returned translation dict can be treated as an opaque object most of the time.

Format string in i18n text

When there are keyword argument passed to _(), the function would do string format similar to Python str.format(). If string format is always needed even there’s no keyword argument passed, use StringFormat(). For example:

self.ui.SetState(
    _('In test {test}, run {run_id}', test=_('Some test name'), run_id=1))

if x_exists:
  format_string = _('{{x}} = {x}')
  kwargs = {'x': x}
else:
  format_string = _('{{x}} is not here!')
  kwargs = {}
self.ui.SetState(i18n.StringFormat(format_string, **kwargs))

Display the i18n text on UI

To display the i18n text on UI, for methods that manipulate HTML in StandardUI (SetHTML(), AppendHTML(), SetTitle(), SetState(), SetInstruction()), the argument html accepts either a single string as HTML, a single translation dict, or an arbitrary nested list of either HTML string or translation dict. For example:

self.ui.SetTitle(_('Some text'))
# There's no need to "concatenate" the text.
button = ['<button>', _('Click me!'), '</button>']
self.ui.SetState(['<div>', _('A button here: '), button, '</div>'])

Internationalized test argument

Sometimes, test need to have arguments that accepts internationalized text. Use I18nArg instead of cros.factory.utils.arg_util.Arg in this case.

User can pass a plain string, or a translation dict to the argument. The argument in self.args is either None or a translation dict no matter what the user passed in.

For example:

from cros.factory.test.i18n import _
from cros.factory.test.i18n import arg_utils as i18n_arg_utils
from cros.factory.test import test_ui

class MyTest(test_ui.TestCaseWithUI):
  ARGS = [
    i18n_arg_utils.I18nArg('text', 'Some text', default=_('Default text'))
  ]

  def setUp(self):
    self.ui.SetState(['text: ', self.args.text])

Manipulating i18n text

There are several utility functions for transforming and combining translation dict, for example:

i18n.StringFormat('{a}[{b}]', a=1, b=_('Cancel'))
# => {'en-US': '1[Cancel]', 'zh-CN': '1[取消]'}
i18n.HTMLEscape({'en-US': '&<>', 'zh-CN': '&<>'})
# => {'en-US': '&amp;&lt;&gt;', 'zh-CN': '&amp;&lt;&gt;'}

See Method reference for detail.

I18n in JavaScript

All literal strings that need to be translated should be passed as the first argument of window._().

The i18n API is under the namespace cros.factory.i18n(), and is similar to Python API with three twists:

  1. The method names start with lowercase letter. For example, cros.factory.i18n.stringFormat() instead of StringFormat().

  2. Since JavaScript doesn’t kave keyword arguments, window._() support an additional argument of mapping from name to values. For example:

    _('Test {test}, run {id}', {test: _('some test'), id: 1})
    
  3. Since there’s no standarized “display” methods in JavaScript, two additional methods are introduced:

    • cros.factory.i18n.i18nLabel(), which takes a string or translation dict, and returns a goog.html.SafeHtml() from closure library.

    • cros.factory.i18n.i18nLabelNode(), which takes a string or translation dict, and returns a Node() that can be inserted to document directly.

    Note that the first argument of these two functions do not need to be marked by window._(). For example:

    const div = document.getElementById('some-div')
    goog.dom.safe.setInnerHtml(cros.factory.i18n.i18nLabel('some text'))
    goog.dom.safe.appendChild(cros.factory.i18n.i18nLabelNode('other text'))
    goog.dom.safe.appendChild(
        cros.factory.i18n.i18nLabelNode(
            _('Test {test}, run {id}', {test: _('some test'), id: 1})))
    

I18n in static HTML

Use <i18n-label> for text element that need to be translated. The i18n-label is a custom HTML tag that acts like a span, and styles can be applied on it like a span. For example:

<i18n-label id="some-label" style="color: red">Some red text</i18n-label>

would show a red text according to current selected language.

Note that all continuous space characters inside the tag would be replaced by one space, and leading / trailing spaces are removed, so the following two snippets are equivalent:

<i18n-label>I am some text</i18n-label>
<i18n-label>
  I am
  some
  text
</i18n-label>

I18n in JSON test list

All string in either label or args can be prefixed by "i18n! " to make it an internationalized text. Those string would be extracted to po files automatically, and pytest can use I18nArg to handle this kind of argument. See Internationalized test argument for detail.

Example:

{
  "definitions": {
    "MyTest": {
      "pytest_name": "my_test",
      "label": "i18n! My Special Test",
      "args": {
        "text": "i18n! I'm an argument!",
        "other_arg": {
          "something": ["i18n! Transform is done recursively like eval!."]
        }
      }
    }
  }
}

Since the translation dict is a normal dict, a dict can be passed directly from JSON. This is not advised since this makes it much harder to modify translation or add new locale support, but can be useful when developing or debugging. For example:

{
  "label": {
    "en-US": "Inline translation dict. Don't do this except for debugging!",
    "zh-CN": "..."
  }
}

Manage translation files

See Localization for ChromeOS Factory Software for detail on how to update or add .po files.

Method reference

cros.factory.test.i18n.translation.DEFAULT_LOCALE = 'en-US'

The default locale used in code.

cros.factory.test.i18n._(_format_string, **kwargs)

Wrapper for i18n string processing.

This function acts as Translation() when no kwargs is given, and as StringFormat() when kwargs is given. This function is also a marker for pygettext.

cros.factory.test.i18n.Translated(obj, translate=True)

Ensure that the argument is a translation dict, pass it to Translation() or NoTranslation() if it isn’t.

This will also make sure that the return translation dict contains all supported locales. The value of DEFAULT_LOCALE would be used to fill in locales not in the input obj regardless of the argument translate.

Parameters
  • obj – The string to be translated, or the translation dict.

  • translate – True to pass things that are not translation dict to Translation(), False to pass to NoTranslation().

Returns

The translation dict.

cros.factory.test.i18n.StringFormat(_format_string, **kwargs)

Do format string on a translation dict.

Parameters
  • _format_string – The format string used, can either be a string (would be passed to Translation()), or a translation dict.

  • kwargs – arguments of format string.

Example:

StringFormat('{str1} {str2}', str1='String-1', str2=_('String-2'))

If the text '{str1} {str2}' has translation '{str1}-{str2}' in zh-CN, the text 'String-1' has translation 'Text-1' in zh-CN, the text 'String-2' has translation 'Text-2' in zh-CN, then the returned translation dict would be:

{
  'en-US': 'String-1 String-2',
  'zh-CN': 'String-1-Text-2'
}
cros.factory.test.i18n.NoTranslation(obj)

Get a translation dict that maps the input unmodified for all supported locales.

Used to explicitly set an object as don’t translate when passing to various i18n functions.

Parameters

obj – The object to be used.

Returns

The translation dict for all supported locales.

cros.factory.test.i18n.Translation(text)

Get the translation dict in all supported locales of a string.

Parameters

text – The string to be translated.

Returns

The translation dict for all supported locales.

cros.factory.test.i18n.HTMLEscape(text)

HTML-escape all entries in a given translation dict.

Parameters

text – The translation dict to be HTML-escaped.

Returns

The new translation dict with all values HTML-escaped.

cros.factory.test.i18n.arg_utils.I18nArg(name, help_msg, default=_DEFAULT_NOT_SET)

Define an argument for i18n text.

The argument should either be a string that would be passed to Translation(), or a dict looks like:

{
  'en-US': 'Default English value',
  'zh-CN': 'Chinese Translate'
}

The key 'en-US' is mandatory and would be used for locales that don’t have value specified.

Parameters
  • name – The name of the argument.

  • help_msg – The help message of the argument.

  • default – The default value of the message.

Returns

The arg_utils.Arg object.