aboutsummaryrefslogtreecommitdiffstats
path: root/sources/pyside6/doc/tutorials/basictutorial/translations.rst
blob: 21c16cdcdc5cce88e69aab33df1ba09010223d9b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
.. _translations:

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

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

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

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

The :ref:`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:

.. 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.

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:

.. code-block:: bash

    pyside6-project lupdate .

``.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

.. _Qt Linguist: https://doc.qt.io/qt-6/qtlinguist-index.html

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

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

The :ref:`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:

.. 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

.. _GNU gettext: https://docs.python.org/3/library/gettext.html