dsh-bottom-info-bar 1.9.2 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -16,6 +16,37 @@ var module = { exports: {} }; var exports = module.exports;
16
16
  'use strict';
17
17
 
18
18
  const React = require('react');
19
+ const LOCALE_NAMESPACE = 'dsh-bottom-info-bar';
20
+ const LOCALES = {"zh":{"ui.requestTimedOut":"请求超时","ui.requestCanceled":"请求已取消","ui.couldNotParseResponse":"响应解析失败","ui.rpcFailed":"RPC 失败","ui.refreshFailed":"刷新失败","color.red":"红","color.green":"绿","color.blue":"蓝","color.purple":"紫","color.orange":"橙","color.neutral":"中性","mode.balance":"余额制","mode.subscription":"订阅制","mode.billing":"账单制","mode.native":"原生统计行","mode.common":"通用","ui.nativeStatsField":"原生统计行字段","ui.shownIn":"出现于","ui.pleaseTryAgainLater":"请稍后再试","ui.restoreDefaultColor":"恢复默认颜色","ui.infoBarSettings":"信息底栏设置","ui.settingsAreTemporarilyUnavailable":"配置暂时无法读取","ui.loadingInfoBarSettings":"正在载入信息底栏设置…","ui.couldNotLoadInfoBar":"信息底栏设置暂时无法读取:","ui.changesAppliedButCouldNot":"改动已生效,但写入本地配置文件失败:","ui.unknownReason":"未知原因","ui.couldNotSave":"{errorPrefix}保存失败:{value}","ui.color":"「{value}」颜色","ui.hasAnInvalidColorEnter":"「{value}」颜色格式无效:请输入 #RRGGBB(例如 #0044CC)","ui.defaultColorsRestored":"已恢复默认颜色","ui.defaultLabelsRestored":"已恢复默认标签","ui.couldNotReset":"重置失败:{value}","ui.identifiesTheProviderAndModel":"身份锚点:当前对话的服务商与模型标识。","ui.keepEnabledToSeeThese":"关闭后相应提示不再出现,建议保留。","ui.recommended":"建议保留","ui.show":"显示{label}","ui.clickToHide":"点击隐藏","ui.clickToShow":"点击显示","ui.default":"默认","ui.current":"当前:{valueText}","ui.providerAndModel":"(身份锚点)","ui.presetColor":"{label}的预设颜色","ui.customColor":"{label}的自定义颜色","ui.customColorOpenColorPicker":"自定义颜色(打开系统取色器)","ui.hexColor":"{label}的十六进制颜色","ui.saving":"正在保存…","ui.chooseWhichFieldsToShow":"选择信息底栏显示哪些信息、为每个信息挑一个颜色。改动即时生效并自动保存,重启后仍然保留;隐藏信息不会删除任何记账数据。","ui.visibleFields":"显示字段","ui.hiddenFieldsTakeNoSpace":"关闭的字段立刻从信息栏隐藏且不占位;「刷新失败」「账单未保存」等提示建议保留,方便发现数据异常。","ui.fieldColors":"字段颜色","ui.chooseAPresetUseThe":"挑选预设色或用取色器、十六进制自定义;颜色会自动适配浅色与深色主题,保持可读。未改动前与现在的外观完全一致。","ui.resetLabels":"重置标签","ui.resetColors":"重置颜色","ui.couldNotDisplayInfoBar":"信息底栏设置渲染出错:{value}","ui.infoBar":"信息底栏","ui.weekly":"周","ui.monthly":"月","ui.window":"窗口","ui.supportsImageInput":"支持图像输入。","ui.vision":"视觉","ui.unknown":"未知","ui.unknownModel":"未知模型","ui.pluginVersion":"\n插件版本:{current}","ui.provider":"服务商:{provLabel} {modelLabel}\n","ui.pricingPeakOffPeakBeijing":"定价:峰谷价(工作日高峰 9-12、14-18 点;周末全天空闲)","ui.pricingFixed":"定价:固定价","ui.pricingNotListedUsingDefaults":"定价:未收录,按默认计","ui.zhipu":"智谱","ui.xiaomiMiMo":"小米 MiMo","ui.subscription":"订阅","ui.cloudBilling":"云账单","ui.plan":"\n套餐:{plan}","ui.expiresLocalTime":"\n到期:{value}(本地时区)","ui.subscriptionServiceModel":"订阅服务:{serviceName}\n模型:{rawModelLabel}{planLine}{expiryLine}{versionLine}","ui.balanceLookupIsNotYet":"该服务商暂未适配余额查询,后续版本将支持。","ui.notSupported":"未适配","ui.notConfigured":"未配置 ","ui.notConfiguredConfigureItIn":"未配置 {credName}。请在\"设置 → 模型\"中填写。","ui.notConfiguredSettingsModels":"未配置 {credName} → 设置→模型 填写","ui.estimatedBalance":"估算余额:{symbol}{value}","ui.balance":"余额:{symbol}{value}","ui.balance.pushBalanceGroups":"余额","ui.low":"低","ui.estimated":"(估算)","ui.balanceIsTemporarilyUnavailableShowing":"余额暂不可用;正在显示上次数据并自动重试。","ui.couldNotLoadBalanceCheck":"余额获取失败。请检查网络和 API Key。","ui.balanceUnavailable":"余额获取失败","ui.beijingTime":"北京时间 ","ui.peakPrice":"高峰价","ui.offPeakPrice":"空闲价","ui.input":":输入 ¥","ui.mCachedInput":"/M · 缓存 ¥","ui.mOutput":"/M · 输出 ¥","ui.beijingTimeSwitchesTo":"北京时间 {atLabel} 切换为","ui.until":"距","ui.offPeak":"空闲","ui.peak":"高峰","ui.today":"今天 {symbol}{value}","ui.lastDays":"近一月 {symbol}{value}","ui.allTime":"全部 {symbol}{value}","ui.sessionIncludingSubagents":"本会话 {costTxt}(含子代理){value}","ui.session":"本会话","ui.spendIsTemporarilyUnavailableChat":"花费暂不可用;不会影响对话。","ui.spendUnavailable":"花费获取失败","ui.noSignInCredentialsFound":"未找到 {serviceName} 登录凭证。请重新授权。","ui.credentialsHaveExpiredPleaseReauthorize":"{serviceName} 登录凭证已失效。请重新授权。","ui.deniedAccessReauthorizeOrTry":"{serviceName} 拒绝访问。请重新授权或稍后再试。","ui.rateLimitReachedPleaseTry":"{serviceName} 请求过于频繁。请稍后再试。","ui.timedOutCheckYourConnection":"{serviceName} 响应超时。请检查网络后再试。","ui.returnedAnUnrecognizedResponsePlease":"{serviceName} 返回的数据暂时无法识别。请稍后再试。","ui.isTemporarilyUnavailableCheckYour":"{serviceName} 暂不可用。请检查网络后再试。","ui.subscriptionExpiresLocalTime":"订阅到期:{value}(本地时区)","ui.expires":"到期","ui.subscriptionSource":"订阅源:{value}(","ui.prepaidBalance":"充值余额","ui.availableBalance":"可用余额:{balTxt}","ui.subscriptionSource.titleLines":"订阅源:{value}","ui.windowRemainingUsed":"{label}窗口:剩余 {value}%(已用 {usedPercent}%)","ui.resetsResetsIn":" · 重置 {value} · 距重置 {value2}","ui.windowRemainingUsedResets":"{label}窗口 剩余 {value}%(已用 {usedPercent}%) · 重置 {value4}","ui.resetsIn":"距重置","ui.configureItInSettingsModels":"。请在\"设置 → 模型\"中填写。","ui.deniedAccessTheTokenMay":"{serviceName} 拒绝访问:Token 可能缺少账单读取权限。","ui.billingIsTemporarilyUnavailableCheck":"{serviceName} 账单暂不可用。请检查网络与权限后再试。","ui.billingSource":"账单源:{value}","ui.thisMonthSSpend":"本月花费:{symbol}{value}","ui.budgetUsed":"预算使用:{value}%","ui.dailyFreeQuotaRemaining":"每日免费额度剩余:{value}","ui.thisMonth":"本月","ui.thisMonthSUsage":"本月用量","ui.budget":"预算","ui.free":"免费","ui.resetsIn.pushBillingGroups":"{value} · 距重置 {value2}","ui.billingServiceModel":"账单服务:{serviceName}\n模型:{modelLabel}{versionLine}","ui.pricing":"定价","ui.spend":"花费","ui.mode":"模式","ui.subscriptionQuota":"订阅额度","ui.billing":"账单","ui.temporarilyUnavailableKeepingTheLast":"暂不可用;正在保留上次数据并自动重试。","ui.spendJournalSavedButThe":"账单流水已保存,但可直接查看的账单文件暂未更新:","ui.thisSpendRecordWasNot":"本次账单未保存,不会计入金额:","ui.ledgerUpdatePending":"账单待整理","ui.spendNotSaved":"账单未保存","ui.askYourAgentToUpdate":"请告知你的 Agent 将本插件更新到“{latest}”版本。","ui.updateAvailable":"新版本提醒","ui.tools":"工具调用","ui.avgTTFT":"首 token 平均","ui.cacheHit":"缓存命中","ui.input.BottomInfoBar":"输入","ui.output":"输出","ui.savingView":"正在保存切换…","ui.clickToSwitchFullCompact":"单击切换 完整/简洁","ui.pressEnterOrSpaceFor":"按 Enter 或空格切换为简洁模式。","ui.pressEnterOrSpaceFor.BottomInfoBar":"按 Enter 或空格切换为完整模式。","host.hour":"5 小时","host.unknownProvider":"未知服务商","host.unexpectedSubscriptionQuotaRequestFailure":"订阅额度请求未知异常","host.settingsFileCouldNotBe":"设置文件写入未完成","host.everydayQuestions":"日常问答","host.mediumCodingTask":"中等编码任务","host.longDocumentAnalysisCodeReview":"长文档分析/代码审查","host.largeProjectRefactorAcrossMultiple":"大工程重构(多轮)","host.subagentWorkflow":"子代理工作流","host.couldNotSaveSettingsJson":"settings.json 落盘失败:{value}","host.couldNotReadCredentials":"凭据读取失败","host.notConfigured":"未配置 {credential}","host.requestFailedHTTP":"请求失败(HTTP {status})","host.unexpectedResponseFormat":"响应格式异常","host.chatgptSubscriptionIsNotConnected":"未找到 ChatGPT 订阅登录凭证(~/.codex/auth.json),请安装 dsh-chatgpt-subscription 插件绑定","host.chatgptSubscriptionCredentialsAreMissing":"ChatGPT 订阅登录凭证缺少 id_token,请安装 dsh-chatgpt-subscription 插件重新绑定","host.opencodeGoIsNotConfigured":"未配置 OpenCode Go(OPENCODE_GO_API_KEY 或 opencode auth.json)","host.zhipu":"智谱 {mapped}","host.zhipu.parseZaiQuota":"智谱 {value}{value2}","host.zhipuAPIKeyIsNot":"未配置智谱 API Key(ZAI_API_KEY 或 ZAI_CODING_CN_API_KEY)","host.zhipuAPIAuthenticationFailedThe":"智谱 API 认证失败(密钥过期或不正确)","host.requestFailed":"请求失败({value} {msg})","host.xiaomiMiMoTokenPlanCredentials":"未配置小米 MiMo Token Plan 凭据({credName} 或 XIAOMI_API_KEY)","host.requestFailedHTTP.fetchXiaomiTokenPlanUsage":"请求失败(HTTP {value})","host.notConfiguredTOGETHERAPIKEY":"未配置 TOGETHER_API_KEY","host.actualMonthlyBillFromThe":"本月真实账单(Together 官方 Usage API)","host.notConfiguredFIREWORKSAPIKEY":"未配置 FIREWORKS_API_KEY","host.couldNotReadAccountAccount":"账户解析失败(未取得 account_id)","host.actualBillForThisPeriod":"本周期真实账单(Fireworks Billing Summary)","host.actualUsageForThisPeriod":"本周期真实用量(billingUsage 回退端点,无金额)","host.notConfiguredAWSCredentialsAWS":"未配置 AWS 凭据(AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY)","host.requestFailedHTTPTheToken":"请求失败(HTTP {status})——可能缺少 ce:GetCostAndUsage 权限","host.actualMonthlyBillFromAWS":"AWS Cost Explorer 本月真实账单(账单延迟约 24 小时)","host.notConfiguredCLOUDFLAREAPIKEY":"未配置 CLOUDFLARE_API_KEY(需账号级 Token 并授予 Billing 读权限)","host.notConfiguredCLOUDFLAREACCOUNTID":"未配置 CLOUDFLARE_ACCOUNT_ID","host.requestFailedHTTPTheToken.fetchCloudflareBilling":"请求失败(HTTP {status})——Token 需 Billing 读权限","host.actualMonthlyUsageFromThe":"Cloudflare Billable Usage API(Alpha),本月真实用量","host.unexpectedBillingRequestFailure":"账单请求未知异常","host.spendSummaryFileMissingArchived":"账单汇总文件缺失:折叠期间的历史金额暂未计入显示(明细仍在冷归档 usage-archive/,可人工恢复)","host.spendLedgerCouldNotBe":"账单文件写入未完成","host.couldNotSaveArchivedSpend":"折叠汇总落盘失败:{value}","host.basedOnYourLastSessions":"基于你最近 {count} 次会话","host.estimatedFromSpendingOverThe":"基于过去 {SPEND_DAYS} 天消耗速度的估算","host.yourTypicalSession":"你的典型会话","host.patchMustIncludeFieldsOr":"patch 需包含 fields 或 colors 之一","host.fieldsMustBeAnObject":"fields 必须是对象","host.unknownFieldId":"未知字段 id: {key}","host.fieldVisibilityMustBeA":"字段开关必须是布尔值: {key}","host.colorsMustBeAnObject":"colors 必须是对象","host.colorMustBeAPreset":"颜色只接受预设色名或 #RRGGBB: {key}","field.anchorGroup.label":"服务商与模型","field.anchorGroup.note":"当前对话使用的服务商与模型名,信息栏的身份锚点","field.subServiceGroup.label":"订阅服务与模型","field.subServiceGroup.note":"订阅服务名(ChatGPT/Codex/OpenCode Go/智谱/小米 MiMo)与模型或套餐档位","field.billingServiceGroup.label":"账单服务与模型","field.billingServiceGroup.note":"云账单服务名(Together/Fireworks/AWS Bedrock/Cloudflare)与模型名","field.sessionCost.label":"本会话花费","field.sessionCost.note":"当前会话(含子代理)的真实花费;新会话尚无记账时显示 ¥0.000,悬停可看今天/近一月/全部","field.balance.note":"服务商账户的真实余额;低余额时数字变红并带「低」字","field.period.label":"定价时段","field.period.note":"仅峰谷价模型显示(当前为 DeepSeek 系列):高峰价红色 / 空闲价绿色","field.countdown.label":"时段切换倒计时","field.countdown.note":"仅峰谷价模型显示:距空闲/距高峰的小时计倒计时","field.expiry.label":"订阅到期日","field.expiry.note":"订阅登录凭据中带有到期信息时显示(如 Codex 套餐)","field.subWindow5h.label":"5 小时额度窗口","field.subWindow5h.note":"5 小时滚动额度剩余百分比;简洁模式下仅显示时间最短的可用窗口","field.subWindowWeek.label":"周额度窗口","field.subWindowWeek.note":"每周额度剩余百分比;完整模式显示,简洁模式让位给更短窗口","field.subWindowMonth.label":"月额度窗口","field.subWindowMonth.note":"每月额度剩余百分比;完整模式显示,简洁模式让位给更短窗口","field.resetCountdown.label":"额度重置倒计时","field.resetCountdown.note":"当前显示的额度窗口距重置的倒计时","field.subBalance.note":"订阅源为充值余额形态时显示(如智谱按量账户),与额度窗口互斥","field.billingSpend.label":"本月花费/用量","field.billingSpend.note":"云账单当前计费周期的真实花费或用量","field.budget.label":"预算使用","field.budget.note":"仅提供预算查询的服务商显示(当前为 AWS Bedrock)","field.freeQuota.label":"免费额度与重置","field.freeQuota.note":"接口显式给出每日免费额度与重置时刻时才显示,绝不编造","field.turnsSteps.label":"轮次与步数","field.turnsSteps.note":"「N 轮 · M 步」对话进度统计","field.llmTime.label":"LLM 耗时","field.llmTime.note":"模型推理累计耗时","field.toolTime.label":"工具调用耗时","field.toolTime.note":"工具调用累计耗时","field.cacheHit.note":"提示词缓存命中率百分比","field.tokensIO.label":"输入/输出 tokens","field.tokensIO.note":"「输入 X tok · 输出 Y tok」本次会话累计用量","field.unmapped.label":"未适配提示","field.unmapped.note":"当前服务商暂无余额查询适配时出现的弱提示","field.noKeyHint.label":"未配置凭据提示","field.noKeyHint.note":"缺少 API Key 时的设置引导文案","field.balanceError.label":"余额获取/刷新失败提示","field.balanceError.note":"余额查询失败或正在显示上次数据时的提示","field.usageError.label":"花费获取失败提示","field.usageError.note":"花费数据暂时不可用时的提示","field.refreshFailure.label":"刷新失败提示","field.refreshFailure.note":"任一数据源降级时在行尾合并显示的一个提醒(多个来源自动去重)","field.persistWarning.label":"账单未保存提醒","field.persistWarning.note":"账本落盘异常时出现;隐藏后金额可能悄悄不准,建议保留","field.updateNotice.note":"npm 上有新版本时提醒更新","group.native":"原生字段","group.plugin":"插件字段","ui.sentenceSeparator":"","ui.listSeparator":"、","ui.sentenceEnd":"。","ui.fieldErrorPrefix":"「{label}」:","ui.turnCount":"{count} 轮","ui.turnCountPlural":"{count} 轮","ui.stepCount":"{count} 步","ui.stepCountPlural":"{count} 步"},"en":{"ui.requestTimedOut":"Request timed out","ui.requestCanceled":"Request canceled","ui.couldNotParseResponse":"Could not parse response","ui.rpcFailed":"RPC failed","ui.refreshFailed":"Refresh failed","color.red":"Red","color.green":"Green","color.blue":"Blue","color.purple":"Purple","color.orange":"Orange","color.neutral":"Neutral","mode.balance":"Balance","mode.subscription":"Subscription","mode.billing":"Billing","mode.native":"Native stats","mode.common":"General","ui.nativeStatsField":"Native stats field","ui.shownIn":"Shown in: ","ui.pleaseTryAgainLater":"Please try again later","ui.restoreDefaultColor":"Restore default color","ui.infoBarSettings":"Info Bar settings","ui.settingsAreTemporarilyUnavailable":"Settings are temporarily unavailable","ui.loadingInfoBarSettings":"Loading Info Bar settings…","ui.couldNotLoadInfoBar":"Could not load Info Bar settings: ","ui.changesAppliedButCouldNot":"Changes applied, but could not be saved locally: ","ui.unknownReason":"Unknown reason","ui.couldNotSave":"{errorPrefix}Could not save: {value}","ui.color":"\"{value}\" color: ","ui.hasAnInvalidColorEnter":"\"{value}\" has an invalid color. Enter #RRGGBB (for example, #0044CC).","ui.defaultColorsRestored":"Default colors restored","ui.defaultLabelsRestored":"Default labels restored","ui.couldNotReset":"Could not reset: {value}","ui.identifiesTheProviderAndModel":"Identifies the provider and model for this conversation.","ui.keepEnabledToSeeThese":"Keep enabled to see these alerts.","ui.recommended":"Recommended","ui.show":"Show {label}","ui.clickToHide":"Click to hide","ui.clickToShow":"Click to show","ui.default":"Default","ui.current":"Current: {valueText}","ui.providerAndModel":" (provider and model)","ui.presetColor":"{label} preset color","ui.customColor":"{label} custom color","ui.customColorOpenColorPicker":"Custom color (open color picker)","ui.hexColor":"{label} hex color","ui.saving":"Saving…","ui.chooseWhichFieldsToShow":"Choose which fields to show and customize their colors. Changes apply immediately and are saved across restarts. Hiding a field does not delete spend records.","ui.visibleFields":"Visible fields","ui.hiddenFieldsTakeNoSpace":"Hidden fields take no space. Keep alerts such as Refresh failed and Spend not saved enabled to spot data issues.","ui.fieldColors":"Field colors","ui.chooseAPresetUseThe":"Choose a preset, use the color picker, or enter a hex color. Colors adjust for readability in light and dark themes. Unchanged fields keep their default appearance.","ui.resetLabels":"Reset labels","ui.resetColors":"Reset colors","ui.couldNotDisplayInfoBar":"Could not display Info Bar settings: {value}","ui.infoBar":"Info Bar","ui.weekly":"Weekly","ui.monthly":"Monthly","ui.window":"Window","ui.supportsImageInput":"Supports image input.","ui.vision":"Vision","ui.unknown":"Unknown","ui.unknownModel":"Unknown model","ui.pluginVersion":"\nPlugin version: {current}","ui.provider":"Provider: {provLabel} {modelLabel}\n","ui.pricingPeakOffPeakBeijing":"Pricing: peak/off-peak (Beijing time; weekday peaks 09:00–12:00 and 14:00–18:00; weekends off-peak)","ui.pricingFixed":"Pricing: fixed","ui.pricingNotListedUsingDefaults":"Pricing: not listed; using defaults","ui.zhipu":"Zhipu","ui.xiaomiMiMo":"Xiaomi MiMo","ui.subscription":"Subscription","ui.cloudBilling":"Cloud billing","ui.plan":"\nPlan: {plan}","ui.expiresLocalTime":"\nExpires: {value} (local time)","ui.subscriptionServiceModel":"Subscription service: {serviceName}\nModel: {rawModelLabel}{planLine}{expiryLine}{versionLine}","ui.balanceLookupIsNotYet":"Balance lookup is not yet supported for this provider.","ui.notSupported":"Not supported","ui.notConfigured":"Not configured: ","ui.notConfiguredConfigureItIn":"Not configured: {credName}. Configure it in Settings → Models.","ui.notConfiguredSettingsModels":"Not configured: {credName} → Settings → Models","ui.estimatedBalance":"Estimated balance: {symbol}{value}","ui.balance":"Balance: {symbol}{value}","ui.balance.pushBalanceGroups":"Balance","ui.low":"Low","ui.estimated":"(estimated)","ui.balanceIsTemporarilyUnavailableShowing":"Balance is temporarily unavailable. Showing the last data and retrying automatically.","ui.couldNotLoadBalanceCheck":"Could not load balance. Check your connection and API key.","ui.balanceUnavailable":"Balance unavailable","ui.beijingTime":"Beijing time: ","ui.peakPrice":"Peak price","ui.offPeakPrice":"Off-peak price","ui.input":": input ¥","ui.mCachedInput":"/M · cached input ¥","ui.mOutput":"/M · output ¥","ui.beijingTimeSwitchesTo":"Beijing time: {atLabel} switches to ","ui.until":"Until ","ui.offPeak":"off-peak","ui.peak":"peak","ui.today":"Today {symbol}{value}","ui.lastDays":"Last 30 days {symbol}{value}","ui.allTime":"All time {symbol}{value}","ui.sessionIncludingSubagents":"Session {costTxt} (including subagents){value}","ui.session":"Session","ui.spendIsTemporarilyUnavailableChat":"Spend is temporarily unavailable. Chat is unaffected.","ui.spendUnavailable":"Spend unavailable","ui.noSignInCredentialsFound":"No sign-in credentials found for {serviceName}. Please reauthorize.","ui.credentialsHaveExpiredPleaseReauthorize":"{serviceName} credentials have expired. Please reauthorize.","ui.deniedAccessReauthorizeOrTry":"{serviceName} denied access. Reauthorize or try again later.","ui.rateLimitReachedPleaseTry":"{serviceName} rate limit reached. Please try again later.","ui.timedOutCheckYourConnection":"{serviceName} timed out. Check your connection and try again.","ui.returnedAnUnrecognizedResponsePlease":"{serviceName} returned an unrecognized response. Please try again later.","ui.isTemporarilyUnavailableCheckYour":"{serviceName} is temporarily unavailable. Check your connection and try again.","ui.subscriptionExpiresLocalTime":"Subscription expires: {value} (local time)","ui.expires":"Expires","ui.subscriptionSource":"Subscription source: {value} (","ui.prepaidBalance":"Prepaid balance","ui.availableBalance":"Available balance: {balTxt}","ui.subscriptionSource.titleLines":"Subscription source: {value}","ui.windowRemainingUsed":"{label} window: remaining {value}% (used {usedPercent}%)","ui.resetsResetsIn":" · Resets {value} · Resets in {value2}","ui.windowRemainingUsedResets":"{label} window: remaining {value}% (used {usedPercent}%) · Resets {value4}","ui.resetsIn":"Resets in","ui.configureItInSettingsModels":". Configure it in Settings → Models.","ui.deniedAccessTheTokenMay":"{serviceName} denied access: the token may lack billing read permissions.","ui.billingIsTemporarilyUnavailableCheck":"{serviceName} billing is temporarily unavailable. Check your connection and permissions, then try again.","ui.billingSource":"Billing source: {value}","ui.thisMonthSSpend":"This month’s spend: {symbol}{value}","ui.budgetUsed":"Budget used: {value}%","ui.dailyFreeQuotaRemaining":"Daily free quota remaining: {value}","ui.thisMonth":"This month","ui.thisMonthSUsage":"This month’s usage","ui.budget":"Budget","ui.free":"Free","ui.resetsIn.pushBillingGroups":"{value} · Resets in {value2}","ui.billingServiceModel":"Billing service: {serviceName}\nModel: {modelLabel}{versionLine}","ui.pricing":"Pricing","ui.spend":"Spend","ui.mode":"Mode","ui.subscriptionQuota":"Subscription quota","ui.billing":"Billing","ui.temporarilyUnavailableKeepingTheLast":": temporarily unavailable. Keeping the last data and retrying automatically.","ui.spendJournalSavedButThe":"Spend journal saved, but the readable ledger has not been updated: ","ui.thisSpendRecordWasNot":"This spend record was not saved and will not be added to totals: ","ui.ledgerUpdatePending":"Ledger update pending","ui.spendNotSaved":"Spend not saved","ui.askYourAgentToUpdate":"Ask your agent to update this plugin to version {latest}.","ui.updateAvailable":"Update available","ui.tools":"Tools","ui.avgTTFT":"Avg. TTFT","ui.cacheHit":"Cache hit","ui.input.BottomInfoBar":"Input","ui.output":"Output","ui.savingView":"Saving view…","ui.clickToSwitchFullCompact":"Click to switch full/compact view","ui.pressEnterOrSpaceFor":"Press Enter or Space for compact view.","ui.pressEnterOrSpaceFor.BottomInfoBar":"Press Enter or Space for full view.","host.hour":"5-hour","host.unknownProvider":"Unknown provider","host.unexpectedSubscriptionQuotaRequestFailure":"Unexpected subscription quota request failure","host.settingsFileCouldNotBe":"Settings file could not be saved","host.everydayQuestions":"Everyday questions","host.mediumCodingTask":"Medium coding task","host.longDocumentAnalysisCodeReview":"Long document analysis / code review","host.largeProjectRefactorAcrossMultiple":"Large project refactor across multiple turns","host.subagentWorkflow":"Subagent workflow","host.couldNotSaveSettingsJson":"Could not save settings.json: {value}","host.couldNotReadCredentials":"Could not read credentials","host.notConfigured":"Not configured: {credential}","host.requestFailedHTTP":"Request failed: HTTP {status}.","host.unexpectedResponseFormat":"Unexpected response format","host.chatgptSubscriptionIsNotConnected":"ChatGPT subscription is not connected: credentials not found in ~/.codex/auth.json. Install dsh-chatgpt-subscription and sign in.","host.chatgptSubscriptionCredentialsAreMissing":"ChatGPT subscription credentials are missing id_token. Install dsh-chatgpt-subscription and reauthorize.","host.opencodeGoIsNotConfigured":"OpenCode Go is not configured. Set OPENCODE_GO_API_KEY or use opencode auth.json.","host.zhipu":"Zhipu {mapped}","host.zhipu.parseZaiQuota":"Zhipu {value}{value2}","host.zhipuAPIKeyIsNot":"Zhipu API key is not configured. Set ZAI_API_KEY or ZAI_CODING_CN_API_KEY.","host.zhipuAPIAuthenticationFailedThe":"Zhipu API authentication failed: the key is expired or invalid.","host.requestFailed":"Request failed: {value} {msg}.","host.xiaomiMiMoTokenPlanCredentials":"Xiaomi MiMo Token Plan credentials are not configured: {credName} or XIAOMI_API_KEY.","host.requestFailedHTTP.fetchXiaomiTokenPlanUsage":"Request failed: HTTP {value}.","host.notConfiguredTOGETHERAPIKEY":"Not configured: TOGETHER_API_KEY","host.actualMonthlyBillFromThe":"Actual monthly bill from the Together Usage API","host.notConfiguredFIREWORKSAPIKEY":"Not configured: FIREWORKS_API_KEY","host.couldNotReadAccountAccount":"Could not read account (account_id missing)","host.actualBillForThisPeriod":"Actual bill for this period (Fireworks Billing Summary)","host.actualUsageForThisPeriod":"Actual usage for this period (billingUsage fallback; no spend amount)","host.notConfiguredAWSCredentialsAWS":"Not configured: AWS credentials — AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY","host.requestFailedHTTPTheToken":"Request failed: HTTP {status}. The token may lack ce:GetCostAndUsage permission","host.actualMonthlyBillFromAWS":"Actual monthly bill from AWS Cost Explorer — about 24 hours behind","host.notConfiguredCLOUDFLAREAPIKEY":"Not configured: CLOUDFLARE_API_KEY — requires an account-level token with Billing read permission","host.notConfiguredCLOUDFLAREACCOUNTID":"Not configured: CLOUDFLARE_ACCOUNT_ID","host.requestFailedHTTPTheToken.fetchCloudflareBilling":"Request failed: HTTP {status}. The token requires Billing read permission","host.actualMonthlyUsageFromThe":"Actual monthly usage from the Cloudflare Billable Usage API — Alpha","host.unexpectedBillingRequestFailure":"Unexpected billing request failure","host.spendSummaryFileMissingArchived":"Spend summary file missing: archived amounts are not included in displayed totals. Details remain in usage-archive/ for manual recovery.","host.spendLedgerCouldNotBe":"Spend ledger could not be saved","host.couldNotSaveArchivedSpend":"Could not save archived spend summaries: {value}","host.basedOnYourLastSessions":"Based on your last {count} sessions","host.estimatedFromSpendingOverThe":"Estimated from spending over the last {SPEND_DAYS} days","host.yourTypicalSession":"Your typical session","host.patchMustIncludeFieldsOr":"patch must include fields or colors","host.fieldsMustBeAnObject":"fields must be an object","host.unknownFieldId":"Unknown field id: {key}","host.fieldVisibilityMustBeA":"Field visibility must be a boolean: {key}","host.colorsMustBeAnObject":"colors must be an object","host.colorMustBeAPreset":"Color must be a preset name or #RRGGBB: {key}","field.anchorGroup.label":"Provider and model","field.anchorGroup.note":"Identifies the provider and model used in this conversation.","field.subServiceGroup.label":"Subscription service and model","field.subServiceGroup.note":"Subscription service (ChatGPT, Codex, OpenCode Go, Zhipu or Xiaomi MiMo) and model or plan tier.","field.billingServiceGroup.label":"Billing service and model","field.billingServiceGroup.note":"Cloud billing service (Together, Fireworks, AWS Bedrock or Cloudflare) and model.","field.sessionCost.label":"Session spend","field.sessionCost.note":"Actual session spend, including subagents; shows ¥0.000 before any records exist. Hover for today, the last 30 days and all time.","field.balance.note":"Actual account balance. A low balance appears in red with a Low label.","field.period.label":"Pricing period","field.period.note":"For models with peak/off-peak pricing (currently DeepSeek): red for peak, green for off-peak.","field.countdown.label":"Price switch countdown","field.countdown.note":"Time until peak/off-peak pricing changes, for models with time-based pricing.","field.expiry.label":"Subscription expiry","field.expiry.note":"Shown when subscription credentials include an expiry date, such as a Codex plan.","field.subWindow5h.label":"5-hour quota","field.subWindow5h.note":"Remaining quota in the rolling 5-hour window. Compact view shows only the shortest available window.","field.subWindowWeek.label":"Weekly quota","field.subWindowWeek.note":"Weekly quota remaining. Shown in full view; compact view prioritizes shorter windows.","field.subWindowMonth.label":"Monthly quota","field.subWindowMonth.note":"Monthly quota remaining. Shown in full view; compact view prioritizes shorter windows.","field.resetCountdown.label":"Quota reset countdown","field.resetCountdown.note":"Time until the displayed quota window resets.","field.subBalance.note":"Shown for prepaid accounts, such as Zhipu pay-as-you-go, in place of quota windows.","field.billingSpend.label":"Monthly spend / usage","field.billingSpend.note":"Actual cloud spend or usage for the current billing period.","field.budget.label":"Budget used","field.budget.note":"Shown for providers that support budget queries (currently AWS Bedrock).","field.freeQuota.label":"Free quota and reset","field.freeQuota.note":"Shown only when the API reports a daily free allowance and reset time.","field.turnsSteps.label":"Turns and steps","field.turnsSteps.note":"Conversation progress: N turns · M steps.","field.llmTime.label":"LLM time","field.llmTime.note":"Total model inference time.","field.toolTime.label":"Tool time","field.toolTime.note":"Total tool execution time.","field.cacheHit.note":"Prompt cache hit rate as a percentage.","field.tokensIO.label":"Input / output tokens","field.tokensIO.note":"Total session usage: Input X tok · Output Y tok.","field.unmapped.label":"Unsupported provider hint","field.unmapped.note":"Shown when balance lookup is not supported for the current provider.","field.noKeyHint.label":"Missing credentials hint","field.noKeyHint.note":"Setup instructions when an API key is missing.","field.balanceError.label":"Balance error","field.balanceError.note":"Shown when balance lookup fails or the last data is being displayed.","field.usageError.label":"Spend error","field.usageError.note":"Shown when spend data is temporarily unavailable.","field.refreshFailure.label":"Refresh error","field.refreshFailure.note":"One alert at the end of the bar when any data source fails; duplicate alerts are combined.","field.persistWarning.label":"Unsaved spend alert","field.persistWarning.note":"Shown when the ledger cannot be saved. Keep enabled to spot missing amounts.","field.updateNotice.note":"Shown when a new version is available on npm.","group.native":"Native fields","group.plugin":"Plugin fields","ui.sentenceSeparator":" ","ui.listSeparator":", ","ui.sentenceEnd":".","ui.fieldErrorPrefix":"\"{label}\": ","ui.turnCount":"{count} turn","ui.turnCountPlural":"{count} turns","ui.stepCount":"{count} step","ui.stepCountPlural":"{count} steps"}};
21
+ let t;
22
+ // Compatibility with existing host snapshots, whose display fields are text.
23
+ // Known labels and messages follow the browser locale even while snapshots are cached.
24
+ function localizeHostText(message, translate, dictionaries) {
25
+ if (typeof message !== 'string' || message.length === 0) return message
26
+ for (const key of Object.keys(dictionaries.zh)) {
27
+ if (dictionaries.zh[key] === message || dictionaries.en[key] === message) return translate(key)
28
+ }
29
+ for (const key of Object.keys(dictionaries.zh)) {
30
+ if (!key.startsWith('host.')) continue
31
+ for (const language of ['zh', 'en']) {
32
+ const template = dictionaries[language][key]
33
+ if (!/\{\w+\}/.test(template)) continue
34
+ const names = []
35
+ const pattern = template.split(/(\{\w+\})/).map(function (part) {
36
+ if (/^\{\w+\}$/.test(part)) { names.push(part.slice(1, -1)); return '([\\s\\S]*?)' }
37
+ return part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
38
+ }).join('')
39
+ const match = new RegExp('^' + pattern + '$').exec(message)
40
+ if (match) {
41
+ const params = {}
42
+ names.forEach(function (name, index) { params[name] = match[index + 1] })
43
+ return translate(key, params)
44
+ }
45
+ }
46
+ }
47
+ return message
48
+ }
49
+ function hostText(message) { return localizeHostText(message, t, LOCALES); }
19
50
 
