Localization
Internationalization and localization for Linux desktop apps: gettext, Qt Linguist, plurals, dates, RTL layouts, fonts, metadata translation, and a workflow that keeps translations current.
An app that only works in English is an app that excludes most of the world. Localization is not a late-stage translation pass; it is a set of decisions about strings, layout, formatting, and fonts that you make while writing the first screen.
The Linux ecosystem has strong translation infrastructure: gettext, Weblate, GNOME’s Damned Lies, KDE’s Lokalize, and distribution translation teams. Use it instead of inventing a JSON translation format.
Internationalization vs localization
Section titled “Internationalization vs localization”- Internationalization (i18n): making the code capable of supporting multiple languages and locales. No hardcoded strings, no concatenation, no fixed date formats.
- Localization (l10n): providing the actual translations, formats, and cultural adaptations for a locale.
You do i18n once; translators do l10n continuously.
Strings
Section titled “Strings”Mark every user-facing string
Section titled “Mark every user-facing string”C / GLib:
#include <glib/gi18n.h>
label = gtk_label_new(_("Open note"));Python:
import gettext
gettext.bindtextdomain("notes", "/usr/share/locale")gettext.textdomain("notes")_ = gettext.gettextngettext = gettext.ngettext
label.set_text(_("Open note"))Inside a Flatpak, install .mo files under $FLATPAK_DEST/share/locale/ and keep the same domain name.
Qt / C++:
auto *label = new QLabel(tr("Open note"));QML:
Label { text: qsTr("Open note") }Rust:
Use gettext-rs with the same .po workflow, or Fluent for message-heavy apps. Do not hardcode English strings in Rust macros that bypass extraction.
Do not concatenate
Section titled “Do not concatenate”Translators need the full sentence, not fragments.
# Bad: word order differs across languagesmessage = _("Deleted") + " " + str(count) + " " + _("notes")
# Good: one translatable string with a placeholdermessage = ngettext("Deleted %d note", "Deleted %d notes", count) % countUse placeholders, not string interpolation
Section titled “Use placeholders, not string interpolation”# Goodlabel = _("Welcome back, {name}").format(name=user.name)Keep placeholders named and documented. Do not change them without updating translators.
Plurals
Section titled “Plurals”Use the platform’s plural API. Plural rules differ across languages; a simple count == 1 check is wrong for many locales.
ngettext("%d note", "%d notes", count)ngettext("%d note", "%d notes", count)Qt: tr("%n note(s)", "", count). QML: qsTr("%n note(s)", "", count).
Context
Section titled “Context”Words like “Open” (verb) and “Open” (adjective), or “File” (noun) and “File” (verb), need context. Use pgettext() / QT_TRANSLATE_NOOP with a context string.
_("Open") // ambiguouspgettext("verb", "Open") // clear to the translatorTranslator comments
Section titled “Translator comments”Add comments for ambiguous strings, placeholders, and UI constraints.
// TRANSLATORS: Title of the dialog that appears when a note is deleted._("Delete note?")Build integration
Section titled “Build integration”Meson + gettext
Section titled “Meson + gettext”subdir('po')i18n = import('i18n')i18n.gettext('notes', preset: 'glib')i18n.gettext() resolves POTFILES.in and LINGUAS relative to the directory that calls it, so keep the call in po/meson.build or move the files to the root.
src/main.pydeesfrpt_BR# Extract strings into a POT templatexgettext --from-code=UTF-8 --add-comments=TRANSLATORS -o po/notes.pot src/*.py
# Merge the template into existing translationsmsgmerge --update po/de.po po/notes.pot
# Compile for testingmsgfmt po/de.po -o po/de.moInstall .mo files to $prefix/share/locale/<lang>/LC_MESSAGES/notes.mo.
Use lupdate to extract tr() strings into .ts files, translate them in Qt Linguist, and lrelease them into .qm files. With CMake, qt_add_translations() wires the build and install steps. Load the .qm with QTranslator before creating the UI, or install them under $prefix/share/<app>/translations/.
Dates, numbers, and currency
Section titled “Dates, numbers, and currency”Never format dates and numbers by hand.
- GLib:
GDateTimewith locale-aware format strings; useg_date_time_format()for display, ISO 8601 for storage. - Qt:
QLocalefor dates, numbers, currency, and measurement units. - Python:
datetime+localeorbabel; preferbabelfor explicit locale control. - Rust: ICU4X for full CLDR data;
chrono/timealone do not provide complete locale-aware formatting.
Store timestamps in UTC (or with an explicit offset) and convert for display. Do not assume the first day of the week is Sunday or Monday; read the locale.
Layout and RTL
Section titled “Layout and RTL”Right-to-left languages (Arabic, Hebrew, Persian, Urdu) require more than mirrored text.
- Use start/end layout APIs (
set_margin_start,set_margin_end) instead of left/right. - Let the toolkit mirror the layout automatically.
- Mirror directional icons (arrows, back/forward) but not universal symbols (play, check, warning).
- Test with a generated RTL locale (for example
LANG=ar_EG.UTF-8) and a full RTL desktop session;locale -ashows what the machine actually has. - Avoid absolute positioning and hardcoded x-coordinates.
- Check that progress bars, sliders, and timelines make sense in RTL.
GTK and Qt handle most mirroring when the locale is RTL. Your job is to avoid assumptions that defeat it.
Fonts and text rendering
Section titled “Fonts and text rendering”- Use the system UI font; it has fallbacks for the user’s language.
- Do not hardcode a font family that lacks CJK, Arabic, or Devanagari glyphs.
- Test with long German compounds, Japanese, Arabic, and Hindi.
- Do not use fixed-width labels for translated text.
- Allow text to wrap; avoid truncating essential information.
- Check line height for scripts with tall glyphs and diacritics.
Ubuntu ships Noto fonts for broad coverage. If you bundle a font for branding, pair it with the system fallback rather than replacing the UI font entirely.
Input methods and bidirectional text
Section titled “Input methods and bidirectional text”Wayland routes text input through the text-input and input-method protocols. Do not hardcode a single input-method framework:
- Use the toolkit’s text widgets and let the compositor’s input method handle Compose, dead keys, and CJK input.
- On Wayland, do not set
GTK_IM_MODULEorQT_IM_MODULE; the compositor and portal select the input method. On X11, the user’s session decides. - Test with IBus and Fcitx5, at least one CJK input method, and Compose sequences.
- For user-supplied bidirectional text, wrap the embedded value in Unicode isolate characters (U+2068 FSI and U+2069 PDI) or let the toolkit’s bidi-aware widget handle it.
- Do not assume the UI direction equals the direction of user content.
Metadata and desktop entry
Section titled “Metadata and desktop entry”Translate user-visible metadata, not just the UI.
Name=NotesName[de]=NotizenComment=Write and organize notesComment[de]=Notizen schreiben und organisierenKeywords=notes;writing;Keywords[de]=Notizen;Schreiben;<name>Notes</name><name xml:lang="de">Notizen</name><summary>Write and organize notes</summary><summary xml:lang="de">Notizen schreiben und organisieren</summary>AppStream supports localized names, summaries, descriptions, and screenshots. Software centers show the user’s language when available.
Translation workflow
Section titled “Translation workflow”- Extract strings into a
.pottemplate on every change. - Translate in a platform that supports context, plurals, and comments (Weblate, Damned Lies, Lokalize, Crowdin).
- Review by a native speaker, especially for UI-critical strings.
- Test with the translated build, not just the
.pofile. - Freeze strings before a release; give translators a deadline.
- Credit translators in the about dialog or release notes.
Do not machine-translate the UI and ship it unreviewed. Flathub explicitly rejects low-quality machine translation, and users notice.
Testing localization
Section titled “Testing localization”- Run with
LANG=de_DE.UTF-8,LANG=ar_EG.UTF-8,LANG=ja_JP.UTF-8. - Use pseudo-localization (replace letters with accented variants and expand strings by 30%) to catch truncation.
- Test plural forms with 0, 1, 2, 5, 11, 21, and 100.
- Test date, time, number, and currency formats.
- Test RTL layout and mirrored icons.
- Test with a larger system font and 200% scaling.
- Check the desktop entry and software-center listing in the translated locale.
Localization checklist
Section titled “Localization checklist”- ☐ No user-facing string is hardcoded outside the translation system.
- ☐ No string concatenation for sentences.
- ☐ Plurals use
ngettext/tr("%n"). - ☐ Translators have context and comments for ambiguous strings.
- ☐ Dates, numbers, and currency use locale APIs.
- ☐ Layout uses start/end, not left/right.
- ☐ Icons are mirrored where appropriate.
- ☐ The UI works with long translations and 200% text scaling.
- ☐ Desktop entry and AppStream metadata are localized.
- ☐ English is complete; additional languages are reviewed.
- ☐ The app runs in an RTL locale.
- ☐ Translators are credited.
Sources
Section titled “Sources”Next: Architecture — how to structure the code so the UI stays fast and the app stays maintainable.