179 lines
9.6 KiB
Markdown
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`.
|