20
51
  const RPC_BASE = '/_dsh/dsh-bottom-info-bar';
21
52
 
@@ -37,8 +68,8 @@ const FORCE_REFRESH_WINDOW_MS = 6000;
37
68
  function rpc(method, args, externalSignal) {
38
69
  let abortReason = null;
39
70
  const controller = new AbortController();
40
- const timer = window.setTimeout(function () { abortReason = '请求超时'; controller.abort(); }, RPC_TIMEOUT_MS);
41
- function onExternalAbort() { abortReason = '请求已取消'; controller.abort(); }
71
+ const timer = window.setTimeout(function () { abortReason = t('ui.requestTimedOut'); controller.abort(); }, RPC_TIMEOUT_MS);
72
+ function onExternalAbort() { abortReason = t('ui.requestCanceled'); controller.abort(); }
42
73
  function cleanup() {
43
74
  window.clearTimeout(timer);
44
75
  if (externalSignal) externalSignal.removeEventListener('abort', onExternalAbort);
@@ -46,7 +77,7 @@ function rpc(method, args, externalSignal) {
46
77
  if (externalSignal) {
47
78
  if (externalSignal.aborted) {
48
79
  cleanup();
49
- return Promise.reject(new Error('请求已取消'));
80
+ return Promise.reject(new Error(t('ui.requestCanceled')));
50
81
  }
51
82
  externalSignal.addEventListener('abort', onExternalAbort);
52
83
  }
@@ -64,12 +95,12 @@ function rpc(method, args, externalSignal) {
64
95
  });
65
96
  }
66
97
  return res.text().then(function (raw) {
67
- try { return JSON.parse(raw); } catch (e) { throw new Error('响应解析失败'); }
98
+ try { return JSON.parse(raw); } catch (e) { throw new Error(t('ui.couldNotParseResponse')); }
68
99
  });
69
100
  }).catch(function (err) {
70
101
  // 本函数主动 abort(超时/外部取消)→ 统一为可读错误;其余错误原样抛出
71
102
  if (abortReason !== null) throw new Error(abortReason);
72
- if (err && err.name === 'AbortError') throw new Error('请求已取消');
103
+ if (err && err.name === 'AbortError') throw new Error(t('ui.requestCanceled'));
73
104
  throw err;
74
105
  }).finally(cleanup);
