.. _test-i18n-api: Test Internationalization API ============================= .. py:module:: cros.factory.test 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 :func:`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 :func:`window._` (This is automatically injected to test iframe by Goofy): .. code-block:: javascript const label = _('Some displayed text and should be translated.') window.template.appendChild(cros.factory.i18n.i18nLabelNode(label)) * Static HTML: Use ````: .. code-block:: html Some displayed text and should be translated. * JSON test list items: Prefix the text with ``"i18n! "``: .. code-block:: json { "definitions": { "SomeTest": { "pytest_name": "some_test", "args": { "html": "i18n! Some displayed text and show be translated." } } } } 2. In chroot under ``~/trunk/src/platform/factory/po``, run: .. code-block:: bash 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. 3. 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: .. code-block:: none #: ../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. 4. Run ``make update`` again to make sure the po file is formatted correctly. 5. 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 :func:`~i18n._`. The return value of :func:`~i18n._` is a *translation dict*, that is, a plain python :class:`dict` with locale name as key, and translated string as value. For example, the value of ``_('Cancel')`` is :code:`{'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 :func:`~i18n._`, the function would do string format similar to Python :func:`str.format`. If string format is always needed even there's no keyword argument passed, use :func:`~i18n.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 :class:`~test_ui.StandardUI` (:func:`~test_ui.UI.SetHTML`, :func:`~test_ui.UI.AppendHTML`, :func:`~test_ui.StandardUI.SetTitle`, :func:`~test_ui.StandardUI.SetState`, :func:`~test_ui.StandardUI.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 = [''] self.ui.SetState(['
', _('A button here: '), button, '
']) Internationalized test argument ``````````````````````````````` Sometimes, test need to have arguments that accepts internationalized text. Use :class:`~test.i18n.arg_utils.I18nArg` instead of :class:`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': '&<>', 'zh-CN': '&<>'} See `Method reference`_ for detail. I18n in JavaScript ------------------ All literal strings that need to be translated should be passed as the first argument of :func:`window._`. The i18n API is under the namespace :js:class:`cros.factory.i18n`, and is similar to Python API with three twists: 1. The method names start with lowercase letter. For example, :js:func:`cros.factory.i18n.stringFormat` instead of :js:func:`StringFormat`. 2. Since JavaScript doesn't kave keyword arguments, :js:func:`window._` support an additional argument of mapping from name to values. For example: .. code-block:: javascript _('Test {test}, run {id}', {test: _('some test'), id: 1}) 3. Since there's no standarized "display" methods in JavaScript, two additional methods are introduced: * :js:func:`cros.factory.i18n.i18nLabel`, which takes a string or translation dict, and returns a :js:class:`goog.html.SafeHtml` from `closure library `_. * :js:func:`cros.factory.i18n.i18nLabelNode`, which takes a string or translation dict, and returns a :js:class:`Node` that can be inserted to document directly. Note that the first argument of these two functions do not need to be marked by :func:`window._`. For example: .. code-block:: javascript 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 ```` 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: .. code-block:: html Some red text 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: .. code-block:: html I am some text .. code-block:: html I am some text 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 :class:`I18nArg` to handle this kind of argument. See `Internationalized test argument`_ for detail. Example: .. code-block:: json { "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 :class:`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: .. code-block:: json { "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 ---------------- .. autodata:: cros.factory.test.i18n.translation.DEFAULT_LOCALE .. py:module:: cros.factory.test.i18n .. autofunction:: _ .. autofunction:: Translated .. autofunction:: StringFormat .. autofunction:: NoTranslation .. autofunction:: Translation .. autofunction:: HTMLEscape .. py:module:: cros.factory.test.i18n.arg_utils .. autofunction:: I18nArg(name, help_msg, default=_DEFAULT_NOT_SET)