/** * Minimal zh/en copy for the sidebar. The plugin renders outside the slot * system's locale seat, so it reads the browser language directly (the DSH * locale preference and the browser language agree in practice; both default * from navigator.language anyway). */ const zh = { explorer: '资源管理器', git: '源代码管理', terminal: '终端', editor: '编辑器', newTab: '新建标签页', openExplorer: '资源管理器', openGit: 'Git 面板', newTerminal: '新终端', terminalLimit: '终端数量已达上限 (3)', close: '关闭', collapse: '折叠侧边栏', expand: '展开侧边栏', expandFullscreen: '展开为全屏', restoreFullscreen: '还原宽度', terminalError: '终端连接失败', terminalConnectFailed: '终端多次连接失败', terminalRetry: '重试', preview: '预览', edit: '编辑', refresh: '刷新', save: '保存', saved: '已保存', unsaved: '未保存', saveFailed: '保存失败', truncation: '文件过大,仅显示前 512KB', binary: '二进制文件,无法预览', loading: '加载中…', error: '加载失败', retry: '重试', splitLeft: '向左分栏', splitRight: '向右分栏', splitUp: '向上分栏', splitDown: '向下分栏', notRepo: '当前目录不是 git 仓库', noChanges: '没有变更', stage: '暂存', unstage: '取消暂存', stageAll: '全部暂存', unstageAll: '全部取消暂存', commitPlaceholder: '提交信息 (Ctrl+Enter)', commit: '提交', commitError: '提交失败', branch: '分支', checkoutError: '切换分支失败', history: '历史', changes: '变更', staged: '已暂存', unstaged: '未暂存', cancel: '取消', diffEmpty: '没有文本差异', diffLoadError: '加载差异失败', diffBinary: '二进制', diffAdded: '新增', diffDeleted: '删除', diffRenamed: '重命名', diffExpand: '展开其余 {count} 行', diffCollapse: '收起', discard: '放弃更改', discardTitle: '放弃更改', discardDesc: '将丢弃「{path}」的工作区修改(不可恢复)。', viewCommitDiff: '查看提交差异', copyShortHash: '复制短哈希', copyFullHash: '复制完整哈希', copySubject: '复制提交信息', revertCommit: '还原此提交', revertTitle: '还原此提交', revertDesc: '将在当前分支创建一个反转「{subject}」的新提交。', cherryPickCommit: '捡取此提交', cherryPickTitle: '捡取此提交', cherryPickDesc: '将「{subject}」的更改应用到当前分支。', timeJustNow: '刚刚', timeMinutesAgo: '{n} 分钟前', timeHoursAgo: '{n} 小时前', timeYesterday: '昨天', loadMore: '加载更多', historyLoadError: '加载更多历史失败', produced: '本次产出', producedOpen: '在侧边栏中打开', disconnected: '终端连接断开,重连中…', exited: '终端进程已退出', noSession: '选择一个会话以使用侧边栏', pluginNotLoaded: '插件未加载,标签页暂不可用:', hiddenFiles: '隐藏文件', parent: '上级目录', copied: '已复制', copy: '复制', newFile: '新文件', openEditor: '打开编辑器', gitDetail: '查看变更详情', referenceFile: '@文件', copyRelative: '复制相对地址', copyAbsolute: '复制绝对地址', download: '下载', settingsNav: '侧边卡片', settingsOpenTitle: '新会话默认打开', settingsOpenDesc: '新建会话时自动展开侧边卡片;已存在的会话保持各自布局', settingsWidthTitle: '默认宽度占比', settingsWidthDesc: '新建会话时侧边卡片占窗口宽度的百分比 (20–60)', settingsWidthSuffix: '%', settingsSaveFailed: '保存失败', settingsConflict: '设置已被其他窗口修改,请重试', binaryNoPreview: '此文件类型不支持预览', downloadToView: '下载查看', officeTooLarge: '文件过大,无法预览', officeCorrupt: '文件损坏或格式无效', officeEncrypted: '不支持加密文件', officeLoadFailed: 'Office 预览组件加载失败', previousSlide: '上一页', nextSlide: '下一页', zoom: '缩放', zoomHint: 'Alt + 滚轮', settingsSubagentTitle: '检测到子代理时自动展开子代理页面', settingsSubagentDesc: '当前会话产生新的子代理时,自动展开侧边栏并打开子代理页面;关闭后需手动打开', settingsToolsTitle: '为模型注入终端工具', settingsToolsDesc: '开启后,模型可通过 terminal_create 等 8 个工具创建并操作侧边栏终端(默认关闭)', settingsTabsTitle: '侧边栏内容', settingsViewersTitle: '文件预览', settingsViewerCatchAll: '兜底:任意文件', viewerImage: '图片', viewerPdf: 'PDF', viewerDocx: 'Word 文档', viewerXlsx: 'Excel 表格', viewerPptx: 'PPT 演示', viewerMarkdown: 'Markdown', viewerCode: '代码', viewerBinary: '二进制下载', subagent: '子代理', openSubagent: '子代理', subagentMainAgent: '主代理', subagentEmpty: '暂无子代理', subagentEmptyDesc: '当前主代理派生的子代理将显示在这里', subagentRunning: '运行中', subagentInactive: '空闲', subagentModeOneShot: '一次性', subagentModeContinuable: '可续接', subagentCount: '{count} 个子代理', subagentCountRunning: '{count} 个子代理 · {running} 运行中', subagentDiagCorrupt: '目录损坏', subagentDiagUnsupported: '不支持的条目', subagentDiagUnavailable: '不可用', subagentThinking: '思考中…', } const en: Record = { explorer: 'Explorer', git: 'Source Control', terminal: 'Terminal', editor: 'Editor', newTab: 'New tab', openExplorer: 'Explorer', openGit: 'Git panel', newTerminal: 'New terminal', terminalLimit: 'Terminal limit reached (3)', close: 'Close', collapse: 'Collapse sidebar', expand: 'Expand sidebar', expandFullscreen: 'Expand to full width', restoreFullscreen: 'Restore width', terminalError: 'Terminal connection failed', terminalConnectFailed: 'Terminal failed to connect repeatedly', terminalRetry: 'Retry', preview: 'Preview', edit: 'Edit', refresh: 'Refresh', save: 'Save', saved: 'Saved', unsaved: 'Unsaved', saveFailed: 'Save failed', truncation: 'File too large — showing the first 512KB', binary: 'Binary file, preview unavailable', loading: 'Loading…', error: 'Failed to load', retry: 'Retry', splitLeft: 'Split left', splitRight: 'Split right', splitUp: 'Split up', splitDown: 'Split down', notRepo: 'This directory is not a git repository', noChanges: 'No changes', stage: 'Stage', unstage: 'Unstage', stageAll: 'Stage all', unstageAll: 'Unstage all', commitPlaceholder: 'Commit message (Ctrl+Enter)', commit: 'Commit', commitError: 'Commit failed', branch: 'Branch', checkoutError: 'Branch switch failed', history: 'History', changes: 'Changes', staged: 'Staged', unstaged: 'Unstaged', cancel: 'Cancel', diffEmpty: 'No text changes', diffLoadError: 'Failed to load diff', diffBinary: 'Binary', diffAdded: 'Added', diffDeleted: 'Deleted', diffRenamed: 'Renamed', diffExpand: 'Expand {count} more rows', diffCollapse: 'Collapse', discard: 'Discard changes', discardTitle: 'Discard changes', discardDesc: 'This discards the worktree changes of "{path}" (not recoverable).', viewCommitDiff: 'View commit diff', copyShortHash: 'Copy short hash', copyFullHash: 'Copy full hash', copySubject: 'Copy subject', revertCommit: 'Revert commit', revertTitle: 'Revert commit', revertDesc: 'Create a new commit on the current branch that reverts "{subject}".', cherryPickCommit: 'Cherry-pick commit', cherryPickTitle: 'Cherry-pick commit', cherryPickDesc: 'Apply the changes of "{subject}" to the current branch.', timeJustNow: 'just now', timeMinutesAgo: '{n} min ago', timeHoursAgo: '{n} h ago', timeYesterday: 'yesterday', loadMore: 'Load more', historyLoadError: 'Failed to load more history', produced: 'Produced', producedOpen: 'Open in sidebar', disconnected: 'Terminal disconnected, reconnecting…', exited: 'Terminal process exited', noSession: 'Select a conversation to use the sidebar', pluginNotLoaded: 'Plugin not loaded; tab unavailable:', hiddenFiles: 'Hidden files', parent: 'Parent directory', copied: 'Copied', copy: 'Copy', newFile: 'New file', openEditor: 'Open editor', gitDetail: 'View change details', referenceFile: '@file', copyRelative: 'Copy relative path', copyAbsolute: 'Copy absolute path', download: 'Download', settingsNav: 'Side card', settingsOpenTitle: 'Open by default for new conversations', settingsOpenDesc: 'Expand the side card automatically for brand-new conversations; existing conversations keep their own layouts', settingsWidthTitle: 'Default width share', settingsWidthDesc: 'The side card\'s default share of the window width for new conversations (20–60)', settingsWidthSuffix: '%', settingsSaveFailed: 'Failed to save', settingsConflict: 'The setting changed in another window — please retry', binaryNoPreview: 'This file type cannot be previewed', downloadToView: 'Download to view', officeTooLarge: 'File too large to preview', officeCorrupt: 'File is corrupt or in an invalid format', officeEncrypted: 'Encrypted files are not supported', officeLoadFailed: 'Office preview component failed to load', previousSlide: 'Previous', nextSlide: 'Next', zoom: 'Zoom', zoomHint: 'Alt + wheel', settingsSubagentTitle: 'Auto-open the Subagent page when a subagent appears', settingsSubagentDesc: 'Expand the side card and open the Subagent page when the current conversation spawns a new subagent; turn off to open it manually', settingsToolsTitle: 'Inject terminal tools for the model', settingsToolsDesc: 'When enabled, the model can create and drive sidebar terminals through the 8 terminal_* tools (off by default)', settingsTabsTitle: 'Sidebar content', settingsViewersTitle: 'File viewers', settingsViewerCatchAll: 'Catch-all: any file', viewerImage: 'Image', viewerPdf: 'PDF', viewerDocx: 'Word', viewerXlsx: 'Excel', viewerPptx: 'PowerPoint', viewerMarkdown: 'Markdown', viewerCode: 'Code', viewerBinary: 'Binary download', subagent: 'Subagents', openSubagent: 'Subagents', subagentMainAgent: 'Main agent', subagentEmpty: 'No subagents', subagentEmptyDesc: 'Subagents spawned under the main agent will appear here', subagentRunning: 'Running', subagentInactive: 'Inactive', subagentModeOneShot: 'One-shot', subagentModeContinuable: 'Continuable', subagentCount: '{count} subagents', subagentCountRunning: '{count} subagents · {running} running', subagentDiagCorrupt: 'Corrupt', subagentDiagUnsupported: 'Unsupported', subagentDiagUnavailable: 'Unavailable', subagentThinking: 'Thinking…', } /** Translate a copy key in the browser's language (zh → zh, else en). */ export type CopyKey = keyof typeof zh /** Translate a copy key; `{name}` placeholders interpolate from `params`. */ export function t(key: CopyKey, params?: Record): string { const lang = typeof navigator !== 'undefined' ? navigator.language : 'en' const dict = lang.toLowerCase().startsWith('zh') ? zh : en let text = dict[key] if (params !== undefined) { for (const [name, value] of Object.entries(params)) { text = text.replaceAll(`{${name}}`, String(value)) } } return text } /** Whether the browser language is Chinese (used for selectors). */ export function isZh(): boolean { return typeof navigator !== 'undefined' && navigator.language.toLowerCase().startsWith('zh') } /** Format an ISO 8601 author date relative to now (刚刚 / N 分钟前 / N 小时前 / 昨天 / date). */ export function relativeTime(iso: string): string { const then = Date.parse(iso) if (Number.isNaN(then)) return iso const seconds = Math.floor((Date.now() - then) / 1000) if (seconds < 60) return t('timeJustNow') if (seconds < 3600) return t('timeMinutesAgo', { n: Math.floor(seconds / 60) }) if (seconds < 86400) return t('timeHoursAgo', { n: Math.floor(seconds / 3600) }) if (seconds < 172800) return t('timeYesterday') const date = new Date(then) const pad = (value: number): string => String(value).padStart(2, '0') return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}` }