75
106
  }
@@ -88,7 +119,7 @@ function mergeLoadResults(prev, results) {
88
119
  } else {
89
120
  next[key] = prev[key];
90
121
  const reason = r && r.reason;
91
- next.errors[key] = reason && reason.message ? String(reason.message) : String(reason || 'RPC 失败');
122
+ next.errors[key] = reason && reason.message ? String(reason.message) : String(reason || t('ui.rpcFailed'));
92
123
  }
93
124
  }
94
125
  return next;
@@ -98,40 +129,40 @@ function mergeLoadResults(prev, results) {
98
129
  // 字段注册表/预设色板由构建从 constants.js 注入(单一来源,宿主白名单同源)
99
130
  const FIELD_REGISTRY = [
100
131
  // 插件字段 · 服务锚点(D6:与其他字段同等可隐藏)
101
- { id: 'anchorGroup', label: '服务商与模型', group: 'plugin', modes: ['balance'], anchor: true, colorKind: 'provider', note: '当前对话使用的服务商与模型名,信息栏的身份锚点' },
102
- { id: 'subServiceGroup', label: '订阅服务与模型', group: 'plugin', modes: ['subscription'], anchor: true, colorKind: 'provider', note: '订阅服务名(ChatGPT/Codex/OpenCode Go/智谱/小米 MiMo)与模型或套餐档位' },
103
- { id: 'billingServiceGroup', label: '账单服务与模型', group: 'plugin', modes: ['billing'], anchor: true, colorKind: 'provider', note: '云账单服务名(Together/Fireworks/AWS Bedrock/Cloudflare)与模型名' },
132
+ { id: 'anchorGroup', label: "field.anchorGroup.label", group: 'plugin', modes: ['balance'], anchor: true, colorKind: 'provider', note: "field.anchorGroup.note" },
133
+ { id: 'subServiceGroup', label: "field.subServiceGroup.label", group: 'plugin', modes: ['subscription'], anchor: true, colorKind: 'provider', note: "field.subServiceGroup.note" },
134
+ { id: 'billingServiceGroup', label: "field.billingServiceGroup.label", group: 'plugin', modes: ['billing'], anchor: true, colorKind: 'provider', note: "field.billingServiceGroup.note" },
104
135
  // 插件字段 · 通用(多模式共用)
105
- { id: 'sessionCost', label: '本会话花费', group: 'plugin', modes: ['balance', 'subscription'], colorKind: 'inherit', note: '当前会话(含子代理)的真实花费;新会话尚无记账时显示 ¥0.000,悬停可看今天/近一月/全部' },
136
+ { id: 'sessionCost', label: "field.sessionCost.label", group: 'plugin', modes: ['balance', 'subscription'], colorKind: 'inherit', note: "field.sessionCost.note" },
106
137
  // 插件字段 · 余额制
107
- { id: 'balance', label: '余额', group: 'plugin', modes: ['balance'], colorKind: 'inherit', note: '服务商账户的真实余额;低余额时数字变红并带「低」字' },
108
- { id: 'period', label: '定价时段', group: 'plugin', modes: ['balance'], colorKind: 'period', note: '仅峰谷价模型显示(当前为 DeepSeek 系列):高峰价红色 / 空闲价绿色' },
109
- { id: 'countdown', label: '时段切换倒计时', group: 'plugin', modes: ['balance'], colorKind: 'inherit', note: '仅峰谷价模型显示:距空闲/距高峰的小时计倒计时' },
138
+ { id: 'balance', label: "ui.balance.pushBalanceGroups", group: 'plugin', modes: ['balance'], colorKind: 'inherit', note: "field.balance.note" },
139
+ { id: 'period', label: "field.period.label", group: 'plugin', modes: ['balance'], colorKind: 'period', note: "field.period.note" },
140
+ { id: 'countdown', label: "field.countdown.label", group: 'plugin', modes: ['balance'], colorKind: 'inherit', note: "field.countdown.note" },
110
141
  // 插件字段 · 订阅制
111
- { id: 'expiry', label: '订阅到期日', group: 'plugin', modes: ['subscription'], colorKind: 'inherit', note: '订阅登录凭据中带有到期信息时显示(如 Codex 套餐)' },
112
- { id: 'subWindow5h', label: '5 小时额度窗口', group: 'plugin', modes: ['subscription'], colorKind: 'inherit', note: '5 小时滚动额度剩余百分比;简洁模式下仅显示时间最短的可用窗口' },
113
- { id: 'subWindowWeek', label: '周额度窗口', group: 'plugin', modes: ['subscription'], colorKind: 'inherit', note: '每周额度剩余百分比;完整模式显示,简洁模式让位给更短窗口' },
114
- { id: 'subWindowMonth', label: '月额度窗口', group: 'plugin', modes: ['subscription'], colorKind: 'inherit', note: '每月额度剩余百分比;完整模式显示,简洁模式让位给更短窗口' },
115
- { id: 'resetCountdown', label: '额度重置倒计时', group: 'plugin', modes: ['subscription'], colorKind: 'inherit', note: '当前显示的额度窗口距重置的倒计时' },
116
- { id: 'subBalance', label: '充值余额', group: 'plugin', modes: ['subscription'], colorKind: 'inherit', note: '订阅源为充值余额形态时显示(如智谱按量账户),与额度窗口互斥' },
142
+ { id: 'expiry', label: "field.expiry.label", group: 'plugin', modes: ['subscription'], colorKind: 'inherit', note: "field.expiry.note" },
143
+ { id: 'subWindow5h', label: "field.subWindow5h.label", group: 'plugin', modes: ['subscription'], colorKind: 'inherit', note: "field.subWindow5h.note" },
144
+ { id: 'subWindowWeek', label: "field.subWindowWeek.label", group: 'plugin', modes: ['subscription'], colorKind: 'inherit', note: "field.subWindowWeek.note" },
145
+ { id: 'subWindowMonth', label: "field.subWindowMonth.label", group: 'plugin', modes: ['subscription'], colorKind: 'inherit', note: "field.subWindowMonth.note" },
146
+ { id: 'resetCountdown', label: "field.resetCountdown.label", group: 'plugin', modes: ['subscription'], colorKind: 'inherit', note: "field.resetCountdown.note" },
147
+ { id: 'subBalance', label: "ui.prepaidBalance", group: 'plugin', modes: ['subscription'], colorKind: 'inherit', note: "field.subBalance.note" },
117
148
  // 插件字段 · 账单制
118
- { id: 'billingSpend', label: '本月花费/用量', group: 'plugin', modes: ['billing'], colorKind: 'inherit', note: '云账单当前计费周期的真实花费或用量' },
119
- { id: 'budget', label: '预算使用', group: 'plugin', modes: ['billing'], colorKind: 'inherit', note: '仅提供预算查询的服务商显示(当前为 AWS Bedrock)' },
120
- { id: 'freeQuota', label: '免费额度与重置', group: 'plugin', modes: ['billing'], colorKind: 'inherit', note: '接口显式给出每日免费额度与重置时刻时才显示,绝不编造' },
149
+ { id: 'billingSpend', label: "field.billingSpend.label", group: 'plugin', modes: ['billing'], colorKind: 'inherit', note: "field.billingSpend.note" },
150
+ { id: 'budget', label: "field.budget.label", group: 'plugin', modes: ['billing'], colorKind: 'inherit', note: "field.budget.note" },
151
+ { id: 'freeQuota', label: "field.freeQuota.label", group: 'plugin', modes: ['billing'], colorKind: 'inherit', note: "field.freeQuota.note" },
121
152
  // 原生字段(DeepSeek 原生底部栏原有;完整模式独占可见)
122
- { id: 'turnsSteps', label: '轮次与步数', group: 'native', modes: ['native'], colorKind: 'inherit', note: '「N 轮 · M 步」对话进度统计' },
123
- { id: 'llmTime', label: 'LLM 耗时', group: 'native', modes: ['native'], colorKind: 'inherit', note: '模型推理累计耗时' },
124
- { id: 'toolTime', label: '工具调用耗时', group: 'native', modes: ['native'], colorKind: 'inherit', note: '工具调用累计耗时' },
125
- { id: 'cacheHit', label: '缓存命中', group: 'native', modes: ['native'], colorKind: 'inherit', note: '提示词缓存命中率百分比' },
126
- { id: 'tokensIO', label: '输入/输出 tokens', group: 'native', modes: ['native'], colorKind: 'inherit', note: '「输入 X tok · 输出 Y tok」本次会话累计用量' },
153
+ { id: 'turnsSteps', label: "field.turnsSteps.label", group: 'native', modes: ['native'], colorKind: 'inherit', note: "field.turnsSteps.note" },
154
+ { id: 'llmTime', label: "field.llmTime.label", group: 'native', modes: ['native'], colorKind: 'inherit', note: "field.llmTime.note" },
155
+ { id: 'toolTime', label: "field.toolTime.label", group: 'native', modes: ['native'], colorKind: 'inherit', note: "field.toolTime.note" },
156
+ { id: 'cacheHit', label: "ui.cacheHit", group: 'native', modes: ['native'], colorKind: 'inherit', note: "field.cacheHit.note" },
157
+ { id: 'tokensIO', label: "field.tokensIO.label", group: 'native', modes: ['native'], colorKind: 'inherit', note: "field.tokensIO.note" },
127
158
  // 插件字段 · 状态与提醒(建议保留)
128
- { id: 'unmapped', label: '未适配提示', group: 'plugin', modes: ['balance'], colorKind: 'muted', note: '当前服务商暂无余额查询适配时出现的弱提示' },
129
- { id: 'noKeyHint', label: '未配置凭据提示', group: 'plugin', modes: ['balance'], suggestKeep: true, colorKind: 'alert', note: '缺少 API Key 时的设置引导文案' },
130
- { id: 'balanceError', label: '余额获取/刷新失败提示', group: 'plugin', modes: ['balance'], suggestKeep: true, colorKind: 'alert', note: '余额查询失败或正在显示上次数据时的提示' },
131
- { id: 'usageError', label: '花费获取失败提示', group: 'plugin', modes: ['balance', 'subscription'], suggestKeep: true, colorKind: 'alert', note: '花费数据暂时不可用时的提示' },
132
- { id: 'refreshFailure', label: '刷新失败提示', group: 'plugin', modes: ['balance', 'subscription', 'billing'], suggestKeep: true, colorKind: 'alert', note: '任一数据源降级时在行尾合并显示的一个提醒(多个来源自动去重)' },
133
- { id: 'persistWarning', label: '账单未保存提醒', group: 'plugin', modes: ['balance', 'subscription', 'billing'], suggestKeep: true, colorKind: 'alert', note: '账本落盘异常时出现;隐藏后金额可能悄悄不准,建议保留' },
134
- { id: 'updateNotice', label: '新版本提醒', group: 'plugin', modes: ['balance', 'subscription', 'billing'], suggestKeep: true, colorKind: 'alert', note: 'npm 上有新版本时提醒更新' },
159
+ { id: 'unmapped', label: "field.unmapped.label", group: 'plugin', modes: ['balance'], colorKind: 'muted', note: "field.unmapped.note" },
160
+ { id: 'noKeyHint', label: "field.noKeyHint.label", group: 'plugin', modes: ['balance'], suggestKeep: true, colorKind: 'alert', note: "field.noKeyHint.note" },
161
+ { id: 'balanceError', label: "field.balanceError.label", group: 'plugin', modes: ['balance'], suggestKeep: true, colorKind: 'alert', note: "field.balanceError.note" },
162
+ { id: 'usageError', label: "field.usageError.label", group: 'plugin', modes: ['balance', 'subscription'], suggestKeep: true, colorKind: 'alert', note: "field.usageError.note" },
163
+ { id: 'refreshFailure', label: "field.refreshFailure.label", group: 'plugin', modes: ['balance', 'subscription', 'billing'], suggestKeep: true, colorKind: 'alert', note: "field.refreshFailure.note" },
164
+ { id: 'persistWarning', label: "field.persistWarning.label", group: 'plugin', modes: ['balance', 'subscription', 'billing'], suggestKeep: true, colorKind: 'alert', note: "field.persistWarning.note" },
165
+ { id: 'updateNotice', label: "ui.updateAvailable", group: 'plugin', modes: ['balance', 'subscription', 'billing'], suggestKeep: true, colorKind: 'alert', note: "field.updateNotice.note" },
135
166
  ];
136
167
  const PRESET_COLORS = ['red', 'green', 'blue', 'purple', 'orange', 'neutral'];
137
168
  const PRESET_COLOR_SET = new Set(PRESET_COLORS);
@@ -322,7 +353,7 @@ function assembleInfoBarRow(groups, trailingErrorGroups, createElement) {
322
353
  const seenRefreshFailure = { value: false };
323
354
  const visibleErrors = trailingErrorGroups.filter(function (node) {
324
355
  const text = trailingErrorText(node);
325
- if (text !== '刷新失败') return true;
356
+ if (text !== t('ui.refreshFailed')) return true;
326
357
  if (seenRefreshFailure.value) return false;
327
358
  seenRefreshFailure.value = true;
328
359
  return true;
@@ -420,27 +451,28 @@ function installStyles() {
420
451
  // (官方 dsh-client-ui-settings-general 同型写法),不再有 async 串接。
421
452
  // 约定:本段顶层标识符一律 bibSet* / InfoBarSettings* 前缀,绝不与主包重名。
422
453
  const BIB_SET_EVENT = 'dsh-bib-config-changed';
423
- const BIB_SET_PRESET_LABELS = { red: '红', green: '绿', blue: '蓝', purple: '紫', orange: '橙', neutral: '中性' };
454
+ const BIB_SET_PRESET_LABELS = { red: "color.red", green: "color.green", blue: "color.blue", purple: "color.purple", orange: "color.orange", neutral: "color.neutral" };
424
455
  // 原生取色器(input[type=color])在未自定义时显示的代表色(浅色主题值;实际信息栏渲染仍按主题变量)
425
456
  const BIB_SET_PRESET_WELL_HEX = { red: '#D92D20', green: '#087F5B', blue: '#0044CC', purple: '#6941C6', orange: '#B54708', neutral: '#333333' };
426
457
  const BIB_SET_HEX_PATTERN = /^#[0-9a-fA-F]{6}$/;
427
458
  // 「显示字段」两级分组(D6 用户拍板:原生在前、插件在后)——构建从 constants.js 注入(单一来源)
428
459
  const FIELD_GROUP_ORDER = ['native', 'plugin'];
429
460
  const FIELD_GROUP_LABELS = {
430
- native: '原生字段',
431
- plugin: '插件字段',
461
+ native: "group.native",
462
+ plugin: "group.plugin",
432
463
  };
433
464
  // 每行出现条件文案:modes → 中文(余额制/订阅制/账单制/原生统计行/通用)
434
- const BIB_SET_MODE_LABELS = { balance: '余额制', subscription: '订阅制', billing: '账单制', native: '原生统计行', common: '通用' };
465
+ const BIB_SET_MODE_LABELS = { balance: "mode.balance", subscription: "mode.subscription", billing: "mode.billing", native: "mode.native", common: "mode.common" };
435
466
  function bibSetModeText(field) {
436
467
  if (!field || !Array.isArray(field.modes) || field.modes.length === 0) return '';
437
- if (field.modes.length === 1 && field.modes[0] === 'native') return '原生统计行字段';
468
+ if (field.modes.length === 1 && field.modes[0] === 'native') return t('ui.nativeStatsField');
438
469
  const labels = [];
439
470
  for (let i = 0; i < field.modes.length; i++) {
440
- const label = BIB_SET_MODE_LABELS[field.modes[i]];
471
+ const key = BIB_SET_MODE_LABELS[field.modes[i]];
472
+ const label = key && t(key);
441
473
  if (label && labels.indexOf(label) === -1) labels.push(label);
442
474
  }
443
- return labels.length > 0 ? '出现于' + labels.join('、') : '';
475
+ return labels.length > 0 ? t('ui.shownIn') + labels.join(t('ui.listSeparator')) : '';
444
476
  }
445
477
 
446
478
  function bibSetDispatchChanged() {
@@ -449,7 +481,7 @@ function bibSetDispatchChanged() {
449
481
  }
450
482
 
451
483
  function bibSetOperationMessage(err) {
452
- return String((err && err.message) || err || '请稍后再试');
484
+ return String((err && err.message) || err || t('ui.pleaseTryAgainLater'));
453
485
  }
454
486
 
455
487
  // ---------- 设置页样式(融入 DSH 设置面板:卡片/行布局/控件全部走 --dsw-alias-* 令牌) ----------
@@ -563,7 +595,7 @@ function bibSetPalette(props) {
563
595
  const isDefault = option === 'default';
564
596
  const coreStyle = {};
565
597
  if (!isDefault) coreStyle.background = 'var(--bi-palette-' + option + ')';
566
- const ariaLabel = isDefault ? '恢复默认颜色' : (BIB_SET_PRESET_LABELS[option] || option);
598
+ const ariaLabel = isDefault ? t('ui.restoreDefaultColor') : (t(BIB_SET_PRESET_LABELS[option]) || option);
567
599
  return React.createElement('button', {
568
600
  key: option,
569
601
  type: 'button',
@@ -584,7 +616,7 @@ function bibSetPalette(props) {
584
616
  // ---------- 页面组件 ----------
585
617
  // M2 首渲骨架:页面标题行——任何状态下都先输出可见标题(数据未到、加载失败也一样有东西看)
586
618
  function bibSetPageTitle() {
587
- return React.createElement('h1', { className: 'bib-set-page-title' }, '信息底栏设置');
619
+ return React.createElement('h1', { className: 'bib-set-page-title' }, t('ui.infoBarSettings'));
588
620
  }
589
621
 
590
622
  function InfoBarSettingsSection(props) {
@@ -607,7 +639,7 @@ function InfoBarSettingsSection(props) {
607
639
  setSnapshot({ fields: cfg.fields, colors: cfg.colors || {}, configVersion: cfg.configVersion || 0 });
608
640
  setStatus('ready');
609
641
  } else {
610
- setLoadError('配置暂时无法读取'); setStatus('error');
642
+ setLoadError(t('ui.settingsAreTemporarilyUnavailable')); setStatus('error');
611
643
  }
612
644
  }).catch(function (err) {
613
645
  if (!active) return;
@@ -632,13 +664,13 @@ function InfoBarSettingsSection(props) {
632
664
  if (status === 'loading') {
633
665
  return React.createElement('div', { className: 'bib-set-root bib-settings' },
634
666
  bibSetPageTitle(),
635
- React.createElement('p', { className: 'bib-set-status' }, '正在载入信息底栏设置…'));
667
+ React.createElement('p', { className: 'bib-set-status' }, t('ui.loadingInfoBarSettings')));
636
668
  }
637
669
  // RPC 加载失败:照旧显示可见错误文字(角色 alert),页面不空白
638
670
  if (status === 'error') {
639
671
  return React.createElement('div', { className: 'bib-set-root bib-settings' },
640
672
  bibSetPageTitle(),
641
- React.createElement('p', { className: 'bib-set-alert', role: 'alert' }, '信息底栏设置暂时无法读取:' + (loadError || '请稍后再试')));
673
+ React.createElement('p', { className: 'bib-set-alert', role: 'alert' }, t('ui.couldNotLoadInfoBar') + (hostText(loadError) || t('ui.pleaseTryAgainLater'))));
642
674
  }
643
675
 
644
676
  const fieldsById = {};
@@ -648,7 +680,7 @@ function InfoBarSettingsSection(props) {
648
680
  const value = snapshot.colors[id];
649
681
  return typeof value === 'string' && value.length > 0 ? value : null;
650
682
  }
651
- function labelOf(id) { const f = fieldsById[id]; return f ? f.label : id; }
683
+ function labelOf(id) { const f = fieldsById[id]; return f ? t(f.label) : id; }
652
684
  function makePair(key, value) { const pair = {}; pair[key] = value; return pair; }
653
685
 
654
686
  // 服务端快照回写(configVersion 一并更新;persisted=false 时如实提示落盘失败)
@@ -661,7 +693,7 @@ function InfoBarSettingsSection(props) {
661
693
  configVersion: typeof res.configVersion === 'number' ? res.configVersion : ((prev && prev.configVersion) || 0),
662
694
  };
663
695
  });
664
- if (res.persisted === false) setNotice('改动已生效,但写入本地配置文件失败:' + (res.warning || '未知原因'));
696
+ if (res.persisted === false) setNotice({ text: function () { return t('ui.changesAppliedButCouldNot') + (hostText(res.warning) || t('ui.unknownReason')); } });
665
697
  }
666
698
 
667
699
  // 乐观更新 + 失败回退(参照 density toggle)+ 版本号守卫
@@ -680,7 +712,9 @@ function InfoBarSettingsSection(props) {
680
712
  if (seq !== opSeqRef.current) { endOp(); return; }
681
713
  endOp();
682
714
  revertOptimistic();
683
- setOpError(errorPrefix + '保存失败:' + bibSetOperationMessage(err));
715
+ // Translate feedback during rendering, including its field label. The
716
+ // object wrapper prevents React from treating the thunk as a state updater.
717
+ setOpError({ text: function () { return t('ui.couldNotSave', { errorPrefix: errorPrefix(), value: hostText(bibSetOperationMessage(err)) }); } });
684
718
  });
685
719
  }
686
720
 
@@ -690,7 +724,7 @@ function InfoBarSettingsSection(props) {
690
724
  commit({ fields: makePair(id, next) },
691
725
  function () { setSnapshot(function (s) { return Object.assign({}, s, { fields: Object.assign({}, s.fields, makePair(id, next)) }); }); },
692
726
  function () { setSnapshot(function (s) { return Object.assign({}, s, { fields: Object.assign({}, s.fields, makePair(id, previous)) }); }); },
693
- '「' + labelOf(id) + '」');
727
+ function () { return t('ui.fieldErrorPrefix', { label: labelOf(id) }); });
694
728
  }
695
729
 
696
730
  function setColor(id, next) {
@@ -702,7 +736,7 @@ function InfoBarSettingsSection(props) {
702
736
  commit({ colors: makePair(id, next) },
703
737
  function () { setSnapshot(function (s) { return Object.assign({}, s, { colors: Object.assign({}, s.colors, makePair(id, next)) }); }); },
704
738
  function () { setSnapshot(function (s) { return Object.assign({}, s, { colors: Object.assign({}, s.colors, makePair(id, previous)) }); }); },
705
- '「' + labelOf(id) + '」颜色');
739
+ function () { return t('ui.color', { value: labelOf(id) }); });
706
740
  }
707
741
 
708
742
  // hex 输入:输入中仅标记非法(描红 + aria-invalid);Enter/失焦时合法才提交,非法回退当前值
@@ -725,7 +759,7 @@ function InfoBarSettingsSection(props) {
725
759
  setHexDrafts(function (drafts) { return Object.assign({}, drafts, makePair(id, undefined)); });
726
760
  if (value.length === 0) return;
727
761
  if (!BIB_SET_HEX_PATTERN.test(value)) {
728
- setOpError('「' + labelOf(id) + '」颜色格式无效:请输入 #RRGGBB(例如 #0044CC)');
762
+ setOpError({ text: function () { return t('ui.hasAnInvalidColorEnter', { value: labelOf(id) }); } });
729
763
  return;
730
764
  }
731
765
  setColor(id, value.toUpperCase());
@@ -741,11 +775,11 @@ function InfoBarSettingsSection(props) {
741
775
  endOp();
742
776
  applyServerResult(res);
743
777
  bibSetDispatchChanged();
744
- setNotice(kind === 'colors' ? '已恢复默认颜色' : '已恢复默认标签');
778
+ setNotice({ text: function () { return kind === 'colors' ? t('ui.defaultColorsRestored') : t('ui.defaultLabelsRestored'); } });
745
779
  }).catch(function (err) {
746
780
  if (seq !== opSeqRef.current) { endOp(); return; }
747
781
  endOp();
748
- setOpError('重置失败:' + bibSetOperationMessage(err));
782
+ setOpError({ text: function () { return t('ui.couldNotReset', { value: hostText(bibSetOperationMessage(err)) }); } });
749
783
  });
750
784
  }
751
785
 
@@ -761,26 +795,26 @@ function InfoBarSettingsSection(props) {
761
795
  const isAnchor = field.anchor === true;
762
796
  const descParts = [];
763
797
  // D6:锚点与其他字段同等可隐藏——「身份锚点」仅作为说明保留,不再恒开/禁用
764
- if (isAnchor) descParts.push('身份锚点:当前对话的服务商与模型标识。');
798
+ if (isAnchor) descParts.push(t('ui.identifiesTheProviderAndModel'));
765
799
  const modeText = bibSetModeText(field);
766
- if (modeText) descParts.push(modeText + '。');
767
- if (field.note) descParts.push(field.note);
768
- if (field.suggestKeep) descParts.push('关闭后相应提示不再出现,建议保留。');
800
+ if (modeText) descParts.push(modeText + t('ui.sentenceEnd'));
801
+ if (field.note) descParts.push(t(field.note));
802
+ if (field.suggestKeep) descParts.push(t('ui.keepEnabledToSeeThese'));
769
803
  rows.push(React.createElement('div', { key: field.id, className: 'bib-set-row' },
770
804
  React.createElement('div', { className: 'bib-set-rowText' },
771
805
  React.createElement('div', { className: 'bib-set-rowTitle' },
772
- field.label,
773
- field.suggestKeep ? React.createElement('span', { className: 'bib-set-keep' }, '建议保留') : null),
774
- React.createElement('div', { className: 'bib-set-rowDesc' }, descParts.join(''))),
806
+ t(field.label),
807
+ field.suggestKeep ? React.createElement('span', { className: 'bib-set-keep' }, t('ui.recommended')) : null),
808
+ React.createElement('div', { className: 'bib-set-rowDesc' }, descParts.join(t('ui.sentenceSeparator')))),
775
809
  bibSetSwitch({
776
- label: '显示' + field.label,
810
+ label: t('ui.show', { label: t(field.label) }),
777
811
  checked: fieldOn(field.id),
778
- title: fieldOn(field.id) ? '点击隐藏' : '点击显示',
812
+ title: fieldOn(field.id) ? t('ui.clickToHide') : t('ui.clickToShow'),
779
813
  onToggle: function (next) { setFieldFlag(field.id, next); },
780
814
  })));
781
815
  }
782
816
  groupsChildren.push(React.createElement('div', { key: 'g-' + group },
783
- React.createElement('div', { className: 'bib-set-group-title' }, FIELD_GROUP_LABELS[group] || group),
817
+ React.createElement('div', { className: 'bib-set-group-title' }, FIELD_GROUP_LABELS[group] ? t(FIELD_GROUP_LABELS[group]) : group),
784
818
  rows));
785
819
  }
