etherpad-lite/AGENTS.MD
John McLear ff7c4d5310
fix(admin): replace hardcoded German strings with i18n keys (#7735) (#7736)
* fix(admin): replace ~50 hardcoded German strings with i18n keys (#7735)

PR #7716 ("chore: fixed admin design rework") rebuilt admin/src/pages
with literal German copy inline — "Update verfügbar", "Aktualisieren",
"Keine Pads gefunden", "Hook-Bindings", "de-DE" date formatters, etc.
Non-DE users see a French/English/German salad: <Trans i18nKey="…"/>
calls resolve correctly via translatewiki, but every literal stays
German regardless of browser locale.

This change:

  - Adds 90+ keys to src/locales/en.json under admin.*, admin_login.*,
    admin_pads.*, admin_plugins.*, admin_plugins_info.*, admin_settings.*,
    admin_shout.*, and the previously-orphaned update.page.{disabled,
    unauthorized,error}.
  - Replaces every hardcoded literal in admin/src/{App,LoginScreen,
    HomePage,HelpPage,PadPage,SettingsPage,ShoutPage,UpdatePage}.tsx with
    t() or <Trans>.
  - Threads i18n.language into PadPage so relativeTime() and
    toLocale*() honour the user's locale instead of forcing de-DE.

Test coverage:

  - src/tests/backend-new/specs/admin-i18n-source-lint.test.ts (vitest):
    scans admin/src/pages/*.tsx + App.tsx for a denylist of German
    literals introduced by #7716, asserts PadPage no longer hardcodes
    'de-DE', and pins the set of new en.json keys.
  - src/tests/frontend-new/admin-spec/admini18n.spec.ts (Playwright):
    extended to assert rendered English text on every page (Home, Pads,
    Help, Login) and verify no German leakage on the English path.

Non-EN locales pick up translations from translatewiki on its normal
cadence; until then i18next falls back to en.json.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(admin): reuse existing ep_admin_pads:* keys instead of duplicating

Pre-rework admin already had:
  ep_admin_pads:ep_adminpads2_action       ("Action")
  ep_admin_pads:ep_adminpads2_last-edited  ("Last edited")
  ep_admin_pads:ep_adminpads2_no-results   ("No results")

Initial pass added admin_pads.{col.action, col.last_edited,
sort.last_edited, empty_state} duplicating those — drop the duplicates
from en.json and point PadPage.tsx at the existing translatewiki-fed
keys. Stats/column heads that genuinely didn't exist before
(admin_pads.col.{pad,users,revisions}, the filter chips, relative-time,
pagination, etc.) stay as new keys.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(admin): address Qodo finding + restore #7716 regressions

Qodo flagged a reliability bug in PadPage on PR #7736: i18n.language
flows from user-controlled ?lng= straight into Intl.* formatters, which
throw RangeError on malformed tags (e.g. 'en_US', '💥'). Crashing the
pads page on a crafted URL.

Wrap the locale in a sanitizeLocale() helper that normalises '_' → '-'
and validates via Intl.DateTimeFormat.supportedLocalesOf(), falling back
to 'en' so dates render in a sane locale rather than the user's browser
default fighting page copy.

Same audit surfaced four additional regressions from #7716 still on
develop, fixed here on-theme:

  - HomePage dropped <a href="https://npmjs.com/..."> wrappers on both
    installed and available plugin rows. Restored with .pm-plugin-link.
  - "Downloads" column / "Most popular" default sort / "Popular" tag
    were dead UI — src/static/js/pluginfw/installer.ts::search() never
    populates `downloads`. Removed the column, default sort, and tag;
    dropped `downloads` from PluginDef + SearchParams.sortBy.
  - PadPage sort dropdown hardcoded `ascending: e.target.value ===
    'padName'`, leaving no way to invert direction. Replaced with a
    paired ↑/↓ button (.pm-sort-dir) for both HomePage and PadPage.
  - "1 Core" stat hint hardcoded count=1. Derived from
    installedPlugins.filter(p => p.name === 'ep_etherpad-lite').length.
  - Deleted orphan modules SearchField.tsx and sorting.ts (no longer
    imported anywhere after #7716).

Tests:

  - admin-i18n-source-lint.test.ts: +3 assertions (sanitizeLocale
    pattern, dead-downloads check, orphan-module deletion, sort-dir
    toggle) → 14 passing.
  - admini18n.spec.ts: +2 assertions (npmjs link on ep_etherpad-lite
    row, sort-direction toggle visible).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(agents): add mandatory i18n + a11y guardrails

PR #7716 ("admin design rework") shipped ~50 hardcoded German literals,
dropped npmjs.com link affordances, removed the sort-direction control
on PadPage, and forced `de-DE` into Intl formatters — none of which the
AGENTS.MD guide explicitly forbade. Document the rules so the next UI
refresh cannot regress these in the same way:

- i18n section spells out which slots must be localised (JSX text,
  placeholders, titles, aria-labels, alts, toasts, options, alerts),
  which API to use per surface (<Trans>/t() in React, data-l10n-id in
  the legacy pad UI, never window._ rebound), where keys live
  (src/locales/en.json — never hand-edit non-EN locales), to reuse
  existing keys before duplicating, pluralisation via _one/_other,
  defaultValue is safety not a substitute, and points at the
  source-lint test that enforces the denylist.

- a11y section spells out the lessons surfaced by the audit: icon-only
  buttons need aria-label AND title (both localised), sort controls
  must be focusable + reversible, semantic HTML over div soup, external
  navigation is <a>, "don't drop affordances when restyling" is a
  hard rule, Playwright specs must assert rendered strings + at least
  one structural affordance for UI changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 10:09:44 +01:00

12 KiB

Agent Guide - Etherpad

Welcome to the Etherpad project. This guide provides essential context and instructions for AI agents and developers to effectively contribute to the codebase.

Project Overview

Etherpad is a real-time collaborative editor designed to be lightweight, scalable, and highly extensible via plugins.

Technical Stack

  • Runtime: Node.js >= 22.12.0
  • Package Manager: pnpm (>= 11.0.0)
  • Languages: TypeScript (primary for new code), JavaScript (legacy), CSS, HTML
  • Backend: Express.js 5, Socket.io 4
  • Frontend: Legacy core (src/static), Modern React UI (ui/), Admin UI (admin/)
  • Database: ueberdb2 abstraction (supports dirtyDB, MySQL, PostgreSQL, Redis)
  • Build Tools: Vite (for ui and admin), esbuild, tsx
  • Testing: Mocha (backend), Playwright (frontend E2E), Vitest (unit)
  • Auth: JWT (jose library), OIDC provider

Directory Structure

  • src/node/ - Backend logic, API handlers, database models, hooks
  • src/static/ - Core frontend logic (legacy jQuery-based editor)
  • src/static/js/pluginfw/ - Plugin framework (installer, hook system)
  • src/tests/ - Test suites (backend, frontend, container)
  • ui/ - Modern React OIDC login UI (Vite + TypeScript)
  • admin/ - Modern React admin panel (Vite + TypeScript + Radix UI)
  • bin/ - CLI utilities, build scripts, plugin management tools
  • bin/plugins/ - Plugin maintenance scripts (checkPlugin.ts, updateCorePlugins.sh)
  • doc/ - Documentation (VitePress + Markdown/AsciiDoc)
  • local_plugins/ - Directory for developing and testing plugins locally
  • var/ - Runtime data (logs, dirtyDB, etc. - ignored by git)

Quick Start

pnpm install                                    # Install all dependencies
pnpm run build:etherpad                         # Build admin UI and static assets
pnpm --filter ep_etherpad-lite run dev          # Start dev server (port 9001)
pnpm --filter ep_etherpad-lite run prod         # Start production server

Core Mandates & Conventions

Coding Style

  • Indentation: 2 spaces for all files (JS/TS/CSS/HTML). No tabs.
  • TypeScript: All new code should be TypeScript. Strict mode is enabled.
  • Comments: Provide clear comments for complex logic only.
  • Backward Compatibility: Always ensure compatibility with older versions of the database and configuration files.

Internationalisation (i18n) — Mandatory

  • Every user-facing string MUST go through i18n. That means JSX text, placeholder, title, aria-label, alt, toast/alert titles, <option> labels, error messages, breadcrumbs, empty-state copy — anything a user can see or hear via a screen reader.
  • React (admin SPA): use <Trans i18nKey="…"/> for JSX text and t('…') for attribute values. The t comes from useTranslation().
  • Pad UI (legacy): use data-l10n-id="…" (html10n) — never bind window._ directly to html10n.get and call it (it's unbound; returns undefined).
  • String keys live in src/locales/en.json. Other locales sync from translatewiki on its own cadence — never hand-edit non-EN locale files.
  • Reuse existing keys before inventing new ones. Check src/locales/en.json and the relevant plugin's static/locale/en.json (e.g. admin/public/ep_admin_pads/en.json) for an existing match. Duplicating a key like ep_admin_pads:ep_adminpads2_last-edited as a fresh admin_pads.col.last_edited fragments the translation surface for translatewiki.
  • Naming: dot-namespaced, kebab-or-underscore-cased: admin_plugins.subtitle, admin_pads.filter.all. Group by page/feature.
  • Pluralisation: use i18next's _one/_other suffix forms with t('key', {count: n}) — never n > 1 ? 'X items' : '1 item'.
  • Locale-aware formatters: pass a sanitised locale to Intl.* / toLocaleString. i18n.language is influenced by ?lng= (user-controlled) and a malformed tag throws RangeError. Use a sanitizeLocale() helper that normalises _- and validates via Intl.DateTimeFormat.supportedLocalesOf(), falling back to 'en'.
  • defaultValue: in t() is for safety, not a substitute for adding the key to en.json. If you're tempted to inline English with defaultValue: and skip the key, you're shipping a future-broken translation.
  • Hard prohibition: literal German / French / Spanish / any non-English in JSX. The denylist test at src/tests/backend-new/specs/admin-i18n-source-lint.test.ts enforces this for the admin SPA — extend it when adding new admin files or new known-bad words.

Accessibility (a11y) — Mandatory

  • Icon-only buttons MUST have aria-label AND title (both — screen readers prefer aria-label; hover users get title). Lucide icons inside a button are not text content. Both labels must be t('…')-localised.
  • Sort controls are focusable. A <select> plus a paired direction toggle is fine; a clickable column header is fine; "click invisible part of the row to sort" is not. When restyling, never strip a direction toggle without adding back an equivalent.
  • Semantic HTML over <div> soup. Use <nav>, <main>, <button>, <a> (for navigation), <table> for tabular data. If a thing navigates externally, it is <a target="_blank" rel="noopener noreferrer">, not a click-handler on a <span>.
  • Don't drop existing affordances when restyling. A UI refresh that removes <a href="https://npmjs.com/{plugin}"> links, removes a sort-direction control, or replaces semantic elements with non-focusable divs is a regression even if it looks nicer. Audit the before/after for: focus order, keyboard reachability, external links, aria-labels, alt text.
  • Tests assert rendered strings + structural affordances. For UI changes, the Playwright spec must assert at least one rendered translated string (catches broken i18n loading) and one structural affordance you added/preserved (links, toggles, headings).

Development Workflow

  • Branching: Work in feature branches. Issue PRs against the develop branch. Never PR directly to master.
  • Commits: Maintain a linear history (no merge commits). Use meaningful messages in the format: submodule: description.
  • Feature Flags: New features should be placed behind feature flags and disabled by default.
  • Deprecation: Never remove features abruptly; deprecate them first with a WARN log.
  • Forks: For etherpad-lite changes, commit to johnmclear/etherpad-lite fork on a new branch, then PR to ether/etherpad. For plugins (ep_* repos), committing directly is acceptable.

Testing & Validation

  • Requirement: Every bug fix MUST include a regression test in the same commit.
  • Always run tests locally before pushing to CI.
  • Linting: pnpm run lint
  • Type Check: pnpm --filter ep_etherpad-lite run ts-check
  • Build: pnpm run build:etherpad before production deployment

Running Backend Tests Locally

Backend tests use Mocha with tsx and run against a real server instance (started automatically by the test harness). No separate server process is needed.

# Run ALL backend tests (includes plugin tests)
pnpm --filter ep_etherpad-lite run test

# Run only utility tests (faster, ~5s timeout)
pnpm --filter ep_etherpad-lite run test-utils

# Run a single test file directly
cd src && cross-env NODE_ENV=production npx mocha --import=tsx --timeout 120000 tests/backend/specs/YOUR_TEST.ts

# Run unit tests (Vitest)
cd src && npx vitest
  • Tests run with NODE_ENV=production.
  • Default timeout is 120 seconds per test.
  • Test files live in src/tests/backend/specs/.
  • Plugin backend tests live in node_modules/ep_*/static/tests/backend/specs/ (at repo root) and are included automatically by the test script.

Running Frontend E2E Tests Locally

Frontend tests use Playwright. You must have a running Etherpad server before launching them — the Playwright config does not auto-start the server.

Before running frontend or admin tests, ensure Playwright browsers are installed. Check and install if needed:

# Check which browsers are installed
cd src && npx playwright install --dry-run

# Install all browsers and their system dependencies (must run from src/)
cd src && npx playwright install
cd src && sudo npx playwright install-deps

If sudo is unavailable, install system dependencies for webkit manually:

# Check which system libraries are missing for webkit
ldd ~/.cache/ms-playwright/webkit-*/minibrowser-wpe/MiniBrowser 2>&1 | grep "not found"

If browsers or system dependencies are missing, tests will fail silently or timeout — always verify browser installation before debugging test failures.

# 1. Start the dev server in a separate terminal
pnpm --filter ep_etherpad-lite run dev

# 2. Run frontend E2E tests
pnpm --filter ep_etherpad-lite run test-ui

# 3. Run with interactive Playwright UI (useful for debugging)
pnpm --filter ep_etherpad-lite run test-ui:ui

# Run a single test file
cd src && cross-env NODE_ENV=production npx playwright test tests/frontend-new/specs/YOUR_TEST.spec.ts
  • Tests expect the server at localhost:9001.
  • Test files live in src/tests/frontend-new/specs/.
  • Runs against chromium and firefox by default (webkit is disabled).
  • Playwright config is at src/playwright.config.ts.

Running Admin Panel Tests Locally

# Requires a running server and Playwright browsers installed (same as frontend tests)
pnpm --filter ep_etherpad-lite run test-admin

# Interactive UI mode
pnpm --filter ep_etherpad-lite run test-admin:ui
  • Admin tests run with --workers 1 (sequential) on chromium and firefox only.
  • Test files live in src/tests/frontend-new/admin-spec/.

Backend Test Auth

Tests use JWT authentication, not API keys. Pattern:

import * as common from 'ep_etherpad-lite/tests/backend/common';

const agent = await common.init();  // Starts server, returns supertest agent
const token = await common.generateJWTToken();
agent.get('/api/1/endpoint').set('authorization', token);

Do not use APIKEY.txt — it may not exist in the test environment.

Key Concepts

Easysync

The real-time synchronization engine. It is complex; refer to doc/public/easysync/ before modifying core synchronization logic.

Plugin Framework

Most functionality should be implemented as plugins (ep_*). Avoid modifying the core unless absolutely necessary.

Plugin structure:

ep_myplugin/
├── ep.json              # Hook declarations (server_hooks, client_hooks)
├── index.js             # Server-side hook implementations
├── package.json
├── static/
│   ├── js/              # Client-side code
│   ├── css/
│   └── tests/
│       ├── backend/specs/    # Backend tests (Mocha)
│       └── frontend-new/     # Frontend tests (Playwright)
├── templates/           # EJS templates
└── locales/             # i18n files

Plugin management:

pnpm run plugins i ep_plugin_name       # Install from npm
pnpm run plugins i --path ../plugin     # Install from local path
pnpm run plugins rm ep_plugin_name      # Remove
pnpm run plugins ls                     # List installed

Plugin installation internals: Plugins are installed to src/plugin_packages/ via live-plugin-manager, which stores them at src/plugin_packages/.versions/ep_name@version/. Symlinks are created: src/node_modules/ep_namesrc/plugin_packages/ep_name.versions/ep_name@ver/.

Plugin Repositories

  • Monorepo: ether/ether-plugins contains 80+ plugins with shared CI/publishing
  • Standalone repos: Individual ether/ep_* repos still exist for many plugins
  • Plugin CI templates: bin/plugins/lib/ contains workflow templates pushed to standalone plugin repos via checkPlugin.ts
  • Shared pipelines: ether/ether-pipelines contains reusable GitHub Actions workflows for plugin CI

Settings

Configured via settings.json. A template is available at settings.json.template. Environment variables can override any setting using "${ENV_VAR}" or "${ENV_VAR:default_value}".

Monorepo Structure

This project uses pnpm workspaces. The workspaces are:

  • src/ - Core Etherpad (package: ep_etherpad-lite)
  • bin/ - CLI tools and plugin scripts
  • ui/ - Login UI
  • admin/ - Admin panel
  • doc/ - Documentation

Root-level commands operate across all workspaces. Use pnpm --filter <package> to target specific workspaces.

AI-Specific Guidance

AI/Agent contributions are explicitly welcomed by the maintainers, provided they strictly adhere to the guidelines in CONTRIBUTING.md and this guide. Always prioritize stability, readability, and compatibility.