mirror of
https://github.com/ether/etherpad-lite.git
synced 2026-05-05 04:06:37 +02:00
* fix(admin): restore i18n on /admin by copying locales to the right path The admin SPA fetches `/admin/locales/<lang>.json`. Building with vite-plugin-static-copy and `src: '../src/locales'` was placing the 115 core locale files at `src/templates/admin/src/locales/` (the plugin's `dirClean` strips a leading `../` but keeps the remaining parent path). The express admin handler 404'd those fetches, fell back to serving `index.html`, JSON.parse silently failed, and every `<Trans>` rendered its raw key — see #7586. Replace the plugin with a small inline build/dev plugin: at build time copy `src/locales/*.json` to `<outDir>/locales/`; in dev serve the same files via middleware so `vite dev` also works. Drop the now-unused `vite-plugin-static-copy` dependency. Add regression coverage that none of the existing admin specs had: - backend HTTP test for GET /admin/locales/{en,de}.json - Playwright admin i18n spec asserting translated <h1> renders for the default locale and for ?lng=de, plus a request-level check that the response is JSON, not the SPA fallback. Closes #7586 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(admin): bundle locales via import.meta.glob, drop copy plugin The first pass at #7586 replaced vite-plugin-static-copy with a custom build/dev plugin that copied src/locales/*.json into the admin output and served them in dev. That works, but the vite-plugin-static-copy README explicitly recommends the public directory or a JS import for this case, and the import path is strictly cleaner: no copy step, no /admin/locales/* express route, no SPA-fallback-shaped failure mode. Use import.meta.glob in admin/src/localization/i18n.ts so each language ships as its own hashed JSON chunk and is lazy-loaded on demand. The vite config goes back to just react + base + outDir. The plugin namespaces (e.g. ep_admin_pads) keep their existing admin/public/<ns>/<lang>.json layout. Tests: - Drop tests/backend/specs/adminLocales.ts — it asserted on a /admin/locales/<lang>.json route that this approach no longer uses; the regression mechanism it pinned doesn't exist anymore and the test required the admin frontend to be built before the backend test runs (which CI doesn't do). - Keep tests/frontend-new/admin-spec/admini18n.spec.ts (rendered <h1> in default and ?lng=de). Verified red→green: reverting just the loader to the pre-fix /admin/locales fetch makes both specs fail; restoring makes them pass. Also update pnpm-lock.yaml to drop the now-unused vite-plugin-static-copy entries — fixes ERR_PNPM_OUTDATED_LOCKFILE that was failing every CI install upfront. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ea57f764ad
commit
5fd600d608
@ -37,7 +37,6 @@
|
||||
"typescript": "^6.0.3",
|
||||
"vite": "npm:rolldown-vite@7.2.10",
|
||||
"vite-plugin-babel": "^1.6.0",
|
||||
"vite-plugin-static-copy": "^4.1.0",
|
||||
"zustand": "^5.0.12"
|
||||
},
|
||||
"overrides": {
|
||||
|
||||
@ -1,36 +1,47 @@
|
||||
import i18n from 'i18next'
|
||||
import {initReactI18next} from "react-i18next";
|
||||
import LanguageDetector from 'i18next-browser-languagedetector'
|
||||
import type {BackendModule} from 'i18next';
|
||||
|
||||
// Core translations live in /src/locales (shared with the pad UI). Letting
|
||||
// Vite resolve them via import.meta.glob means each language ships as its own
|
||||
// hashed JSON chunk, lazy-loaded on demand — no build-time copy step or
|
||||
// /admin/locales/* express route. Earlier setups copying files into the build
|
||||
// output were fragile (see https://github.com/ether/etherpad/issues/7586).
|
||||
const coreLocales = import.meta.glob<{default: Record<string, unknown>}>(
|
||||
'../../../src/locales/*.json');
|
||||
|
||||
import { BackendModule } from 'i18next';
|
||||
const coreLocaleByLang = (language: string) =>
|
||||
coreLocales[`../../../src/locales/${language}.json`];
|
||||
|
||||
const LazyImportPlugin: BackendModule = {
|
||||
type: 'backend',
|
||||
init: function () {
|
||||
},
|
||||
read: async function (language, namespace, callback) {
|
||||
|
||||
let baseURL = import.meta.env.BASE_URL
|
||||
if(namespace === "translation") {
|
||||
// If default we load the translation file
|
||||
baseURL+=`/locales/${language}.json`
|
||||
} else {
|
||||
// Else we load the former plugin translation file
|
||||
baseURL+=`/${namespace}/${language}.json`
|
||||
}
|
||||
|
||||
const localeJSON = await fetch(baseURL)
|
||||
let json;
|
||||
|
||||
try {
|
||||
json = JSON.parse(await localeJSON.text())
|
||||
} catch(e) {
|
||||
callback(new Error("Error loading"), null);
|
||||
if (namespace === 'translation') {
|
||||
const loader = coreLocaleByLang(language);
|
||||
if (!loader) {
|
||||
callback(new Error(`No core locale for "${language}"`), null);
|
||||
return;
|
||||
}
|
||||
const mod = await loader();
|
||||
callback(null, mod.default);
|
||||
return;
|
||||
}
|
||||
// Plugin namespaces (e.g. ep_admin_pads) are still served as static
|
||||
// assets from admin/public/<namespace>/<lang>.json.
|
||||
const baseURL = `${import.meta.env.BASE_URL}/${namespace}/${language}.json`;
|
||||
const res = await fetch(baseURL);
|
||||
if (!res.ok) {
|
||||
callback(new Error(`HTTP ${res.status} loading ${baseURL}`), null);
|
||||
return;
|
||||
}
|
||||
callback(null, await res.json());
|
||||
} catch (e) {
|
||||
callback(e instanceof Error ? e : new Error(String(e)), null);
|
||||
}
|
||||
|
||||
|
||||
callback(null, json);
|
||||
},
|
||||
|
||||
save: function () {
|
||||
|
||||
@ -1,36 +1,31 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import {viteStaticCopy} from "vite-plugin-static-copy";
|
||||
import react from '@vitejs/plugin-react';
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [viteStaticCopy({
|
||||
targets: [
|
||||
{
|
||||
src: '../src/locales',
|
||||
dest: ''
|
||||
}
|
||||
]
|
||||
}), react({
|
||||
babel: {
|
||||
plugins: ['babel-plugin-react-compiler'],
|
||||
}})],
|
||||
base: '/admin',
|
||||
build:{
|
||||
outDir: '../src/templates/admin',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
server:{
|
||||
plugins: [
|
||||
react({
|
||||
babel: {
|
||||
plugins: ['babel-plugin-react-compiler'],
|
||||
},
|
||||
}),
|
||||
],
|
||||
base: '/admin',
|
||||
build: {
|
||||
outDir: '../src/templates/admin',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/socket.io/*': {
|
||||
target: 'http://localhost:9001',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||
},
|
||||
'/admin-auth/': {
|
||||
target: 'http://localhost:9001',
|
||||
changeOrigin: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
'/admin-auth/': {
|
||||
target: 'http://localhost:9001',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
17
pnpm-lock.yaml
generated
17
pnpm-lock.yaml
generated
@ -115,9 +115,6 @@ importers:
|
||||
vite-plugin-babel:
|
||||
specifier: ^1.6.0
|
||||
version: 1.6.0(@babel/core@7.29.0)(rolldown-vite@7.2.10(@types/node@25.6.0)(tsx@4.21.0))
|
||||
vite-plugin-static-copy:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0(rolldown-vite@7.2.10(@types/node@25.6.0)(tsx@4.21.0))
|
||||
zustand:
|
||||
specifier: ^5.0.12
|
||||
version: 5.0.12(@types/react@19.2.14)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5))
|
||||
@ -5452,12 +5449,6 @@ packages:
|
||||
'@babel/core': ^7.0.0
|
||||
vite: '>=7.3.2'
|
||||
|
||||
vite-plugin-static-copy@4.1.0:
|
||||
resolution: {integrity: sha512-9XOarNV7LgP0KBB7AApxdgFikLXx3daZdqjC3AevYsL6MrUH62zphonLUs2a6LZc1HN1GY+vQdheZ8VVJb6dQQ==}
|
||||
engines: {node: ^22.0.0 || >=24.0.0}
|
||||
peerDependencies:
|
||||
vite: '>=7.3.2'
|
||||
|
||||
vite@8.0.8:
|
||||
resolution: {integrity: sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@ -11001,14 +10992,6 @@ snapshots:
|
||||
'@babel/core': 7.29.0
|
||||
vite: rolldown-vite@7.2.10(@types/node@25.6.0)(tsx@4.21.0)
|
||||
|
||||
vite-plugin-static-copy@4.1.0(rolldown-vite@7.2.10(@types/node@25.6.0)(tsx@4.21.0)):
|
||||
dependencies:
|
||||
chokidar: 3.6.0
|
||||
p-map: 7.0.4
|
||||
picocolors: 1.1.1
|
||||
tinyglobby: 0.2.16
|
||||
vite: rolldown-vite@7.2.10(@types/node@25.6.0)(tsx@4.21.0)
|
||||
|
||||
vite@8.0.8(@types/node@25.6.0)(esbuild@0.28.0)(tsx@4.21.0):
|
||||
dependencies:
|
||||
lightningcss: 1.32.0
|
||||
|
||||
34
src/tests/frontend-new/admin-spec/admini18n.spec.ts
Normal file
34
src/tests/frontend-new/admin-spec/admini18n.spec.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import {expect, test} from "@playwright/test";
|
||||
import {loginToAdmin} from "../helper/adminhelper";
|
||||
|
||||
// Regression coverage for https://github.com/ether/etherpad/issues/7586
|
||||
//
|
||||
// 2.7.0 shipped with the admin SPA's locale files copied to a wrong
|
||||
// build path; fetches for them silently fell back to the SPA's
|
||||
// index.html, JSON.parse failed, and every <Trans> rendered as its
|
||||
// raw key. None of the existing admin specs asserted on translated
|
||||
// strings, so the regression slipped through. We now bundle the
|
||||
// translations through Vite (import.meta.glob) — these tests pin the
|
||||
// rendered behaviour rather than the file path so any future
|
||||
// loading-mechanism change is covered too.
|
||||
test.beforeEach(async ({ page })=>{
|
||||
await loginToAdmin(page, 'admin', 'changeme1');
|
||||
});
|
||||
|
||||
test.describe('admin i18n', () => {
|
||||
test('renders translated text on /admin (default English)', async ({page}) => {
|
||||
await page.goto('http://localhost:9001/admin/');
|
||||
// HomePage renders <h1><Trans i18nKey="admin_plugins"/></h1>. If
|
||||
// translations fail to load, the visible text becomes the raw key
|
||||
// "admin_plugins". Asserting on the translated form catches that.
|
||||
await expect(page.locator('h1', { hasText: /^Plugin manager$/ }))
|
||||
.toBeVisible({ timeout: 30000 });
|
||||
await expect(page.getByText('admin_plugins', { exact: true })).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('switches language to German via ?lng=de', async ({page}) => {
|
||||
await page.goto('http://localhost:9001/admin/?lng=de');
|
||||
await expect(page.locator('h1', { hasText: /^Pluginverwaltung$/ }))
|
||||
.toBeVisible({ timeout: 30000 });
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user