786
820
 
@@ -799,23 +833,23 @@ function InfoBarSettingsSection(props) {
799
833
  const hexValue = draft !== null ? draft : committedHexText(field.id);
800
834
  const hexInvalid = draft !== null && draft.trim().length > 0 && !BIB_SET_HEX_PATTERN.test(draft.trim());
801
835
  const wellValue = isHex ? value : (isPreset ? (BIB_SET_PRESET_WELL_HEX[value] || '#333333') : '#333333');
802
- const valueText = value === null ? '默认'
803
- : (isPreset ? (BIB_SET_PRESET_LABELS[value] || value) : value.toUpperCase());
836
+ const valueText = value === null ? t('ui.default')
837
+ : (isPreset ? (t(BIB_SET_PRESET_LABELS[value]) || value) : value.toUpperCase());
804
838
  rows.push(React.createElement('div', { key: field.id, className: 'bib-set-row' },
805
839
  React.createElement('div', { className: 'bib-set-rowText' },
806
- React.createElement('div', { className: 'bib-set-rowTitle' }, field.label),
807
- React.createElement('div', { className: 'bib-set-rowDesc' }, '当前:' + valueText + (field.anchor === true ? '(身份锚点)' : ''))),
840
+ React.createElement('div', { className: 'bib-set-rowTitle' }, t(field.label)),
841
+ React.createElement('div', { className: 'bib-set-rowDesc' }, t('ui.current', { valueText: valueText }) + (field.anchor === true ? t('ui.providerAndModel') : ''))),
808
842
  React.createElement('div', { className: 'bib-set-controls' },
809
843
  React.createElement(bibSetPalette, {
810
- label: field.label + '的预设颜色',
844
+ label: t('ui.presetColor', { label: t(field.label) }),
811
845
  value: value,
812
846
  onSelect: function (next) { setColor(field.id, next); },
813
847
  }),
814
848
  React.createElement('label', { className: 'bib-set-well' },
815
849
  React.createElement('input', {
816
850
  type: 'color',
817
- 'aria-label': field.label + '的自定义颜色',
818
- title: '自定义颜色(打开系统取色器)',
851
+ 'aria-label': t('ui.customColor', { label: t(field.label) }),
852
+ title: t('ui.customColorOpenColorPicker'),
819
853
  value: wellValue,
820
854
  onChange: function (event) {
821
855
  const picked = event && event.target ? event.target.value : null;
@@ -825,7 +859,7 @@ function InfoBarSettingsSection(props) {
825
859
  React.createElement('input', {
826
860
  type: 'text',
827
861
  className: 'bib-set-hex',
828
- 'aria-label': field.label + '的十六进制颜色',
862
+ 'aria-label': t('ui.hexColor', { label: t(field.label) }),
829
863
  'aria-invalid': hexInvalid ? 'true' : 'false',
830
864
  'data-invalid': hexInvalid ? 'true' : 'false',
831
865
  placeholder: '#RRGGBB',
@@ -838,52 +872,54 @@ function InfoBarSettingsSection(props) {
838
872
  }))));
839
873
  }
840
874
  colorChildren.push(React.createElement('div', { key: 'c-' + group },
841
- React.createElement('div', { className: 'bib-set-group-title' }, FIELD_GROUP_LABELS[group] || group),
875
+ React.createElement('div', { className: 'bib-set-group-title' }, FIELD_GROUP_LABELS[group] ? t(FIELD_GROUP_LABELS[group]) : group),
842
876
  rows));
843
877
  }
844
878
 
845
879
  const feedback = [];
846
- if (opError) feedback.push(React.createElement('p', { key: 'err', className: 'bib-set-alert', role: 'alert' }, opError));
847
- else if (notice) feedback.push(React.createElement('p', { key: 'notice', className: 'bib-set-notice', role: 'status' }, notice));
848
- else if (saving) feedback.push(React.createElement('p', { key: 'saving', className: 'bib-set-notice', 'aria-live': 'polite' }, '正在保存…'));
880
+ if (opError) feedback.push(React.createElement('p', { key: 'err', className: 'bib-set-alert', role: 'alert' }, opError.text()));
881
+ else if (notice) feedback.push(React.createElement('p', { key: 'notice', className: 'bib-set-notice', role: 'status' }, notice.text()));
882
+ else if (saving) feedback.push(React.createElement('p', { key: 'saving', className: 'bib-set-notice', 'aria-live': 'polite' }, t('ui.saving')));
849
883
 
850
884
  return React.createElement('div', { className: 'bib-set-root bib-settings' },
851
885
  bibSetPageTitle(),
852
886
  React.createElement('p', { className: 'bib-set-intro' },
853
- '选择信息底栏显示哪些信息、为每个信息挑一个颜色。改动即时生效并自动保存,重启后仍然保留;隐藏信息不会删除任何记账数据。'),
887
+ t('ui.chooseWhichFieldsToShow')),
854
888
  React.createElement('section', { className: 'bib-set-card', 'aria-labelledby': 'bib-set-fields-title' },
855
889
  React.createElement('div', { className: 'bib-set-card-header' },
856
- React.createElement('h2', { id: 'bib-set-fields-title', className: 'bib-set-card-title' }, '显示字段'),
890
+ React.createElement('h2', { id: 'bib-set-fields-title', className: 'bib-set-card-title' }, t('ui.visibleFields')),
857
891
  React.createElement('div', { className: 'bib-set-card-desc' },
858
- '关闭的字段立刻从信息栏隐藏且不占位;「刷新失败」「账单未保存」等提示建议保留,方便发现数据异常。')),
892
+ t('ui.hiddenFieldsTakeNoSpace'))),
859
893
  React.createElement('div', { className: 'bib-set-body' }, groupsChildren)),
860
894
  React.createElement('section', { className: 'bib-set-card', 'aria-labelledby': 'bib-set-colors-title' },
861
895
  React.createElement('div', { className: 'bib-set-card-header' },
862
- React.createElement('h2', { id: 'bib-set-colors-title', className: 'bib-set-card-title' }, '字段颜色'),
896
+ React.createElement('h2', { id: 'bib-set-colors-title', className: 'bib-set-card-title' }, t('ui.fieldColors')),
863
897
  React.createElement('div', { className: 'bib-set-card-desc' },
864
- '挑选预设色或用取色器、十六进制自定义;颜色会自动适配浅色与深色主题,保持可读。未改动前与现在的外观完全一致。')),
898
+ t('ui.chooseAPresetUseThe'))),
865
899
  React.createElement('div', { className: 'bib-set-body' }, colorChildren)),
