# 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/.po` | one catalog per language. This is what a translator edits. | | `locale//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/.po`: replace the msgstr, leave the msgid alone. Any text editor, or `poedit` if you prefer one. 5. `make translations` compiles `locale//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 ```bash # 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 ```bash 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 ` | 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 [team]` | Create `po/.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/.po > tmp && mv tmp po/.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 (``, ``) 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 ` 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`.