dsh-consumption-badge 1.3.2 → 1.3.3

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/README.md CHANGED
@@ -249,6 +249,8 @@ no second settings page.
249
249
  | Slot labels | The action-row label and the settings tab title are **thunks**, resolved by the slot system at read time, so the tab renames itself on a switch |
250
250
  | Late service | If the `locale` service is provided after this plugin's `apply()` ran (boot order differs per composition), the first render retries the bind |
251
251
  | Missing service | Without the locale service everything falls back to Chinese — the plugin still works standalone |
252
+ | Both dictionaries | The plugin registers **both** `zh` (identity: key→key) and `en`, so the Chinese result never depends on how a particular host build resolves a missing locale (some use `en` as the fallback locale, which would otherwise leak English into a Chinese UI) |
253
+ | Language override | Settings → Usage → **Language**: `Follow shell` / `Chinese` / `English`. It overrides the shell for this plugin only, persists in `localStorage: dsh-consumption:language`, and is the escape hatch when a shell reports an unexpected locale |
252
254
  | Host errors | `lib/index.js` never returns localized prose: failures carry a language-neutral `code` + `detail` (and an English `error` for logs). The client maps `code` → copy in the active language, and prints the host's `error` only for an unknown code |
253
255
 
254
256
  Adding a language means adding one dictionary next to `DICT_EN`; the lookup order is
@@ -421,7 +423,7 @@ plugin by "running" it — a session-scoped dynamic plugin always dies with the
421
423
  - Do not hard-code user-visible English or Chinese in new JSX: call `tt('…')` with the Chinese copy
422
424
  as the key and add the English entry to `DICT_EN`. `check.mjs` fails if any `tt()` key has no
423
425
  English translation or if a translation still contains Chinese.
