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:
@@ -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 — 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
|
||||
|
||||
Reference in New Issue
Block a user