spool-memory 0.1.0

Local-first developer memory system — persistent, structured knowledge for AI coding tools
Documentation
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
}