183 lines
5.3 KiB
TypeScript

const translations = new Map<string, string>([
['3D View', '3D 视图'],
['Add file to project', '添加项目文件'],
['All', '全部'],
['Angularity', '倾斜度'],
['Ask me anything', '有问题尽管问我'],
['Back to HiCAD', '返回 HiCAD'],
['Bodies', '实体'],
['Chamfer', '倒角'],
['Choose your adventure', '选择一种建模方式'],
['Circular Pattern', '圆周阵列'],
['Code', '代码'],
['Code Editor', '代码编辑器'],
['Code-driven CAD', '代码驱动 CAD'],
['Commands', '命令'],
['Connected', '已连接'],
['Connecting and setting up scene...', '正在连接并初始化场景...'],
['Create a new project', '新建项目'],
['Design using traditional point-and-click', '使用传统点击方式设计'],
['Dismiss', '关闭'],
['Edit', '编辑'],
['Export', '导出'],
['Export part', '导出零件'],
['Extrude', '拉伸'],
['Feature Tree', '特征树'],
['Fillet', '圆角'],
['Flip Surface', '翻转曲面'],
['Front plane', '前视基准面'],
['Helix', '螺旋线'],
['Hello there, I\u2019m Zookeeper.', '你好,我是 HiCAD AI 助手。'],
["Hello there, I'm Zookeeper.", '你好,我是 HiCAD AI 助手。'],
['Help', '帮助'],
['Hole', '孔'],
['How would you like to start?', '想从哪里开始?'],
['Insert', '插入'],
['Logs', '日志'],
['Loft', '放样'],
['Mirror', '镜像'],
['No selection', '未选择'],
['No selection.', '未选择。'],
['Offset Plane', '偏移基准面'],
['Open project', '打开项目'],
['Project Files', '项目文件'],
['Publish', '发布'],
['Redo', '重做'],
['Revolve', '旋转'],
['Settings', '设置'],
['Shell', '抽壳'],
['Side plane', '侧视基准面'],
['Sketch', '草图'],
['Split', '分割'],
['Start Sketch', '新建草图'],
['Sweep', '扫掠'],
['Top plane', '顶视基准面'],
['Translate', '平移'],
['Undo', '撤销'],
['Union', '并集'],
['Variables', '变量'],
['View', '视图'],
['View settings', '视图设置'],
['Zookeeper', 'HiCAD AI 助手'],
['Zookeeper, our conversational CAD agent', 'HiCAD 对话式 CAD 助手'],
['Zoom to fit', '缩放至合适大小'],
])
const translatableAttributes = ['aria-label', 'placeholder', 'title'] as const
const excludedSelector = [
'code',
'pre',
'script',
'style',
'textarea',
'input',
'[contenteditable="true"]',
'.cm-editor',
'.cm-content',
'.cm-line',
].join(',')
function isExcluded(node: Node) {
const element =
node.nodeType === Node.ELEMENT_NODE
? (node as Element)
: node.parentElement
return Boolean(element?.closest(excludedSelector))
}
function translateTextNode(node: Text) {
if (isExcluded(node)) return
const value = node.nodeValue || ''
const normalized = value.trim()
const translated = translations.get(normalized)
if (!translated || translated === normalized) return
const leadingWhitespace = value.match(/^\s*/)?.[0] || ''
const trailingWhitespace = value.match(/\s*$/)?.[0] || ''
node.nodeValue = `${leadingWhitespace}${translated}${trailingWhitespace}`
}
function translateElement(element: Element) {
if (isExcluded(element)) return
for (const attribute of translatableAttributes) {
const value = element.getAttribute(attribute)?.trim()
if (!value) continue
const translated = translations.get(value)
if (translated) element.setAttribute(attribute, translated)
}
}
function translateTree(root: Node) {
if (root.nodeType === Node.TEXT_NODE) {
translateTextNode(root as Text)
return
}
if (root.nodeType === Node.ELEMENT_NODE) {
translateElement(root as Element)
}
const walker = document.createTreeWalker(
root,
NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT
)
let current = walker.nextNode()
while (current) {
if (current.nodeType === Node.TEXT_NODE) {
translateTextNode(current as Text)
} else {
translateElement(current as Element)
}
current = walker.nextNode()
}
}
export function installHiCadLocalization() {
document.documentElement.lang = 'zh-CN'
document.documentElement.dataset.hicadEmbedded = 'true'
const style = document.createElement('style')
style.textContent = `
html[data-hicad-embedded='true'] body {
font-family: Inter, "PingFang SC", "Microsoft YaHei", sans-serif;
}
html[data-hicad-embedded='true'] [data-testid='download-desktop-app'],
html[data-hicad-embedded='true'] [data-testid='billing-remaining-bar'],
html[data-hicad-embedded='true'] [data-hicad-official-account-link] {
display: none !important;
}
`
document.head.appendChild(style)
translateTree(document.documentElement)
let frame = 0
const pendingRoots = new Set<Node>()
const flush = () => {
frame = 0
for (const root of pendingRoots) translateTree(root)
pendingRoots.clear()
}
const schedule = (root: Node) => {
pendingRoots.add(root)
if (!frame) frame = window.requestAnimationFrame(flush)
}
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'childList') {
for (const node of mutation.addedNodes) schedule(node)
} else {
schedule(mutation.target)
}
}
})
observer.observe(document.documentElement, {
attributeFilter: [...translatableAttributes],
attributes: true,
characterData: true,
childList: true,
subtree: true,
})
}