Skip to content

Commit 26d545f

Browse files
JohnMcLearclaude
andcommitted
fix(a11y): add Dialog titles/descriptions and missing index.code key
Closes #7835. - src/locales/en.json: add `index.code` (referenced by src/templates/index.html for the session-receive code input but never defined, producing a "Couldn't find translation key" console error on the landing page). - admin/src/utils/LoadingScreen.tsx, admin/src/pages/PadPage.tsx, admin/src/pages/AuthorPage.tsx: every @radix-ui/react-dialog `Dialog.Content` now has a `Dialog.Title` and `Dialog.Description` (visually hidden via `@radix-ui/react-visually-hidden` where there is no visible heading), silencing Radix's a11y console warnings on every admin page load. - src/tests/backend-new/specs/template-l10n-keys.test.ts: regression coverage — fails CI if any `data-l10n-id` in `src/templates/*.html` is missing from `src/locales/en.json`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 689dd9d commit 26d545f

8 files changed

Lines changed: 84 additions & 14 deletions

File tree

admin/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"devDependencies": {
2525
"@radix-ui/react-dialog": "^1.1.15",
2626
"@radix-ui/react-toast": "^1.2.15",
27+
"@radix-ui/react-visually-hidden": "^1.2.3",
2728
"@types/react": "^19.2.15",
2829
"@types/react-dom": "^19.2.3",
2930
"@typescript-eslint/eslint-plugin": "^8.59.4",

admin/public/ep_admin_authors/en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
"cap-warning": "Showing the first 1000 authors. Narrow your search to see more.",
1515
"feature-disabled-banner": "Author erasure is disabled. Set \"gdprAuthorErasure\": {\"enabled\": true} in settings.json to enable.",
1616
"no-results": "No authors match this search.",
17+
"confirm-dialog-title": "Confirm author erasure",
18+
"confirm-dialog-description": "Review the impact of erasing this author and confirm or cancel.",
1719
"confirm-preview-title": "Erase author {{name}}",
1820
"confirm-preview-counters": "Will clear {{tokenMappings}} token mappings, {{externalMappings}} mapper bindings, and {{chatMessages}} chat messages across {{affectedPads}} pads.",
1921
"confirm-irreversible": "This cannot be undone.",

admin/src/pages/AuthorPage.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {Trans, useTranslation} from "react-i18next";
22
import {useEffect, useMemo, useState} from "react";
33
import * as Dialog from "@radix-ui/react-dialog";
4+
import {VisuallyHidden} from "@radix-ui/react-visually-hidden";
45
import {ChevronLeft, ChevronRight, Trash2} from "lucide-react";
56
import {useStore} from "../store/store.ts";
67
import {SearchField} from "../components/SearchField.tsx";
@@ -153,16 +154,20 @@ export const AuthorPage = () => {
153154
<Dialog.Portal>
154155
<Dialog.Overlay className="dialog-confirm-overlay"/>
155156
<Dialog.Content className="dialog-confirm-content">
157+
<VisuallyHidden asChild>
158+
<Dialog.Title>{t('ep_admin_authors:confirm-dialog-title')}</Dialog.Title>
159+
</VisuallyHidden>
160+
<VisuallyHidden asChild>
161+
<Dialog.Description>{t('ep_admin_authors:confirm-dialog-description')}</Dialog.Description>
162+
</VisuallyHidden>
156163
{dialog.phase === 'loading-preview' && <div>
157164
<Trans i18nKey="ep_admin_authors:loading-preview" ns="ep_admin_authors"/>
158165
</div>}
159166
{(dialog.phase === 'preview' || dialog.phase === 'committing') && (() => {
160167
const p = dialog.preview;
161168
return <div>
162-
<Dialog.Title asChild>
163-
<h3>{t('ep_admin_authors:confirm-preview-title',
164-
{name: p.name || p.authorID})}</h3>
165-
</Dialog.Title>
169+
<h3>{t('ep_admin_authors:confirm-preview-title',
170+
{name: p.name || p.authorID})}</h3>
166171
<p>{t('ep_admin_authors:confirm-preview-counters', {
167172
tokenMappings: p.removedTokenMappings,
168173
externalMappings: p.removedExternalMappings,

admin/src/pages/PadPage.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {useStore} from "../store/store.ts";
44
import {PadFilter, PadSearchQuery, PadSearchResult} from "../utils/PadSearch.ts";
55
import {useDebounce} from "../utils/useDebounce.ts";
66
import * as Dialog from "@radix-ui/react-dialog";
7+
import {VisuallyHidden} from "@radix-ui/react-visually-hidden";
78
import {ChevronLeft, ChevronRight, Eye, Trash2, FileStack, PlusIcon, Search, X, RefreshCw, History} from "lucide-react";
89
import {useForm} from "react-hook-form";
910
import type {TFunction} from "i18next";
@@ -165,7 +166,10 @@ export const PadPage = () => {
165166
<Dialog.Portal>
166167
<Dialog.Overlay className="dialog-confirm-overlay"/>
167168
<Dialog.Content className="dialog-confirm-content">
168-
<div>{t('ep_admin_pads:ep_adminpads2_confirm', {padID: padToDelete})}</div>
169+
<VisuallyHidden asChild><Dialog.Title>{t('admin_pads.delete_pad_dialog_title')}</Dialog.Title></VisuallyHidden>
170+
<Dialog.Description asChild>
171+
<div>{t('ep_admin_pads:ep_adminpads2_confirm', {padID: padToDelete})}</div>
172+
</Dialog.Description>
169173
<div className="settings-button-bar">
170174
<button onClick={() => setDeleteDialog(false)}><Trans i18nKey="admin_pads.cancel"/></button>
171175
<button onClick={() => { deletePad(padToDelete); setDeleteDialog(false) }}>{t('admin_pads.confirm_button')}</button>
@@ -178,7 +182,10 @@ export const PadPage = () => {
178182
<Dialog.Portal>
179183
<Dialog.Overlay className="dialog-confirm-overlay"/>
180184
<Dialog.Content className="dialog-confirm-content">
181-
<div>{t('admin_pads.error_prefix')}: {errorText}</div>
185+
<VisuallyHidden asChild><Dialog.Title>{t('admin_pads.error_prefix')}</Dialog.Title></VisuallyHidden>
186+
<Dialog.Description asChild>
187+
<div>{t('admin_pads.error_prefix')}: {errorText}</div>
188+
</Dialog.Description>
182189
<div className="settings-button-bar">
183190
<button onClick={() => setErrorText(null)}>{t('admin_pads.confirm_button')}</button>
184191
</div>
@@ -191,6 +198,7 @@ export const PadPage = () => {
191198
<Dialog.Overlay className="dialog-confirm-overlay"/>
192199
<Dialog.Content className="dialog-confirm-content">
193200
<Dialog.Title className="dialog-confirm-title"><Trans i18nKey="index.newPad"/></Dialog.Title>
201+
<VisuallyHidden asChild><Dialog.Description>{t('admin_pads.create_pad_dialog_description')}</Dialog.Description></VisuallyHidden>
194202
<form onSubmit={handleSubmit(onPadCreate)}>
195203
<button className="dialog-close-button" type="button" onClick={() => setCreatePadDialogOpen(false)}>×</button>
196204
<div style={{display: 'grid', gap: '10px', gridTemplateColumns: 'auto auto', marginBottom: '1rem'}}>

admin/src/utils/LoadingScreen.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import {useStore} from "../store/store.ts";
22
import * as Dialog from '@radix-ui/react-dialog';
3+
import {VisuallyHidden} from '@radix-ui/react-visually-hidden';
4+
import {useTranslation} from 'react-i18next';
35
import brand from './brand.svg'
46

57
export const LoadingScreen = ()=>{
68
const showLoading = useStore(state => state.showLoading)
9+
const {t} = useTranslation()
710

811
return <Dialog.Root open={showLoading}><Dialog.Portal>
912
<Dialog.Overlay className="loading-screen fixed inset-0 bg-black bg-opacity-50 z-50 dialog-overlay" />
1013
<Dialog.Content className="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-50 dialog-content">
14+
<VisuallyHidden asChild><Dialog.Title>{t('admin.loading')}</Dialog.Title></VisuallyHidden>
15+
<VisuallyHidden asChild><Dialog.Description>{t('admin.loading_description')}</Dialog.Description></VisuallyHidden>
1116
<div className="flex flex-col items-center">
1217
<div className="animate-spin w-16 h-16 border-t-2 border-b-2 border-[--fg-color] rounded-full"></div>
1318
<div className="mt-4 text-[--fg-color]">

pnpm-lock.yaml

Lines changed: 11 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/locales/en.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"admin.page-title": "Admin Dashboard - Etherpad",
33
"admin.loading": "Loading…",
4+
"admin.loading_description": "Please wait while the page is loading.",
45
"admin.toggle_sidebar": "Toggle sidebar",
56
"admin.shout": "Communication",
67
"admin_shout.online_one": "There is currently {{count}} user online",
@@ -20,7 +21,11 @@
2021
"admin_pads.col.revisions": "Revisions",
2122
"admin_pads.col.users": "Users",
2223
"admin_pads.confirm_button": "OK",
24+
"admin_pads.create_pad_dialog_description": "Choose a name for the new pad.",
25+
"admin_pads.delete_pad_dialog_description": "Confirm or cancel pad deletion.",
26+
"admin_pads.delete_pad_dialog_title": "Delete pad",
2327
"admin_pads.empty_never_edited": "empty · never edited",
28+
"admin_pads.error_dialog_description": "An error has occurred.",
2429
"admin_pads.error_prefix": "Error",
2530
"admin_pads.filter.active": "Active",
2631
"admin_pads.filter.all": "All",
@@ -217,6 +222,7 @@
217222
"index.copyLinkButton": "Copy link to clipboard",
218223
"index.transferToSystem": "3. Copy session to new system",
219224
"index.transferToSystemDescription": "Open the copied link in the target browser or device to transfer your session.",
225+
"index.code": "Code",
220226
"index.transferSessionDescription": "Transfer your current session to browser or device by clicking the button below. This will copy a link to a page that will transfer your session when opened in the target browser or device.",
221227
"index.createOpenPad": "Open pad by name",
222228
"index.openPad": "open an existing Pad with the name:",
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
'use strict';
2+
3+
// Regression: src/templates/index.html referenced `data-l10n-id="index.code"`
4+
// but src/locales/en.json had no `index.code` key, producing a "Couldn't find
5+
// translation key index.code" console error on the landing page (issue #7835).
6+
// This test asserts every data-l10n-id attribute in our shipped templates has
7+
// a matching source string in en.json so the class of bug fails in CI.
8+
9+
import {readFileSync, readdirSync} from 'fs';
10+
import {join} from 'path';
11+
import {describe, it, expect} from 'vitest';
12+
13+
const repoRoot = join(__dirname, '..', '..', '..', '..');
14+
const templatesDir = join(repoRoot, 'src', 'templates');
15+
const enJsonPath = join(repoRoot, 'src', 'locales', 'en.json');
16+
17+
const en = JSON.parse(readFileSync(enJsonPath, 'utf8')) as Record<string, string>;
18+
19+
const collectKeys = (html: string): string[] => {
20+
const out: string[] = [];
21+
const re = /data-l10n-id="([^"]+)"/g;
22+
let m;
23+
while ((m = re.exec(html)) !== null) out.push(m[1]);
24+
return out;
25+
};
26+
27+
const templateFiles = readdirSync(templatesDir)
28+
.filter((f) => f.endsWith('.html'))
29+
.map((f) => join(templatesDir, f));
30+
31+
describe('template l10n keys', () => {
32+
for (const file of templateFiles) {
33+
it(`every data-l10n-id in ${file.replace(repoRoot + '/', '')} exists in en.json`, () => {
34+
const html = readFileSync(file, 'utf8');
35+
const keys = collectKeys(html);
36+
const missing = keys.filter((k) => !(k in en));
37+
expect(missing, `missing keys in en.json: ${missing.join(', ')}`).toEqual([]);
38+
});
39+
}
40+
});

0 commit comments

Comments
 (0)