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>
137 lines
6.5 KiB
HTML
137 lines
6.5 KiB
HTML
{% extends "admin/_layout.html" %}
|
|
{% from "_macros.html" import icon %}
|
|
{% set section = "general" %}
|
|
|
|
{% block title %}General - {{ brand.name }}{% endblock %}
|
|
{% block heading %}General{% endblock %}
|
|
|
|
{% block admin_content %}
|
|
<p class="admin-lede">{{ t("Instance-wide settings. These are stored in the database and take effect immediately — no restart, and they survive one.") }}</p>
|
|
|
|
{% if saved %}
|
|
<div class="alert alert--success">{{ icon("check", "icon--sm") }} <span>{{ t("Settings saved.") }}</span></div>
|
|
{% endif %}
|
|
|
|
{#
|
|
The instance name used to be here. It lives on Customization now, with the
|
|
logo, the wording and the themes -- one field, one page. Left as a link rather
|
|
than duplicated: two controls writing one value is how each becomes the answer
|
|
to "why did my change not stick?".
|
|
#}
|
|
<p class="admin-lede">
|
|
The name, the logo, the wording and the themes are on
|
|
<a href="/admin/customization">Customization</a>.
|
|
</p>
|
|
|
|
<form method="post" action="/admin/general">
|
|
|
|
<section class="card">
|
|
<h2 class="card__title">{{ t("Language") }}</h2>
|
|
<p class="card__lede">{{ t("What the interface is in for anybody who has not chosen for themselves. Everyone can pick their own under Appearance in their settings, and that choice wins here.") }}</p>
|
|
<div class="field">
|
|
<label class="field__label" for="language">{{ t("Interface language") }}</label>
|
|
<select class="select" id="language" name="language">
|
|
{% for code, name in languages %}
|
|
<option value="{{ code }}" {{ 'selected' if values.language == code
|
|
or (not values.language and code == "en") }}>{{ name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
<p class="field__hint">
|
|
This is the interface only. Models answer in whatever language you write
|
|
to them in, which is a line you can edit under
|
|
<a href="/admin/prompts">Prompts</a>.
|
|
</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="card">
|
|
<h2 class="card__title">{{ t("Default system prompt") }}</h2>
|
|
<p class="card__lede">{{ t("Applied to every chat that does not have a prompt of its own. A model's prompt overrides this, and a chat's prompt overrides both — most specific wins outright rather than the three being stacked together.") }}</p>
|
|
<div class="field">
|
|
<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="{{ t('You are a helpful assistant.') }}">{{ values.system_prompt }}</textarea>
|
|
<p class="field__hint">{{ t("Leave empty to send no system prompt at all.") }}</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="card">
|
|
<h2 class="card__title">{{ t("Compaction") }}</h2>
|
|
<p class="card__lede">{{ t("A long conversation eventually fills the model's context. When it gets close, the earlier turns are summarised and the summary is sent in their place. The messages themselves are kept and stay readable in the transcript — they simply stop being sent.") }}</p>
|
|
<div class="field">
|
|
<label class="field__label" for="compact-threshold">{{ t("Compact at") }}</label>
|
|
<input class="input" id="compact-threshold" name="compact_threshold" type="number"
|
|
min="0" max="99" value="{{ values.compact_threshold }}">
|
|
<p class="field__hint">
|
|
Percent of the model's context length. <code>0</code> turns automatic
|
|
compaction off; the button in each chat still works. Nothing happens for
|
|
a model whose context length is unset under
|
|
<a href="/admin/models">Models</a> — that is "unknown", not "small", and
|
|
this will not act on a number nobody supplied. The wording of the
|
|
summary is under <a href="/admin/prompts">Prompts</a>.
|
|
</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="card">
|
|
<h2 class="card__title">{{ t("Tool calls in an ordinary chat") }}</h2>
|
|
<p class="card__lede">{{ t("A model ends its own turn the moment it stops asking for tools — that is it saying it has what it needs, and nothing here overrides it. This is a ceiling for the case where it never says so.") }}</p>
|
|
<div class="field">
|
|
<label class="field__label" for="max-chat-rounds">{{ t("Most rounds of tool calls") }}</label>
|
|
<input class="input" id="max-chat-rounds" name="max_chat_rounds" type="number"
|
|
min="0" max="100" value="{{ values.max_chat_rounds }}">
|
|
<p class="field__hint">
|
|
Several tools can be called in one round, so this is not a count of
|
|
tools. Leave room for at least two: <code>knowledge_get</code> and
|
|
<code>notes_get</code> read a document by an id a <em>{{ t("search") }}</em>
|
|
returned, so a ceiling of one leaves the library searchable and not
|
|
readable. <code>0</code> means no ceiling, which is how an agent chat
|
|
already works — those are bounded under
|
|
<a href="/admin/agents">Agents</a> by time and tokens instead.
|
|
</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="card">
|
|
<h2 class="card__title">
|
|
Registration
|
|
{% if values.allow_signup %}
|
|
<span class="badge badge--success">{{ t("open") }}</span>
|
|
{% else %}
|
|
<span class="badge badge--danger">{{ t("closed") }}</span>
|
|
{% endif %}
|
|
</h2>
|
|
|
|
<div class="field">
|
|
<label class="checkbox">
|
|
<input type="checkbox" name="allow_signup" value="true"
|
|
{{ 'checked' if values.allow_signup }}>
|
|
<span>{{ t("Anyone who can reach this instance may create an account") }}</span>
|
|
</label>
|
|
<p class="field__hint">{{ t('Turn this off once your users exist. Sign-in is unaffected — existing accounts keep working, and the "Create one" link disappears from the sign-in page.') }}</p>
|
|
</div>
|
|
|
|
{% if values.allow_signup and user_count > 0 %}
|
|
<div class="alert alert--warning">
|
|
{{ icon("warning", "alert__icon") }}
|
|
<div>
|
|
<strong>{{ t("Registration is open.") }}</strong>
|
|
<div class="text-sm" style="margin-top: var(--sp-1)">
|
|
This instance has {{ user_count }} account{{ '' if user_count == 1 else 's' }}.
|
|
Anyone who can reach it can add another, and every account can use your
|
|
configured models and API keys.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<p class="field__hint">
|
|
<code>LEMBAS_ALLOW_SIGNUP</code> in the environment sets only the starting
|
|
value. Once saved here, this setting wins.
|
|
</p>
|
|
</section>
|
|
|
|
<div class="form-actions"><button class="btn btn--primary" type="submit">{{ t("Save settings") }}</button></div>
|
|
</form>
|
|
{% endblock %}
|