Impossible Translations

Hi,

Since 15 days, I have worked on Translations for one of my project. All was working nearly fine, I have just forgotten to translate in my pro file my ui files so I 've had a partial translation. Not a really big issue.
After including my ui files in my pro file, I’ve been able to translate then and generate my qm files like usual. I’ve decided to move them in a translation folder for reaching my final structure. Ui and Python files are in separated folder at the same level and my start_app.py file run my app without any issue. See the screenshot for project structure.

What is surrounded in red does not exist in my final structure. It was just for testing but even so it doesn’t work.

I’ve tried several combination in my start_app file but nothing until now. Initially, I was thinking that just a path problem. But Even if I have qm files at the same level than start_app file it doesn’t work.

I precise that I’m an on Linux Manjaro with Python 3.8.3, Qt 5.15.0, PyQt5 5.15.0

Now, I don’t know what doing. I’m coming completely crazy with this issue. If somebody can help me, that will be great.

PS it is a minimal example.

Here my code of my start_app.py. Like you can see,I’ve tried several things.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import QTranslator, QLocale, QLibraryInfo, QDir

from windows.minimal import MiniMal


if __name__ == "__main__":
application = QApplication(sys.argv)

# Translate application
# locale = QLocale.system().name()
# TranslationsPath = QDir('./translations').canonicalPath()
# translator = QTranslator()
# path = QLibraryInfo.location(QLibraryInfo.TranslationsPath)
# translator.load(locale, path)
# if len(sys.argv) == 1:
#     # locale = QLocale().system().name()
#     translator.load(locale, "minimal", ".")
#     # translator.load('qt_%s' % locale, path)
# else:
#     translator.load("minimal."+ sys.argv[1])
#     # translator.load(locale + sys.argv[1])
# application.installTranslator(translator)
enNativeLanguage = len(sys.argv) == 1
if enNativeLanguage:
    locale = QLocale().system().name()
else:
    languageCountry = sys.argv[1]
translators = []
for prefixQm in ("minimal.", "qt_", "qtbase_"):
    translator = QTranslator()
    translators.append(translator)
    if enNativeLanguage:
        translator.load(locale, prefixQm)
    else:
        translator.load(prefixQm+languageCountry)
    application.installTranslator(translator)

window = MiniMal()
window.show()
rc = application.exec_()
sys.exit(rc)

Here the code of the minimal.py (in the folder called windows)

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
from PyQt5.QtWidgets import QMainWindow, QMessageBox
from ui.exempleui import Ui_MainWindow


class MiniMal(QMainWindow):
    def __init__(self, *args, **kwargs):
        super(MiniMal, self).__init__(*args, **kwargs)
        self.setupUi()
        self.connectActions()

        format_capture = ['Dv Raw (.dv)', 'DV 2 (.avi)', 'Dv (.avi)', 'Hdv (.m2t)', 'Mpeg 2 (.mpg)', 'Mov (.mov)']
        for format in format_capture:
            self.ui.cmbformatacquisition.addItem(format)
            self.ui.cmbformatacquisition.setCurrentIndex(0)

    def setupUi(self):

        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        self.ui.lnetext.setText(self.tr("My Awesome Movie"))
        self.ui.btnsomething.setToolTip(self.tr("Choose the Dv Acquisition mode"))
        self.ui.lnetext.setToolTip(self.tr("Set the Film Name"))
        self.ui.cmbformatacquisition.setToolTip(self.tr("Choose a format"))
        self.ui.chksystray.setToolTip(self.tr("Application in the systray"))
        self.ui.lnetext.setStatusTip(self.tr('The Film name is '))
        self.ui.btnsomething.setStatusTip(self.tr('Dv Acquisition mode is used'))
        self.ui.cmbformatacquisition.setStatusTip(self.tr("Acquisition Mode Changed"))
        self.ui.chksystray.setStatusTip(self.tr('Show this window in the systray'))

    def connectActions(self):
        self.ui.cmbformatacquisition.currentIndexChanged.connect(self.callSomething)
        self.ui.lnetext.textChanged.connect(self.showName)
        self.ui.chksystray.toggled.connect(self.putSystray)
        self.ui.btnsomething.clicked.connect(self.callSomething)

    def changeChoice(self):
        pass

    def putSystray(self):
        pass

    def showName(self):
        pass

    def callSomething(self):
        pass

