diff --git a/admin/package.json b/admin/package.json index 4c2f171cb..81d053a50 100644 --- a/admin/package.json +++ b/admin/package.json @@ -24,6 +24,7 @@ "devDependencies": { "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-toast": "^1.2.15", + "@radix-ui/react-visually-hidden": "^1.2.3", "@types/react": "^19.2.15", "@types/react-dom": "^19.2.3", "@typescript-eslint/eslint-plugin": "^8.59.4", diff --git a/admin/public/ep_admin_authors/en.json b/admin/public/ep_admin_authors/en.json index d4215b3b4..f482d60a1 100644 --- a/admin/public/ep_admin_authors/en.json +++ b/admin/public/ep_admin_authors/en.json @@ -14,6 +14,8 @@ "cap-warning": "Showing the first 1000 authors. Narrow your search to see more.", "feature-disabled-banner": "Author erasure is disabled. Set \"gdprAuthorErasure\": {\"enabled\": true} in settings.json to enable.", "no-results": "No authors match this search.", + "confirm-dialog-title": "Confirm author erasure", + "confirm-dialog-description": "Review the impact of erasing this author and confirm or cancel.", "confirm-preview-title": "Erase author {{name}}", "confirm-preview-counters": "Will clear {{tokenMappings}} token mappings, {{externalMappings}} mapper bindings, and {{chatMessages}} chat messages across {{affectedPads}} pads.", "confirm-irreversible": "This cannot be undone.", diff --git a/admin/src/pages/AuthorPage.tsx b/admin/src/pages/AuthorPage.tsx index ea119e633..0b5626b25 100644 --- a/admin/src/pages/AuthorPage.tsx +++ b/admin/src/pages/AuthorPage.tsx @@ -1,6 +1,7 @@ import {Trans, useTranslation} from "react-i18next"; import {useEffect, useMemo, useState} from "react"; import * as Dialog from "@radix-ui/react-dialog"; +import {VisuallyHidden} from "@radix-ui/react-visually-hidden"; import {ChevronLeft, ChevronRight, Trash2} from "lucide-react"; import {useStore} from "../store/store.ts"; import {SearchField} from "../components/SearchField.tsx"; @@ -153,16 +154,20 @@ export const AuthorPage = () => { + + {t('ep_admin_authors:confirm-dialog-title')} + + + {t('ep_admin_authors:confirm-dialog-description')} + {dialog.phase === 'loading-preview' &&
} {(dialog.phase === 'preview' || dialog.phase === 'committing') && (() => { const p = dialog.preview; return
- -

{t('ep_admin_authors:confirm-preview-title', - {name: p.name || p.authorID})}

-
+

{t('ep_admin_authors:confirm-preview-title', + {name: p.name || p.authorID})}

{t('ep_admin_authors:confirm-preview-counters', { tokenMappings: p.removedTokenMappings, externalMappings: p.removedExternalMappings, diff --git a/admin/src/pages/PadPage.tsx b/admin/src/pages/PadPage.tsx index 2b8ba06d1..c216ffab7 100644 --- a/admin/src/pages/PadPage.tsx +++ b/admin/src/pages/PadPage.tsx @@ -4,6 +4,7 @@ import {useStore} from "../store/store.ts"; import {PadFilter, PadSearchQuery, PadSearchResult} from "../utils/PadSearch.ts"; import {useDebounce} from "../utils/useDebounce.ts"; import * as Dialog from "@radix-ui/react-dialog"; +import {VisuallyHidden} from "@radix-ui/react-visually-hidden"; import {ChevronLeft, ChevronRight, Eye, Trash2, FileStack, PlusIcon, Search, X, RefreshCw, History} from "lucide-react"; import {useForm} from "react-hook-form"; import type {TFunction} from "i18next"; @@ -165,7 +166,10 @@ export const PadPage = () => { -

{t('ep_admin_pads:ep_adminpads2_confirm', {padID: padToDelete})}
+ {t('admin_pads.delete_pad_dialog_title')} + +
{t('ep_admin_pads:ep_adminpads2_confirm', {padID: padToDelete})}
+
@@ -178,7 +182,10 @@ export const PadPage = () => { -
{t('admin_pads.error_prefix')}: {errorText}
+ {t('admin_pads.error_prefix')} + +
{t('admin_pads.error_prefix')}: {errorText}
+
@@ -191,6 +198,7 @@ export const PadPage = () => { + {t('admin_pads.create_pad_dialog_description')}
diff --git a/admin/src/utils/LoadingScreen.tsx b/admin/src/utils/LoadingScreen.tsx index 60c80fd60..f80c99ed4 100644 --- a/admin/src/utils/LoadingScreen.tsx +++ b/admin/src/utils/LoadingScreen.tsx @@ -1,13 +1,18 @@ import {useStore} from "../store/store.ts"; import * as Dialog from '@radix-ui/react-dialog'; +import {VisuallyHidden} from '@radix-ui/react-visually-hidden'; +import {useTranslation} from 'react-i18next'; import brand from './brand.svg' export const LoadingScreen = ()=>{ const showLoading = useStore(state => state.showLoading) + const {t} = useTranslation() return + {t('admin.loading')} + {t('admin.loading_description')}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 72252e8d6..63327005e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,6 +68,9 @@ importers: '@radix-ui/react-toast': specifier: ^1.2.15 version: 1.2.15(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-visually-hidden': + specifier: ^1.2.3 + version: 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@types/react': specifier: ^19.2.15 version: 19.2.15 @@ -8342,10 +8345,10 @@ snapshots: '@rushstack/eslint-patch': 1.16.1 '@typescript-eslint/eslint-plugin': 7.18.0(@typescript-eslint/parser@7.18.0(eslint@10.4.0)(typescript@6.0.3))(eslint@10.4.0)(typescript@6.0.3) '@typescript-eslint/parser': 7.18.0(eslint@10.4.0)(typescript@6.0.3) - eslint-import-resolver-typescript: 3.9.1(eslint-plugin-import@2.32.0)(eslint@10.4.0) + eslint-import-resolver-typescript: 3.9.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.4.0)(typescript@6.0.3))(eslint@10.4.0))(eslint@10.4.0) eslint-plugin-cypress: 2.15.2(eslint@10.4.0) eslint-plugin-eslint-comments: 3.2.0(eslint@10.4.0) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.4.0)(typescript@6.0.3))(eslint-import-resolver-typescript@3.9.1)(eslint@10.4.0) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.4.0)(typescript@6.0.3))(eslint-import-resolver-typescript@3.9.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.4.0)(typescript@6.0.3))(eslint@10.4.0))(eslint@10.4.0))(eslint@10.4.0) eslint-plugin-mocha: 10.5.0(eslint@10.4.0) eslint-plugin-n: 17.24.0(eslint@10.4.0)(typescript@6.0.3) eslint-plugin-prefer-arrow: 1.2.3(eslint@10.4.0) @@ -8366,7 +8369,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.9.1(eslint-plugin-import@2.32.0)(eslint@10.4.0): + eslint-import-resolver-typescript@3.9.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.4.0)(typescript@6.0.3))(eslint@10.4.0))(eslint@10.4.0): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3(supports-color@8.1.1) @@ -8377,18 +8380,18 @@ snapshots: stable-hash: 0.0.5 tinyglobby: 0.2.16 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.4.0)(typescript@6.0.3))(eslint-import-resolver-typescript@3.9.1)(eslint@10.4.0) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.4.0)(typescript@6.0.3))(eslint-import-resolver-typescript@3.9.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.4.0)(typescript@6.0.3))(eslint@10.4.0))(eslint@10.4.0))(eslint@10.4.0) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@7.18.0(eslint@10.4.0)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.9.1)(eslint@10.4.0): + eslint-module-utils@2.12.1(@typescript-eslint/parser@7.18.0(eslint@10.4.0)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.9.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.4.0)(typescript@6.0.3))(eslint@10.4.0))(eslint@10.4.0))(eslint@10.4.0): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 7.18.0(eslint@10.4.0)(typescript@6.0.3) eslint: 10.4.0 eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.9.1(eslint-plugin-import@2.32.0)(eslint@10.4.0) + eslint-import-resolver-typescript: 3.9.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.4.0)(typescript@6.0.3))(eslint@10.4.0))(eslint@10.4.0) transitivePeerDependencies: - supports-color @@ -8410,7 +8413,7 @@ snapshots: eslint: 10.4.0 ignore: 5.3.2 - eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.4.0)(typescript@6.0.3))(eslint-import-resolver-typescript@3.9.1)(eslint@10.4.0): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.4.0)(typescript@6.0.3))(eslint-import-resolver-typescript@3.9.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.4.0)(typescript@6.0.3))(eslint@10.4.0))(eslint@10.4.0))(eslint@10.4.0): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -8421,7 +8424,7 @@ snapshots: doctrine: 2.1.0 eslint: 10.4.0 eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@7.18.0(eslint@10.4.0)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.9.1)(eslint@10.4.0) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@7.18.0(eslint@10.4.0)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.9.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.4.0)(typescript@6.0.3))(eslint@10.4.0))(eslint@10.4.0))(eslint@10.4.0) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 diff --git a/src/locales/en.json b/src/locales/en.json index 9fc2db428..96f705080 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1,6 +1,7 @@ { "admin.page-title": "Admin Dashboard - Etherpad", "admin.loading": "Loading…", + "admin.loading_description": "Please wait while the page is loading.", "admin.toggle_sidebar": "Toggle sidebar", "admin.shout": "Communication", "admin_shout.online_one": "There is currently {{count}} user online", @@ -20,7 +21,11 @@ "admin_pads.col.revisions": "Revisions", "admin_pads.col.users": "Users", "admin_pads.confirm_button": "OK", + "admin_pads.create_pad_dialog_description": "Choose a name for the new pad.", + "admin_pads.delete_pad_dialog_description": "Confirm or cancel pad deletion.", + "admin_pads.delete_pad_dialog_title": "Delete pad", "admin_pads.empty_never_edited": "empty · never edited", + "admin_pads.error_dialog_description": "An error has occurred.", "admin_pads.error_prefix": "Error", "admin_pads.filter.active": "Active", "admin_pads.filter.all": "All", @@ -217,6 +222,7 @@ "index.copyLinkButton": "Copy link to clipboard", "index.transferToSystem": "3. Copy session to new system", "index.transferToSystemDescription": "Open the copied link in the target browser or device to transfer your session.", + "index.code": "Code", "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.", "index.createOpenPad": "Open pad by name", "index.openPad": "open an existing Pad with the name:", diff --git a/src/tests/backend-new/specs/template-l10n-keys.test.ts b/src/tests/backend-new/specs/template-l10n-keys.test.ts new file mode 100644 index 000000000..b41c105a3 --- /dev/null +++ b/src/tests/backend-new/specs/template-l10n-keys.test.ts @@ -0,0 +1,40 @@ +'use strict'; + +// Regression: src/templates/index.html referenced `data-l10n-id="index.code"` +// but src/locales/en.json had no `index.code` key, producing a "Couldn't find +// translation key index.code" console error on the landing page (issue #7835). +// This test asserts every data-l10n-id attribute in our shipped templates has +// a matching source string in en.json so the class of bug fails in CI. + +import {readFileSync, readdirSync} from 'fs'; +import {join} from 'path'; +import {describe, it, expect} from 'vitest'; + +const repoRoot = join(__dirname, '..', '..', '..', '..'); +const templatesDir = join(repoRoot, 'src', 'templates'); +const enJsonPath = join(repoRoot, 'src', 'locales', 'en.json'); + +const en = JSON.parse(readFileSync(enJsonPath, 'utf8')) as Record; + +const collectKeys = (html: string): string[] => { + const out: string[] = []; + const re = /data-l10n-id="([^"]+)"/g; + let m; + while ((m = re.exec(html)) !== null) out.push(m[1]); + return out; +}; + +const templateFiles = readdirSync(templatesDir) + .filter((f) => f.endsWith('.html')) + .map((f) => join(templatesDir, f)); + +describe('template l10n keys', () => { + for (const file of templateFiles) { + it(`every data-l10n-id in ${file.replace(repoRoot + '/', '')} exists in en.json`, () => { + const html = readFileSync(file, 'utf8'); + const keys = collectKeys(html); + const missing = keys.filter((k) => !(k in en)); + expect(missing, `missing keys in en.json: ${missing.join(', ')}`).toEqual([]); + }); + } +});