Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions admin/public/ep_admin_authors/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
13 changes: 9 additions & 4 deletions admin/src/pages/AuthorPage.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -153,16 +154,20 @@ export const AuthorPage = () => {
<Dialog.Portal>
<Dialog.Overlay className="dialog-confirm-overlay"/>
<Dialog.Content className="dialog-confirm-content">
<VisuallyHidden asChild>
<Dialog.Title>{t('ep_admin_authors:confirm-dialog-title')}</Dialog.Title>
</VisuallyHidden>
<VisuallyHidden asChild>
<Dialog.Description>{t('ep_admin_authors:confirm-dialog-description')}</Dialog.Description>
</VisuallyHidden>
{dialog.phase === 'loading-preview' && <div>
<Trans i18nKey="ep_admin_authors:loading-preview" ns="ep_admin_authors"/>
</div>}
{(dialog.phase === 'preview' || dialog.phase === 'committing') && (() => {
const p = dialog.preview;
return <div>
<Dialog.Title asChild>
<h3>{t('ep_admin_authors:confirm-preview-title',
{name: p.name || p.authorID})}</h3>
</Dialog.Title>
<h3>{t('ep_admin_authors:confirm-preview-title',
{name: p.name || p.authorID})}</h3>
<p>{t('ep_admin_authors:confirm-preview-counters', {
tokenMappings: p.removedTokenMappings,
externalMappings: p.removedExternalMappings,
Expand Down
12 changes: 10 additions & 2 deletions admin/src/pages/PadPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -165,7 +166,10 @@ export const PadPage = () => {
<Dialog.Portal>
<Dialog.Overlay className="dialog-confirm-overlay"/>
<Dialog.Content className="dialog-confirm-content">
<div>{t('ep_admin_pads:ep_adminpads2_confirm', {padID: padToDelete})}</div>
<VisuallyHidden asChild><Dialog.Title>{t('admin_pads.delete_pad_dialog_title')}</Dialog.Title></VisuallyHidden>
<Dialog.Description asChild>
<div>{t('ep_admin_pads:ep_adminpads2_confirm', {padID: padToDelete})}</div>
</Dialog.Description>
<div className="settings-button-bar">
<button onClick={() => setDeleteDialog(false)}><Trans i18nKey="admin_pads.cancel"/></button>
<button onClick={() => { deletePad(padToDelete); setDeleteDialog(false) }}>{t('admin_pads.confirm_button')}</button>
Expand All @@ -178,7 +182,10 @@ export const PadPage = () => {
<Dialog.Portal>
<Dialog.Overlay className="dialog-confirm-overlay"/>
<Dialog.Content className="dialog-confirm-content">
<div>{t('admin_pads.error_prefix')}: {errorText}</div>
<VisuallyHidden asChild><Dialog.Title>{t('admin_pads.error_prefix')}</Dialog.Title></VisuallyHidden>
<Dialog.Description asChild>
<div>{t('admin_pads.error_prefix')}: {errorText}</div>
</Dialog.Description>
<div className="settings-button-bar">
<button onClick={() => setErrorText(null)}>{t('admin_pads.confirm_button')}</button>
</div>
Expand All @@ -191,6 +198,7 @@ export const PadPage = () => {
<Dialog.Overlay className="dialog-confirm-overlay"/>
<Dialog.Content className="dialog-confirm-content">
<Dialog.Title className="dialog-confirm-title"><Trans i18nKey="index.newPad"/></Dialog.Title>
<VisuallyHidden asChild><Dialog.Description>{t('admin_pads.create_pad_dialog_description')}</Dialog.Description></VisuallyHidden>
<form onSubmit={handleSubmit(onPadCreate)}>
<button className="dialog-close-button" type="button" onClick={() => setCreatePadDialogOpen(false)}>×</button>
<div style={{display: 'grid', gap: '10px', gridTemplateColumns: 'auto auto', marginBottom: '1rem'}}>
Expand Down
5 changes: 5 additions & 0 deletions admin/src/utils/LoadingScreen.tsx
Original file line number Diff line number Diff line change
@@ -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 <Dialog.Root open={showLoading}><Dialog.Portal>
<Dialog.Overlay className="loading-screen fixed inset-0 bg-black bg-opacity-50 z-50 dialog-overlay" />
<Dialog.Content className="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-50 dialog-content">
<VisuallyHidden asChild><Dialog.Title>{t('admin.loading')}</Dialog.Title></VisuallyHidden>
<VisuallyHidden asChild><Dialog.Description>{t('admin.loading_description')}</Dialog.Description></VisuallyHidden>
<div className="flex flex-col items-center">
<div className="animate-spin w-16 h-16 border-t-2 border-b-2 border-[--fg-color] rounded-full"></div>
<div className="mt-4 text-[--fg-color]">
Expand Down
19 changes: 11 additions & 8 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions src/locales/en.json
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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:",
Expand Down
40 changes: 40 additions & 0 deletions src/tests/backend-new/specs/template-l10n-keys.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>;

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([]);
});
}
});
Loading