Impossible Translations

Troubleshooting PyQt6 translation loading issues with QTranslator
Heads up! You've already completed this tutorial.

Getting translations working in a PyQt6 application can be one of the most frustrating experiences in GUI development. The translation system doesn't raise errors when something goes wrong — it just silently does nothing. Your app loads, your widgets appear, but everything stays in the original language, and you're left wondering what went wrong.

This tutorial walks through the common problems people run into when setting up translations with QTranslator in PyQt6, and shows you how to get everything working correctly. We'll cover loading .qm translation files, getting file paths right, and handling both Qt's built-in translations and your own custom ones.

How Qt translations work

Before we get into the problems, a quick refresher on how the translation system fits together.

Qt uses .qm files for translations. These are compiled binary files generated from .ts (translation source) files. The pipeline looks like this:

  1. You mark strings in your code as translatable using self.tr("Some text") or QCoreApplication.translate("Context", "Some text").
  2. You use pylupdate6 (or lupdate from Qt) to extract those strings into .ts files.
  3. You translate the strings in the .ts files (using Qt Linguist or a text editor).
  4. You compile the .ts files into .qm files using lrelease.
  5. At runtime, you load the .qm files using QTranslator and install them on your QApplication.

The tricky part is step 5. If anything goes wrong during loading — wrong path, wrong filename, wrong naming convention — QTranslator.load() simply returns False and your app runs untranslated. No exception, no warning, no log message.

The silent failure problem

Here's a minimal example of loading a translation:

python
translator = QTranslator()
translator.load("myapp_fr_FR", "translations")
app.installTranslator(translator)

If the file translations/myapp_fr_FR.qm doesn't exist, or the path translations can't be found relative to where the script is running, load() returns False and nothing happens. This is the root cause of most translation headaches.

Always check the return value of load():

python
translator = QTranslator()
if translator.load("myapp_fr_FR", "translations"):
    print("Translation loaded successfully!")
    app.installTranslator(translator)
else:
    print("Failed to load translation file.")

Adding this check is the single most useful thing you can do when debugging translations. It tells you immediately whether the file was found and loaded.

Packaging Python Applications with PyInstaller by Martin Fitzpatrick — This step-by-step guide walks you through packaging your own Python applications from simple examples to complete installers and signed executables.

Get the book

Getting file paths right

The most common reason translations fail to load is that the path to the .qm file is wrong. When you pass a relative path like "translations" to QTranslator.load(), it's resolved relative to the current working directory — not relative to your Python script.

If you run your script from a different directory (which happens all the time during development and deployment), the relative path breaks silently.

The fix is to construct an absolute path based on the location of your script:

python
import os
from PyQt6.QtCore import QTranslator, QLocale, QLibraryInfo

# Build an absolute path to the translations folder
basedir = os.path.dirname(os.path.abspath(__file__))
translations_path = os.path.join(basedir, "translations")

locale = QLocale.system().name()  # e.g., "fr_FR"

translator = QTranslator()
if translator.load(f"myapp_{locale}", translations_path):
    app.installTranslator(translator)

Using os.path.dirname(os.path.abspath(__file__)) gives you the directory containing the current script, regardless of where the script was launched from.

File naming conventions

Qt's QTranslator.load() method has some flexibility in how it searches for files, but it follows specific naming conventions. When you call:

python
translator.load("myapp_fr_FR", "/path/to/translations")

Qt looks for the file /path/to/translations/myapp_fr_FR.qm. If that's not found, it tries progressively shorter names: myapp_fr.qm, then myapp.qm.

A common mistake is getting the separator between the base name and locale wrong. In the forum discussion that inspired this article, the solution turned out to be using a dot separator instead of an underscore:

python
# This didn't work:
translator.load("minimal_%s" % locale, "translations")

# This worked:
translator.load("minimal.%s" % locale, "translations")

The separator you use in load() must match the actual filename of your .qm file. If your file is called minimal.fr_FR.qm, you need "minimal.%s" % locale. If it's called minimal_fr_FR.qm, you need "minimal_%s" % locale.

Check your actual filenames carefully. This tiny difference — a dot vs. an underscore — can be the entire problem.

Loading Qt's built-in translations

Qt itself has translatable strings — things like "OK", "Cancel", "About Qt", and the text in standard dialogs. These translations ship with Qt and are stored in .qm files at a system-level path.

To load them, use QLibraryInfo.location(QLibraryInfo.TranslationsPath) to find where Qt keeps its translation files:

python
from PyQt6.QtCore import QTranslator, QLocale, QLibraryInfo

