Translating a Lab

Internationalization with tr() / make_tr()

from SRE.lib_sre import make_tr

default_language = 'en'
tr = make_tr(default_language)

title = tr("SSH Lab", fr="TP SSH")
# In NetScheme.__init__:
self.informations = tr("## Description\nConfigure SSH.",
                       fr="## Description\nConfigurez SSH.")

tr(default, **lang_overrides) returns a TranslatedText dict subclass. Call .resolve('fr') to get the string for a given language.

make_tr(default_lang, translations=None) returns a tr() function bound to default_lang as the key for the first positional argument. When translations= is omitted, the caller module’s globals are inspected for a _TRANSLATIONS dict at each call; pass it explicitly to avoid relying on module-level state.

Marking strings that must NOT be translated: no_tr()

from SRE.lib_sre import no_tr

label = no_tr("MTU")             # short internal label, identical in every language

no_tr() is an identity passthrough at runtime — it returns the string unchanged. Its sole purpose is to flag a literal so the translation toolchain leaves it alone: it is never wrapped in tr() and never registered in _TRANSLATIONS. Use it for short internal labels (grade-element titles, protocol names, command names) that are not natural-language prose.

By default, prepare-sre-translations already wraps any whitespace-free single-token bare string in no_tr() rather than tr() (see --translate-isolated-words below to override).

Centralizing translations with _TRANSLATIONS

For labs with many strings, keeping translations inline in every tr() call becomes unwieldy. The _TRANSLATIONS dict centralises them all in one place:

from SRE.lib_sre import make_tr

default_language = 'en'
tr = make_tr(default_language)

_TRANSLATIONS = {
    'fr': {
        "SSH Lab":                  "TP SSH",
        "Configure the SSH server": "Configurez le serveur SSH.",
        "SSH port":                 None,   # not yet translated
    },
}

title       = tr("SSH Lab")
description = tr("Configure the SSH server")

None marks a string that still needs translation; tr() falls back to the default-language text in that case.

You can pass the dict explicitly to make_tr(), which avoids any reliance on module-level state:

tr = make_tr('en', translations=_TRANSLATIONS)

Where to put _TRANSLATIONS

Two valid placements; the choice affects which tr() calls can resolve translations at import time.

At the top of the file (default for prepare-sre-translations): _TRANSLATIONS is defined right after tr = make_tr(...), before any tr() call. Every tr() call — including module-level and class-body ones evaluated at import time — can look up its translation. Inline fr=/de=/… kwargs are not needed.

At the end of the file (prepare-sre-translations --translations-at-the-end): _TRANSLATIONS lives at the very bottom, out of the way of the lab code. Because Python evaluates the module top-to-bottom, any tr() call in the module body or in class bodies runs before _TRANSLATIONS exists. To keep those import-time calls correct, prepare-sre-translations keeps (and re-adds on each run) inline lang kwargs on every such call. tr() calls inside method bodies — which only run at lab-execution time — do not need the kwargs and remain bare.

Dynamic strings with format placeholders

For strings that embed runtime values, use {placeholder} syntax in _TRANSLATIONS values and call .format() on the result:

_TRANSLATIONS = {
    'fr': {
        "The machine {machine} is configured": "La machine {machine} est configurée",
    },
}

msg = tr("The machine {machine} is configured").format(machine=m)

The static key "The machine {machine} is configured" is what tr() looks up in _TRANSLATIONS; .format() applies str.format to every language value at once and returns a new TranslatedText.

This is the equivalent of the inline f-string form:

msg = tr(f"The machine {m} is configured",
         fr=f"La machine {m} est configurée")

The inline f-string form still works and is fine for one-off strings. Use _TRANSLATIONS + .format() when the same template appears in many places, or when you want the translation toolchain (check-sre-translations, add-sre-translations) to see the string as a static literal.

Translation toolchain

Three command-line tools manage the complete translation lifecycle.

1. prepare-sre-translations — migrate and scaffold