866
900
  React.createElement('div', { className: 'bib-set-footer' },
867
901
  feedback,
868
902
  React.createElement('button', {
869
903
  type: 'button', className: 'bib-set-btn', disabled: saving,
870
904
  onClick: function () { runReset('fields'); },
871
- }, '重置标签'),
905
+ }, t('ui.resetLabels')),
872
906
  React.createElement('button', {
873
907
  type: 'button', className: 'bib-set-btn', disabled: saving,
874
908
  onClick: function () { runReset('colors'); },
875
- }, '重置颜色')));
909
+ }, t('ui.resetColors'))));
876
910
  } catch (err) {
877
911
  // M2:渲染期异常绝不以白屏收场——改为页面内可见的红色错误框(role=alert,含错误信息)
878
912
  return React.createElement('div', { className: 'bib-set-root bib-settings' },
879
913
  bibSetPageTitle(),
880
- React.createElement('p', { className: 'bib-set-alert', role: 'alert' }, '信息底栏设置渲染出错:' + bibSetOperationMessage(err)));
914
+ React.createElement('p', { className: 'bib-set-alert', role: 'alert' }, t('ui.couldNotDisplayInfoBar', { value: bibSetOperationMessage(err) })));
881
915
  }
882
916
  }
883
917
 
884
918
  module.exports = {
885
- inject: ['slots'],
919
+ inject: ['slots', 'locale'],
886
920
  async apply(ctx) {
921
+ ctx.effect(function () { return ctx.locale.register(LOCALE_NAMESPACE, LOCALES); }, 'info bar: dictionaries');
922
+ t = ctx.locale.bind(LOCALE_NAMESPACE);
887
923
  // slots 服务可能晚于 apply 就绪:优先 ctx.slots(inject 注入属性),回退 ctx.get('slots');
888
924
  // 仍不可用则轮询等待(最多 60×300ms ≈ 18s),绝不提前退出导致注册丢失
889
925
  let slots = ctx.slots || ctx.get('slots');
@@ -915,7 +951,7 @@ module.exports = {
915
951
  if (occupantDispose) { occupantDispose(); occupantDispose = null; }
916
952
  occupantDispose = slots.register(
917
953
  // 静态注册无动态沙箱的优先级自动分配:显式给低 priority(最低者渲染)以遮蔽原生 stats 栏(priority 0)
918
- { name: 'conversation.composer.dock', id: 'stats', priority: -1000 },
954
+ { name: 'conversation.composer.dock', id: 'stats', priority: -1000, locale: LOCALE_NAMESPACE },
919
955
  function (slotProps) {
920
956
  return React.createElement(BottomInfoBar, Object.assign({}, slotProps, { density: density, onToggleDensity: onToggleDensity }));
921
957
  }
@@ -964,7 +1000,7 @@ module.exports = {
964
1000
  }, 'dsh-bottom-info-bar: settings styles');
965
1001
  slots.inject('settings.section', function () {
966
1002
  return slots.register(
967
- { name: 'settings.section', id: 'bottom-info-bar', order: 100, label: '信息底栏' },
1003
+ { name: 'settings.section', id: 'bottom-info-bar', order: 100, locale: LOCALE_NAMESPACE, label: function () { return t('ui.infoBar'); } },
968
1004
  InfoBarSettingsSection);
969
1005
  });
970
1006
 
@@ -1240,11 +1276,15 @@ module.exports = {
1240
1276
  return Math.max(0, 100 - w.usedPercent);
1241
1277
  }
1242
1278
  // 订阅窗口紧凑行标签(5小时 → '5h',周 → '周',月 → '月');hover 明细仍用完整标签
1279
+ function quotaWindowLabel(window) {
1280
+ const keys = { five_hour: 'host.hour', seven_day: 'ui.weekly', monthly: 'ui.monthly' };
1281
+ return keys[window.key] ? t(keys[window.key]) : window.label;
1282
+ }
1243
1283
  function compactWindowLabel(key) {
1244
1284
  if (key === 'five_hour') return '5h';
1245
- if (key === 'seven_day') return '周';
1246
- if (key === 'monthly') return '月';
1247
- return '窗口';
1285
+ if (key === 'seven_day') return t('ui.weekly');
1286
+ if (key === 'monthly') return t('ui.monthly');
1287
+ return t('ui.window');
1248
1288
  }
1249
1289
 
1250
1290
  // 数字统一加粗(仅数字本身)
@@ -1265,9 +1305,9 @@ module.exports = {
1265
1305
  return React.createElement('span', { className: 'bi-model-capability-pending', 'aria-hidden': 'true' }, modelLabel);
1266
1306
  }
1267
1307
  if (!pr || pr.acceptsImageInput !== true) return React.createElement('span', { className: 'bi-model-name' }, modelLabel);
1268
- return React.createElement('span', { className: 'bi-vision', title: '支持图像输入。' },
1308
+ return React.createElement('span', { className: 'bi-vision', title: t('ui.supportsImageInput') },
1269
1309
  React.createElement('span', { className: 'bi-vision-model' }, modelLabel),
1270
- React.createElement('span', { className: 'bi-vision-kind' }, '视觉'));
1310
+ React.createElement('span', { className: 'bi-vision-kind' }, t('ui.vision')));
1271
1311
  }
1272
1312
 
1273
1313
  function modelSeparator() {
@@ -1293,15 +1333,15 @@ module.exports = {
1293
1333
  // 既保留服务商信息,也不让模型名重复前缀。
1294
1334
  function providerGroup() {
1295
1335
  const pr = visiblePricing;
1296
- const provLabel = (pr && pr.providerDisplay) ? pr.providerDisplay : '未知';
1336
+ const provLabel = (pr && pr.providerDisplay) ? pr.providerDisplay : t('ui.unknown');
1297
1337
  const modelLabel = (pr && pr.modelDisplay) ? pr.modelDisplay
1298
- : (pr && pr.model ? pr.model : '未知模型');
1338
+ : (pr && pr.model ? pr.model : t('ui.unknownModel'));
1299
1339
  const modelName = modelLabelWithoutProvider(modelLabel, provLabel);
1300
1340
  const versionLine = updateInfo && typeof updateInfo.current === 'string'
1301
- ? '\n插件版本:' + updateInfo.current : '';
1302
- const provTitle = '服务商:' + provLabel + ' ' + modelLabel + '\n'
1303
- + (pr && pr.mode === 'peak-valley' ? '定价:峰谷价(工作日高峰 9-12、14-18 点;周末全天空闲)'
1304
- : (pr && pr.mode === 'flat' ? '定价:固定价' : '定价:未收录,按默认计'))
1341
+ ? t('ui.pluginVersion', { current: updateInfo.current }) : '';
1342
+ const provTitle = t('ui.provider', { provLabel: provLabel, modelLabel: modelLabel })
1343
+ + (pr && pr.mode === 'peak-valley' ? t('ui.pricingPeakOffPeakBeijing')
1344
+ : (pr && pr.mode === 'flat' ? t('ui.pricingFixed') : t('ui.pricingNotListedUsingDefaults')))
1305
1345
  + versionLine;
1306
1346
  return React.createElement('span', { key: 'prov', className: 'bi-model-group', title: provTitle },
1307
1347
  React.createElement('b', { className: 'bi-model-provider' }, provLabel),
@@ -1315,9 +1355,9 @@ module.exports = {
1315
1355
  if (provider === 'chatgpt' || provider === 'openai-codex') return 'ChatGPT';
1316
1356
  if (provider === 'codex') return 'Codex';
1317
1357
  if (provider === 'opencode-go' || provider === 'opencode') return 'OpenCode Go';
1318
- if (provider === 'zai' || provider === 'zai-coding-cn') return '智谱';
1319
- if (provider === 'xiaomi-token-plan-cn' || provider === 'xiaomi-token-plan-sgp' || provider === 'xiaomi-token-plan-ams') return '小米 MiMo';
1320
- return '订阅';
1358
+ if (provider === 'zai' || provider === 'zai-coding-cn') return t('ui.zhipu');
1359
+ if (provider === 'xiaomi-token-plan-cn' || provider === 'xiaomi-token-plan-sgp' || provider === 'xiaomi-token-plan-ams') return t('ui.xiaomiMiMo');
1360
+ return t('ui.subscription');
1321
1361
  }
1322
1362
 
1323
1363
  // 账单型服务名(v1.7:云账单 provider 显示品牌名,未知保持兜底)
@@ -1326,7 +1366,7 @@ module.exports = {
1326
1366
  if (provider === 'fireworks') return 'Fireworks';
1327
1367
  if (provider === 'amazon-bedrock') return 'AWS Bedrock';
1328
1368
  if (provider === 'cloudflare-ai-gateway' || provider === 'cloudflare-workers-ai') return 'Cloudflare';
1329
- return '云账单';
1369
+ return t('ui.cloudBilling');
1330
1370
  }
1331
1371
 
1332
1372
  // 套餐档位短名(JWT 订阅卡:plus/pro/team/enterprise → Plus/Pro/Team/Enterprise;未知返回 null 显示模型名)
@@ -1352,14 +1392,14 @@ module.exports = {
1352
1392
  const subSnapshot = state.sub;
1353
1393
  const planShort = subSnapshot && subSnapshot.planType ? subscriptionPlanShort(subSnapshot.planType) : null;
1354
1394
  const rawModelLabel = (pr && pr.modelDisplay) ? pr.modelDisplay
1355
- : (pr && pr.model ? pr.model : '未知模型');
1395
+ : (pr && pr.model ? pr.model : t('ui.unknownModel'));
1356
1396
  const modelLabel = planShort ? planShort : rawModelLabel;
1357
1397
  const modelName = modelLabelWithoutProvider(modelLabel, serviceName);
1358
1398
  const versionLine = updateInfo && typeof updateInfo.current === 'string'
1359
- ? '\n插件版本:' + updateInfo.current : '';
1360
- const planLine = subSnapshot && subSnapshot.plan ? '\n套餐:' + subSnapshot.plan : '';
1361
- const expiryLine = subSnapshot && subSnapshot.expiryAt ? '\n到期:' + formatDate(subSnapshot.expiryAt) + '(本地时区)' : '';
1362
- const title = '订阅服务:' + serviceName + '\n模型:' + rawModelLabel + planLine + expiryLine + versionLine;
1399
+ ? t('ui.pluginVersion', { current: updateInfo.current }) : '';
1400
+ const planLine = subSnapshot && subSnapshot.plan ? t('ui.plan', { plan: hostText(subSnapshot.plan) }) : '';
1401
+ const expiryLine = subSnapshot && subSnapshot.expiryAt ? t('ui.expiresLocalTime', { value: formatDate(subSnapshot.expiryAt) }) : '';
1402
+ const title = t('ui.subscriptionServiceModel', { serviceName: serviceName, rawModelLabel: rawModelLabel, planLine: planLine, expiryLine: expiryLine, versionLine: versionLine });
1363
1403
  return React.createElement('span', { key: 'subprov', className: 'bi-model-group', title: title },
1364
1404
  React.createElement('b', { className: 'bi-model-provider' }, serviceName),
1365
1405
  modelDetail(pr, modelName),
@@ -1386,45 +1426,45 @@ module.exports = {
1386
1426
  if (bal && bal.unmapped) {
1387
1427
  if (fieldVisible('unmapped')) {
1388
1428
  trailingErrorGroups.push(fieldSpan('unmapped', 'unmapped',
1389
- React.createElement('span', { className: 'bi-muted', title: '该服务商暂未适配余额查询,后续版本将支持。' }, '未适配')));
1429
+ React.createElement('span', { className: 'bi-muted', title: t('ui.balanceLookupIsNotYet') }, t('ui.notSupported'))));
1390
1430
  }
1391
1431
  }
1392
1432
  // 余额(纯金额;hover 仅展示余额,不显示充值/赠金)
1393
1433
  // v1.6 T7:未配置提示改为按账户显示凭据名(去掉写死的 DeepSeek 文案)
1394
1434
  else if (bal && bal.error && bal.error.kind === 'no-key') {
1395
1435
  if (fieldVisible('noKeyHint')) {
1396
- const credName = bal.error.message ? String(bal.error.message).replace('未配置 ', '') : 'API_KEY';
1436
+ const credName = bal.error.message ? String(hostText(bal.error.message)).replace(/(?:未配置 |Not configured: )/, '') : 'API_KEY';
1397
1437
  trailingErrorGroups.push(fieldSpan('noKeyHint', 'nokey',
1398
- React.createElement('span', { className: 'bi-err', title: '未配置 ' + credName + '。请在"设置 → 模型"中填写。' },
1399
- '未配置 ' + credName + ' → 设置→模型 填写')));
1438
+ React.createElement('span', { className: 'bi-err', title: t('ui.notConfiguredConfigureItIn', { credName: credName }) },
1439
+ t('ui.notConfiguredSettingsModels', { credName: credName }))));
1400
1440
  }
1401
1441
  } else if (bal && bal.data) {
1402
1442
  const symbol = bal.currency === 'USD' ? '$' : '¥';
1403
1443
  const balTitle = bal.estimate
1404
- ? '估算余额:' + symbol + fmt(bal.data.total)
1405
- : '余额:' + symbol + fmt(bal.data.total);
1444
+ ? t('ui.estimatedBalance', { symbol: symbol, value: fmt(bal.data.total) })
1445
+ : t('ui.balance', { symbol: symbol, value: fmt(bal.data.total) });
1406
1446
  if (fieldVisible('balance')) {
1407
1447
  groups.push(fieldSpan('balance', 'bal', React.createElement('span', { title: balTitle },
1408
- metric('余额', symbol + fmt(bal.data.total), alertActive ? 'bi-alert-num' : ''),
1409
- alertActive ? React.createElement('span', { className: 'bi-low-status' }, '低') : null,
1410
- bal.estimate ? React.createElement('span', { className: 'bi-muted' }, '(估算)') : null,
1448
+ metric(t('ui.balance.pushBalanceGroups'), symbol + fmt(bal.data.total), alertActive ? 'bi-alert-num' : ''),
1449
+ alertActive ? React.createElement('span', { className: 'bi-low-status' }, t('ui.low')) : null,
1450
+ bal.estimate ? React.createElement('span', { className: 'bi-muted' }, t('ui.estimated')) : null,
1411
1451
  )));
1412
1452
  }
1413
1453
  // host 快照失败(bal.error)或本次 RPC 失败(errors.balance)→ 均保留旧数据 + 降级标记
1414
1454
  if (fieldVisible('balanceError') && (bal.error || errors.balance)) {
1415
1455
  trailingErrorGroups.push(fieldSpan('balanceError', 'balerr',
1416
- React.createElement('span', { className: 'bi-stale', title: '余额暂不可用;正在显示上次数据并自动重试。' }, '刷新失败')));
1456
+ React.createElement('span', { className: 'bi-stale', title: t('ui.balanceIsTemporarilyUnavailableShowing') }, t('ui.refreshFailed'))));
1417
1457
  }
1418
1458
  } else if (bal && bal.error) {
1419
1459
  if (fieldVisible('balanceError')) {
1420
1460
  trailingErrorGroups.push(fieldSpan('balanceError', 'berr',
1421
- React.createElement('span', { className: 'bi-err', title: '余额获取失败。请检查网络和 API Key。' }, '余额获取失败')));
1461
+ React.createElement('span', { className: 'bi-err', title: t('ui.couldNotLoadBalanceCheck') }, t('ui.balanceUnavailable'))));
1422
1462
  }
1423
1463
  } else if (errors.balance) {
1424
1464
  // 本次 RPC 失败且无旧数据:只降级余额块,其余端点数据照常渲染
1425
1465
  if (fieldVisible('balanceError')) {
1426
1466
  trailingErrorGroups.push(fieldSpan('balanceError', 'berr',
1427
- React.createElement('span', { className: 'bi-err', title: '余额获取失败。请检查网络和 API Key。' }, '余额获取失败')));
1467
+ React.createElement('span', { className: 'bi-err', title: t('ui.couldNotLoadBalanceCheck') }, t('ui.balanceUnavailable'))));
1428
1468
  }
