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
@@ -54,19 +54,16 @@
enctype="multipart/form-data" class="btn-row">
<input class="input input--file" type="file" name="image"
accept="image/png,image/jpeg,image/webp,image/gif" required
aria-label="Model image">
aria-label="{{ t('Model image') }}">
<button class="btn btn--sm" type="submit">{{ icon("image", "icon--sm") }} Upload image</button>
</form>
{% if model.image_path %}
<form method="post" action="/admin/models/{{ model.id }}/image/delete">
<button class="btn btn--sm btn--danger" type="submit">Remove image</button>
<button class="btn btn--sm btn--danger" type="submit">{{ t("Remove image") }}</button>
</form>
{% endif %}
</div>
<p class="field__hint">
PNG, JPEG, WEBP or GIF, under 2 MB. Without one, the model gets a generated
badge whose colour is derived from its id.
</p>
<p class="field__hint">{{ t("PNG, JPEG, WEBP or GIF, under 2 MB. Without one, the model gets a generated badge whose colour is derived from its id.") }}</p>
</section>
{# Empty, hidden, and outside every other form: the Detect button further down
@@ -77,18 +74,18 @@
<form method="post" action="/admin/models/{{ model.id }}">
<section class="card">
<h2 class="card__title">Presentation</h2>
<h2 class="card__title">{{ t("Presentation") }}</h2>
<div class="grid grid--2">
<div class="field">
<label class="field__label" for="display-name">Display name</label>
<label class="field__label" for="display-name">{{ t("Display name") }}</label>
<input class="input" id="display-name" name="display_name"
value="{{ model.display_name }}" placeholder="{{ model.model_id }}">
<p class="field__hint">Shown instead of the raw id. Empty uses the id.</p>
<p class="field__hint">{{ t("Shown instead of the raw id. Empty uses the id.") }}</p>
</div>
<div class="field">
<label class="field__label" for="position">Position</label>
<label class="field__label" for="position">{{ t("Position") }}</label>
<input class="input" id="position" name="position" type="number" min="1"
max="{{ total }}" value="{{ position_of }}">
<p class="field__hint">
@@ -99,19 +96,15 @@
</div>
<div class="field">
<label class="field__label" for="context-length">Context length</label>
<label class="field__label" for="context-length">{{ t("Context length") }}</label>
<input class="input" id="context-length" name="context_length" type="number"
min="0" step="1" placeholder="unknown"
min="0" step="1" placeholder="{{ t('unknown') }}"
value="{{ model.context_length or '' }}">
<p class="field__hint">
How many tokens this model can hold, filled in from the endpoint where
it says. Leave it empty if you do not know: the context percentage and
automatic compaction both stay off rather than working from a guess.
</p>
<p class="field__hint">{{ t("How many tokens this model can hold, filled in from the endpoint where it says. Leave it empty if you do not know: the context percentage and automatic compaction both stay off rather than working from a guess.") }}</p>
</div>
<div class="field">
<span class="field__label">Reasoning efforts this model accepts</span>
<span class="field__label">{{ t("Reasoning efforts this model accepts") }}</span>
<div class="btn-row">
{% for value in efforts %}
<label class="checkbox">
@@ -162,14 +155,14 @@
</button>
<p class="field__hint">
The vocabulary is <strong>not the same for every model</strong>, and
The vocabulary is <strong>{{ t("not the same for every model") }}</strong>, and
sending one a model does not know is not ignored — it is rendered into
the model's chat template, which raises and fails the whole reply.
gpt-oss takes <span class="mono">low/medium/high</span>; Bonsai takes
<span class="mono">low/medium/xhigh</span> and refuses
<span class="mono">high</span>; OpenAI has added
<span class="mono">minimal</span>, <span class="mono">xhigh</span> and
<span class="mono">max</span> at various points.
gpt-oss takes <span class="mono">{{ t("low/medium/high") }}</span>; Bonsai takes
<span class="mono">{{ t("low/medium/xhigh") }}</span> and refuses
<span class="mono">{{ t("high") }}</span>; OpenAI has added
<span class="mono">{{ t("minimal") }}</span>, <span class="mono">{{ t("xhigh") }}</span> and
<span class="mono">{{ t("max") }}</span> at various points.
<br>
Tick none and the common three are offered, which is right for almost
everything. If an endpoint ever refuses one anyway, that reply is
@@ -179,9 +172,9 @@
</div>
<div class="field">
<label class="field__label" for="default-effort">Default reasoning effort</label>
<label class="field__label" for="default-effort">{{ t("Default reasoning effort") }}</label>
<select class="select" id="default-effort" name="default_effort">
<option value="">None — send nothing</option>
<option value="">{{ t("None — send nothing") }}</option>
{% for value in model_efforts %}
<option value="{{ value }}"
{{ 'selected' if model.params_json.get('reasoning_effort') == value }}>
@@ -190,19 +183,19 @@
{% endfor %}
</select>
<p class="field__hint">
A <em>seed</em>, not a per-request setting: it is copied onto a chat when
A <em>{{ t("seed") }}</em>, not a per-request setting: it is copied onto a chat when
the chat is created and when somebody switches to this model, and from
then on the chat's own value is what is sent. Changing it here therefore
does nothing to chats that already exist. The composer's picker shows
whichever level is actually in force, so what somebody sees there is
what goes out. Anyone can change it per chat with
<span class="mono">/effort</span>, and the control only appears on a
model marked <strong>Reasoning</strong> above.
<span class="mono">{{ t("/effort") }}</span>, and the control only appears on a
model marked <strong>{{ t("Reasoning") }}</strong> above.
<br>
Sent two ways at once, because there is no one field that works: OpenAI
and vLLM read <span class="mono">reasoning_effort</span>, while
and vLLM read <span class="mono">{{ t("reasoning_effort") }}</span>, while
llama.cpp drops it silently and reads only
<span class="mono">chat_template_kwargs</span> — which is the route by
<span class="mono">{{ t("chat_template_kwargs") }}</span> — which is the route by
which it reaches gpt-oss. Both go out, and only on a chat that has an
effort set, so an endpoint strict about unknown parameters is untouched
until somebody chooses one.
@@ -210,9 +203,9 @@
</div>
<div class="field">
<label class="field__label" for="description">Description</label>
<label class="field__label" for="description">{{ t("Description") }}</label>
<textarea class="textarea" id="description" name="description" rows="2"
placeholder="What is this model good at?">{{ model.description }}</textarea>
placeholder="{{ t('What is this model good at?') }}">{{ model.description }}</textarea>
<p class="field__hint">
Shown in the chat settings panel and your users' settings &mdash; and, if any
model here may ask another one a question, read by those models too.
@@ -220,26 +213,18 @@
</div>
<div class="field">
<label class="field__label" for="notes">Facts for other models</label>
<label class="field__label" for="notes">{{ t("Facts for other models") }}</label>
<textarea class="textarea" id="notes" name="notes" rows="3"
placeholder="Parameters, quantisation, a benchmark figure, what it is bad at">{{ model.notes }}</textarea>
<p class="field__hint">
Never shown to a person. It goes into the list of the other models that a
model sees when it is allowed to ask one of them a question, so write what
would help it choose: size, what this one is good and bad at, a score you
trust. Leave it empty and the description above carries that on its own.
</p>
placeholder="{{ t('Parameters, quantisation, a benchmark figure, what it is bad at') }}">{{ model.notes }}</textarea>
<p class="field__hint">{{ t("Never shown to a person. It goes into the list of the other models that a model sees when it is allowed to ask one of them a question, so write what would help it choose: size, what this one is good and bad at, a score you trust. Leave it empty and the description above carries that on its own.") }}</p>
</div>
</section>
<section class="card">
<h2 class="card__title">System prompt</h2>
<p class="card__lede">
Used by chats on this model that have no prompt of their own. A chat's own
prompt overrides this; this overrides the instance prompt.
</p>
<h2 class="card__title">{{ t("System prompt") }}</h2>
<p class="card__lede">{{ t("Used by chats on this model that have no prompt of their own. A chat's own prompt overrides this; this overrides the instance prompt.") }}</p>
<div class="field">
<label class="field__label visually-hidden" for="system-prompt">System prompt</label>
<label class="field__label visually-hidden" for="system-prompt">{{ t("System prompt") }}</label>
<textarea class="textarea" id="system-prompt" name="system_prompt" rows="5"
placeholder="{{ instance_prompt or 'No instance prompt is set.' }}"
>{{ model.system_prompt }}</textarea>
@@ -254,14 +239,14 @@
</section>
<section class="card">
<h2 class="card__title">Capabilities</h2>
<h2 class="card__title">{{ t("Capabilities") }}</h2>
<p class="card__lede">
What this endpoint can do. Endpoints rarely advertise it reliably, so it
is your call. <strong>reasoning</strong> shows the thinking block,
<strong>vision</strong> lets images be sent, and <strong>tools</strong> is
is your call. <strong>{{ t("reasoning") }}</strong> shows the thinking block,
<strong>{{ t("vision") }}</strong> lets images be sent, and <strong>{{ t("tools") }}</strong> is
whether a tool list may be sent at all — turn it on for a model that does
not support tool calling and every one of its replies fails.
<strong>embeddings</strong> is the odd one out: it says this is not a chat
<strong>{{ t("embeddings") }}</strong> is the odd one out: it says this is not a chat
model at all, but one that turns text into vectors, and it is what
<a href="/admin/extraction">Extraction</a> picks from.
</p>
@@ -280,14 +265,14 @@
<h2 class="card__title">
Built-in tools
{% if not (model.capabilities_json or {}).get("tools") %}
<span class="badge">needs tools</span>
<span class="badge">{{ t("needs tools") }}</span>
{% endif %}
</h2>
<p class="card__lede">
What this model is given, as opposed to what it is capable of. None of it
applies unless <strong>tools</strong> is ticked above. Each one is also
applies unless <strong>{{ t("tools") }}</strong> is ticked above. Each one is also
subject to the instance being configured for it and to the reader's own
permissions — this only decides whether it is offered to <em>this</em>
permissions — this only decides whether it is offered to <em>{{ t("this") }}</em>
model.
</p>
<div class="checkbox-row">
@@ -306,17 +291,17 @@
</section>
<section class="card">
<h2 class="card__title">Availability</h2>
<h2 class="card__title">{{ t("Availability") }}</h2>
<div class="field">
<div class="checkbox-row">
<label class="checkbox">
<input type="checkbox" name="enabled" value="true" {{ 'checked' if model.enabled }}>
<span>Enabled — offered in chats</span>
<span>{{ t("Enabled — offered in chats") }}</span>
</label>
<label class="checkbox">
<input type="checkbox" name="pinned" value="true" {{ 'checked' if model.pinned }}>
<span>Pinned — shortcut in the chat sidebar</span>
<span>{{ t("Pinned — shortcut in the chat sidebar") }}</span>
</label>
</div>
</div>
@@ -324,16 +309,13 @@
<div class="field">
<label class="checkbox">
<input type="checkbox" name="public" value="true" {{ 'checked' if model.public }}>
<span>Available to everyone</span>
<span>{{ t("Available to everyone") }}</span>
</label>
<p class="field__hint">
Uncheck to restrict this model to chosen groups. Administrators always
have access.
</p>
<p class="field__hint">{{ t("Uncheck to restrict this model to chosen groups. Administrators always have access.") }}</p>
</div>
<div class="field">
<span class="field__label">Groups with access</span>
<span class="field__label">{{ t("Groups with access") }}</span>
{% if groups %}
<div class="checkbox-row">
{% for group in groups %}
@@ -344,7 +326,7 @@
</label>
{% endfor %}
</div>
<p class="field__hint">Ignored while the model is available to everyone.</p>
<p class="field__hint">{{ t("Ignored while the model is available to everyone.") }}</p>
{% else %}
<p class="field__hint">
No groups yet — <a href="/admin/groups">create one</a> to restrict access.
@@ -354,7 +336,7 @@
</section>
<div class="btn-row">
<button class="btn btn--primary" type="submit">Save changes</button>
<button class="btn btn--primary" type="submit">{{ t("Save changes") }}</button>
<a class="btn btn--ghost" href="/admin/models">Back to all models</a>
</div>
</form>
@@ -362,28 +344,28 @@
{# Outside the form above, and it has to be: two forms cannot nest, and this one
posts somewhere else. See the note beside the Detect button. #}
<section class="card">
<h2 class="card__title">Default personality</h2>
<h2 class="card__title">{{ t("Default personality") }}</h2>
<p class="card__lede">
Who this model is before it has worked out who it is with somebody. Different
from the system prompt above: that is an instruction you write, this is a
character it can be — and, with <strong>Edit its own personality</strong>
character it can be — and, with <strong>{{ t("Edit its own personality") }}</strong>
ticked, one it rewrites for itself.
</p>
<p class="card__lede">
<strong>A personality belongs to a person.</strong> Each account gets its own
<strong>{{ t("A personality belongs to a person.") }}</strong> Each account gets its own
version of this model's character, starting from what you write here and
diverging from it the first time the model writes its own. Changing this
afterwards does not reach anybody who already has one, and it is not stacked
underneath theirs — two personalities at once would contradict each other and
nobody could tell which was losing. What the model *is*, as opposed to who it
has become with somebody, belongs in <strong>Description</strong> and
<strong>Facts for other models</strong> above, which are the same for everyone.
has become with somebody, belongs in <strong>{{ t("Description") }}</strong> and
<strong>{{ t("Facts for other models") }}</strong> above, which are the same for everyone.
</p>
<form method="post" action="/admin/models/{{ model.id }}/persona">
<div class="field">
<label class="field__label visually-hidden" for="persona">Default personality</label>
<label class="field__label visually-hidden" for="persona">{{ t("Default personality") }}</label>
<textarea class="textarea" id="persona" name="content" rows="6"
placeholder="Nothing yet. Write one, or let the model write its own."
placeholder="{{ t('Nothing yet. Write one, or let the model write its own.') }}"
>{{ persona.content if persona else "" }}</textarea>
<p class="field__hint">
Up to {{ persona_limit }} characters, in the first person. Empty removes it
@@ -392,11 +374,11 @@
</p>
</div>
<div class="btn-row">
<button class="btn" type="submit">Save personality</button>
<button class="btn" type="submit">{{ t("Save personality") }}</button>
{% if persona and persona.author == "model" %}
<span class="badge badge--leaf">last written by the model</span>
<span class="badge badge--leaf">{{ t("last written by the model") }}</span>
{% elif persona %}
<span class="badge">last written here</span>
<span class="badge">{{ t("last written here") }}</span>
{% endif %}
</div>
</form>
@@ -407,20 +389,16 @@
<h2 class="card__title">
Earlier defaults <span class="badge">{{ persona.revisions|length }}</span>
</h2>
<p class="card__lede">
What this default said before each change. Each person's own personality keeps
its own history, which they can see and restore in their own settings — this is
the starting point's history, not theirs.
</p>
<p class="card__lede">{{ t("What this default said before each change. Each person's own personality keeps its own history, which they can see and restore in their own settings — this is the starting point's history, not theirs.") }}</p>
<ul class="model-list">
{% for revision in persona.revisions %}
<li class="model-list__item">
<div style="min-width: 0">
<strong>{{ revision.created_at.strftime("%Y-%m-%d %H:%M") }}</strong>
{% if revision.author == "model" %}
<span class="badge badge--leaf">model</span>
<span class="badge badge--leaf">{{ t("model") }}</span>
{% else %}
<span class="badge">you</span>
<span class="badge">{{ t("you") }}</span>
{% endif %}
{% if revision.note %}<div class="text-xs faint">{{ revision.note }}</div>{% endif %}
<div class="text-xs faint">
@@ -428,8 +406,8 @@
</div>
</div>
<form method="post" action="/admin/models/{{ model.id }}/persona/revert"
data-confirm="Put this version back? The current one is kept in the history."
data-confirm-label="Restore" data-confirm-danger="false">
data-confirm="{{ t('Put this version back? The current one is kept in the history.') }}"
data-confirm-label="{{ t('Restore') }}" data-confirm-danger="false">
<input type="hidden" name="revision_id" value="{{ revision.id }}">
<button class="btn btn--sm" type="submit">
{{ icon("refresh", "icon--sm") }} Restore