Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
100 commits
Select commit Hold shift + click to select a range
92a9c24
i18n: mark writer interface and scaffold nine locale catalogues
callumalpass Oct 23, 2026
7baa839
i18n(writer): translate Portuguese entries 1-120
callumalpass Oct 23, 2026
c73f8bf
i18n(writer): translate Portuguese entries 121-232
callumalpass Oct 23, 2026
9d35027
i18n(writer): translate Portuguese entries 233-343
callumalpass Oct 23, 2026
7111810
i18n(writer): translate Spanish batch 1
callumalpass Oct 23, 2026
ebf2e8f
Translate first 120 Russian writer messages
callumalpass Oct 23, 2026
9d4e4a8
i18n(writer): translate first Chinese catalogue batch
callumalpass Oct 23, 2026
79f2130
i18n(writer): translate Chinese catalogue entries 121-231
callumalpass Oct 23, 2026
e04b454
i18n(writer): translate Spanish batch 2
callumalpass Oct 23, 2026
888de23
i18n(writer): translate Korean catalogue batch
callumalpass Oct 23, 2026
e6d10b9
i18n(writer): translate Chinese catalogue entries 343-545
callumalpass Oct 23, 2026
b2fbad5
i18n(writer): translate Spanish catalogue batch
callumalpass Oct 23, 2026
f60c7d1
i18n: adopt shared runtime and enforce real catalogue gates
callumalpass Oct 23, 2026
8f3623e
i18n(writer): finish Chinese catalogue translation
callumalpass Oct 23, 2026
1e30f37
i18n(writer): translate Spanish catalogue batch 2
callumalpass Oct 23, 2026
ee9237f
i18n: integrate first authored Korean catalogue slice
callumalpass Oct 23, 2026
5efcc56
i18n(writer): complete Spanish catalogue
callumalpass Oct 23, 2026
9c6eb43
i18n(writer): translate Korean catalogue batch
callumalpass Oct 23, 2026
af17a60
i18n(writer): translate Korean catalogue batch
callumalpass Oct 23, 2026
d6612bf
i18n(writer): translate Korean catalogue batch
callumalpass Oct 23, 2026
8981c4a
i18n(writer): translate Korean catalogue batch
callumalpass Oct 23, 2026
916db39
i18n: localize core guidance only at interface boundaries
callumalpass Oct 23, 2026
5fabde1
i18n(writer): translate Korean catalogue batch
callumalpass Oct 23, 2026
f9f0882
Complete Portuguese writer catalogue
callumalpass Oct 23, 2026
539b449
i18n: merge authored es catalogue slice
callumalpass Oct 23, 2026
8e278d2
i18n: merge authored zh catalogue slice
callumalpass Oct 23, 2026
7b6a1c7
i18n: compile accepted locale slices and remove obsolete messages
callumalpass Oct 23, 2026
1921d69
i18n(writer): translate German entries 301-450
callumalpass Oct 23, 2026
727475e
i18n(fr): translate writer catalogue entries 1-120
callumalpass Oct 23, 2026
c5cd5c4
i18n(fr): translate writer catalogue entries 121-240
callumalpass Oct 23, 2026
cdab1a1
i18n(fr): translate writer catalogue entries 241-360
callumalpass Oct 23, 2026
6e2c875
i18n(fr): translate writer catalogue entries 361-480
callumalpass Oct 23, 2026
6da4485
i18n(fr): translate writer catalogue entries 481-600
callumalpass Oct 23, 2026
b0c8876
i18n(fr): translate writer catalogue entries 601-686
callumalpass Oct 23, 2026
6f7a4ab
Translate Russian writer messages: comments to export and find/insert…
callumalpass Oct 23, 2026
16cb2e3
i18n: merge authored ko catalogue slice
callumalpass Oct 23, 2026
df733a5
i18n: merge authored pt catalogue slice
callumalpass Oct 23, 2026
ad5adef
i18n: compile Korean and Portuguese authored slices
callumalpass Oct 23, 2026
a6f3f52
i18n: review identical terms and exercise authored Chinese UI
callumalpass Oct 23, 2026
05dd8b9
i18n(writer): translate German entries 451-686
callumalpass Oct 23, 2026
d5eefe4
Translate Russian writer messages: Move/Open/Page through Resize batch
callumalpass Oct 23, 2026
8428247
i18n: refresh retained editor labels on language switches
callumalpass Oct 23, 2026
1dd0ad2
i18n: merge authored French catalogue
callumalpass Oct 23, 2026
614b6ec
i18n: compile authored French catalogue
callumalpass Oct 23, 2026
4d6c742
i18n(writer): translate German entries 1-150
callumalpass Oct 23, 2026
a33e331
i18n(writer): translate German entries 151-300
callumalpass Oct 23, 2026
0fa64d4
i18n: review unchanged French cognates and key legends
callumalpass Oct 23, 2026
671689e
Translate Russian writer messages: Resize through manuscript-export b…
callumalpass Oct 23, 2026
adf8b48
i18n: normalize merged catalogue source metadata with extraction
callumalpass Oct 23, 2026
74add9e
Translate Russian writer messages: final batch (manuscript errors thr…
callumalpass Oct 23, 2026
9d02846
docs: record exact translation integration and remaining delta
callumalpass Oct 23, 2026
5143744
i18n(writer): complete German strings and align formal register
callumalpass Oct 23, 2026
2c126d6
docs: explain shared interface language and release contract
callumalpass Oct 23, 2026
6c2c079
i18n(writer): translate French delta catalogue
callumalpass Oct 23, 2026
4499f99
i18n(writer): translate Spanish delta messages
callumalpass Oct 23, 2026
a023902
i18n: merge authored de catalogue
callumalpass Oct 23, 2026
5645451
i18n: merge authored es-v2 catalogue
callumalpass Oct 23, 2026
3afe4c7
i18n: merge authored fr-v2 catalogue
callumalpass Oct 23, 2026
dbfe266
i18n: compile German and refreshed Spanish/French catalogues
callumalpass Oct 23, 2026
9911807
i18n: review German invariant terms and completed locale metadata
callumalpass Oct 23, 2026
0f1c83b
i18n(writer): translate zh delta catalogue
callumalpass Oct 23, 2026
45597de
Polish Russian writer messages after full consistency read-through
callumalpass Oct 23, 2026
5f16c01
i18n: compose shared UI catalogues and adopt fixed kit CLI
callumalpass Oct 23, 2026
74e77ec
i18n: merge complete Chinese app catalogue delta
callumalpass Oct 23, 2026
2572a08
i18n: compile complete Chinese catalogue with shared UI composition
callumalpass Oct 23, 2026
1421305
Complete Writer Chinese delta catalogue
callumalpass Oct 23, 2026
0644e50
i18n: merge authored ru catalogue
callumalpass Oct 23, 2026
d2a5bba
i18n: merge authored zh-v2 catalogue
callumalpass Oct 23, 2026
2ef6906
i18n: compile Russian and refreshed Chinese catalogues
callumalpass Oct 23, 2026
a2ca953
i18n: reconcile Russian against current source identities
callumalpass Oct 23, 2026
2dcbc0d
i18n(writer): complete Korean delta
callumalpass Oct 23, 2026
8e18ff8
i18n(de): port writer translations to 7b6a1c7 catalogue
callumalpass Oct 23, 2026
93d0135
i18n(ru): port writer translations to 7b6a1c7 catalogue
callumalpass Oct 23, 2026
6b3f5e4
i18n: merge complete de-v2 catalogue delta
callumalpass Oct 23, 2026
1689279
i18n: merge complete ko-v2 catalogue delta
callumalpass Oct 23, 2026
92939d2
i18n: merge complete ru-v2 catalogue delta
callumalpass Oct 23, 2026
3694b3e
i18n: compile German Korean and Russian catalogue updates
callumalpass Oct 23, 2026
63eb3ac
i18n(de): translate writer delta
callumalpass Oct 23, 2026
8c73c6d
i18n(ru): translate writer catalogue delta
callumalpass Oct 23, 2026
8940103
docs: record composed catalogue acceptance and outstanding blockers
callumalpass Oct 23, 2026
b3d6fe4
i18n: merge latest de-v2 authored delta
callumalpass Oct 23, 2026
86355cd
i18n: merge latest ru-v2 authored delta
callumalpass Oct 23, 2026
da1ebb1
i18n: compile complete German and Russian app catalogues
callumalpass Oct 23, 2026
3895d1e
i18n: prune retired invariant IDs and record current completion
callumalpass Oct 23, 2026
56aad34
i18n(de): localize modifier-click shortcut
callumalpass Oct 23, 2026
1dcab86
i18n(writer): translate new Portuguese strings
callumalpass Oct 23, 2026
ba58994
fix(i18n): localize writer shortcut hint
callumalpass Oct 23, 2026
cd0bb24
i18n(ja): port writer translations onto 5f16c01
callumalpass Oct 23, 2026
1eb403e
i18n(ja): translate writer diagnostics and hints
callumalpass Oct 23, 2026
9e0d7db
Merge commit '56aad34' into feat/i18n
callumalpass Oct 23, 2026
045034e
Merge commit '1eb403e' into feat/i18n
callumalpass Oct 23, 2026
e189645
Merge commit '1dcab86' into feat/i18n
callumalpass Oct 23, 2026
7ee4b78
Merge commit 'ba58994' into feat/i18n
callumalpass Oct 23, 2026
45ff8b5
i18n: finish all eight app catalogues and check shared composition
callumalpass Oct 23, 2026
dc6f47f
i18n: adopt strict shared UI and normalize target plural categories
callumalpass Oct 23, 2026
d94f6a5
i18n: adopt latest shared kit composition and source checks
callumalpass Oct 23, 2026
d044829
docs: record latest kit source-guard integration blocker
callumalpass Oct 23, 2026
9c86d59
i18n: adopt field-aware kit and current shared UI; verify all gates
callumalpass Oct 23, 2026
b9b79e3
i18n: adopt final shared pins and avoid redundant chapter language re…
callumalpass Oct 23, 2026
b353683
fix(i18n): retain localized phrases across keyed chapter remounts
callumalpass Oct 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ jobs:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Validate authored catalogues and generated JSON
run: |
pnpm i18n:check
git diff --exit-code -- apps/writer/src/i18n/locales
- run: pnpm check
# Demo is development/LAB-only, so verify the production build separately.
- run: pnpm build
Expand Down Expand Up @@ -70,6 +74,7 @@ jobs:
result=0
pnpm --filter @mdbase-writer/app exec node scripts/browser-test.mjs || result=1
pnpm --filter @mdbase-writer/app test:browser:reliability || result=1
pnpm --filter @mdbase-writer/app test:browser:i18n || result=1
exit "$result"
- name: Verify private feedback with demo data and intercepted delivery
run: |
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,21 @@ privately as described in the [security policy](SECURITY.md).
For local setup, tests, deployment, and implementation details, see the
[development guide](docs/development.md) and [architecture notes](docs/architecture.md).

### Interface translations (integration branch)

Writer shares the `@mdbase-dev/i18n` Lingui runtime and strict catalogue checks with the
other mdbase apps. English loads synchronously; other interface languages are lazy chunks
from Writer's own deployment. The browser language is the default; an explicit choice is
remembered in this browser. This never changes manuscript text, frontmatter, citation
language or exported prose.

All eight target-language catalogues are merged and all nine locale controls are enabled
on this branch. Strict completeness and app/library composition checks run in normal checks
and builds. See [translation status and authoring](docs/translations.md).
The kit and shared UI are temporarily vendored for coordinated development; replace both
with their published npm versions at merge time. CI only validates local-agent-authored translations and never
calls a translation service.

## License

mdbase writer is available under the [MIT License](LICENSE). Bundled fonts, Typst packages, and
Expand Down
17 changes: 17 additions & 0 deletions apps/writer/i18n-policy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"allowIdentical": {
"de": ["GPQyjo","6stxj1","D2fBSU","WBbQ6j","YNmZG2","kjd02W","quZLv8","oRJEwT","oCl6qZ","YPtBzd","3c6Jwh","L0jxRL","x4F4rp","bvnb9m","ef0nW8","PKDyzC","4_lfk6","6y5Vqg","uBAxNB","kI1qVD","238qc6","yNCLLZ","rdU729","eavifN","yzF66j","6YtxFj","6Aih4U","D-NlUC","fUDM0u","ACjxWW","4RPJcT","nbrVo1"],
"fr": ["vZtY0S","GPQyjo","6stxj1","D2fBSU","WBbQ6j","YNmZG2","kjd02W","quZLv8","bvnb9m","PKDyzC","N8JYNj","f6e0Ry","nwJZJE","6y5Vqg","DoJzLz","mYGY3B","YUAoyO","kI1qVD","HhVDr2","_hpEcX","AnYKFm","1DBGsz","tdta9X","BhZ5PL","wdxz7K","sBJ5MF","9yoWSx","fUDM0u","ACjxWW","4RPJcT","1X0LyN"],
"es": ["GPQyjo","6stxj1","D2fBSU","WBbQ6j","YNmZG2","kjd02W","quZLv8","bvnb9m","ef0nW8","PKDyzC","uBAxNB","fUDM0u","ACjxWW","4RPJcT"],
"ja": ["GPQyjo","6stxj1","D2fBSU","WBbQ6j","YNmZG2","kjd02W","quZLv8","oRJEwT","oCl6qZ","YPtBzd","3c6Jwh","L0jxRL","x4F4rp","bvnb9m","ef0nW8","PKDyzC","4_lfk6"],
"ko": ["GPQyjo","6stxj1","D2fBSU","WBbQ6j","YNmZG2","kjd02W","quZLv8","oRJEwT","oCl6qZ","YPtBzd","3c6Jwh","L0jxRL","x4F4rp","bvnb9m","ef0nW8","PKDyzC","4_lfk6"],
"pt": ["vZtY0S","GPQyjo","6stxj1","D2fBSU","WBbQ6j","YNmZG2","kjd02W","quZLv8","oRJEwT","oCl6qZ","YPtBzd","3c6Jwh","L0jxRL","x4F4rp","bvnb9m","ef0nW8","PKDyzC","4_lfk6","uBAxNB","rdU729","eavifN","fUDM0u","ACjxWW"],
"ru": ["GPQyjo","6stxj1","D2fBSU","WBbQ6j","YNmZG2","kjd02W","quZLv8","oRJEwT","oCl6qZ","YPtBzd","3c6Jwh","L0jxRL","x4F4rp","bvnb9m","ef0nW8","PKDyzC","4_lfk6","4RPJcT"],
"zh": ["GPQyjo","6stxj1","D2fBSU","WBbQ6j","YNmZG2","kjd02W","quZLv8","oRJEwT","oCl6qZ","YPtBzd","3c6Jwh","L0jxRL","x4F4rp","bvnb9m","ef0nW8","PKDyzC","4_lfk6"]
},
"source": {
"include": ["src"],
"exclude": ["**/generated/**", "**/test/**"],
"brands": ["PDF", "Word", "Typst", "Pandoc", "{#fig-plan}"]
}
}
6 changes: 6 additions & 0 deletions apps/writer/lingui.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { i18nConfig } from "@mdbase-dev/i18n/config";