424
- - Self-check without booting DSH: `node check.mjs` (320 assertions over the route contract, the
426
+ - Self-check without booting DSH: `node check.mjs` (334 assertions over the route contract, the
425
427
  client bundle contract, the pricing/aggregation maths and the bilingual copy). It lives outside
426
428
  the published `files` list.
427
429
 
package/lib/client.js CHANGED
@@ -434,6 +434,8 @@ window.__ModuleLoader__.load({
434
434
  '按模型筛选;同时作为计价兜底': 'Filter by model; also the pricing fallback',
435
435
  '仅 {model}': 'only {model}',
436
436
  '日志 id {id}': 'log id {id}',
437
+ '语言': 'Language',
438
+ '跟随外壳': 'Follow shell',
437
439
  '{count} 个模型各自计价': '{count} models priced separately',
438
440
  '按 {model} 约还可跑 {tokens} tokens': 'about {tokens} tokens left at {model} prices',
439
441
  '金额按各模型官方价估算(DeepSeek 区分峰谷 ×1/×2,其它厂商按 USD 价 ×7.1 折人民币),仅供参考;数据源为本会话日志,刷新后自动恢复。': 'Costs are estimated from each model’s list price (DeepSeek applies peak/off-peak ×1/×2; other vendors are converted from USD at 7.1), for reference only; the source is this session log, restored automatically after a refresh.',
@@ -577,6 +579,30 @@ window.__ModuleLoader__.load({
577
579
  let boundTranslate = null
578
580
  let localeService = null
579
581
  let appCtx = null
582
+ // 插件自带的语言覆盖:'' = 跟随外壳,'zh' / 'en' = 用户显式指定。
583
+ // 存在的理由:外壳的 locale 服务在不同版本里回退策略不同(本版 FALLBACK_LOCALE=zh,
584
+ // 换成 en 的实现就会让中文界面吃 en 词典而显示英文),npm 包又装在别人的环境里,
585
+ // 所以这里既**显式注册 zh 词典**,也允许用户自己钉死语言。
586
+ const LS_LANGUAGE = 'dsh-consumption:language'
587
+ let languageOverride = readLanguage()
588
+
589
+ function readLanguage() {
590
+ const raw = lsGet(LS_LANGUAGE)
591
+ return raw === 'zh' || raw === 'en' ? raw : ''
592
+ }
593
+ function setLanguageOverride(next) {
594
+ const value = next === 'zh' || next === 'en' ? next : ''
595
+ languageOverride = value
596
+ lsSet(LS_LANGUAGE, value)
597
+ }
598
+ /** 语言下拉的选项(跟随外壳 / 中文 / English)。 */
599
+ function languageOptions() {
600
+ return [
601
+ { id: '', label: tt('跟随外壳') },
602
+ { id: 'zh', label: '中文' },
603
+ { id: 'en', label: 'English' },
604
+ ]
605
+ }
580
606
 
581
607
  /** Fill `{name}` placeholders (same template syntax as the locale service). */
582
608
  function applyParams(template, params) {
@@ -615,14 +641,20 @@ window.__ModuleLoader__.load({
615
641
  if (service === null || typeof service.register !== 'function' || typeof service.bind !== 'function') return
616
642
  // Only the `en` side is registered: the key already is the `zh` copy, and
617
643
  // the locale service resolves a miss to the key itself.
644
+ // 中英两侧都显式注册:zh 侧是恒等映射(key 就是中文原文),
645
+ // 这样中文界面的结果不再取决于宿主 locale 实现的回退策略(有的版本回退到 en)。
646
+ const dictZh = {}
647
+ for (const key in DICT_EN) {
648
+ if (Object.prototype.hasOwnProperty.call(DICT_EN, key)) dictZh[key] = key
649
+ }
618
650
  let ns = LOCALE_NS
619
651
  let dispose = null
620
652
  try {
621
- dispose = service.register(ns, 'en', DICT_EN)
653
+ dispose = service.register(ns, { zh: dictZh, en: DICT_EN })
622
654
  } catch (e) {
623
655
  ns = LOCALE_NS + '#' + BUILD_TAG + '-' + Math.random().toString(36).slice(2, 7)
624
656
  try {
625
- dispose = service.register(ns, 'en', DICT_EN)
657
+ dispose = service.register(ns, { zh: dictZh, en: DICT_EN })
626
658
  console.warn('[consumption-badge] previous dictionary still registered; bound a private locale namespace ' + ns)
627
659
  } catch (e2) {
628
660
  console.warn('[consumption-badge] locale dictionary registration failed: ' + msgOf(e2))
@@ -648,8 +680,16 @@ window.__ModuleLoader__.load({
648
680
  attachLocale(appCtx)
649
681
  }
650
682
 
651
- /** Translate a key with the active locale; falls back to the key itself. */
683
+ /**
684
+ * 取当前语言的文案。优先级:用户显式选择 → 外壳 locale 服务 → 原文(中文)。
685
+ * 显式选中文时直接返回 key(中文就是 key 本身),完全不经过宿主,避免任何回退策略问题。
686
+ */
652
687
  function tt(key, params) {
688
+ if (languageOverride === 'zh') return applyParams(key, params)
689
+ if (languageOverride === 'en') {
690
+ const hit = Object.prototype.hasOwnProperty.call(DICT_EN, key) ? DICT_EN[key] : key
691
+ return applyParams(hit, params)
692
+ }
653
693
  if (localeService === null) ensureLocaleBound()
654
694
  if (boundTranslate !== null) {
655
695
  try {
@@ -671,8 +711,9 @@ window.__ModuleLoader__.load({
671
711
  }
672
712
  }
673
713
 
674
- /** Active locale id ('zh' when the service is absent). */
714
+ /** Active locale id: 覆盖优先,其次外壳,最后按中文。 */
675
715
  function activeLocaleId() {
716
+ if (languageOverride !== '') return languageOverride
676
717
  try {
677
718
  const snapshot = localeService !== null && typeof localeService.getLocale === 'function' ? localeService.getLocale() : null
678
719
  if (snapshot && typeof snapshot.active === 'string' && snapshot.active) return snapshot.active
@@ -799,10 +840,10 @@ window.__ModuleLoader__.load({
799
840
 
800
841
  /* ================= 消耗弹窗背景样式(与个性化插件联动) ================= */
801
842
  const MODAL_STYLE_KEY = 'dsh-consumption:modal-style'
802
- const BUILD_TAG = 'dcb-1.3.2'
843
+ const BUILD_TAG = 'dcb-1.3.3'
803
844
  // 本地版本:本包是零构建的手写客户端包,无法 import package.json,因此把版本号作为常量维护,
804
845
  // 并由 check.mjs 守卫与 package.json 严格一致(发布前不一致直接失败)。
805
- const PLUGIN_VERSION = '1.3.2'
846
+ const PLUGIN_VERSION = '1.3.3'
806
847
  const PKG_NAME = 'dsh-consumption-badge'
807
848
  const UPDATE_REGISTRY_URL = 'https://registry.npmjs.org/' + PKG_NAME + '/latest'
808
849
  const UPDATE_COMMAND = 'dsh plugin --profile web update ' + PKG_NAME
@@ -1195,6 +1236,8 @@ window.__ModuleLoader__.load({
1195
1236
  currency: readCurrency(),
1196
1237
  // 模型筛选 / 计价兜底('' = 全部模型,不筛选;同时作为查不到模型时的计价基准)。
1197
1238
  modelFilter: readModelFilter(),
1239
+ // 语言覆盖('' = 跟随外壳;'zh' / 'en' = 用户显式指定)。
1240
+ language: readLanguage(),
1198
1241
  rates: null,
1199
1242
  ratesState: 'idle',
1200
1243
  ratesError: '',
@@ -1448,6 +1491,14 @@ window.__ModuleLoader__.load({
1448
1491
  emit()
1449
1492
  if (next !== 'CNY') ensureRates(false)
1450
1493
  }
1494
+ /** 切换界面语言:'' 跟随外壳,或钉死 zh / en(落 localStorage,立即重绘)。 */
1495
+ function setLanguage(next) {
1496
+ const value = next === 'zh' || next === 'en' ? next : ''
1497
+ if (state.language === value) return
1498
+ setLanguageOverride(value)
1499
+ state.language = value
1500
+ emit()
1501
+ }
1451
1502
  /** 强制刷新汇率(设置页的「刷新汇率」按钮)。 */
1452
1503
  function refreshRates() {
1453
1504
  state.rates = null
@@ -1627,6 +1678,7 @@ window.__ModuleLoader__.load({
1627
1678
  setCurrency: setCurrency,
1628
1679
  refreshRates: refreshRates,
1629
1680
  setModelFilter: setModelFilter,
1681
+ setLanguage: setLanguage,
1630
1682
  openPanel: openPanel,
1631
1683
  closePanel: closePanel,
1632
1684
  setModalStyle: setModalStyle,
@@ -2672,6 +2724,7 @@ window.__ModuleLoader__.load({
2672
2724
  const setAllStats = s8[1]
2673
2725
  // 「模型」下拉的状态放在 store 里(弹层与设置页共享同一个选择)。
2674
2726
  const modelFilter = store.state.modelFilter
2727
+ const language = store.state.language
2675
2728
  const s9 = React.useState(0)
2676
2729
  const setStoreTick = s9[1]
2677
2730
 
@@ -2928,6 +2981,18 @@ window.__ModuleLoader__.load({
2928
2981
  },
2929
2982
  h('option', { key: 'all', value: '' }, tt('全部模型')),
2930
2983
  modelOptionNodes(MODELS))),
2984
+ // 语言:默认跟随外壳;外壳语言不对劲时可以直接钉死中文/英文。
2985
+ h('div', { className: 'dcb-field' },
2986
+ h('label', { className: 'dcb-field-label', htmlFor: 'dcb-language-select' }, tt('语言')),
2987
+ h('select', {
2988
+ id: 'dcb-language-select',
2989
+ className: 'dcb-select',
2990
+ value: language,
2991
+ onChange: function (event) { store.setLanguage(event.target.value) },
2992
+ },
2993
+ languageOptions().map(function (opt) {
2994
+ return h('option', { key: opt.id || 'auto', value: opt.id }, opt.label)
2995
+ }))),
2931
2996
  // 显示币种:报价表永远是人民币,这里只决定展示时换算成什么货币。
2932
2997
  h('div', { className: 'dcb-field' },
2933
2998
  h('label', { className: 'dcb-field-label', htmlFor: 'dcb-currency-select' }, tt('货币')),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-consumption-badge",
3
- "version": "1.3.2",
3
+ "version": "1.3.3",
4
4
  "description": "DSH Web UI plugin: a per-message consumption badge (tokens | estimated cost | peak/valley) plus a session detail modal with cumulative tokens, estimated cost, Beijing-time peak/off-peak pricing, a per-turn dual-axis chart, and the DeepSeek account balance",
5
5
  "keywords": [
6
6
  "deepseek-harness",