diff --git a/js/i18n.js b/js/i18n.js index 8725e92..79b2c2e 100644 --- a/js/i18n.js +++ b/js/i18n.js @@ -5,7 +5,7 @@ class I18nManager { constructor() { - this.currentLang = this.getLanguageFromURL() || 'zh'; + this.currentLang = this.getInitialLanguage(); this.translations = { zh: { title: 'AFX · 支付宝体验技术部 · 引领体验科技,驱动数字生活', @@ -35,6 +35,59 @@ class I18nManager { return urlParams.get('lang'); } + /** + * 规范化语言值 + */ + normalizeLanguage(lang) { + if (!lang) { + return null; + } + + const normalizedLang = lang.toLowerCase(); + + if (normalizedLang.startsWith('zh')) { + return 'zh'; + } + + if (normalizedLang.startsWith('en')) { + return 'en'; + } + + return null; + } + + /** + * 检测浏览器语言 + */ + detectBrowserLanguage() { + const browserLanguages = navigator.languages?.length + ? navigator.languages + : [navigator.language]; + + for (const lang of browserLanguages) { + const normalizedLang = this.normalizeLanguage(lang); + + if (normalizedLang) { + return normalizedLang; + } + } + + return 'en'; + } + + /** + * 获取初始化语言 + */ + getInitialLanguage() { + const urlLanguage = this.getLanguageFromURL(); + + if (urlLanguage !== null) { + return this.normalizeLanguage(urlLanguage) || 'en'; + } + + return this.detectBrowserLanguage(); + } + /** * 更新URL参数 */ diff --git a/tests/aria-label-i18n.test.mjs b/tests/aria-label-i18n.test.mjs index 8b6e336..b6a4583 100644 --- a/tests/aria-label-i18n.test.mjs +++ b/tests/aria-label-i18n.test.mjs @@ -1,6 +1,7 @@ import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import test from 'node:test'; +import vm from 'node:vm'; const html = readFileSync(new URL('../index.html', import.meta.url), 'utf8'); const i18nScript = readFileSync( @@ -27,6 +28,54 @@ function getExternalLinks() { .filter(({ attributes }) => attributes.target === '_blank'); } +function createI18nManager({ search = '', language = 'zh-CN', languages } = {}) { + const context = { + window: { + location: { search }, + history: { replaceState() {} }, + }, + document: { + addEventListener() {}, + getElementById() { + return null; + }, + querySelector() { + return null; + }, + querySelectorAll() { + return []; + }, + dispatchEvent() {}, + documentElement: { lang: 'zh-CN' }, + }, + navigator: { + language, + languages: languages ?? [language], + }, + URL, + URLSearchParams, + CustomEvent: class { + constructor(type, init) { + this.type = type; + this.detail = init?.detail; + } + }, + }; + + vm.createContext(context); + + const i18nScriptWithoutInit = i18nScript.replace( + /\n?\s*new I18nManager\(\);\s*$/, + '' + ); + vm.runInContext( + `${i18nScriptWithoutInit}\nglobalThis.I18nManager = I18nManager;`, + context + ); + + return new context.I18nManager(); +} + test('external links provide localized aria-label values', () => { const externalLinks = getExternalLinks(); @@ -62,3 +111,39 @@ test('language switch updates localized aria-label values', () => { assert.match(i18nScript, /\[data-aria-zh\]\[data-aria-en\]/); assert.match(i18nScript, /setAttribute\(\s*['"]aria-label['"]/); }); + +test('defaults to browser English when no lang query is provided', () => { + const manager = createI18nManager({ language: 'en-US' }); + + assert.equal(manager.currentLang, 'en'); +}); + +test('defaults to browser Chinese when no lang query is provided', () => { + const manager = createI18nManager({ language: 'zh-TW' }); + + assert.equal(manager.currentLang, 'zh'); +}); + +test('valid lang query takes precedence over browser language detection', () => { + const manager = createI18nManager({ + search: '?lang=en', + language: 'zh-CN', + }); + + assert.equal(manager.currentLang, 'en'); +}); + +test('invalid lang query ignores browser locale and falls back to English', () => { + const manager = createI18nManager({ + search: '?lang=fr', + language: 'zh-CN', + }); + + assert.equal(manager.currentLang, 'en'); +}); + +test('unrecognized browser language falls back to English', () => { + const manager = createI18nManager({ language: 'fr-FR', languages: ['fr-FR'] }); + + assert.equal(manager.currentLang, 'en'); +});