export default i18nConfig({
include: ["<rootDir>/src"],
exclude: ["**/*.test.*", "**/test/**"],
});
16 changes: 14 additions & 2 deletions apps/writer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
"type": "module",
"scripts": {
"icons": "node scripts/write-pwa-icons.mjs",
"build": "pnpm manifest && pnpm manifest:validate && vite build",
"i18n:extract": "mdbase-i18n extract",
"i18n:compile": "mdbase-i18n compile",
"i18n:source": "mdbase-i18n source",
"i18n:check": "mdbase-i18n check --compose 'node_modules/@mdbase-dev/ui/dist/locales/{locale}/messages.json'",
"build": "pnpm i18n:check && pnpm manifest && pnpm manifest:validate && vite build",
"build:lab": "MDBASE_WRITER_ORIGIN=https://lab.mdbase-writer.pages.dev VITE_MDBASE_ENV=lab pnpm build",
"build:feedback-test": "VITE_WRITER_DEMO=1 MDBASE_WRITER_ORIGIN=https://lab.mdbase-writer.pages.dev VITE_MDBASE_ENV=lab VITE_MDBASE_FEEDBACK_URL=http://127.0.0.1:8892/v1/feedback pnpm build",
"build:editing-test": "VITE_WRITER_DEMO=1 pnpm build",
Expand All @@ -14,6 +18,7 @@
"manifest:validate": "node scripts/validate-manifest.mjs public/.well-known/mdbase-app.json",
"preview": "vite preview --host 127.0.0.1",
"test": "pnpm manifest && node --test scripts/*.test.mjs && vitest run",
"test:browser:i18n": "node scripts/i18n-browser-test.mjs",
"test:browser": "node scripts/browser-test.mjs && node scripts/browser-reliability-test.mjs",
"test:feedback": "node scripts/feedback-browser-test.mjs",
"test:browser:reliability": "node scripts/browser-reliability-test.mjs",
Expand All @@ -35,10 +40,13 @@
"@codemirror/view": "^6.43.8",
"@lezer/highlight": "^1.2.3",
"@lezer/markdown": "^1.4.3",
"@lingui/core": "6.9.0",
"@lingui/react": "6.9.0",
"@mdbase-dev/connect": "file:../../vendor/connect-beta125-legacy-query-2a89e9ef.tgz",
"@mdbase-dev/connect-protocol": "file:vendor/mdbase-dev-connect-protocol-0.1.0-beta.129-c280cd194ffd.tgz",
"@mdbase-dev/i18n": "file:../../vendor/mdbase-dev-i18n-0.1.0-beta.141-a36fa009.tgz",
"@mdbase-dev/sdk": "file:../../vendor/sdk-eaa4e697-production-beta141.tgz",
"@mdbase-dev/ui": "file:../../vendor/mdbase-dev-ui-dev-9553ad66.tgz",
"@mdbase-dev/ui": "file:../../vendor/mdbase-dev-ui-0.1.0-beta.141-a36fa009.tgz",
"@mdbase-writer/core": "workspace:*",
"@myriaddreamin/typst-ts-renderer": "0.7.0",
"@myriaddreamin/typst-ts-web-compiler": "0.7.0",
Expand All @@ -50,12 +58,16 @@
},
"devDependencies": {
"@callumalpass/mdbase": "0.3.0-rc.5",
"@lingui/cli": "6.9.0",
"@lingui/format-po": "6.9.0",
"@lingui/vite-plugin": "6.9.0",
"@mdbase-dev/connect-dev": "0.1.0-beta.125",
"@mdbase-dev/connect-testing": "0.1.0-beta.125",
"@testing-library/react": "16.3.2",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"babel-plugin-macros": "3.1.0",
"jsdom": "29.1.1",
"playwright": "1.61.1",
"vite": "^8.3.1",
Expand Down
102 changes: 102 additions & 0 deletions apps/writer/scripts/i18n-browser-test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import assert from "node:assert/strict";
import { mkdir } from "node:fs/promises";
import { chromium } from "playwright";

// Disposable in-memory demo only. Block all non-local authority/compiler traffic.
const base = process.env.BASE ?? "http://127.0.0.1:19400/";
const origin = new URL(base).origin;
assert.ok(["127.0.0.1", "localhost"].includes(new URL(base).hostname));
await mkdir("out/i18n", { recursive: true });
const browser = await chromium.launch({ headless: true });
try {
const context = await browser.newContext({ viewport: { width: 1440, height: 960 }, locale: "en-US", recordVideo: { dir: "out/i18n/recordings", size: { width: 1440, height: 960 } } });
const page = await context.newPage();
const errors = [];
page.on("pageerror", error => errors.push(String(error)));
await page.route("**/*", route => new URL(route.request().url()).origin === origin ? route.continue() : route.abort());
await page.goto(new URL("?demo", base).href);
const picker = page.locator(".interface-language select:visible");
await picker.waitFor();
await page.locator(".manuscript-row").filter({ hasText: "Patient Observation" }).waitFor();
assert.equal(await page.locator("html").getAttribute("lang"), "en");

await picker.selectOption("ja");
await page.waitForFunction(() => document.documentElement.lang === "ja");
assert.equal(await page.evaluate(() => localStorage.getItem("mdbase-writer:interface-locale")), "ja");
await page.screenshot({ path: "out/i18n/japanese-home.png", fullPage: true });
await page.locator(".manuscript-row").filter({ hasText: "Patient Observation" }).click();
const editor = page.locator(".cm-content");
await editor.waitFor();
const manuscriptSource = () => page.evaluate(() => {
const workspace = window.writer.workspace;
return workspace.getSnapshot().records.get(workspace.main).snapshot.body;
});
const before = await manuscriptSource();
await editor.click();
await page.keyboard.press("Control+f");
await page.locator(".cm-search").waitFor();
await page.waitForFunction(() => document.querySelector('.cm-search input[name="search"]')?.getAttribute("placeholder") === "検索");
await page.screenshot({ path: "out/i18n/japanese-editor.png", fullPage: true });

await page.keyboard.press("Escape");
await page.keyboard.press("Control+,");
await page.locator(".settings-sheet").waitFor();
await picker.selectOption("de");
await page.waitForFunction(() => document.documentElement.lang === "de");
assert.equal(await manuscriptSource(), before, "language switching must not edit the manuscript");
await page.screenshot({ path: "out/i18n/german-settings.png", fullPage: true });
await page.reload();
await page.waitForFunction(() => document.documentElement.lang === "de");
await page.locator(".cm-content").waitFor();
await page.keyboard.press("Control+,");
await page.locator(".settings-sheet").waitFor();
assert.equal(await picker.inputValue(), "de");
await page.setViewportSize({ width: 390, height: 844 });
await picker.scrollIntoViewIfNeeded();
await page.screenshot({ path: "out/i18n/narrow-language-control.png", fullPage: true });
const bounds = await picker.boundingBox();
assert.ok(bounds && bounds.x >= 0 && bounds.x + bounds.width <= 390 && bounds.y >= 0 && bounds.y + bounds.height <= 844, "language setting remains reachable on a narrow viewport");
// Chinese is an authored product catalogue, not just a translated editor phrase set.
await picker.selectOption("zh");
await page.waitForFunction(() => document.documentElement.lang === "zh");
assert.match(await picker.getAttribute("aria-label"), /[\u3400-\u9fff]/);
assert.equal(await manuscriptSource(), before);
await page.screenshot({ path: "out/i18n/chinese-settings-narrow.png", fullPage: true });
// Menu descriptors must not cache labels from the previous interface language.
await page.keyboard.press("Escape");
await editor.click();
await page.keyboard.press("Control+End");
await page.keyboard.type("\n/");
await page.locator(".cm-tooltip-autocomplete").waitFor();
const insertLabels = await page.locator(".cm-tooltip-autocomplete li").allTextContents();
assert.ok(insertLabels.some(label => label.includes("引文")), "slash commands update after a locale switch");
await page.keyboard.press("Escape");
await page.keyboard.press("Backspace");
await page.keyboard.press("Backspace");
assert.equal(await manuscriptSource(), before, "the test restores its disposable slash-menu probe");
assert.deepEqual(errors, []);
await context.close();

// A fresh context avoids an already cached target import.
const failure = await browser.newContext({ locale: "en-US" });
const failedPage = await failure.newPage();
await failedPage.route("**/*", route => {
const url = new URL(route.request().url());
if (url.origin !== origin || /\/locales\/de\/messages\.json/.test(url.pathname)) return route.abort();
return route.continue();
});
await failedPage.goto(new URL("?demo", base).href);
const failedPicker = failedPage.locator(".interface-language select");
await failedPicker.waitFor();
await failedPicker.selectOption("de");
await failedPage.locator(".interface-language-error").waitFor();
assert.equal(await failedPage.locator("html").getAttribute("lang"), "en");
assert.notEqual(await failedPage.evaluate(() => localStorage.getItem("mdbase-writer:interface-locale")), "de");
await failedPicker.selectOption("ja");
await failedPage.waitForFunction(() => document.documentElement.lang === "ja");
assert.equal(await failedPage.locator(".interface-language-error").count(), 0, "a successful retry clears the prior error");
await failure.close();
console.log("i18n browser: synchronous English, selection/persistence, Japanese CodeMirror, unchanged manuscript, narrow picker, authored Chinese settings and chunk-failure retention passed. Authored Japanese and German catalogues activated; screenshots captured for wide/narrow layout review.");
} finally {
await browser.close();
}
9 changes: 9 additions & 0 deletions apps/writer/scripts/i18n-cli.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { test } from "node:test";

test("the packed i18n CLI executes and rejects invalid commands rather than silently succeeding", () => {
const result = spawnSync("pnpm", ["exec", "mdbase-i18n", "definitely-not-a-command"], { encoding: "utf8" });
assert.equal(result.status, 1);
assert.match(result.stderr, /Usage: mdbase-i18n/);
});
10 changes: 8 additions & 2 deletions apps/writer/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { t } from "@lingui/core/macro";
import { useLingui } from "@lingui/react";
import { lazy, Suspense, useEffect, useMemo, useState } from "react";
import { AppSwitcher } from "@mdbase-dev/ui/app-switcher";
import { FeedbackButton } from "@mdbase-dev/ui/feedback";
import { loadThemePreference, saveThemePreference, type ThemePreference } from "@mdbase-dev/ui/theme";
import { ThemeSelect } from "@mdbase-dev/ui/theme-select";
import { InterfaceLanguage } from "./i18n/provider.js";
import { OpeningScreen } from "@mdbase-dev/ui/screens";
import { appUrls } from "./apps.js";
import { ThemeChoice, TopbarSlot } from "./ui/topbar.js";
Expand All @@ -12,18 +15,21 @@ import { SdkWriterRoot, type SdkWriterRootProps } from "./ui/SdkWriterRoot.js";
// Production keeps its existing default; switching/bootstrap belongs to owner GO.
const LegacyApp = lazy(async () => ({ default: (await import("./LegacyApp.js")).LegacyApp }));
export function App(props: Partial<SdkWriterRootProps> = {}) {
useLingui();
return props.sdk ? <NativeApp {...props} sdk={props.sdk} /> :
<Suspense fallback={<OpeningScreen app="writer" title="Opening mdbase writer" />}><LegacyApp /></Suspense>;
<Suspense fallback={<OpeningScreen app="writer" title={t`Opening mdbase writer`} />}><LegacyApp /></Suspense>;
}
function NativeApp(props: SdkWriterRootProps) {
useLingui();
const [theme, setTheme] = useState<ThemePreference>(() => loadThemePreference());
const [slot, setSlot] = useState<HTMLElement | null>(null);
useEffect(() => saveThemePreference(theme), [theme]);
const themeChoice = useMemo(() => ({ theme, setTheme }), [theme]);
return <ThemeChoice.Provider value={themeChoice}><TopbarSlot.Provider value={slot}>
<div className="app"><header className="topbar">
<AppSwitcher current="writer" urls={appUrls} /><div className="topbar-slot" ref={setSlot} />
<ThemeSelect className="topbar-theme" value={theme} onChange={setTheme} /><FeedbackButton />
<InterfaceLanguage />
<ThemeSelect className="topbar-theme" value={theme} onChange={setTheme} /><FeedbackButton />
</header><SdkWriterRoot {...props} /></div>
</TopbarSlot.Provider></ThemeChoice.Provider>;
}
Loading