mirror of
https://github.com/hansjone/mind-map.git
synced 2026-10-11 22:50:47 +08:00
feat: sync canvas toolbar locale with DSH zh/en
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
288ebf4c90
commit
d95f033d1f
6 changed files with 322 additions and 46 deletions
|
|
@ -1,8 +1,6 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
/**
|
/**
|
||||||
* Fail the build if canvas UI CJK is corrupted or missing.
|
* Fail the build if canvas UI CJK/i18n tables are corrupted or incomplete.
|
||||||
* Root cause we guard against: Windows tools rewriting TopologyCanvas.tsx
|
|
||||||
* with "?" for multibyte UTF-8, and Latin-1 middot 0xb7 breaking UTF-8.
|
|
||||||
*/
|
*/
|
||||||
import { readFileSync, existsSync } from "node:fs";
|
import { readFileSync, existsSync } from "node:fs";
|
||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
|
|
@ -26,59 +24,68 @@ function mustUtf8(rel) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const requiredZh = ["选择", "平移", "连线", "单点", "整枝", "气泡", "卡片", "撤销", "未命名"];
|
||||||
|
const requiredEn = [
|
||||||
|
"Select",
|
||||||
|
"Pan",
|
||||||
|
"Link",
|
||||||
|
"Node",
|
||||||
|
"Branch",
|
||||||
|
"Bubble",
|
||||||
|
"Card",
|
||||||
|
"Undo",
|
||||||
|
"Untitled",
|
||||||
|
];
|
||||||
|
|
||||||
const labels = mustUtf8("src/canvas-ui-text.ts");
|
const labels = mustUtf8("src/canvas-ui-text.ts");
|
||||||
if (labels) {
|
if (labels) {
|
||||||
const required = [
|
for (const s of requiredZh) {
|
||||||
"选择",
|
if (!labels.includes(s)) errors.push(`canvas-ui-text.ts missing zh: ${s}`);
|
||||||
"平移",
|
}
|
||||||
"连线",
|
for (const s of requiredEn) {
|
||||||
"单点",
|
if (!labels.includes(`"${s}"`) && !labels.includes(`'${s}'`)) {
|
||||||
"整枝",
|
errors.push(`canvas-ui-text.ts missing en: ${s}`);
|
||||||
"气泡",
|
}
|
||||||
"卡片",
|
}
|
||||||
"撤销",
|
if (!labels.includes("const zh:") && !labels.includes("const zh =")) {
|
||||||
"未命名",
|
errors.push("canvas-ui-text.ts must define zh table");
|
||||||
];
|
}
|
||||||
for (const s of required) {
|
if (!labels.includes("const en:") && !labels.includes("const en =")) {
|
||||||
if (!labels.includes(s)) errors.push(`canvas-ui-text.ts missing: ${s}`);
|
errors.push("canvas-ui-text.ts must define en table");
|
||||||
}
|
}
|
||||||
if (/^\s*\w+:\s*["']\?{2,}["']/m.test(labels)) {
|
if (/^\s*\w+:\s*["']\?{2,}["']/m.test(labels)) {
|
||||||
errors.push("canvas-ui-text.ts contains corrupted ?? string literals");
|
errors.push("canvas-ui-text.ts contains corrupted ?? string literals");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hook = mustUtf8("src/useCanvasUi.ts");
|
||||||
|
if (hook && !hook.includes("dsh-mind-map:locale")) {
|
||||||
|
errors.push("useCanvasUi.ts must listen for dsh-mind-map:locale");
|
||||||
|
}
|
||||||
|
|
||||||
const canvas = mustUtf8("src/TopologyCanvas.tsx");
|
const canvas = mustUtf8("src/TopologyCanvas.tsx");
|
||||||
if (canvas) {
|
if (canvas) {
|
||||||
// Toolbar / controls must use canvasUi — no inline ?? placeholders.
|
|
||||||
const badUi = [
|
const badUi = [
|
||||||
...canvas.matchAll(/\["select",\s*"(\?+)"/g),
|
...canvas.matchAll(/\["select",\s*"(\?+)"/g),
|
||||||
...canvas.matchAll(/\["pan",\s*"(\?+)"/g),
|
...canvas.matchAll(/\["pan",\s*"(\?+)"/g),
|
||||||
...canvas.matchAll(/\["link",\s*"(\?+)"/g),
|
...canvas.matchAll(/\["link",\s*"(\?+)"/g),
|
||||||
];
|
];
|
||||||
if (badUi.length) {
|
if (badUi.length) {
|
||||||
errors.push(
|
errors.push("TopologyCanvas.tsx has corrupted tool labels (??)");
|
||||||
"TopologyCanvas.tsx has corrupted tool labels (??). Use canvasUi from canvas-ui-text.ts",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (!canvas.includes("canvas-ui-text") && !canvas.includes("canvasUi")) {
|
if (!canvas.includes("useCanvasUi")) {
|
||||||
errors.push("TopologyCanvas.tsx must import canvasUi from canvas-ui-text.ts");
|
errors.push("TopologyCanvas.tsx must use useCanvasUi()");
|
||||||
}
|
}
|
||||||
// Detect classic corruption: many consecutive ? inside quotes in JSX title/aria
|
|
||||||
const titleQ = [...canvas.matchAll(/title=\{?["'](\?{2,})["']\}?/g)];
|
const titleQ = [...canvas.matchAll(/title=\{?["'](\?{2,})["']\}?/g)];
|
||||||
if (titleQ.length >= 3) {
|
if (titleQ.length >= 3) {
|
||||||
errors.push(
|
errors.push(`TopologyCanvas.tsx CJK-corrupted (${titleQ.length} title="??")`);
|
||||||
`TopologyCanvas.tsx looks CJK-corrupted (${titleQ.length} title="??" hits). Restore via canvasUi.`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errors.length) {
|
if (errors.length) {
|
||||||
console.error("[check-canvas-cjk] FAILED:");
|
console.error("[check-canvas-cjk] FAILED:");
|
||||||
for (const e of errors) console.error(" -", e);
|
for (const e of errors) console.error(" -", e);
|
||||||
console.error(
|
|
||||||
"Fix: edit src/canvas-ui-text.ts (UTF-8 only). Never pipe CJK through PowerShell Set-Content.",
|
|
||||||
);
|
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[check-canvas-cjk] OK");
|
console.log("[check-canvas-cjk] OK (zh/en i18n)");
|
||||||
|
|
|
||||||
7
apps/web/scripts/verify-dist-i18n.py
Normal file
7
apps/web/scripts/verify-dist-i18n.py
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
js = next((Path(__file__).resolve().parents[1] / "dist" / "assets").glob("index-*.js")).read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
print({k: (k in js) for k in ["选择", "Select", "平移", "Pan", "连线", "Link", "dsh-mind-map:locale"]})
|
||||||
|
|
@ -24,7 +24,7 @@ import {
|
||||||
import { drawPropSheet, NodePropOverlay } from "./node-props";
|
import { drawPropSheet, NodePropOverlay } from "./node-props";
|
||||||
import { branchPalette, readSystemTheme } from "./system-theme";
|
import { branchPalette, readSystemTheme } from "./system-theme";
|
||||||
import { useAppStore } from "./store";
|
import { useAppStore } from "./store";
|
||||||
import { canvasUi as U } from "./canvas-ui-text";
|
import { useCanvasUi } from "./useCanvasUi";
|
||||||
|
|
||||||
const NODE_W = 200;
|
const NODE_W = 200;
|
||||||
|
|
||||||
|
|
@ -37,6 +37,7 @@ const MINI_W = 168;
|
||||||
const MINI_H = 118;
|
const MINI_H = 118;
|
||||||
|
|
||||||
export function TopologyCanvas() {
|
export function TopologyCanvas() {
|
||||||
|
const { U } = useCanvasUi();
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
const miniRef = useRef<HTMLCanvasElement>(null);
|
const miniRef = useRef<HTMLCanvasElement>(null);
|
||||||
const hostRef = useRef<HTMLDivElement>(null);
|
const hostRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
@ -676,6 +677,7 @@ export function TopologyCanvas() {
|
||||||
canvas?.prefs.showRelationEdges,
|
canvas?.prefs.showRelationEdges,
|
||||||
canvas?.prefs.nodeBadges,
|
canvas?.prefs.nodeBadges,
|
||||||
imageTick,
|
imageTick,
|
||||||
|
U,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const drawMini = useCallback(() => {
|
const drawMini = useCallback(() => {
|
||||||
|
|
@ -888,6 +890,7 @@ export function TopologyCanvas() {
|
||||||
fitView,
|
fitView,
|
||||||
setZoomAroundCenter,
|
setZoomAroundCenter,
|
||||||
viewport.zoom,
|
viewport.zoom,
|
||||||
|
U,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const cursor =
|
const cursor =
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,72 @@
|
||||||
/**
|
/**
|
||||||
* Canvas UI copy (zh). Keep ALL user-facing CJK here.
|
* Canvas UI copy (zh / en). Keep ALL user-facing strings here.
|
||||||
* TopologyCanvas.tsx must not embed Chinese literals — they get corrupted
|
* TopologyCanvas must not embed literals — Windows tools corrupt CJK in large files.
|
||||||
* by Windows/PowerShell default encodings (question-mark placeholders / invalid 0xb7 bytes).
|
|
||||||
*/
|
*/
|
||||||
export const canvasUi = {
|
|
||||||
|
export type CanvasLocale = "zh" | "en";
|
||||||
|
|
||||||
|
export type CanvasUi = {
|
||||||
|
unnamed: string;
|
||||||
|
newNode: string;
|
||||||
|
leftChild: string;
|
||||||
|
rightChild: string;
|
||||||
|
sibling: string;
|
||||||
|
relationLabel: string;
|
||||||
|
toolbarAria: string;
|
||||||
|
controlsAria: string;
|
||||||
|
backRosterTitle: string;
|
||||||
|
backRoster: string;
|
||||||
|
toolSelect: string;
|
||||||
|
toolPan: string;
|
||||||
|
toolLink: string;
|
||||||
|
dragNode: string;
|
||||||
|
dragBranch: string;
|
||||||
|
dragNodeTitle: string;
|
||||||
|
dragBranchTitle: string;
|
||||||
|
bubble: string;
|
||||||
|
card: string;
|
||||||
|
bubbleTitle: string;
|
||||||
|
cardTitle: string;
|
||||||
|
unpin: string;
|
||||||
|
unpinTitle: string;
|
||||||
|
undo: string;
|
||||||
|
fullscreen: string;
|
||||||
|
exitFullscreen: string;
|
||||||
|
zoomIn: string;
|
||||||
|
zoomInTitle: string;
|
||||||
|
zoomOut: string;
|
||||||
|
zoomOutTitle: string;
|
||||||
|
fit: string;
|
||||||
|
fitTitle: string;
|
||||||
|
lock: string;
|
||||||
|
unlock: string;
|
||||||
|
lockTitle: string;
|
||||||
|
unlockTitle: string;
|
||||||
|
addLeftChildTitle: string;
|
||||||
|
addRightChildTitle: string;
|
||||||
|
hintLocked: string;
|
||||||
|
hintNodeDrag: string;
|
||||||
|
hintBranchDrag: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const zh: CanvasUi = {
|
||||||
unnamed: "未命名",
|
unnamed: "未命名",
|
||||||
newNode: "新节点",
|
newNode: "新节点",
|
||||||
leftChild: "左侧子主题",
|
leftChild: "左侧子主题",
|
||||||
rightChild: "右侧子主题",
|
rightChild: "右侧子主题",
|
||||||
sibling: "兄弟节点",
|
sibling: "兄弟节点",
|
||||||
relationLabel: "关联",
|
relationLabel: "关联",
|
||||||
|
|
||||||
toolbarAria: "画布工具",
|
toolbarAria: "画布工具",
|
||||||
controlsAria: "视图控制",
|
controlsAria: "视图控制",
|
||||||
|
|
||||||
backRosterTitle: "返回画布列表",
|
backRosterTitle: "返回画布列表",
|
||||||
backRoster: "返回",
|
backRoster: "返回",
|
||||||
|
|
||||||
toolSelect: "选择",
|
toolSelect: "选择",
|
||||||
toolPan: "平移",
|
toolPan: "平移",
|
||||||
toolLink: "连线",
|
toolLink: "连线",
|
||||||
|
|
||||||
dragNode: "单点",
|
dragNode: "单点",
|
||||||
dragBranch: "整枝",
|
dragBranch: "整枝",
|
||||||
dragNodeTitle: "单点拖拽:只移动当前节点 (N);按住 Shift 也可临时单点",
|
dragNodeTitle: "单点拖拽:只移动当前节点 (N);按住 Shift 也可临时单点",
|
||||||
dragBranchTitle: "整枝拖拽:节点与子孙一起移动 (B)",
|
dragBranchTitle: "整枝拖拽:节点与子孙一起移动 (B)",
|
||||||
|
|
||||||
bubble: "气泡",
|
bubble: "气泡",
|
||||||
card: "卡片",
|
card: "卡片",
|
||||||
bubbleTitle: "气泡视图:精简标题",
|
bubbleTitle: "气泡视图:精简标题",
|
||||||
|
|
@ -33,7 +74,6 @@ export const canvasUi = {
|
||||||
unpin: "解钉",
|
unpin: "解钉",
|
||||||
unpinTitle: "取消钉住:让选中节点重新参与自动布局",
|
unpinTitle: "取消钉住:让选中节点重新参与自动布局",
|
||||||
undo: "撤销",
|
undo: "撤销",
|
||||||
|
|
||||||
fullscreen: "全屏",
|
fullscreen: "全屏",
|
||||||
exitFullscreen: "退出全屏",
|
exitFullscreen: "退出全屏",
|
||||||
zoomIn: "放大",
|
zoomIn: "放大",
|
||||||
|
|
@ -46,13 +86,79 @@ export const canvasUi = {
|
||||||
unlock: "解锁",
|
unlock: "解锁",
|
||||||
lockTitle: "锁定画布 (L)",
|
lockTitle: "锁定画布 (L)",
|
||||||
unlockTitle: "解锁画布 (L)",
|
unlockTitle: "解锁画布 (L)",
|
||||||
|
|
||||||
addLeftChildTitle: "添加左侧子主题",
|
addLeftChildTitle: "添加左侧子主题",
|
||||||
addRightChildTitle: "添加右侧子主题",
|
addRightChildTitle: "添加右侧子主题",
|
||||||
|
|
||||||
hintLocked: "已锁定 · 仅浏览/缩放/平移",
|
hintLocked: "已锁定 · 仅浏览/缩放/平移",
|
||||||
hintNodeDrag: "单点拖 · 只移动当前节点 · Shift 临时单点 · Esc 取消",
|
hintNodeDrag: "单点拖 · 只移动当前节点 · Shift 临时单点 · Esc 取消",
|
||||||
hintBranchDrag: "整枝拖 · 空白钉住 · 落到节点换父 · N 单点 · Esc 取消",
|
hintBranchDrag: "整枝拖 · 空白钉住 · 落到节点换父 · N 单点 · Esc 取消",
|
||||||
} as const;
|
};
|
||||||
|
|
||||||
export type CanvasUiKey = keyof typeof canvasUi;
|
const en: CanvasUi = {
|
||||||
|
unnamed: "Untitled",
|
||||||
|
newNode: "New node",
|
||||||
|
leftChild: "Left child",
|
||||||
|
rightChild: "Right child",
|
||||||
|
sibling: "Sibling",
|
||||||
|
relationLabel: "Link",
|
||||||
|
toolbarAria: "Canvas tools",
|
||||||
|
controlsAria: "View controls",
|
||||||
|
backRosterTitle: "Back to canvas list",
|
||||||
|
backRoster: "Back",
|
||||||
|
toolSelect: "Select",
|
||||||
|
toolPan: "Pan",
|
||||||
|
toolLink: "Link",
|
||||||
|
dragNode: "Node",
|
||||||
|
dragBranch: "Branch",
|
||||||
|
dragNodeTitle: "Move node only (N); hold Shift for temporary node-drag",
|
||||||
|
dragBranchTitle: "Move node with descendants (B)",
|
||||||
|
bubble: "Bubble",
|
||||||
|
card: "Card",
|
||||||
|
bubbleTitle: "Bubble view: title only",
|
||||||
|
cardTitle: "Card view: property sheet",
|
||||||
|
unpin: "Unpin",
|
||||||
|
unpinTitle: "Unpin selected node for auto layout",
|
||||||
|
undo: "Undo",
|
||||||
|
fullscreen: "Fullscreen",
|
||||||
|
exitFullscreen: "Exit fullscreen",
|
||||||
|
zoomIn: "Zoom in",
|
||||||
|
zoomInTitle: "Zoom in (+)",
|
||||||
|
zoomOut: "Zoom out",
|
||||||
|
zoomOutTitle: "Zoom out (-)",
|
||||||
|
fit: "Fit",
|
||||||
|
fitTitle: "Fit view (F)",
|
||||||
|
lock: "Lock",
|
||||||
|
unlock: "Unlock",
|
||||||
|
lockTitle: "Lock canvas (L)",
|
||||||
|
unlockTitle: "Unlock canvas (L)",
|
||||||
|
addLeftChildTitle: "Add left child",
|
||||||
|
addRightChildTitle: "Add right child",
|
||||||
|
hintLocked: "Locked · browse / zoom / pan only",
|
||||||
|
hintNodeDrag: "Node drag · Shift temp node · Esc cancel",
|
||||||
|
hintBranchDrag: "Branch drag · pin on blank · drop to reparent · N node · Esc cancel",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const canvasUiTables = { zh, en } as const;
|
||||||
|
|
||||||
|
export function normalizeCanvasLocale(raw: unknown): CanvasLocale {
|
||||||
|
const s = String(raw ?? "")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
if (s.startsWith("zh")) return "zh";
|
||||||
|
if (!s) {
|
||||||
|
// Standalone / no hint: prefer browser language
|
||||||
|
if (typeof navigator !== "undefined") {
|
||||||
|
const nav = String(navigator.language || "").toLowerCase();
|
||||||
|
if (nav.startsWith("zh")) return "zh";
|
||||||
|
}
|
||||||
|
return "en";
|
||||||
|
}
|
||||||
|
return "en";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCanvasUi(locale: CanvasLocale | string): CanvasUi {
|
||||||
|
const loc = normalizeCanvasLocale(locale);
|
||||||
|
return canvasUiTables[loc];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @deprecated Prefer getCanvasUi(locale) — kept so old imports fail loudly in check. */
|
||||||
|
export const canvasUi = zh;
|
||||||
|
|
|
||||||
81
apps/web/src/useCanvasUi.ts
Normal file
81
apps/web/src/useCanvasUi.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import {
|
||||||
|
getCanvasUi,
|
||||||
|
normalizeCanvasLocale,
|
||||||
|
type CanvasLocale,
|
||||||
|
type CanvasUi,
|
||||||
|
} from "./canvas-ui-text";
|
||||||
|
|
||||||
|
const MSG_TYPE = "dsh-mind-map:locale";
|
||||||
|
|
||||||
|
function readLocaleFromUrl(): CanvasLocale | null {
|
||||||
|
try {
|
||||||
|
const q = new URLSearchParams(window.location.search);
|
||||||
|
const raw = q.get("lang") || q.get("locale");
|
||||||
|
if (!raw) return null;
|
||||||
|
return normalizeCanvasLocale(raw);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readLocaleFromDocument(): CanvasLocale | null {
|
||||||
|
try {
|
||||||
|
const raw = document.documentElement?.lang;
|
||||||
|
if (!raw) return null;
|
||||||
|
return normalizeCanvasLocale(raw);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initialLocale(): CanvasLocale {
|
||||||
|
return (
|
||||||
|
readLocaleFromUrl() ||
|
||||||
|
readLocaleFromDocument() ||
|
||||||
|
normalizeCanvasLocale(navigator.language)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Follow DSH host language: ?lang= on embed URL, then postMessage, then <html lang>.
|
||||||
|
*/
|
||||||
|
export function useCanvasUi(): { locale: CanvasLocale; U: CanvasUi } {
|
||||||
|
const [locale, setLocale] = useState<CanvasLocale>(initialLocale);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const apply = (raw: unknown) => {
|
||||||
|
const next = normalizeCanvasLocale(raw);
|
||||||
|
setLocale((prev) => (prev === next ? prev : next));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMessage = (ev: MessageEvent) => {
|
||||||
|
const data = ev?.data;
|
||||||
|
if (!data || typeof data !== "object") return;
|
||||||
|
if (data.type !== MSG_TYPE) return;
|
||||||
|
apply(data.lang ?? data.locale);
|
||||||
|
};
|
||||||
|
window.addEventListener("message", onMessage);
|
||||||
|
|
||||||
|
const mo = new MutationObserver(() => {
|
||||||
|
const fromDoc = readLocaleFromDocument();
|
||||||
|
if (fromDoc) apply(fromDoc);
|
||||||
|
});
|
||||||
|
mo.observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ["lang"],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-read URL once (in case router rewrote search without remount)
|
||||||
|
const fromUrl = readLocaleFromUrl();
|
||||||
|
if (fromUrl) apply(fromUrl);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("message", onMessage);
|
||||||
|
mo.disconnect();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const U = useMemo(() => getCanvasUi(locale), [locale]);
|
||||||
|
return { locale, U };
|
||||||
|
}
|
||||||
|
|
@ -21,6 +21,47 @@ window.__ModuleLoader__.load({
|
||||||
const API = '/dsh-mind-map'
|
const API = '/dsh-mind-map'
|
||||||
const LOCALE_NS = 'settings.dshMindMap'
|
const LOCALE_NS = 'settings.dshMindMap'
|
||||||
const TAB_ID = 'dsh-mind-map'
|
const TAB_ID = 'dsh-mind-map'
|
||||||
|
/** Filled in apply(); also used by resolveHostLang / iframe locale sync. */
|
||||||
|
const runtime = { ctx: null }
|
||||||
|
|
||||||
|
function resolveHostLang() {
|
||||||
|
try {
|
||||||
|
const loc = runtime.ctx && runtime.ctx.locale
|
||||||
|
const snap = loc && typeof loc.getSnapshot === 'function' ? loc.getSnapshot() : null
|
||||||
|
const raw =
|
||||||
|
(snap && (snap.active || snap.locale || snap.preference || snap.lang)) ||
|
||||||
|
(loc && loc.active) ||
|
||||||
|
(document.documentElement && document.documentElement.lang) ||
|
||||||
|
(typeof navigator !== 'undefined' && (navigator.language || navigator.userLanguage)) ||
|
||||||
|
''
|
||||||
|
return String(raw).trim().toLowerCase().startsWith('zh') ? 'zh' : 'en'
|
||||||
|
} catch {
|
||||||
|
try {
|
||||||
|
return String(document.documentElement.lang || '')
|
||||||
|
.toLowerCase()
|
||||||
|
.startsWith('zh')
|
||||||
|
? 'zh'
|
||||||
|
: 'en'
|
||||||
|
} catch {
|
||||||
|
return 'en'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function postLocaleToMindmapFrames(lang) {
|
||||||
|
const next = lang === 'zh' ? 'zh' : 'en'
|
||||||
|
const frames = document.querySelectorAll('iframe.dsh-mm-frame')
|
||||||
|
for (const frame of frames) {
|
||||||
|
try {
|
||||||
|
frame.contentWindow?.postMessage(
|
||||||
|
{ type: 'dsh-mind-map:locale', lang: next },
|
||||||
|
'*',
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const DEFAULTS = {
|
const DEFAULTS = {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|
@ -199,6 +240,7 @@ window.__ModuleLoader__.load({
|
||||||
).replace(/\/$/, '')
|
).replace(/\/$/, '')
|
||||||
const q = new URLSearchParams({ embed: '1', _v: String(Date.now()) })
|
const q = new URLSearchParams({ embed: '1', _v: String(Date.now()) })
|
||||||
if (canvasId) q.set('canvasId', String(canvasId))
|
if (canvasId) q.set('canvasId', String(canvasId))
|
||||||
|
q.set('lang', resolveHostLang())
|
||||||
// Pass DSH host CSS tokens into the iframe (cross-origin).
|
// Pass DSH host CSS tokens into the iframe (cross-origin).
|
||||||
try {
|
try {
|
||||||
const root = document.documentElement
|
const root = document.documentElement
|
||||||
|
|
@ -555,6 +597,36 @@ window.__ModuleLoader__.load({
|
||||||
return () => window.removeEventListener('message', onMsg)
|
return () => window.removeEventListener('message', onMsg)
|
||||||
}, [view, sessionId, canvasId])
|
}, [view, sessionId, canvasId])
|
||||||
|
|
||||||
|
// Keep iframe UI language in sync with DSH (no full reload).
|
||||||
|
useEffect(() => {
|
||||||
|
if (view !== 'canvas' || !src) return undefined
|
||||||
|
const push = () => postLocaleToMindmapFrames(resolveHostLang())
|
||||||
|
push()
|
||||||
|
const onLoad = () => push()
|
||||||
|
const frame = document.querySelector('iframe.dsh-mm-frame')
|
||||||
|
if (frame) frame.addEventListener('load', onLoad)
|
||||||
|
const mo = new MutationObserver(push)
|
||||||
|
mo.observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['lang'],
|
||||||
|
})
|
||||||
|
let off = null
|
||||||
|
try {
|
||||||
|
const loc = runtime.ctx && runtime.ctx.locale
|
||||||
|
if (loc && typeof loc.subscribe === 'function') off = loc.subscribe(push)
|
||||||
|
else if (loc && typeof loc.on === 'function') off = loc.on('change', push)
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
const timer = setInterval(push, 2000)
|
||||||
|
return () => {
|
||||||
|
if (frame) frame.removeEventListener('load', onLoad)
|
||||||
|
mo.disconnect()
|
||||||
|
clearInterval(timer)
|
||||||
|
if (typeof off === 'function') off()
|
||||||
|
}
|
||||||
|
}, [view, src])
|
||||||
|
|
||||||
async function backToRoster() {
|
async function backToRoster() {
|
||||||
if (sessionId) {
|
if (sessionId) {
|
||||||
await fetch(`${API}/active`, {
|
await fetch(`${API}/active`, {
|
||||||
|
|
@ -722,7 +794,7 @@ window.__ModuleLoader__.load({
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Filled in apply(); settings card uses it to openTab. */
|
/** Filled in apply(); settings card uses it to openTab. */
|
||||||
const runtime = { ctx: null }
|
// runtime declared near top (locale + iframe helpers)
|
||||||
/** Suppress roster-on-activate while AI is jumping into a canvas. */
|
/** Suppress roster-on-activate while AI is jumping into a canvas. */
|
||||||
let aiJumpUntil = 0
|
let aiJumpUntil = 0
|
||||||
/** Latest AI-requested canvas enter (survives tab remount / event race). */
|
/** Latest AI-requested canvas enter (survives tab remount / event race). */
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue