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:
- You mark strings in your code as translatable using
self.tr("Some text")orQCoreApplication.translate("Context", "Some text"). - You use
pylupdate6(orlupdatefrom Qt) to extract those strings into.tsfiles. - You translate the strings in the
.tsfiles (using Qt Linguist or a text editor). - You compile the
.tsfiles into.qmfiles usinglrelease. - At runtime, you load the
.qmfiles usingQTranslatorand install them on yourQApplication.
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:
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():
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.
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:
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:
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:
# 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:
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:
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:
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:
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:
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.
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:
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
.profile are relative to where the.profile is located. If your.profile 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 useQCoreApplication.translate()). If you're using Qt Designer to build your interfaces, see our guide on using Qt Designer with PyQt6 for more on how.uifiles fit into this workflow. - You can list multiple files on one line with
SOURCES +=or split them across multiple lines. Both formats work:
# 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:
pylupdate6 myproject.pro
This generates (or updates) the .ts files. Translate them with Qt Linguist, then compile to .qm:
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:
myproject/
├── main.py
├── translations/
│ └── myapp.fr_FR.qm (compiled translation file)
└── myproject.pro
main.py:
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:
SOURCES += main.py
TRANSLATIONS += translations/myapp.fr_FR.ts
To generate the translation files and compile them:
# 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 aprint()after eachload()call to see if it returnsTrueorFalse. - Check the filename exactly. Is it
myapp.fr_FR.qmormyapp_fr_FR.qm? The separator in yourload()call must match. - Check the path. Print
translations_pathand verify the.qmfile 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
QMainWindowbefore installing the translator, its strings won't be translated. - Keep references to your
QTranslatorobjects. Don't let them get garbage collected. - Make sure your
.profile lists all source files. If a file isn't listed, its translatable strings won't appear in the.tsfile, and they won't end up in the.qmfile 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.