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

179 lines
9.6 KiB
Markdown

# 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
```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 <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`.