spacr.qt.i18n

Runtime localization for the spaCR Qt application.

The application historically embedded English text directly in its widgets. Replacing every call site at once would make localization brittle, so this module provides two complementary layers:

  • tr() translates text at construction time and always falls back to the original English source string.

  • retranslate_widget_tree() safely updates already-created static Qt labels, buttons, menus, tabs, combo-box choices and accessibility text. Original English strings are retained as dynamic Qt properties, allowing a window to switch from Swedish to Korean (for example) without translating a translation. Editable values and user-entered paths are never touched.

Ten languages ship without optional dependencies or network access: English, Swedish, German, Spanish, Simplified Chinese (Mandarin), Portuguese, Hindi, Korean, Icelandic and French. Catalog entries cover application navigation, every registered module, Preferences, common actions and common settings terminology. Uncatalogued scientific or third-party terms remain in English rather than being guessed.

Classes

Language

One selectable UI language.

Functions

add_translation(→ bool)

Add one parallel translation row after the catalogs are built.

catalog_coverage(→ tuple[int, int])

Return (translated, total) for an iterable of source strings.

current_language(→ str)

Return the active persisted language without creating an import cycle.

has_translation(→ bool)

Return whether text has an exact or conservative term translation.

install_dialog_translation(→ None)

Translate transient Qt dialogs when they are shown.

install_qt_translations(→ bool)

Load Qt's own translations for language. True if one loaded.

language_choices(→ tuple[tuple[str, str], ...])

Return (display label, code) choices for Preferences.

normalize_language(→ str)

Return a supported language code, falling back to English.

retranslate_widget_tree(→ None)

Retranslate static text in root and all existing descendants.

set_translatable_items(→ None)

Fill a dropdown with translated captions over untranslatable values.

set_translatable_text(→ None)

Set dynamic UI text while retaining its canonical template and values.

tr(→ str)

Translate one English UI string.

ui_language_resolved_once()

Resolve the UI language once for the body of one synchronous build.

Module Contents

class spacr.qt.i18n.Language[source]

One selectable UI language.

Parameters:
  • code – stable persisted language code.

  • native_name – language name written in that language.

  • english_name – language name written in English.

property display_name: str[source]

Return an unambiguous native/English selector label.

spacr.qt.i18n.add_translation(source: str, values: Iterable[str]) → bool[source]

Add one parallel translation row after the catalogs are built.

The half of the app-registration seam that lands here. Every app name and every section name has to appear in every one of the nine catalogs — tests/qt/test_i18n.py walks spacr.qt.app.APPS and asserts it — so an app registered from its own module used to need nine hand-edits in this file. It now gives its translations once, to spacr.qt.app.register_app(), and they arrive here.

_ROWS and CATALOGS are both updated IN PLACE. Rebinding either would strand every module that imported the name, and retranslate_widget_tree holds one for the life of a window.

Parameters:
  • source – the English string, exactly as the UI spells it.

  • values – its translations, in LANGUAGES order after English (sv, de, es, zh_CN, pt, hi, ko, is, fr).

Returns:

True if the row was added, False if the same source and translations were already catalogued.

Raises:

ValueError – if values is not one string per language, or any of them is blank. A missing translation fails here, where the app name is in the message, rather than as a blank sidebar row in Korean.

spacr.qt.i18n.catalog_coverage(sources: Iterable[str], language: str | None = None) → tuple[int, int][source]

Return (translated, total) for an iterable of source strings.

Parameters:
  • sources – English UI strings; each is converted with str and duplicates are counted once.

  • language – language code to check; None uses the current language.

spacr.qt.i18n.current_language() → str[source]

Return the active persisted language without creating an import cycle.

spacr.qt.i18n.has_translation(text: object, language: str | None = None) → bool[source]

Return whether text has an exact or conservative term translation.

Parameters:
  • text – the English UI string, converted with str. For English itself the answer is whether the string is a catalog row.

  • language – language code to check; None uses the current language.

spacr.qt.i18n.install_dialog_translation(app) → None[source]

Translate transient Qt dialogs when they are shown.

File pickers, message boxes, input prompts and progress dialogs are often constructed and executed in one expression, so they do not exist during the main-window language pass. An application event filter catches only top-level QDialog show events and applies the same conservative exact catalog translation to their title, labels, buttons and accessible text. Dynamic paths, table data and user text remain outside that traversal.

Parameters:

app – the QApplication to install the event filter on. None does nothing, and an application that already has the filter is left as it is.

spacr.qt.i18n.install_qt_translations(app, language: str | None = None) → bool[source]

Load Qt’s own translations for language. True if one loaded.

Idempotent: a translator installed by an earlier call is removed first, so switching language twice does not leave the first one underneath answering for strings the second does not carry.

Parameters:
  • app – the QApplication the qtbase translator is installed on; None returns False.

  • language – language code to load; None uses the current language. English and languages without a Qt catalog install nothing.

spacr.qt.i18n.language_choices() → tuple[tuple[str, str], ...][source]

Return (display label, code) choices for Preferences.

spacr.qt.i18n.normalize_language(code: object) → str[source]

Return a supported language code, falling back to English.

Locale-shaped values such as pt_BR and zh-CN resolve to their bundled base/catalog variants. This also makes a manually edited QSettings file harmless.

Parameters:

