import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from "react";

export type UiLanguage = "zh" | "en";

const STORAGE_KEY = "uiLanguage:v2";
const LEGACY_STORAGE_KEY = "uiLanguage";

const enPlural = (count: number, one: string, many = `${one}s`) => `${count} ${count === 1 ? one : many}`;

const zh = {
  app: {
    brandSubtitle: "短剧 Agent 工坊",
    newSession: "新建 Session",
    unnamed: "未命名",
    shotCount: (count: number) => `${count} 镜`,
    archiveSessions: "历史 session",
    loadingSessions: "正在加载 session...",
    emptySession: "点上方「新建 Session」开工",
    galleryNav: "Gallery",
    galleryEyebrow: "创作者分享平台",
    galleryTitle: "Gallery 创作广场",
    gallerySubtitle: "预览别人分享的成片，一键复制 session 到自己的画布继续改。",
    galleryPublish: "发布到 Gallery",
    galleryPublishTitle: "发布当前 session 到 Gallery",
    galleryPublishSubmit: "发布",
    galleryPublishCancel: "取消",
    galleryTitlePlaceholder: "Gallery 标题",
    galleryDescriptionPlaceholder: "介绍这个创作，可选",
    galleryCreatorPlaceholder: "创作者署名，可选",
    galleryTagsPlaceholder: "标签，用空格或逗号分隔",
    galleryPublished: (title: string) => `已发布到 Gallery：${title}`,
    galleryMeta: (shots: number, duration: number) => `${shots} 镜 · ${duration}s`,
    galleryCopy: "复制继续创作",
    galleryDelete: "删除",
    galleryDeleteTitle: "从 Gallery 删除",
    galleryDeleteConfirm: (title: string) => `确定从 Gallery 删除「${title}」吗？此操作不可撤销。`,
    galleryDeleted: (title: string) => `已从 Gallery 删除：${title}`,
    galleryBackToCanvas: "回到 Canvas",
    galleryNoPreview: "暂无视频预览",
    galleryNoDescription: "创作者没有留下介绍。",
    galleryAnonymous: "匿名创作者",
    galleryEmptyTitle: "Gallery 还没有作品",
    galleryEmptyBody: "在任意 session 生成视频后，点击「发布到 Gallery」分享给其他人 remix。",
    deleteSessionTitle: "删除 session（不可撤销）",
    deleteSelectedSessionsTitle: "删除选中的 session（不可撤销）",
    deleteSelectedSessions: (count: number) => `删除选中 ${count} 个`,
    deleteSelectedSessionsConfirm: (count: number) => `删除选中的 ${count} 个 session？此操作不可撤销，并会清理不再被引用的本地和云端媒体。`,
    promoteSessionTitle: "置顶到当前",
    delete: "删除",
    sessionNameAria: "Session 名称",
    shareTitle: "复制本 session 的可分享链接",
    copiedLink: (url: string) => `已复制链接：${url}`,
    copyPrompt: "复制下面的链接：",
    copyLink: "🔗 复制链接",
    createProjectTitle: "创建一个短片项目",
    languageToggleTitle: "切换界面语言",
    zhLabel: "中文",
    enLabel: "EN",
    visionReview: "自审重试",
    visionReviewTitle: "开启后：每张资产/分镜图、每条分镜视频生成完会用 vision 模型自审，违背 prompt/参考图就重生，最多 5 次。会消耗额外 token。",
    refresh: "刷新",
    usage: "查看用量",
    usageTitle: "查看各 session 的 Seedream / Seedance token 用量",
    serverDownTitle: "⚠ 服务端不可达",
    serverDownBody: "后端 dev server 没响应。检查终端是否还在跑 npm run dev，必要时重启。这条会在后端恢复时自动消失。",
    deleteSessionConfirm: (title: string) => `删除 session「${title}」？此操作不可撤销。`,
    clearTokenUsageConfirm: (title: string) => `清空 session「${title}」的所有节点 token 统计？`,
    createShotUndo: "新建视频",
    deleteUndo: (label: string) => `删除${label}`,
    pendingVideoName: "上传中视频",
    pendingUpload: "上传中…",
    uploadFailed: "上传失败",
    uploadFailedWithMessage: (message: string) => `上传失败：${message}`,
    importSession: "载入 Session",
    importSessionTitle: "载入 .seereel-session 包，复制成当前浏览器的可编辑 session",
    importSessionInvalid: "不是有效的 SeeReel session 包",
    importSessionComplete: (title: string) => `已载入 Session：${title}`,
    exportSession: "下载 Session",
    exportSessionTitle: "下载当前 session、节点、连线和本地预览媒体",
    sessionExportStarted: (title: string) => `开始下载 Session 包：${title}`,
    credentialPanelSet: "API Keys",
    credentialPanelReady: "API Keys 已配置",
    credentialPanelMissingTitle: "配置 API Keys",
    credentialPanelConfiguredTitle: (source: string, fingerprint: string) => fingerprint ? `当前浏览器已配置${source}：${fingerprint}` : `当前浏览器已配置${source}`,
    standardCredentialTab: "API 设置",
    byteplusCredentialTab: "BP API",
    cnCredentialTab: "CN API",
    byteplusCredentialLabel: "BytePlus API",
    cnCredentialLabel: "CN API",
    agentPlanCredentialTab: "Agent Plan 设置",
    standardCredentialSet: "配置标准 API Key",
    standardCredentialReady: "标准 API 已配置",
    standardCredentialPlaceholder: "粘贴 BP_ARK_API_KEY / BP_SEEDANCE_API_KEY",
    cnCredentialPlaceholder: "粘贴 CN_ARK_API_KEY / CN_SEEDANCE_API_KEY",
    standardCredentialHelpText: "BytePlus 路由：https://ark.ap-southeast.bytepluses.com/api/v3；优先级最高。",
    cnCredentialHelpText: "火山 CN 路由：https://ark.cn-beijing.volces.com/api/v3；未配置 BP API 时使用。",
    agentPlanSet: "配置火山 Agent Plan Key",
    agentPlanReady: "火山 Agent Plan 已配置",
    agentPlanMissingTitle: "输入你自己的火山 Agent Plan Key",
    agentPlanConfiguredTitle: (fingerprint: string) => fingerprint ? `当前浏览器已配置火山 Agent Plan：${fingerprint}` : "当前浏览器已配置火山 Agent Plan",
    agentPlanFingerprint: (fingerprint: string) => `指纹 ${fingerprint}`,
    agentPlanPlaceholder: "粘贴火山 Agent Plan Key",
    agentPlanHelpText: "用于调用 Agent Plan 路由；未配置标准 API Key 时使用。",
    freeTrialRemaining: (remaining: number, limit: number) => `免费试用剩余 ${remaining}/${limit} 次`,
    freeTrialDepleted: "免费试用已用完，请配置自己的 Agent Plan Key",
    agentPlanOpenTitle: "开通火山 Agent Plan",
    agentPlanSave: "保存",
    agentPlanClear: "清除",
    adminButton: "admin",
    adminButtonTitle: "打开控制台",
    adminLoginTitle: "控制台登录",
    adminLoginHint: "仅供站点管理员使用。请输入管理员凭据。",
    adminUsernamePlaceholder: "管理员用户名",
    adminPasswordPlaceholder: "管理员密码",
    adminLogin: "登录",
    adminConsoleTitle: "控制台",
    adminTrialTitle: "站点 Agent Plan",
    adminTrialConfigured: (fingerprint: string | undefined, source: string | undefined) =>
      `已配置${fingerprint ? ` · ${fingerprint}` : ""}${source ? ` · ${source}` : ""}`,
    adminTrialMissing: "未配置，免费用户无法使用管理员试用额度",
    adminAgentPlanPlaceholder: "粘贴站点 Agent Plan Key",
    adminSave: "保存",
    adminClear: "清除",
    adminLogout: "退出",
    adminSecurityTitle: "登录安全",
    adminSecurityHelp: "设置新的管理员用户名和密码。密码会以哈希形式保存。",
    adminSecurityConfigured: (source: "ui" | "env" | "none"): string => source === "ui" ? "已使用控制台保存的凭据" : "已使用环境变量凭据",
    adminSecurityMissing: "尚未配置管理员密码",
    adminNewUsernamePlaceholder: "新用户名（留空沿用当前）",
    adminNewPasswordPlaceholder: "新密码（至少 8 位）",
    adminSecuritySave: "更新凭据",
    adminUserKeysTitle: "用户输入的 Agent Plan Keys",
    adminUserKeysRefresh: "刷新用户 Agent Plan Keys",
    adminUserKeysStorage: (mode: "database" | "memory", databaseConfigured: boolean, encryptionConfigured: boolean) =>
      mode === "database"
        ? `火山数据库已接入${encryptionConfigured ? " · 已加密" : " · 未配置加密密钥"}`
        : `仅内存缓存${databaseConfigured ? " · 数据库不可用" : " · 未配置数据库"}`,
    adminUserKeysEmpty: "还没有用户保存过 Agent Plan Key"
  },
  token: {
    summary: (input: string, output: string, calls: number) => `输入 ${input} · 输出 ${output} · ${calls} 次调用`,
    clear: "清空统计",
    clearTitle: "清空本 session 的所有节点 token 统计",
    calls: (count: number) => `${count} 次`,
    total: "合计",
    noTrackedFamilies: "还没有 session 记录到这四类模型的 token usage。",
    noUsage: "还没有记录到模型 token usage；生成图片/视频后会自动出现。",
    recentOp: (count: number, op: string) => `${count} 次 · 最近 ${op}`
  },
  flow: {
    emptyTitle: "这里是节点画布",
    emptyHint: "← 左侧侧栏点「新建 Session」或选一个已有 session",
    summary: (shots: number, duration: number | undefined, language: UiLanguage) => `${shots} 个分镜 · 目标 ${duration ?? 0}s · 语言 ${language} · `,
    createNodeHint: "右键空白处可新建节点",
    undo: "↶ 撤销",
    redo: "↷ 恢复",
    undoTitle: (description?: string) => description ? `撤销「${description}」(⌘Z)` : "没有可撤销的操作",
    redoTitle: (description?: string) => description ? `恢复「${description}」(⇧⌘Z)` : "没有可恢复的操作",
    createCharacter: "+ 角色",
    createScene: "+ 场景",
    createShot: "+ 视频",
    uploadVideo: "+ 上传视频",
    createCharacterTitle: "新建一个角色资产做跨分镜的身份锚",
    createSceneTitle: "新建一个场景资产做跨分镜的环境锚",
    createShotTitle: "新增一个视频节点",
    uploadVideoTitle: "上传一段视频文件作为参考。会自动 AI 拆分镜表；也可拖到 Shot 上做 Seedance reference_video。",
    executeFast: "加速执行",
    executing: "执行中...",
    executeFastTitle: "先生成执行计划；无依赖的镜头并发跑，有尾帧/参考上游依赖的镜头按拓扑顺序等待。默认只补齐缺失/失败/正在生成的节点。",
    stitchByConnections: "按连接顺序拼接",
    stitchAll: "拼接全片",
    zoomControlsAria: "画布缩放控制",
    zoom: "缩放"
  },
  menu: {
    aria: "新建节点",
    title: "新建节点",
    hint: "右键空白处呼出",
    image: "图片节点",
    imageHint: "生成或编辑一张图片，I",
    character: "图片节点",
    characterHint: "生成或编辑一张图片，I",
    scene: "图片节点",
    sceneHint: "生成或编辑一张图片，I",
    moodboard: "Moodboard",
    moodboardHint: "统一色彩、光影、画风和年代感，M",
    storyboard: "分镜板",
    storyboardHint: "新增一镜并显示分镜板节点，B",
    shot: "视频节点",
    shotHint: "新增一个视频节点，N",
    stitch: "拼接节点",
    stitchHint: "手动放置完整视频拼接入口，J",
    voice: "声音节点",
    voiceHint: "保存音色/试听，V",
    music: "音乐节点",
    musicHint: "生成可复用 BGM/歌曲，M",
    audioTrack: "音轨节点",
    audioTrackHint: "可选连接到拼接节点，A",
    uploadImage: "上传图片",
    uploadImageHint: "本地图片，作为可编辑图片节点，U",
    uploadCharacter: "上传图片",
    uploadCharacterHint: "本地图片，作为可编辑图片节点，U",
    uploadScene: "上传图片",
    uploadSceneHint: "本地图片，作为可编辑图片节点，U",
    uploadVideo: "上传参考视频",
    uploadVideoHint: "本地视频，拖到 shot 上做参考视频，R"
  },
  nodes: {
    modelPickerTitle: "选择模型版本",
    download: "下载",
    assetTypes: { image: "图片", character: "图片", scene: "图片", prop: "图片", style: "图片", moodboard: "图片", voice: "声音", music: "音乐", other: "其它" },
    statusDraft: "草稿",
    statusScripted: "已写脚本",
    statusQueued: "Seedance 排队中",
    statusRunning: "Seedance 渲染中",
    statusGenerating: "生成中",
    statusReady: "已完成",
    statusError: "出错",
    statusCancelled: "已取消",
    reviewStale: "VLM 已过期",
    reviewRunning: "VLM 审核中",
    reviewError: "VLM 失败",
    reviewNeedsFix: (score: number) => `VLM ${Math.round(score)} 需修`,
    reviewPass: (score: number) => `VLM ${Math.round(score)} ✓`,
    reviewNotRun: "VLM 未审",
    generating: "生成中",
    pendingAssetTitle: "自审重试 + Seedream 单轮 ≈ 30-40s，最多 5 轮",
    reviewImageTitle: "对当前图片做 VLM 评分",
    downloadOriginalImage: (name: string) => `下载 ${name} 原图`,
    notGenerated: "未生成",
    seedreamReviewHint: "Seedream + 自审重试，最长 ~3 分钟",
    nextAssetModelTitle: "该资产下次「重新出图」使用的模型",
    reviewAttempts: (count: number) => `自审重试 ${count}`,
    storyboard: "分镜板",
    storyboardPendingTitle: "Seedream 生成分镜板 + 自审，可能 30s-2min",
    downloadStoryboard: "下载分镜板原图",
    storyboardEmpty: "未生成 · 点开右侧编辑",
    storyboardHint: "Seedream 分镜板，最长 ~2 分钟",
    nextStoryboardModelTitle: "该分镜板下次「重新出图」使用的 Seedream 版本",
    panelCount: (count: number) => `${count} 面板`,
    refImageCount: (count: number) => `参考图 ${count}`,
    video: "视频",
    reviewShotTitle: "用多帧 VLM 标准审核这一镜",
    downloadShot: "下载这一镜的 mp4",
    firstFrame: "首帧",
    firstFrameTitle: "从当前视频抽取首帧，在画布上生成独立首帧节点",
    tailframe: "尾帧",
    tailframeTitle: "从当前视频抽取尾帧，在画布上生成独立尾帧节点",
    tailClip: "尾段视频",
    tailClipTitle: "从当前视频裁出最后 2 秒，在画布上生成独立尾段视频节点",
    elapsed: (value: string) => `已用时 ${value}`,
    nextSeedanceModelTitle: "该镜头下次「生成视频」使用的 Seedance 版本",
    stitch: "拼接",
    stitched: "已拼接",
    stitching: "拼接中",
    stitchError: "出错",
    notStitched: "未拼接",
    connectedSegmentsHint: (count: number) => `已连接 ${count} 段，点开按顺序拼接`,
    stitchEmptyHint: "连接视频到这里，或点开按分镜顺序拼接",
    fullVideo: "完整视频",
    finalReview: "终审",
    finalReviewTitle: "用多帧 VLM 标准审核完整片",
    downloadFinal: "下载完整片",
    targetDuration: (seconds: number | undefined) => `${seconds ?? 0}s 目标`,
    referenceVideo: "参考视频",
    parsing: "解析中",
    parsedShots: (count: number) => `已解析 ${count} 镜`,
    parseFailed: "解析失败",
    parsePending: "待解析",
    downloadReferenceVideo: "下载参考视频",
    notUploaded: "未上传",
    applyParsedHint: "从这里把镜头分析\"应用到\"右侧某条 shot",
    videoProcessor: "视频处理",
    trim15: "截前 15s",
    speedup: "整体加速",
    sampleConcat: "多段拼接",
    unclipped: "未裁剪",
    downloadClip: "下载裁剪结果",
    source: (name: string) => `源：${name}`,
    notExtracted: "未抽取",
    frameAnchor: "帧锚点",
    fromShot: (title: string) => `来自 ${title}`,
    usedBy: (titles: string) => `用于 ${titles}`,
    dragToVideo: "拖到视频节点作为首帧",
    playVideoTitle: "在画布播放视频",
    clickNodeForInspector: "点击节点在右侧编辑"
  },
  toast: {
    downloadStarted: "已开始下载",
    undone: "已撤销",
    redone: "已恢复",
    undoFailed: (message: string) => `撤销失败：${message}`,
    redoFailed: (message: string) => `恢复失败：${message}`
  },
  lightbox: {
    preview: "预览",
    downloadOriginal: "下载原文件",
    download: "下载",
    close: "关闭",
    videoFailed: "视频在浏览器内无法直接播放。",
    openNewTab: "在新标签页打开",
    downloadLocal: "下载到本地"
  },
  mention: {
    aria: "@-mention suggestions",
    wired: "已连",
    empty: "没有匹配的资产 — 按 Esc 关闭，或先连一个进来"
  },
  inspector: {
    generatedAt: "生成时间",
    createdAt: "创建时间",
    openFullSize: "点击查看完整尺寸",
    reviewing: "审片中",
    reviewRunningText: "VLM 正在检查画面与 prompt / 参考资产的一致性。",
    reviewFailed: "审核失败",
    pass: "通过",
    needsFix: "需修",
    selfReviewIssue: "自审发现瑕疵",
    selfReviewIssueText: "最近一次自审发现生成结果仍有瑕疵，可以按下面原因调整 prompt 或参考图后重生。",
    mainIssues: "主要问题",
    suggestedFixes: "建议怎么改",
    addIssuesToPrompt: "把上述问题写进「场景描述 / prompt」的正向约束里。",
    reconnectReferences: "如果是人物或场景不一致，补充或重新连接更明确的参考资产。",
    shotPrefix: (value: string | number) => `镜头 ${value}`,
    framePrefix: (value: string | number) => `帧 ${value}`,
    frameCount: (count: number) => `${count} 帧`,
    videoReviewRunning: "VLM 审片中…正在按多帧标准检查人物、场景、节奏和伪影。",
    videoReviewFailed: (message: string) => `VLM 审核失败：${message}`,
    videoReviewEmpty: "尚未进行 VLM 审片。",
    finalReviewStale: "终审已过期：完整片已重新拼接，请重新 VLM 终审。",
    reviewPromptStale: "VLM 审核已过期：prompt 已更新，请重新审核当前版本。",
    fatalIssues: "致命问题",
    reasons: "主要原因",
    fixes: "建议修复",
    criteria: "评分维度",
    rawVlmJson: "原始 VLM JSON",
    imageReviewRunning: "VLM 审图中…正在检查画面质量、主体结构、prompt 对齐和参考图一致性。",
    imageReviewFailed: (message: string) => `VLM 审图失败：${message}`,
    imageReviewEmpty: "尚未进行 VLM 审图。点击「VLM 评分」可只审核当前图片，不会重新出图。",
    assetTag: (type: string) => `资产 · ${type}`,
    close: "关闭",
    name: "名称",
    myDescription: "我写的描述",
    assetDescriptionHint: (type: string) => `用自由文字描述你想要这个 ${type} 长什么样`,
    assetTypeName: (type: string): string => type === "character" ? "角色" : type === "scene" ? "场景" : type === "moodboard" ? "Moodboard" : "资产",
    promptExpanded: "手写 prompt",
    promptExpandedHint: "按用户手写内容原样提交给 Seedream。",
    promptPlaceholderCharacter: "例：35 岁中年男性，黄仁勋本人，黑色皮夹克，温和坚定的眼神…",
    promptPlaceholderScene: "例：硅谷晚间会议室，落地窗外灯火，桌上摆着 GPU 模型…",
    composedPlaceholder: "直接写最终 prompt",
    composedReady: "按这份 prompt 原样提交。",
    composedMissing: "直接写描述后点「出图」。",
    vlmImageToggleTitle: "勾选后，出图时启用 VLM 审核；审核结果只给反馈，不会改写 prompt。",
    vlmImageToggle: "VLM 审核此图片节点",
    expandPromptTitle: "已停用：prompt 由用户手写",
    expandPrompt: "手写 prompt",
    generateImageTitle: "按当前描述原样提交。点击前会自动保存当前编辑。",
    generateImage: "出图",
    vlmScoreTitle: "只对当前图片做 VLM 打分，不会重新出图",
    vlmScore: "VLM 评分",
    deleteAssetConfirm: (name: string) => `删除资产「${name}」？删除后可在画布顶部「↶ 撤销」恢复。`,
    deleteAssetTitle: "删除这个资产节点（可撤销）",
    delete: "删除",
    saved: "✓ 已保存",
    saving: "保存中…",
    dirty: "● 未保存（即将自动保存）",
    downloadOriginalImage: "⬇ 下载原图",
    currentImagePreview: "当前图片预览（点开看大图）",
    imageGeneratedAt: "图片生成时间",
    vlmImageScore: "VLM 图片评分",
    latestImageReview: "最近一次 VLM 审图",
    moreMetadata: "更多元数据 / 备注",
    actualModel: (model: string) => `实际执行模型：${model}`,
    rawNote: "原始备注（可选；不送给 AI，仅供 asset 列表查看）",
    lastPromptAudit: "上次实际送出的 prompt（审计）",
    retryLog: (count: number) => `自审重试记录（重试 ${count} 次）`,
    autoSaveFailed: "自动保存失败",
    previewFailed: "预览失败",
    saveFailed: "保存失败",
    generateFailed: "生成失败",
    assetGenerateFailedToast: (name: string, message: string) => `❌ ${name || "资产"} 生成失败：${message}`,
    vlmScoreFailed: "VLM 评分失败",
    deleteFailed: "删除失败",
    storyboardTag: (index: number) => `分镜板 · Shot ${index}`,
    storyboardSceneLabel: "场景描述（支持 @ 已连线资产；会喂给 Seedream 的故事板组合 prompt）",
    storyboardScenePlaceholder: "先把角色 / 场景 / 道具 / 风格资产连到分镜，再输入 @ 选择；被 @ 的公开图片会作为 Seedream 4.5 参考图",
    storyboardAutoLayout: (count: number, layout: string) => `自动推算：${count} 个面板 · ${layout} 布局。可在描述里用 \`Beat 1:\` / \`Beat 2:\` / \`Frame 3:\` 标出节拍，系统会按最大编号自动决定面板数。`,
    storyboardSequential: "严格按时间顺序生成（sequential 模式）",
    storyboardSequentialHint: (count: number) => `勾上后每个 panel 都用一次 Seedream（耗时 ~10s × ${count} 张）+ ffmpeg 拼网格。每张以前一张为参考，Beat 顺序硬保证——TL→TR→BL→BR 严格对应你写的 Beat 1/2/3。建议在「场景描述」里用 \`Beat 1: …\` \`Beat 2: …\` 显式分段；不勾就走默认 group call 一次出图，更快但 Seedream 偶尔会乱序。`,
    referenceAssets: "参考资产（跨分镜身份锚定）",
    referenceAssetsHint: "这里仅显示已连线资产；勾选=固定带去 Seedream。也可以在上面的场景描述里输入 @ 选择已连线资产，server 会按当前 asset id 读取最新公开图片。未发布的本地 /media 图不会直接传给远端模型。",
    noReferenceAssets: "还没有连进来的参考资产；先从画布把角色 / 场景资产拖线连接到这个分镜。",
    noImage: "无图",
    selectedReferenceImages: (count: number, names: string) => `已选 ${count} 张：${names}`,
    seedreamFinalPrompt: "送给 Seedream 的最终 prompt（草稿，可改）",
    previewComposePlaceholder: "预览将提交给 Seedream 的 prompt",
    defaultComposeHint: "空表示走默认组装；非空则下次「重新出图」原样使用这一份",
    previewCompose: "预览 prompt",
    regenerateImage: "重新出图",
    generateStoryboard: "生成分镜板",
    downloadStoryboard: "⬇ 下载分镜板原图",
    currentStoryboardPreview: "当前分镜板预览（点开看大图）",
    storyboardTitle: (title: string) => `${title} · 分镜板`,
    storyboardGeneratedAt: "分镜板生成时间",
    lastReferenceImages: (count: number) => `上次实际带去的参考图 (${count})`,
    storyboardGenerateFailed: (index: number, message: string) => `❌ Shot ${index} 分镜板生成失败：${message}`,
    imageGenerateFailed: "出图失败"
  },
  errors: {
    operationFailed: "操作失败",
    unknown: "未知错误",
    networkDown: "网络中断 / 服务端可能挂了 — 检查终端是否还在跑 dev server，必要时重启后重试"
  }
};

