Skip to content

Commit f490809

Browse files
author
Eugene Olonov
committed
Localize the "message" and "attachment" role descriptions
Add ACTIVITY_MESSAGE_ARIA_ROLE_ALT and ACTIVITY_ATTACHMENT_ARIA_ROLE_ALT and use them for aria-roledescription of the message and attachment groups in stacked and carousel layouts, instead of hard-coded English.
1 parent 705befe commit f490809

8 files changed

Lines changed: 128 additions & 7 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ Legends:
6363
- Fixed video and animation cards disappearing in production builds when the video URL is relative or invalid, in PR [#5889](https://github.com/microsoft/BotFramework-WebChat/pull/5889), by [@OEvgeny](https://github.com/OEvgeny)
6464
- Fixed timestamps always using the 12-hour clock, such as "4:41 PM" instead of "16:41" in German, in PR [#5920](https://github.com/microsoft/BotFramework-WebChat/pull/5920), by [@OEvgeny](https://github.com/OEvgeny)
6565
- Fixed carousel cards focused with <kbd>TAB</kbd> staying partially out of view, in PR [#5926](https://github.com/microsoft/BotFramework-WebChat/pull/5926), by [@OEvgeny](https://github.com/OEvgeny)
66+
- Fixed the "message" and "attachment" role descriptions being hard-coded in English, in PR [#XXXX](https://github.com/microsoft/BotFramework-WebChat/pull/XXXX), by [@OEvgeny](https://github.com/OEvgeny)
6667

6768
## [4.19.2] - 2026-10-08
6869

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<!doctype html>
2+
<html lang="en-US">
3+
<head>
4+
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
5+
</head>
6+
<body>
7+
<main id="webchat"></main>
8+
<script type="importmap">
9+
{
10+
"imports": {
11+
"@testduet/wait-for": "https://esm.sh/@testduet/wait-for",
12+
"botframework-webchat": "/__dist__/packages/bundle/static/botframework-webchat.js",
13+
"botframework-webchat/": "/__dist__/packages/bundle/static/botframework-webchat/",
14+
"react": "/__dist__/packages/bundle/static/react.js",
15+
"react-dom": "/__dist__/packages/bundle/static/react-dom.js",
16+
"react-dom/client": "/__dist__/packages/bundle/static/react-dom/client.js"
17+
}
18+
}
19+
</script>
20+
<script type="module">
21+
import '/test-harness.mjs';
22+
import '/test-page-object.mjs';
23+
24+
import { waitFor } from '@testduet/wait-for';
25+
import { createStoreWithOptions, ReactWebChat } from 'botframework-webchat';
26+
import { createElement } from 'react';
27+
import { createRoot } from 'react-dom/client';
28+
29+
// TODO: This is for `createDirectLineEmulator` only, should find ways to eliminate this line.
30+
window.WebChat = { createStoreWithOptions };
31+
32+
run(async function () {
33+
const { directLine, store } = testHelpers.createDirectLineEmulator();
34+
const root = createRoot(document.getElementById('webchat'));
35+
const render = props =>
36+
root.render(
37+
createElement(ReactWebChat, { directLine, store, styleOptions: { hideSendBox: true }, ...props })
38+
);
39+
40+
// Role descriptions of the message and attachments, per activity.
41+
// Selects by role, not by the attribute, so an element missing its role description shows up as null.
42+
const getRoleDescriptions = () =>
43+
pageElements
44+
.activities()
45+
.map(activity =>
46+
Array.from(activity.querySelectorAll('[role="group"], [role="listitem"]'), element =>
47+
element.getAttribute('aria-roledescription')
48+
)
49+
);
50+
51+
// GIVEN: Web Chat in the default locale (en-US).
52+
render({});
53+
54+
await pageConditions.uiConnected();
55+
56+
// GIVEN: A message with text and 2 attachments in each attachment layout.
57+
for (const attachmentLayout of ['list', 'carousel']) {
58+
await directLine.emulateIncomingActivity({
59+
attachmentLayout,
60+
attachments: [1, 2].map(index => ({
61+
content: { title: `Card ${index} (${attachmentLayout})` },
62+
contentType: 'application/vnd.microsoft.card.hero'
63+
})),
64+
from: { role: 'bot' },
65+
text: `Message with ${attachmentLayout} attachments`,
66+
type: 'message'
67+
});
68+
}
69+
70+
await pageConditions.numActivitiesShown(2);
71+
72+
// THEN: A stacked message, then a carousel message, each with its text and 2 cards.
73+
await host.snapshot('local');
74+
75+
// THEN: Message and attachments are described in English.
76+
expect(getRoleDescriptions()).toEqual([
77+
['message', 'attachment', 'attachment'],
78+
['message', 'attachment', 'attachment']
79+
]);
80+
81+
// WHEN: The host switches to German and provides the strings (no translation in de-DE yet).
82+
render({
83+
locale: 'de-DE',
84+
overrideLocalizedStrings: {
85+
ACTIVITY_ATTACHMENT_ARIA_ROLE_ALT: 'Anhang',
86+
ACTIVITY_MESSAGE_ARIA_ROLE_ALT: 'Nachricht'
87+
}
88+
});
89+
90+
// THEN: Message and attachments are described in German.
91+
await waitFor(() =>
92+
expect(getRoleDescriptions()).toEqual([
93+
['Nachricht', 'Anhang', 'Anhang'],
94+
['Nachricht', 'Anhang', 'Anhang']
95+
])
96+
);
97+
});
98+
</script>
99+
</body>
100+
</html>
14.4 KB
Loading

‎packages/api/src/localization/en-US.json‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
"_ACTIVITY_YOU_SAID_ALT.comment": "This is for screen reader only.",
1313
"ACTIVITY_BOT_ATTACHED_ALT": "Bot attached:",
1414
"_ACTIVITY_BOT_ATTACHED_ALT.comment": "This is for screen reader and is narrated before each attachments sent by the bot.",
15+
"ACTIVITY_ATTACHMENT_ARIA_ROLE_ALT": "attachment",
16+
"_ACTIVITY_ATTACHMENT_ARIA_ROLE_ALT.comment": "This is for screen reader for the aria-roledescription of each attachment in a message, narrated as the type of the element.",
17+
"ACTIVITY_MESSAGE_ARIA_ROLE_ALT": "message",
18+
"_ACTIVITY_MESSAGE_ARIA_ROLE_ALT.comment": "This is for screen reader for the aria-roledescription of the text of a message, narrated as the type of the element.",
1519
"ACTIVITY_CODE_ALT": "Code sample: $1",
1620
"_ACTIVITY_CODE_ALT.comment": "This is for screen reader code sample dialog label, $1 is the dialog title.",
1721
"ACTIVITY_CONTENT_CAUTION": "AI-generated content may be incorrect",

‎packages/component/src/Activity/AttachmentRow.tsx‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1-
import { reactNode, validateProps } from '@msinternal/botframework-webchat-react-valibot';
1+
import { reactNode, validateProps, type InferReactProps } from '@msinternal/botframework-webchat-react-valibot';
22
import { useStyles } from '@msinternal/botframework-webchat-styles/react';
3+
import { hooks } from 'botframework-webchat-api';
34
import cx from 'classnames';
45
import React, { memo } from 'react';
5-
import { boolean, object, optional, pipe, readonly, string, type InferInput } from 'valibot';
6+
import { boolean, object, optional, pipe, readonly, string } from 'valibot';
67

78
import ScreenReaderText from '../ScreenReaderText';
89
import Bubble from './Bubble';
910

1011
import styles from './StackedLayout.module.css';
1112

13+
const { useLocalizer } = hooks;
14+
1215
const attachmentRowPropsSchema = pipe(
1316
object({
1417
attachedAlt: string(),
@@ -21,7 +24,7 @@ const attachmentRowPropsSchema = pipe(
2124
readonly()
2225
);
2326

24-
type AttachmentRowProps = InferInput<typeof attachmentRowPropsSchema>;
27+
type AttachmentRowProps = InferReactProps<typeof attachmentRowPropsSchema>;
2528

2629
function AttachmentRow(props: AttachmentRowProps) {
2730
const {
@@ -33,9 +36,14 @@ function AttachmentRow(props: AttachmentRowProps) {
3336
showBubble = true
3437
} = validateProps(attachmentRowPropsSchema, props);
3538
const classNames = useStyles(styles);
39+
const localize = useLocalizer();
3640

3741
return (
38-
<div aria-roledescription="attachment" className={classNames['stacked-layout__attachment-row']} role="group">
42+
<div
43+
aria-roledescription={localize('ACTIVITY_ATTACHMENT_ARIA_ROLE_ALT')}
44+
className={classNames['stacked-layout__attachment-row']}
45+
role="group"
46+
>
3947
<ScreenReaderText text={attachedAlt} />
4048
{showBubble ? (
4149
<Bubble

‎packages/component/src/Activity/CarouselFilmStrip.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,11 @@ const CarouselFilmStrip = ({
158158
</div>
159159
<div className={classNames['carousel-filmstrip__content']}>
160160
{!!activityDisplayText && (
161-
<div aria-roledescription="message" className={classNames['carousel-filmstrip__message']} role="group">
161+
<div
162+
aria-roledescription={localize('ACTIVITY_MESSAGE_ARIA_ROLE_ALT')}
163+
className={classNames['carousel-filmstrip__message']}
164+
role="group"
165+
>
162166
<ScreenReaderText text={greetingAlt} />
163167
<Bubble
164168
className={classNames['carousel-filmstrip__bubble']}

‎packages/component/src/Activity/CarouselFilmStripAttachment.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const CarouselFilmStripAttachment = ({
3030

3131
return (
3232
<li
33-
aria-roledescription="attachment"
33+
aria-roledescription={localize('ACTIVITY_ATTACHMENT_ARIA_ROLE_ALT')}
3434
className={cx(
3535
classNames['carousel-filmstrip-attachment'],
3636
{

‎packages/component/src/Activity/StackedLayout.tsx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,11 @@ const StackedLayoutInner = memo(
7272
return (
7373
<StackedLayoutMain avatar={showAvatar && renderAvatar && renderAvatar()}>
7474
{!!(hasDisplayText || messageThing?.abstract[0]) && (
75-
<div aria-roledescription="message" className={cx(classNames['stacked-layout__message-row'])} role="group">
75+
<div
76+
aria-roledescription={localize('ACTIVITY_MESSAGE_ARIA_ROLE_ALT')}
77+
className={cx(classNames['stacked-layout__message-row'])}
78+
role="group"
79+
>
7680
<ScreenReaderText text={greetingAlt} />
7781
<Bubble
7882
className={classNames['stacked-layout__message']}

0 commit comments

Comments
 (0)