Here the code of the exempleui.py (in the folder called ui)

# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'exemple.ui'
#
# Created by: PyQt5 UI code generator 5.14.2
#
# WARNING! All changes made in this file will be lost!


from PyQt5 import QtCore, QtGui, QtWidgets


class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(560, 339)
        MainWindow.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedKingdom))
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.lnetext = QtWidgets.QLineEdit(self.centralwidget)
        self.lnetext.setGeometry(QtCore.QRect(210, 30, 251, 26))
        self.lnetext.setObjectName("lnetext")
        self.label = QtWidgets.QLabel(self.centralwidget)
        self.label.setGeometry(QtCore.QRect(36, 30, 101, 20))
        self.label.setObjectName("label")
        self.btnsomething = QtWidgets.QPushButton(self.centralwidget)
        self.btnsomething.setGeometry(QtCore.QRect(70, 90, 361, 71))
        self.btnsomething.setObjectName("btnsomething")
        self.cmbformatacquisition = QtWidgets.QComboBox(self.centralwidget)
        self.cmbformatacquisition.setGeometry(QtCore.QRect(80, 175, 351, 51))
        self.cmbformatacquisition.setObjectName("cmbformatacquisition")
        self.chksystray = QtWidgets.QCheckBox(self.centralwidget)
        self.chksystray.setGeometry(QtCore.QRect(60, 250, 371, 26))
        self.chksystray.setObjectName("chksystray")
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 560, 28))
        self.menubar.setObjectName("menubar")
        self.menuFile = QtWidgets.QMenu(self.menubar)
        self.menuFile.setObjectName("menuFile")
        self.menuHelp = QtWidgets.QMenu(self.menubar)
        self.menuHelp.setObjectName("menuHelp")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)
        self.actionQuit = QtWidgets.QAction(MainWindow)
        self.actionQuit.setObjectName("actionQuit")
        self.actionAbout = QtWidgets.QAction(MainWindow)
        self.actionAbout.setObjectName("actionAbout")
        self.menuFile.addAction(self.actionQuit)
        self.menuHelp.addAction(self.actionAbout)
        self.menubar.addAction(self.menuFile.menuAction())
        self.menubar.addAction(self.menuHelp.menuAction())

        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
        self.label.setText(_translate("MainWindow", "Name"))
        self.btnsomething.setText(_translate("MainWindow", "Click me"))
        self.chksystray.setText(_translate("MainWindow", "Put in this window in the systray"))
        self.menuFile.setTitle(_translate("MainWindow", "File"))
        self.menuHelp.setTitle(_translate("MainWindow", "Help"))
        self.actionQuit.setText(_translate("MainWindow", "Quit"))
        self.actionAbout.setText(_translate("MainWindow", "About"))


if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    MainWindow = QtWidgets.QMainWindow()
    ui = Ui_MainWindow()
    ui.setupUi(MainWindow)
    MainWindow.show()
    sys.exit(app.exec_())

@martin
BTW Still an idea for the next version of the book.

Sorry for the delay in answering this, needed a couple of days to recover from the book :wink: This will be in the next update, because I know how horrible it can be. It’s been a while since I did it, and had to go digging into an old projects, but I found this snippet that I know was working.

This loads the base Qt translations, and then custom translations.

# Get the locale (language) name
locale = QLocale.system().name()

# Load base QT translations from the normal place
translator_qt = QTranslator()
if translator_qt.load("qt_%s" % locale, QLibraryInfo.location(QLibraryInfo.TranslationsPath)):
    app.installTranslator(translator_qt)

# Load custom translations
translator_custom = QTranslator()
if translator_custom .load("custom_%s" % locale, 'translations'):
        app.installTranslator(translator_custom )

In this setup I had a folder translations which contained all my custom translations. I used the current scripts folder to construct that path (for when it was packaged), perhaps you’ll see something similar?

The following lines confuse me a bit

    if enNativeLanguage:
        translator.load(locale, prefixQm)

…as this means if you pass the language on the command line, it doesn’t prefix the files, but looks in a folder named with the prefix (right?) …that doesn’t seem right.

No worries. I understand and what great work. :heart_eyes:
To answer your question about the file, that’s correct. :wink:

