Multilingual translation

How to make an app that automatically translates text in the user's language

Making sure your app can speak your user's language makes it more likely they'll engage with the app. Fortunately, translating app content into any language is easy on the Meya platform. Keep reading to learn how.

640

Testing a welcome message in English and French

Flagging content for translation

The first step in the translation process is to identify, or flag, content that needs to be translated.

Python

To translate content that appears within a Python file, import gettext from meya.util.translation and apply the function to the content.

Example:

from dataclasses import dataclass
from typing import List

from meya.component.element import Component
from meya.element.field import element_field
from meya.entry import Entry
from meya.text.event.say import SayEvent
from meya.util.translation import gettext as _


@dataclass
class WelcomeComponentElement(Component):

    async def start(self) -> List[Entry]:
      	text = _("Hi!")
        text_event = SayEvent(text=text)
        return self.respond(text_event)

Special cases

There are special cases in Python when you might want the text to appear translated in other contexts, but don't want the Python file to be altered.

One example would be an enum where the values are displayed to the user in other parts of your code, but should have an unchanging value within the enum. In this case, you can use the gettext_noop function to flag the text as something that requires a translation, but shouldn't be altered in this context.

Example:

from enum import Enum

from meya.util.translation import gettext_noop as _


class UserType(Enum):
    ROBOT = _("Robot")
    HUMAN = _("Human")

BFML

There are two ways of flagging text for translation in BFML.

The _ function

This method can be used for short pieces of text that take up a single line in your BFML code:

steps:
  - say: (@ _("Hi!") )

📘

Syntax

Notice that the text needs to be wrapped in double quotes ".

📘

Using functions in BFML

Because the _ is a function, it needs to be wrapped in Jinja2 delimiters: (@ and ).

The (% trans %) statement

Multiline strings should be wrapped with the (% trans %) and (% endtrans %) statements, like this:

steps:
	- say: >
  	(% trans %) This is a long string of text.
      It covers multiple lines. 
      This is the last line. (% endtrans %)

Variable replacement in translation strings

Often your need to render a variable in a string to the user e.g. - say: Hi, (@ user.name)

If you want to translate this string, you'll need to use a special substitution syntax to account for the variables.

steps:
  - say: >
      (% trans name=user.name %)
      Hi, (@ name)!
      (% endtrans %)

Here is how you do it in Python:

from dataclasses import dataclass
from typing import List

from meya.component.element import Component
from meya.element.field import element_field
from meya.entry import Entry
from meya.text.event.say import SayEvent
from meya.util.translation import gettext as _


@dataclass
class WelcomeComponentElement(Component):

    async def start(self) -> List[Entry]:
      	text = _("Hi, %(name)!") % dict(name=self.user.name)
        text_event = SayEvent(text=text)
        return self.respond(text_event)

Translation config

Now that you've flagged all of the strings that need to be translated, you can generate files where you'll store the translated content. To do this, you'll be using a tool called PyBabel.

First, let's set up the PyBabel config file. In your app's root folder, create a file called translation_mapping.ini and copy this code into it:

[ignore: venv/**]

[python: **.py]

[bfml: **.yaml]

This file states that files with the extension .py at any level inside the directory should be processed by the python extraction method, and .yaml files with the bfml extraction method. Files that don’t match any of the mapping patterns are ignored.

Extract translations

Next, we'll tell PyBabel to look through our app files and extract text that's been flagged for translation.

Open a terminal in your app's root folder and run these commands:

mkdir translation
pybabel extract --omit-header --mapping-file translation_mapping.ini --output-file translation/app.pot .

If you examine the contents of translation/app.pot, you'll see it contains all of the text that needs to be translated along with the file names and line numbers where the text was found.

🚧

Don't edit app.pot

Don't put translations directly in the app.pot file.

📘

If the same piece of text appears more than once, it will be consolidated into one entry in your .pot file. This is really handy for standard text you'll re-use throughout your app (e.g. Okay, Cancel) since it means you'll only need to translate these strings once.

Initialize translations

Next, we need to create a translation file for each language we want to translate the content into.

To do this, run this command in your terminal:

pybabel init --domain app --input-file translation/app.pot --output-dir translation --locale LOCALE

📘

Replace LOCALE with a two-letter language code like fr for French, or ru for Russian.

❗️

Only run this once per locale

Running this command again will overwrite any translations you've added to the .po file.

If you've updated translated content in your Python and BFML code and want to update your translation files to reflect the changes, run the pybabel update command shown in the Updating translations section below.

This command will generate a .po file in the translations/<LOCALE>/LC_MESSAGES folder. If you open the file, you'll see a number of entries that look similar to this:

#: component/hello_component.py:15
msgid "Hi!"
msgstr ""

Replace the empty msgstr string with the appropriate translation, like this:

#: component/hello_component.py:15
msgid "Hi!"
msgstr "Salut!"

Once you've entered all of your translations, save the file, and continue with the Compile section below.

Compile translations

In this last step, we'll create an .mo file, which compiles all of our translations.

🚧

Whenever you update the .po file, you'll need to re-compile the .mo file.

pybabel compile --domain app --directory translation

Create a debug flow

When creating translations, it is helpful to be able to switch between languages easily. You can do that by creating a flow that updates user.language to a valid language code.

In your app's flow folder, create a folder called debug. Inside the new folder, create a file called language.yaml and copy this code into it:

triggers:
  - keyword: _language
  - keyword: _french
    action:
      jump: set
      data:
        language: fr
  - keyword: _english
    action:
      jump: set
      data:
        language: en

steps:
  - ask: Enter a language code
  - flow_set: language

  - (set)
  - user_set:
      language: (@ flow.language )
  - say: (@ _("Language updated") )

In this example, you can switch between English and French by typing _english or _french.

Save the file and push it to the Grid using these commands in your terminal:

meya format
meya push

Test it out

In your app's simulator, verify that the default language still works. Then try switching languages with the language flow created in the previous step.

640

👍

Awesome! You can now create a multilingual app.

Updating translations

As you update your app code, you may add, modify, or remove content that is flagged for translation. You'll need to update the translation files as well. Here's how:

  1. Run pybabel extract to update your .pot file with the latest file names, line numbers, and message IDs.
pybabel extract --omit-header --mapping-file translation_mapping.ini --output-file translation/app.pot .
  1. Run pybabel update to update your .po files.
pybabel update --domain app --input-file translation/app.pot --output-dir translation --locale LOCALE

📘

Replace LOCALE with a language code like fr for French, or ru for Russian.

  1. Edit the .po files with your new translations.

  2. Run pybabel compile to recompile your .mo files.

pybabel compile --domain app --directory translation