locale = QLocale.system().name()

qt_translator = QTranslator()
if qt_translator.load(f"qt_{locale}",
                       QLibraryInfo.location(QLibraryInfo.TranslationsPath)):
    app.installTranslator(qt_translator)

On Linux, this typically resolves to something like /usr/share/qt6/translations/. On other platforms, the path will be different, but QLibraryInfo handles that for you.

You should load the Qt base translations in addition to your own custom translations. They're separate translators installed on the same application:

python
locale = QLocale.system().name()

# Load Qt's own translations
qt_translator = QTranslator()
if qt_translator.load(f"qt_{locale}",
                       QLibraryInfo.location(QLibraryInfo.TranslationsPath)):
    app.installTranslator(qt_translator)

# Load your app's translations
app_translator = QTranslator()
if app_translator.load(f"myapp.{locale}", translations_path):
    app.installTranslator(app_translator)

Keeping translators alive

There's a subtle gotcha in Python that doesn't exist in C++ Qt code. If you create a QTranslator as a local variable inside a function and don't keep a reference to it, Python's garbage collector may destroy it. Once the translator object is garbage collected, the translations stop working — even though you installed it on the application.

This is a problem:

python
def setup_translations(app):
    translator = QTranslator()
    translator.load("myapp_fr_FR", translations_path)
    app.installTranslator(translator)
    # 'translator' goes out of scope and may be garbage collected!

Instead, keep a reference to the translator object. You can store it as an attribute on the application, or keep it in a list at module level:

python
def setup_translations(app):
    translator = QTranslator()
    if translator.load("myapp_fr_FR", translations_path):
        app.installTranslator(translator)
        app._translator = translator  # Keep a reference!

Or, if you're loading multiple translators:

python
translators = []

def setup_translations(app):
    locale = QLocale.system().name()

    qt_translator = QTranslator()
    if qt_translator.load(f"qt_{locale}",
                           QLibraryInfo.location(QLibraryInfo.TranslationsPath)):
        app.installTranslator(qt_translator)
        translators.append(qt_translator)

    app_translator = QTranslator()
    if app_translator.load(f"myapp.{locale}", translations_path):
        app.installTranslator(app_translator)
        translators.append(app_translator)

Translations must be loaded before creating widgets

The order of operations matters. You must load and install your translators before you create any widgets. When a widget is created, it calls retranslateUi() (if using .ui files converted to Python) or reads the self.tr() strings. If the translator isn't installed yet, those strings come through untranslated.

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

    # Load translations FIRST
    locale = QLocale.system().name()

    qt_translator = QTranslator()
    if qt_translator.load(f"qt_{locale}",
                           QLibraryInfo.location(QLibraryInfo.TranslationsPath)):
        app.installTranslator(qt_translator)

    app_translator = QTranslator()
    if app_translator.load(f"myapp.{locale}", translations_path):
        app.installTranslator(app_translator)

    # THEN create widgets
    window = MainWindow()
    window.show()

    sys.exit(app.exec())

Setting up the .pro file

To extract translatable strings from your source files, you need a .pro file that tells pylupdate6 where to look. Here's a clean example:

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

A few things to watch for:

  • Paths in the .pro file are relative to where the .pro file is located. If your .pro file is at the project root, the paths should be relative to the project root.
  • List all files that contain translatable strings. This includes both your hand-written Python files (with self.tr() calls) and generated UI files (which use QCoreApplication.translate()). If you're using Qt Designer to build your interfaces, see our guide on using Qt Designer with PyQt6 for more on how .ui files fit into this workflow.
  • You can list multiple files on one line with SOURCES += or split them across multiple lines. Both formats work:
text
# Single line
SOURCES += windows/minimal.py ui/exempleui.py

# Multiple lines (use backslash for continuation)
SOURCES += windows/minimal.py \
           ui/exempleui.py

After editing the .pro file, run:

sh
pylupdate6 myproject.pro

This generates (or updates) the .ts files. Translate them with Qt Linguist, then compile to .qm:

sh
lrelease myproject.pro

Complete working example

Here's a full working example that puts everything together. This example has a simple main window with translatable strings and loads translations correctly. If you're new to building PyQt6 applications, you may want to start with creating your first window before tackling translations.

Project structure:

text
myproject/
├── main.py
├── translations/
│   └── myapp.fr_FR.qm    (compiled translation file)
└── myproject.pro

main.py:

python
import os
import sys