code – any value; converted with str (None and other falsy values count as empty), hyphens read as underscores and matched case-insensitively. Anything unrecognised returns DEFAULT_LANGUAGE.

spacr.qt.i18n.retranslate_widget_tree(root, language: str | None = None, *, only_new: bool = False) → None[source]

Retranslate static text in root and all existing descendants.

The function is intentionally best-effort and idempotent. It never edits line-edit contents, text editors, table cells, model data, filenames or console output.

Qt’s OWN text follows too – see _follow_qt_own_catalogs() – so a language chosen after launch reaches the right-click menu of every text field, not only the captions spaCR wrote.

only_new skips widgets this pass would translate to exactly what they already say. Every visited widget is stamped with the language it was translated into and the catalog generation that was current; a later pass asked for only_new skips the ones whose stamp still matches. A language change changes the stamp, and so does a catalog gaining a row, so neither can be missed.

IT IS OFF BY DEFAULT AND THAT IS DELIBERATE. A caller that has just replaced a caption itself wants the full pass – _translate_qt_text detects an outside setter by comparing the rendered value, and a skipped widget is not compared. The one caller that asks for it is _LateCaptionTranslator, where three near-root passes an event turn apart re-walk the same tree while a module screen is being assembled.

Parameters:
  • root – the widget (or other QObject) whose child widgets and actions are walked; it is included itself when it is a QWidget. None does nothing.

  • language – language code to translate into; None uses the current language.

  • only_new – skip widgets already stamped for this language and catalog generation, as described above.

spacr.qt.i18n.set_translatable_items(combo, sources: Iterable[str], values: Iterable[object] | None = None, language: str | None = None) → None[source]

Fill a dropdown with translated captions over untranslatable values.

A combo box whose entries a handler reads back with currentText() cannot be translated: the caption moves and every comparison misses. That is why the live preview’s dropdowns were marked untranslatable outright, and why the ones that were not marked went wrong quietly – the segmentation object box handed cellen to a worker that only knows cell, and the threshold method wrote medelvärde into a settings key that only accepts mean.

Each entry here carries what the code matches on in its item DATA, so currentData() answers the same English value whatever the caption reads. The English sources are recorded on the widget, so the ordinary language pass re-renders the captions on every later change instead of freezing the language the dropdown happened to be built in.

The selected entry is kept by its value, never by its caption, and signals stay blocked while the entries are replaced.

Parameters:
  • combo – the dropdown to fill; its existing entries are replaced.

  • sources – the English captions, in order.

  • values – what each entry means to the code, in the same order; defaults to sources itself.

  • language – language to render in; the current one by default.

Raises:

ValueError – if values is not one value per caption.

spacr.qt.i18n.set_translatable_text(widget, source: str, language: str | None = None, **values: object) → None[source]

Set dynamic UI text while retaining its canonical template and values.

This is for application chrome such as Connecting to {provider}…. User text, AI replies, worker output and scientific results must not use this helper because they intentionally remain untouched by localization.

Parameters:
  • widget – a widget with setText; the template and values are stored on it so a later language pass can re-render the text.

  • source – the English template, translated with tr().

  • language – language code; None uses the current language.

  • values – placeholder values applied with str.format after translation.

spacr.qt.i18n.tr(text: object, language: str | None = None, **values: object) → str[source]

Translate one English UI string.

Missing entries intentionally remain English. Keyword values are applied with str.format after translation, allowing catalogs to reorder placeholders safely.

Parameters:
  • text – the English UI string, converted with str.

  • language – language code; None uses the current language.

  • values – placeholder values for str.format; a template the values do not fit is returned unformatted.

spacr.qt.i18n.ui_language_resolved_once()[source]

Resolve the UI language once for the body of one synchronous build.

current_language() reaches into QSettings on every call, and a dialog build calls it once per tr(). Building Preferences was measured asking the preference store what language the interface was in 346 times, through 415 QSettings reads, for one dialog; the Mask screen’s panel build had the same shape at 3,516.

THE SCOPE IS THE UNIT, not the process. A permanent cache would keep a language the user has just changed, which is the one moment the answer must be re-read; inside a single synchronous build it cannot change, because nothing runs between the calls. Nested scopes share the outermost dict and only the outermost discards it, so a screen that wraps its whole build and a helper that wraps itself do not fight.

This is the ContextVar arm of current_language(), which was declared and read but set nowhere until this existed. The environment override still wins: it is consulted before the scope is filled.

Nested helpers

_term_translation._inside_an_identifier(text: str, start: int, end: int) → bool

Whether the word at start:end is part of a code name.

A word touching _ or a digit is a piece of an identifier – cell_area, channel_1, image_path – and not a word of prose. Translating it rewrites a column name, a settings key or an SQL example into something that no longer names anything: the database browser’s own example predicate, cell_area > 1000, was shown to a Swedish user as Cell_area > 1000, and the search hint offered 'Kanal_1' for a column called channel_1.

spacr/qt/i18n.py:3675

_term_translation.replace(match: re.Match[str]) → str

Replace one matched term, recording that something changed.

spacr/qt/i18n.py:3692

install_dialog_translation._DialogTranslationFilter.eventFilter(self, watched, event)

Retranslate a dialog’s tree the first time it is shown.

spacr/qt/i18n.py:4652