Translating Applications

Translation Image

Qt Linguist

Qt Linguist and its related tools can be used to provide translations for applications.

The Qt Linguist Example example illustrates this. The example is very simple, it has a menu and shows a list of programming languages with multiselection.

Translation works by passing the message strings through function calls that look up the translation. Each QObject instance provides a tr() function for that purpose. There is also QCoreApplication.translate() for adding translated texts to non-QObject classes.

Qt ships its own translations containing the error messages and standard dialog captions.

The linguist example has a number of messages enclosed in self.tr(). The status bar message shown in response to a selection change uses a plural form depending on a count:

count = len(self._list_widget.selectionModel().selectedRows())
message = self.tr("%n language(s) selected", "", count)

The translation workflow for the example is as follows: The translated messages are extracted using the lupdate tool, producing XML-based .ts files:

pyside6-lupdate main.py -ts example_de.ts

If example_de.ts already exists, it will be updated with the new messages added to the code in-between.

If there are form files (.ui) and/or QML files (.qml) in the project, they should be passed to the pyside6-lupdate tool as well:

pyside6-lupdate main.py main.qml form.ui -ts example_de.ts

The source files generated by pyside6-uic from the form files should not be passed.

The lupdate mode of pyside6-project can also be used for this. It collects all source files and runs pyside6-lupdate when .ts file(s) are given in the .pyproject file:

pyside6-project lupdate .

.ts files are translated using Qt Linguist. Once this is complete, the files are converted to a binary form (.qm files):

pyside6-lrelease example_de.ts -qm example_de.qm

pyside6-project will build the .qm file automatically when .ts file(s) are given in the .pyproject file:

pyside6-project build .

To avoid having to ship the .qm files, it is recommend to put them into a Qt resource file along with icons and other applications resources (see Using .qrc Files (pyside6-rcc)). The resource file linguist.qrc provides the example_de.qm under :/translations:

<!DOCTYPE RCC><RCC version="1.0">
<qresource prefix="translations">
    <file>example_de.qm</file>
</qresource>
</RCC>

At runtime, the translations need to be loaded using the QTranslator class:

path = QLibraryInfo.location(QLibraryInfo.TranslationsPath)
translator = QTranslator(app)
if translator.load(QLocale.system(), 'qtbase', '_', path):
    app.installTranslator(translator)
translator = QTranslator(app)
path = ':/translations'
if translator.load(QLocale.system(), 'example', '_', path):
    app.installTranslator(translator)

The code first loads the translations shipped for Qt and then the translations of the applications loaded from resources.

The example can then be run in German:

LANG=de python main.py

GNU gettext

The GNU gettext module can be used to provide translations for applications.

The GNU gettext Example example illustrates this. The example is very simple, it has a menu and shows a list of programming languages with multiselection.

Translation works by passing the message strings through function calls that look up the translation. It is common to alias the main translation function to _. There is a special translation function for sentences that contain a plural form depending on a count (“{0} items(s) selected”). It is commonly aliased to ngettext.

Those functions are defined at the top:

import gettext
# ...
_ = None
ngettext = None

and later assigned as follows:

src_dir = Path(__file__).resolve().parent
try:
    translation = gettext.translation('example', localedir=src_dir / 'locales')
    if translation:
        translation.install()
        _ = translation.gettext
        ngettext = translation.ngettext
except FileNotFoundError:
    pass
if not _:
    _ = gettext.gettext
    ngettext = gettext.ngettext

This specifies that our translation file has the base name example and will be found in the source tree under locales. The code will try to load a translation matching the current language.

Messages to be translated look like:

file_menu = self.menuBar().addMenu(_("&File"))

The status bar message shown in response to a selection change uses a plural form depending on a count:

count = len(self._list_widget.selectionModel().selectedRows())
message = ngettext("{0} language selected",
                   "{0} languages selected", count).format(count)

The ngettext() function takes the singular form, plural form and the count. The returned string still contains the formatting placeholder, so it needs to be passed through format().

In order to translate the messages to say German, a template file (.pot) is first created:

mkdir -p locales/de_DE/LC_MESSAGES
xgettext -L Python -o locales/example.pot main.py

This file has a few generic placeholders which can be replaced by the appropriate values. It is then copied to the de_DE/LC_MESSAGES directory.

cd locales/de_DE/LC_MESSAGES/
cp ../../example.pot .

Further adaptions need to be made to account for the German plural form and encoding:

"Project-Id-Version: PySide6 gettext example\n"
"POT-Creation-Date: 2021-07-05 14:16+0200\n"
"Language: de_DE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"

Below, the translated messages can be given:

#: main.py:57
msgid "&File"
msgstr "&Datei"

Finally, the .pot is converted to its binary form (machine object file, .mo), which needs to be deployed:

msgfmt -o example.mo example.pot

The example can then be run in German:

LANG=de python main.py