feat: introduce archify skill for generating architecture diagrams

- Added a new Archify skill, enabling users to create polished architecture, workflow, sequence, data-flow, and lifecycle diagrams.
- Implemented comprehensive functionality including rendering, validation, and delivery of diagrams in various formats.
- Integrated a user-friendly command-line interface for generating and previewing diagrams.
- Developed supporting files including package.json, LICENSE, and SKILL.md for documentation and licensing.
- Added unit tests to ensure reliability and functionality of the new skill.

These changes enhance the application by providing a structured approach to visualizing system architecture and workflows, improving user experience and data representation.
This commit is contained in:
shancheas
2026-08-31 11:31:29 +07:00
parent 050fafd731
commit 166e0d40ac
221 changed files with 191228 additions and 1 deletions
@@ -0,0 +1,594 @@
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 every locale 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': ['request', '请求'],
'legend.sequence.return': ['return', '返回'],
'legend.sequence.security': ['security', '安全'],
'legend.sequence.dashed': ['async trace', '异步追踪'],
'legend.sequence.default': ['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': ['start', '开始'],
'legend.lifecycle.active': ['active state', '活动状态'],
'legend.lifecycle.waiting': ['waiting', '等待'],
'legend.lifecycle.decision': ['decision', '决策'],
'legend.lifecycle.success': ['terminal success', '成功终态'],
'legend.lifecycle.failure': ['failure / exit', '失败 / 退出'],
'legend.lifecycle.neutral': ['neutral', '中性状态'],
'legend.lifecycle.external': ['external', '外部状态'],
'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': ['S cycles', 'S 循环切换'],
'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.story': ['the guided story', '引导故事'],
'viewer.owner.chapter': ['the active chapter', '当前章节'],
'viewer.owner.chapterPreview': ['the chapter delta preview', '章节差异预览'],
'viewer.owner.handoff': ['the chapter handoff', '章节交接'],
'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.copyShareCard': ['Copy 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.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.period': ['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.copiedShare': ['Copied Share Card', '已复制分享卡片'],
'viewer.export.downloadedShare': ['Downloaded Share Card', '已下载分享卡片'],
'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': ['Route: {source} → {target} · {count} directed hop', '路径:{source} → {target} · {count} 个有向跳转'],
'viewer.export.card.routeSummary.other': ['Route: {source} → {target} · {count} directed hops', '路径:{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.card.defaultBadge': ['ARCHIFY · {preset} · {theme}', 'ARCHIFY · {preset} · {theme}'],
'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.guided.region': ['Guided diagram views', '图表引导视图'],
'viewer.guided.previous': ['Previous guided view', '上一个引导视图'],
'viewer.guided.previous.title': ['Previous guided view ([)', '上一个引导视图(['],
'viewer.guided.next': ['Next guided view', '下一个引导视图'],
'viewer.guided.next.title': ['Next guided view (])', '下一个引导视图(]'],
'viewer.guided.views': ['Guided views', '引导视图'],
'viewer.guided.explore': ['Explore this system', '探索此系统'],
'viewer.guided.intro': ['Step through curated paths without changing the source diagram.', '沿精选路径逐步查看,而不改变源图表。'],
'viewer.guided.trail': ['Story trail', '故事轨迹'],
'viewer.guided.beat': ['Beat', '节点'],
'viewer.guided.nextBeat': ['Next', '下一步'],
'viewer.guided.play': ['Play guided story', '播放引导故事'],
'viewer.guided.play.title': ['Play guided story (P)', '播放引导故事(P'],
'viewer.guided.pause': ['Pause guided story', '暂停引导故事'],
'viewer.guided.pause.title': ['Pause guided story (P)', '暂停引导故事(P'],
'viewer.guided.replay': ['Replay guided story', '重播引导故事'],
'viewer.guided.replay.title': ['Replay guided story (P)', '重播引导故事(P'],
'viewer.guided.playStory': ['Play story', '播放故事'],
'viewer.guided.pauseStory': ['Pause', '暂停'],
'viewer.guided.replayStory': ['Replay story', '重播故事'],
'viewer.guided.motionUnavailable': ['Story playback unavailable while motion is Still', '静态模式下无法播放故事'],
'viewer.guided.enableMotion': ['Switch motion to Live to play the guided story', '切换为动态模式以播放引导故事'],
'viewer.guided.selectBeatLink': ['Select a Story Beat to copy its exact link', '选择故事节点以复制其精确链接'],
'viewer.guided.copyMoment': ['Copy moment', '复制此刻'],
'viewer.guided.momentCopied': ['Moment link copied', '已复制时刻链接'],
'viewer.guided.momentCopyFailed': ['Could not copy story moment link', '无法复制故事时刻链接'],
'viewer.guided.copied': ['Copied', '已复制'],
'viewer.guided.copyFailed': ['Copy failed', '复制失败'],
'viewer.guided.showAll': ['Show all', '显示全部'],
'viewer.guided.showAll.aria': ['Show entire diagram', '显示完整图表'],
'viewer.guided.chapters': ['Story chapters', '故事章节'],
'viewer.guided.storyTrail': ['Story trail for {label}: {count} beats', '{label}的故事轨迹:{count} 个节点'],
'viewer.guided.chapter.open': ['Open chapter {index} of {total}: {label}, {count} stops', '打开第 {index}/{total} 章:{label}{count} 个停靠点'],
'viewer.guided.chapter.current': ['Current chapter {index} of {total}: {label}, {count} stops', '当前第 {index}/{total} 章:{label}{count} 个停靠点'],
'viewer.guided.chapter.selectedNodes': ['{count} selected nodes', '已选择 {count} 个节点'],
'viewer.guided.chapter.stops': ['{count} stops', '{count} 个停靠点'],
'viewer.guided.chapter.stop.one': ['{count} stop', '{count} 个停靠点'],
'viewer.guided.chapter.stop.other': ['{count} stops', '{count} 个停靠点'],
'viewer.guided.chapter.current.title': ['{label} — current chapter, {count} stops', '{label} — 当前章节,{count} 个停靠点'],
'viewer.guided.chapter.delta.expanded': ['{stay} stay, {enter} enter, {leave} leave', '{stay} 个保留,{enter} 个进入,{leave} 个离开'],
'viewer.guided.chapter.delta.aria': ['Open chapter {index} of {total}: {label}. Chapter focus delta: {delta}', '打开第 {index}/{total} 章:{label}。章节聚焦差异:{delta}'],
'viewer.guided.chapter.delta.title': ['{label} — {delta} chapter focus', '{label} — 章节聚焦 {delta}'],
'viewer.guided.handoff': ['{from} → {to} · via {label}', '{from} → {to} · 经由{label}'],
'viewer.guided.share.chapter': ['Chapter {index} / {total}', '章节 {index} / {total}'],
'viewer.guided.share.initial': ['Chapter 01 / 01', '章节 01 / 01'],
'viewer.guided.share.default': ['Guided chapter', '引导章节'],
'viewer.guided.state.ready': ['Ready', '就绪'],
'viewer.guided.state.playing': ['Playing', '播放中'],
'viewer.guided.state.settled': ['Settled', '已完成'],
'viewer.guided.state.paused': ['Paused', '已暂停'],
'viewer.guided.state.pinned': ['Pinned', '已固定'],
'viewer.guided.state.still': ['Still', '静态'],
'viewer.guided.share.step': ['Step {index} / {total} · {label}', '步骤 {index} / {total} · {label}'],
'viewer.guided.share.staticMoment': ['{step} · Static moment', '{step} · 静态时刻'],
'viewer.guided.share.complete': ['{count} steps complete · {note}', '{count} 个步骤已完成 · {note}'],
'viewer.guided.share.settled': ['Path settled for reading.', '路径已稳定,可供阅读。'],
'viewer.guided.share.staticPath': ['{count} steps · Static path', '{count} 个步骤 · 静态路径'],
'viewer.guided.share.ready': ['{count} steps · Ready', '{count} 个步骤 · 就绪'],
'viewer.guided.share.aria': ['{state} chapter {index} of {total}: {label}. {beat}. {route}', '{state},第 {index}/{total} 章:{label}。{beat}。{route}'],
'viewer.guided.beat.start': ['Beat {index} / {total} · {label} · starting point', '节点 {index} / {total} · {label} · 起点'],
'viewer.guided.beat.forward': ['Beat {index} / {total} · {from} → {to}', '节点 {index} / {total} · {from} → {to}'],
'viewer.guided.beat.reverse': ['Beat {index} / {total} · {from} → {to} · reverse authored link', '节点 {index} / {total} · {from} → {to} · 反向编写连接'],
'viewer.guided.beat.multiple': ['Beat {index} / {total} · {from} ⇄ {to} · {count} authored links', '节点 {index} / {total} · {from} ⇄ {to} · {count} 条编写连接'],
'viewer.guided.beat.group': ['Beat {index} / {total} · {from} · {to} · grouped · no direct link', '节点 {index} / {total} · {from} · {to} · 分组 · 无直接连接'],
'viewer.guided.beat.aria.prefix': ['Story beat {index} of {total}: {label}. ', '故事节点 {index}/{total}{label}。'],
'viewer.guided.beat.aria.start': ['Starting point.', '起点。'],
'viewer.guided.beat.aria.forward': ['From {from} through one authored forward relationship.', '从{from}经一条正向编写关系到达。'],
'viewer.guided.beat.aria.reverse': ['From {from}; the authored relationship points from {to} to {from}.', '从{from}出发;编写关系实际由{to}指向{from}。'],
'viewer.guided.beat.aria.multiple': ['From {from} through {count} authored relationships; shown without arbitrary motion.', '从{from}经 {count} 条编写关系到达;不使用任意动效。'],
'viewer.guided.beat.aria.group': ['Grouped from {from} with no direct authored relationship.', '与{from}分组展示,没有直接编写关系。'],
'viewer.guided.caption.start': ['Starting point', '起点'],
'viewer.guided.caption.grouped': ['Grouped transition · no direct authored link', '分组过渡 · 无直接编写连接'],
'viewer.guided.caption.more': [' +{count} more', ' +另外 {count} 条'],
'viewer.guided.caption.reverse': ['Reverse authored relationship', '反向编写关系'],
'viewer.guided.caption.relationships': ['{count} authored relationships', '{count} 条编写关系'],
'viewer.guided.caption.relationship': ['Authored relationship', '编写关系'],
'viewer.guided.caption.direction': ['authored direction: {from} → {to}', '编写方向:{from} → {to}'],
'viewer.guided.caption.starting': ['Authored starting point', '编写起点'],
'viewer.guided.beatLink': ['Copy link to current story moment: Beat {index} of {total}: {label}', '复制当前故事时刻链接:第 {index}/{total} 个节点:{label}'],
'viewer.guided.noStory': ['This diagram has no authored guided story.', '此图表没有编写引导故事。'],
'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.story': ['Play the guided story', '播放引导故事'],
'viewer.guide.story.hint': ['Walk the authored chapters and real relationships.', '浏览已编写的章节和真实关系。'],
'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} · {views}', '{nodes} · {relationships} · {views}'],
'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.fact.view.one': ['{count} guided view', '{count} 个引导视图'],
'viewer.guide.fact.view.other': ['{count} guided views', '{count} 个引导视图'],
'viewer.guide.story.available.one': ['Walk {count} authored chapter and its real relationships.', '浏览 {count} 个已编写章节及其真实关系。'],
'viewer.guide.story.available.other': ['Walk {count} authored chapters and their real relationships.', '浏览 {count} 个已编写章节及其真实关系。'],
'viewer.guide.story.unavailable': ['No authored guided story in this diagram.', '此图表没有编写引导故事。'],
'viewer.guide.open': ['Open diagram guide', '打开图表指南'],
'viewer.guide.noStory': ['This diagram has no authored guided story.', '此图表没有编写引导故事。'],
'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.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.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': ['Route probe', '路径探测'],
'viewer.route.start': ['Choose a start node', '选择起点节点'],
'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': ['Traced route', '已追踪路径'],
'viewer.route.pickTwo': ['Pick two semantic nodes on the diagram', '在图表中选择两个语义节点'],
'viewer.route.pickOne': ['Pick a semantic 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': ['Choose the source, then the destination. Direction matters.', '先选择来源,再选择目标;方向很重要。'],
'viewer.route.destination': ['Choose a destination from {label}', '选择从{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': ['A route needs two distinct semantic nodes.', '一条路径需要两个不同的语义节点。'],
'viewer.route.unreachable': ['No directed route to {label}', '没有通往{label}的有向路径'],
'viewer.route.unreachable.detail': ['{target} is not reachable from {source}. Pick a highlighted destination.', '从{source}无法到达{target}。请选择高亮的目标。'],
'viewer.route.start.instructions': ['Select the source. The next step will reveal only directed destinations.', '选择来源。下一步只会显示有向可达的目标。'],
'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 authored route', '{nodes} · {hops} · 最短编写路径'],
'viewer.route.overview.node.one': ['{count} node', '{count} 个节点'],
'viewer.route.overview.node.other': ['{count} nodes', '{count} 个节点'],
'viewer.route.overview.hop.one': ['{count} directed hop', '{count} 个有向跳转'],
'viewer.route.overview.hop.other': ['{count} directed hops', '{count} 个有向跳转'],
'viewer.route.phase.playing': ['Playing', '播放中'],
'viewer.route.phase.complete': ['Complete', '已完成'],
'viewer.route.phase.inspecting': ['Inspecting', '检查中'],
'viewer.route.destination.count.one': ['{count} directed destination available. Pick a highlighted node.', '有 {count} 个有向目标可用。请选择高亮节点。'],
'viewer.route.destination.count.other': ['{count} directed destinations available. Pick a highlighted node.', '有 {count} 个有向目标可用。请选择高亮节点。'],
'viewer.route.noOutgoing': ['No outgoing route starts here. Clear and choose another source.', '此处没有可用的出向路径。请清除后选择其他来源。'],
'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.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 CATALOGS = Object.fromEntries(SUPPORTED_LOCALES.map((locale, index) => [
locale,
Object.fromEntries(Object.entries(MESSAGE_PAIRS).map(([key, pair]) => [key, pair[index]])),
]));
export function resolveLocale(locale) {
return SUPPORTED_LOCALES.includes(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);
if (!Object.hasOwn(CATALOGS[resolved], key)) {
throw new Error(`Missing Archify i18n message ${JSON.stringify(key)} for ${resolved}`);
}
return formatMessage(CATALOGS[resolved][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(CATALOGS[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 Object.keys(MESSAGE_PAIRS);
}