The Problem
Almost any application, at some level, faces the problem of being internationalizable/localizable in order to reach a big audience. One aspect of making an App internationalizable/localizable is by supporting multiple languages. This can be achieved by simply translating all the strings in the User Interface (UI) depending on the chosen language.
Technically speaking, there are multiple strategies that can be implemented in order to create a smooth translation without breaking any feature or touching the executable code. One way of achieving this is by using what we call the .PO/.POT files..
The Semantics
PO and POT files stand, respectively, for Portable Object File and Portable Object Template File. According to the GNU gettext Project, "The letters PO in .po files means Portable Object, to distinguish it from .mo files, where MO stands for Machine Object. This paradigm, as well as the PO file format, is inspired by the NLS standard developed by Uniforum, and first implemented by Sun in their Solaris system."
In Practice
The .POT file is the general template that can be used by programmers and translators to create a mapping between the default language (e.g. English) and a target language (e.g. French). An instance of this mapping, let's say from English to French, represents the .PO file. This PO file then can be parsed and loaded or compiled to MO format in order to serve the translation.
Those .PO files have the following format:
white-space
# translator-comments
#. extracted-comments
#: reference...
#, flag...
#| msgid previous-untranslated-string
msgid untranslated-string
msgstr translated-string
There are three important lines that we will focus on :
#: reference...
which points to the original string in the source codemsgid
is the keyword that specifies the original stringmsgstr
is the keyword that specifies the translation
For the use of the remaining lines, one can refer to the gettext documentation.
An instance of a valid entry may look like:
#: model:ir.model.fields,field_description:library.field_library_book__name
msgid "Name"
msgstr "Nom"
Here, the reference is identifying a string inside an Odoo module named "library" which contains a model named "Book". This model has a field named "name", and displayed as "Name", that will be translated into French and displayed as "Nom".
Identifying the references can differ depending on the used framework. For instance, Django uses the path of the source code file combined with the number of the line from which the string was used. As analogy of the previous example, this may look like:
#: library/book/models.py:19
msgid "Name"
msgstr "Nom"
"More" In Practice
Let's present a more complete example. Let's say that we are working on some Python application with the following code source:
from dataclasses import dataclass
import gettext
import logging
fr_i18n = gettext.translation("person", "./locale", languages=["fr"])
fr_i18n.install()
logger = logging.getLogger(__name__)
@dataclass
class Person:
name: str
age: int
def __post_init__(self):
if self.age < 0 and self.name != "Hector Barbossa":
logger.warning(
_("Age must be a positive integer ; except if you are cursed!")
)
self.age = 0
if __name__ == "__main__":
jack = Person("Jack Sparrow", -42)
hector = Person("Hector Barbossa", -58)
will = Person("Will Turner", 27)
This application displays a warning when the age of a person is negative ; except of course if this person is cursed!
We put our Python source code inside a file called person.py
and we run python person.py
. This should display the following message: Age must be a positive integer ; except if you are cursed!
. We suppose of course that Jack Sparrow still has not taken any gold piece from Hernán Cortés treasure 😀
We want to display the same message but in French. We assume that you are on some Linux environment and that you have gettext installed. We create a folder named locale
which will contain our translation files.
Next, we generate the POT file by executing the following command:
xgettext --verbose --language=Python person.py --output=locale/person.pot
This will generate our template which will look like:
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-10-25 10:37+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
#: person.py:20
msgid "Age must be a positive integer ; except if you are cursed!"
msgstr ""
Now we can use this template to translate the strings into our desired language (i.e. French). In order to abide to some conventions, we will create a new folder inside locale
named fr
. Then another named LC_MESSAGES
. This last one will contain our PO file named person.po
which holds the French translation:
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-10-25 10:37+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: person.py:20
msgid "Age must be a positive integer ; except if you are cursed!"
msgstr "L'âge doit être un nombre entier positif ; sauf si vous êtes frappé d'une malédiction !"
Finally, to serve our translation, we will generate the binary MO file using the following command:
msgfmt locale/fr/LC_MESSAGES/person.po -o locale/fr/LC_MESSAGES/person.mo
In order to avoid any encoding issues, make sure that charset
in the PO file is set to UTF-8
.
Now, if we run python person.py
the message displayed should be: L'âge doit être un nombre entier positif ; sauf si vous êtes frappé d'une malédiction !