For my part, I came to a fairly close conclusion in the following way.
It is true that it is annoying when it does not work because we have no errors
therefore no way to resolve this situation. We can only fumble. I am
almost arrived at the same conclusion with the following code by taking all the code, unfortunately
I haven’t had time to test yet:


locale_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "translations")
locale = QLocale().system().name()

qt_locale_path = QLibraryInfo.location(QLibraryInfo.TranslationsPath)

locale = os.path.join (locale_path, locale)

To answer your question about the file, that’s correct. It was badly formatted and Drop this part. It was an error.

I too have delved into a project on which I had introduced the translations
at least October but the situation is different because there is no ui file and no
translations folder. Everything is on the same level. And strangely, it doesn’t work now, has
when I’m sure it worked in October. I even checked if it didn’t
was not from my version of QT / PyQt but at first glance not.

I also even asked myself the question if there was not a problem with my pro file, my ts files and therefore qm.

SOURCES + = ../windows/minimal.py ../ui/exempleui.py
TRANSLATIONS + = ../translations/minimal.fr_FR.ts ../translations/minimal.en_US.ts

Speaking of which, I would like you to give me your opinion on my .pro file.
I wonder if all would not come from him with an error on the roads.
Besides, should we do it as I did or should we specify the source for
each file and for each translation, in this style:

SOURCES + =
SOURCES + =
....
TRANSLATIONS + =
TRANSLATIONS + =
TRANSLATIONS + =
....

No in fact it was to have the qt_fr files, qt_base_fr if minimal.qm existed. The
2 first are necessary to translate the ui (buttons, menus, …). It was very poorly formulated, I understand it well.

In the next few days I will test this code and I will give you news I think Thursday Evening.

@martin

I have tested the following code that you have given to me and it doesn’t work. The Ui is not translated automatically and if I precise the locale by this one python start_app.py fr_FR nothing too.

import sys
from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import QTranslator, QLocale, QLibraryInfo

from windows.minimal import MiniMal


if __name__ == "__main__":
    application = QApplication(sys.argv)

    # # Translate application
    locale = QLocale().system().name()
    # qt_path_locale = QLibraryInfo.location(QLibraryInfo.TranslationsPath)
    translator_qt = QTranslator()
    # if translator_qt.load("qt_%s" % locale, qt_path_locale):
    if translator_qt.load("qt_%s" % locale, QLibraryInfo.location(QLibraryInfo.TranslationsPath)):
        application.installTranslator(translator_qt)

    translator_custom = QTranslator()
    if translator_custom.load("minimal_%s" % locale, "translations"):
        application.installTranslator(translator_custom) 
    window = MiniMal()
    window.show()
    rc = application.exec_()
    sys.exit(rc)

Hi Ah . :heart_eyes: :stuck_out_tongue_winking_eye: :hot_face: :smiling_imp:

I’ve found the solution. That’s work at least on the minimal code above. In fact, there were actually 2 problems ; one with the path of folder translations and another with the end of my qm files.

I’ve just modified this line :

...
translator_custom = QTranslator()
    if translator_custom.load("minimal.%s" % locale, "translations"):
....

Now I’m going to modified my real program and check if qt_** translations of Ui’s works fines.
And i’ll give you a feedback perhaps today but nothing is sure. :crazy_face:

1 Like

As promised I give feedback.
My initial project works as well as my example.
Only downside: the translation of software other than that of the system does not work. We cannot perform this operation
by launching start_app.py specifying the target language. Example: Switch software from French to English: python start_up.py en_US

For now, it is not a priority, the main is done but I will come back to it.

Afterwards, there will only be dynamic translation of the software, by selecting the language in a QComboBox, for example.

Another idea for the next version. To put in your TODO list. :stuck_out_tongue_winking_eye:

I close this thread as it is resolved.

Thanks again. :wink:

Thanks for documenting what you did @Eolinwen it’ll be a great help when I come to write this chapter :wink:

I’ll give you some feedback when I will do that but not for the moment. :+1:

I am still looking for the leanest way to translate PyQt5 applications, ideally without the need to compile anything. After all, I chose PyQt5 specifically because it doesn’t need to be compiled…

This is what I have, notice the QUESTIONs:

#!/usr/bin/env python3
# -*- coding: UTF-8 -*-