sbin/prepare-sre-translations reads a lab file and:

  • wraps bare translatable strings (module-level title, description, informations, lab_name; self.informations / self.description; title=/description=/informations= kwargs; first positional arg of question_text/question_form/question_dummy/add_grade_element/add_grade_part) in tr() — or in no_tr() if the string is a single whitespace-free token

  • recurses into BinOp ("x" + var) and IfExp ("x" if c else "y") expressions in those positions, wrapping each leaf

  • lowers bare f-strings to tr("template").format(var=var, ...)

  • creates or updates _TRANSLATIONS with one entry per string per language (value None for strings not yet translated)

  • inserts tr = make_tr(...) and the matching from SRE.lib_sre import make_tr (and no_tr if needed) if absent

prepare-sre-translations [--move-tr-strings] [--default-language xx]
                         [--change-default-language xx]
                         [--translations-at-the-end]
                         [--translate-isolated-words] file

Option

Effect

(no options)

Wrap bare strings; create/update _TRANSLATIONS after tr = make_tr(...); infer default language from file or assume en

--default-language xx

Declare (or confirm) the default language; error if the file already uses a different one

--change-default-language xx

Pivot the file’s source language to xx: rewrite every tr() literal to its xx translation, re-key _TRANSLATIONS so the inner keys are the new source texts, and preserve the previous default as a regular language. Errors out if any tr() string lacks an xx translation, or any translatable string is still bare.

--move-tr-strings

Strip inline fr=/de=/… kwargs from existing tr() calls and move them into _TRANSLATIONS

--translations-at-the-end

Place _TRANSLATIONS at the very end of the file; keep/add inline lang kwargs on every import-time tr() call (module/class body) so they don’t depend on the late definition

--translate-isolated-words

Also wrap whitespace-free single-token strings in tr() (default: wrap them in no_tr())

--default-language and --change-default-language are mutually exclusive.

Strings already wrapped in no_tr(...) are left untouched: never wrapped in tr() and never added to _TRANSLATIONS.

Typical first run on an existing lab:

# Wrap bare strings, declare the language, migrate inline kwargs:
sbin/prepare-sre-translations --default-language en --move-tr-strings lab/my_lab.py

The file is modified in-place. Run it repeatedly — it is idempotent: already-wrapped strings and existing translations are left unchanged.

2. check-sre-translations — verify consistency

sbin/check-sre-translations performs a static (AST-level) analysis and reports three categories of problem:

Category

Meaning

MISSING

String appears in a tr() call but has no entry in _TRANSLATIONS

UNTRANSLATED

Entry exists but its value is None (translation not yet done)

VANISHED

Entry exists in _TRANSLATIONS but no tr() call uses that string any more

sbin/check-sre-translations lab/my_lab.py
# lab/my_lab.py: MISSING       [fr] 'SSH port'
# lab/my_lab.py: UNTRANSLATED  [fr] 'Configure the SSH server'
# lab/my_lab.py: VANISHED      [de] 'Old string'
# lab/my_lab.py: ok (12 strings, ['fr', 'de'] languages)

Exit code is 0 if no issues are found, 1 otherwise. Suitable for CI.

Multiple files are accepted; let the shell expand globs:

