feat(client): add Traditional Chinese (zh-TW) language support (#612)

* feat(client): add Traditional Chinese (zh-TW) language support

Update language switcher and i18n configuration to include Traditional Chinese.
Modify locale resolution to prioritize exact matches (zh-TW) over base codes (zh),
ensuring accurate browser language detection. Add complete translation set.

Co-authored-by: Copilot <copilot@github.com>

* feat(i18n): improve BCP-47 locale resolution and browser languages

Refactor resolveLocale to use a normalize helper that correctly maps BCP-47 tags to supported locale keys, properly distinguishing Traditional vs Simplified Chinese variants. Additionally, iterate over navigator.languages instead of relying solely on navigator.language to respect the browser's full language preference list.

Co-authored-by: Copilot <copilot@github.com>

---------

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Weil Jimmer
2026-05-11 21:22:57 +08:00
committed by GitHub
parent 9c5fe1ec7d
commit c6fb449a19
4 changed files with 1184 additions and 15 deletions
@@ -5,7 +5,8 @@ import { NSelect } from 'naive-ui'
const { locale } = useI18n()
const options = [
{ label: '中文', value: 'zh' },
{ label: '简体中文', value: 'zh' },
{ label: '繁體中文', value: 'zh-TW' },
{ label: 'English', value: 'en' },
{ label: '日本語', value: 'ja' },
{ label: '한국어', value: 'ko' },
+27 -6
View File
@@ -2,24 +2,45 @@ import { createI18n } from 'vue-i18n'
import { messages } from './messages'
const saved = localStorage.getItem('hermes_locale')
const detected = navigator.language.slice(0, 2)
const supportedLocales = ['en', 'zh', 'ja', 'ko', 'fr', 'es', 'de', 'pt'] as const
const supportedLocales = ['en', 'zh', 'zh-TW', 'ja', 'ko', 'fr', 'es', 'de', 'pt'] as const
type SupportedLocale = (typeof supportedLocales)[number]
function resolveLocale(saved: string | null, detected: string): SupportedLocale {
function resolveLocale(saved: string | null): SupportedLocale {
if (saved && (supportedLocales as readonly string[]).includes(saved)) {
return saved as SupportedLocale
}
if ((supportedLocales as readonly string[]).includes(detected)) {
return detected as SupportedLocale
// Normalize a single BCP-47 tag to a supported locale key.
// Covers zh-Hant-TW, zh-TW, zh-HK, zh-MO, zh-Hant → zh-TW
// zh-Hans-*, zh-CN, zh-SG, zh → zh
function normalize(tag: string): SupportedLocale | null {
const lower = tag.toLowerCase()
if (lower.startsWith('zh')) {
const isTraditional =
lower.includes('hant') ||
lower.includes('-tw') ||
lower.includes('-hk') ||
lower.includes('-mo')
return isTraditional ? 'zh-TW' : 'zh'
}
const short = tag.slice(0, 2)
if ((supportedLocales as readonly string[]).includes(tag)) return tag as SupportedLocale
if ((supportedLocales as readonly string[]).includes(short)) return short as SupportedLocale
return null
}
for (const lang of navigator.languages) {
const resolved = normalize(lang)
if (resolved) return resolved
}
return 'en'
}
export const i18n = createI18n({
legacy: false,
locale: resolveLocale(saved, detected),
locale: resolveLocale(saved),
fallbackLocale: 'en',
messages,
})
File diff suppressed because it is too large Load Diff
+10 -8
View File
@@ -6,18 +6,20 @@ import ja from './locales/ja'
import ko from './locales/ko'
import pt from './locales/pt'
import zh from './locales/zh'
import zhTW from './locales/zh-TW'
export type LocaleMessages = Record<string, unknown>
export const rawMessages = {
en,
zh,
ja,
ko,
fr,
es,
de,
pt,
'en': en,
'zh': zh,
'zh-TW': zhTW,
'ja': ja,
'ko': ko,
'fr': fr,
'es': es,
'de': de,
'pt': pt,
} satisfies Record<string, LocaleMessages>
function isPlainObject(value: unknown): value is LocaleMessages {