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:
@@ -13,26 +13,23 @@
|
||||
card follows.
|
||||
#}
|
||||
<div class="field">
|
||||
<label class="field__label" for="schedule-title">Name</label>
|
||||
<label class="field__label" for="schedule-title">{{ t("Name") }}</label>
|
||||
<input class="input" id="schedule-title" name="title" required maxlength="200"
|
||||
value="{{ form.title }}"
|
||||
placeholder="Monday build check">
|
||||
<p class="field__hint">What this appears as in the list.</p>
|
||||
placeholder="{{ t('Monday build check') }}">
|
||||
<p class="field__hint">{{ t("What this appears as in the list.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="schedule-instruction">What should happen</label>
|
||||
<label class="field__label" for="schedule-instruction">{{ t("What should happen") }}</label>
|
||||
<textarea class="input" id="schedule-instruction" name="instruction" rows="5" required
|
||||
placeholder="Check whether the build is passing and summarise anything that broke since last week."
|
||||
placeholder="{{ t('Check whether the build is passing and summarise anything that broke since last week.') }}"
|
||||
>{{ form.instruction }}</textarea>
|
||||
<p class="field__hint">
|
||||
Written for a model that will read it with no conversation around it, so say
|
||||
the whole thing. Nobody will be there to answer a question about it.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Written for a model that will read it with no conversation around it, so say the whole thing. Nobody will be there to answer a question about it.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="schedule-target">Where the result goes</label>
|
||||
<label class="field__label" for="schedule-target">{{ t("Where the result goes") }}</label>
|
||||
<select class="select" id="schedule-target" name="target">
|
||||
{% for value, label in targets %}
|
||||
<option value="{{ value }}" {{ 'selected' if form.target == value }}>
|
||||
@@ -43,26 +40,26 @@
|
||||
</div>
|
||||
|
||||
<fieldset class="schedule-repeat">
|
||||
<legend class="field__label">When</legend>
|
||||
<legend class="field__label">{{ t("When") }}</legend>
|
||||
|
||||
<label class="checkbox">
|
||||
<input type="radio" name="repeat" value="once"
|
||||
{{ 'checked' if form.repeat == 'once' }}>
|
||||
<span>Once</span>
|
||||
<span>{{ t("Once") }}</span>
|
||||
</label>
|
||||
<label class="checkbox">
|
||||
<input type="radio" name="repeat" value="every"
|
||||
{{ 'checked' if form.repeat == 'every' }}>
|
||||
<span>Every so often</span>
|
||||
<span>{{ t("Every so often") }}</span>
|
||||
</label>
|
||||
<label class="checkbox">
|
||||
<input type="radio" name="repeat" value="calendar"
|
||||
{{ 'checked' if form.repeat == 'calendar' }}>
|
||||
<span>On particular days</span>
|
||||
<span>{{ t("On particular days") }}</span>
|
||||
</label>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="schedule-start">Starting</label>
|
||||
<label class="field__label" for="schedule-start">{{ t("Starting") }}</label>
|
||||
<div class="btn-row">
|
||||
<input class="input" type="date" id="schedule-start" name="start_date">
|
||||
<input class="input" type="time" name="start_time" value="09:00">
|
||||
@@ -74,7 +71,7 @@
|
||||
</div>
|
||||
|
||||
<div class="field" data-repeat="every">
|
||||
<label class="field__label" for="schedule-every">Repeat every</label>
|
||||
<label class="field__label" for="schedule-every">{{ t("Repeat every") }}</label>
|
||||
<div class="btn-row">
|
||||
<input class="input" type="number" id="schedule-every" name="every_amount"
|
||||
min="1" value="{{ form.every_amount }}" style="max-width: 8rem">
|
||||
@@ -87,7 +84,7 @@
|
||||
</div>
|
||||
|
||||
<div class="field" data-repeat="calendar">
|
||||
<label class="field__label">On these days</label>
|
||||
<label class="field__label">{{ t("On these days") }}</label>
|
||||
<div class="checkbox-row">
|
||||
{% for index, name in weekday_names %}
|
||||
<label class="checkbox">
|
||||
@@ -97,32 +94,29 @@
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<p class="field__hint">Leave all of them unticked for every day.</p>
|
||||
<p class="field__hint">{{ t("Leave all of them unticked for every day.") }}</p>
|
||||
|
||||
<label class="field__label" for="schedule-times">At</label>
|
||||
<label class="field__label" for="schedule-times">{{ t("At") }}</label>
|
||||
<input class="input" id="schedule-times" name="times"
|
||||
value="{{ form.times or '09:00' }}" placeholder="09:00, 17:00">
|
||||
<p class="field__hint">
|
||||
One or more times, separated by commas. These are wall-clock times: 09:00
|
||||
stays 09:00 when the clocks change.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("One or more times, separated by commas. These are wall-clock times: 09:00 stays 09:00 when the clocks change.") }}</p>
|
||||
|
||||
<label class="field__label" for="schedule-monthdays">Only on these dates</label>
|
||||
<label class="field__label" for="schedule-monthdays">{{ t("Only on these dates") }}</label>
|
||||
<input class="input" id="schedule-monthdays" name="month_days"
|
||||
value="{{ form.month_days }}" placeholder="1, 15">
|
||||
<p class="field__hint">Days of the month, if you want it narrower. Optional.</p>
|
||||
<p class="field__hint">{{ t("Days of the month, if you want it narrower. Optional.") }}</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="schedule-count">Stop after</label>
|
||||
<label class="field__label" for="schedule-count">{{ t("Stop after") }}</label>
|
||||
<input class="input" type="number" id="schedule-count" name="count" min="0"
|
||||
value="{{ form.count }}" style="max-width: 10rem">
|
||||
<p class="field__hint">Number of runs. Leave it at 0 to keep going until you stop it.</p>
|
||||
<p class="field__hint">{{ t("Number of runs. Leave it at 0 to keep going until you stop it.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="schedule-until">Or stop on</label>
|
||||
<label class="field__label" for="schedule-until">{{ t("Or stop on") }}</label>
|
||||
<input class="input" type="date" id="schedule-until" name="until">
|
||||
<p class="field__hint">Optional. Nothing runs after this date.</p>
|
||||
<p class="field__hint">{{ t("Optional. Nothing runs after this date.") }}</p>
|
||||
</div>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
|
||||
<div class="btn-row">
|
||||
<form method="post" action="/api/schedules/{{ schedule.id }}/run">
|
||||
<button class="btn btn--sm" type="submit" title="Run it now without using up the next scheduled run">
|
||||
<button class="btn btn--sm" type="submit" title="{{ t('Run it now without using up the next scheduled run') }}">
|
||||
{{ icon("bolt", "icon--sm") }} Run now
|
||||
</button>
|
||||
</form>
|
||||
@@ -59,10 +59,7 @@
|
||||
The schedule was removed but its chat was kept, which is the default when
|
||||
somebody deletes one. Said plainly rather than left as an empty bar.
|
||||
#}
|
||||
<p class="nav-empty">
|
||||
This chat belonged to a schedule that has been removed. It is kept as a
|
||||
record of what was done.
|
||||
</p>
|
||||
<p class="nav-empty">{{ t("This chat belonged to a schedule that has been removed. It is kept as a record of what was done.") }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,22 +12,19 @@
|
||||
<form method="post" action="/api/schedules/{{ schedule.id }}" class="card">
|
||||
{% include "schedules/_form.html" %}
|
||||
<div class="btn-row">
|
||||
<button class="btn btn--primary" type="submit">Save</button>
|
||||
<button class="btn btn--primary" type="submit">{{ t("Save") }}</button>
|
||||
<a class="btn" href="/scheduled">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="card">
|
||||
<h2 class="card__title">Remove</h2>
|
||||
<p class="card__lede">
|
||||
Stops it running. Its chat is kept by default and becomes an ordinary one,
|
||||
so the transcript of everything it has already done stays where it is.
|
||||
</p>
|
||||
<h2 class="card__title">{{ t("Remove") }}</h2>
|
||||
<p class="card__lede">{{ t("Stops it running. Its chat is kept by default and becomes an ordinary one, so the transcript of everything it has already done stays where it is.") }}</p>
|
||||
<form method="post" action="/api/schedules/{{ schedule.id }}/delete"
|
||||
data-confirm="Remove this schedule? It will stop running.">
|
||||
data-confirm="{{ t('Remove this schedule? It will stop running.') }}">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="keep_chat" value="1" checked>
|
||||
<span>Keep its chat</span>
|
||||
<span>{{ t("Keep its chat") }}</span>
|
||||
</label>
|
||||
<button class="btn btn--danger btn--sm" type="submit">
|
||||
{{ icon("trash", "icon--sm") }} Remove schedule
|
||||
|
||||
@@ -11,19 +11,13 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block schedules_content %}
|
||||
<p class="admin-lede">
|
||||
Work that runs on its own, whether or not you are here. Each one has its own
|
||||
chat, and replies into it every time it comes round.
|
||||
</p>
|
||||
<p class="admin-lede">{{ t("Work that runs on its own, whether or not you are here. Each one has its own chat, and replies into it every time it comes round.") }}</p>
|
||||
|
||||
{% if not schedules %}
|
||||
<div class="empty">
|
||||
{{ icon("clock", "empty__mark") }}
|
||||
<h2 class="empty__title">Nothing scheduled</h2>
|
||||
<p class="empty__text">
|
||||
Set something to run later — a daily summary, a check every Monday
|
||||
morning, a reminder in an hour.
|
||||
</p>
|
||||
<h2 class="empty__title">{{ t("Nothing scheduled") }}</h2>
|
||||
<p class="empty__text">{{ t("Set something to run later — a daily summary, a check every Monday morning, a reminder in an hour.") }}</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<ul class="model-list">
|
||||
@@ -43,13 +37,13 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
{% if item.row.target == "report" %}<span class="badge">files a report</span>{% endif %}
|
||||
{% if item.row.target == "report" %}<span class="badge">{{ t("files a report") }}</span>{% endif %}
|
||||
{# Which of these nobody typed. A schedule is invisible until it fires, so
|
||||
the list is where a model's decision is checkable at all -- and the
|
||||
reader needs to be able to tell one apart from their own without
|
||||
opening it. #}
|
||||
{% if item.row.origin == "model" %}<span class="badge">set up for you</span>{% endif %}
|
||||
{% if not item.row.enabled %}<span class="badge badge--warning">paused</span>{% endif %}
|
||||
{% if item.row.origin == "model" %}<span class="badge">{{ t("set up for you") }}</span>{% endif %}
|
||||
{% if not item.row.enabled %}<span class="badge badge--warning">{{ t("paused") }}</span>{% endif %}
|
||||
<a class="btn btn--sm" href="/scheduled/{{ item.row.id }}/edit">Edit</a>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -18,9 +18,9 @@
|
||||
#}
|
||||
<form method="post" action="/api/schedules/describe" class="card">
|
||||
<div class="field">
|
||||
<label class="field__label" for="describe">Say it in your own words</label>
|
||||
<label class="field__label" for="describe">{{ t("Say it in your own words") }}</label>
|
||||
<textarea class="input" id="describe" name="request" rows="3" required
|
||||
placeholder="Every Monday morning, check whether the build is passing and write me a report."
|
||||
placeholder="{{ t('Every Monday morning, check whether the build is passing and write me a report.') }}"
|
||||
>{{ described }}</textarea>
|
||||
<p class="field__hint">
|
||||
Times are read as {{ timezone }}. Say how often, and what should happen.
|
||||
@@ -59,9 +59,9 @@
|
||||
|
||||
{% if models %}
|
||||
<div class="field">
|
||||
<label class="field__label" for="schedule-model">Model</label>
|
||||
<label class="field__label" for="schedule-model">{{ t("Model") }}</label>
|
||||
<select class="select" id="schedule-model" name="model_id">
|
||||
<option value="">Whatever is default at the time</option>
|
||||
<option value="">{{ t("Whatever is default at the time") }}</option>
|
||||
{% for model in models %}
|
||||
<option value="{{ model.model_id }}">{{ model.display_name }}</option>
|
||||
{% endfor %}
|
||||
|
||||
Reference in New Issue
Block a user