Skip to content

Show catalogue copy for API errors on participant screens - #449

Merged
lgelauff merged 16 commits into
mainfrom
fix/i18n-api-error-copy
Sep 23, 2026
Merged

lgelauff merged 16 commits into
mainfrom
fix/i18n-api-error-copy

Conversation

@lgelauff

Copy link
Copy Markdown
Owner

Closes #397.

Why

Four participant screens printed the server's developer-facing English error text, which a Dutch reader can't read and no translator ever sees:

  • the statement composer's submit error
  • the "consultation unavailable" screen
  • the join form's error
  • the not-eligible page's reason

What changes

  • Code→message tables in i18n/server-labels.ts, one per screen, each with a generic fallback. The server's message is never rendered.
  • Statement composer:
    • It reuses conv-err-proposal-limit, conv-err-similarity and conv-err-submit-statement.
    • command_outcome_unknown says the statement was sent and may already be in, so it need not be sent again.
    • A removed original tells the participant to copy their text before cancelling.
    • 409 and 403 get a new "you cannot add statements right now" message.
    • 401 uses MediaWiki core's exception-nologin-text, copied exactly ("Please log in to be able to access this page or action."), and the join form uses it too.
  • Server fix: a failed read of the original statement, before anything is sent upstream, was reported as command_outcome_unknown. It is now upstream_unavailable (app.py), with a test.
  • Workspace: a consultation that doesn't exist gets the 404 heading and line.
  • Not-eligible page: it ignores details.displayMessage (Proto's own diagnostics since Switch join eligibility gate to canivote (#406) #425) and takes its reason from the error code.
  • Accessibility:
    • The composer error uses the form-error style and is tied to the textarea (aria-describedby, and aria-invalid for the similarity error).
    • The not-eligible page moves focus to its heading.
  • Docs: the README's "Reuse before you mint" step and rule 5 of plan_i18n.md now say to look for a MediaWiki core message first and copy it exactly.
  • Messages: new messages in English and Dutch, plus Dutch for the three reused composer messages. The README carries the new counts.

Merged with main

main gained the voucher flow (#441, #442), which appended messages and tests at the same spots as this PR. main is merged in with both sides kept; common-cancel takes main's {{Identical|Cancel}}. README counts: 975 keys, 597 offered, 378 held back.

Tests

  • Every mapped code is tested under qqx, with an English server message in the mock that must not appear.
  • Each fix was mutation-checked.
  • pytest 930, vitest 226, tsc and ruff clean (after merging main).
  • Checked live on a local Docker stack: with the composer open in Dutch when Explore closed, the server's "Explore voting is not open." was replaced by "U kunt op dit moment geen stellingen aan deze raadpleging toevoegen.", tied to the text box.

Review

This PR had a review panel with a cross-review round. Its findings are fixed in the second and third commits.

🤖 Generated with Claude Code

lgelauff and others added 8 commits September 21, 2026 21:45
Four places printed the server's developer-facing English, which a Dutch
reader cannot read and no translator ever sees: the Explore composer's
submit error, the "consultation unavailable" screen, the join form's
error, and the not-eligible page's reason. Each now maps the error's code
to its own message, through tables in i18n/server-labels.ts, and anything
the table does not know (a new code, a rate limit, a network failure) gets
that form's generic message rather than the server text.

The composer reuses the unused conv-err-proposal-limit, conv-err-similarity
and conv-err-submit-statement. An unknown outcome gets its own message
telling the participant not to resubmit as a new statement, since a fresh
composer means a fresh idempotency key and a possible duplicate. The
workspace reuses the 404 page's line for a consultation that does not
exist. The not-eligible page ignores details.displayMessage, which since
#425 only carries Proto's own diagnostics about its checker, and picks its
reason from the code.

Four new messages, in English and Dutch, plus Dutch for the three reused
composer messages. The i18n README carries the new counts.

Closes #397.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…are about

A failed read of the original statement, before anything is sent upstream,
was reported as command_outcome_unknown, so a participant rewording a
statement during an outage was told it might have been posted. It is now
upstream_unavailable, like the sibling preparation failures.

The unknown-outcome message invited a retry that could only show it again:
nothing reconciles the pending attempt, and edited text under the same key
is an idempotency conflict. It now says the statement was sent, may already
be in, need not be sent again, and that the participant can cancel and
carry on. The removed-original message gives the steps the reword composer
actually allows: copy the text before cancelling, then add it as a new
statement.

A 409 (statements no longer taken, or not joined) and a 403 (banned) now
say the participant cannot add statements right now, in a new message,
rather than "try again". A 401 keeps the generic message; no participant
message says "sign in again" yet.

The composer's error uses the form-error style, has an id the textarea
points at, and marks the field invalid only when the rewording was too far
from the original. The not-eligible page takes focus from the form it
replaces. A consultation that does not exist gets the 404 heading rather
than "Consultation unavailable".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A statement or join refused with 401 said "try again", which fails the same
way every time. Both forms now show common-err-nologin, whose text is
MediaWiki core's exception-nologin-text copied exactly, so translation
memory offers translators core's existing translations; qqq cites it with
{{msg-mw|exception-nologin-text}}. The workspace load needs no entry: it
redirects a 401 to the login before its error branch.

The i18n README's "reuse before you mint" step and rule 5 of plan_i18n.md
now say to look for a MediaWiki core message before minting one, and to
copy it exactly when it fits. The README carries the new counts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Following "reuse before you mint", eleven messages now carry text copied
from MediaWiki and its components, with MediaWiki's Dutch where it has one:
the toast's close button is Codex's "Close", the skip link is Vector's
"Jump to content", the new-tab hint is core's newwindow, the countdown units
are core's days/hours/minutes-abbrev ("2 d 4 h 30 min"), and the statement
steppers are core's "Previous"/"Next". errorpage-500-title already matched
OOUI's ooui-dialog-process-error.

The three loading texts become one message, common-loading, which is
MobileFrontend's "Loading…": the app shell placeholder (still server-stamped),
the workspace placeholder, and the reroll button while it is busy. It
replaces base-loading-conversations, conv-loading and accept-reroll-loading.
Decorative glyphs leave the messages for the markup, aria-hidden: the
steppers' arrows and the reroll button's ↻ (so accept-reroll loses it too).
The new-tab hint's leading space moves into the markup as well.

Thirty-two short labels already read exactly like a MediaWiki message in the
same sense ("Cancel", "Reason", "$1 of $2"...). Their English is unchanged,
their Dutch too where it differs from MediaWiki's; qqq now cites the source.

Every copied message is listed in the new i18n/ATTRIBUTION.md with its
source key, file and licence, with the MIT notices of OOUI and VisualEditor
and a credit to translatewiki.net's translators. All sources are
GPL-2.0-or-later or MIT, so compatible with GPL-3.0. Each copied message's
qqq cites the source with {{msg-mw|...}} and names the licence;
common-err-nologin gets the same treatment. test_i18n.py fails if qqq and
ATTRIBUTION.md disagree on which messages are copied, if a qqq entry does
not name its licence, or if the English recorded there no longer ships.

Six messages no call site uses on any branch are deleted: conv-network-error,
conv-network-error-retry, conv-err-connect, conv-error-load,
conv-err-change-vote and conv-vote-loading.

The README's "reuse before you mint" step and rule 5 of plan_i18n.md now say
to copy only from GPL-3.0-compatible sources and to record the source and
licence. The README carries the new counts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…h it elsewhere

Where our Dutch differed from MediaWiki's for a message we copy, MediaWiki's
wins, lowercased where the English is:
- conv-arg-unhide: "tonen" -> "zichtbaar maken"
- conv-orient-collapse: "inklappen" -> "samenvouwen"
- errorpage-403-title: "Geen toegang" -> "Niet toegestaan"
- conv-results-quoted: curly -> straight quotes, as quotation-marks has them

The other Dutch messages that used the same words for the same thing follow:
the statement panel's accessible names now say "uitvouwen"/"samenvouwen".

ATTRIBUTION.md records these as copied Dutch too. The qqq entries for the
copied short labels tell translators to use MediaWiki's translation, not that
it "usually fits".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
main gained the voucher flow (#441, #442), which appended its messages where
this branch appended accept-err-join, and its refusal tests where this branch
added its error-copy tests. Both are kept. common-cancel takes main's
{{Identical|Cancel}} (voucher-switch-cancel now shares its text). The README
counts are recomputed: 975 keys, 597 offered, 378 held back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
common-cancel keeps its MediaWiki citation and takes main's
{{Identical|Cancel}}. main's new voucher-switch-cancel is also "Cancel", so it
cites core's cancel as well and gets its row in ATTRIBUTION.md (English only;
the voucher pages have no Dutch yet). The README counts are recomputed: 967
keys, 589 offered, 378 held back, 539 Dutch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The two lists still have to agree on which messages are copied, and a citation
still has to name its licence. Re-asserting every copied English string here
as well only turns a wording change into a second edit in a second file, which
is a chore rather than a guard: nothing user-visible breaks if the quoted
English drifts. ATTRIBUTION.md no longer promises that check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lgelauff and others added 2 commits September 23, 2026 08:15
#447 merged while this branch was open, so main carries conv-bar-title and
conv-bar-label without their literal "%" (the number brings its own) and their
rewritten qqq. Those win. The messages this branch deletes as unused —
conv-network-error, conv-network-error-retry, conv-err-change-vote — stay
deleted, and the conv-err-* documentation stays as rewritten here.

Counts: 969 keys, 591 offered, 378 held back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"This looks like a different claim" judges what the participant meant; the
check only measures how far the text is from the original. It now says that,
in both languages, and the qqq notes the check is off unless an organizer
sets a threshold.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lgelauff and others added 5 commits September 23, 2026 09:43
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#443 and #398 merged while this branch was open. The "still English on
purpose" list drops the moderation-log fallbacks (#398 fixed them) and no
longer names server error messages, which is what this branch fixes; it keeps
the sentence saying participant screens map an error's code to catalogue copy.
Counts recomputed: 978 keys, 600 offered, 378 held back, 542 Dutch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
That branch reworded conv-err-similarity (it now says the text deviates from
the original rather than judging it a different claim), which lands next to
the network messages this branch deletes. Its wording wins, the deletions
stay. Counts: 970 keys, 592 offered, 378 held back, 540 Dutch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…wn word

From the review panel on #450:

- Arrows in the markup cannot be mirrored by a translator and are not mirrored
  by Unicode either, so a right-to-left page pointed them the wrong way. Every
  decorative ← and → in the SPA now carries .dir-glyph, which flips under
  [dir="rtl"] — the 18 spans this branch and earlier work introduced, plus the
  identity-reveal back link, whose arrow was a bare literal inside the link's
  accessible name. Arrows that live inside a message stay the translator's.
- common-loading is written for a whole line and read wrong in the reroll
  button: capitalised next to a lowercase label, and 18 characters of Dutch in
  a control a few characters wide. The button has accept-reroll-loading back
  ("loading…" / "laden…"); common-loading keeps the page-level states.
- accept-reroll-aria did not contain the word the button shows, so voice
  control could not target it (WCAG 2.5.3). It now reads "Reroll: generate new
  pseudonym options", and the qqq for it and for the two nav labels says the
  visible text has to appear in the accessible name.
- test_spa_shell_locale used a temp catalogue value identical to app.py's
  hardcoded fallback, so deleting the key left it green. It now stamps
  CATALOGUE-EN-LOADING, and the missing-catalogue test still asserts the
  fallback.

Counts: 971 keys, 593 offered, 378 held back, 541 Dutch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reuse MediaWiki's messages where they fit, and credit them
lgelauff added a commit that referenced this pull request Sep 23, 2026
The reservation case built its time at import — five minutes ahead — so the
suite had five minutes to reach the test. A CI run that took 5m41s found the
reservation lapsed, and a lapsed reservation is claimable
(services/vouchers.py:225 blocks only while reserved_until > now, :255 sweeps
it up), so the code redeemed and the test that expects a refusal failed. It
failed on #449, whose diff has nothing to do with vouchers.

#470 fixed the timing by turning both times into offsets from now, which left
the direction of time carried by a minus sign — {'expires_at': -timedelta(days=1)}
beside {'reserved_until': timedelta(minutes=5)} — and put the rule that read
them (any timedelta means now plus that) eleven lines below the list.

Neither case cares how far a date is from now, only which side it falls on:
one code is past its expiry, another is still reserved by someone else. Two
sentinels say that, the values are plain dicts again, and a date cannot be
overtaken by a slow suite. The cases are named as well, so the next failure
says "reserved" instead of "kwargs2".

Checked, beyond the suite: a throwaway matrix over status x expiry x
reservation (84 cases) agreed with the rule in services/vouchers.py; with the
clock moved forward 6 minutes, 2 hours, 400 days and 60 years the sentinel
cases kept their meaning, while the old five-minute reservation flipped to
redeemable at 6 minutes; tz-aware and naive values classify alike. That matrix
was deliberately not kept.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
main gained #470/#471 (the voucher reservation test no longer races the clock,
which is what failed this branch's CI, nothing here touched vouchers) and #468
(the status table says "committed" rather than a tick). Main's wording wins;
counts recomputed: 971 keys, 593 offered, 378 held back, 541 Dutch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@lgelauff
lgelauff merged commit 06e1138 into main Sep 23, 2026
4 checks passed
@lgelauff
lgelauff deleted the fix/i18n-api-error-copy branch September 23, 2026 16:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Show catalogue copy for API errors on participant screens, not the server's English message

1 participant