1429
1469
  }
1430
1470
 
@@ -1433,19 +1473,19 @@ module.exports = {
1433
1473
  if (pr && pr.mode === 'peak-valley' && fieldVisible('period')) {
1434
1474
  const peakNow = pr.period === 'peak';
1435
1475
  const p = pr.prices || {};
1436
- const periodTitle = '北京时间 ' + (peakNow ? '高峰价' : '空闲价') + ':输入 ¥' + (p.inputCacheMiss != null ? p.inputCacheMiss : '?')
1437
- + '/M · 缓存 ¥' + (p.inputCacheHit != null ? p.inputCacheHit : '?')
1438
- + '/M · 输出 ¥' + (p.output != null ? p.output : '?') + '/M';
1476
+ const periodTitle = t('ui.beijingTime') + (peakNow ? t('ui.peakPrice') : t('ui.offPeakPrice')) + t('ui.input') + (p.inputCacheMiss != null ? p.inputCacheMiss : '?')
1477
+ + t('ui.mCachedInput') + (p.inputCacheHit != null ? p.inputCacheHit : '?')
1478
+ + t('ui.mOutput') + (p.output != null ? p.output : '?') + '/M';
1439
1479
  groups.push(fieldSpan('period', 'period', React.createElement('span', { className: peakNow ? 'bi-peak' : 'bi-offpeak', title: periodTitle },
1440
- peakNow ? '高峰价' : '空闲价')));
1480
+ peakNow ? t('ui.peakPrice') : t('ui.offPeakPrice'))));
1441
1481
  }
1442
1482
 
1443
1483
  // 倒计时:仅峰谷价服务商显示"距高峰/距空闲"(hover 展示下次切换时刻;数字加粗)
1444
1484
  if (pr && pr.mode === 'peak-valley' && pr.nextSwitch && fieldVisible('countdown')) {
1445
1485
  const peakNow = pr.period === 'peak';
1446
- const countdownTitle = '北京时间 ' + pr.nextSwitch.atLabel + ' 切换为' + (peakNow ? '空闲价' : '高峰价') + '。';
1486
+ const countdownTitle = t('ui.beijingTimeSwitchesTo', { atLabel: pr.nextSwitch.atLabel }) + (peakNow ? t('ui.offPeakPrice') : t('ui.peakPrice')) + t('ui.sentenceEnd');
1447
1487
  groups.push(fieldSpan('countdown', 'countdown', React.createElement('span', { title: countdownTitle },
1448
- metric('距' + (peakNow ? '空闲' : '高峰'), fmtCountdown(pr.nextSwitch.at - now)))));
1488
+ metric(t('ui.until') + (peakNow ? t('ui.offPeak') : t('ui.peak')), fmtCountdown(pr.nextSwitch.at - now)))));
1449
1489
  }
1450
1490
 
1451
1491
  // 本会话花费(公共小部件 pushSessionCost:只显示钱;hover 显示 今天/近一月/全部)
@@ -1468,17 +1508,17 @@ module.exports = {
1468
1508
  const symbol = usdSymbol ? '$' : '¥';
1469
1509
  const costTxt = costCNY != null ? '¥' + costCNY.toFixed(3)
1470
1510
  : (costUSD != null ? '$' + costUSD.toFixed(3) : symbol + (0).toFixed(3));
1471
- const today = usg.todaySpend != null ? '今天 ' + symbol + fmt(usg.todaySpend, 3) : '';
1472
- const month = usg.monthSpend != null ? '近一月 ' + symbol + fmt(usg.monthSpend, 3) : '';
1473
- const total = usg.totalSpend != null ? '全部 ' + symbol + fmt(usg.totalSpend, 3) : '';
1511
+ const today = usg.todaySpend != null ? t('ui.today', { symbol: symbol, value: fmt(usg.todaySpend, 3) }) : '';
1512
+ const month = usg.monthSpend != null ? t('ui.lastDays', { symbol: symbol, value: fmt(usg.monthSpend, 3) }) : '';
1513
+ const total = usg.totalSpend != null ? t('ui.allTime', { symbol: symbol, value: fmt(usg.totalSpend, 3) }) : '';
1474
1514
  const detail = [today, month, total].filter(function (s) { return s.length > 0; }).join(' · ');
1475
1515
  groups.push(fieldSpan('sessionCost', 'convo', React.createElement('span', {
1476
- title: '本会话 ' + costTxt + '(含子代理)' + (detail ? '\n' + detail : '') },
1477
- metric('本会话', costTxt))));
1516
+ title: t('ui.sessionIncludingSubagents', { costTxt: costTxt, value: detail ? '\n' + detail : '' }) },
1517
+ metric(t('ui.session'), costTxt))));
1478
1518
  }