export type Dictionary = typeof zh;

const en: Dictionary = {
  app: {
    brandSubtitle: "Agent-native Drama Studio",
    newSession: "New Session",
    unnamed: "Untitled",
    shotCount: (count) => enPlural(count, "shot"),
    archiveSessions: "Past sessions",
    loadingSessions: "Loading sessions...",
    emptySession: "Click “New Session” above to start",
    galleryNav: "Gallery",
    galleryEyebrow: "Creator sharing platform",
    galleryTitle: "Gallery",
    gallerySubtitle: "Preview shared cuts, copy a session into your own canvas, and keep creating.",
    galleryPublish: "Publish to Gallery",
    galleryPublishTitle: "Publish current session to Gallery",
    galleryPublishSubmit: "Publish",
    galleryPublishCancel: "Cancel",
    galleryTitlePlaceholder: "Gallery title",
    galleryDescriptionPlaceholder: "Describe this creation, optional",
    galleryCreatorPlaceholder: "Creator name, optional",
    galleryTagsPlaceholder: "Tags, separated by spaces or commas",
    galleryPublished: (title) => `Published to Gallery: ${title}`,
    galleryMeta: (shots, duration) => `${enPlural(shots, "shot")} · ${duration}s`,
    galleryCopy: "Copy and remix",
    galleryDelete: "Delete",
    galleryDeleteTitle: "Delete from Gallery",
    galleryDeleteConfirm: (title) => `Delete "${title}" from Gallery? This cannot be undone.`,
    galleryDeleted: (title) => `Deleted from Gallery: ${title}`,
    galleryBackToCanvas: "Back to Canvas",
    galleryNoPreview: "No video preview yet",
    galleryNoDescription: "The creator did not add a description.",
    galleryAnonymous: "Anonymous creator",
    galleryEmptyTitle: "No Gallery posts yet",
    galleryEmptyBody: "Generate a video in any session, then publish it to Gallery so others can remix it.",
    deleteSessionTitle: "Delete session (cannot be undone)",
    deleteSelectedSessionsTitle: "Delete selected sessions (cannot be undone)",
    deleteSelectedSessions: (count) => `Delete ${enPlural(count, "selected session")}`,
    deleteSelectedSessionsConfirm: (count) => `Delete ${enPlural(count, "selected session")}? This cannot be undone, and unreferenced local/cloud media will be cleaned up.`,
    promoteSessionTitle: "Pin as current",
    delete: "Delete",
    sessionNameAria: "Session name",
    shareTitle: "Copy shareable link for this session",
    copiedLink: (url) => `Link copied: ${url}`,
    copyPrompt: "Copy this link:",
    copyLink: "🔗 Copy link",
    createProjectTitle: "Create a short film project",
    languageToggleTitle: "Switch UI language",
    zhLabel: "中文",
    enLabel: "EN",
    visionReview: "Self-review retry",
    visionReviewTitle: "When enabled, generated assets/storyboards/videos are reviewed by a vision model and regenerated if they miss the prompt/reference, up to 5 times. Uses extra tokens.",
    refresh: "Refresh",
    usage: "Usage",
    usageTitle: "View Seedream / Seedance token usage by session",
    serverDownTitle: "⚠ Server unreachable",
    serverDownBody: "The backend dev server is not responding. Check whether npm run dev is still running and restart if needed. This banner disappears automatically when the backend recovers.",
    deleteSessionConfirm: (title) => `Delete session “${title}”? This cannot be undone.`,
    clearTokenUsageConfirm: (title) => `Clear token usage for all nodes in session “${title}”?`,
    createShotUndo: "Create video",
    deleteUndo: (label) => `Delete ${label}`,
    pendingVideoName: "Uploading video",
    pendingUpload: "Uploading…",
    uploadFailed: "Upload failed",
    uploadFailedWithMessage: (message) => `Upload failed: ${message}`,
    importSession: "Load Session",
    importSessionTitle: "Load a .seereel-session package as an editable copy for this browser",
    importSessionInvalid: "This is not a valid SeeReel session package",
    importSessionComplete: (title) => `Loaded session: ${title}`,
    exportSession: "Download Session",
    exportSessionTitle: "Download the current session, nodes, edges, and local preview media",
    sessionExportStarted: (title) => `Started session package download: ${title}`,
    credentialPanelSet: "API Keys",
    credentialPanelReady: "API Keys Ready",
    credentialPanelMissingTitle: "Configure API Keys",
    credentialPanelConfiguredTitle: (source, fingerprint) => fingerprint ? `${source} is configured for this browser: ${fingerprint}` : `${source} is configured for this browser`,
    standardCredentialTab: "API",
    byteplusCredentialTab: "BP API",
    cnCredentialTab: "CN API",
    byteplusCredentialLabel: "BytePlus API",
    cnCredentialLabel: "CN API",
    agentPlanCredentialTab: "Agent Plan",
    standardCredentialSet: "Set standard API Key",
    standardCredentialReady: "Standard API Ready",
    standardCredentialPlaceholder: "Paste BP_ARK_API_KEY / BP_SEEDANCE_API_KEY",
    cnCredentialPlaceholder: "Paste CN_ARK_API_KEY / CN_SEEDANCE_API_KEY",
    standardCredentialHelpText: "BytePlus route: https://ark.ap-southeast.bytepluses.com/api/v3. Highest priority.",
    cnCredentialHelpText: "Volcengine CN route: https://ark.cn-beijing.volces.com/api/v3. Used when BP API is not configured.",
    agentPlanSet: "Set Volcengine Agent Plan Key",
    agentPlanReady: "Volcengine Agent Plan Ready",
    agentPlanMissingTitle: "Enter your own Volcengine Agent Plan Key",
    agentPlanConfiguredTitle: (fingerprint) => fingerprint ? `Volcengine Agent Plan is configured for this browser: ${fingerprint}` : "Volcengine Agent Plan is configured for this browser",
    agentPlanFingerprint: (fingerprint) => `Fingerprint ${fingerprint}`,
    agentPlanPlaceholder: "Paste Volcengine Agent Plan Key",
    agentPlanHelpText: "Uses the Agent Plan route when no standard API Key is configured.",
    freeTrialRemaining: (remaining, limit) => `Free demo ${remaining}/${limit} left`,
    freeTrialDepleted: "Free demo is used up. Add your own Agent Plan Key",
    agentPlanOpenTitle: "Open Volcengine Agent Plan",
    agentPlanSave: "Save",
    agentPlanClear: "Clear",
    adminButton: "admin",
    adminButtonTitle: "Open console",
    adminLoginTitle: "Console login",
    adminLoginHint: "For site operators only. Enter your operator credentials.",
    adminUsernamePlaceholder: "Operator username",
    adminPasswordPlaceholder: "Operator password",
    adminLogin: "Login",
    adminConsoleTitle: "Console",
    adminTrialTitle: "Site Agent Plan",
    adminTrialConfigured: (fingerprint, source) =>
      `Configured${fingerprint ? ` · ${fingerprint}` : ""}${source ? ` · ${source}` : ""}`,
    adminTrialMissing: "Not configured. Free users cannot use the admin demo quota.",
    adminAgentPlanPlaceholder: "Paste site Agent Plan Key",
    adminSave: "Save",
    adminClear: "Clear",
    adminLogout: "Logout",
    adminSecurityTitle: "Login security",
    adminSecurityHelp: "Set a new operator username and password. Passwords are stored as hashes.",
    adminSecurityConfigured: (source) => source === "ui" ? "Using console-saved credentials" : "Using environment credentials",
    adminSecurityMissing: "No operator password is configured yet.",
    adminNewUsernamePlaceholder: "New username (blank keeps current)",
    adminNewPasswordPlaceholder: "New password (8+ chars)",
    adminSecuritySave: "Update credentials",
    adminUserKeysTitle: "User Agent Plan Keys",
    adminUserKeysRefresh: "Refresh user Agent Plan Keys",
    adminUserKeysStorage: (mode, databaseConfigured, encryptionConfigured) =>
      mode === "database"
        ? `Volcengine database connected${encryptionConfigured ? " · encrypted" : " · missing encryption secret"}`
        : `Memory only${databaseConfigured ? " · database unavailable" : " · database not configured"}`,
    adminUserKeysEmpty: "No user has saved an Agent Plan Key yet"
  },
  token: {
    summary: (input, output, calls) => `Input ${input} · output ${output} · ${enPlural(calls, "call")}`,
    clear: "Clear stats",
    clearTitle: "Clear token stats for all nodes in this session",
    calls: (count) => enPlural(count, "call"),
    total: "Total",
    noTrackedFamilies: "No session has recorded token usage for these model families yet.",
    noUsage: "No model token usage recorded yet; it will appear after image/video generation.",
    recentOp: (count, op) => `${enPlural(count, "call")} · latest ${op}`
  },
  flow: {
    emptyTitle: "This is the node canvas",
    emptyHint: "← Click “New Session” in the sidebar or select an existing session",
    summary: (shots, duration, language) => `${enPlural(shots, "shot")} · target ${duration ?? 0}s · language ${language === "en" ? "English" : "Chinese"} · `,
    createNodeHint: "Right-click empty space to create nodes",
    undo: "↶ Undo",
    redo: "↷ Redo",
    undoTitle: (description?: string) => description ? `Undo “${description}” (⌘Z)` : "Nothing to undo",
    redoTitle: (description?: string) => description ? `Redo “${description}” (⇧⌘Z)` : "Nothing to redo",
    createCharacter: "+ Character",
    createScene: "+ Scene",
    createShot: "+ Video",
    uploadVideo: "+ Upload video",
    createCharacterTitle: "Create a character asset as an identity anchor across shots",
    createSceneTitle: "Create a scene asset as an environment anchor across shots",
    createShotTitle: "Add a video node",
    uploadVideoTitle: "Upload a reference video. It will be analyzed into shots and can be dragged onto a Shot as Seedance reference_video.",
    executeFast: "Run workflow",
    executing: "Running...",
    executeFastTitle: "Plan execution first; independent shots run in parallel while tailframe/upstream dependencies wait topologically. By default only missing/failed/in-progress nodes are filled.",
    stitchByConnections: "Stitch connected order",
    stitchAll: "Stitch full film",
    zoomControlsAria: "Canvas zoom controls",
    zoom: "Zoom"
  },
  menu: {
    aria: "Create node",
    title: "Create node",
    hint: "Open by right-clicking empty space",
    image: "Image node",
    imageHint: "Generate or edit one image, I",
    character: "Image node",
    characterHint: "Generate or edit one image, I",
    scene: "Image node",
    sceneHint: "Generate or edit one image, I",
    moodboard: "Moodboard",
    moodboardHint: "Unify palette, lighting, texture, style, and era, M",
    storyboard: "Storyboard",
    storyboardHint: "Add a shot with a storyboard node, B",
    shot: "Video node",
    shotHint: "Add one video node, N",
    stitch: "Stitch node",
    stitchHint: "Manually place a final-video stitch entry, J",
    voice: "Voice node",
    voiceHint: "Save a reusable voice, V",
    music: "Music node",
    musicHint: "Generate reusable BGM/song, M",
    audioTrack: "Audio track node",
    audioTrackHint: "Optionally connect it to a stitch node, A",
    uploadImage: "Upload image",
    uploadImageHint: "Local image as an editable image node, U",
    uploadCharacter: "Upload image",
    uploadCharacterHint: "Local image as an editable image node, U",
    uploadScene: "Upload image",
    uploadSceneHint: "Local image as an editable image node, U",
    uploadVideo: "Upload reference video",
    uploadVideoHint: "Local video; drag to a shot as reference video, R"
  },
  nodes: {
    modelPickerTitle: "Choose model version",
    download: "Download",
    assetTypes: { image: "Image", character: "Image", scene: "Image", prop: "Image", style: "Image", moodboard: "Image", voice: "Voice", music: "Music", other: "Other" },
    statusDraft: "Draft",
    statusScripted: "Scripted",
    statusQueued: "Seedance queued",
    statusRunning: "Seedance rendering",
    statusGenerating: "Generating",
    statusReady: "Ready",
    statusError: "Error",
    statusCancelled: "Cancelled",
    reviewStale: "VLM stale",
    reviewRunning: "VLM reviewing",
    reviewError: "VLM failed",
    reviewNeedsFix: (score) => `VLM ${Math.round(score)} needs fixes`,
    reviewPass: (score) => `VLM ${Math.round(score)} ✓`,
    reviewNotRun: "VLM not run",
    generating: "Generating",
    pendingAssetTitle: "Self-review + Seedream is ~30–40s per round, up to 5 rounds",
    reviewImageTitle: "Score this image with VLM",
    downloadOriginalImage: (name) => `Download original image for ${name}`,
    notGenerated: "Not generated",
    seedreamReviewHint: "Seedream + self-review retry, up to ~3 minutes",
    nextAssetModelTitle: "Model used next time this asset is regenerated",
    reviewAttempts: (count) => `Self-review retries ${count}`,
    storyboard: "Storyboard",
    storyboardPendingTitle: "Seedream storyboard + review, maybe 30s–2min",
    downloadStoryboard: "Download storyboard image",
    storyboardEmpty: "Not generated · open the Inspector to edit",
    storyboardHint: "Seedream storyboard, up to ~2 minutes",
    nextStoryboardModelTitle: "Seedream version used next time this storyboard is regenerated",
    panelCount: (count) => enPlural(count, "panel"),
    refImageCount: (count) => `${enPlural(count, "reference image")}`,
    video: "Video",
    reviewShotTitle: "Review this shot with multi-frame VLM standards",
    downloadShot: "Download this shot mp4",
    firstFrame: "First frame",
    firstFrameTitle: "Extract the current video's first frame as a standalone canvas node",
    tailframe: "Tail frame",
    tailframeTitle: "Extract the current video's tail frame as a standalone canvas node",
    tailClip: "Tail video",
    tailClipTitle: "Extract the final 2 seconds as a standalone reference-video node",
    elapsed: (value) => `Elapsed ${value}`,
    nextSeedanceModelTitle: "Seedance version used next time this shot is generated",
    stitch: "Stitch",
    stitched: "Stitched",
    stitching: "Stitching",
    stitchError: "Error",
    notStitched: "Not stitched",
    connectedSegmentsHint: (count) => `${enPlural(count, "connected segment")} · open to stitch in order`,
    stitchEmptyHint: "Connect videos here, or open to stitch by shot order",
    fullVideo: "Full video",
    finalReview: "Final review",
    finalReviewTitle: "Review the full film with multi-frame VLM standards",
    downloadFinal: "Download full film",
    targetDuration: (seconds) => `${seconds ?? 0}s target`,
    referenceVideo: "Reference video",
    parsing: "Parsing",
    parsedShots: (count) => `Parsed ${enPlural(count, "shot")}`,
    parseFailed: "Parse failed",
    parsePending: "Pending parse",
    downloadReferenceVideo: "Download reference video",
    notUploaded: "Not uploaded",
    applyParsedHint: "Apply parsed shot analysis from here to a shot on the right",
    videoProcessor: "Video processor",
    trim15: "Trim first 15s",
    speedup: "Speed up full clip",
    sampleConcat: "Sample concat",
    unclipped: "Unclipped",
    downloadClip: "Download clip result",
    source: (name) => `Source: ${name}`,
    notExtracted: "Not extracted",
    frameAnchor: "Frame anchor",
    fromShot: (title) => `From ${title}`,
    usedBy: (titles) => `Used by ${titles}`,
    dragToVideo: "Drag to a video node as first frame",
    playVideoTitle: "Play video on canvas",
    clickNodeForInspector: "Click the node to edit in the Inspector"
  },
  toast: {
    downloadStarted: "Download started",
    undone: "Undone",
    redone: "Redone",
    undoFailed: (message) => `Undo failed: ${message}`,
    redoFailed: (message) => `Redo failed: ${message}`
  },
  lightbox: {
    preview: "Preview",
    downloadOriginal: "Download original file",
    download: "Download",
    close: "Close",
    videoFailed: "This video cannot be played directly in the browser.",
    openNewTab: "Open in new tab",
    downloadLocal: "Download locally"
  },
  mention: {
    aria: "@-mention suggestions",
    wired: "wired",
    empty: "No matching assets — press Esc to close, or connect one first"
  },
  inspector: {
    generatedAt: "Generated at",
    createdAt: "Created at",
    openFullSize: "Click to view full size",
    reviewing: "Reviewing",
    reviewRunningText: "VLM is checking consistency with the prompt and reference assets.",
    reviewFailed: "Review failed",
    pass: "Pass",
    needsFix: "Needs fixes",
    selfReviewIssue: "Self-review found issues",
    selfReviewIssueText: "The latest self-review still found issues. Adjust the prompt or references using the reasons below, then regenerate.",
    mainIssues: "Main issues",
    suggestedFixes: "Suggested fixes",
    addIssuesToPrompt: "Add the issues above as positive constraints in the scene description / prompt.",
    reconnectReferences: "If character or scene consistency is off, add or reconnect clearer reference assets.",
    shotPrefix: (value) => `Shot ${value}`,
    framePrefix: (value) => `Frame ${value}`,
    frameCount: (count) => enPlural(count, "frame"),
    videoReviewRunning: "VLM video review in progress… checking characters, scene, pacing, and artifacts across frames.",
    videoReviewFailed: (message) => `VLM review failed: ${message}`,
    videoReviewEmpty: "No VLM video review yet.",
    finalReviewStale: "Final review is stale: the full video was stitched again. Please run final VLM review again.",
    reviewPromptStale: "VLM review is stale: the prompt changed. Review the current version again.",
    fatalIssues: "Fatal issues",
    reasons: "Reasons",
    fixes: "Suggested fixes",
    criteria: "Criteria",
    rawVlmJson: "Raw VLM JSON",
    imageReviewRunning: "VLM image review in progress… checking visual quality, subject structure, prompt alignment, and reference consistency.",
    imageReviewFailed: (message) => `VLM image review failed: ${message}`,
    imageReviewEmpty: "No VLM image review yet. Click “VLM score” to review this image only; it will not regenerate.",
    assetTag: (type) => `Asset · ${type}`,
    close: "Close",
    name: "Name",
    myDescription: "My description",
    assetDescriptionHint: (type) => `Describe what this ${type} should look like in free text`,
    assetTypeName: (type): string => type === "character" ? "character" : type === "scene" ? "scene" : type === "moodboard" ? "moodboard" : "asset",
    promptExpanded: "User-written prompt",
    promptExpandedHint: "Submitted to Seedream exactly as written.",
    promptPlaceholderCharacter: "Example: 35-year-old middle-aged man, black leather jacket, warm but determined eyes…",
    promptPlaceholderScene: "Example: Silicon Valley conference room at night, city lights beyond floor-to-ceiling windows, GPU model on the table…",
    composedPlaceholder: "Write the final prompt directly",
    composedReady: "This prompt will be submitted verbatim.",
    composedMissing: "Write the description, then generate.",
    vlmImageToggleTitle: "When checked, image generation uses VLM review; review results give feedback only and do not rewrite prompts.",
    vlmImageToggle: "VLM review this image node",
    expandPromptTitle: "Disabled: prompts are user-written",
    expandPrompt: "User prompt",
    generateImageTitle: "Submit the current description verbatim. Current edits auto-save first.",
    generateImage: "Generate image",
    vlmScoreTitle: "Score only the current image with VLM; does not regenerate",
    vlmScore: "VLM score",
    deleteAssetConfirm: (name) => `Delete asset “${name}”? You can restore it with “↶ Undo” in the canvas toolbar.`,
    deleteAssetTitle: "Delete this asset node (undoable)",
    delete: "Delete",
    saved: "✓ Saved",
    saving: "Saving…",
    dirty: "● Unsaved (auto-saving soon)",
    downloadOriginalImage: "⬇ Download original image",
    currentImagePreview: "Current image preview (click to enlarge)",
    imageGeneratedAt: "Image generated at",
    vlmImageScore: "VLM image score",
    latestImageReview: "Latest VLM image review",
    moreMetadata: "More metadata / notes",
    actualModel: (model) => `Actual model: ${model}`,
    rawNote: "Raw note (optional; not sent to AI, only shown in asset lists)",
    lastPromptAudit: "Last submitted prompt (audit)",
    retryLog: (count) => `Self-review retry log (${count} retries)`,
    autoSaveFailed: "Auto-save failed",
    previewFailed: "Preview failed",
    saveFailed: "Save failed",
    generateFailed: "Generation failed",
    assetGenerateFailedToast: (name, message) => `❌ ${name || "asset"} generation failed: ${message}`,
    vlmScoreFailed: "VLM score failed",
    deleteFailed: "Delete failed",
    storyboardTag: (index) => `Storyboard · Shot ${index}`,
    storyboardSceneLabel: "Scene description (supports @ wired assets; sent to Seedream as the storyboard composition prompt)",
    storyboardScenePlaceholder: "Wire character / scene / prop / style assets into this storyboard first, then type @ to choose them; public mentioned images are used as Seedream 4.5 references",
    storyboardAutoLayout: (count, layout) => `Auto inferred: ${enPlural(count, "panel")} · ${layout} layout. Use \`Beat 1:\` / \`Beat 2:\` / \`Frame 3:\` in the description; the system infers panel count from the largest marker number.`,
    storyboardSequential: "Generate strictly in timeline order (sequential mode)",
    storyboardSequentialHint: (count) => `When enabled, each panel uses one Seedream call (~10s × ${count} images) plus ffmpeg tiling. Each image references the previous one, so Beat order is guaranteed: TL→TR→BL→BR match your Beat 1/2/3. Use explicit segments like \`Beat 1: …\` \`Beat 2: …\` in the scene description. Off uses one faster group call, but Seedream may occasionally reorder panels.`,
    referenceAssets: "Reference assets (cross-shot identity anchors)",
    referenceAssetsHint: "Only wired assets are shown here. Checked assets are fixed into Seedream. You can also type @ in the scene description to choose wired assets; the server reads the latest public image by asset id. Local /media images are not sent directly to remote models.",
    noReferenceAssets: "No reference assets are wired in yet. Drag a character / scene asset into this storyboard on the canvas first.",
    noImage: "No image",
    selectedReferenceImages: (count, names) => `Selected ${enPlural(count, "image")}: ${names}`,
    seedreamFinalPrompt: "Final prompt sent to Seedream (draft, editable)",
    previewComposePlaceholder: "Preview the prompt that will be submitted to Seedream",
    defaultComposeHint: "Empty means default composition; non-empty means the next regeneration uses this text verbatim",
    previewCompose: "Preview prompt",
    regenerateImage: "Regenerate image",
    generateStoryboard: "Generate storyboard",
    downloadStoryboard: "⬇ Download storyboard image",
    currentStoryboardPreview: "Current storyboard preview (click to enlarge)",
    storyboardTitle: (title) => `${title} · Storyboard`,
    storyboardGeneratedAt: "Storyboard generated at",
    lastReferenceImages: (count) => `Last reference images used (${count})`,
    storyboardGenerateFailed: (index, message) => `❌ Shot ${index} storyboard generation failed: ${message}`,
    imageGenerateFailed: "Image generation failed"
  },
  errors: {
    operationFailed: "Operation failed",
    unknown: "Unknown error",
    networkDown: "Network interrupted / server may be down — check whether the dev server is still running and restart if needed"
  }
};