sbin/check-sre-translations lab/s4/*.py

3. add-sre-translations — machine-translate missing strings

sbin/add-sre-translations calls an online translation service to fill in every None value for one target language, then writes the result back into the file.

add-sre-translations --language XX [--service YY]
    [--api-key KEY] [--credentials FILE] [--region REGION]
    [--libre-url URL] [--auto] file

Services (--service):

Service

Default credentials

deepl (default)

--api-key or $DEEPL_API_KEY

google

--api-key or $GOOGLE_API_KEY (REST); or --credentials service-account.json / $GOOGLE_APPLICATION_CREDENTIALS

libre

--api-key or $LIBRE_API_KEY (optional); --libre-url for a self-hosted instance (default: https://libretranslate.com)

azure

--api-key or $AZURE_TRANSLATOR_KEY; --region (default: global)

amazon

Standard AWS credential chain (~/.aws/ or env vars); --region or $AWS_DEFAULT_REGION

Interactive mode (default when stdin is a TTY):

For each string the tool shows the machine translation and prompts:

[fr] 'SSH port'
     → 'Port SSH'
  Accept [Enter] or type correction (q to quit):

Press Enter to accept, type a correction, or q to stop early. Progress is always saved, even on Ctrl-C or an API error. Pass --auto to accept all suggestions without prompting.

Typical session:

export DEEPL_API_KEY=your-key-here

# Fill in French translations interactively:
sbin/add-sre-translations --language fr lab/my_lab.py

# Fill in German translations automatically:
sbin/add-sre-translations --language de --auto lab/my_lab.py

# Verify everything is done:
sbin/check-sre-translations lab/my_lab.py

Complete translation workflow

For a new lab starting from scratch in English:

# 1. Scaffold: wrap strings, create _TRANSLATIONS skeleton at the end of the file
sbin/prepare-sre-translations --default-language en --translations-at-the-end lab/my_lab.py

# 2. Verify what needs translating
sbin/check-sre-translations lab/my_lab.py

# 3. Fill in French with interactive review
sbin/add-sre-translations --language fr lab/my_lab.py

# 4. Final check — should report no issues
sbin/check-sre-translations lab/my_lab.py

For a lab that already uses inline tr("text", fr="traduction") kwargs:

# Migrate inline kwargs into _TRANSLATIONS in one step
sbin/prepare-sre-translations --move-tr-strings lab/my_lab.py

# Review and fill in anything still missing
sbin/check-sre-translations lab/my_lab.py
sbin/add-sre-translations --language fr lab/my_lab.py

Switching the default language: --change-default-language

prepare-sre-translations --change-default-language xx pivots the file’s source language to xx. The resulting file is semantically equivalent to the original — same translations, just keyed off a different source — and check-sre-translations should report it clean immediately after the pivot.

It performs four edits:

  • default_language = '...' is set to xx;

  • the literal first argument of make_tr('...') is set to xx (a Name arg like make_tr(default_language) is left as-is and inherits the new value);

  • every tr("old_text", ...) call’s first positional arg is replaced with its xx translation; the xx=... kwarg, if present, is dropped (it is now the default), and if the call had any inline language kwargs, the previous default is added as <prev>="old_text" to preserve the inline-kwarg style;

  • _TRANSLATIONS is re-keyed: the inner keys flip from the previous source texts to the new xx source texts, the xx top-level entry disappears, and the previous default appears as a regular language entry mapping new-source → previous-source text.

Prerequisites

The pivot only runs if the file is already fully prepared and every tr() literal has an xx translation reachable via inline kwargs or _TRANSLATIONS. The script reports an error and leaves the file untouched when it finds any of:

  • a tr() literal without an xx translation (add-sre-translations --language xx is the suggested fix);

  • a tr() call whose first argument is not a string literal (variables, f-strings, expressions cannot be statically rewritten — lower or inline these first by running prepare-sre-translations without --change-default-language);

  • a bare translatable string still waiting to be wrapped (same fix: run prepare-sre-translations without --change-default-language first).

--change-default-language is incompatible with --default-language, and rejects a pivot to the language that is already the file’s default.

Example: switching to English as a pivot before translating

Machine-translation services produce noticeably better results when English is the source language, especially for less-common target pairs. If your lab is currently in another language but you want to pivot to English so every subsequent add-sre-translations --language fr/de/es/... call uses English as source_lang:

# 1. Fill in English translations first — these become the new source.
sbin/add-sre-translations --language en lab/my_lab.py

# 2. Pivot the file: tr() literals + _TRANSLATIONS keys + declarations all move to en.
sbin/prepare-sre-translations --change-default-language en lab/my_lab.py

# 3. Every add-sre-translations call now uses English as source_lang.
sbin/add-sre-translations --language fr lab/my_lab.py
sbin/add-sre-translations --language de lab/my_lab.py
sbin/add-sre-translations --language es lab/my_lab.py