Skip to content

fix(a11y): announce that a reply is generating, and that one failed - #3014

Open
opensource-joe wants to merge 2 commits into
Chainlit:mainfrom
opensource-joe:a11y/announce-reply-status
Open

fix(a11y): announce that a reply is generating, and that one failed#3014
opensource-joe wants to merge 2 commits into
Chainlit:mainfrom
opensource-joe:a11y/announce-reply-status

Conversation

@opensource-joe

@opensource-joe opensource-joe commented Aug 19, 2026

Copy link
Copy Markdown

The chat UI has no live regions anywhere, so a screen reader user gets no signal that a reply is coming or that one failed. I checked before assuming: zero aria-live, role="status", role="log", role="alert" or aria-atomic across all 208 files in frontend/src.

This adds the two that matter most. It is a small change and I have kept it deliberately narrow.

Generating

The only cue that a reply is on its way is BlinkingCursor, which is an empty <span> animated by CSS. It has no text, so there is nothing for assistive technology to announce.

I added a visually hidden role="status" region in MessagesContainer, driven by the same loading value that drives the cursor. The announcement therefore covers exactly the period the cursor covers, no more and no less.

It announces the state, not the reply. Wrapping streamed content in a live region makes assistive technology re-read the partial message on every chunk, which is worse than saying nothing.

Failed

An errored reply is currently distinguished only by a red avatar and red text, which is colour alone. role="alert" goes on the message wrapper when message.isError is set. These are complete messages rather than streamed ones, so an alert does not re-read.

Verified against a running app

Not just a build. I ran the server from source with a deliberately slow-streaming app and read the DOM live.

While a reply was generating:

{ "status": "Generating response", "steps": 2, "alerts": 0 }

and once it completed the region emptied again, so it announces once per reply rather than repeating:

{ "status": "", "steps": 2, "alerts": 0 }

Sending a cl.ErrorMessage:

{ "alertCount": 1,
  "alertText": ["Something went wrong generating that reply."],
  "aiMessages": [{ "role": "alert", "text": "Something went wrong generating that reply." }] }

Also green: pnpm type-check, pnpm lint and pnpm format-check on the changed files, prettier --check on all 23 locale files, and pnpm run --recursive test at 32 passed.

Two things worth your call

Translations. The new key is in all 23 locale files, since they are otherwise kept in sync at 136 keys each. The non-English values are the English string left untranslated rather than machine translated, so they can go to translators. fallbackLng means behaviour is identical either way, so I am happy to reduce this to en-US.json only if you would rather not carry placeholder values.

No unit test. I tried. Message reaches useLayoutMaxWidth and then useConfig, which needs a Recoil provider the current frontend/tests setup does not have, and mocking the tree ended up larger than the change itself. I would rather say that than add scaffolding you have deliberately avoided. Happy to add one if you can point me at the pattern you would want.

Not included on purpose

Loader is a bare spinning LoaderIcon with no accessible name, used in App.tsx, ReadOnlyThread.tsx, Messages/index.tsx and ActionButton.tsx. That is a real gap and a separate change, so I left it out rather than widen this one. Glad to follow up if you want it.


Summary by cubic

Announces reply generation and failures for screen readers to meet a11y expectations. Previously there were no live regions; now a hidden role="status" announces "Generating response" while loading, and errored replies are marked with role="alert".

  • MessagesContainer: adds a screen-reader-only role="status" region bound to loading; does not wrap streamed content to avoid re-reads.
  • Message: sets role="alert" when message.isError; visual behavior unchanged.
  • Adds chat.messages.status.generating to all locale files with English placeholders; fallbackLng keeps runtime behavior identical.
  • No API or UI changes; no migration required.

Written for commit 5a104c1. Summary will update on new commits.

Review in cubic

The chat has no live regions at all, so a screen reader user gets no
signal for either state.

Generating. The only cue that a reply is on its way is `BlinkingCursor`,
an empty `<span>` animated by CSS. It carries no text, so there is
nothing to announce and nothing to read. Adds a visually hidden
`role="status"` region in `MessagesContainer`, driven by the same
`loading` value that drives the cursor, so the announcement covers
exactly the period the cursor covers and no more.

It announces the state rather than the reply. Wrapping streamed content
in a live region makes assistive technology re-read the partial message
on every chunk, which is worse than silence.

Failed. An errored reply is marked only by a red avatar and red text,
which is colour alone. Adds `role="alert"` to the message wrapper when
`message.isError` is set. These are complete messages rather than
streamed ones, so an alert does not re-read.