1479
1519
  } else if (errs.usage && fieldVisible('usageError')) {
1480
1520
  trailingErrorGroups.push(fieldSpan('usageError', 'usageerr',
1481
- React.createElement('span', { className: 'bi-err', title: '花费暂不可用;不会影响对话。' }, '花费获取失败')));
1521
+ React.createElement('span', { className: 'bi-err', title: t('ui.spendIsTemporarilyUnavailableChat') }, t('ui.spendUnavailable'))));
1482
1522
  }
1483
1523
  }
1484
1524
 
@@ -1488,16 +1528,16 @@ module.exports = {
1488
1528
  function subscriptionFailureHint(error, source) {
1489
1529
  const kind = error && error.kind;
1490
1530
  const serviceName = subscriptionServiceName(source);
1491
- const message = error && typeof error.message === 'string' ? error.message : '';
1531
+ const message = error && typeof error.message === 'string' ? hostText(error.message) : '';
1492
1532
  const statusMatch = message.match(/HTTP (\d{3})/);
1493
1533
  const status = statusMatch ? statusMatch[1] : '';
1494
- if (kind === 'no-key') return '未找到 ' + serviceName + ' 登录凭证。请重新授权。';
1495
- if (kind === 'auth' || status === '401') return serviceName + ' 登录凭证已失效。请重新授权。';
1496
- if (status === '403') return serviceName + ' 拒绝访问。请重新授权或稍后再试。';
1497
- if (status === '429') return serviceName + ' 请求过于频繁。请稍后再试。';
1498
- if (kind === 'timeout' || /timeout|timed out|abort/i.test(message)) return serviceName + ' 响应超时。请检查网络后再试。';
1499
- if (kind === 'parse') return serviceName + ' 返回的数据暂时无法识别。请稍后再试。';
1500
- return serviceName + ' 暂不可用。请检查网络后再试。';
1534
+ if (kind === 'no-key') return t('ui.noSignInCredentialsFound', { serviceName: serviceName });
1535
+ if (kind === 'auth' || status === '401') return t('ui.credentialsHaveExpiredPleaseReauthorize', { serviceName: serviceName });
1536
+ if (status === '403') return t('ui.deniedAccessReauthorizeOrTry', { serviceName: serviceName });
1537
+ if (status === '429') return t('ui.rateLimitReachedPleaseTry', { serviceName: serviceName });
1538
+ if (kind === 'timeout' || /timeout|timed out|abort/i.test(message)) return t('ui.timedOutCheckYourConnection', { serviceName: serviceName });
1539
+ if (kind === 'parse') return t('ui.returnedAnUnrecognizedResponsePlease', { serviceName: serviceName });
1540
+ return t('ui.isTemporarilyUnavailableCheckYour', { serviceName: serviceName });
1501
1541
  }
1502
1542
 
1503
1543
  function pushSubscriptionGroups(groups, trailingErrorGroups) {
@@ -1509,14 +1549,14 @@ module.exports = {
1509
1549
  const errors = state.errors || {};
1510
1550
  // v1.7 FR-8:JWT 订阅卡——真实套餐到期日(纯本地解码;无登录态/解析失败不显示此处)
1511
1551
  if (sub && sub.planType && sub.expiryAt && fieldVisible('expiry')) {
1512
- groups.push(fieldSpan('expiry', 'subexp', React.createElement('span', { title: '订阅到期:' + formatDate(sub.expiryAt) + '(本地时区)' },
1513
- metric('到期', formatDate(sub.expiryAt)))));
1552
+ groups.push(fieldSpan('expiry', 'subexp', React.createElement('span', { title: t('ui.subscriptionExpiresLocalTime', { value: formatDate(sub.expiryAt) }) },
1553
+ metric(t('ui.expires'), formatDate(sub.expiryAt)))));
1514
1554
  }
1515
1555
  if (!sub) {
1516
1556
  if (errors.sub && fieldVisible('refreshFailure')) {
1517
1557
  // 本次 RPC 失败且无旧数据:显示失败信息而非永久"加载中…"
1518
1558
  trailingErrorGroups.push(fieldSpan('refreshFailure', 'suberr',
1519
- React.createElement('span', { className: 'bi-stale', title: subscriptionFailureHint({ kind: 'exception', message: String(errors.sub) }, visibleBillingMode && visibleBillingMode.provider) }, '刷新失败')));
1559
+ React.createElement('span', { className: 'bi-stale', title: subscriptionFailureHint({ kind: 'exception', message: String(errors.sub) }, visibleBillingMode && visibleBillingMode.provider) }, t('ui.refreshFailed'))));
1520
1560
  }
1521
1561
  return;
1522
1562
  }
@@ -1532,7 +1572,7 @@ module.exports = {
1532
1572
  if (sub.error && !hasData) {
1533
1573
  if (fieldVisible('refreshFailure')) {
1534
1574
  trailingErrorGroups.push(fieldSpan('refreshFailure', 'substale',
1535
- React.createElement('span', { className: 'bi-stale', title: subscriptionFailureHint(sub.error, sub.source || (visibleBillingMode && visibleBillingMode.provider)) }, '刷新失败')));
1575
+ React.createElement('span', { className: 'bi-stale', title: subscriptionFailureHint(sub.error, sub.source || (visibleBillingMode && visibleBillingMode.provider)) }, t('ui.refreshFailed'))));
1536
1576
  }
1537
1577
  return;
1538
1578
  }
@@ -1541,17 +1581,17 @@ module.exports = {
1541
1581
  if (!hasData && typeof sub.balance === 'number' && isFinite(sub.balance)) {
1542
1582
  if (fieldVisible('subBalance')) {
1543
1583
  const balTxt = '¥' + fmt(sub.balance, 2);
1544
- const titleLines = ['订阅源:' + subscriptionServiceName(visibleBillingMode && visibleBillingMode.provider) + '(' + (sub.plan || '充值余额') + ')',
1545
- '可用余额:' + balTxt];
1584
+ const titleLines = [t('ui.subscriptionSource', { value: subscriptionServiceName(visibleBillingMode && visibleBillingMode.provider) }) + (hostText(sub.plan) || t('ui.prepaidBalance')) + ')',
1585
+ t('ui.availableBalance', { balTxt: balTxt })];
1546
1586
  groups.push(fieldSpan('subBalance', 'subbal', React.createElement('span', { title: titleLines.join('\n') },
1547
- metric('余额', balTxt))));
1587
+ metric(t('ui.balance.pushBalanceGroups'), balTxt))));
1548
1588
  }
1549
1589
  // 充值余额用户按量付费,花销与余额同等重要 → 追加公共花费块(含子代理聚合)
1550
1590
  pushSessionCost(groups, trailingErrorGroups, false);
1551
1591
  // host 快照失败(sub.error)或本次 RPC 失败(errors.sub)→ 保留旧数据 + 降级标记
1552
1592
  if (fieldVisible('refreshFailure') && (sub.error || errors.sub)) {
1553
1593
  trailingErrorGroups.push(fieldSpan('refreshFailure', 'substale',
1554
- React.createElement('span', { className: 'bi-stale', title: subscriptionFailureHint(sub.error || { kind: 'exception', message: String(errors.sub || '') }, sub.source || (visibleBillingMode && visibleBillingMode.provider)) }, '刷新失败')));
1594
+ React.createElement('span', { className: 'bi-stale', title: subscriptionFailureHint(sub.error || { kind: 'exception', message: String(errors.sub || '') }, sub.source || (visibleBillingMode && visibleBillingMode.provider)) }, t('ui.refreshFailed'))));
1555
1595
  }
1556
1596
  return;
1557
1597
  }
@@ -1574,10 +1614,10 @@ module.exports = {
1574
1614
 
1575
1615
  // 预警触发条件:已用 ≥80%(= 剩余 ≤20%)→ 鲜红色文字;正常额度使用中性文字。
1576
1616
  const LOW_QUOTA_PERCENT = 20;
1577
- const titleLines = ['订阅源:' + subscriptionServiceName(visibleBillingMode && visibleBillingMode.provider) + (sub.plan ? '(' + sub.plan + ')' : '')]
1617
+ const titleLines = [t('ui.subscriptionSource.titleLines', { value: subscriptionServiceName(visibleBillingMode && visibleBillingMode.provider) }) + (sub.plan ? ' (' + hostText(sub.plan) + ')' : '')]
1578
1618
  .concat(windows.map(function (w) {
1579
- return w.label + '窗口:剩余 ' + remainingPercent(w) + '%(已用 ' + w.usedPercent + '%)'
1580
- + (w.resetsAt ? ' · 重置 ' + formatDateTime(w.resetsAt) + ' · 距重置 ' + fmtResetCountdown(w.resetsAt - now) : '');
1619
+ return t('ui.windowRemainingUsed', { label: quotaWindowLabel(w), value: remainingPercent(w), usedPercent: w.usedPercent })
1620
+ + (w.resetsAt ? t('ui.resetsResetsIn', { value: formatDateTime(w.resetsAt), value2: fmtResetCountdown(w.resetsAt - now) }) : '');
1581
1621
  }));
1582
1622
  const winNodes = [];
1583
1623
  for (let i = 0; i < visible.length; i++) {
@@ -1588,7 +1628,7 @@ module.exports = {
1588
1628
  // 每个窗口独立 data-field(subWindow5h/Week/Month),色变量按字段注入;「低」字标签保留
1589
1629
  winNodes.push(fieldSpan(WINDOW_FIELD_IDS[w.key] || 'subWindow5h', 'w' + i,
1590
1630
  metric(compactWindowLabel(w.key), remaining + '%', numberClass)));
1591
- if (remaining <= LOW_QUOTA_PERCENT) winNodes.push(React.createElement('span', { key: 'low' + i, className: 'bi-low-status' }, '低'));
1631
+ if (remaining <= LOW_QUOTA_PERCENT) winNodes.push(React.createElement('span', { key: 'low' + i, className: 'bi-low-status' }, t('ui.low')));
1592
1632
  }
1593
1633
  // 全部窗口被隐藏(或紧凑模式无候选)→ 整组不推送,分隔符由组装层正确收合
1594
1634
  if (winNodes.length > 0) {
@@ -1597,14 +1637,13 @@ module.exports = {
1597
1637
  // host 快照失败(sub.error)或本次 RPC 失败(errors.sub)→ 均保留旧数据 + 降级标记
1598
1638
  if (fieldVisible('refreshFailure') && (sub.error || errors.sub)) {
1599
1639
  trailingErrorGroups.push(fieldSpan('refreshFailure', 'substale',
1600
- React.createElement('span', { className: 'bi-stale', title: subscriptionFailureHint(sub.error || { kind: 'exception', message: String(errors.sub || '') }, sub.source || (visibleBillingMode && visibleBillingMode.provider)) }, '刷新失败')));
1640
+ React.createElement('span', { className: 'bi-stale', title: subscriptionFailureHint(sub.error || { kind: 'exception', message: String(errors.sub || '') }, sub.source || (visibleBillingMode && visibleBillingMode.provider)) }, t('ui.refreshFailed'))));
1601
1641
  }
1602
1642
  // 距重置倒计时(与显示的窗口一致,确保额度与倒计时匹配)
1603
1643
  if (displayWindow && displayWindow.resetsAt && fieldVisible('resetCountdown')) {
1604
- const cdTitle = displayWindow.label + '窗口 剩余 ' + remainingPercent(displayWindow)
1605
- + '%(已用 ' + displayWindow.usedPercent + '%) · 重置 ' + formatDateTime(displayWindow.resetsAt);
1644
+ const cdTitle = t('ui.windowRemainingUsedResets', { label: quotaWindowLabel(displayWindow), value: remainingPercent(displayWindow), usedPercent: displayWindow.usedPercent, value4: formatDateTime(displayWindow.resetsAt) });
1606
1645
  groups.push(fieldSpan('resetCountdown', 'subcd', React.createElement('span', { title: cdTitle },
1607
- metric('距重置', fmtResetCountdown(displayWindow.resetsAt - now)))));
1646
+ metric(t('ui.resetsIn'), fmtResetCountdown(displayWindow.resetsAt - now)))));
1608
1647
  }
1609
1648
  }
1610
1649
  }
@@ -1613,14 +1652,14 @@ module.exports = {
1613
1652
  // 账单服务+模型 → 本月真实花费 →(预算%)→(免费额度+重置倒计时);余额/额度/本会话花费均不显示 ----
1614
1653
  function billingFailureHint(error, provider) {
1615
1654
  const serviceName = billingServiceName(provider);
1616
- const message = error && typeof error.message === 'string' ? error.message : '';
1655
+ const message = error && typeof error.message === 'string' ? hostText(error.message) : '';
1617
1656
  const statusMatch = message.match(/HTTP (\d{3})/);
1618
1657
  const status = statusMatch ? statusMatch[1] : '';
1619
- if (error && error.kind === 'no-key') return '未配置 ' + message.replace('未配置 ', '') + '。请在"设置 → 模型"中填写。';
1620
- if (status === '403' || /缺少 .*权限/.test(message)) return serviceName + ' 拒绝访问:Token 可能缺少账单读取权限。';
1621
- if (error && error.kind === 'parse') return serviceName + ' 返回的数据暂时无法识别。请稍后再试。';
1622
- if (/timeout|timed out|abort/i.test(message)) return serviceName + ' 响应超时。请检查网络后再试。';
1623
- return serviceName + ' 账单暂不可用。请检查网络与权限后再试。';
1658
+ if (error && error.kind === 'no-key') return t('ui.notConfigured') + message.replace(/(?:未配置 |Not configured: )/, '') + t('ui.configureItInSettingsModels');
1659
+ if (status === '403' || /缺少 .*权限|lack .*permission/.test(message)) return t('ui.deniedAccessTheTokenMay', { serviceName: serviceName });
1660
+ if (error && error.kind === 'parse') return t('ui.returnedAnUnrecognizedResponsePlease', { serviceName: serviceName });
1661
+ if (/timeout|timed out|abort/i.test(message)) return t('ui.timedOutCheckYourConnection', { serviceName: serviceName });
1662
+ return t('ui.billingIsTemporarilyUnavailableCheck', { serviceName: serviceName });
1624
1663
  }
1625
1664
 
1626
1665
  function pushBillingGroups(groups, trailingErrorGroups) {
@@ -1633,7 +1672,7 @@ module.exports = {
1633
1672
  if (!bill) {
1634
1673
  if (errors.billing && fieldVisible('refreshFailure')) {
1635
1674
  trailingErrorGroups.push(fieldSpan('refreshFailure', 'billerr',
1636
- React.createElement('span', { className: 'bi-stale', title: billingFailureHint({ kind: 'exception', message: String(errors.billing) }, visibleBillingMode && visibleBillingMode.provider) }, '刷新失败')));
1675
+ React.createElement('span', { className: 'bi-stale', title: billingFailureHint({ kind: 'exception', message: String(errors.billing) }, visibleBillingMode && visibleBillingMode.provider) }, t('ui.refreshFailed'))));
1637
1676
  }
1638
1677
  return;
1639
1678
  }
@@ -1642,39 +1681,39 @@ module.exports = {
1642
1681
  if (bill.error && !hasSpend) {
1643
1682
  if (fieldVisible('refreshFailure')) {
1644
1683
  trailingErrorGroups.push(fieldSpan('refreshFailure', 'billstale',
1645
- React.createElement('span', { className: 'bi-stale', title: billingFailureHint(bill.error, bill.type || (visibleBillingMode && visibleBillingMode.provider)) }, '刷新失败')));
1684
+ React.createElement('span', { className: 'bi-stale', title: billingFailureHint(bill.error, bill.type || (visibleBillingMode && visibleBillingMode.provider)) }, t('ui.refreshFailed'))));
1646
1685
  }
1647
1686
  return;
1648
1687
  }
