mirror of
https://github.com/hansjone/mind-map.git
synced 2026-10-09 11:40:45 +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
|
|
@ -24,7 +24,7 @@ import {
|
|||
import { drawPropSheet, NodePropOverlay } from "./node-props";
|
||||
import { branchPalette, readSystemTheme } from "./system-theme";
|
||||
import { useAppStore } from "./store";
|
||||
import { canvasUi as U } from "./canvas-ui-text";
|
||||
import { useCanvasUi } from "./useCanvasUi";
|
||||
|
||||
const NODE_W = 200;
|
||||
|
||||
|
|
@ -37,6 +37,7 @@ const MINI_W = 168;
|
|||
const MINI_H = 118;
|
||||
|
||||
export function TopologyCanvas() {
|
||||
const { U } = useCanvasUi();
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const miniRef = useRef<HTMLCanvasElement>(null);
|
||||
const hostRef = useRef<HTMLDivElement>(null);
|
||||
|
|
@ -676,6 +677,7 @@ export function TopologyCanvas() {
|
|||
canvas?.prefs.showRelationEdges,
|
||||
canvas?.prefs.nodeBadges,
|
||||
imageTick,
|
||||
U,
|
||||
]);
|
||||
|
||||
const drawMini = useCallback(() => {
|
||||
|
|
@ -888,6 +890,7 @@ export function TopologyCanvas() {
|
|||
fitView,
|
||||
setZoomAroundCenter,
|
||||
viewport.zoom,
|
||||
U,
|
||||
]);
|
||||
|
||||
const cursor =
|
||||
|
|
|
|||
|
|
@ -1,31 +1,72 @@
|
|||
/**
|
||||
* Canvas UI copy (zh). Keep ALL user-facing CJK here.
|
||||
* TopologyCanvas.tsx must not embed Chinese literals — they get corrupted
|
||||
* by Windows/PowerShell default encodings (question-mark placeholders / invalid 0xb7 bytes).
|
||||
* Canvas UI copy (zh / en). Keep ALL user-facing strings here.
|
||||
* TopologyCanvas must not embed literals — Windows tools corrupt CJK in large files.
|
||||
*/
|
||||
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: "未命名",
|
||||
newNode: "新节点",
|
||||
leftChild: "左侧子主题",
|
||||
rightChild: "右侧子主题",
|
||||
sibling: "兄弟节点",
|
||||
relationLabel: "关联",
|
||||
|
||||
toolbarAria: "画布工具",
|
||||
controlsAria: "视图控制",
|
||||
|
||||
backRosterTitle: "返回画布列表",
|
||||
backRoster: "返回",
|
||||
|
||||
toolSelect: "选择",
|
||||
toolPan: "平移",
|
||||
toolLink: "连线",
|
||||
|
||||
dragNode: "单点",
|
||||
dragBranch: "整枝",
|
||||
dragNodeTitle: "单点拖拽:只移动当前节点 (N);按住 Shift 也可临时单点",
|
||||
dragBranchTitle: "整枝拖拽:节点与子孙一起移动 (B)",
|
||||
|
||||
bubble: "气泡",
|
||||
card: "卡片",
|
||||
bubbleTitle: "气泡视图:精简标题",
|
||||
|
|
@ -33,7 +74,6 @@ export const canvasUi = {
|
|||
unpin: "解钉",
|
||||
unpinTitle: "取消钉住:让选中节点重新参与自动布局",
|
||||
undo: "撤销",
|
||||
|
||||
fullscreen: "全屏",
|
||||
exitFullscreen: "退出全屏",
|
||||
zoomIn: "放大",
|
||||
|
|
@ -46,13 +86,79 @@ export const canvasUi = {
|
|||
unlock: "解锁",
|
||||
lockTitle: "锁定画布 (L)",
|
||||
unlockTitle: "解锁画布 (L)",
|
||||
|
||||
addLeftChildTitle: "添加左侧子主题",
|
||||
addRightChildTitle: "添加右侧子主题",
|
||||
|
||||
hintLocked: "已锁定 · 仅浏览/缩放/平移",
|
||||
hintNodeDrag: "单点拖 · 只移动当前节点 · Shift 临时单点 · 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 };
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue