The interface speaks Slovak

969 strings, an instance default and a per-person choice, and no half-done corner:
the admin prose is translated too. Design and reasoning: LLeMbas.wiki/Translations.

KEYED BY THE ENGLISH SENTENCE

A missing entry renders the key, which is the English -- so an untranslated string
looks as it always did, an English instance is byte-for-byte 1.6.0, and a
half-finished catalogue is a half-translated page rather than a page of dotted key
names. The cost is that editing an English sentence orphans its translation
silently, which is what tests/test_translations.py asserts in both directions.

No gettext: .po -> .mo is a build step and this project does not have one.

A JINJA GLOBAL, AND THEREFORE A CONTEXTVAR

`t()` is a global for the reason `brand` already documents -- render() is bypassed
by 25 TemplateResponse calls and 8 get_template().render() calls, the latter being
the SSE frames, which have no Request at all. A global is bound once at import and
the language is per person, so the active language is a ContextVar set per request.

🚨 `get_current_user` had to become `async def`. FastAPI runs a sync dependency in a
threadpool, and anyio copies the context in and discards it on the way out -- so
the language was set where nothing could see it and every page rendered in the
instance's language whatever anybody had chosen, with no error anywhere.

NOT TRANSLATED, ON PURPOSE

Everything a model reads: the 60 prompt fragments, and the dates in harness.py,
schedule/runner.py and schedule/compile.py. Only `i18n.stamp` is localised, and
only where a person reads it -- with the *format* translatable as well as the
words, because "26. septembra 2026" is a different pattern rather than the same one
with different words in it. A process locale is not an option: global, not
thread-safe, two people's pages at once.

A second fragment telling models to answer in the reader's language was written
during this work and removed: `core.style` has said it since long before, and
test_an_empty_override_turns_a_fragment_off caught the duplicate.

THE BULK PASS

