From 689dd9d434d0f040269d1156266ed4c7c2aa1b07 Mon Sep 17 00:00:00 2001 From: SamTV12345 <40429738+samtv12345@users.noreply.github.com> Date: Fri, 22 May 2026 22:43:25 +0200 Subject: [PATCH 01/69] chore: fixed backend tests --- .../specs/hooks/express/updateStatus.test.ts | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/src/tests/backend-new/specs/hooks/express/updateStatus.test.ts b/src/tests/backend-new/specs/hooks/express/updateStatus.test.ts index cfe95099d..05b6f423e 100644 --- a/src/tests/backend-new/specs/hooks/express/updateStatus.test.ts +++ b/src/tests/backend-new/specs/hooks/express/updateStatus.test.ts @@ -19,6 +19,8 @@ import supertest from 'supertest'; import type {Express} from 'express'; import type {UpdateState} from '../../../../../node/updater/types'; import {EMPTY_STATE} from '../../../../../node/updater/types'; +import {getEpVersion} from '../../../../../node/utils/Settings'; +import {parseSemver} from '../../../../../node/updater/versionCompare'; // --------------------------------------------------------------------------- // Module mocks — must appear before any import that transitively imports them. @@ -83,34 +85,41 @@ const makeState = (latest: UpdateState['latest']): UpdateState => ({ latest, }); -/** A fake ReleaseInfo for a version that is a minor-version ahead of 3.1.0. */ +// Build fixtures relative to the actual current version so the test stays +// stable across release bumps. Hard-coded versions break every time the +// codebase rolls forward past the fixture (e.g. once 3.2.0 shipped, a +// MINOR_AHEAD pinned at "3.2.0" no longer counted as minor-behind). +const CUR = parseSemver(getEpVersion())!; +const v = (major: number, minor: number, patch: number) => `${major}.${minor}.${patch}`; + +/** A fake ReleaseInfo for a version that is one minor ahead of current. */ const MINOR_AHEAD: UpdateState['latest'] = { - version: '3.2.0', - tag: 'v3.2.0', + version: v(CUR.major, CUR.minor + 1, 0), + tag: `v${v(CUR.major, CUR.minor + 1, 0)}`, body: '', publishedAt: '2026-05-01T00:00:00Z', prerelease: false, - htmlUrl: 'https://github.com/ether/etherpad/releases/tag/v3.2.0', + htmlUrl: `https://github.com/ether/etherpad/releases/tag/v${v(CUR.major, CUR.minor + 1, 0)}`, }; -/** A fake ReleaseInfo that is only a patch ahead of 3.1.0 (no minor/major delta). */ +/** A fake ReleaseInfo that is only a patch ahead of current (no minor/major delta). */ const PATCH_AHEAD: UpdateState['latest'] = { - version: '3.1.1', - tag: 'v3.1.1', + version: v(CUR.major, CUR.minor, CUR.patch + 1), + tag: `v${v(CUR.major, CUR.minor, CUR.patch + 1)}`, body: '', publishedAt: '2026-05-01T00:00:00Z', prerelease: false, - htmlUrl: 'https://github.com/ether/etherpad/releases/tag/v3.1.1', + htmlUrl: `https://github.com/ether/etherpad/releases/tag/v${v(CUR.major, CUR.minor, CUR.patch + 1)}`, }; -/** A fake ReleaseInfo that is behind or equal to 3.1.0 (current >= latest). */ +/** A fake ReleaseInfo that is behind current (current >= latest). */ const SAME_OR_BEHIND: UpdateState['latest'] = { - version: '3.0.0', - tag: 'v3.0.0', + version: v(Math.max(0, CUR.major - 1), 0, 0), + tag: `v${v(Math.max(0, CUR.major - 1), 0, 0)}`, body: '', publishedAt: '2026-01-01T00:00:00Z', prerelease: false, - htmlUrl: 'https://github.com/ether/etherpad/releases/tag/v3.0.0', + htmlUrl: `https://github.com/ether/etherpad/releases/tag/v${v(Math.max(0, CUR.major - 1), 0, 0)}`, }; // --------------------------------------------------------------------------- From 4ff3363b9bd0b8a720cab4b55570fddce5027d36 Mon Sep 17 00:00:00 2001 From: John McLear Date: Mon, 25 May 2026 09:39:25 +0100 Subject: [PATCH 02/69] docs(docker): document settings.json writable-layer + env-var-vs-file semantics (#7819) (#7827) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related operator-facing docs gaps, both surfaced by #7819: 1. settings.json on disk is a *template*; env-var substitution happens at load time in memory only. Operators repeatedly mistake the templated file for a stale config because the docs never spell out that the on-disk file is intentionally unchanged by env vars. 2. The default docker-compose.yml puts settings.json in the container's writable layer with no host mount, which means admin /settings edits are silently lost on `docker compose down && up`, `pull`, or watchtower — but preserved across plain `restart`. Operators don't reliably know which compose verbs recreate the container. Adds two prose sections to doc/docker.md (explaining both gotchas, with a recreate-vs-restart table) and a commented-out `./settings.json:…` bind mount in both docker-compose.yml and the README compose example. Bind mount is opt-in so existing setups behave identically. No runtime change. Co-authored-by: Claude Opus 4.7 (1M context) --- doc/docker.md | 38 ++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 8 ++++++++ 2 files changed, 46 insertions(+) diff --git a/doc/docker.md b/doc/docker.md index 8be4dbdaa..815a836aa 100644 --- a/doc/docker.md +++ b/doc/docker.md @@ -29,6 +29,37 @@ Edit `/settings.json.docker` at your will. When rebuilding the image, t **Each configuration parameter can also be set via an environment variable**, using the syntax `"${ENV_VAR}"` or `"${ENV_VAR:default_value}"`. For details, refer to `settings.json.template`. +### How `settings.json` and environment variables interact + +This trips people up often enough that it's worth calling out explicitly (see [#7819](https://github.com/ether/etherpad/issues/7819)): + +* `settings.json` inside the container is a **template** containing `${VAR:default}` placeholders. +* Environment variable substitution happens at **load time, in memory only** — env vars never overwrite `settings.json` on disk. +* `docker exec cat /opt/etherpad-lite/settings.json` will therefore always show the *templated* file (e.g. `"port": "${PORT:9001}"`), regardless of what `PORT` is set to in your environment. The resolved value is what Etherpad uses at runtime; the file is unchanged. +* The admin /settings page also reads this file directly, so the raw view shows placeholders too. The page now surfaces a banner and an "Effective" tab that displays the in-memory resolved values when placeholders are present. + +### Persisting admin /settings edits across container recreates + +`settings.json` lives in the container's writable layer by default. That means: + +| Operation | Effect on `settings.json` | +|------------------------------------------|------------------------------------------| +| `docker restart` | Preserved (writable layer is reused) | +| `docker compose restart` | Preserved | +| `docker compose down && docker compose up` | **Reset** to the image template | +| `docker compose pull && docker compose up` | **Reset** to the new image template | +| Watchtower / image auto-update | **Reset** to the new image template | +| `docker rm` + `docker run` | **Reset** to the image template | + +If you intend to edit `settings.json` through the admin UI (rather than relying solely on env vars), mount the file from the host so edits survive container recreate: + +```yaml +volumes: + - ./settings.json:/opt/etherpad-lite/settings.json +``` + +(Bootstrap by copying `settings.json.docker` to `./settings.json` on the host before the first `up`.) The default compose example below ships this line commented out — uncomment it if you need persistent on-disk edits. + ### Rebuilding including some plugins If you want to install some plugins in your container, it is sufficient to list them in the ETHERPAD_PLUGINS build variable. The variable value has to be a space separated, double quoted list of plugin names (see examples). @@ -282,6 +313,13 @@ services: volumes: - plugins:/opt/etherpad-lite/src/plugin_packages - etherpad-var:/opt/etherpad-lite/var + # OPTIONAL: persist admin /settings edits across container recreates. + # Without this mount, settings.json lives in the image's writable + # layer — `docker compose restart` preserves it, but `docker compose + # down && up`, `pull`, or watchtower reverts it to the image + # template. Uncomment if you intend to edit settings.json through + # the /admin UI. See https://github.com/ether/etherpad/issues/7819. + # - ./settings.json:/opt/etherpad-lite/settings.json depends_on: - postgres environment: diff --git a/docker-compose.yml b/docker-compose.yml index e009c99f8..90d2f86b5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,6 +7,14 @@ services: volumes: - plugins:/opt/etherpad-lite/src/plugin_packages - etherpad-var:/opt/etherpad-lite/var + # OPTIONAL: persist admin /settings edits across container recreates. + # Without this mount, settings.json lives in the image's writable + # layer — `docker compose restart` preserves it, but `docker compose + # down && up`, `pull`, or watchtower reverts it to the image + # template. Uncomment if you intend to edit settings.json through + # the /admin UI instead of (or in addition to) env vars. See + # https://github.com/ether/etherpad/issues/7819. + # - ./settings.json:/opt/etherpad-lite/settings.json depends_on: - postgres environment: From 53aa7a1c5a5ffa90a87027baf1516b5fa515cb88 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 11:04:31 +0200 Subject: [PATCH 03/69] build(deps): bump mssql from 12.5.3 to 12.5.4 (#7810) Bumps [mssql](https://github.com/tediousjs/node-mssql) from 12.5.3 to 12.5.4. - [Release notes](https://github.com/tediousjs/node-mssql/releases) - [Changelog](https://github.com/tediousjs/node-mssql/blob/master/CHANGELOG.txt) - [Commits](https://github.com/tediousjs/node-mssql/compare/v12.5.3...v12.5.4) --- updated-dependencies: - dependency-name: mssql dependency-version: 12.5.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 60 ++++++++++++++++++++++++------------------------ src/package.json | 2 +- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 75b3d624c..72252e8d6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -157,7 +157,7 @@ importers: version: 4.22.3 ueberdb2: specifier: ^6.1.2 - version: 6.1.2(@elastic/elasticsearch@9.4.1)(async@3.2.6)(cassandra-driver@4.8.0)(dirty-ts@1.1.8)(mongodb@7.2.0)(mssql@12.5.3(@azure/core-client@1.10.1))(mysql2@3.22.3(@types/node@25.9.1))(nano@11.0.5)(pg@8.21.0)(redis@5.12.1(@opentelemetry/api@1.9.1))(rethinkdb@2.4.2)(rusty-store-kv@1.3.1)(surrealdb@2.0.3(tslib@2.8.1)(typescript@6.0.3)) + version: 6.1.2(@elastic/elasticsearch@9.4.1)(async@3.2.6)(cassandra-driver@4.8.0)(dirty-ts@1.1.8)(mongodb@7.2.0)(mssql@12.5.4(@azure/core-client@1.10.1))(mysql2@3.22.3(@types/node@25.9.1))(nano@11.0.5)(pg@8.21.0)(redis@5.12.1(@opentelemetry/api@1.9.1))(rethinkdb@2.4.2)(rusty-store-kv@1.3.1)(surrealdb@2.0.3(tslib@2.8.1)(typescript@6.0.3)) devDependencies: '@types/node': specifier: ^25.9.1 @@ -281,8 +281,8 @@ importers: specifier: ^7.1.1 version: 7.2.0 mssql: - specifier: ^12.5.3 - version: 12.5.3(@azure/core-client@1.10.1) + specifier: ^12.5.4 + version: 12.5.4(@azure/core-client@1.10.1) mysql2: specifier: ^3.22.3 version: 3.22.3(@types/node@25.9.1) @@ -357,7 +357,7 @@ importers: version: 4.22.3 ueberdb2: specifier: ^6.1.2 - version: 6.1.2(@elastic/elasticsearch@9.4.1)(async@3.2.6)(cassandra-driver@4.8.0)(dirty-ts@1.1.8)(mongodb@7.2.0)(mssql@12.5.3(@azure/core-client@1.10.1))(mysql2@3.22.3(@types/node@25.9.1))(nano@11.0.5)(pg@8.21.0)(redis@5.12.1(@opentelemetry/api@1.9.1))(rethinkdb@2.4.2)(rusty-store-kv@1.3.1)(surrealdb@2.0.3(tslib@2.8.1)(typescript@6.0.3)) + version: 6.1.2(@elastic/elasticsearch@9.4.1)(async@3.2.6)(cassandra-driver@4.8.0)(dirty-ts@1.1.8)(mongodb@7.2.0)(mssql@12.5.4(@azure/core-client@1.10.1))(mysql2@3.22.3(@types/node@25.9.1))(nano@11.0.5)(pg@8.21.0)(redis@5.12.1(@opentelemetry/api@1.9.1))(rethinkdb@2.4.2)(rusty-store-kv@1.3.1)(surrealdb@2.0.3(tslib@2.8.1)(typescript@6.0.3)) underscore: specifier: 1.13.8 version: 1.13.8 @@ -589,16 +589,16 @@ packages: resolution: {integrity: sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==} engines: {node: '>=20.0.0'} - '@azure/msal-browser@5.10.1': - resolution: {integrity: sha512-hTbvOi9Ko2Jvn+G/fSmjzHf9WbNcf/o3epMtbeGx/pMwMrVAbi6OgCJVeCfsAb8IybSRpaCSc4EDRlYAhgngUQ==} + '@azure/msal-browser@5.11.0': + resolution: {integrity: sha512-zkGNYS3TwY8lUpPIafAmsFCYZbgFixY9y/LZB9GUg0IILoHTqpN26j5OrkL1AQThh/YdZsawe4iWXfp85lFVxg==} engines: {node: '>=0.8.0'} - '@azure/msal-common@16.6.1': - resolution: {integrity: sha512-VxKdEtUwDuLD0F1hOQP7kye0YadZxFJfv37Em440geEf/w9uggKnHpRrqwZJOdxmPUOdhZ9kyRtKuAJW8wUcRg==} + '@azure/msal-common@16.6.2': + resolution: {integrity: sha512-hQjjsekAjB00cM1EmatWJlzhEoK2Qhz7Rj5gvM6tYf8iL7RM3tkxlpU9fG0+ofkulzg9AEEA6dIEnSmDr5ZqUA==} engines: {node: '>=0.8.0'} - '@azure/msal-node@5.2.1': - resolution: {integrity: sha512-tmQiQ2HvtzaeLqYGy3BemiPOSGPY4wCy1IW5zDWITKSs/s35WEd7Zij/hCxvUdAOzj6U3qnyaGbYXY91ortFEQ==} + '@azure/msal-node@5.2.2': + resolution: {integrity: sha512-toS+2AePxqyzb0YOKttDOOiSl3jrkK9aiqIvpurpis0O34QcIS5gToqrgT39p04Dpxw3YoUU0lxJKTpSFFfA6Q==} engines: {node: '>=20'} '@babel/code-frame@7.29.0': @@ -4238,8 +4238,8 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - mssql@12.5.3: - resolution: {integrity: sha512-+xKl4f/gVLUcX1VvN6MBIPbhwpR3CD7MERwybNQBglLbtky3V/avndByIcN9HSIoDI8wJbGiFpS4A437geoXjg==} + mssql@12.5.4: + resolution: {integrity: sha512-f8UzhpO1STCYhxBybEgT4kaPa2Pda+nucQcMMad7RqOmmTZu3tjkvpPeI9h0RdSK//eua4ybRsLcalz/ttagwQ==} engines: {node: '>=18.19.0'} hasBin: true @@ -5969,8 +5969,8 @@ snapshots: '@azure/core-tracing': 1.3.1 '@azure/core-util': 1.13.1 '@azure/logger': 1.3.0 - '@azure/msal-browser': 5.10.1 - '@azure/msal-node': 5.2.1 + '@azure/msal-browser': 5.11.0 + '@azure/msal-node': 5.2.2 open: 10.2.0 tslib: 2.8.1 transitivePeerDependencies: @@ -6014,15 +6014,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@azure/msal-browser@5.10.1': + '@azure/msal-browser@5.11.0': dependencies: - '@azure/msal-common': 16.6.1 + '@azure/msal-common': 16.6.2 - '@azure/msal-common@16.6.1': {} + '@azure/msal-common@16.6.2': {} - '@azure/msal-node@5.2.1': + '@azure/msal-node@5.2.2': dependencies: - '@azure/msal-common': 16.6.1 + '@azure/msal-common': 16.6.2 jsonwebtoken: 9.0.3 '@babel/code-frame@7.29.0': @@ -6940,7 +6940,7 @@ snapshots: '@types/accepts@1.3.7': dependencies: - '@types/node': 25.9.0 + '@types/node': 25.9.1 '@types/async@3.2.25': {} @@ -6956,7 +6956,7 @@ snapshots: '@types/connect@3.4.38': dependencies: - '@types/node': 25.9.0 + '@types/node': 25.9.1 '@types/content-disposition@0.5.9': {} @@ -6971,11 +6971,11 @@ snapshots: '@types/connect': 3.4.38 '@types/express': 5.0.6 '@types/keygrip': 1.0.6 - '@types/node': 25.9.0 + '@types/node': 25.9.1 '@types/cors@2.8.19': dependencies: - '@types/node': 25.9.0 + '@types/node': 25.9.1 '@types/cross-spawn@6.0.6': dependencies: @@ -7133,13 +7133,13 @@ snapshots: '@types/readable-stream@4.0.23': dependencies: - '@types/node': 25.9.0 + '@types/node': 25.9.1 '@types/semver@7.7.1': {} '@types/send@1.2.1': dependencies: - '@types/node': 25.9.0 + '@types/node': 25.9.1 '@types/serve-static@2.2.0': dependencies: @@ -9743,7 +9743,7 @@ snapshots: ms@2.1.3: {} - mssql@12.5.3(@azure/core-client@1.10.1): + mssql@12.5.4(@azure/core-client@1.10.1): dependencies: '@tediousjs/connection-string': 1.1.0 commander: 11.1.0 @@ -10167,7 +10167,7 @@ snapshots: proxy-agent@6.5.0: dependencies: agent-base: 7.1.3 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.1 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 lru-cache: 7.18.3 @@ -10879,7 +10879,7 @@ snapshots: '@azure/identity': 4.13.1 '@azure/keyvault-keys': 4.10.0(@azure/core-client@1.10.1) '@js-joda/core': 5.7.0 - '@types/node': 25.9.0 + '@types/node': 25.9.1 bl: 6.1.6 iconv-lite: 0.7.2 js-md4: 0.3.2 @@ -11017,14 +11017,14 @@ snapshots: typescript@6.0.3: {} - ueberdb2@6.1.2(@elastic/elasticsearch@9.4.1)(async@3.2.6)(cassandra-driver@4.8.0)(dirty-ts@1.1.8)(mongodb@7.2.0)(mssql@12.5.3(@azure/core-client@1.10.1))(mysql2@3.22.3(@types/node@25.9.1))(nano@11.0.5)(pg@8.21.0)(redis@5.12.1(@opentelemetry/api@1.9.1))(rethinkdb@2.4.2)(rusty-store-kv@1.3.1)(surrealdb@2.0.3(tslib@2.8.1)(typescript@6.0.3)): + ueberdb2@6.1.2(@elastic/elasticsearch@9.4.1)(async@3.2.6)(cassandra-driver@4.8.0)(dirty-ts@1.1.8)(mongodb@7.2.0)(mssql@12.5.4(@azure/core-client@1.10.1))(mysql2@3.22.3(@types/node@25.9.1))(nano@11.0.5)(pg@8.21.0)(redis@5.12.1(@opentelemetry/api@1.9.1))(rethinkdb@2.4.2)(rusty-store-kv@1.3.1)(surrealdb@2.0.3(tslib@2.8.1)(typescript@6.0.3)): optionalDependencies: '@elastic/elasticsearch': 9.4.1 async: 3.2.6 cassandra-driver: 4.8.0 dirty-ts: 1.1.8 mongodb: 7.2.0 - mssql: 12.5.3(@azure/core-client@1.10.1) + mssql: 12.5.4(@azure/core-client@1.10.1) mysql2: 3.22.3(@types/node@25.9.1) nano: 11.0.5 pg: 8.21.0 diff --git a/src/package.json b/src/package.json index 91042398d..e0ca98d49 100644 --- a/src/package.json +++ b/src/package.json @@ -62,7 +62,7 @@ "measured-core": "^2.0.0", "mime-types": "^3.0.2", "mongodb": "^7.1.1", - "mssql": "^12.5.3", + "mssql": "^12.5.4", "mysql2": "^3.22.3", "nano": "^11.0.5", "nodemailer": "^8.0.7", From d106ed9c5e3951cbc3cda7050c020ed750212a18 Mon Sep 17 00:00:00 2001 From: John McLear Date: Mon, 25 May 2026 10:43:10 +0100 Subject: [PATCH 04/69] fix(a11y): add Dialog titles/descriptions and missing index.code key (#7836) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- admin/package.json | 1 + admin/public/ep_admin_authors/en.json | 2 + admin/src/pages/AuthorPage.tsx | 13 ++++-- admin/src/pages/PadPage.tsx | 12 +++++- admin/src/utils/LoadingScreen.tsx | 5 +++ pnpm-lock.yaml | 19 +++++---- src/locales/en.json | 6 +++ .../specs/template-l10n-keys.test.ts | 40 +++++++++++++++++++ 8 files changed, 84 insertions(+), 14 deletions(-) create mode 100644 src/tests/backend-new/specs/template-l10n-keys.test.ts 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([]); + }); + } +}); From 40f0a4021a6aa94c56c682db257bcd7b012f39fc Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 25 May 2026 14:03:23 +0200 Subject: [PATCH 05/69] Localisation updates from https://translatewiki.net. --- src/locales/ar.json | 3 +- src/locales/it.json | 185 ++++++++++++++++++++++++++++++++++++++- src/locales/ko.json | 205 +++++++++++++++++++++++++++++++++++++++++++- src/locales/mk.json | 27 ++++++ 4 files changed, 416 insertions(+), 4 deletions(-) diff --git a/src/locales/ar.json b/src/locales/ar.json index 11eb6e1cd..882e1e618 100644 --- a/src/locales/ar.json +++ b/src/locales/ar.json @@ -7,6 +7,7 @@ "Haytham morsy", "Meno25", "Mido", + "Mohammed Qays", "Shbib Al-Subaie", "Tala Ali", "Test Create account", @@ -57,7 +58,7 @@ "index.transferSessionNow": "نقل الجلسة الآن", "index.copyLink": "2. نسخ الرابط", "index.copyLinkDescription": "انقر على الزر أدناه لنسخ الرابط إلى الحافظة الخاصة بك.", - "index.copyLinkButton": "نسخ الرابط إلى الحافظة", + "index.copyLinkButton": "نُسخت الوصلة إلى الحافظة", "index.transferToSystem": "3. نسخ الجلسة إلى النظام الجديد", "index.transferToSystemDescription": "افتح الرابط المنسوخ في المتصفح أو الجهاز المستهدف لنقل جلستك.", "index.transferSessionDescription": "انقل جلستك الحالية إلى المتصفح أو الجهاز بالنقر على الزر أدناه. سيؤدي هذا إلى نسخ رابط لصفحة ستنقل جلستك عند فتحها في المتصفح أو الجهاز المستهدف.", diff --git a/src/locales/it.json b/src/locales/it.json index bafd00a43..d19f868d8 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -15,13 +15,75 @@ ] }, "admin.page-title": "Pannello amministrativo - Etherpad", + "admin.loading": "Caricamento…", + "admin.toggle_sidebar": "Attiva/disattiva barra laterale", + "admin.shout": "Comunicazione", + "admin_shout.online_one": "Attualmente ci sono {{count}} utenti online", + "admin_shout.online_other": "Attualmente ci sono {{count}} utenti online", + "admin_shout.sticky_toggle": "Cambiare il messaggio adesivo", + "admin_login.title": "Etherpad", + "admin_login.username": "Nome utente", + "admin_login.password": "Password", + "admin_login.submit": "Accedi", + "admin_login.failed": "Accesso non riuscito!", + "admin_pads.all_pads": "Tutti i Pads", + "admin_pads.bulk.cleanup_history": "Pulizia della cronologia", + "admin_pads.bulk.clear_selection": "Cancella selezione", + "admin_pads.bulk.delete": "Elimina", + "admin_pads.cancel": "Annulla", + "admin_pads.col.pad": "Pad", + "admin_pads.col.revisions": "Revisioni", + "admin_pads.col.users": "Utenti", + "admin_pads.confirm_button": "OK", + "admin_pads.empty_never_edited": "vuoto · mai modificato", + "admin_pads.error_prefix": "Errore", + "admin_pads.filter.active": "Attivo", + "admin_pads.filter.all": "Tutto", + "admin_pads.filter.empty": "Vuoto", + "admin_pads.filter.recent": "Questa settimana", + "admin_pads.filter.stale": "Stale (>1y)", + "admin_pads.open": "Aperto", + "admin_pads.pagination.next": "Successivo", + "admin_pads.pagination.previous": "Precedente", + "admin_pads.refresh": "Aggiorna", + "admin_pads.relative.days": "{{count}}d fa", + "admin_pads.relative.hours": "{{count}} ore fa", + "admin_pads.relative.just_now": "proprio ora", + "admin_pads.relative.minutes": "{{count}}m fa", + "admin_pads.relative.months": "{{count}}mese fa", + "admin_pads.relative.weeks": "{{count}} settimane fa", + "admin_pads.relative.years": "{{count}} anni fa", + "admin_pads.revisions_count": "{{count}} revisioni", + "admin_pads.selected_count": "{{count}} selezionati", + "admin_pads.show": "Mostra", + "admin_pads.sort.name": "Nome (A–Z)", + "admin_pads.sort.revision_number": "Revisioni", + "admin_pads.sort.user_count": "Utenti", + "admin_pads.stats.across_pads": "su tutti i pad", + "admin_pads.stats.active_users": "Utenti attivi", + "admin_pads.stats.empty_pads": "Pads vuoti", + "admin_pads.stats.last_activity": "Ultima attività", + "admin_pads.stats.no_active_users": "Nessun utente attivo", + "admin_pads.stats.revisions_zero": "0 revisioni", + "admin_pads.stats.total": "Pads totali", + "admin_pads.stats.users_active": "{{count}} attualmente attivi", + "admin_pads.subtitle": "Panoramica di tutti i pad su questa istanza di Etherpad. Cerca, pulisci, apri.", "admin_plugins": "Gestione plugin", "admin_plugins.available": "Plugin disponibili", "admin_plugins.available_not-found": "Nessun plugin trovato.", "admin_plugins.available_fetching": "Recupero in corso…", "admin_plugins.available_install.value": "Installa", "admin_plugins.available_search.placeholder": "Cerca i plugin da installare", + "admin_plugins.check_updates": "Verifica la presenza di aggiornamenti", + "admin_plugins.core_count": "{{count}} core", + "admin_plugins.catalog_disabled": "Il catalogo dei plugin è disabilitato dal tuo operatore (privacy.pluginCatalog=false). Per installare un plugin, esegui `pnpm run plugins i ep_ dal server.", + "admin_plugins.crumbs": "Plugin", "admin_plugins.description": "Descrizione", + "admin_plugins.disables.label": "Disabilitati:", + "admin_plugins.disables.warning_title": "Questo plugin rimuove intenzionalmente le funzionalità di Etherpad elencate.", + "admin_plugins.error_retrieving": "Errore durante il recupero dei plugin", + "admin_plugins.install_error": "Impossibile installare {{plugin}}: {{error}}", + "admin_plugins.install_error_requires_newer_etherpad": "Impossibile installare {{plugin}}: richiede una versione più recente di Etherpad. Aggiorna Etherpad e riprova.", "admin_plugins.installed": "Plugin installati", "admin_plugins.installed_fetching": "Recupero dei plugin installati…", "admin_plugins.installed_nothing": "Non hai ancora installato alcun plugin.", @@ -29,27 +91,120 @@ "admin_plugins.last-update": "Ultimo aggiornamento", "admin_plugins.name": "Nome", "admin_plugins.page-title": "Gestore dei plugin - Etherpad", + "admin_plugins.reload_catalog": "Ricarica il catalogo", + "admin_plugins.search_npm": "Cerca su npm", + "admin_plugins.sort_ascending": "Ordinamento crescente", + "admin_plugins.sort_descending": "Ordinamento decrescente", + "admin_plugins.sort.last_updated": "Ultimo aggiornamento", + "admin_plugins.sort.name": "Nome (A–Z)", + "admin_plugins.sort.version": "Versione", + "admin_plugins.source": "Fonte del plugin", + "admin_plugins.subtitle": "Installa, aggiorna e rimuovi i plugin di Etherpad. Le modifiche richiedono il riavvio del server.", + "admin_plugins.tag_core": "Core", + "admin_plugins.update_tooltip": "Aggiornamento", + "admin_plugins.updates_available": "Aggiornamenti disponibili", + "admin_plugins.update_now": "Aggiorna", "admin_plugins.version": "Versione", "admin_plugins_info": "Informazioni sulla risoluzione dei problemi", + "admin_plugins_info.bindings_label": "{{count}} bindings", + "admin_plugins_info.copy_diagnostics": "Copia diagnostica", + "admin_plugins_info.copy_value": "Copia {{label}}", + "admin_plugins_info.git_sha": "Git SHA", + "admin_plugins_info.hook_bindings": "Hook bindings", "admin_plugins_info.hooks": "Hook installati", "admin_plugins_info.hooks_client": "Hook lato client", "admin_plugins_info.hooks_server": "Hook lato server", + "admin_plugins_info.no_hooks": "Nessun hooks trovato", "admin_plugins_info.parts": "Parti installate", "admin_plugins_info.plugins": "Plugin installati", "admin_plugins_info.page-title": "Informazioni sul plugin - Etherpad", + "admin_plugins_info.search_placeholder": "Cerca hook o parte…", + "admin_plugins_info.subtitle": "Diagnostica di sistema: versione installata, componenti registrati e hook.", + "admin_plugins_info.tab_client": "Client", + "admin_plugins_info.tab_server": "Server", + "admin_plugins_info.up_to_date": "Aggiornato", + "admin_plugins_info.update_available": "Aggiornamento disponibile: {{version}}", "admin_plugins_info.version": "Versione di Etherpad", "admin_plugins_info.version_latest": "Ultima versione disponibile", "admin_plugins_info.version_number": "Numero di versione", "admin_settings": "Impostazioni", + "admin_settings.create_pad": "Crea pad", "admin_settings.current": "Configurazione attuale", "admin_settings.current_example-devel": "Esempio di modello di impostazioni di sviluppo", "admin_settings.current_example-prod": "Esempio di modello di impostazioni di produzione", "admin_settings.current_restart.value": "Riavvia Etherpad", "admin_settings.current_save.value": "Salva impostazioni", + "admin_settings.invalid_json": "JSON non valido", + "admin_settings.current_test.value": "Validare JSON", + "admin_settings.toast.saved": "Impostazioni salvate correttamente.", + "admin_settings.toast.save_failed": "Salvataggio non riuscito: impossibile scrivere il file settings.json.", + "admin_settings.toast.json_invalid": "Errore di sintassi: verificare virgole, parentesi graffe e virgolette.", + "admin_settings.toast.disconnected": "Impossibile salvare: connessione al server non riuscita.", + "admin_settings.toast.validation_ok": "Il formato JSON è valido.", + "admin_settings.toast.validation_failed": "Il formato JSON non è valido: correggere gli errori di sintassi.", + "admin_settings.mode.form": "Modulo", + "admin_settings.mode.aria_label": "Modalità editor", + "admin_settings.section.general": "Generale", + "admin_settings.parse_error.title": "Impossibile analizzare il file settings.json", + "admin_settings.parse_error.cta": "Passa al formato raw per modificare", + "admin_settings.env_pill.tooltip": "Legge dalla variabile d'ambiente {{variable}}. Il valore seguente viene utilizzato quando {{variable}} non è impostata.", + "admin_settings.env_pill.default_label": "predefinito", + "admin_settings.env_pill.input_aria": "Valore predefinito per {{variable}}", + "admin_settings.env_pill.runtime_label": "valore attivo", + "admin_settings.env_pill.runtime_tooltip": "Etherpad sta attualmente utilizzando questo valore, risolto da {{variable}} o dal suo valore predefinito.", + "admin_settings.env_pill.redacted_tooltip": "Etherpad sta utilizzando un valore per {{variable}}, ma è nascosto perché è un segreto.", "admin_settings.page-title": "Impostazioni - Etherpad", + "admin_settings.save_error": "Impostazioni di salvataggio degli errori", + "admin_settings.saved_success": "Impostazioni salvate con successo", + "update.banner.title": "Aggiornamento disponibile", + "update.banner.body": "Etherpad {{latest}} è disponibile (stai eseguendo {{current}}).", + "update.banner.cta": "Visualizza aggiornamento", + "update.page.title": "Aggiornamenti di Etherpad", + "update.page.current": "Versione attuale", + "update.page.latest": "Ultima versione", + "update.page.last_check": "Ultimo controllo effettuato", + "update.page.install_method": "Metodo di installazione", + "update.page.changelog": "Changelog", + "update.page.up_to_date": "Stiamo utilizzando l'ultima versione.", + "update.page.disabled": "I controlli degli aggiornamenti sono disabilitati (updates.tier = \"off\").", + "update.page.unauthorized": "Non sei autorizzato a visualizzare lo stato degli aggiornamenti.", + "update.page.error": "Impossibile caricare lo stato dell'aggiornamento (stato {{status}}).", + "update.badge.severe": "Etherpad su questo server è gravemente obsoleto. Segnalalo all'amministratore.", + "update.badge.vulnerable": "Su questo server è in esecuzione una versione di Etherpad con vulnerabilità di sicurezza note. Informa l'amministratore.", + "update.page.apply": "Applica aggiornamento", "update.page.cancel": "Annulla", + "update.page.log": "Registro dei log (ultime 200 righe)", + "update.page.execution": "Stato", + "update.page.policy.install-method-not-writable": "Gli aggiornamenti dall'interfaccia di amministrazione richiedono un'installazione da Git. Aggiorna tramite il tuo gestore di pacchetti.", + "update.page.policy.rollback-failed-terminal": "Un precedente aggiornamento non è riuscito e non è stato possibile annullarlo. Premi Conferma al termine dell'installazione per rimuovere il blocco.", + "update.page.policy.up-to-date": "Stiamo utilizzando l'ultima versione.", + "update.page.policy.tier-off": "Gli aggiornamenti sono disabilitati (updates.tier = \"off\").", + "update.page.policy.maintenance-window-missing": "Il livello 4 (autonomo) richiede una finestra di manutenzione. Imposta updates.maintenanceWindow in settings.json per abilitare gli aggiornamenti autonomi.", + "update.page.last_result.verified": "Ultimo aggiornamento a {{tag}} verificato.", + "update.page.last_result.rolled-back": "Ultimo tentativo di aggiornamento di {{tag}} annullato: {{reason}}.", + "update.page.last_result.rollback-failed": "Ultimo tentativo di aggiornamento non riuscito E rollback non riuscito: {{reason}}. Intervento manuale necessario.", + "update.page.last_result.cancelled": "Ultimo tentativo di aggiornamento di {{tag}} annullato dall'amministratore.", + "update.execution.idle": "Idle", + "update.execution.scheduled": "Aggiornamento programmato", + "update.execution.executing": "Aggiornamento...", + "update.execution.pending-verification": "In attesa di verifica", "update.execution.verified": "Verificato", + "update.execution.rolling-back": "Rollback", + "update.execution.rolled-back": "Rolled back", + "update.execution.rollback-failed": "Rollback fallito", + "update.banner.terminal.rollback-failed": "Il tentativo di aggiornamento non è riuscito e non è stato possibile annullarlo. È necessario un intervento manuale.", + "update.banner.scheduled": "Aggiornamento automatico programmato per {{tag}} — si applica tra {{remaining}}.", + "update.banner.maintenance-window-missing": "Gli aggiornamenti automatici sono disabilitati fino a quando non viene configurata una finestra di manutenzione.", "update.page.scheduled.title": "Aggiornamento programmato", + "update.page.scheduled.countdown": "Etherpad inizierà l'aggiornamento a {{tag}} in {{remaining}}.", + "update.page.scheduled.deferred_until": "Fuori dalla finestra di manutenzione. L'aggiornamento inizierà all'apertura della finestra alle {{at}}.", + "update.page.scheduled.apply_now": "Applica ora", + "update.window.title": "Finestra di manutenzione", + "update.window.unset": "Non configurato.", + "update.window.next_opens_at": "La prossima finestra si aprirà alle {{at}}.", + "update.drain.t60": "Etherpad si riavvierà tra 60 secondi per applicare un aggiornamento.", + "update.drain.t30": "Etherpad si riavvierà tra 30 secondi per applicare un aggiornamento.", + "update.drain.t10": "Etherpad si riavvierà tra 10 secondi per applicare un aggiornamento.", "index.newPad": "Nuovo pad", "index.settings": "Impostazioni", "index.transferSessionTitle": "Sessione di trasferimento", @@ -113,6 +268,16 @@ "pad.settings.language": "Lingua:", "pad.settings.deletePad": "Elimina Pad", "pad.delete.confirm": "Vuoi veramente cancellare questo pad?", + "pad.deletionToken.modalTitle": "Salva il token di eliminazione del tuo pad", + "pad.deletionToken.modalBody": "Questo token è l'unico modo per eliminare questo pad se perdi la sessione del browser o il dispositivo di commutazione. Conservalo in un luogo sicuro - qui viene mostrato esattamente solo una volta.", + "pad.deletionToken.copy": "Copia", + "pad.deletionToken.copied": "Copiato", + "pad.deletionToken.acknowledge": "L'ho salvato", + "pad.deletionToken.deleteWithToken": "Cancellare il pad con il token", + "pad.deletionToken.tokenFieldLabel": "Token di eliminazione del pad", + "pad.deletionToken.tokenValueLabel": "Il tuo token di cancellazione del pad (solo-lettura)", + "pad.deletionToken.invalid": "Quel token non è valido per questo pad.", + "pad.deletionToken.notCreator": "Non sei tu il creatore di questo pad, quindi non puoi eliminarlo.", "pad.settings.about": "Informazioni", "pad.settings.poweredBy": "Realizzato con", "pad.importExport.import_export": "Importazione/esportazione", @@ -125,6 +290,13 @@ "pad.importExport.exportword": "Microsoft Word", "pad.importExport.exportpdf": "PDF", "pad.importExport.exportopen": "ODF (Open Document Format)", + "pad.importExport.exportetherpada.title": "Esporta come Etherpad", + "pad.importExport.exporthtmla.title": "Esporta come HTML", + "pad.importExport.exportplaina.title": "Esporta come testo semplice", + "pad.importExport.exportworda.title": "Esporta come Microsoft Word", + "pad.importExport.exportpdfa.title": "Esporta in formato PDF", + "pad.importExport.exportopena.title": "Esporta in formato ODF (Open Document Format)", + "pad.importExport.noConverter.innerHTML": "È possibile importare solo file di testo semplice o in formato HTML. Per funzionalità di importazione più avanzate, si prega di installare LibreOffice .", "pad.modals.connected": "Connesso.", "pad.modals.reconnecting": "Riconnessione al pad in corso…", "pad.modals.forcereconnect": "Forza la riconnessione", @@ -171,16 +343,25 @@ "timeslider.toolbar.returnbutton": "Ritorna al Pad", "pad.historyMode.banner": "Visualizza cronologia", "pad.historyMode.revisionLabel": "Versione {{rev}}", + "pad.historyMode.controlsLabel": "Controlli della cronologia del pad", "pad.historyMode.sliderLabel": "Versione Pad", + "pad.historyMode.settings.follow": "Segui gli aggiornamenti dei contenuti del pad", "pad.historyMode.settings.followShort": "Segui", "pad.historyMode.settings.playbackSpeed": "Velocità di riproduzione:", + "pad.historyMode.chat.replayHeader": "Chat del {{time}}", "pad.historyMode.users.authorsHeader": "Autori in questa versione", + "pad.editor.skipToContent": "Vai all'editor", + "pad.editor.keyboardHint": "Premi Esc per uscire dall'editor. Premi Alt+F9 per accedere alla barra degli strumenti.", + "pad.editor.toolbar.formatting": "Strumento di formattazione", + "pad.editor.toolbar.actions": "Barra degli strumenti delle azioni del pad", + "pad.editor.toolbar.showMore": "Mostra altri pulsanti della barra degli strumenti", "timeslider.toolbar.authors": "Autori:", "timeslider.toolbar.authorsList": "Nessun autore", "timeslider.toolbar.exportlink.title": "Esporta", "timeslider.exportCurrent": "Esporta la versione corrente come:", "timeslider.version": "Versione {{version}}", "timeslider.saved": "Salvato {{day}} {{month}} {{year}}", + "timeslider.settings.playbackSpeed": "Velocità di riproduzione:", "timeslider.settings.playbackSpeed.original": "Velocità originale", "timeslider.settings.playbackSpeed.realtime": "In tempo reale", "timeslider.settings.playbackSpeed.200ms": "200 ms", @@ -207,6 +388,7 @@ "pad.savedrevs.timeslider": "Puoi vedere le versioni salvate visitando la cronologia", "pad.userlist.entername": "Inserisci il tuo nome", "pad.userlist.unnamed": "senza nome", + "pad.userlist.onlineCount": "{[ plurale(count) uno: {{count}} utente connesso, altro: {{count}} utenti connessi ]}", "pad.editbar.clearcolors": "Eliminare i colori degli autori sull'intero documento? Questa azione non può essere annullata", "pad.impexp.importbutton": "Importa ora", "pad.impexp.importing": "Importazione in corso...", @@ -217,5 +399,6 @@ "pad.impexp.importfailed": "Importazione fallita", "pad.impexp.copypaste": "Si prega di copiare e incollare", "pad.impexp.exportdisabled": "L'esportazione come {{type}} è disabilitata. Contattare l'amministratore per i dettagli.", - "pad.impexp.maxFileSize": "File troppo grande. Contatta l'amministratore del sito per incrementare la dimensione consentita per l'importazione" + "pad.impexp.maxFileSize": "File troppo grande. Contatta l'amministratore del sito per incrementare la dimensione consentita per l'importazione", + "pad.social.description": "Un documento collaborativo che tutti possono modificare in tempo reale." } diff --git a/src/locales/ko.json b/src/locales/ko.json index 44a706fa9..6d48cd750 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -19,13 +19,80 @@ ] }, "admin.page-title": "관리 대시보드 - 이더패드", + "admin.loading": "불러오는 중…", + "admin.loading_description": "페이지가 로딩되는 동안 잠시 기다려 주세요.", + "admin.toggle_sidebar": "사이드바 토글", + "admin.shout": "커뮤니케이션", + "admin_shout.online_one": "현재 {{count}}명의 사용자가 온라인 상태입니다", + "admin_shout.online_other": "현재 {{count}}명의 사용자가 온라인 상태입니다", + "admin_shout.sticky_toggle": "고정 메시지 변경", + "admin_login.title": "이더패드", + "admin_login.username": "사용자 이름", + "admin_login.password": "비밀번호", + "admin_login.submit": "로그인", + "admin_login.failed": "로그인 실패", + "admin_pads.all_pads": "모든 패드", + "admin_pads.bulk.cleanup_history": "역사 지우기", + "admin_pads.bulk.clear_selection": "선택 항목 지우기", + "admin_pads.bulk.delete": "삭제", + "admin_pads.cancel": "취소", + "admin_pads.col.pad": "패드", + "admin_pads.col.revisions": "판", + "admin_pads.col.users": "사용자", + "admin_pads.confirm_button": "확인", + "admin_pads.create_pad_dialog_description": "새 패드의 이름을 선택하세요.", + "admin_pads.delete_pad_dialog_description": "패드 삭제를 확인하거나 취소하세요.", + "admin_pads.delete_pad_dialog_title": "패드 삭제", + "admin_pads.empty_never_edited": "비었음 · 편집된 적 없음", + "admin_pads.error_dialog_description": "오류가 발생했습니다.", + "admin_pads.error_prefix": "오류", + "admin_pads.filter.active": "활성", + "admin_pads.filter.all": "모두", + "admin_pads.filter.empty": "비었음", + "admin_pads.filter.recent": "이번 주", + "admin_pads.filter.stale": "오래됨 (>1년)", + "admin_pads.open": "열기", + "admin_pads.pagination.next": "다음", + "admin_pads.pagination.previous": "이전", + "admin_pads.refresh": "새로 고침", + "admin_pads.relative.days": "{{count}}일 전", + "admin_pads.relative.hours": "{{count}}시간 전", + "admin_pads.relative.just_now": "방금", + "admin_pads.relative.minutes": "{{count}}분 전", + "admin_pads.relative.months": "{{count}}달 전", + "admin_pads.relative.weeks": "{{count}}주 전", + "admin_pads.relative.years": "{{count}}년 전", + "admin_pads.revisions_count": "수정 내역 {{count}}개", + "admin_pads.selected_count": "{{count}}개 선택됨", + "admin_pads.show": "보기", + "admin_pads.sort.name": "이름 (A-Z)", + "admin_pads.sort.revision_number": "수정내역", + "admin_pads.sort.user_count": "사용자", + "admin_pads.stats.across_pads": "모든 패드에 걸쳐서", + "admin_pads.stats.active_users": "활동적인 사용자", + "admin_pads.stats.empty_pads": "빈 패드", + "admin_pads.stats.last_activity": "마지막 활동", + "admin_pads.stats.no_active_users": "활동적인 사용자 없음", + "admin_pads.stats.revisions_zero": "수정 내역 없음", + "admin_pads.stats.total": "총 패드", + "admin_pads.stats.users_active": "{{count}}명 현재 활동중", + "admin_pads.subtitle": "이 이더패드 인스턴스의 모든 패드 개요입니다. 검색, 정리, 열기를 할 수 있습니다.", "admin_plugins": "플러그인 관리자", "admin_plugins.available": "사용 가능한 플러그인", "admin_plugins.available_not-found": "플러그인이 없습니다.", "admin_plugins.available_fetching": "검색 중...", "admin_plugins.available_install.value": "설치", "admin_plugins.available_search.placeholder": "설치할 플러그인을 검색", + "admin_plugins.check_updates": "업데이트 확인", + "admin_plugins.core_count": "코어 {{count}}개", + "admin_plugins.catalog_disabled": "운영자가 플러그인 카탈로그를 비활성화했습니다(privacy.pluginCatalog=false). 플러그인을 설치하려면 서버에서 `pnpm run plugins i ep_`을 실행하세요.", + "admin_plugins.crumbs": "플러그인", "admin_plugins.description": "설명", + "admin_plugins.disables.label": "비활성화:", + "admin_plugins.disables.warning_title": "이 플러그인은 나열된 이더패드 기능을 의도적으로 제거합니다.", + "admin_plugins.error_retrieving": "플러그인을 가져오는 중 오류 발생", + "admin_plugins.install_error": "{{plugin}} 설치 실패: {{error}}", + "admin_plugins.install_error_requires_newer_etherpad": "{{plugin}}을 설치할 수 없습니다. 더 새로운 버전의 이더패드가 필요합니다. 이더패드를 업그레이드한 뒤 다시 시도하세요.", "admin_plugins.installed": "설치된 플러그인", "admin_plugins.installed_fetching": "설치된 플러그인을 검색하는 중...", "admin_plugins.installed_nothing": "아직 플러인을 설치하지 않으셨습니다.", @@ -33,50 +100,156 @@ "admin_plugins.last-update": "마지막 업데이트", "admin_plugins.name": "이름", "admin_plugins.page-title": "플러그인 관리자 - 이더패드", + "admin_plugins.reload_catalog": "카탈로그 새로고침", + "admin_plugins.search_npm": "npm에서 검색", + "admin_plugins.sort_ascending": "오름차순 정렬", + "admin_plugins.sort_descending": "내림차순 정렬", + "admin_plugins.sort.last_updated": "최근 업데이트", + "admin_plugins.sort.name": "이름 (A-Z)", + "admin_plugins.sort.version": "버전", + "admin_plugins.source": "플러그인 소스", + "admin_plugins.subtitle": "이더패드 플러그인을 설치, 업데이트 및 제거합니다. 변경 사항을 적용하려면 서버를 다시 시작해야 합니다.", + "admin_plugins.tag_core": "코어", + "admin_plugins.update_tooltip": "업데이트", + "admin_plugins.updates_available": "업데이트가 있습니다", + "admin_plugins.update_now": "업데이트", "admin_plugins.version": "버전", "admin_plugins_info": "문제 해결 정보", + "admin_plugins_info.bindings_label": "바인딩 {{count}}개", + "admin_plugins_info.copy_diagnostics": "진단 복사", + "admin_plugins_info.copy_value": "{{label}} 복사", + "admin_plugins_info.git_sha": "깃 SHA", + "admin_plugins_info.hook_bindings": "후크 바인딩", "admin_plugins_info.hooks": "설치된 훅", "admin_plugins_info.hooks_client": "클라이언트 사이드 훅", "admin_plugins_info.hooks_server": "서버사이드 훅", + "admin_plugins_info.no_hooks": "훅이 없습니다", "admin_plugins_info.parts": "설치된 항목", "admin_plugins_info.plugins": "설치된 플러그인", "admin_plugins_info.page-title": "플러그인 정보 - 이더패드", + "admin_plugins_info.search_placeholder": "후크 또는 파츠 검색…", + "admin_plugins_info.subtitle": "시스템 진단: 설치된 버전, 등록된 파츠 및 후크입니다.", + "admin_plugins_info.tab_client": "클라이언트", + "admin_plugins_info.tab_server": "서버", + "admin_plugins_info.up_to_date": "현재 최신 버전", + "admin_plugins_info.update_available": "업데이트 가능: {{version}}", "admin_plugins_info.version": "이더패드 버전", "admin_plugins_info.version_latest": "사용 가능한 최신 버전", "admin_plugins_info.version_number": "버전 번호", "admin_settings": "설정", + "admin_settings.create_pad": "패드 만들기", "admin_settings.current": "현재 구성", "admin_settings.current_example-devel": "예시 개발용 설정 틀", "admin_settings.current_example-prod": "예시 운영용 설정 틀", "admin_settings.current_restart.value": "이더패드 다시 시작", "admin_settings.current_save.value": "설정 저장", + "admin_settings.invalid_json": "잘못된 JSON", + "admin_settings.current_test.value": "JSON 검증", + "admin_settings.current_prettify.value": "JSON 정리", "admin_settings.toast.saved": "설정을 성공적으로 저장했습니다.", + "admin_settings.toast.save_failed": "저장 실패: settings.json에 쓰기 할 수 없습니다.", + "admin_settings.toast.json_invalid": "구문 오류: 쉼표, 중괄호, 따옴표를 확인하세요.", + "admin_settings.toast.disconnected": "저장할 수 없음: 서버에 연결되어 있지 않습니다.", + "admin_settings.toast.validation_ok": "JSON이 유효합니다.", + "admin_settings.toast.validation_failed": "JSON이 유효하지 않습니다. 구문 오류를 수정하세요.", + "admin_settings.toast.prettify_failed": "정리할 수 없습니다. 먼저 구문 오류를 수정하세요.", + "admin_settings.prettify_confirm": "정리하면 모든 의견이 제거됩니다. 계속하시겠습니까?", "admin_settings.mode.form": "형태", "admin_settings.mode.raw": "원본", + "admin_settings.mode.aria_label": "편집기 모드", "admin_settings.section.general": "일반", + "admin_settings.parse_error.title": "settings.json을 구문 분석할 수 없습니다", "admin_settings.parse_error.cta": "원본 편집으로 전환", + "admin_settings.env_pill.tooltip": "{{variable}} 환경 변수에서 읽습니다. {{variable}}이 설정되지 않은 경우 아래 값이 사용됩니다.", "admin_settings.env_pill.default_label": "기본값", + "admin_settings.env_pill.input_aria": "{{variable}}의 기본값", + "admin_settings.env_pill.runtime_label": "활성 값", + "admin_settings.env_pill.runtime_tooltip": "이더패드는 현재 {{variable}} 또는 해당 기본값에서 확인된 이 값을 사용하고 있습니다.", + "admin_settings.env_pill.redacted_tooltip": "이더패드가 {{variable}}의 값을 사용하고 있지만, 비밀 값이므로 숨겨져 있습니다.", "admin_settings.page-title": "설정 - 이더패드", + "admin_settings.save_error": "설정 저장 오류", + "admin_settings.saved_success": "설정이 성공적으로 저장되었습니다", "update.banner.title": "업데이트 사용 가능", + "update.banner.body": "이더패드 {{latest}}을 사용할 수 있습니다(현재 {{current}} 실행 중).", "update.banner.cta": "업데이트 보기", + "update.page.title": "이더패드 업데이트", "update.page.current": "현재 판", "update.page.latest": "최신 버전", "update.page.last_check": "마지막으로 확인함", "update.page.install_method": "설치 방법", + "update.page.tier": "업데이트 티어", "update.page.changelog": "바뀐기록", "update.page.up_to_date": "최신 버전을 사용 중입니다.", + "update.page.disabled": "업데이트 확인이 비활성화되어 있습니다(updates.tier = \"off\").", + "update.page.unauthorized": "업데이트 상태를 볼 권한이 없습니다.", + "update.page.error": "업데이트 상태를 불러올 수 없습니다(상태 {{status}}).", + "update.badge.severe": "이 서버의 이더패드가 심각하게 오래되었습니다. 관리자에게 알리세요.", + "update.badge.vulnerable": "이 서버의 이더패드는 알려진 보안 문제가 있는 버전을 실행하고 있습니다. 관리자에게 알리세요.", "update.page.apply": "업데이트 적용", "update.page.cancel": "취소", + "update.page.acknowledge": "확인", + "update.page.log": "업데이트 로그 (최근 200줄)", "update.page.execution": "상태", + "update.page.policy.install-method-not-writable": "관리자 UI에서 업데이트하려면 Git 설치가 필요합니다. 패키지 관리자를 통해 업데이트하세요.", + "update.page.policy.rollback-failed-terminal": "이전 업데이트가 실패했으며 롤백할 수 없었습니다. 설치가 정상화된 뒤 확인을 눌러 잠금을 해제하세요.", "update.page.policy.up-to-date": "최신 버전을 사용 중입니다.", + "update.page.policy.tier-off": "업데이트가 비활성화되었습니다 (updates.tier = \"off\").", + "update.page.policy.maintenance-window-missing": "계층 4(자율)는 유지보수 기간이 필요합니다. 자율 업데이트를 활성화하려면 settings.json에서 updates.maintenanceWindow를 설정하세요.", + "update.page.policy.maintenance-window-invalid": "updates.maintenanceWindow 형식이 잘못되어 계층 4(자율)가 비활성화되었습니다. HH:MM 시간과 \"local\" 또는 \"utc\"인 tz를 포함한 {start, end, tz} 형식이 필요합니다.", + "update.page.last_result.verified": "{{tag}}의 마지막 업데이트가 확인되었습니다.", + "update.page.last_result.rolled-back": "{{tag}}에 대한 마지막 업데이트 시도가 롤백되었습니다: {{reason}}.", + "update.page.last_result.rollback-failed": "마지막 업데이트 시도가 실패했고 롤백도 실패했습니다: {{reason}}. 수동 개입이 필요합니다.", + "update.page.last_result.preflight-failed": "{{tag}}에 대한 마지막 업데이트 시도가 사전 검사에서 실패했습니다: {{reason}}.", + "update.page.last_result.cancelled": "관리자에 의해 {{tag}}에 대한 마지막 업데이트 시도가 취소되었습니다.", + "update.execution.idle": "유휴 상태", + "update.execution.scheduled": "예정된 업데이트", + "update.execution.preflight": "비행 전 점검", + "update.execution.preflight-failed": "비행 전 점검 실패", + "update.execution.draining": "세션 정리 중", "update.execution.executing": "업데이트 중...", + "update.execution.pending-verification": "확인 대기 중", "update.execution.verified": "확인됨", "update.execution.rolling-back": "되돌리는 중", "update.execution.rolled-back": "되돌려짐", "update.execution.rollback-failed": "되돌리기 실패", + "update.banner.terminal.rollback-failed": "업데이트 시도가 실패했으며 롤백할 수 없습니다. 수동 조치가 필요합니다.", + "update.banner.scheduled": "{{tag}}에 대한 자동 업데이트가 예약되었습니다. {{remaining}}에 적용됩니다.", + "update.banner.maintenance-window-missing": "유지 관리 기간이 설정될 때까지 자동 업데이트가 비활성화됩니다.", + "update.banner.maintenance-window-invalid": "유지 관리 기간이 잘못 설정되어 있어 자동 업데이트가 비활성화되었습니다.", + "update.page.scheduled.title": "예정된 업데이트", + "update.page.scheduled.countdown": "이더패드는 {{remaining}} 후에 {{tag}}(으)로의 업데이트를 시작합니다.", + "update.page.scheduled.deferred_until": "유지 보수 시간 외입니다. 업데이트는 유지 보수가 시작되는 {{at}}에 시작됩니다.", + "update.page.scheduled.apply_now": "지금 적용", + "update.window.title": "유지보수 창", + "update.window.summary": "{{start}}–{{end}} ({{tz}})", + "update.window.unset": "구성되지 않았습니다.", + "update.window.next_opens_at": "다음 창은 {{at}}에 열립니다.", + "update.drain.t60": "업데이트 적용을 위해 이더패드가 60초 뒤에 재시작됩니다.", + "update.drain.t30": "업데이트 적용을 위해 이더패드가 30초 뒤에 재시작됩니다.", + "update.drain.t10": "업데이트 적용을 위해 이더패드가 10초 뒤에 재시작됩니다.", "index.newPad": "새 패드", + "index.settings": "설정", + "index.transferSessionTitle": "전송 세션", + "index.receiveSessionTitle": "수신 세션", + "index.receiveSessionDescription": "여기에서 다른 브라우저나 기기에서 이더패드 세션을 수신할 수 있습니다. 단, 이렇게 하면 현재 세션이 삭제되므로 주의하세요.", + "index.transferSession": "1. 전송 세션", + "index.transferSessionNow": "전송 세션을 지금 시작", + "index.copyLink": "2. 링크 복사", + "index.copyLinkDescription": "아래 버튼을 클릭하여 링크를 클립보드에 복사하세요.", + "index.copyLinkButton": "클립보드에 링크 복사하기", + "index.transferToSystem": "3. 세션을 새 시스템으로 복사", + "index.transferToSystemDescription": "복사한 링크를 대상 브라우저 또는 기기에서 열어 세션을 전송하세요.", + "index.code": "코드", + "index.transferSessionDescription": "아래 버튼을 클릭하여 현재 세션을 브라우저 또는 기기로 전송하세요. 이 버튼을 클릭하면 대상 브라우저 또는 기기에서 열었을 때 세션이 전송될 문서 링크가 복사됩니다.", "index.createOpenPad": "이름으로 패드 열기", "index.openPad": "이름으로 기존 패드 열기:", + "index.recentPads": "최근 패드", + "index.recentPadsEmpty": "최근 패드가 없습니다.", + "index.generateNewPad": "무작위 패드 이름 생성", + "index.labelPad": "패드 이름 (선택 사항)", + "index.placeholderPadEnter": "패드 이름을 입력...", + "index.createAndShareDocuments": "실시간으로 문서를 생성하고 공유하세요", + "index.createAndShareDocumentsDescription": "이더패드를 사용하면 마치 브라우저에서 실행되는 실시간 멀티플레이어 편집기처럼 문서를 실시간으로 공동 편집할 수 있습니다.", "pad.toolbar.bold.title": "굵게 (Ctrl+B)", "pad.toolbar.italic.title": "기울임꼴 (Ctrl+I)", "pad.toolbar.underline.title": "밑줄 (Ctrl+U)", @@ -109,33 +282,45 @@ "pad.settings.stickychat": "화면에 항상 대화 보기", "pad.settings.chatandusers": "대화와 사용자 보기", "pad.settings.colorcheck": "작성자 표시 색상", + "pad.settings.fadeInactiveAuthorColors": "비활성 저자의 색상 흐리기", "pad.settings.linenocheck": "줄 번호", "pad.settings.rtlcheck": "우횡서(오른쪽에서 왼쪽으로)입니까?", "pad.settings.enforceSettings": "다른 사용자에게 설정 적용", + "pad.settings.enforcedNotice": "이 설정은 패드 제작자가 잠가 놓았습니다. 설정을 변경해야 하는 경우 패드 제작자에게 문의하세요.", "pad.settings.fontType": "글꼴 종류:", "pad.settings.fontType.normal": "보통", "pad.settings.language": "언어:", "pad.settings.deletePad": "패드 삭제", "pad.delete.confirm": "정말로 이 패드를 삭제하겠습니까?", + "pad.deletionToken.modalTitle": "패드 삭제 토큰을 저장", + "pad.deletionToken.modalBody": "이 토큰은 브라우저 세션이 종료되거나 기기를 변경했을 때 이 패드를 삭제할 수 있는 유일한 방법입니다. 안전한 곳에 저장해 두세요. 이 토큰은 여기에 딱 한 번만 표시됩니다.", "pad.deletionToken.copy": "복사", "pad.deletionToken.copied": "복사됨", + "pad.deletionToken.acknowledge": "저장했습니다", + "pad.deletionToken.deleteWithToken": "토큰으로 패드 삭제", + "pad.deletionToken.tokenFieldLabel": "패드 삭제 토큰", + "pad.deletionToken.tokenValueLabel": "패드 삭제 토큰(읽기 전용)", + "pad.deletionToken.invalid": "이 패드에 유효한 토큰이 아닙니다.", + "pad.deletionToken.notCreator": "당신은 이 패드의 제작자가 아니므로 삭제할 수 없습니다.", "pad.settings.about": "정보", "pad.settings.poweredBy": "제공:", "pad.importExport.import_export": "가져오기/내보내기", "pad.importExport.import": "텍스트 파일이나 문서 올리기", "pad.importExport.importSuccessful": "성공!", "pad.importExport.export": "다음으로 현재 패드 내보내기:", - "pad.importExport.exportetherpad": "Etherpad", + "pad.importExport.exportetherpad": "이더패드", "pad.importExport.exporthtml": "HTML", "pad.importExport.exportplain": "일반 텍스트", "pad.importExport.exportword": "Microsoft 워드", "pad.importExport.exportpdf": "PDF", "pad.importExport.exportopen": "ODF (Open Document Format, 개방형 문서 형식)", + "pad.importExport.exportetherpada.title": "이더패드 형식으로 내보내기", "pad.importExport.exporthtmla.title": "HTML로 내보내기", "pad.importExport.exportplaina.title": "일반 텍스트로 내보내기", "pad.importExport.exportworda.title": "Microsoft Word 파일로 내보내기", "pad.importExport.exportpdfa.title": "PDF로 내보내기", "pad.importExport.exportopena.title": "ODF(Open Document Format) 형식으로 내보내기", + "pad.importExport.noConverter.innerHTML": "일반 텍스트나 HTML 형식으로만 가져올 수 있습니다. 고급 가져오기 기능에 대해서는 LibreOffice를 설치하세요.", "pad.modals.connected": "연결함.", "pad.modals.reconnecting": "내 패드에 다시 연결하는 중...", "pad.modals.forcereconnect": "강제로 다시 연결", @@ -181,9 +366,23 @@ "timeslider.pageTitle": "{{appTitle}} 시간슬라이더", "timeslider.toolbar.returnbutton": "패드로 돌아가기", "pad.historyMode.banner": "역사 보는 중", + "pad.historyMode.return": "라이브로 돌아가기", "pad.historyMode.revisionLabel": "판 {{rev}}", + "pad.historyMode.controlsLabel": "패드 기록 제어", + "pad.historyMode.sliderLabel": "패드 판", + "pad.historyMode.settings.title": "역사 재생", + "pad.historyMode.settings.follow": "패드 콘텐츠의 갱신 주시하기", "pad.historyMode.settings.followShort": "팔로우", + "pad.historyMode.followOn": "패드 변경 사항 추적중 — 추적 중지하려면 클릭", + "pad.historyMode.followOff": "패드 변경 사항 추적 안함 — 추적하려면 클릭", + "pad.historyMode.settings.playbackSpeed": "재생 속도:", + "pad.historyMode.chat.replayHeader": "{{time}} 시점의 채팅", "pad.historyMode.users.authorsHeader": "이 판의 작성자", + "pad.editor.skipToContent": "편집기로 바로 가기", + "pad.editor.keyboardHint": "Esc 키를 눌러 편집기를 종료하세요. Alt+F9 키를 눌러 도구 모음에 액세스하세요.", + "pad.editor.toolbar.formatting": "도구 모음 설정", + "pad.editor.toolbar.actions": "패드 작업 도구 모음", + "pad.editor.toolbar.showMore": "도구 모음 버튼 더 보기", "timeslider.toolbar.authors": "작성자:", "timeslider.toolbar.authorsList": "저자 없음", "timeslider.toolbar.exportlink.title": "내보내기", @@ -217,6 +416,7 @@ "pad.savedrevs.timeslider": "당신은 타임슬라이더를 통해 저장된 버전을 볼 수 있습니다", "pad.userlist.entername": "이름을 입력하세요", "pad.userlist.unnamed": "이름없음", + "pad.userlist.onlineCount": "{[ plural(count) one: {{count}}명의 연결된 사용자, other: {{count}}명의 연결된 사용자 ]}", "pad.editbar.clearcolors": "전체 문서의 작성자 표시 색상을 지우시겠습니까? 이 작업은 취소할 수 없습니다", "pad.impexp.importbutton": "지금 가져오기", "pad.impexp.importing": "가져오는 중...", @@ -227,5 +427,6 @@ "pad.impexp.importfailed": "가져오기를 실패했습니다", "pad.impexp.copypaste": "복사하여 붙여넣으세요", "pad.impexp.exportdisabled": "{{type}} 형식으로 내보내기가 비활성화되어 있습니다. 자세한 내용은 시스템 관리자에게 문의하시기 바랍니다.", - "pad.impexp.maxFileSize": "파일의 용량이 너무 큽니다. 가져올 파일의 허용 크기를 늘리려면 사이트 관리자에게 문의하십시오" + "pad.impexp.maxFileSize": "파일의 용량이 너무 큽니다. 가져올 파일의 허용 크기를 늘리려면 사이트 관리자에게 문의하십시오", + "pad.social.description": "모두가 실시간으로 편집할 수 있는 공동 작업 문서입니다." } diff --git a/src/locales/mk.json b/src/locales/mk.json index d79d23d10..95451b146 100644 --- a/src/locales/mk.json +++ b/src/locales/mk.json @@ -111,15 +111,42 @@ "admin_plugins_info.plugins": "Воспоставени приклучоци", "admin_plugins_info.page-title": "Информации за приклучоци — Etherpad", "admin_plugins_info.search_placeholder": "Пребарај пресретник или дел…", + "admin_plugins_info.subtitle": "Системска дијагностика: воспоставена верзија, регистрирани делови и пресретници.", + "admin_plugins_info.tab_client": "Клиент", + "admin_plugins_info.tab_server": "Опслужувач", + "admin_plugins_info.up_to_date": "Подновено", + "admin_plugins_info.update_available": "Достапна поднова: {{version}}", "admin_plugins_info.version": "Верзија на Etherpad", "admin_plugins_info.version_latest": "Најнова достапна верзија", "admin_plugins_info.version_number": "Број на верзијата", "admin_settings": "Нагодувања", + "admin_settings.create_pad": "Создај тетратка", "admin_settings.current": "Тековна поставеност", "admin_settings.current_example-devel": "Предлошка за примерни разработни нагодувања", "admin_settings.current_example-prod": "Предлошка за примерни производни нагодувања", "admin_settings.current_restart.value": "Пушти го Etherpad одново", "admin_settings.current_save.value": "Зачувај нагодувања", + "admin_settings.invalid_json": "Неважечки JSON", + "admin_settings.current_test.value": "Провери го JSON", + "admin_settings.current_prettify.value": "Разубави го JSON", + "admin_settings.toast.saved": "Нагодувањата се успешно зачувани.", + "admin_settings.toast.save_failed": "Зачувувањето на успеа: не можев да запишам во settings.json.", + "admin_settings.toast.json_invalid": "Синтаксна грешка: проверете запирки, загради и наводници.", + "admin_settings.toast.disconnected": "Не можам да зачуван: не сте поврзани на опслужувачот.", + "admin_settings.toast.validation_ok": "JSON е важечки.", + "admin_settings.toast.validation_failed": "JSON е неважечки: исправете ги синтаксните грешки.", + "admin_settings.toast.prettify_failed": "Не можам да разубавам: прво поправете ги синтаксните грешки.", + "admin_settings.prettify_confirm": "Разубавувањето ќе ги отстрани сите коментари. Да продолжам?", + "admin_settings.mode.form": "Образец", + "admin_settings.mode.raw": "Сирово", + "admin_settings.mode.aria_label": "Уредувачки режим", + "admin_settings.section.general": "Општо", + "admin_settings.parse_error.title": "Не можам да го расчленам settings.json", + "admin_settings.parse_error.cta": "Префрлете се на сирово за да уредувате", + "admin_settings.env_pill.tooltip": "Чита од околинската променлива {{variable}}. Вредноста подолу се користи кога нема зададено ништо за {{variable}}.", + "admin_settings.env_pill.default_label": "по основно", + "admin_settings.env_pill.input_aria": "Стандардна вредност за {{variable}}", + "admin_settings.env_pill.runtime_label": "активна вредност", "admin_settings.page-title": "Нагодувања — Etherpad", "update.banner.title": "Достапна е надградба", "update.banner.body": "Достапен е Etherpad {{latest}} (ја користите {{current}}).", From d9dabe352a01a8e2d7a7556e68e61135a73f04e4 Mon Sep 17 00:00:00 2001 From: John McLear Date: Mon, 25 May 2026 13:23:10 +0100 Subject: [PATCH 06/69] feat(admin): explain env-var substitution in /settings, surface auth errors (#7819) (#7826) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(admin): explain env-var substitution in /settings, surface auth errors (#7819) Three small, env-var-only UX improvements driven by issue #7819, where a Docker operator saved an ep_oauth block in the admin /settings raw view and reported it "disappeared" — but the underlying confusion was that settings.json on disk is a *template*, not the effective config. None of these changes is visible to installs that don't use ${VAR} placeholders. * Banner above the editor explaining the template/env-substitution model, only rendered when the loaded file contains a ${VAR} placeholder. Tells the operator that the file is not env-substituted in place and that the Effective tab shows the live values. * Effective tab in the mode toggle, read-only, also gated on ${VAR}. The backend was already emitting redacted runtime settings as `resolved` alongside every `load`; the SPA now exposes them so an operator can verify what Etherpad is actually using. * admin_auth_error event from the /settings socket handler. The handler previously silently returned when the connecting session wasn't admin, which made misrouted Traefik+SSO auth look like "save did nothing" with no error path in the UI. Emit a dedicated event before dropping the socket so the SPA can show a clear toast. Tests: - src/tests/backend/specs/admin/adminSettingsAuthError.ts — new spec for the auth_error/disconnect contract. - src/tests/frontend-new/admin-spec/adminsettings.spec.ts — new Playwright test asserting the banner + Effective tab only appear after a ${VAR} is added to settings.json, and that the Effective view is read-only + shows [REDACTED] for secrets. No behaviour change for installs without ${VAR} placeholders — banner, Effective tab, and auth-error contract are all the same as before. Co-Authored-By: Claude Opus 4.7 (1M context) * fix(admin): drop fragile pre-condition + add reconnect-loop guard (#7819) CI's admin-UI workflow seeds settings.json by copying settings.json.template verbatim, which contains ~30 \${VAR} placeholders. The new Playwright test asserted "banner not present before adding placeholder" — true on a fresh dev machine, false in CI. Drop that assertion: the negative path is covered by the SettingsPage ENV_VAR_PATTERN regex itself; what matters at the UI level is the positive path (banner + Effective tab render correctly when placeholders are present), which this test still exercises. Also: the server's admin_auth_error path calls socket.disconnect(), which the SPA's existing disconnect handler interprets as "io server disconnect" and immediately reconnects — creating a reject/reconnect loop. Track an authErrored flag and suppress the reconnect once an auth_error has been received. Reset on successful connect, so a legitimate re-auth path still works. Co-Authored-By: Claude Opus 4.7 (1M context) --------- Co-authored-by: Claude Opus 4.7 (1M context) --- admin/src/App.css | 31 +++++ admin/src/App.tsx | 25 +++- admin/src/components/settings/ModeToggle.tsx | 21 ++- admin/src/pages/SettingsPage.tsx | 127 ++++++++++++------ src/locales/en.json | 5 + src/node/hooks/express/adminsettings.ts | 12 +- .../specs/admin/adminSettingsAuthError.ts | 56 ++++++++ .../backend/specs/admin/adminSettingsSave.ts | 1 + .../admin-spec/adminsettings.spec.ts | 30 +++++ 9 files changed, 266 insertions(+), 42 deletions(-) create mode 100644 src/tests/backend/specs/admin/adminSettingsAuthError.ts diff --git a/admin/src/App.css b/admin/src/App.css index a064a56f9..c0e02cd0e 100644 --- a/admin/src/App.css +++ b/admin/src/App.css @@ -34,6 +34,37 @@ textarea.settings:focus { border-top: 1px solid #ddd; } +/* --- env-var banner --- */ +/* Shown only when settings.json contains ${VAR} placeholders. The + typical reader is a Docker/K8s operator who has just been surprised + by env-var substitution semantics, so the copy must explain rather + than warn — visual weight matches a note, not an error. */ +.settings-envvar-banner { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 12px 16px; + margin-bottom: 16px; + background: #f5f7ff; + border: 1px solid #c7d2fe; + border-radius: 6px; + color: #1e293b; +} +.settings-envvar-banner svg { + flex-shrink: 0; + color: #4f46e5; + margin-top: 2px; +} +.settings-envvar-banner strong { + display: block; + margin-bottom: 4px; +} +.settings-envvar-banner p { + margin: 0; + font-size: 13px; + line-height: 1.5; +} + /* --- mode toggle --- */ .settings-mode-toggle { display: inline-flex; diff --git a/admin/src/App.tsx b/admin/src/App.tsx index b182be02f..d7395251b 100644 --- a/admin/src/App.tsx +++ b/admin/src/App.tsx @@ -32,12 +32,19 @@ export const App = () => { const settingSocket = connect(`${WS_URL}/settings`, {transports: ['websocket']}); const pluginsSocket = connect(`${WS_URL}/pluginfw/installer`, {transports: ['websocket']}) + // When the server explicitly rejects us for not being admin, we must + // NOT reconnect on the subsequent `disconnect` event — otherwise the + // socket cycles forever (connect → admin_auth_error → server + // disconnect → SPA auto-reconnect → …). The flag is reset on each + // successful connect. + let authErrored = false; pluginsSocket.on('connect', () => { useStore.getState().setPluginsSocket(pluginsSocket); }); settingSocket.on('connect', () => { + authErrored = false; useStore.getState().setSettingsSocket(settingSocket); useStore.getState().setShowLoading(false) settingSocket.emit('load'); @@ -46,7 +53,7 @@ export const App = () => { settingSocket.on('disconnect', (reason) => { useStore.getState().setShowLoading(true) - if (reason === 'io server disconnect') settingSocket.connect(); + if (reason === 'io server disconnect' && !authErrored) settingSocket.connect(); }); settingSocket.on('settings', (settings: any) => { @@ -75,6 +82,22 @@ export const App = () => { const detail = payload?.message ?? ''; setToastState({open: true, title: t('admin_settings.toast.save_failed') + (detail ? ` (${detail})` : ''), success: false}); } + }); + + // Backend emits this when the connecting socket does not have an + // admin session. Previously the server just dropped silently, so the + // SPA would sit on a loading screen with no clue. Surface it AND + // suppress the automatic reconnect — without this flag the SPA would + // immediately reconnect to a socket that will reject it again. + settingSocket.on('admin_auth_error', (payload?: {message?: string}) => { + authErrored = true; + const {setToastState} = useStore.getState(); + setToastState({ + open: true, + title: payload?.message || t('admin_settings.toast.auth_error'), + success: false, + }); + useStore.getState().setShowLoading(false); }) return () => { diff --git a/admin/src/components/settings/ModeToggle.tsx b/admin/src/components/settings/ModeToggle.tsx index 0f87f62ce..bce7ef81d 100644 --- a/admin/src/components/settings/ModeToggle.tsx +++ b/admin/src/components/settings/ModeToggle.tsx @@ -1,13 +1,17 @@ import { Trans, useTranslation } from 'react-i18next'; -export type Mode = 'form' | 'raw'; +export type Mode = 'form' | 'raw' | 'effective'; type Props = { mode: Mode; onChange: (mode: Mode) => void; + // When false, the Effective tab is hidden. We hide it for installs that + // aren't using env-var substitution at all — there's no useful difference + // between the raw file and the effective in-memory config for them. + showEffective?: boolean; }; -export const ModeToggle = ({ mode, onChange }: Props) => { +export const ModeToggle = ({ mode, onChange, showEffective = false }: Props) => { const { t } = useTranslation(); return (
@@ -31,6 +35,19 @@ export const ModeToggle = ({ mode, onChange }: Props) => { > + {showEffective && ( + + )}
); }; diff --git a/admin/src/pages/SettingsPage.tsx b/admin/src/pages/SettingsPage.tsx index 7511b8e39..8c5529ea8 100644 --- a/admin/src/pages/SettingsPage.tsx +++ b/admin/src/pages/SettingsPage.tsx @@ -1,22 +1,42 @@ -import React, { useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { useStore } from '../store/store'; import { isJSONClean, cleanComments } from '../utils/utils'; import { Trans, useTranslation } from 'react-i18next'; import { IconButton } from '../components/IconButton'; -import { RotateCw, Save, AlignLeft, ShieldCheck } from 'lucide-react'; +import { RotateCw, Save, AlignLeft, ShieldCheck, Info } from 'lucide-react'; import { FormView } from '../components/settings/FormView'; import { ModeToggle, type Mode } from '../components/settings/ModeToggle'; const TAB_INDENT = ' '; +// Heuristic: `${VAR}` or `${VAR:default}` in the file means the operator is +// running with env-var substitution (overwhelmingly Docker / Kubernetes). +// We use this to gate the Docker-aware UX (the explanatory banner and the +// Effective-config tab) so non-container installs see the existing UI +// unchanged. Conservative on purpose — false negatives just keep the old +// behaviour. +const ENV_VAR_PATTERN = /\$\{[A-Za-z_][A-Za-z0-9_]*(?::[^}]*)?\}/; + export const SettingsPage = () => { const { t } = useTranslation(); const settingsSocket = useStore(state => state.settingsSocket); const settings = useStore(state => state.settings) ?? ''; + const resolved = useStore(state => state.resolved); + + const usesEnvVars = useMemo(() => ENV_VAR_PATTERN.test(settings), [settings]); const [mode, setMode] = useState('form'); const [exposeExperimental] = useState(false); + // The Effective tab is only meaningful when there is a `resolved` + // payload AND the file uses substitution. Falling back to Raw on + // either condition keeps the toggle honest if the user opens this + // page against an older server. + const canShowEffective = usesEnvVars && resolved != null; + useEffect(() => { + if (mode === 'effective' && !canShowEffective) setMode('raw'); + }, [mode, canShowEffective]); + // Tab in textarea inserts two spaces instead of moving focus; rAF restores caret position after React re-renders. const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key !== 'Tab') return; @@ -57,49 +77,80 @@ export const SettingsPage = () => { settingsSocket.emit('saveSettings', settings); }; + const effectiveJson = useMemo(() => { + if (resolved == null) return ''; + try { return JSON.stringify(resolved, null, 2); } catch { return ''; } + }, [resolved]); + return (

- + {usesEnvVars && ( +
+
+ )} - {mode === 'form' - ? setMode('raw')} /> - : ( -