9.6 KiB
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
_("...")marks it insrc/(orshared/) — the two directories the GUI is built from.N_("...")is for strings that are only collected when they are not translated yet:src/help.cppkeeps 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.make potwritespo/gkeymapper.potfrom the sources.scripts/i18n.sh updatemerges the pot into every catalog. New strings come in with an empty msgstr (= untranslated), existing translations are kept.- Translating is editing
po/<lang>.po: replace the msgstr, leave the msgid alone. Any text editor, orpoeditif you prefer one. make translationscompileslocale/<lang>/LC_MESSAGES/gkeymapper.mo.- 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. Internallyapply_gui_language()(src/settings.cpp) setsLANGUAGEand re-runssetlocale—LANGUAGEwins overLANG/LC_ALLin gettext, which is what makes switching without a restart and an explicitenon a German desktop work. The catalogs are searched under$GKEYMAPPER_LOCALEDIR, falling back to the compiled-inLOCALEDIR.
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 potis 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 itsLanguage:header matches its file name; po/en.pois an identity catalog;po/*.poand 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:linecomments in the catalogs. The locations are noise for a translator and churn whenever a line moves, so they live only in the pot (updatetherefore usesmsgmerge --no-location) andtodoshows them from there when they are needed. This is also why the catalogs are merged with the script instead of a baremsgmerge --update, which would copy the locations in. - The build does not regenerate the pot (
make translationsonly runsmsgfmt). A template that is rewritten by every build is a template nobody looks at; insteadmake potis a deliberate step andmake i18n-checkfails when it was forgotten. - Mnemonics stay mnemonics.
_Canceluses the_to underline a letter;_Abbrechenmoves 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 -ccatches a mismatch. - No plurals yet: the program only uses
_(), so no catalog carries aPlural-Formsheader. With the firstngettext()that header has to go into every catalog (andscripts/i18n.sh newshould 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.poonly — 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 ismsgmerge's backup from a manualmsgmerge --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
#, fuzzyline). - "not merged against the current pot" in
check— runscripts/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.