The new string is added to all 23 locale files. The non-English values
are the English text, untranslated, rather than machine translation, so
they can be routed to translators. `fallbackLng` means behaviour is
identical either way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dosubot dosubot Bot added size:S This PR changes 10-29 lines, ignoring generated files. frontend Pertains to the frontend. translation Translation or localisation of strings. labels Aug 19, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

10 issues found across 25 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="backend/chainlit/translations/bn.json">

<violation number="1" location="backend/chainlit/translations/bn.json:102">
P2: The added key uses the English placeholder "Generating response" in the Bengali locale file bn.json. Since this string is what a screen reader announces in the live region, Bengali users will hear English text, contradicting the a11y goal of the PR. Translate the value to Bengali (e.g., "প্রতিক্রিয়া তৈরি হচ্ছে") so the announcement is localized like the surrounding keys in this file.</violation>
</file>

<file name="backend/chainlit/translations/te.json">

<violation number="1" location="backend/chainlit/translations/te.json:102">
P2: The new key uses an English placeholder ("Generating response") in the Telugu locale (and the other 22 non-English locale files). Because this value drives the role="status" live region, Telugu screen-reader users — the very audience this a11y change targets — will hear the announcement in English. The PR description suggests narrowing to en-US.json, but keeping untranslated placeholders in every locale means the shipped announcement is wrong in all of them. Translate this value (and the equivalents in the other locale files), or if the key should only exist in en-US.json, drop it from the non-English files.</violation>
</file>

<file name="backend/chainlit/translations/de-DE.json">

<violation number="1" location="backend/chainlit/translations/de-DE.json:102">
P2: The new `generating` key in the German locale ships the English string "Generating response" instead of a German translation, while every sibling key (`using`, `used`) is translated. Since this live-region text is announced aloud to screen-reader users, German users hear English during generation — the exact a11y behavior this PR adds will be in the wrong language. Use the German translation (e.g., "Antwort wird generiert") in this file.</violation>
</file>

<file name="backend/chainlit/translations/zh-CN.json">

<violation number="1" location="backend/chainlit/translations/zh-CN.json:102">
P2: This key's value is the English placeholder "Generating response" in the Simplified-Chinese locale file, so Chinese screen-reader users will hear the new a11y announcement in English. Every sibling key under chat.messages.status is translated (使用中, 已使用). Provide a Chinese translation, e.g. "正在生成回复", here (and in the other non-English locale files). The live-region announcement added elsewhere in this PR only achieves its a11y goal when it is localized.</violation>
</file>

<file name="backend/chainlit/translations/it.json">

<violation number="1" location="backend/chainlit/translations/it.json:102">
P2: The new generating value is an English placeholder in the Italian locale, so Italian UI users get the English string "Generating response" in the screen-reader live region instead of a localized announcement. This key is displayed to users (not just a key-maintenance artifact), so it should be translated, e.g. "Generazione della risposta".</violation>
</file>

<file name="backend/chainlit/translations/es.json">

<violation number="1" location="backend/chainlit/translations/es.json:102">
P2: The Spanish locale file ships the untranslated placeholder "Generating response" for the new `chat.messages.status.generating` key. Since this string drives the `role="status"` live region, Spanish-locale screen reader users will hear the generation announcement in English while the surrounding keys ("Usando", "Usado") are properly localized. Replace the placeholder with Spanish, e.g. "Generando respuesta".</violation>
</file>

<file name="backend/chainlit/translations/hi.json">

<violation number="1" location="backend/chainlit/translations/hi.json:102">
P2: The `generating` value in the Hindi locale file is left as the English placeholder "Generating response", so Hindi users with screen readers will hear the new role="status" announcement in English. Translate it (e.g., "प्रतिक्रिया उत्पन्न हो रही है") instead of committing the English placeholder to the hi.json file.</violation>
</file>

<file name="backend/chainlit/translations/fr-FR.json">

<violation number="1" location="backend/chainlit/translations/fr-FR.json:102">
P2: The `generating` value in fr-FR.json is the English placeholder "Generating response", but this key drives the new live region in MessagesContainer (`t('chat.messages.status.generating')`). French screen reader users will be announced "Generating response" in English, which undermines the very accessibility feature this PR adds. The PR description flags the placeholder as a way to keep keys in sync, but for this announcement the locale value is the user-facing content. Translate it (e.g. "Génération de la réponse en cours") rather than shipping English to every non-English locale; missing keys can fall back instead.</violation>
</file>

<file name="backend/chainlit/translations/ja.json">