const dictionaries: Record<UiLanguage, Dictionary> = { zh, en };

let currentUiLanguage: UiLanguage = "zh";

function normalizeLanguage(value: unknown): UiLanguage | undefined {
  return value === "en" || value === "zh" ? value : undefined;
}

export function resolveInitialLanguage(storedLanguage: unknown, _legacyLanguage?: unknown): UiLanguage {
  return normalizeLanguage(storedLanguage) || "zh";
}

function readStoredLanguage(): UiLanguage {
  if (typeof window === "undefined") return "zh";
  return resolveInitialLanguage(
    window.localStorage.getItem(STORAGE_KEY),
    window.localStorage.getItem(LEGACY_STORAGE_KEY)
  );
}

export function getCurrentUiLanguage() {
  return currentUiLanguage;
}

export function getDictionary(lang: UiLanguage = currentUiLanguage) {
  return dictionaries[lang];
}

export function networkDownMessage() {
  return getDictionary().errors.networkDown;
}

interface I18nContextValue {
  lang: UiLanguage;
  setLang: (lang: UiLanguage) => void;
  toggleLang: () => void;
  t: Dictionary;
}

const I18nContext = createContext<I18nContextValue | null>(null);

export function I18nProvider({ children }: { children: ReactNode }) {
  const [lang, setLangState] = useState<UiLanguage>(() => readStoredLanguage());

  useEffect(() => {
    currentUiLanguage = lang;
    if (typeof window !== "undefined") {
      window.localStorage.setItem(STORAGE_KEY, lang);
      window.localStorage.removeItem(LEGACY_STORAGE_KEY);
    }
    if (typeof document !== "undefined") document.documentElement.lang = lang === "zh" ? "zh-CN" : "en";
  }, [lang]);

  const value = useMemo<I18nContextValue>(() => ({
    lang,
    setLang: setLangState,
    toggleLang: () => setLangState((value) => (value === "zh" ? "en" : "zh")),
    t: dictionaries[lang]
  }), [lang]);

  return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>;
}

export function useI18n() {
  const context = useContext(I18nContext);
  if (!context) throw new Error("useI18n must be used inside I18nProvider");
  return context;
}
