From 0ae997e20585b88b84fbbb7f99d39bdd310996d5 Mon Sep 17 00:00:00 2001 From: che cheng Date: Thu, 16 Jul 2026 13:08:18 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20i18n=20=E4=B8=89=E8=AA=9E=E6=9E=B6?= =?UTF-8?q?=E6=A7=8B=20=E2=80=94=20=E6=96=B0=E5=A2=9E=E6=97=A5=E6=96=87?= =?UTF-8?q?=E3=80=81=E8=AA=9E=E8=A8=80=E9=81=B8=E5=96=AE=E5=8F=96=E4=BB=A3?= =?UTF-8?q?=20toggle=20(#2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Locale 開放為 zh-TW/en/ja;ja 完整字典(型別保證 key 一致) - detectLocale 白名單 stored + ja 前綴對應 - 語言切換改 select 選單(原生語言名),change 事件換字 + aria-label 重設 - langToggle key 廢棄 → langSelectLabel(三語) Co-Authored-By: Claude Fable 5 --- src/app.ts | 15 +++++++++++---- src/i18n/index.ts | 9 +++++++-- src/i18n/strings.ts | 37 +++++++++++++++++++++++++++++++++---- tests/i18n.test.ts | 23 +++++++++++++++-------- tests/main-layout.test.ts | 22 +++++++++++++++++----- 5 files changed, 83 insertions(+), 23 deletions(-) diff --git a/src/app.ts b/src/app.ts index 352f552..92c7d3b 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,4 +1,4 @@ -import { applyI18n, getLocale, setLocale, t } from './i18n'; +import { applyI18n, getLocale, setLocale, t, type Locale } from './i18n'; import type { Capabilities } from './support'; import { Session, buildExport } from './core/session'; import { mountTrail } from './panels/trail'; @@ -14,7 +14,11 @@ export function renderApp(root: HTMLElement, session: Session, caps: Capabilitie

${t('pageTitle')}

${t('pageSubtitle')}

- + ${caps.touchDevice ? `` : ''}
@@ -35,9 +39,12 @@ export function renderApp(root: HTMLElement, session: Session, caps: Capabilitie mountScroll(root.querySelector('#panel-scroll') as HTMLElement, session); mountPinch(root.querySelector('#panel-pinch') as HTMLElement, session, caps); - (root.querySelector('#lang-toggle') as HTMLButtonElement).addEventListener('click', () => { - setLocale(getLocale() === 'zh-TW' ? 'en' : 'zh-TW'); + const langSelect = root.querySelector('#lang-select') as HTMLSelectElement; + langSelect.value = getLocale(); + langSelect.addEventListener('change', () => { + setLocale(langSelect.value as Locale); applyI18n(document); + langSelect.setAttribute('aria-label', t('langSelectLabel')); document.title = t('pageTitle'); }); diff --git a/src/i18n/index.ts b/src/i18n/index.ts index 0c4c39b..736ab15 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -3,9 +3,14 @@ import { strings, type Locale, type Strings } from './strings'; const STORAGE_KEY = 'mac-trackpad-test-lang'; let current: Locale = 'zh-TW'; +const SUPPORTED: readonly Locale[] = ['zh-TW', 'en', 'ja']; + export function detectLocale(navLang: string, stored: string | null): Locale { - if (stored === 'zh-TW' || stored === 'en') return stored; - return navLang.toLowerCase().startsWith('zh') ? 'zh-TW' : 'en'; + if (stored && (SUPPORTED as readonly string[]).includes(stored)) return stored as Locale; + const lang = navLang.toLowerCase(); + if (lang.startsWith('zh')) return 'zh-TW'; + if (lang.startsWith('ja')) return 'ja'; + return 'en'; } // localStorage 在無痕模式(舊 Safari)可能拋錯、在部分測試環境不存在——一律防禦性存取。 diff --git a/src/i18n/strings.ts b/src/i18n/strings.ts index d4a91a8..3ea55dd 100644 --- a/src/i18n/strings.ts +++ b/src/i18n/strings.ts @@ -1,7 +1,7 @@ -export type Locale = 'zh-TW' | 'en'; +export type Locale = 'zh-TW' | 'en' | 'ja'; export interface Strings { - pageTitle: string; pageSubtitle: string; langToggle: string; touchBanner: string; + pageTitle: string; pageSubtitle: string; langSelectLabel: string; touchBanner: string; trailTitle: string; trailHint: string; hzLabel: string; maxJumpLabel: string; jumpCountLabel: string; clearBtn: string; gridTitle: string; gridHint: string; @@ -20,7 +20,7 @@ export const strings: Record = { 'zh-TW': { pageTitle: 'Mac 觸控板健康檢查', pageSubtitle: '依序做完五關。每一關都在測觸控板的一種故障模式(死區、微動開關、壓力感應、慣性捲動、多指手勢)。', - langToggle: 'EN', + langSelectLabel: '介面語言', touchBanner: '本工具供 Mac 觸控板檢測,請在 Mac 上用 Safari 開啟。', trailTitle: '1. 軌跡測試 — 找死區與跳動', trailHint: '單指掃滿整個觸控板表面,來回蓋滿。畫出的線應該連續平滑:斷線 = 死區。紅色線段 = 速度突變異常(這一步超過 50px 且是前一步的 3 倍以上)——快滑不會誤標,出現紅色代表游標從平順移動中突然瞬移。', @@ -49,7 +49,7 @@ export const strings: Record = { en: { pageTitle: 'Mac Trackpad Test', pageSubtitle: 'Run all five checks. Each targets one trackpad failure mode (dead zones, click switches, pressure sensing, inertial scrolling, multi-finger gestures).', - langToggle: '中文', + langSelectLabel: 'Language', touchBanner: 'This tool checks Mac trackpads — please open it in Safari on a Mac.', trailTitle: '1. Trail — dead zones & cursor jumps', trailHint: 'Sweep one finger across the whole trackpad surface until covered. The line should be continuous and smooth: a gap = dead zone. Red segments = velocity anomalies (a step over 50px and 3× the previous step) — fast swipes are not flagged; red means the cursor teleported mid-motion.', @@ -74,5 +74,34 @@ export const strings: Record = { needSafariTitle: 'Safari required', needSafariForce: 'Pressure sensing uses Safari-only webkitForce events; Chrome/Firefox cannot run this check. Other checks still work.', needSafariPinch: 'Pinch gestures use Safari-only GestureEvent; Chrome/Firefox cannot run this check. Other checks still work.' + }, + ja: { + pageTitle: 'Mac トラックパッド健康チェック', + pageSubtitle: '5つのチェックを順番に行ってください。それぞれがトラックパッドの故障モード(デッドゾーン、クリックスイッチ、感圧センサー、慣性スクロール、マルチタッチジェスチャー)を検査します。', + langSelectLabel: '言語', + touchBanner: 'このツールは Mac のトラックパッド検査用です。Mac の Safari で開いてください。', + trailTitle: '1. 軌跡テスト — デッドゾーンとカーソル飛び', + trailHint: '指1本でトラックパッド全面をまんべんなくなぞってください。線は連続して滑らかなはずです:途切れ=デッドゾーン。赤い線分=速度異常(1ステップが50px超かつ直前の3倍以上)——素早いスワイプは誤検出されません。赤が出たら、カーソルが滑らかな動きから突然瞬間移動した証拠です。', + hzLabel: 'イベントレート', maxJumpLabel: '最大ステップ', jumpCountLabel: '異常', clearBtn: 'クリア', + gridTitle: '2. 3×3 クリックグリッド — 四隅までクリック確認', + gridHint: '9マスすべてを左クリック(緑に変化)、次に右クリック/2本指クリック(青に変化)してください。反応しないマス=そのゾーンのスイッチまたはセンサー異常。', + leftLabel: '左', rightLabel: '右', dblLabel: 'ダブルクリック', + dblPass: '合格', dblPending: '未実施(任意のマスをダブルクリック)', + forceTitle: '3. Force Touch 感圧', + forceHint: '下のバーを押したまま、徐々に力を加えてください:バーは滑らかに上昇し、白線を超えると強めのクリック(触覚フィードバック)が発動します。数値が飛ぶ、または白線に届かない場合=感圧レイヤーの異常。', + forceNowLabel: '現在', forceMaxLabel: '最大', forceClickLabel: '強めのクリック', + forceClickDone: '発動 ✓', forceClickPending: '未発動', + scrollTitle: '4. 2本指スクロール — 滑らかさと慣性', + scrollHint: 'ボックス内を2本指で上下にスクロールし、指を離して慣性で流れるのを確認してください。指に追従し、滑らかに減速するはずです。', + scrollLine: '— 滑らかにスクロール', dyLabel: '現在 ΔY', wheelCountLabel: 'イベント数', + pinchTitle: '5. ピンチズームと回転', + pinchHint: '青い正方形の上で2本指のピンチイン/ピンチアウト・回転をしてください。リアルタイムに追従すれば、各指が独立してトラッキングされている証拠です。', + pinchTarget: '2本指', scaleLabel: '拡大率', rotationLabel: '回転', + exportTitle: '6. 結果のエクスポート', + exportHint: '5つのチェックを終えたら下のボタンを押してください:結果がクリップボードにコピーされ(JSON もダウンロード)、AI や技術者に貼り付けて判定してもらえます。', + exportBtn: '結果をコピー', exportCopied: 'コピーしました ✓', exportCopyFailed: 'クリップボード失敗 — ダウンロードしました', + needSafariTitle: 'Safari が必要です', + needSafariForce: '感圧センサーは Safari 専用の webkitForce イベントを使用します。Chrome/Firefox ではこのチェックは実行できません。他のチェックは利用できます。', + needSafariPinch: 'ピンチジェスチャーは Safari 専用の GestureEvent を使用します。Chrome/Firefox ではこのチェックは実行できません。他のチェックは利用できます。' } }; diff --git a/tests/i18n.test.ts b/tests/i18n.test.ts index 0e44710..24c0ed9 100644 --- a/tests/i18n.test.ts +++ b/tests/i18n.test.ts @@ -2,32 +2,39 @@ import { strings } from '../src/i18n/strings'; import { detectLocale, setLocale, getLocale, t, applyI18n } from '../src/i18n'; describe('i18n', () => { - it('兩份字典 key 完全一致', () => { - expect(Object.keys(strings['zh-TW']).sort()).toEqual(Object.keys(strings.en).sort()); + it('三份字典 key 完全一致', () => { + const zhKeys = Object.keys(strings['zh-TW']).sort(); + expect(Object.keys(strings.en).sort()).toEqual(zhKeys); + expect(Object.keys(strings.ja).sort()).toEqual(zhKeys); }); - it('detectLocale:stored 優先、zh 前綴 → zh-TW、其他 → en', () => { + it('detectLocale:stored 優先、zh 前綴 → zh-TW、ja 前綴 → ja、其他 → en', () => { expect(detectLocale('zh-TW', null)).toBe('zh-TW'); expect(detectLocale('zh-CN', null)).toBe('zh-TW'); - expect(detectLocale('ja-JP', null)).toBe('en'); + expect(detectLocale('ja-JP', null)).toBe('ja'); + expect(detectLocale('ja', null)).toBe('ja'); + expect(detectLocale('ko-KR', null)).toBe('en'); expect(detectLocale('zh-TW', 'en')).toBe('en'); expect(detectLocale('en-US', 'zh-TW')).toBe('zh-TW'); + expect(detectLocale('en-US', 'ja')).toBe('ja'); expect(detectLocale('en-US', 'garbage')).toBe('en'); }); - it('setLocale + t 切換字典', () => { + it('setLocale + t 切換三份字典', () => { setLocale('zh-TW'); expect(t('exportBtn')).toBe(strings['zh-TW'].exportBtn); + setLocale('ja'); + expect(getLocale()).toBe('ja'); + expect(t('exportBtn')).toBe(strings.ja.exportBtn); setLocale('en'); - expect(getLocale()).toBe('en'); expect(t('exportBtn')).toBe(strings.en.exportBtn); }); it('applyI18n 原地換掉 [data-i18n] 的文字', () => { - setLocale('en'); + setLocale('ja'); const div = document.createElement('div'); div.innerHTML = '舊字'; applyI18n(div); - expect(div.querySelector('span')!.textContent).toBe(strings.en.trailTitle); + expect(div.querySelector('span')!.textContent).toBe(strings.ja.trailTitle); }); }); diff --git a/tests/main-layout.test.ts b/tests/main-layout.test.ts index 0416ad1..ab996fe 100644 --- a/tests/main-layout.test.ts +++ b/tests/main-layout.test.ts @@ -4,13 +4,15 @@ import { Session } from '../src/core/session'; afterEach(() => { document.body.innerHTML = ''; }); describe('renderApp', () => { - it('掛載六個 section 與語言切換鈕;不支援的面板顯示降級卡', () => { + it('掛載六個 section 與語言選單;不支援的面板顯示降級卡', () => { const root = document.createElement('div'); document.body.appendChild(root); const session = new Session(); renderApp(root, session, { forceTouch: false, gesture: false, touchDevice: false }); expect(root.querySelectorAll('section')).toHaveLength(6); - expect(root.querySelector('#lang-toggle')).not.toBeNull(); + const select = root.querySelector('#lang-select') as HTMLSelectElement; + expect(select).not.toBeNull(); + expect([...select.options].map((o) => o.value)).toEqual(['zh-TW', 'en', 'ja']); expect(root.querySelectorAll('.notice-card')).toHaveLength(2); expect(session.skipped.sort()).toEqual(['force', 'pinch']); }); @@ -22,12 +24,22 @@ describe('renderApp', () => { expect(root.querySelector('.banner')).not.toBeNull(); }); - it('語言切換鈕換字', () => { + it('語言選單切換換字(en 與 ja)', () => { const root = document.createElement('div'); document.body.appendChild(root); renderApp(root, new Session(), { forceTouch: true, gesture: true, touchDevice: false }); + const select = root.querySelector('#lang-select') as HTMLSelectElement; const before = root.querySelector('h1')!.textContent; - (root.querySelector('#lang-toggle') as HTMLButtonElement).click(); - expect(root.querySelector('h1')!.textContent).not.toBe(before); + + select.value = 'en'; + select.dispatchEvent(new Event('change', { bubbles: true })); + const enTitle = root.querySelector('h1')!.textContent; + expect(enTitle).not.toBe(before); + + select.value = 'ja'; + select.dispatchEvent(new Event('change', { bubbles: true })); + const jaTitle = root.querySelector('h1')!.textContent; + expect(jaTitle).not.toBe(enTitle); + expect(jaTitle).not.toBe(before); }); }); From 8c6cdcfb1269ab7dd79db634993c6a868a183c6f Mon Sep 17 00:00:00 2001 From: che cheng Date: Thu, 16 Jul 2026 13:33:19 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20address=20verify=20R1=20findings=20?= =?UTF-8?q?=E2=80=94=20LOCALES=20=E5=96=AE=E4=B8=80=E4=BA=8B=E5=AF=A6?= =?UTF-8?q?=E4=BE=86=E6=BA=90=E3=80=81isLocale=20=E5=AE=88=E8=A1=9B?= =?UTF-8?q?=E3=80=81=E6=97=A5=E6=96=87=E6=BD=A4=E9=A3=BE=E3=80=81=E8=A3=9C?= =?UTF-8?q?=E6=B8=AC=E8=A9=A6=20(#2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LOCALES/localeNames 為單一來源;Locale type、偵測白名單、選單 options 全派生(三 AI 收斂 finding) - select change 加 isLocale 型別守衛;選單 label 改語言原生名(繁體中文/English/日本語) - 日文潤飾 8 處(exportHint 共有語意、exportCopyFailed、最大移動量/異常数、作動/未検出等) - 測試:LOCALES 對齊斷言、isLocale 案例、export language:'ja' 覆蓋、切換測試固定起點 Co-Authored-By: Claude Fable 5 --- src/app.ts | 9 ++++----- src/i18n/index.ts | 10 ++++++---- src/i18n/strings.ts | 24 +++++++++++++++++------- tests/i18n.test.ts | 16 ++++++++++++++-- tests/main-layout.test.ts | 3 +++ tests/session.test.ts | 5 +++++ 6 files changed, 49 insertions(+), 18 deletions(-) diff --git a/src/app.ts b/src/app.ts index 92c7d3b..ad57827 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,4 +1,4 @@ -import { applyI18n, getLocale, setLocale, t, type Locale } from './i18n'; +import { applyI18n, getLocale, setLocale, t, isLocale, LOCALES, localeNames } from './i18n'; import type { Capabilities } from './support'; import { Session, buildExport } from './core/session'; import { mountTrail } from './panels/trail'; @@ -15,9 +15,7 @@ export function renderApp(root: HTMLElement, session: Session, caps: Capabilitie

${t('pageSubtitle')}

${caps.touchDevice ? `` : ''} @@ -42,7 +40,8 @@ export function renderApp(root: HTMLElement, session: Session, caps: Capabilitie const langSelect = root.querySelector('#lang-select') as HTMLSelectElement; langSelect.value = getLocale(); langSelect.addEventListener('change', () => { - setLocale(langSelect.value as Locale); + if (!isLocale(langSelect.value)) return; // options 由 LOCALES 生成,此守衛防未來 DOM 漂移 + setLocale(langSelect.value); applyI18n(document); langSelect.setAttribute('aria-label', t('langSelectLabel')); document.title = t('pageTitle'); diff --git a/src/i18n/index.ts b/src/i18n/index.ts index 736ab15..4eb31a6 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -1,12 +1,14 @@ -import { strings, type Locale, type Strings } from './strings'; +import { strings, LOCALES, type Locale, type Strings } from './strings'; const STORAGE_KEY = 'mac-trackpad-test-lang'; let current: Locale = 'zh-TW'; -const SUPPORTED: readonly Locale[] = ['zh-TW', 'en', 'ja']; +export function isLocale(value: string): value is Locale { + return (LOCALES as readonly string[]).includes(value); +} export function detectLocale(navLang: string, stored: string | null): Locale { - if (stored && (SUPPORTED as readonly string[]).includes(stored)) return stored as Locale; + if (stored && isLocale(stored)) return stored; const lang = navLang.toLowerCase(); if (lang.startsWith('zh')) return 'zh-TW'; if (lang.startsWith('ja')) return 'ja'; @@ -42,4 +44,4 @@ export function applyI18n(root: ParentNode): void { }); } -export { strings, type Locale, type Strings }; +export { strings, LOCALES, localeNames, type Locale, type Strings } from './strings'; diff --git a/src/i18n/strings.ts b/src/i18n/strings.ts index 3ea55dd..3402b42 100644 --- a/src/i18n/strings.ts +++ b/src/i18n/strings.ts @@ -1,4 +1,14 @@ -export type Locale = 'zh-TW' | 'en' | 'ja'; +// 單一事實來源:新增語言只在此檔動三處(LOCALES、localeNames、strings 字典), +// Locale type、偵測白名單、選單 options 全部由此派生(verify #2 R1 收斂 finding)。 +export const LOCALES = ['zh-TW', 'en', 'ja'] as const; +export type Locale = (typeof LOCALES)[number]; + +// 選單顯示用的語言原生名(以各語言自身書寫,i18n 慣例,不隨介面語言翻譯) +export const localeNames: Record = { + 'zh-TW': '繁體中文', + en: 'English', + ja: '日本語' +}; export interface Strings { pageTitle: string; pageSubtitle: string; langSelectLabel: string; touchBanner: string; @@ -81,8 +91,8 @@ export const strings: Record = { langSelectLabel: '言語', touchBanner: 'このツールは Mac のトラックパッド検査用です。Mac の Safari で開いてください。', trailTitle: '1. 軌跡テスト — デッドゾーンとカーソル飛び', - trailHint: '指1本でトラックパッド全面をまんべんなくなぞってください。線は連続して滑らかなはずです:途切れ=デッドゾーン。赤い線分=速度異常(1ステップが50px超かつ直前の3倍以上)——素早いスワイプは誤検出されません。赤が出たら、カーソルが滑らかな動きから突然瞬間移動した証拠です。', - hzLabel: 'イベントレート', maxJumpLabel: '最大ステップ', jumpCountLabel: '異常', clearBtn: 'クリア', + trailHint: '指1本でトラックパッド全面をまんべんなくなぞってください。線は連続して滑らかなはずです:途切れ=デッドゾーン。赤い線分=速度異常(1回の移動量が50pxを超え、直前の3倍以上)——素早いスワイプは誤検出されません。赤が出たら、カーソルが突然飛んだ証拠です。', + hzLabel: 'イベントレート', maxJumpLabel: '最大移動量', jumpCountLabel: '異常数', clearBtn: 'クリア', gridTitle: '2. 3×3 クリックグリッド — 四隅までクリック確認', gridHint: '9マスすべてを左クリック(緑に変化)、次に右クリック/2本指クリック(青に変化)してください。反応しないマス=そのゾーンのスイッチまたはセンサー異常。', leftLabel: '左', rightLabel: '右', dblLabel: 'ダブルクリック', @@ -90,16 +100,16 @@ export const strings: Record = { forceTitle: '3. Force Touch 感圧', forceHint: '下のバーを押したまま、徐々に力を加えてください:バーは滑らかに上昇し、白線を超えると強めのクリック(触覚フィードバック)が発動します。数値が飛ぶ、または白線に届かない場合=感圧レイヤーの異常。', forceNowLabel: '現在', forceMaxLabel: '最大', forceClickLabel: '強めのクリック', - forceClickDone: '発動 ✓', forceClickPending: '未発動', + forceClickDone: '作動 ✓', forceClickPending: '未検出', scrollTitle: '4. 2本指スクロール — 滑らかさと慣性', scrollHint: 'ボックス内を2本指で上下にスクロールし、指を離して慣性で流れるのを確認してください。指に追従し、滑らかに減速するはずです。', scrollLine: '— 滑らかにスクロール', dyLabel: '現在 ΔY', wheelCountLabel: 'イベント数', pinchTitle: '5. ピンチズームと回転', - pinchHint: '青い正方形の上で2本指のピンチイン/ピンチアウト・回転をしてください。リアルタイムに追従すれば、各指が独立してトラッキングされている証拠です。', + pinchHint: '青い正方形の上で2本指のピンチイン/ピンチアウトや回転を行ってください。リアルタイムに追従すれば、各指が独立してトラッキングされている証拠です。', pinchTarget: '2本指', scaleLabel: '拡大率', rotationLabel: '回転', exportTitle: '6. 結果のエクスポート', - exportHint: '5つのチェックを終えたら下のボタンを押してください:結果がクリップボードにコピーされ(JSON もダウンロード)、AI や技術者に貼り付けて判定してもらえます。', - exportBtn: '結果をコピー', exportCopied: 'コピーしました ✓', exportCopyFailed: 'クリップボード失敗 — ダウンロードしました', + exportHint: '5つのチェックを終えたら下のボタンを押してください:結果がクリップボードにコピーされ(JSON もダウンロード)、AI や技術者に共有して判定してもらえます。', + exportBtn: '結果をコピー', exportCopied: 'コピーしました ✓', exportCopyFailed: 'クリップボードへのコピーに失敗 — ダウンロードしました', needSafariTitle: 'Safari が必要です', needSafariForce: '感圧センサーは Safari 専用の webkitForce イベントを使用します。Chrome/Firefox ではこのチェックは実行できません。他のチェックは利用できます。', needSafariPinch: 'ピンチジェスチャーは Safari 専用の GestureEvent を使用します。Chrome/Firefox ではこのチェックは実行できません。他のチェックは利用できます。' diff --git a/tests/i18n.test.ts b/tests/i18n.test.ts index 24c0ed9..fa8ca30 100644 --- a/tests/i18n.test.ts +++ b/tests/i18n.test.ts @@ -1,5 +1,5 @@ -import { strings } from '../src/i18n/strings'; -import { detectLocale, setLocale, getLocale, t, applyI18n } from '../src/i18n'; +import { strings, LOCALES, localeNames } from '../src/i18n/strings'; +import { detectLocale, setLocale, getLocale, t, applyI18n, isLocale } from '../src/i18n'; describe('i18n', () => { it('三份字典 key 完全一致', () => { @@ -8,6 +8,18 @@ describe('i18n', () => { expect(Object.keys(strings.ja).sort()).toEqual(zhKeys); }); + it('LOCALES 為單一事實來源:strings 與 localeNames 完全對齊', () => { + expect(Object.keys(strings).sort()).toEqual([...LOCALES].sort()); + expect(Object.keys(localeNames).sort()).toEqual([...LOCALES].sort()); + }); + + it('isLocale:LOCALES 全 true,其他 false', () => { + for (const l of LOCALES) expect(isLocale(l)).toBe(true); + expect(isLocale('garbage')).toBe(false); + expect(isLocale('ZH-TW')).toBe(false); + expect(isLocale('')).toBe(false); + }); + it('detectLocale:stored 優先、zh 前綴 → zh-TW、ja 前綴 → ja、其他 → en', () => { expect(detectLocale('zh-TW', null)).toBe('zh-TW'); expect(detectLocale('zh-CN', null)).toBe('zh-TW'); diff --git a/tests/main-layout.test.ts b/tests/main-layout.test.ts index ab996fe..6a065bb 100644 --- a/tests/main-layout.test.ts +++ b/tests/main-layout.test.ts @@ -1,5 +1,6 @@ import { renderApp } from '../src/app'; import { Session } from '../src/core/session'; +import { setLocale } from '../src/i18n'; afterEach(() => { document.body.innerHTML = ''; }); @@ -13,6 +14,7 @@ describe('renderApp', () => { const select = root.querySelector('#lang-select') as HTMLSelectElement; expect(select).not.toBeNull(); expect([...select.options].map((o) => o.value)).toEqual(['zh-TW', 'en', 'ja']); + expect([...select.options].map((o) => o.textContent)).toEqual(['繁體中文', 'English', '日本語']); expect(root.querySelectorAll('.notice-card')).toHaveLength(2); expect(session.skipped.sort()).toEqual(['force', 'pinch']); }); @@ -25,6 +27,7 @@ describe('renderApp', () => { }); it('語言選單切換換字(en 與 ja)', () => { + setLocale('zh-TW'); // 固定起點,消除測試順序依賴(verify R1 加固) const root = document.createElement('div'); document.body.appendChild(root); renderApp(root, new Session(), { forceTouch: true, gesture: true, touchDevice: false }); diff --git a/tests/session.test.ts b/tests/session.test.ts index 4aea138..4b7f348 100644 --- a/tests/session.test.ts +++ b/tests/session.test.ts @@ -42,6 +42,11 @@ describe('Session / buildExport', () => { expect(out['手勢']['最後縮放']).toBe(2.28); }); + it('language 欄位跟隨 meta(ja 案例,verify R1 覆蓋缺口)', () => { + const out = buildExport(new Session(), { ...META, language: 'ja' }) as Record; + expect(out['language']).toBe('ja'); + }); + it('skipped 清單:重複標記去重', () => { const s = new Session(); s.markSkipped('force'); s.markSkipped('pinch'); s.markSkipped('force');