1213 sites wrapped by a one-off script that only touched patterns it could not
misread. It got three wrong in a way that mattered -- `t('…')` inside `attr="…"`
where the sentence held an apostrophe, closing the Jinja string and 500ing two
pages whose partials no test renders. tests/test_translations.py now compiles all
110 templates. It also wrapped the product's own name, an SSH key header, a
keystroke hint and an example URL, all taken back out: a string is not
translatable just because it is a string.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-26 14:46:33 +00:00
co-authored by Claude Opus 5
parent da0797ccad
commit a16510aba8
104 changed files with 3833 additions and 1788 deletions
+30 -34
View File
@@ -106,7 +106,7 @@
<textarea class="composer__input" name="content" rows="1"
data-autosize data-max-height="320" data-composer-input
placeholder="{% if chat %}Send a message…{% else %}Ask anything…{% endif %}"
aria-label="Message" {{ 'autofocus' if not chat }}></textarea>
aria-label="{{ t('Message') }}" {{ 'autofocus' if not chat }}></textarea>
</div>
<div class="composer__toolbar">
@@ -118,34 +118,34 @@
<div class="picker picker--up" data-picker>
<button class="btn btn--icon composer__btn" type="button" data-picker-toggle
aria-haspopup="menu" aria-expanded="false"
aria-label="Attach" title="Attach">
aria-label="{{ t('Attach') }}" title="{{ t('Attach') }}">
{{ icon("attach") }}
</button>
<div class="picker__menu picker__menu--compact" data-picker-menu role="menu"
hidden aria-label="Attach">
hidden aria-label="{{ t('Attach') }}">
<button class="picker__option" type="button" role="menuitem"
data-attach="file">
{{ icon("attach", "icon--sm") }}
<span class="picker__option-body">
<span class="picker__option-name">File</span>
<span class="picker__option-note">PDF, text, code</span>
<span class="picker__option-name">{{ t("File") }}</span>
<span class="picker__option-note">{{ t("PDF, text, code") }}</span>
</span>
</button>
<button class="picker__option" type="button" role="menuitem"
data-attach="image">
{{ icon("image", "icon--sm") }}
<span class="picker__option-body">
<span class="picker__option-name">Image</span>
<span class="picker__option-note">Sent only to vision models</span>
<span class="picker__option-name">{{ t("Image") }}</span>
<span class="picker__option-note">{{ t("Sent only to vision models") }}</span>
</span>
</button>
<button class="picker__option" type="button" role="menuitem"
data-attach="link">
{{ icon("link", "icon--sm") }}
<span class="picker__option-body">
<span class="picker__option-name">Link</span>
<span class="picker__option-note">Fetch a page and attach its text</span>
<span class="picker__option-name">{{ t("Link") }}</span>
<span class="picker__option-note">{{ t("Fetch a page and attach its text") }}</span>
</span>
</button>
{% if can.get("library.use") %}
@@ -153,8 +153,8 @@
data-attach="knowledge">
{{ icon("archive", "icon--sm") }}
<span class="picker__option-body">
<span class="picker__option-name">Knowledge</span>
<span class="picker__option-note">From your library</span>
<span class="picker__option-name">{{ t("Knowledge") }}</span>
<span class="picker__option-note">{{ t("From your library") }}</span>
</span>
</button>
{% endif %}
@@ -209,20 +209,18 @@
<div class="picker picker--up" data-picker>
<button class="btn btn--icon composer__btn" type="button" data-picker-toggle
aria-haspopup="menu" aria-expanded="false"
aria-label="Toggle" title="Toggle">
aria-label="{{ t('Toggle') }}" title="{{ t('Toggle') }}">
{{ icon("sliders") }}
</button>
<div class="picker__menu picker__menu--scope" data-picker-menu role="menu"
hidden aria-label="Toggle">
hidden aria-label="{{ t('Toggle') }}">
{% if has_scope %}
<p class="picker__lede">
Switched off here only. Everything is on unless you say otherwise.
</p>
<p class="picker__lede">{{ t("Switched off here only. Everything is on unless you say otherwise.") }}</p>
{% endif %}
{% if scope_families %}
<p class="picker__group">Tools</p>
<p class="picker__group">{{ t("Tools") }}</p>
{% for family in scope_families %}
<label class="picker__option picker__option--toggle">
{% if scope_prospective %}
@@ -243,7 +241,7 @@
{% endif %}
{% if scope_skills %}
<p class="picker__group">Skills</p>
<p class="picker__group">{{ t("Skills") }}</p>
{% for skill in scope_skills %}
<label class="picker__option picker__option--toggle">
{% if scope_prospective %}
@@ -277,7 +275,7 @@
empty body. #}
{% if scope_allow %}
<div class="picker__allow">
<p class="picker__group">Always allowed here</p>
<p class="picker__group">{{ t("Always allowed here") }}</p>
{% for entry in scope_allow %}
<p class="picker__allow-entry">{{ entry }}</p>
{% endfor %}
@@ -286,7 +284,7 @@
hx-target="closest .picker__allow" hx-swap="outerHTML">
{{ icon("trash", "icon--sm") }}
<span class="picker__option-body">
<span class="picker__option-name">Ask me about these again</span>
<span class="picker__option-name">{{ t("Ask me about these again") }}</span>
</span>
</button>
</div>
@@ -316,7 +314,7 @@
<input type="hidden" name="kind" value="{{ starting_kind | default('chat') }}"
id="chat-kind">
<div class="segmented" role="group" aria-label="Kind of chat">
<div class="segmented" role="group" aria-label="{{ t('Kind of chat') }}">
<label class="segmented__option">
<input type="radio" name="kind_choice" value="chat"
{{ '' if starting_kind == 'agent' else 'checked' }}>
@@ -331,7 +329,7 @@
<span class="composer__agent" data-agent-extra hidden>
<select class="select select--sm composer__connection"
name="ssh_profile_id" aria-label="Connection">
name="ssh_profile_id" aria-label="{{ t('Connection') }}">
{% for profile in agent_profiles %}
<option value="{{ profile.id }}" data-dir="{{ profile.default_dir }}"
{{ 'disabled' if not profile.verified }}>
@@ -346,14 +344,14 @@
is also something you would rather find than spell. #}
<input type="hidden" name="project_dir" value="" data-dir-value>
<button class="btn btn--sm composer__dir" type="button" data-dir-browse
aria-label="Project directory"
title="Choose the directory this chat works in">
aria-label="{{ t('Project directory') }}"
title="{{ t('Choose the directory this chat works in') }}">
{{ icon("folder", "icon--sm") }}
<span class="composer__dir-path" data-dir-label>/</span>
</button>
{% if agent_modes %}
<select class="select select--sm" name="agent_mode" aria-label="Approval mode">
<select class="select select--sm" name="agent_mode" aria-label="{{ t('Approval mode') }}">
{% for value, label, hint in agent_modes %}
<option value="{{ value }}" title="{{ hint }}">{{ label }}</option>
{% endfor %}
@@ -376,7 +374,7 @@
through this element's *ancestors* -- which a sibling form is not.
`form=` scopes the values, and only the values. #}
<div class="composer__context">
<select class="select select--sm" name="agent_mode" aria-label="Approval mode"
<select class="select select--sm" name="agent_mode" aria-label="{{ t('Approval mode') }}"
form="agent-mode-form"
hx-patch="/api/chats/{{ chat.id }}" hx-swap="none">
{% for value, label, hint in agent_modes %}
@@ -428,7 +426,7 @@
#}
{% if current_model and current_model.capabilities_json.get("reasoning") %}
<select class="select select--sm" name="reasoning_effort" data-effort
aria-label="Reasoning effort" title="How hard this model should think"
aria-label="{{ t('Reasoning effort') }}" title="{{ t('How hard this model should think') }}"
{% if chat %}
form="chat-params-form"
hx-patch="/api/chats/{{ chat.id }}" hx-swap="none"
@@ -449,7 +447,7 @@
#}
{% set chosen = resolved_effort if chat
else (current_model.params_json or {}).get('reasoning_effort') %}
<option value="off" {{ 'selected' if chosen not in efforts }}>Effort: off</option>
<option value="off" {{ 'selected' if chosen not in efforts }}>{{ t("Effort: off") }}</option>
{% for value in efforts %}
<option value="{{ value }}" {{ 'selected' if chosen == value }}>
Effort: {{ value }}
@@ -464,7 +462,7 @@
data-mic-state and the icon with it. #}
<button class="btn btn--icon composer__btn composer__mic" type="button"
data-mic data-mic-state="idle"
aria-label="Dictate a message" title="Dictate a message">
aria-label="{{ t('Dictate a message') }}" title="{{ t('Dictate a message') }}">
<span class="composer__icon composer__icon--mic">{{ icon("mic") }}</span>
<span class="composer__icon composer__icon--recording" aria-hidden="true">
{{ icon("stop-circle") }}
@@ -483,7 +481,7 @@
layout and cannot flash an empty button.
#}
<button class="btn btn--primary btn--icon composer__btn" type="submit"
data-composer-action="send" aria-label="Send">
data-composer-action="send" aria-label="{{ t('Send') }}">
<span class="composer__icon composer__icon--send">{{ icon("send") }}</span>
<span class="composer__icon composer__icon--stop" aria-hidden="true">
<span class="composer__stop-square"></span>
@@ -514,9 +512,7 @@
<p class="composer__hint">
Enter to send, Shift+Enter for a new line.
<button class="composer__hint-link" type="button"
onclick="window.lembasCommands &amp;&amp; window.lembasCommands.help()">
/ for commands
</button>,
onclick="window.lembasCommands &amp;&amp; window.lembasCommands.help()">{{ t("/ for commands") }}</button>,
@ for files.
{% if can.get("files.upload") %}
Drag files in, or paste an image.
@@ -531,7 +527,7 @@
{% if can.get("files.upload") %}
<div class="dropzone-overlay" aria-hidden="true">
{{ icon("attach", "icon--lg") }}
<span>Drop to attach</span>
<span>{{ t("Drop to attach") }}</span>
</div>
{% endif %}
</div>