Files
archify-vscode-ext/vendor/archify/renderers/shared/i18n.mjs
T
root-at-skicandClaude Opus 5.5 22815a9940 Archify Diagram Viewer 0.1.0
VS Code extension that previews Archify diagrams from their JSON sources
(live, as you type) and opens rendered Archify HTML in a viewer tab.
Bundles the Archify 3.0.1 renderer and runs it on VS Code's Node runtime.
Adds validation diagnostics, JSON schema help, source-link navigation,
export saving, render-to-file and open-in-browser commands.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 12:44:15 +03:00

604 lines
42 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
export const SUPPORTED_LOCALES = ['en', 'zh-CN'];
export const DEFAULT_LOCALE = 'en';
const ESCAPE_MAP = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
export function escapeHtml(value) {
return String(value ?? '').replace(/[&<>"']/g, (character) => ESCAPE_MAP[character]);
}
// One catalog feeds renderer-time SVG/HTML copy and the selected runtime
// catalog embedded in each standalone artifact. Keeping both built-in locales in one
// tuple makes missing translations impossible to hide behind an English
// fallback during development.
const MESSAGE_PAIRS = {
'page.title': ['{title} Diagram', '{title}'],
'diagram.description.architecture': ['An architecture diagram generated by Archify.', '由 Archify 生成的架构图。'],
'diagram.description.workflow': ['A workflow diagram generated by Archify.', '由 Archify 生成的工作流图。'],
'diagram.description.sequence': ['A sequence diagram generated by Archify.', '由 Archify 生成的时序图。'],
'diagram.description.dataflow': ['A data-flow diagram generated by Archify.', '由 Archify 生成的数据流图。'],
'diagram.description.lifecycle': ['A lifecycle diagram generated by Archify.', '由 Archify 生成的生命周期图。'],
'node.focus': ['Focus {label}', '聚焦{label}'],
'node.focus.detail': ['Focus {label}, {detail}', '聚焦{label},{detail}'],
'node.context.architecture': ['Architecture component', '架构组件'],
'node.context.workflow': ['Workflow node', '工作流节点'],
'node.context.sequence': ['Sequence participant', '时序参与者'],
'node.context.dataflow': ['Data-flow node', '数据流节点'],
'node.context.lifecycle': ['Lifecycle state', '生命周期状态'],
'legend.title': ['Legend', '图例'],
'legend.architecture.frontend': ['Frontend', '前端'],
'legend.architecture.backend': ['Backend', '后端'],
'legend.architecture.database': ['Database', '数据库'],
'legend.architecture.cloud': ['Cloud', '云服务'],
'legend.architecture.security': ['Security', '安全'],
'legend.architecture.messagebus': ['Message bus', '消息总线'],
'legend.architecture.external': ['External', '外部系统'],
'legend.workflow.frontend': ['User UI', '用户界面'],
'legend.workflow.backend': ['Agent logic', 'Agent 逻辑'],
'legend.workflow.security': ['Policy', '策略'],
'legend.workflow.messagebus': ['Tool action', '工具操作'],
'legend.workflow.database': ['Context / trace', '上下文 / 追踪'],
'legend.workflow.cloud': ['Cloud service', '云服务'],
'legend.workflow.external': ['External system', '外部系统'],
'legend.sequence.emphasis': ['main request', '主路径请求'],
'legend.sequence.return': ['return', '返回'],
'legend.sequence.security': ['security', '安全'],
'legend.sequence.dashed': ['async trace', '异步追踪'],
'legend.sequence.default': ['message', '普通消息'],
'legend.dataflow.emphasis': ['primary data', '主要数据'],
'legend.dataflow.security': ['policy / PII', '策略 / PII'],
'legend.dataflow.dashed': ['async batch', '异步批处理'],
'legend.dataflow.database': ['data store', '数据存储'],
'legend.dataflow.default': ['data flow', '数据流'],
'legend.lifecycle.start': ['initial state', '起点'],
'legend.lifecycle.active': ['active', '进行中'],
'legend.lifecycle.waiting': ['waiting', '等待'],
'legend.lifecycle.decision': ['decision', '决策'],
'legend.lifecycle.success': ['success', '成功'],
'legend.lifecycle.failure': ['failure / exit', '失败 / 退出'],
'legend.lifecycle.neutral': ['neutral', '中性状态'],
'legend.lifecycle.external': ['external', '外部状态'],
'legend.lifecycle.final': ['final state', '终态'],
'viewer.kind.frontend': ['Frontend', '前端'],
'viewer.kind.backend': ['Backend', '后端'],
'viewer.kind.database': ['Database', '数据库'],
'viewer.kind.cloud': ['Cloud', '云服务'],
'viewer.kind.security': ['Security', '安全'],
'viewer.kind.messagebus': ['Message bus', '消息总线'],
'viewer.kind.external': ['External', '外部系统'],
'viewer.kind.neutral': ['Neutral', '中性'],
'viewer.kind.node': ['Node', '节点'],
'viewer.kind.start': ['Start', '开始'],
'viewer.kind.active': ['Active', '活动'],
'viewer.kind.waiting': ['Waiting', '等待'],
'viewer.kind.decision': ['Decision', '决策'],
'viewer.kind.success': ['Success', '成功'],
'viewer.kind.failure': ['Failure', '失败'],
'viewer.toolbar.actions': ['Diagram actions', '图表操作'],
'viewer.theme.toggle.title': ['Toggle theme (T)', '切换主题(T)'],
'viewer.theme.toggle': ['Toggle color theme', '切换颜色主题'],
'viewer.theme.dark': ['Dark', '深色'],
'viewer.theme.light': ['Light', '浅色'],
'viewer.preset.choose.title': ['Choose visual style (S cycles)', '选择视觉风格(S 循环切换)'],
'viewer.preset.choose': ['Choose visual style', '选择视觉风格'],
'viewer.preset.style': ['Style', '风格'],
'viewer.preset.menu': ['Visual style', '视觉风格'],
'viewer.preset.identity': ['Visual identity', '视觉表达'],
'viewer.preset.cycles': ['to cycle', '循环切换'],
'viewer.preset.classic': ['Classic', '经典'],
'viewer.preset.classic.short': ['Classic', '经典'],
'viewer.preset.classic.hint': ['Stable technical default', '稳定的技术默认风格'],
'viewer.preset.flow': ['Signal Flow', '信号流'],
'viewer.preset.flow.short': ['Flow', '流动'],
'viewer.preset.flow.hint': ['Motion-forward presentation', '突出动态流向'],
'viewer.preset.blueprint': ['Blueprint', '蓝图'],
'viewer.preset.blueprint.hint': ['Engineering review', '工程评审'],
'viewer.preset.editorial': ['Editorial', '编辑风格'],
'viewer.preset.editorial.hint': ['Publication and launch notes', '适合发布与上线说明'],
'viewer.preset.badge.signalFlow': ['SIGNAL FLOW', '信号流'],
'viewer.preset.badge.blueprint': ['BLUEPRINT / REV 01', '蓝图 / 修订 01'],
'viewer.preset.badge.editorial': ['EDITORIAL / FIELD NOTE', '编辑风格 / 现场笔记'],
'viewer.preset.badge.editorialPlate': ['ARCHIFY / PLATE 04', 'ARCHIFY / 图版 04'],
'viewer.preset.current': ['Visual style: {style}. Choose visual style', '当前视觉风格:{style}。选择视觉风格'],
'viewer.motion.live': ['Live', '动态'],
'viewer.motion.still': ['Still', '静态'],
'viewer.motion.pause': ['Pause motion', '暂停动效'],
'viewer.motion.resume': ['Resume motion', '恢复动效'],
'viewer.motion.reduced': ['Motion paused by reduced-motion preference', '已根据减少动态效果偏好暂停动效'],
'viewer.motion.hidden': ['Motion paused while this page is hidden', '页面不可见时已暂停动效'],
'viewer.motion.yielding': ['Pause motion; currently yielding to {owner}', '暂停动效;当前让位于{owner}'],
'viewer.motion.yielding.title': ['Live preview enabled · yielding to {owner}', '动态预览已启用 · 正在让位于{owner}'],
'viewer.owner.route': ['Route Probe', '路径探测'],
'viewer.owner.lens': ['Semantic Lens', '语义透镜'],
'viewer.owner.relationship': ['Relationship Preview', '关系预览'],
'viewer.owner.intent': ['Intent Trace', '意图追踪'],
'viewer.owner.focus': ['semantic focus', '语义聚焦'],
'viewer.owner.legend': ['legend preview', '图例预览'],
'viewer.owner.reader': ['reader interaction', '读者交互'],
'viewer.present.enter': ['Enter presentation stage', '进入演示模式'],
'viewer.present.enter.title': ['Presentation stage (F)', '演示模式(F)'],
'viewer.present.exit': ['Exit presentation stage', '退出演示模式'],
'viewer.present.exit.title': ['Exit presentation stage (F or Escape)', '退出演示模式(F 或 Escape)'],
'viewer.present.present': ['Present', '演示'],
'viewer.present.exit.label': ['Exit', '退出'],
'viewer.export.button': ['Export', '导出'],
'viewer.export.button.title': ['Export diagram (E)', '导出图表(E)'],
'viewer.export.diagram': ['Export diagram', '导出图表'],
'viewer.export.menu': ['Export', '导出'],
'viewer.export.subtitle': ['Portable, clean outputs', '便携、整洁的输出'],
'viewer.export.share': ['Share', '分享'],
'viewer.export.shareCard': ['Share Card', '分享卡片'],
'viewer.export.routeShareCard': ['Route Share Card', '路径分享卡片'],
'viewer.export.reachShareCard': ['Reach Share Card', '可达范围分享卡片'],
'viewer.export.copyDiagram': ['Copy diagram', '复制图表'],
'viewer.export.clipboardPng': ['PNG to clipboard', '复制 PNG 到剪贴板'],
'viewer.export.raster': ['Raster images', '位图'],
'viewer.export.image': ['Image', '图像'],
'viewer.export.lossless': ['Lossless image', '无损图像'],
'viewer.export.compact': ['Compact image', '紧凑图像'],
'viewer.export.modern': ['Modern image', '现代图像格式'],
'viewer.export.vectorMotion': ['Vector and motion', '矢量与动效'],
'viewer.export.vectorMotion.heading': ['Vector & motion', '矢量与动效'],
'viewer.export.editable': ['Editable vector', '可编辑矢量图'],
'viewer.export.svg.auto': ['SVG · Auto', 'SVG · 自动'],
'viewer.export.svg.auto.hint': ['Matches host theme', '匹配宿主主题'],
'viewer.export.svg.light': ['SVG · Light', 'SVG · 浅色'],
'viewer.export.svg.light.hint': ['Always light', '始终浅色'],
'viewer.export.svg.dark': ['SVG · Dark', 'SVG · 深色'],
'viewer.export.svg.dark.hint': ['Always dark', '始终深色'],
'viewer.export.motion6s': ['6s motion', '6 秒动效'],
'viewer.export.unsupported': ['Not supported by this browser', '当前浏览器不支持'],
'viewer.export.clipboardUnsupported': ['Clipboard image write not supported by this browser', '当前浏览器不支持写入图片剪贴板'],
'viewer.export.clipboardUnsupported.short': ['Clipboard image write not supported in this browser.', '此浏览器不支持写入图片剪贴板。'],
'viewer.export.motionUnavailable': ['Motion capture unavailable in this browser', '当前浏览器无法录制动效'],
'viewer.export.webmUnavailable': ['WebM unavailable in this browser', '当前浏览器不支持 WebM'],
'viewer.export.failed': ['Export failed: {message}', '导出失败:{message}'],
'viewer.export.unknownVariant': ['Unknown Share Card variant: {variant}', '未知的分享卡片类型:{variant}'],
'viewer.export.routeRequired': ['Trace a route before exporting a Route Share Card', '请先追踪路径,再导出路径分享卡片'],
'viewer.export.reachRequired': ['Trace authored reach before exporting a Reach Share Card', '请先追踪编写可达范围,再导出可达范围分享卡片'],
'viewer.export.unknown': ['unknown', '未知错误'],
'viewer.export.routeFailed': ['Route Share Card export failed: {message}', '路径分享卡片导出失败:{message}'],
'viewer.export.reachFailed': ['Reach Share Card export failed: {message}', '可达范围分享卡片导出失败:{message}'],
'viewer.export.copyFailed': ['Copy failed: {message}', '复制失败:{message}'],
'viewer.export.copiedPng': ['Copied PNG to clipboard', '已将 PNG 复制到剪贴板'],
'viewer.export.downloadedRoute': ['Downloaded Route Share Card', '已下载路径分享卡片'],
'viewer.export.downloadedReach': ['Downloaded Reach Share Card', '已下载可达范围分享卡片'],
'viewer.export.downloadedWebm': ['Downloaded WebM', '已下载 WebM'],
'viewer.export.recording': ['Recording 6 seconds of motion…', '正在录制 6 秒动效…'],
'viewer.export.card.routeSummary.one': ['Path: {source} → {target} · {count} step', '路径:{source} → {target} · {count} 步'],
'viewer.export.card.routeSummary.other': ['Path: {source} → {target} · {count} steps', '路径:{source} → {target} · {count} 步'],
'viewer.export.card.reachSummary': ['Authored {direction} from {origin} · {nodes} · {links} · max {hops}', '从{origin}开始的编写{direction} · {nodes} · {links} · 最深 {hops}'],
'viewer.export.card.node.one': ['{count} node', '{count} 个节点'],
'viewer.export.card.node.other': ['{count} nodes', '{count} 个节点'],
'viewer.export.card.link.one': ['{count} link', '{count} 条连接'],
'viewer.export.card.link.other': ['{count} links', '{count} 条连接'],
'viewer.export.card.hop.one': ['{count} hop', '{count} 跳'],
'viewer.export.card.hop.other': ['{count} hops', '{count} 跳'],
'viewer.export.card.routeBadge': ['ARCHIFY · ROUTE · {hops}', 'ARCHIFY · 路径 · {hops}'],
'viewer.export.card.reachBadge': ['ARCHIFY · {direction} REACH', 'ARCHIFY · {direction}可达范围'],
'viewer.export.direction.upstream': ['Upstream', '上游'],
'viewer.export.direction.downstream': ['Downstream', '下游'],
'viewer.export.error.canvasUnavailable': ['Canvas unavailable for {label}', '无法为{label}使用画布'],
'viewer.export.error.contextUnavailable': ['2D canvas context unavailable for {label}', '无法为{label}创建二维画布上下文'],
'viewer.export.error.toBlobUnavailable': ['canvas.toBlob unavailable for {label}', '{label}无法使用 canvas.toBlob'],
'viewer.export.error.toBlobNull': ['canvas.toBlob returned no data for {label}', '{label}的 canvas.toBlob 未返回数据'],
'viewer.export.error.variantsCombined': ['Share Card variants cannot be combined', '无法同时组合多种分享卡片类型'],
'viewer.export.error.viewerState': ['Share Card export could not remove temporary viewer state', '分享卡片导出无法移除临时 Viewer 状态'],
'viewer.export.error.routeState': ['Route Card export could not preserve the resolved route safely', '路径卡片导出无法安全保留已解析路径'],
'viewer.export.error.reachState': ['Reach Card export could not preserve authored reach safely', '可达范围卡片导出无法安全保留编写的可达范围'],
'viewer.export.error.webmRequirements': ['WebM motion export requires a trace animation and browser MediaRecorder support', 'WebM 动效导出需要追踪动画及浏览器 MediaRecorder 支持'],
'viewer.export.error.mediaRecorder': ['MediaRecorder failed', 'MediaRecorder 录制失败'],
'viewer.export.error.emptyWebm': ['MediaRecorder produced an empty WebM', 'MediaRecorder 生成了空的 WebM'],
'viewer.export.error.webmBackground': ['SVG background could not be loaded for WebM export', '无法为 WebM 导出加载 SVG 背景'],
'viewer.guide.title': ['Explore this system', '探索此系统'],
'viewer.focus.selectedNodes': ['{count} selected nodes', '已选择 {count} 个节点'],
'viewer.guide.eyebrow': ['Diagram guide', '图表指南'],
'viewer.guide.close': ['Close diagram guide', '关闭图表指南'],
'viewer.guide.inspecting': ['Inspecting compiled semantics', '正在检查已编译语义'],
'viewer.guide.actions': ['Diagram exploration actions', '图表探索操作'],
'viewer.guide.find': ['Find any node', '查找任意节点'],
'viewer.guide.find.hint': ['Search labels, responsibilities, kinds, and stable IDs.', '搜索标签、职责、类型和稳定 ID。'],
'viewer.guide.route': ['Trace a route', '追踪路径'],
'viewer.guide.route.aria': ['Trace a directed route', '追踪有向路径'],
'viewer.guide.route.hint': ['Ask how two semantic nodes connect in authored direction.', '查看两个语义节点如何按编写方向连接。'],
'viewer.guide.map': ['See the whole system', '查看完整系统'],
'viewer.guide.map.hint': ['Open Semantic Radar with a live viewport and stable nodes.', '打开带实时视口和稳定节点的语义雷达。'],
'viewer.guide.lens': ['Compare semantic kinds', '比较语义类型'],
'viewer.guide.lens.hint': ['Count roles, reveal their traffic, and compare direct authored links.', '统计角色、显示流量并比较直接编写的连接。'],
'viewer.guide.present': ['Enter Presentation Stage', '进入演示模式'],
'viewer.guide.present.hint': ['Give the live diagram the viewport without changing export.', '让实时图表占满视口,同时不改变导出。'],
'viewer.guide.shortcuts': ['Additional keyboard shortcuts', '其他键盘快捷键'],
'viewer.guide.shortcut.export': ['Export', '导出'],
'viewer.guide.shortcut.theme': ['Theme', '主题'],
'viewer.guide.shortcut.style': ['Style', '风格'],
'viewer.guide.shortcut.reset': ['Reset', '重置'],
'viewer.guide.shortcut.zoomIn': ['Zoom in', '放大'],
'viewer.guide.shortcut.zoomOut': ['Zoom out', '缩小'],
'viewer.guide.shortcut.close': ['Close', '关闭'],
'viewer.guide.facts': ['{nodes} · {relationships}', '{nodes} · {relationships}'],
'viewer.guide.fact.node.one': ['{count} semantic node', '{count} 个语义节点'],
'viewer.guide.fact.node.other': ['{count} semantic nodes', '{count} 个语义节点'],
'viewer.guide.fact.relationship.one': ['{count} relationship', '{count} 条关系'],
'viewer.guide.fact.relationship.other': ['{count} relationships', '{count} 条关系'],
'viewer.guide.open': ['Open diagram guide', '打开图表指南'],
'viewer.finder.title': ['Find a node', '查找节点'],
'viewer.finder.close': ['Close node finder', '关闭节点查找器'],
'viewer.finder.placeholder': ['Search labels or IDs', '搜索标签或 ID'],
'viewer.finder.search': ['Search diagram nodes', '搜索图表节点'],
'viewer.finder.results': ['Diagram nodes', '图表节点'],
'viewer.finder.empty': ['No matching nodes', '没有匹配的节点'],
'viewer.finder.result.focus': ['Focus {label}', '聚焦{label}'],
'viewer.finder.result.routeStart': ['Choose {label} as route start', '选择{label}作为路径起点'],
'viewer.finder.result.routeTarget': ['Choose {label} as route destination, {links}', '选择{label}作为路径终点,{links}'],
'viewer.finder.status.empty': ['No matching nodes', '没有匹配的节点'],
'viewer.finder.status.count.one': ['{count} matching node', '{count} 个匹配节点'],
'viewer.finder.status.count.other': ['{count} matching nodes', '{count} 个匹配节点'],
'viewer.finder.noun.nodes': ['nodes', '个节点'],
'viewer.finder.link.one': ['{count} link', '{count} 条连接'],
'viewer.finder.link.other': ['{count} links', '{count} 条连接'],
'viewer.finder.result.focus.one': ['Focus {label}, {count} related connection', '聚焦{label},{count} 条相关连接'],
'viewer.finder.result.focus.other': ['Focus {label}, {count} related connections', '聚焦{label},{count} 条相关连接'],
'viewer.finder.status.filtered': ['{visible} of {available} {noun}', '{visible}/{available} {noun}'],
'viewer.finder.status.all': ['{available} {noun}', '{available} {noun}'],
'viewer.passport.eyebrow': ['Semantic passport', '语义护照'],
'viewer.passport.metadata': ['Node metadata', '节点元数据'],
'viewer.passport.evidence': ['Verified source evidence', '已验证的源代码证据'],
'viewer.passport.verified': ['Verified source', '已验证来源'],
'viewer.passport.verificationScope': ['Verified against local Git at the pinned revision. Remote access has not been checked.', '已按固定修订版本验证本地 Git 证据,未检查远程访问权限。'],
'viewer.passport.reach': ['Authored reach', '编写可达范围'],
'viewer.passport.reach.trace': ['Trace authored reachability', '追踪编写的可达性'],
'viewer.passport.upstream': ['Upstream', '上游'],
'viewer.passport.downstream': ['Downstream', '下游'],
'viewer.passport.upstream.trace': ['Trace upstream authored reachability', '追踪上游编写可达性'],
'viewer.passport.downstream.trace': ['Trace downstream authored reachability', '追踪下游编写可达性'],
'viewer.passport.close': ['Close semantic passport', '关闭语义护照'],
'viewer.passport.move': ['Move semantic passport. Drag, use arrow keys, or press Home to reset.', '移动语义护照。可拖动、使用方向键移动,或按 Home 恢复自动位置。'],
'viewer.passport.copy': ['Copy link', '复制链接'],
'viewer.passport.copy.focus': ['Copy link to focused node', '复制聚焦节点的链接'],
'viewer.passport.relations': ['Relations', '关系'],
'viewer.passport.relations.show': ['Show connected relationships', '显示关联关系'],
'viewer.passport.relations.hide': ['Hide connected relationships', '隐藏关联关系'],
'viewer.passport.relations.list': ['Connected relationships', '关联关系'],
'viewer.passport.copyRelation': ['Copy relation', '复制关系'],
'viewer.passport.copyNode': ['Copy node', '复制节点'],
'viewer.passport.copyPinned': ['Copy link to pinned relationship', '复制固定关系的链接'],
'viewer.passport.copySource': ['Copy link to source node', '复制来源节点的链接'],
'viewer.passport.copy.focused.success': ['Focused node link copied', '已复制聚焦节点链接'],
'viewer.passport.copy.pinned.success': ['Pinned relationship link copied', '已复制固定关系链接'],
'viewer.passport.copy.focused.failed': ['Could not copy focused node link', '无法复制聚焦节点链接'],
'viewer.passport.copy.pinned.failed': ['Could not copy pinned relationship link', '无法复制固定关系链接'],
'viewer.passport.relationship.none': ['No connected relationships', '没有关联关系'],
'viewer.passport.relationship.count.one': ['{count} relation', '{count} 条关系'],
'viewer.passport.relationship.count.other': ['{count} relations', '{count} 条关系'],
'viewer.passport.relationship.show.one': ['Show {count} connected relationship', '显示 {count} 条关联关系'],
'viewer.passport.relationship.show.other': ['Show {count} connected relationships', '显示 {count} 条关联关系'],
'viewer.passport.relationship.summary': ['{out} outgoing · {in} incoming{loops}', '{out} 条出向 · {in} 条入向{loops}'],
'viewer.passport.relationship.loops': [' · {count} loop', ' · {count} 条自环'],
'viewer.passport.relationship.explorer': ['Direct relationship explorer', '直接关系浏览器'],
'viewer.passport.relationship.help': ['Use arrow keys to explore relationships. Press Enter or Space to pin details; Escape clears.', '使用方向键浏览关系。按 Enter 或空格键固定详情;按 Escape 清除。'],
'viewer.passport.relationship.loopsBack': ['loops back', '回环'],
'viewer.passport.relationship.connectsTo': ['connects to', '连接到'],
'viewer.passport.relationship.connectsFrom': ['connects from', '连接自'],
'viewer.passport.relationship.pinned': ['Pinned relationship · {from} → {to} · {label}', '已固定关系 · {from} → {to} · {label}'],
'viewer.passport.relationship.inspect': ['Inspect relationship {index} of {total}: {from} to {to}, {label}. Press Enter for details.', '检查第 {index}/{total} 条关系:{from} 到 {to},{label}。按 Enter 查看详情。'],
'viewer.passport.relationship.group.out': ['Outgoing', '出向'],
'viewer.passport.relationship.group.in': ['Incoming', '入向'],
'viewer.passport.relationship.group.loop': ['Self loops', '自环'],
'viewer.passport.relationship.row': ['{group}: {relationship}, {neighbor}', '{group}:{relationship},{neighbor}'],
'viewer.passport.relationship.direction.out': ['OUT →', '出 →'],
'viewer.passport.relationship.direction.in': ['← IN', '← 入'],
'viewer.passport.relationship.direction.loop': ['LOOP', '自环'],
'viewer.passport.sourceCount.one': ['{count} verified source reference', '{count} 个已验证来源引用'],
'viewer.passport.sourceCount.other': ['{count} verified source references', '{count} 个已验证来源引用'],
'viewer.passport.sourceMarker': ['SRC', '来源'],
'viewer.passport.beacon.one': ['{count} verified source; focus this node to inspect', '{count} 个已验证来源;聚焦此节点以检查'],
'viewer.passport.beacon.other': ['{count} verified sources; focus this node to inspect', '{count} 个已验证来源;聚焦此节点以检查'],
'viewer.passport.repository.open': ['Open verified repository revision {revision}', '打开已验证的仓库修订版本 {revision}'],
'viewer.passport.source.open': ['Open verified source {path} at revision {revision}', '打开修订版本 {revision} 中已验证的来源 {path}'],
'viewer.passport.source.openLink': ['Open ↗', '打开 ↗'],
'viewer.passport.reach.upstream.one': ['Trace {count} upstream authored node', '追踪 {count} 个上游编写节点'],
'viewer.passport.reach.upstream.other': ['Trace {count} upstream authored nodes', '追踪 {count} 个上游编写节点'],
'viewer.passport.reach.downstream.one': ['Trace {count} downstream authored node', '追踪 {count} 个下游编写节点'],
'viewer.passport.reach.downstream.other': ['Trace {count} downstream authored nodes', '追踪 {count} 个下游编写节点'],
'viewer.passport.reach.noUpstream': ['No upstream authored nodes', '没有上游编写节点'],
'viewer.passport.reach.noDownstream': ['No downstream authored nodes', '没有下游编写节点'],
'viewer.passport.reach.status': ['{direction} · {nodes} nodes · {links} links · max {hops} hops', '{direction} · {nodes} 个节点 · {links} 条连接 · 最深 {hops} 跳'],
'viewer.route.eyebrow': ['Path', '路径'],
'viewer.route.start': ['Click where the path starts', '点击路径的起点'],
'viewer.route.start.find': ['Find start', '查找起点'],
'viewer.route.start.find.aria': ['Find a route start', '查找路径起点'],
'viewer.route.copy': ['Copy link', '复制链接'],
'viewer.route.copy.aria': ['Copy link to traced route', '复制已追踪路径的链接'],
'viewer.route.clear': ['Clear', '清除'],
'viewer.route.clear.aria': ['Clear route probe', '清除路径探测'],
'viewer.route.traced': ['Path', '路径'],
'viewer.route.pickTwo': ['Click two nodes on the diagram', '在图上点两个节点'],
'viewer.route.pickOne': ['Click a node on the diagram', '在图上点一个节点'],
'viewer.route.controls': ['Route journey controls', '路径旅程控制'],
'viewer.route.previous': ['Previous route position', '上一个路径位置'],
'viewer.route.play': ['Play route journey', '播放路径旅程'],
'viewer.route.pause': ['Pause route journey', '暂停路径旅程'],
'viewer.route.replay': ['Replay route journey', '重播路径旅程'],
'viewer.route.next': ['Next route position', '下一个路径位置'],
'viewer.route.journey': ['Journey', '旅程'],
'viewer.route.pause.label': ['Pause', '暂停'],
'viewer.route.replay.label': ['Replay', '重播'],
'viewer.route.overview': ['Overview', '总览'],
'viewer.route.overview.aria': ['Show complete route overview', '显示完整路径总览'],
'viewer.route.instructions': ['Click a start, then an end. Paths follow the arrows.', '先点起点,再点终点,沿箭头方向找路。'],
'viewer.route.destination': ['Where does the path from {label} end?', '从{label}出发,走到哪里?'],
'viewer.route.destination.find': ['Find target', '查找目标'],
'viewer.route.destination.find.aria': ['Find a reachable route destination', '查找可达的路径目标'],
'viewer.route.differentDestination': ['Choose a different destination', '选择其他目标'],
'viewer.route.distinct': ['Pick two different nodes.', '请选两个不同的节点。'],
'viewer.route.unreachable': ['Cannot reach {label} from here', '走不到{label}'],
'viewer.route.unreachable.detail': ['{target} cannot be reached from {source} along the arrows. Pick a highlighted node.', '沿箭头方向,从{source}走不到{target}。请选一个高亮节点。'],
'viewer.route.start.instructions': ['Next, pick the end. Only nodes you can reach will light up.', '接着选终点,能走到的节点会亮起。'],
'viewer.route.copy.success': ['Traced route link copied', '已复制路径链接'],
'viewer.route.copy.failed': ['Could not copy traced route link', '无法复制路径链接'],
'viewer.route.position': ['Route position {index} of {total}: {label}', '路径位置 {index}/{total}:{label}'],
'viewer.route.step': ['Step {index} of {total} · {phase} · {label}', '第 {index}/{total} 步 · {phase} · {label}'],
'viewer.route.motionRequired': ['Automatic journey requires Live motion', '自动旅程需要动态模式'],
'viewer.route.trigger.clear': ['Clear traced route', '清除已追踪路径'],
'viewer.route.overview.status': ['{nodes} · {hops} · shortest path', '{nodes} · {hops} · 最短路径'],
'viewer.route.overview.node.one': ['{count} node', '{count} 个节点'],
'viewer.route.overview.node.other': ['{count} nodes', '{count} 个节点'],
'viewer.route.overview.hop.one': ['{count} step', '{count} 步'],
'viewer.route.overview.hop.other': ['{count} steps', '{count} 步'],
'viewer.route.phase.playing': ['Playing', '播放中'],
'viewer.route.phase.complete': ['Complete', '已完成'],
'viewer.route.phase.inspecting': ['Inspecting', '检查中'],
'viewer.route.destination.count.one': ['{count} reachable node is highlighted. Click it.', '有 {count} 个能走到的节点已高亮,点一下。'],
'viewer.route.destination.count.other': ['{count} reachable nodes are highlighted. Click one.', '有 {count} 个能走到的节点已高亮,点一个。'],
'viewer.route.noOutgoing': ['No arrows lead out of this node. Clear it and pick another start.', '这个节点没有向外的箭头。清除后换一个起点。'],
'viewer.route.result.title': ['{source} to {target}', '{source} 到 {target}'],
'viewer.route.finder.source.title': ['Choose route start', '选择路径起点'],
'viewer.route.finder.source.placeholder': ['Search route sources', '搜索路径来源'],
'viewer.route.finder.source.empty': ['No matching route sources', '没有匹配的路径来源'],
'viewer.route.finder.source.results': ['Nodes that can start a route', '可作为路径起点的节点'],
'viewer.route.finder.source.noun': ['route sources', '个路径来源'],
'viewer.route.finder.source.badge': ['start', '起点'],
'viewer.route.finder.target.title': ['Destination from {label}', '从{label}出发的目标'],
'viewer.route.finder.target.placeholder': ['Search reachable destinations', '搜索可达目标'],
'viewer.route.finder.target.empty': ['No matching reachable destinations', '没有匹配的可达目标'],
'viewer.route.finder.target.results': ['Reachable route destinations', '可达路径目标'],
'viewer.route.finder.target.noun': ['reachable destinations', '个可达目标'],
'viewer.route.hop.one': ['{count} hop', '{count} 跳'],
'viewer.route.hop.other': ['{count} hops', '{count} 跳'],
'viewer.lens.eyebrow': ['Semantic lens', '语义透镜'],
'viewer.lens.title': ['Compare system roles', '比较系统角色'],
'viewer.lens.close': ['Close semantic lens', '关闭语义透镜'],
'viewer.lens.instruction': ['Choose up to two semantic kinds. One reveals its real traffic; two compare only direct authored relationships.', '最多选择两种语义类型。选择一种可显示其真实流量;选择两种只比较直接编写的关系。'],
'viewer.lens.kinds': ['Semantic kinds', '语义类型'],
'viewer.lens.choose': ['Choose a kind to inspect its nodes and touching relationships.', '选择一种类型以检查其节点和相连关系。'],
'viewer.lens.copy': ['Copy link to semantic lens', '复制语义透镜链接'],
'viewer.lens.clear': ['Clear semantic lens', '清除语义透镜'],
'viewer.lens.open': ['Open semantic lens', '打开语义透镜'],
'viewer.lens.openActive': ['Open active semantic lens', '打开当前语义透镜'],
'viewer.lens.legend': ['Semantic legend', '语义图例'],
'viewer.lens.legend.inspect.one': ['Inspect {label}, {count} node', '检查{label},{count} 个节点'],
'viewer.lens.legend.inspect.other': ['Inspect {label}, {count} nodes', '检查{label},{count} 个节点'],
'viewer.lens.kind.count.one': ['{label}, {count} node', '{label},{count} 个节点'],
'viewer.lens.kind.count.other': ['{label}, {count} nodes', '{label},{count} 个节点'],
'viewer.lens.compare.one': ['{first} → {second}: {forward} · {second} → {first}: {reverse} · {count} direct relationship', '{first} → {second}:{forward} · {second} → {first}:{reverse} · 共 {count} 条直接关系'],
'viewer.lens.compare.other': ['{first} → {second}: {forward} · {second} → {first}: {reverse} · {count} direct relationships', '{first} → {second}:{forward} · {second} → {first}:{reverse} · 共 {count} 条直接关系'],
'viewer.lens.single': ['{nodes} · {relationships} · connected peers remain visible', '{nodes} · {relationships} · 已连接节点保持可见'],
'viewer.lens.node.one': ['{count} {label} node', '{count} 个{label}节点'],
'viewer.lens.node.other': ['{count} {label} nodes', '{count} 个{label}节点'],
'viewer.lens.relationship.one': ['{count} touching relationship', '{count} 条相连关系'],
'viewer.lens.relationship.other': ['{count} touching relationships', '{count} 条相连关系'],
'viewer.radar.title': ['Semantic radar', '语义雷达'],
'viewer.radar.building': ['Building overview', '正在构建总览'],
'viewer.radar.openFull': ['Open full semantic radar', '打开完整语义雷达'],
'viewer.radar.open': ['Open radar', '打开雷达'],
'viewer.radar.close': ['Close semantic radar', '关闭语义雷达'],
'viewer.radar.surface': ['Diagram overview. Click a node to focus it, or use arrow keys to pan.', '图表总览。点击节点进行聚焦,或使用方向键平移。'],
'viewer.radar.click': ['Click node', '点击节点'],
'viewer.radar.drag': ['Drag to pan', '拖动平移'],
'viewer.radar.space': ['Semantic radar needs more MAP space.', '语义雷达需要更多地图可见空间。'],
'viewer.radar.nodes': ['Semantic diagram radar nodes', '语义图表雷达节点'],
'viewer.radar.focus': ['Focus {label} from Semantic Radar', '从语义雷达聚焦{label}'],
'viewer.radar.status': ['{count} nodes · {viewport}', '{count} 个节点 · {viewport}'],
'viewer.radar.fullMap': ['{count} nodes · full map', '{count} 个节点 · 完整地图'],
'viewer.radar.compacted': ['Radar compacted to avoid covering the Semantic Passport or MAP controls.', '已收紧雷达,避免遮挡语义护照或地图控件。'],
'viewer.radar.cancelWaiting': ['Cancel semantic radar waiting for more MAP space', '取消等待更多地图空间的语义雷达'],
'viewer.radar.needsSpace': ['Semantic radar needs more visible MAP space', '语义雷达需要更多可见地图空间'],
'viewer.radar.viewport.full': ['full map', '完整地图'],
'viewer.radar.viewport.width': ['{percent}% width', '宽度 {percent}%'],
'viewer.radar.viewport.scale': ['{percent}% viewport', '视口 {percent}%'],
'viewer.nav.controls': ['Diagram view controls', '图表视图控制'],
'viewer.nav.route': ['Trace a directed route', '追踪有向路径'],
'viewer.nav.route.title': ['Trace route (R)', '追踪路径(R)'],
'viewer.nav.route.short': ['PATH', '路径'],
'viewer.nav.radar': ['Open semantic radar', '打开语义雷达'],
'viewer.nav.radar.title': ['Semantic radar (M)', '语义雷达(M)'],
'viewer.nav.radar.short': ['MAP', '地图'],
'viewer.nav.lens': ['Open semantic lens', '打开语义透镜'],
'viewer.nav.lens.title': ['Semantic lens (L)', '语义透镜(L)'],
'viewer.nav.lens.short': ['LENS', '透镜'],
'viewer.nav.find': ['Find a node', '查找节点'],
'viewer.nav.find.title': ['Find a node (/)', '查找节点(/)'],
'viewer.outline.title': ['Node index', '节点索引'],
'viewer.rail.show': ['Notes & index', '要点与索引'],
'viewer.rail.controls': ['Side panel', '侧栏'],
'viewer.rail.collapse': ['Collapse side panel', '收起侧栏'],
'viewer.rail.bottom': ['Move panel below the diagram', '移到图下方'],
'viewer.rail.right': ['Move panel beside the diagram', '移到图右侧'],
'viewer.nav.guide': ['Open diagram guide', '打开图表指南'],
'viewer.nav.guide.title': ['Diagram guide (?)', '图表指南(?)'],
'viewer.nav.zoomOut': ['Zoom out', '缩小'],
'viewer.nav.zoomOut.title': ['Zoom out (-)', '缩小(-)'],
'viewer.nav.reset': ['Reset diagram view', '重置图表视图'],
'viewer.nav.reset.title': ['Reset view (0)', '重置视图(0)'],
'viewer.nav.read': ['READ', '阅读'],
'viewer.nav.zoomIn': ['Zoom in', '放大'],
'viewer.nav.zoomIn.title': ['Zoom in (+)', '放大(+)'],
'viewer.nav.camera': ['{hint}. Reset diagram view', '{hint}。重置图表视图'],
'viewer.nav.camera.title': ['{semantic}{hint} · reset view (0)', '{semantic}{hint} · 重置视图(0)'],
'viewer.nav.camera.semantic': ['Semantic camera active · ', '语义相机已启用 · '],
'viewer.nav.level.map': ['MAP', '概览'],
'viewer.nav.level.read': ['READ', '阅读'],
'viewer.nav.level.full': ['FULL', '完整'],
'viewer.nav.level.auto': ['AUTO', '自动'],
'viewer.nav.detail.map': ['Zoom in to reveal relationship labels and node context', '放大以显示关系标签和节点上下文'],
'viewer.nav.detail.read': ['Zoom in again to reveal tags and annotations', '再次放大以显示标签和注释'],
'viewer.nav.detail.full': ['Full diagram detail', '完整图表详情'],
'viewer.intent.summary': ['{label}. {out} outgoing, {in} incoming{loops}. {total} connections. Press Enter for details.', '{label}。{out} 条出向,{in} 条入向{loops}。共 {total} 条连接。按 Enter 查看详情。'],
'viewer.intent.loops': [', {count} self loop', ',{count} 条自环'],
'viewer.common.copied': ['Copied', '已复制'],
'viewer.common.copyFailed': ['Copy failed', '复制失败'],
'viewer.common.copyLink': ['Copy link', '复制链接'],
'viewer.common.clear': ['Clear', '清除'],
'viewer.common.close': ['Close', '关闭'],
};
for (const [key, messages] of Object.entries(MESSAGE_PAIRS)) {
if (messages.length !== SUPPORTED_LOCALES.length || messages.some((message) => typeof message !== 'string')) {
throw new Error(`Incomplete Archify i18n tuple ${JSON.stringify(key)}`);
}
}
const BUILTIN_CATALOGS = Object.fromEntries(SUPPORTED_LOCALES.map((locale, index) => [
locale,
Object.fromEntries(Object.entries(MESSAGE_PAIRS).map(([key, pair]) => [key, pair[index]])),
]));
const EN = BUILTIN_CATALOGS.en;
const CANONICAL_KEYS = Object.keys(EN);
const PLACEHOLDER_PATTERN = /\{([a-zA-Z0-9_]+)\}/g;
function extractPlaceholders(message) {
return new Set([...String(message).matchAll(PLACEHOLDER_PATTERN)].map((match) => match[1]));
}
const CANONICAL_PLACEHOLDERS = Object.fromEntries(
CANONICAL_KEYS.map((key) => [key, extractPlaceholders(EN[key])]),
);
function placeholdersMatch(expected, actual) {
if (expected.size !== actual.size) return false;
for (const token of expected) if (!actual.has(token)) return false;
return true;
}
// Runtime catalogs registered by registerLocale(), keyed by whatever locale
// tag the caller supplied (e.g. an agent-authored 'fr'). Kept separate from
// BUILTIN_CATALOGS so a caller can never accidentally shadow a shipped
// catalog with a partial one.
const RUNTIME_CATALOGS = new Map();
function catalogFor(locale) {
return RUNTIME_CATALOGS.get(locale) || BUILTIN_CATALOGS[locale];
}
// Validates caller-supplied translation data against the canonical (English)
// message-key set. Pure and side-effect free: registerLocale() calls this
// and additionally builds/installs the resolved catalog.
export function validateTranslations(translations = {}) {
const supplied = Object.keys(translations || {});
const suppliedSet = new Set(supplied);
const missingKeys = CANONICAL_KEYS.filter((key) => !suppliedSet.has(key));
const unknownKeys = supplied.filter((key) => !Object.hasOwn(CANONICAL_PLACEHOLDERS, key));
const placeholderMismatches = [];
const usableKeys = [];
for (const key of supplied) {
if (!Object.hasOwn(CANONICAL_PLACEHOLDERS, key)) continue;
const value = translations[key];
if (typeof value !== 'string' || value.length === 0) {
placeholderMismatches.push({ key, expected: [...CANONICAL_PLACEHOLDERS[key]].sort(), actual: null });
continue;
}
const actual = extractPlaceholders(value);
if (placeholdersMatch(CANONICAL_PLACEHOLDERS[key], actual)) {
usableKeys.push(key);
} else {
placeholderMismatches.push({
key,
expected: [...CANONICAL_PLACEHOLDERS[key]].sort(),
actual: [...actual].sort(),
});
}
}
return {
totalKeys: CANONICAL_KEYS.length,
coveredKeys: usableKeys.length,
coverage: CANONICAL_KEYS.length ? usableKeys.length / CANONICAL_KEYS.length : 1,
missingKeys,
unknownKeys,
placeholderMismatches,
};
}
// Registers a fully-resolved catalog for an arbitrary locale tag, built by
// layering validated translations over the English base. A key that is
// missing, non-string, or whose interpolation placeholders don't match the
// canonical set falls back to its English string — partial or malformed
// translation data can never break rendering. Returns the same coverage
// report validateTranslations() would, for the caller to surface as an
// explicit fallback/coverage diagnostic before rendering.
export function registerLocale(locale, translations = {}) {
const report = validateTranslations(translations);
const catalog = { ...EN };
for (const key of CANONICAL_KEYS) {
const value = translations?.[key];
if (typeof value !== 'string' || value.length === 0) continue;
if (placeholdersMatch(CANONICAL_PLACEHOLDERS[key], extractPlaceholders(value))) {
catalog[key] = value;
}
}
RUNTIME_CATALOGS.set(locale, catalog);
return { locale, ...report };
}
export function resolveLocale(locale) {
return catalogFor(locale) ? locale : DEFAULT_LOCALE;
}
export function formatMessage(template, values = {}) {
return String(template).replace(/\{([a-zA-Z0-9_]+)\}/g, (match, key) => (
Object.hasOwn(values, key) ? String(values[key]) : match
));
}
export function translateMessage(locale, key, values = {}) {
const resolved = resolveLocale(locale);
const catalog = catalogFor(resolved);
if (!Object.hasOwn(catalog, key)) {
throw new Error(`Missing Archify i18n message ${JSON.stringify(key)} for ${resolved}`);
}
return formatMessage(catalog[key], values);
}
export function translateCount(locale, key, count, values = {}) {
const suffix = count === 1 ? 'one' : 'other';
return translateMessage(locale, `${key}.${suffix}`, { ...values, count });
}
export function viewerCatalog(locale) {
const resolved = resolveLocale(locale);
return Object.fromEntries(Object.entries(catalogFor(resolved)).filter(([key]) => key.startsWith('viewer.')));
}
export function localizeTemplate(template, locale) {
return template.replace(/\{\{i18n:([a-zA-Z0-9_.-]+)\}\}/g, (_match, key) => escapeHtml(translateMessage(locale, key)));
}
export function catalogKeys() {
return [...CANONICAL_KEYS];
}