import os, sys
import gettext

# QUESTION: This style of imports significantly simplifies the code.
# Does it lead to a noticeable memory/performance penalty though?
# Is it frowned upon?
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *


class Window(QMainWindow):

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle(_("Translate me"))
        self.resize(400, 200)
        self.centralWidget = QLabel(_("If this text is translated, then it is working"))
        self.centralWidget.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
        self.setCentralWidget(self.centralWidget)


if __name__ == "__main__":

    app = QApplication(sys.argv)
    
    # Load the translations for Qt itself as seen in, e.g., the "About Qt" dialog
    # On FreeBSD, this gets resolved to /usr/local/share/qt5/translations/ which contains qt_de.qm
    # which references qtbase_de, qtscript_de, qtmultimedia_de, qtxmlpatterns_de
    qt_translator = QTranslator()
    qt_translator.load("qt_" + QLocale.system().name(),
                       QLibraryInfo.location(QLibraryInfo.TranslationsPath))
    app.installTranslator(qt_translator)
                       
    # Load the translations for this application.
    # On FreeBSD, this gets resolved to 
    # access("Desktop/translated.py/translations/app_de_DE",R_OK) ERR#20 'Not a directory'
    # access("Desktop/translated.py/translations/app_de.qm",R_OK) ERR#20 'Not a directory'
    # access("Desktop/translated.py/translations/app_de",R_OK) ERR#20 'Not a directory'
    # access("Desktop/translated.py/translations/app.qm",R_OK) ERR#20 'Not a directory'
    # access("Desktop/translated.py/translations/app",R_OK) ERR#20 'Not a directory
    # QUESTION: Can we use this to override some strings in Qt that we do not like? (Including English)
    # QUESTION: Can we make it load non-binary .ts files rather than binary .qm files?
    # This would make distributing and changing the strings locally much easier and remove
    # the need for compilation, which is the point in using Python
    app_translator = QTranslator()
    app_translator.load("app_" + QLocale.system().name(),
                       __file__ + "/translations")
    app.installTranslator(app_translator)

    # QUESTION: According to https://doc.bccnsoft.com/docs/PyQt5/i18n.html, 
    # PyQt5 has nasty quirks when it comes to using Qt translations which make this cumbersome.
    # Would it be better to use the Python way using gettext 
    # for our own translatable strings?
    # https://www.mattlayman.com/blog/2015/i18n/
    # https://phrase.com/blog/posts/translate-python-gnu-gettext/
    # On FreeBSD, this gets resolved to 
    # /usr/home/user/Desktop/locale/de_DE.UTF-8/LC_MESSAGES/app.mo
    # /usr/home/user/Desktop/locale/de_DE/LC_MESSAGES/app.mo
    # /usr/home/user/Desktop/locale/de.UTF-8/LC_MESSAGES/app.mo
    # /usr/home/user/Desktop/locale/de/LC_MESSAGES/app.mo
    # QUESTION: Can we make it load non-binary .po files rather than binary .mo files?
    # This would make distributing and changing the strings locally much easier and remove
    # the need for compilation, which is the point in using Python
    localedir = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'locales')
    translate = gettext.translation('messages', localedir, fallback=True)
    _ = translate.gettext
	
    win = Window()
    win.show()
    
    QMessageBox.aboutQt(None)
    
    sys.exit(app.exec_())
    
    # Step 0: The source Python file MUST have:
    # # -*- coding: UTF-8 -*-
    # Step 1: Extract gettext strings from source, by generating a .pot template file with
    # mkdir -p locales/{en,de}/LC_MESSAGES/
    # xgettext --language=Python *.py --output=locales/messages.pot
    # sed -i -e 's|CHARSET|UTF-8|g' locales/messages.pot
    # rm locales/messages.pot-e
    # Step 2: Copy the .pot template file for each language (or let Weblate do it)
    # cp locales/messages.pot locales/de/LC_MESSAGES/messages.po
    # Step 3: Translate locales/de/LC_MESSAGES/messages.po (or let Weblate do it)
    # Step 5: Compile .po into binary .mo (we would like to get rid of this)
    # msgfmt locales/de/LC_MESSAGES/messages.po --output-file=locales/de/LC_MESSAGES/messages.mo
    # This is super annoying because it means that we need to introduce a compilation step