<violation number="1" location="backend/chainlit/translations/ja.json:101">
P2: The ja.json "generating" key uses the English placeholder "Generating response" instead of a Japanese translation. This value is announced to Japanese-speaking screen reader users through the new role="status" live region, so they will hear English text in a Japanese UI. Translate it for the ja locale (e.g., "応答を生成中") rather than shipping the placeholder; only en-US.json should keep the English string.</violation>
</file>

<file name="backend/chainlit/translations/da-DK.json">

<violation number="1" location="backend/chainlit/translations/da-DK.json:101">
P2: The new `messages.status.generating` key ships the English placeholder "Generating response" in the Danish locale file. This string is read aloud to screen-reader users via the new role="status" live region, so Danish users will hear the announcement in English rather than Danish, unlike every neighboring status string in this file (e.g. "using": "Bruger", "used": "Brugte"). Provide a proper Danish translation (e.g. "Genererer svar") before shipping this locale.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

},
"messages": {
"status": {
"generating": "Generating response",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The added key uses the English placeholder "Generating response" in the Bengali locale file bn.json. Since this string is what a screen reader announces in the live region, Bengali users will hear English text, contradicting the a11y goal of the PR. Translate the value to Bengali (e.g., "প্রতিক্রিয়া তৈরি হচ্ছে") so the announcement is localized like the surrounding keys in this file.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/chainlit/translations/bn.json, line 102:

<comment>The added key uses the English placeholder "Generating response" in the Bengali locale file bn.json. Since this string is what a screen reader announces in the live region, Bengali users will hear English text, contradicting the a11y goal of the PR. Translate the value to Bengali (e.g., "প্রতিক্রিয়া তৈরি হচ্ছে") so the announcement is localized like the surrounding keys in this file.</comment>

<file context>
@@ -99,6 +99,7 @@
     },
     "messages": {
       "status": {
+        "generating": "Generating response",
         "using": "ব্যবহার করছে",
         "used": "ব্যবহৃত"
</file context>

},
"messages": {
"status": {
"generating": "Generating response",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The new key uses an English placeholder ("Generating response") in the Telugu locale (and the other 22 non-English locale files). Because this value drives the role="status" live region, Telugu screen-reader users — the very audience this a11y change targets — will hear the announcement in English. The PR description suggests narrowing to en-US.json, but keeping untranslated placeholders in every locale means the shipped announcement is wrong in all of them. Translate this value (and the equivalents in the other locale files), or if the key should only exist in en-US.json, drop it from the non-English files.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/chainlit/translations/te.json, line 102:

<comment>The new key uses an English placeholder ("Generating response") in the Telugu locale (and the other 22 non-English locale files). Because this value drives the role="status" live region, Telugu screen-reader users — the very audience this a11y change targets — will hear the announcement in English. The PR description suggests narrowing to en-US.json, but keeping untranslated placeholders in every locale means the shipped announcement is wrong in all of them. Translate this value (and the equivalents in the other locale files), or if the key should only exist in en-US.json, drop it from the non-English files.</comment>

<file context>
@@ -99,6 +99,7 @@
     },
     "messages": {
       "status": {
+        "generating": "Generating response",
         "using": "ఉపయోగిస్తోంది",
         "used": "ఉపయోగించబడింది"
</file context>

},
"messages": {
"status": {
"generating": "Generating response",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The new generating key in the German locale ships the English string "Generating response" instead of a German translation, while every sibling key (using, used) is translated. Since this live-region text is announced aloud to screen-reader users, German users hear English during generation — the exact a11y behavior this PR adds will be in the wrong language. Use the German translation (e.g., "Antwort wird generiert") in this file.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/chainlit/translations/de-DE.json, line 102:

<comment>The new `generating` key in the German locale ships the English string "Generating response" instead of a German translation, while every sibling key (`using`, `used`) is translated. Since this live-region text is announced aloud to screen-reader users, German users hear English during generation — the exact a11y behavior this PR adds will be in the wrong language. Use the German translation (e.g., "Antwort wird generiert") in this file.</comment>

<file context>
@@ -99,6 +99,7 @@
     },
     "messages": {
       "status": {
+        "generating": "Generating response",
         "using": "Verwendet",
         "used": "Verwendete"
</file context>
Suggested change
"generating": "Generating response",
"generating": "Antwort wird generiert",

},
"messages": {
"status": {
"generating": "Generating response",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This key's value is the English placeholder "Generating response" in the Simplified-Chinese locale file, so Chinese screen-reader users will hear the new a11y announcement in English. Every sibling key under chat.messages.status is translated (使用中, 已使用). Provide a Chinese translation, e.g. "正在生成回复", here (and in the other non-English locale files). The live-region announcement added elsewhere in this PR only achieves its a11y goal when it is localized.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/chainlit/translations/zh-CN.json, line 102:

<comment>This key's value is the English placeholder "Generating response" in the Simplified-Chinese locale file, so Chinese screen-reader users will hear the new a11y announcement in English. Every sibling key under chat.messages.status is translated (使用中, 已使用). Provide a Chinese translation, e.g. "正在生成回复", here (and in the other non-English locale files). The live-region announcement added elsewhere in this PR only achieves its a11y goal when it is localized.</comment>

<file context>
@@ -99,6 +99,7 @@
     },
     "messages": {
       "status": {
+        "generating": "Generating response",
         "using": "使用中",
         "used": "已使用"
</file context>
Suggested change
"generating": "Generating response",
"generating": "正在生成回复",

},
"messages": {
"status": {
"generating": "Generating response",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The new generating value is an English placeholder in the Italian locale, so Italian UI users get the English string "Generating response" in the screen-reader live region instead of a localized announcement. This key is displayed to users (not just a key-maintenance artifact), so it should be translated, e.g. "Generazione della risposta".

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/chainlit/translations/it.json, line 102:

<comment>The new generating value is an English placeholder in the Italian locale, so Italian UI users get the English string "Generating response" in the screen-reader live region instead of a localized announcement. This key is displayed to users (not just a key-maintenance artifact), so it should be translated, e.g. "Generazione della risposta".</comment>

<file context>
@@ -99,6 +99,7 @@
     },
     "messages": {
       "status": {
+        "generating": "Generating response",
         "using": "In uso",
         "used": "Utilizzato"
</file context>

},
"messages": {
"status": {
"generating": "Generating response",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The Spanish locale file ships the untranslated placeholder "Generating response" for the new chat.messages.status.generating key. Since this string drives the role="status" live region, Spanish-locale screen reader users will hear the generation announcement in English while the surrounding keys ("Usando", "Usado") are properly localized. Replace the placeholder with Spanish, e.g. "Generando respuesta".

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/chainlit/translations/es.json, line 102:

<comment>The Spanish locale file ships the untranslated placeholder "Generating response" for the new `chat.messages.status.generating` key. Since this string drives the `role="status"` live region, Spanish-locale screen reader users will hear the generation announcement in English while the surrounding keys ("Usando", "Usado") are properly localized. Replace the placeholder with Spanish, e.g. "Generando respuesta".</comment>

<file context>
@@ -99,6 +99,7 @@
     },
     "messages": {
       "status": {
+        "generating": "Generating response",
         "using": "Usando",
         "used": "Usado"
</file context>
Suggested change
"generating": "Generating response",
"generating": "Generando respuesta",

},
"messages": {
"status": {
"generating": "Generating response",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The generating value in the Hindi locale file is left as the English placeholder "Generating response", so Hindi users with screen readers will hear the new role="status" announcement in English. Translate it (e.g., "प्रतिक्रिया उत्पन्न हो रही है") instead of committing the English placeholder to the hi.json file.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/chainlit/translations/hi.json, line 102:

<comment>The `generating` value in the Hindi locale file is left as the English placeholder "Generating response", so Hindi users with screen readers will hear the new role="status" announcement in English. Translate it (e.g., "प्रतिक्रिया उत्पन्न हो रही है") instead of committing the English placeholder to the hi.json file.</comment>

<file context>
@@ -99,6 +99,7 @@
     },
     "messages": {
       "status": {
+        "generating": "Generating response",
         "using": "उपयोग कर रहे हैं",
         "used": "उपयोग किया"
</file context>
Suggested change
"generating": "Generating response",
"generating": "प्रतिक्रिया उत्पन्न हो रही है",

},
"messages": {
"status": {
"generating": "Generating response",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The generating value in fr-FR.json is the English placeholder "Generating response", but this key drives the new live region in MessagesContainer (t('chat.messages.status.generating')). French screen reader users will be announced "Generating response" in English, which undermines the very accessibility feature this PR adds. The PR description flags the placeholder as a way to keep keys in sync, but for this announcement the locale value is the user-facing content. Translate it (e.g. "Génération de la réponse en cours") rather than shipping English to every non-English locale; missing keys can fall back instead.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/chainlit/translations/fr-FR.json, line 102:

<comment>The `generating` value in fr-FR.json is the English placeholder "Generating response", but this key drives the new live region in MessagesContainer (`t('chat.messages.status.generating')`). French screen reader users will be announced "Generating response" in English, which undermines the very accessibility feature this PR adds. The PR description flags the placeholder as a way to keep keys in sync, but for this announcement the locale value is the user-facing content. Translate it (e.g. "Génération de la réponse en cours") rather than shipping English to every non-English locale; missing keys can fall back instead.</comment>

<file context>
@@ -99,6 +99,7 @@
     },
     "messages": {
       "status": {
+        "generating": "Generating response",
         "using": "Utilise",
         "used": "Utilisé"
</file context>
Suggested change
"generating": "Generating response",
"generating": "Génération de la réponse en cours",

},
"messages": {
"status": {
"generating": "Generating response",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The ja.json "generating" key uses the English placeholder "Generating response" instead of a Japanese translation. This value is announced to Japanese-speaking screen reader users through the new role="status" live region, so they will hear English text in a Japanese UI. Translate it for the ja locale (e.g., "応答を生成中") rather than shipping the placeholder; only en-US.json should keep the English string.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/chainlit/translations/ja.json, line 101:

<comment>The ja.json "generating" key uses the English placeholder "Generating response" instead of a Japanese translation. This value is announced to Japanese-speaking screen reader users through the new role="status" live region, so they will hear English text in a Japanese UI. Translate it for the ja locale (e.g., "応答を生成中") rather than shipping the placeholder; only en-US.json should keep the English string.</comment>

<file context>
@@ -98,6 +98,7 @@
     },
     "messages": {
       "status": {
+        "generating": "Generating response",
         "using": "使用中",
         "used": "使用済み"
</file context>
Suggested change
"generating": "Generating response",
"generating": "応答を生成中",

},
"messages": {
"status": {
"generating": "Generating response",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The new messages.status.generating key ships the English placeholder "Generating response" in the Danish locale file. This string is read aloud to screen-reader users via the new role="status" live region, so Danish users will hear the announcement in English rather than Danish, unlike every neighboring status string in this file (e.g. "using": "Bruger", "used": "Brugte"). Provide a proper Danish translation (e.g. "Genererer svar") before shipping this locale.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/chainlit/translations/da-DK.json, line 101:

<comment>The new `messages.status.generating` key ships the English placeholder "Generating response" in the Danish locale file. This string is read aloud to screen-reader users via the new role="status" live region, so Danish users will hear the announcement in English rather than Danish, unlike every neighboring status string in this file (e.g. "using": "Bruger", "used": "Brugte"). Provide a proper Danish translation (e.g. "Genererer svar") before shipping this locale.</comment>

<file context>
@@ -98,6 +98,7 @@
     },
     "messages": {
       "status": {
+        "generating": "Generating response",
         "using": "Bruger",
         "used": "Brugte"
</file context>

@opensource-joe

Copy link
Copy Markdown
Author

Worth taking seriously, and it made me test something I had asserted without testing. I was wrong in the PR description, so let me correct that first.

I wrote that fallbackLng makes the behaviour identical whether or not the key is present in the non-English files. It does not. Chainlit's own translation wrapper never reaches i18next's fallback, at frontend/src/components/i18n/Translator.tsx:36:

t: (path, options) => {
  if (!i18n.exists(path, options)) {
    return '...';
  }
  return t(path, options);
}

Two other things point the same way. AppWrapper.tsx:21 registers only the selected language with addResourceBundle(languageInUse, ...), so there is no en-US bundle in memory to fall back to. And config.load_translation() returns the matched locale file verbatim on an exact match, with no key-level merge.

I checked it rather than reasoning from the source. Running with language = "bn", with the key present the live region announces the English string. With the key deleted from the served bn.json, the region holds ... for the whole generation:

[{"t":"0.0","status":""},{"t":"0.6","status":"..."},{"t":"1.2","status":"..."},
 {"t":"1.8","status":"..."},{"t":"2.4","status":"..."}]

So removing the placeholder does not give Bengali users Bengali. It gives them ..., which is nothing useful to a screen reader, and that is a worse outcome than English for exactly the people this change is meant to help.

That is why the key is in all 23 files. The values are untranslated English rather than machine translation, so they can go to translators. If you would rather not carry placeholders at all, the option that does not regress anything is to translate them, and I am happy to do that as a follow-up rather than hold this one up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

frontend Pertains to the frontend. size:S This PR changes 10-29 lines, ignoring generated files. translation Translation or localisation of strings.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants