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.
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
_
functionThis 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
(% trans %)
statementMultiline 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 likefr
for French, orru
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.
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:
- 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 .
- 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 likefr
for French, orru
for Russian.
-
Edit the
.po
files with your new translations. -
Run
pybabel compile
to recompile your.mo
files.
pybabel compile --domain app --directory translation
Updated almost 2 years ago