1649
1688
  if (hasSpend) {
1650
1689
  const symbol = d.currency === 'CNY' ? '¥' : '$';
1651
- const titleLines = ['账单源:' + billingServiceName(visibleBillingMode && visibleBillingMode.provider)]
1652
- .concat(d.note ? [d.note] : [])
1653
- .concat(d.currentPeriodSpend != null ? ['本月花费:' + symbol + fmt(d.currentPeriodSpend, 2)] : [])
1654
- .concat(d.budgetPercent != null ? ['预算使用:' + fmt(d.budgetPercent, 0) + '%'] : [])
1655
- .concat(d.freeRemaining != null ? ['每日免费额度剩余:' + fmt(d.freeRemaining, 0)] : []);
1690
+ const titleLines = [t('ui.billingSource', { value: billingServiceName(visibleBillingMode && visibleBillingMode.provider) })]
1691
+ .concat(d.note ? [hostText(d.note)] : [])
1692
+ .concat(d.currentPeriodSpend != null ? [t('ui.thisMonthSSpend', { symbol: symbol, value: fmt(d.currentPeriodSpend, 2) })] : [])
1693
+ .concat(d.budgetPercent != null ? [t('ui.budgetUsed', { value: fmt(d.budgetPercent, 0) })] : [])
1694
+ .concat(d.freeRemaining != null ? [t('ui.dailyFreeQuotaRemaining', { value: fmt(d.freeRemaining, 0) })] : []);
1656
1695
  // v1.9.0 PR2:本月/预算/免费额度三片段各自显隐,分隔符只在可见片段之间
1657
1696
  const nodes = [];
1658
1697
  if (d.currentPeriodSpend != null && fieldVisible('billingSpend')) {
1659
- nodes.push(fieldSpan('billingSpend', 'billspend', metric('本月', symbol + fmt(d.currentPeriodSpend, 2))));
1698
+ nodes.push(fieldSpan('billingSpend', 'billspend', metric(t('ui.thisMonth'), symbol + fmt(d.currentPeriodSpend, 2))));
1660
1699
  } else if (d.usage != null && fieldVisible('billingSpend')) {
1661
- nodes.push(fieldSpan('billingSpend', 'billspend', metric('本月用量', fmt(d.usage, 2) + (d.usageUnit ? ' ' + d.usageUnit : ''))));
1700
+ nodes.push(fieldSpan('billingSpend', 'billspend', metric(t('ui.thisMonthSUsage'), fmt(d.usage, 2) + (d.usageUnit ? ' ' + d.usageUnit : ''))));
1662
1701
  }
1663
1702
  if (d.budgetPercent != null && fieldVisible('budget')) {
1664
1703
  if (nodes.length > 0) nodes.push(' · ');
1665
- nodes.push(fieldSpan('budget', 'billbudget', metric('预算', fmt(d.budgetPercent, 0) + '%')));
1704
+ nodes.push(fieldSpan('budget', 'billbudget', metric(t('ui.budget'), fmt(d.budgetPercent, 0) + '%')));
1666
1705
  }
1667
1706
  // 免费额度仅当接口显式给出(freeRemaining/resetsAt 同时存在)才显示,绝不编造
1668
1707
  if (d.freeRemaining != null && d.resetsAt && fieldVisible('freeQuota')) {
1669
1708
  if (nodes.length > 0) nodes.push(' · ');
1670
- nodes.push(fieldSpan('freeQuota', 'billfree', metric('免费', fmt(d.freeRemaining, 0) + ' · 距重置 ' + fmtResetCountdown(d.resetsAt - now))));
1709
+ nodes.push(fieldSpan('freeQuota', 'billfree', metric(t('ui.free'), t('ui.resetsIn.pushBillingGroups', { value: fmt(d.freeRemaining, 0), value2: fmtResetCountdown(d.resetsAt - now) }))));
1671
1710
  }
1672
1711
  if (nodes.length > 0) {
1673
1712
  groups.push(React.createElement('span', { key: 'bill', title: titleLines.join('\n') }, ...nodes));
1674
1713
  }
1675
1714
  if (fieldVisible('refreshFailure') && (bill.error || errors.billing)) {
1676
1715
  trailingErrorGroups.push(fieldSpan('refreshFailure', 'billstale',
1677
- React.createElement('span', { className: 'bi-stale', title: billingFailureHint(bill.error || { kind: 'exception', message: String(errors.billing || '') }, bill.type || (visibleBillingMode && visibleBillingMode.provider)) }, '刷新失败')));
1716
+ React.createElement('span', { className: 'bi-stale', title: billingFailureHint(bill.error || { kind: 'exception', message: String(errors.billing || '') }, bill.type || (visibleBillingMode && visibleBillingMode.provider)) }, t('ui.refreshFailed'))));
1678
1717
  }
1679
1718
  }
1680
1719
  }
@@ -1684,11 +1723,11 @@ module.exports = {
1684
1723
  const pr = visiblePricing;
1685
1724
  const serviceName = billingServiceName(visibleBillingMode && visibleBillingMode.provider);
1686
1725
  const modelLabel = (pr && pr.modelDisplay) ? pr.modelDisplay
1687
- : (pr && pr.model ? pr.model : '未知模型');
1726
+ : (pr && pr.model ? pr.model : t('ui.unknownModel'));
1688
1727
  const modelName = modelLabelWithoutProvider(modelLabel, serviceName);
1689
1728
  const versionLine = updateInfo && typeof updateInfo.current === 'string'
1690
- ? '\n插件版本:' + updateInfo.current : '';
1691
- const title = '账单服务:' + serviceName + '\n模型:' + modelLabel + versionLine;
1729
+ ? t('ui.pluginVersion', { current: updateInfo.current }) : '';
1730
+ const title = t('ui.billingServiceModel', { serviceName: serviceName, modelLabel: modelLabel, versionLine: versionLine });
1692
1731
  return React.createElement('span', { key: 'billprov', className: 'bi-model-group', title: title },
1693
1732
  React.createElement('b', { className: 'bi-model-provider' }, serviceName),
1694
1733
  modelDetail(pr, modelName),
@@ -1719,16 +1758,16 @@ module.exports = {
1719
1758
  // 全局降级提示:任一端点失败 → 旧数据照常渲染 + 角落提示(title 列出失败项),仅失败项降级
1720
1759
  const errors = state.errors || {};
1721
1760
  const failedLabels = [];
1722
- if (errors.balance) failedLabels.push('余额');
1723
- if (errors.pricing) failedLabels.push('定价');
1724
- if (errors.usage) failedLabels.push('花费');
1725
- if (errors.billingMode) failedLabels.push('模式');
1726
- if (errors.sub) failedLabels.push('订阅额度');
1727
- if (errors.billing) failedLabels.push('账单');
1761
+ if (errors.balance) failedLabels.push(t('ui.balance.pushBalanceGroups'));
1762
+ if (errors.pricing) failedLabels.push(t('ui.pricing'));
1763
+ if (errors.usage) failedLabels.push(t('ui.spend'));
1764
+ if (errors.billingMode) failedLabels.push(t('ui.mode'));
1765
+ if (errors.sub) failedLabels.push(t('ui.subscriptionQuota'));
1766
+ if (errors.billing) failedLabels.push(t('ui.billing'));
1728
1767
  if (failedLabels.length > 0 && fieldVisible('refreshFailure')) {
1729
1768
  trailingErrorGroups.push(fieldSpan('refreshFailure', 'degraded',
1730
1769
  React.createElement('span', { className: 'bi-stale', key: 'degraded',
1731
- title: failedLabels.join('、') + '暂不可用;正在保留上次数据并自动重试。' }, '刷新失败')));
1770
+ title: failedLabels.join(t('ui.listSeparator')) + t('ui.temporarilyUnavailableKeepingTheLast') }, t('ui.refreshFailed'))));
1732
1771
  }
1733
1772
  const persistence = state.usage && state.usage.persistence;
1734
1773
  if (persistence && persistence.state && persistence.state !== 'ok' && fieldVisible('persistWarning')) {
@@ -1736,15 +1775,15 @@ module.exports = {
1736
1775
  trailingErrorGroups.push(fieldSpan('persistWarning', 'ledger-save', React.createElement('span', {
1737
1776
  className: snapshotOnly ? 'bi-stale' : 'bi-err',
1738
1777
  title: snapshotOnly
1739
- ? '账单流水已保存,但可直接查看的账单文件暂未更新:' + (persistence.message || '未知原因')
1740
- : '本次账单未保存,不会计入金额:' + (persistence.message || '未知原因'),
1741
- }, snapshotOnly ? '账单待整理' : '账单未保存')));
1778
+ ? t('ui.spendJournalSavedButThe') + (hostText(persistence.message) || t('ui.unknownReason'))
1779
+ : t('ui.thisSpendRecordWasNot') + (hostText(persistence.message) || t('ui.unknownReason')),
1780
+ }, snapshotOnly ? t('ui.ledgerUpdatePending') : t('ui.spendNotSaved'))));
1742
1781
  }
1743
1782
 
1744
1783
  if (updateInfo && updateInfo.available === true && fieldVisible('updateNotice')) {
1745
1784
  groups.push(fieldSpan('updateNotice', 'update', React.createElement('span', {
1746
- className: 'bi-update', title: '请告知你的 Agent 将本插件更新到“' + updateInfo.latest + '”版本。',
1747
- }, '新版本提醒')));
1785
+ className: 'bi-update', title: t('ui.askYourAgentToUpdate', { latest: updateInfo.latest }),
1786
+ }, t('ui.updateAvailable'))));
1748
1787
  }
1749
1788
 
1750
1789
  // ---- 组装(分隔符收合与「刷新失败」去重见模块级 assembleInfoBarRow) ----
@@ -1777,28 +1816,28 @@ module.exports = {
1777
1816
  // v1.9.0 PR2:原生统计行字段按设置过滤(隐藏组完全不进 ng,不占版式也不进 title)
1778
1817
  if (fieldVisible('turnsSteps')) {
1779
1818
  // 中文界面遵循数字与汉字混排留白:数值与量词视觉上分开,便于快速扫读。
1780
- group([num(statsProj.turns + ' 轮'), ' · ', num(statsProj.steps + ' 步')], false, 'turnsSteps');
1819
+ group([num(t(statsProj.turns === 1 ? 'ui.turnCount' : 'ui.turnCountPlural', { count: statsProj.turns })), ' · ', num(t(statsProj.steps === 1 ? 'ui.stepCount' : 'ui.stepCountPlural', { count: statsProj.steps }))], false, 'turnsSteps');
1781
1820
  }
1782
1821
 
1783
1822
  const durations = [];
1784
1823
  if (statsProj.llmMs > 0 && fieldVisible('llmTime')) durations.push(fieldSpan('llmTime', 'durl', metric('LLM', formatDuration(statsProj.llmMs))));
1785
1824
  if (statsProj.toolMs > 0 && fieldVisible('toolTime')) {
1786
1825
  if (durations.length > 0) durations.push(' · ');
1787
- durations.push(fieldSpan('toolTime', 'durt', metric('工具调用', formatDuration(statsProj.toolMs))));
1826
+ durations.push(fieldSpan('toolTime', 'durt', metric(t('ui.tools'), formatDuration(statsProj.toolMs))));
1788
1827
  }
1789
1828
  if (durations.length > 0) group(durations);
1790
1829
 
1791
1830
  const speeds = [];
1792
- if (statsProj.ttftSteps > 0) speeds.push(metric('首 token 平均', formatDuration(statsProj.ttftMs / statsProj.ttftSteps)));
1831
+ if (statsProj.ttftSteps > 0) speeds.push(metric(t('ui.avgTTFT'), formatDuration(statsProj.ttftMs / statsProj.ttftSteps)));
1793
1832
  if (statsProj.decodeMs > 0) speeds.push(' · ', num(formatTps(statsProj.decodeTokens / (statsProj.decodeMs / 1e3)) + ' tok/s'));
1794
1833
  if (speeds.length > 0) group(speeds, HIDE_SPEED_FIELDS); // 不占可见版式,title 浮窗保留(官方隐藏字段,非用户可配)
1795
1834
 
1796
1835
  if (usageProj && (billedInput(usageProj) > 0 || (usageProj.outputTokens || 0) > 0)) {
1797
1836
  const denom = billedInput(usageProj);
1798
1837
  const hit = denom > 0 ? Math.round(((usageProj.cacheReadTokens || 0) / denom) * 100) : null;
1799
- if (hit != null && fieldVisible('cacheHit')) group([metric('缓存命中', hit + '%')], false, 'cacheHit');
1838
+ if (hit != null && fieldVisible('cacheHit')) group([metric(t('ui.cacheHit'), hit + '%')], false, 'cacheHit');
1800
1839
  if (fieldVisible('tokensIO')) {
1801
- group([metric('输入', formatTokens(billedInput(usageProj)) + ' tok'), ' · ', metric('输出', formatTokens(usageProj.outputTokens || 0) + ' tok')], false, 'tokensIO');
1840
+ group([metric(t('ui.input.BottomInfoBar'), formatTokens(billedInput(usageProj)) + ' tok'), ' · ', metric(t('ui.output'), formatTokens(usageProj.outputTokens || 0) + ' tok')], false, 'tokensIO');
1802
1841
  }
1803
1842
  }
1804
1843
 
@@ -1848,10 +1887,10 @@ module.exports = {
1848
1887
  'aria-disabled': isDensitySaving,
1849
1888
  'data-density': displayDensity,
1850
1889
  'data-density-saving': isDensitySaving,
1851
- title: isDensitySaving ? '正在保存切换…' : '单击切换 完整/简洁',
1890
+ title: isDensitySaving ? t('ui.savingView') : t('ui.clickToSwitchFullCompact'),
1852
1891
  }, animatedRow1, row2,
1853
1892
  React.createElement('span', { id: 'dsh-bottom-info-bar-action', className: 'bi-sr-only' },
1854
- full ? '按 Enter 或空格切换为简洁模式。' : '按 Enter 或空格切换为完整模式。'));
1893
+ full ? t('ui.pressEnterOrSpaceFor') : t('ui.pressEnterOrSpaceFor.BottomInfoBar')));
1855
1894
  }
1856
1895
  },
1857
1896
  };