在 PR #471(file-trace)基础上的演进: - 合并 tab:git tab(id 保留,兼容持久化布局)升级为「文件变动」双视角 tab——Git 视角(原 GitView 全部能力:staged/unstaged/提交/历史/worktree· 子仓库选择/右键菜单/确认弹窗)+ 本轮文件视角(原 file-trace:会话事件 日志实时折叠的模型读写编辑记录,按文件分组、新增类型筛选 chips、相对 时间);file-trace tab 移除(未随任何版本发布)。 - 统一 diff 组件:新 src/client/diff/ 模块(rows.ts 引擎 + DiffRows/ DiffFiles 渲染器 + highlight.ts 语法着色)——git diff 与会话操作 diff 共用一套渲染:mod 改写配对(del-run/add-run 对半标蓝)、行内字符级 高亮(修复了旧实现旧侧失配不高亮的问题,改为行对象身份键)、上下文 hunk 折叠、git hunk 间未输出区间按行号范围生成不可展开折叠、语法着色 色相落位 diff 三色空档、+n −m 统计 chips、行底色 tint;DiffTab 改用 该栈(git diff tab 从此也有高亮)。 - 交互:两视角共用底部可拖拽预览面板(点击文件/提交/操作即预览,不再 直接开 diff tab;面板可刷新、可键盘/拖拽调高、git 目标可一键展开为 独立 diff tab);视角选择与面板高度持久化在 tab.meta;git 行 XY 徽章 按状态字母着色;选中行高亮。 - i18n:fileTrace* 十键重命名为 changes* 前缀,git 键值更新为「文件 变动」,新增 7 键(视角/筛选/面板操作),全部 21 个词典同步。 - 结构:src/client/changes/(ChangesTab/GitLens/SessionLens/DiffPane/ ops)+ src/client/diff/;GitView/DiffView/filetrace/ 删除;git 样式 从 sidebar.module.css 迁至 changes.module.css。 - 测试:builtins 8→7(git tab 承接 badge pin)、diff-rows(unified 转换/mod 配对/间隙折叠/统计)、changes-tab(壳交互 jsdom)、 diff-files-collapse(原 DiffView 折叠行为钉住)、changes-ops(原 filetrace 数据层)、git-view-worktree 迁移至 GitLens;全量 1122 通过。 - 文档:README ×2 功能矩阵合并、external-plugin-guide §4.4 与内置清单 7 tab、mount e2e 菜单标题同步。
261 lines
8.7 KiB
TypeScript
261 lines
8.7 KiB
TypeScript
/**
|
|
* Locale-following spec: the sidebar copy follows the DSH locale service
|
|
* (`ctx.locale`, provided by @deepseek-ai/dsh-client-locale) when attached
|
|
* through `attachLocale`, and falls back to the browser language otherwise.
|
|
* Covers attach/detach, live switching, dictionary parity, and placeholder
|
|
* interpolation.
|
|
*/
|
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
import { LOCALE_NS, attachBetterLocale, attachLocale, en, isZh, ja, relativeTime, t, zh, de, fr, pt, ko, ar, hi, id as idDict, tr, vi, th, ru, it as itDict, nl, sv, pl, zhHK, zhTW, zhMO } from '../src/client/locales.ts'
|
|
|
|
/** Minimal structural fake of the DSH LocaleService face the sidebar uses. */
|
|
class FakeLocale {
|
|
active: string = 'en'
|
|
getSnapshot(): { active: string } {
|
|
return { active: this.active }
|
|
}
|
|
subscribe(_fn: () => void): () => void {
|
|
return () => {}
|
|
}
|
|
register(_ns: string, _locale: string, _dict: Record<string, string>): () => void {
|
|
return () => {}
|
|
}
|
|
switchTo(id: string): void {
|
|
this.active = id
|
|
}
|
|
}
|
|
|
|
/** Point the browser-language fallback at a specific language (undefined = none). */
|
|
function stubNavigatorLanguage(lang: string | undefined): void {
|
|
Object.defineProperty(globalThis, 'navigator', {
|
|
value: lang === undefined ? undefined : { language: lang },
|
|
configurable: true,
|
|
})
|
|
}
|
|
|
|
afterEach(() => {
|
|
attachLocale(undefined)
|
|
attachBetterLocale(undefined)
|
|
stubNavigatorLanguage(undefined)
|
|
})
|
|
|
|
describe('locales (DSH i18n following)', () => {
|
|
it('falls back to the browser language without an attached service', () => {
|
|
stubNavigatorLanguage('en-US')
|
|
expect(t('explorer')).toBe('Explorer')
|
|
expect(isZh()).toBe(false)
|
|
|
|
stubNavigatorLanguage('zh-CN')
|
|
expect(t('explorer')).toBe('资源管理器')
|
|
expect(isZh()).toBe(true)
|
|
})
|
|
|
|
it('defaults to English when no locale service and no browser language are available', () => {
|
|
expect(t('explorer')).toBe('Explorer')
|
|
expect(isZh()).toBe(false)
|
|
})
|
|
|
|
it('follows the attached locale service instead of the browser language', () => {
|
|
stubNavigatorLanguage('en-US')
|
|
const locale = new FakeLocale()
|
|
attachLocale(locale)
|
|
|
|
locale.switchTo('zh')
|
|
expect(t('changes')).toBe('文件变动')
|
|
expect(isZh()).toBe(true)
|
|
|
|
// Live switch: the service's active locale wins even though the
|
|
// browser still asks for en-US.
|
|
locale.switchTo('en')
|
|
expect(t('changes')).toBe('Changes')
|
|
expect(isZh()).toBe(false)
|
|
})
|
|
|
|
it('detaches back to the browser-language fallback', () => {
|
|
const locale = new FakeLocale()
|
|
locale.switchTo('zh')
|
|
attachLocale(locale)
|
|
expect(t('terminal')).toBe('终端')
|
|
|
|
stubNavigatorLanguage('en-US')
|
|
attachLocale(undefined)
|
|
expect(t('terminal')).toBe('Terminal')
|
|
})
|
|
|
|
it('interpolates {name} placeholders in the active locale', () => {
|
|
const locale = new FakeLocale()
|
|
attachLocale(locale)
|
|
locale.switchTo('zh')
|
|
expect(t('timeMinutesAgo', { n: 5 })).toBe('5 分钟前')
|
|
locale.switchTo('en')
|
|
expect(t('timeMinutesAgo', { n: 5 })).toBe('5 min ago')
|
|
})
|
|
|
|
it('drives the relative-time chain through the active locale', () => {
|
|
const locale = new FakeLocale()
|
|
attachLocale(locale)
|
|
locale.switchTo('zh')
|
|
expect(relativeTime(new Date().toISOString())).toBe('刚刚')
|
|
locale.switchTo('en')
|
|
expect(relativeTime(new Date().toISOString())).toBe('just now')
|
|
})
|
|
|
|
it('registers a namespace distinct from DSH ui-sidebar\'s own \'sidebar\'', () => {
|
|
expect(LOCALE_NS).toBe('betterSidebar')
|
|
})
|
|
|
|
it('keeps the zh and en dictionaries key-set-equal', () => {
|
|
expect(Object.keys(en).sort()).toEqual(Object.keys(zh).sort())
|
|
})
|
|
|
|
it('keeps the ja dictionary key-set-equal to zh', () => {
|
|
expect(Object.keys(ja).sort()).toEqual(Object.keys(zh).sort())
|
|
})
|
|
|
|
it('keeps every shipped third-language dictionary key-set-equal to zh', () => {
|
|
// The third-language dicts are typed `Record<keyof typeof zh, string>` via
|
|
// an `as` cast (unchecked by tsc). This test catches missing/extra keys
|
|
// introduced when a zh key is added without updating every language.
|
|
const thirdLangs = {
|
|
de, fr, pt, ko, ar, hi, idDict, tr, vi, th, ru, itDict, nl, sv, pl, zhHK, zhTW, zhMO,
|
|
}
|
|
for (const [lang, dict] of Object.entries(thirdLangs)) {
|
|
expect(Object.keys(dict as Record<string, string>).sort(), lang).toEqual(Object.keys(zh).sort())
|
|
}
|
|
})
|
|
})
|
|
|
|
/** Minimal fake of the better-locale override store (the subset t()/isZh() read). */
|
|
class FakeBetterLocale {
|
|
active: string | undefined = undefined
|
|
private readonly dict: Record<string, Record<string, Record<string, string>>>
|
|
constructor(dict: Record<string, Record<string, Record<string, string>>>) {
|
|
this.dict = dict
|
|
}
|
|
getOverride(dshActive: string, ns: string, key: string): string | undefined {
|
|
if (this.active === undefined) return undefined
|
|
if (dshActive !== 'en') return undefined
|
|
return this.dict[ns]?.[this.active]?.[key]
|
|
}
|
|
isOverrideActive(dshActive: string): boolean {
|
|
return this.active !== undefined && dshActive === 'en'
|
|
}
|
|
subscribe(_listener: () => void): () => void {
|
|
return () => {}
|
|
}
|
|
}
|
|
|
|
describe('locales (better-locale override)', () => {
|
|
it('returns the override text when an override is active and the key exists', () => {
|
|
const locale = new FakeLocale()
|
|
locale.switchTo('en')
|
|
attachLocale(locale)
|
|
|
|
const betterLocale = new FakeBetterLocale({
|
|
[LOCALE_NS]: { ja: { explorer: 'エクスプローラー' } },
|
|
})
|
|
betterLocale.active = 'ja'
|
|
attachBetterLocale(betterLocale)
|
|
|
|
expect(t('explorer')).toBe('エクスプローラー')
|
|
expect(isZh()).toBe(false)
|
|
})
|
|
|
|
it('falls back to the zh/en chain when override is active but the key is missing', () => {
|
|
const locale = new FakeLocale()
|
|
locale.switchTo('en')
|
|
attachLocale(locale)
|
|
|
|
const betterLocale = new FakeBetterLocale({
|
|
[LOCALE_NS]: { ja: { explorer: 'エクスプローラー' } },
|
|
})
|
|
betterLocale.active = 'ja'
|
|
attachBetterLocale(betterLocale)
|
|
|
|
// 'changes' has no ja entry in this fake; should fall back to the en text.
|
|
expect(t('changes')).toBe('Changes')
|
|
})
|
|
|
|
it('falls back to the zh/en chain when no override is active', () => {
|
|
const locale = new FakeLocale()
|
|
locale.switchTo('zh')
|
|
attachLocale(locale)
|
|
|
|
const betterLocale = new FakeBetterLocale({
|
|
[LOCALE_NS]: { ja: { explorer: 'エクスプローラー' } },
|
|
})
|
|
// active is undefined — no override.
|
|
attachBetterLocale(betterLocale)
|
|
|
|
expect(t('explorer')).toBe('资源管理器')
|
|
expect(isZh()).toBe(true)
|
|
})
|
|
|
|
it('override is inert while DSH is on zh (override borrows the en slot)', () => {
|
|
const locale = new FakeLocale()
|
|
locale.switchTo('zh')
|
|
attachLocale(locale)
|
|
|
|
const betterLocale = new FakeBetterLocale({
|
|
[LOCALE_NS]: { ja: { explorer: 'エクスプローラー' } },
|
|
})
|
|
betterLocale.active = 'ja'
|
|
attachBetterLocale(betterLocale)
|
|
|
|
// Override is set to 'ja' but DSH is on zh — override is inert,
|
|
// native zh text wins. The user must switch DSH to en to see ja.
|
|
expect(t('explorer')).toBe('资源管理器')
|
|
expect(isZh()).toBe(true)
|
|
})
|
|
|
|
it('returns to the zh/en chain after detaching the override store', () => {
|
|
const locale = new FakeLocale()
|
|
locale.switchTo('en')
|
|
attachLocale(locale)
|
|
|
|
const betterLocale = new FakeBetterLocale({
|
|
[LOCALE_NS]: { ja: { explorer: 'エクスプローラー' } },
|
|
})
|
|
betterLocale.active = 'ja'
|
|
attachBetterLocale(betterLocale)
|
|
expect(t('explorer')).toBe('エクスプローラー')
|
|
|
|
attachBetterLocale(undefined)
|
|
expect(t('explorer')).toBe('Explorer')
|
|
expect(isZh()).toBe(false)
|
|
})
|
|
|
|
it('interpolates {name} placeholders in the override text', () => {
|
|
const locale = new FakeLocale()
|
|
locale.switchTo('en')
|
|
attachLocale(locale)
|
|
|
|
const betterLocale = new FakeBetterLocale({
|
|
[LOCALE_NS]: { ja: { timeMinutesAgo: '{n} 分前' } },
|
|
})
|
|
betterLocale.active = 'ja'
|
|
attachBetterLocale(betterLocale)
|
|
|
|
expect(t('timeMinutesAgo', { n: 5 })).toBe('5 分前')
|
|
})
|
|
|
|
it('ships a ja dictionary whose every key resolves to a non-empty string', () => {
|
|
// Smoke-check the shipped ja dict: every key in zh must have a non-empty
|
|
// ja entry. This catches copy-paste mistakes where a key was added to zh
|
|
// but its ja translation was left blank or missing.
|
|
const locale = new FakeLocale()
|
|
locale.switchTo('en')
|
|
attachLocale(locale)
|
|
|
|
const betterLocale = new FakeBetterLocale({ [LOCALE_NS]: { ja } })
|
|
betterLocale.active = 'ja'
|
|
attachBetterLocale(betterLocale)
|
|
|
|
for (const key of Object.keys(zh) as (keyof typeof zh)[]) {
|
|
const text = t(key)
|
|
expect(text, `ja translation for "${key}"`).toBeTruthy()
|
|
expect(text, `ja translation for "${key}"`).not.toBe(key)
|
|
}
|
|
})
|
|
})
|