from PyQt6.QtCore import QLibraryInfo, QLocale, QTranslator
from PyQt6.QtWidgets import (
    QApplication,
    QCheckBox,
    QComboBox,
    QLabel,
    QLineEdit,
    QMainWindow,
    QMessageBox,
    QPushButton,
    QVBoxLayout,
    QWidget,
)


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle(self.tr("My Application"))

        central = QWidget()
        layout = QVBoxLayout(central)

        self.label = QLabel(self.tr("Film name:"))
        layout.addWidget(self.label)

        self.line_edit = QLineEdit()
        self.line_edit.setPlaceholderText(self.tr("Enter the film name"))
        layout.addWidget(self.line_edit)

        self.button = QPushButton(self.tr("Click me"))
        self.button.setToolTip(self.tr("Press this button to do something"))
        self.button.clicked.connect(self.on_button_clicked)
        layout.addWidget(self.button)

        self.combo = QComboBox()
        self.combo.addItems([
            self.tr("Option A"),
            self.tr("Option B"),
            self.tr("Option C"),
        ])
        layout.addWidget(self.combo)

        self.checkbox = QCheckBox(self.tr("Enable notifications"))
        layout.addWidget(self.checkbox)

        self.setCentralWidget(central)
        self.resize(400, 250)

    def on_button_clicked(self):
        QMessageBox.information(
            self,
            self.tr("Hello"),
            self.tr("You clicked the button!"),
        )


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

    # Build absolute path to translations folder
    basedir = os.path.dirname(os.path.abspath(__file__))
    translations_path = os.path.join(basedir, "translations")

    # Detect system locale
    locale = QLocale.system().name()
    print(f"System locale: {locale}")

    # Load Qt's built-in translations (for standard dialogs, etc.)
    qt_translator = QTranslator()
    qt_translations_path = QLibraryInfo.location(
        QLibraryInfo.TranslationsPath
    )
    if qt_translator.load(f"qt_{locale}", qt_translations_path):
        app.installTranslator(qt_translator)
        print(f"Loaded Qt translations for {locale}")
    else:
        print(f"No Qt translations found for {locale}")

    # Load this application's custom translations
    app_translator = QTranslator()
    if app_translator.load(f"myapp.{locale}", translations_path):
        app.installTranslator(app_translator)
        print(f"Loaded app translations for {locale}")
    else:
        print(f"No app translations found for {locale}")
        print(f"  Looked in: {translations_path}")

    # Create the window AFTER translations are loaded
    window = MainWindow()
    window.show()

    sys.exit(app.exec())

myproject.pro:

python
SOURCES += main.py
TRANSLATIONS += translations/myapp.fr_FR.ts

To generate the translation files and compile them:

sh
# Extract translatable strings
pylupdate6 myproject.pro

# Edit translations/myapp.fr_FR.ts with Qt Linguist or a text editor

# Compile to binary .qm format
lrelease myproject.pro

Debugging checklist

When your translations aren't loading, work through this list:

  • Check the return value of load(). Add a print() after each load() call to see if it returns True or False.
  • Check the filename exactly. Is it myapp.fr_FR.qm or myapp_fr_FR.qm? The separator in your load() call must match.
  • Check the path. Print translations_path and verify the .qm file actually exists there.
  • Use absolute paths. Build them from __file__ to avoid current-working-directory problems.
  • Load translations before creating widgets. If you create a QMainWindow before installing the translator, its strings won't be translated.
  • Keep references to your QTranslator objects. Don't let them get garbage collected.
  • Make sure your .pro file lists all source files. If a file isn't listed, its translatable strings won't appear in the .ts file, and they won't end up in the .qm file either.

Translations in PyQt6 can feel like working in the dark, because the system never tells you when something goes wrong. The good news is that once you understand the handful of things that can go wrong — file paths, naming conventions, load order, and garbage collection — the system works reliably every time. Once your translations are working, you can move on to packaging your PyQt6 application for distribution, making sure your .qm files are included alongside your code.

PyQt/PySide Development Services — Stuck in development hell? I'll help you get your project focused, finished and released. Benefit from years of practical experience releasing software with Python.

Find out More

Well done, you've finished this tutorial! Mark As Complete
[[ user.completed.length ]] completed [[ user.streak+1 ]] day streak
Martin Fitzpatrick

Impossible Translations was written by Martin Fitzpatrick.

Martin Fitzpatrick has been developing Python/Qt apps for 8 years. Building desktop applications to make data-analysis tools more user-friendly, Python was the obvious choice. Starting with Tk, later moving to wxWidgets and finally adopting PyQt. Martin founded PythonGUIs to provide easy to follow GUI programming tutorials to the Python community. He has written a number of popular Python books on the subject.