Files
gKeyMapper/po
2026-10-18 20:26:22 +02:00
..
2026-10-18 20:26:22 +02:00
2026-10-18 20:26:22 +02:00
2026-10-18 20:26:22 +02:00
2026-10-18 20:26:22 +02:00
2026-10-18 20:26:22 +02:00
2026-10-18 20:26:22 +02:00

Translations (i18n)

Everything about the catalogs, in one place: which file is which, how a string gets from the source code into a translated window, and how to add a language. The commands live in scripts/i18n.sh, wired into the Makefile as pot, i18n-check and i18n-status.

The pieces

file what it is
po/gkeymapper.pot the template: every translatable string in the sources, with the file and line it comes from. Generated by make pot — never translated, and its msgids are never edited by hand.
po/<lang>.po one catalog per language. This is what a translator edits.
locale/<lang>/LC_MESSAGES/gkeymapper.mo the compiled catalog the program reads at runtime. Generated by make translations, gitignored. Never edit — it is overwritten on every build.
scripts/i18n.sh extract, update, check, status, todo, new (see below).

Languages: de, en, es, fr. The set is po/*.po — the Makefile picks up a new catalog by itself (LANGS), nothing has to be registered.

en.po is special: English is the source language, and its catalog repeats every msgid as its msgstr (an identity catalog). It exists so that choosing "English" works on a German desktop, where the session locale would otherwise select the German catalog. i18n-check enforces the identity — a translation that drifts in is reported.

How a string reaches the window

  1. _("...") marks it in src/ (or shared/) — the two directories the GUI is built from. N_("...") is for strings that are only collected when they are not translated yet: src/help.cpp keeps its paragraphs in a static array and translates them when the window is built (_(text)). Both keywords are in the pot; a string marked with neither is invisible to translators.
  2. make pot writes po/gkeymapper.pot from the sources.
  3. scripts/i18n.sh update merges the pot into every catalog. New strings come in with an empty msgstr (= untranslated), existing translations are kept.
  4. Translating is editing po/<lang>.po: replace the msgstr, leave the msgid alone. Any text editor, or poedit if you prefer one.
  5. make translations compiles locale/<lang>/LC_MESSAGES/gkeymapper.mo.
  6. The program reads the catalog whose language it was started with. The GUI picks it up automatically once gettext is set up in src/main.cpp; which language is used comes from the options window (stored in ~/.config/gKeyMapper/gui.conf, language=de), or from the session when that setting is empty. Internally apply_gui_language() (src/settings.cpp) sets LANGUAGE and re-runs setlocale — LANGUAGE wins over LANG/LC_ALL in gettext, which is what makes switching without a restart and an explicit en on a German desktop work. The catalogs are searched under $GKEYMAPPER_LOCALEDIR, falling back to the compiled-in LOCALEDIR.

The common case: a string is added or changed

# 1. in the code
_("New text")                       # or N_("...") for a deferred one

# 2. the template
make pot                            # prints what is new and what is gone

# 3. what is missing where
make i18n-check                     # names every catalog that lacks it

# 4. every catalog takes it in (empty msgstr)
scripts/i18n.sh update              # or: scripts/i18n.sh update de

# 5. translate
scripts/i18n.sh todo de             # untranslated + fuzzy, with the source location
$EDITOR po/de.po                    # and es, fr - and en, where the msgstr is the msgid

# 6. check and compile
make i18n-check                     # must print "everything in sync"
make translations

# 7. see it
GKEYMAPPER_LOCALEDIR=$PWD/locale LANGUAGE=de_DE.UTF-8 ./gkeymapper

Commit the source change, po/gkeymapper.pot and the catalogs together: a pot without the catalogs is a half-done change, and make i18n-check says so.

A new string in one place and the same text in another is one message for gettext — the pot lists both locations, and one msgstr covers both.

Adding a language

scripts/i18n.sh new it Italiano     # po/it.po, header in the project's shape,
                                    # every msgstr empty (the second word is
                                    # Language-Team; it defaults to the code)
scripts/i18n.sh todo it             # work through it
$EDITOR po/it.po

Then offer it in the GUI, or nobody can select it — src/optionsdialog.cpp:

  • kLanguageCodes[] — the codes, in the same order as the labels ("", the empty one, means "follow the session" and has to stay first);
  • languageOptions[] — the labels. Use the endonym ("Italiano", not "Italian"): the list reads the same in every language, so a user who lands in the wrong language can still find their own.

make i18n-check checks exactly that pairing (a catalog that cannot be selected, a code without a catalog, two arrays of different length). make translations compiles the new catalog; make run and the test command above use the working tree's locale/.

Commands

command what it does
scripts/i18n.sh extract (make pot) Regenerate po/gkeymapper.pot from the sources. Prints the messages that appeared and disappeared. Refuses to write an empty pot, so a broken gettext cannot empty the good one.
scripts/i18n.sh update [lang...] Merge the pot into the catalogs, without source locations (see Conventions). Keeps translations, marks new strings untranslated.
scripts/i18n.sh check (make i18n-check) Everything below. Exit code 1 when something is wrong, so it also works as a pre-commit step.
scripts/i18n.sh status (make i18n-status) Per language: messages, translated, fuzzy, untranslated, and whether the compiled catalog is older than the po.
scripts/i18n.sh todo <lang> The untranslated and fuzzy messages, with the source locations taken from the pot. The output is a valid po file, if you want to keep it.
scripts/i18n.sh new <lang> [team] Create po/<lang>.po from the pot. new en writes the identity catalog.

The gettext tools are taken from PATH and can be overridden like the Makefile's variables, e.g. XGETTEXT=/usr/bin/xgettext scripts/i18n.sh extract.

What check looks at

  • the pot against the sources: same messages? A string added or changed without make pot is the mistake this catches (a #: line number that moved is only reported as a note — refresh it whenever it suits you);
  • every catalog: it parses (msgfmt -c, which also compares the format specifiers of msgid and msgstr), it defines the same messages as the pot, nothing is untranslated or fuzzy, and its Language: header matches its file name;
  • po/en.po is an identity catalog;
  • po/*.po and the language list of the options window (src/optionsdialog.cpp) agree;
  • no strings are marked in daemon/: the daemon has no gettext setup, so _() there would be translated into a catalog that is never read.

Notes that are not failures: catalogs that were not merged since the last extract, and obsolete entries (#~, strings that are gone from the sources — msgattrib --no-obsolete po/<lang>.po > tmp && mv tmp po/<lang>.po drops them).

Conventions

  • No #: file:line comments in the catalogs. The locations are noise for a translator and churn whenever a line moves, so they live only in the pot (update therefore uses msgmerge --no-location) and todo shows them from there when they are needed. This is also why the catalogs are merged with the script instead of a bare msgmerge --update, which would copy the locations in.
  • The build does not regenerate the pot (make translations only runs msgfmt). A template that is rewritten by every build is a template nobody looks at; instead make pot is a deliberate step and make i18n-check fails when it was forgotten.
  • Mnemonics stay mnemonics. _Cancel uses the _ to underline a letter; _Abbrechen moves it to a letter that exists in that language. A translation without the underscore loses the keyboard shortcut silently.
  • Format specifiers (%s, %d) keep their number and order — msgfmt -c catches a mismatch.
  • No plurals yet: the program only uses _(), so no catalog carries a Plural-Forms header. With the first ngettext() that header has to go into every catalog (and scripts/i18n.sh new should write it).
  • HTML-ish markup in a msgid (<b>, <i>) has to survive in the msgstr; the help window renders it.
  • German belongs in po/de.po only — sources, comments and documentation are English.

Troubleshooting

  • A window stays English while another window is translated — the string is missing from the catalog (or the catalog was not recompiled: make translations). scripts/i18n.sh todo <lang> lists what is open.
  • Shortcut without an underline — the mnemonic _ is missing in the msgstr.
  • po/*.po~ files appear — that is msgmerge's backup from a manual msgmerge --update; the script deletes it. Never commit them.
  • A fuzzy entry is shown in English — on purpose: fuzzy means "the text was changed, this translation needs a look", and gettext falls back to the msgid until it is confirmed (remove the #, fuzzy line).
  • "not merged against the current pot" in check — run scripts/i18n.sh update; until then the catalog header carries an older date than the pot.
  • Nothing is extracted at all — xgettext's built-in keyword list is not the same everywhere, so the script always passes --keyword=_ --keyword=N_. If the pot comes out empty, the script stops instead of replacing the pot.

The rest of the project's conventions (GUI, daemon, mappings) are in AGENTS.md.