Add i18n scaffolding and mark strings for translation #3

Open
opened 2026-08-19 14:28:35 +00:00 by dusan · 0 comments
Owner

Context

Terminalko is a GTK4/libadwaita application in Rust. User-visible text comes from
two places:

  • Blueprint templates (ui/*.blp) — already marked with _("…"):
    95 strings across window.blp (17), preferences.blp (40),
    session_editor.blp (35), page.blp (3).
  • Rust code49 unmarked literals in crates/terminalko/src/*.rs:
    menu labels built in code (window.rs::session_menu, colour_menu, the New
    menu in rebuild_new_menu, the terminal context menu in
    terminal.rs::context_menu_model), dialog headings and bodies (prompt,
    confirm_delete, confirm_paste), the reattach marker
    (terminal.rs::reattach_marker), page notices, and the fallbacks "Untitled"
    and "Terminalko".

There is currently no po/ directory, no gettext dependency and no extraction
step
, so none of it is translatable.

Why this is an issue rather than a small change

Two separate pieces of work:

  1. Scaffolding — a po/ directory with POTFILES.in and LINGUAS, meson's
    i18n module wired up, gettext-rs (or glib's gettext bindings) initialised
    in main with the right text domain and LOCALEDIR.
  2. Marking 49 call sites in Rust so those strings are extractable. That is
    mechanical but touches most of the GUI crate, which is why it is not being
    done as part of another change.

There is also a build wrinkle worth knowing before starting: xgettext does
not understand .blp
. Options, in rough order of preference:

  • run xgettext with --language=C --keyword=_ --keyword=C_:1c,2 over the
    .blp files, which works because blueprint's _("…") is C-shaped;
  • or list the generated .ui files (they land in OUT_DIR/ui — see
    crates/terminalko/build.rs) and let gettext's GtkBuilder ITS rules handle
    them, which means the extraction step depends on a build having happened.

Pick one and write down why in the spec.

Goal

Every user-visible string is extractable and translated at runtime, with the
English output unchanged when no translation exists.

Suggested approach

  1. Add gettext-rs to crates/terminalko, initialise the text domain in
    main.rs before the application is built, using the LOCALEDIR that meson
    passes (build.rs already forwards TK_APP_ID, TK_VERSION and
    TK_PKGDATADIR this way — add TK_LOCALEDIR alongside them).
  2. Add a gettext helper (or use the crate's macro) and wrap the 49 literals.
    Keep glib::g_warning! and log messages unwrapped: they are for
    developers, not users.
  3. po/POTFILES.in, po/LINGUAS (may start empty), po/meson.build with
    i18n.gettext(gettext_package, preset: 'glib'), and subdir('po') in the
    root meson.build. The gettext package name should be the binary name, as
    conf/build.rs already use elsewhere.
  4. Extend build-aux/ci.sh with a check that the template is up to date — an
    unextracted new string should fail CI rather than be noticed a release later.
  5. Mark the AppStream metainfo and desktop entry for translation too
    (i18n.merge_file for data/*.desktop.in and *.metainfo.xml.in, which are
    currently substituted with configure_file).

Acceptance criteria

  • meson compile -C build terminalko-pot (or the equivalent target) produces a
    .pot containing both the blueprint and the Rust strings; spot-check that
    strings from window.rs::session_menu and preferences.blp are both present.
  • With LANG=C the application looks exactly as it does today.
  • A throwaway translation (for example a xx locale with a couple of strings)
    visibly changes the UI when run with that locale, proving the runtime path
    works end to end.
  • meson install installs the compiled catalogues under
    <prefix>/share/locale.
  • ./build-aux/ci.sh and ./build-aux/smoke.sh pass.
  • A short docs/spec/i18n.md describing the extraction route chosen and why.

Notes

  • Do not translate strings the agent or terminalkoctl print: those are
    developer- and script-facing, and terminalkoctl output is parsed by tests.
  • Plural forms: ngettext where a count is involved — the paste guard's "The
    clipboard holds N lines" in window.rs::confirm_paste is the one that needs it
    today.
## Context Terminalko is a GTK4/libadwaita application in Rust. User-visible text comes from two places: - **Blueprint templates** (`ui/*.blp`) — already marked with `_("…")`: 95 strings across `window.blp` (17), `preferences.blp` (40), `session_editor.blp` (35), `page.blp` (3). - **Rust code** — **49 unmarked literals** in `crates/terminalko/src/*.rs`: menu labels built in code (`window.rs::session_menu`, `colour_menu`, the New menu in `rebuild_new_menu`, the terminal context menu in `terminal.rs::context_menu_model`), dialog headings and bodies (`prompt`, `confirm_delete`, `confirm_paste`), the reattach marker (`terminal.rs::reattach_marker`), page notices, and the fallbacks `"Untitled"` and `"Terminalko"`. There is currently **no `po/` directory, no gettext dependency and no extraction step**, so none of it is translatable. ## Why this is an issue rather than a small change Two separate pieces of work: 1. **Scaffolding** — a `po/` directory with `POTFILES.in` and `LINGUAS`, meson's `i18n` module wired up, `gettext-rs` (or glib's gettext bindings) initialised in `main` with the right text domain and `LOCALEDIR`. 2. **Marking 49 call sites** in Rust so those strings are extractable. That is mechanical but touches most of the GUI crate, which is why it is not being done as part of another change. There is also a build wrinkle worth knowing before starting: **`xgettext` does not understand `.blp`**. Options, in rough order of preference: - run `xgettext` with `--language=C --keyword=_ --keyword=C_:1c,2` over the `.blp` files, which works because blueprint's `_("…")` is C-shaped; - or list the **generated** `.ui` files (they land in `OUT_DIR/ui` — see `crates/terminalko/build.rs`) and let gettext's GtkBuilder ITS rules handle them, which means the extraction step depends on a build having happened. Pick one and write down why in the spec. ## Goal Every user-visible string is extractable and translated at runtime, with the English output unchanged when no translation exists. ## Suggested approach 1. Add `gettext-rs` to `crates/terminalko`, initialise the text domain in `main.rs` before the application is built, using the `LOCALEDIR` that meson passes (`build.rs` already forwards `TK_APP_ID`, `TK_VERSION` and `TK_PKGDATADIR` this way — add `TK_LOCALEDIR` alongside them). 2. Add a `gettext` helper (or use the crate's macro) and wrap the 49 literals. Keep `glib::g_warning!` and log messages **unwrapped**: they are for developers, not users. 3. `po/POTFILES.in`, `po/LINGUAS` (may start empty), `po/meson.build` with `i18n.gettext(gettext_package, preset: 'glib')`, and `subdir('po')` in the root `meson.build`. The gettext package name should be the binary name, as `conf`/`build.rs` already use elsewhere. 4. Extend `build-aux/ci.sh` with a check that the template is up to date — an unextracted new string should fail CI rather than be noticed a release later. 5. Mark the AppStream metainfo and desktop entry for translation too (`i18n.merge_file` for `data/*.desktop.in` and `*.metainfo.xml.in`, which are currently substituted with `configure_file`). ## Acceptance criteria - `meson compile -C build terminalko-pot` (or the equivalent target) produces a `.pot` containing both the blueprint and the Rust strings; spot-check that strings from `window.rs::session_menu` and `preferences.blp` are both present. - With `LANG=C` the application looks exactly as it does today. - A throwaway translation (for example a `xx` locale with a couple of strings) visibly changes the UI when run with that locale, proving the runtime path works end to end. - `meson install` installs the compiled catalogues under `<prefix>/share/locale`. - `./build-aux/ci.sh` and `./build-aux/smoke.sh` pass. - A short `docs/spec/i18n.md` describing the extraction route chosen and why. ## Notes - Do not translate strings the agent or `terminalkoctl` print: those are developer- and script-facing, and `terminalkoctl` output is parsed by tests. - Plural forms: `ngettext` where a count is involved — the paste guard's "The clipboard holds N lines" in `window.rs::confirm_paste` is the one that needs it today.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
dusan/terminalko#3
No description provided.