.. _translations:

Translating Applications
========================

.. image:: translations.png
   :alt: Translation Image

Qt Linguist
-----------

`Qt Linguist <https://doc.qt.io/qt-6/qtlinguist-index.html>`_ and
its related tools can be used to provide translations for applications.

The ``examples/widgets/linguist`` 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:

.. code-block:: python

    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:

.. code-block:: bash

    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:

.. code-block:: bash

    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.

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

.. code-block:: bash

    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:

.. code-block:: bash

    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 :ref:`using_qrc_files`).
The resource file ``linguist.qrc`` provides the ``example_de.qm``
under ``:/translations``:

.. code-block:: xml

    <!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:

.. code-block:: python

    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:

.. code-block:: bash

    LANG=de python main.py

GNU gettext
-----------

The `GNU gettext <https://docs.python.org/3/library/gettext.html>`_ module
can be used to provide translations for applications.

The ``examples/widgets/gettext`` 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:

.. code-block:: python

    import gettext
    ...
    _ = None
    ngettext = None

and later assigned as follows:

.. code-block:: python

    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:

.. code-block:: python

    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:

.. code-block:: python

    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:

.. code-block:: bash

    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.

.. code-block:: bash

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

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

.. code-block::

    "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:

.. code-block::

    #: 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:

.. code-block:: bash

    msgfmt -o example.mo example.pot

The example can then be run in German:

.. code-block:: bash

    LANG=de python main.py
