import { createContext, useContext, useState, type ReactNode } from 'react'
type Lang = 'zh' | 'en'
const translations = {
zh: {
// Nav
'nav.inbox': '收件箱',
'nav.sessions': '会话',
'nav.memory': '记忆',
'nav.status': '状态',
'nav.lint': '健康',
'nav.index': '导航',
// Memory tabs
'memory.active': '活跃记忆',
'memory.review': '待审核',
'memory.context': '上下文检索',
'memory.wakeup': '唤醒包',
'memory.index': '导航',
// Settings
'settings.title': '设置',
'settings.vault': '知识库',
'settings.vault.path': 'Obsidian Vault 路径',
'settings.tools': 'AI 工具',
'settings.tools.detect': '重新检测',
'settings.tools.inject': '注入',
'settings.tools.injected': '已注入',
'settings.tools.not_injected': '未注入',
'settings.tools.not_installed': '未安装',
'settings.rules': '自定义规则',
'settings.rules.extraction': '提炼触发词',
'settings.rules.suppress': '抑制规则',
'settings.rules.save': '保存规则',
'settings.rules.add': '添加',
'settings.data': '数据',
'settings.data.export': '导出记忆',
'settings.data.import': '导入记忆',
'settings.lang': '语言',
'settings.about': '关于',
'settings.about.version': '版本',
'settings.about.runtime': '运行时',
'settings.about.config': '配置',
// Status
'status.connected': '已连接',
'status.standalone': '独立运行',
// Empty states
'empty.no_memories': '暂无活跃记忆。说"记一下"开始积累。',
// Popover
'popover.search.placeholder': '搜索记忆… (/ 聚焦)',
'popover.open_main': '打开主面板',
'popover.tab.pending': '待审',
'popover.tab.wakeup': '可唤',
'popover.project.label': '项目',
'popover.project.switch': '切换',
'popover.loading': '加载中…',
'popover.empty.pending.title': '待审队列为空',
'popover.empty.pending.desc': '当 AI 在对话中提议记忆后,待审项将出现在这里',
'popover.empty.wakeup.title': '暂无可唤醒记录',
'popover.empty.wakeup.desc': '通过审核的记忆会进入可唤醒池,在未来的对话中自动召回',
'popover.empty.search.title': '无匹配记录',
'popover.empty.search.desc': '尝试其他关键词',
'popover.quick.button': '快速记录',
'popover.quick.title.placeholder': '标题',
'popover.quick.summary.placeholder': '摘要',
'popover.quick.cancel': '取消',
'popover.quick.save': '保存',
'popover.action.accept': '采纳',
'popover.action.archive': '归档',
'popover.config.prompt': '请先在主面板完成首次配置',
'popover.config.open': '打开主面板',
// Workbench
'workbench.queue': '队列',
'workbench.tab.pending': '待审核',
'workbench.tab.wakeup': '可唤醒',
'workbench.search.placeholder': '搜索标题…',
'workbench.filter.all': '全部',
'workbench.action.manual': '手动记录',
'workbench.action.propose': 'AI 提议',
'workbench.batch.accept': '批量采纳',
'workbench.batch.archive': '批量归档',
'workbench.batch.selected': '已选 {n} 条',
'workbench.select_all': '全选当前页',
'workbench.empty.pending.title': '待审队列为空',
'workbench.empty.pending.desc': '当 AI 提议记忆后会出现在这里',
'workbench.empty.wakeup.title': '暂无可唤醒记录',
'workbench.empty.wakeup.desc': '通过审核的记忆会进入可唤醒池',
'workbench.detail.empty': '从左侧队列选择一条记录',
'workbench.detail.empty.desc': '查看详情与历史',
'workbench.history.title': '历史',
'workbench.history.events': '{n} 条事件',
'workbench.keyboard.hint': 'j/k 上下 · a 采纳 · p 晋升 · x 归档 · / 搜索',
'workbench.action.accept': '采纳',
'workbench.action.promote': '晋升固化',
'workbench.action.archive': '归档',
},
en: {
// Nav
'nav.inbox': 'Inbox',
'nav.sessions': 'Sessions',
'nav.memory': 'Memory',
'nav.status': 'Status',
'nav.lint': 'Health',
'nav.index': 'Index',
// Memory tabs
'memory.active': 'Active',
'memory.review': 'Review',
'memory.context': 'Context',
'memory.wakeup': 'Wakeup',
'memory.index': 'Index',
// Settings
'settings.title': 'Settings',
'settings.vault': 'Knowledge Base',
'settings.vault.path': 'Obsidian Vault Path',
'settings.tools': 'AI Tools',
'settings.tools.detect': 'Re-detect',
'settings.tools.inject': 'Inject',
'settings.tools.injected': 'Injected',
'settings.tools.not_injected': 'Not Injected',
'settings.tools.not_installed': 'Not Installed',
'settings.rules': 'Custom Rules',
'settings.rules.extraction': 'Extraction Triggers',
'settings.rules.suppress': 'Suppress Rules',
'settings.rules.save': 'Save Rules',
'settings.rules.add': 'Add',
'settings.data': 'Data',
'settings.data.export': 'Export',
'settings.data.import': 'Import',
'settings.lang': 'Language',
'settings.about': 'About',
'settings.about.version': 'Version',
'settings.about.runtime': 'Runtime',
'settings.about.config': 'Config',
// Status
'status.connected': 'Connected',
'status.standalone': 'Standalone',
// Empty states
'empty.no_memories': 'No active memories. Say "remember this" to start.',
// Popover
'popover.search.placeholder': 'Search memories… (/ to focus)',
'popover.open_main': 'Open main panel',
'popover.tab.pending': 'Pending',
'popover.tab.wakeup': 'Wakeup',
'popover.project.label': 'Project',
'popover.project.switch': 'Switch',
'popover.loading': 'Loading…',
'popover.empty.pending.title': 'Queue is empty',
'popover.empty.pending.desc': 'AI-proposed memories will appear here after a conversation',
'popover.empty.wakeup.title': 'No wakeup-ready records',
'popover.empty.wakeup.desc': 'Accepted memories enter the wakeup pool for future recall',
'popover.empty.search.title': 'No matches',
'popover.empty.search.desc': 'Try different keywords',
'popover.quick.button': 'Quick capture',
'popover.quick.title.placeholder': 'Title',
'popover.quick.summary.placeholder': 'Summary',
'popover.quick.cancel': 'Cancel',
'popover.quick.save': 'Save',
'popover.action.accept': 'Accept',
'popover.action.archive': 'Archive',
'popover.config.prompt': 'Please complete setup in the main panel first',
'popover.config.open': 'Open main panel',
// Workbench
'workbench.queue': 'Queue',
'workbench.tab.pending': 'Pending',
'workbench.tab.wakeup': 'Wakeup',
'workbench.search.placeholder': 'Search title…',
'workbench.filter.all': 'All',
'workbench.action.manual': 'Manual',
'workbench.action.propose': 'AI Propose',
'workbench.batch.accept': 'Batch Accept',
'workbench.batch.archive': 'Batch Archive',
'workbench.batch.selected': '{n} selected',
'workbench.select_all': 'Select all',
'workbench.empty.pending.title': 'Queue is empty',
'workbench.empty.pending.desc': 'AI-proposed memories will appear here',
'workbench.empty.wakeup.title': 'No wakeup-ready records',
'workbench.empty.wakeup.desc': 'Accepted memories enter the wakeup pool',
'workbench.detail.empty': 'Select a record from the queue',
'workbench.detail.empty.desc': 'View details and history',
'workbench.history.title': 'History',
'workbench.history.events': '{n} events',
'workbench.keyboard.hint': 'j/k navigate · a accept · p promote · x archive · / search',
'workbench.action.accept': 'Accept',
'workbench.action.promote': 'Promote',
'workbench.action.archive': 'Archive',
},
} as const
type TranslationKey = keyof typeof translations.zh
type I18nContext = {
lang: Lang
setLang: (l: Lang) => void
t: (key: TranslationKey) => string
}
const I18nCtx = createContext<I18nContext | null>(null)
export function I18nProvider({ children }: { children: ReactNode }) {
const [lang, setLangState] = useState<Lang>(() => {
if (typeof window === 'undefined') return 'zh'
return (localStorage.getItem('spool.lang') as Lang) || 'zh'
})
function setLang(l: Lang) {
setLangState(l)
localStorage.setItem('spool.lang', l)
}
function t(key: TranslationKey): string {
return translations[lang][key] || key
}
return (
<I18nCtx.Provider value={{ lang, setLang, t }}>
{children}
</I18nCtx.Provider>
)
}
export function useI18n() {
const ctx = useContext(I18nCtx)
if (!ctx) throw new Error('useI18n must be inside <I18nProvider>')
return ctx
}