From 0d4f149e8afaa7cca3e640f19f58f49bf92985e6 Mon Sep 17 00:00:00 2001 From: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> Date: Thu, 17 Sep 2026 14:50:53 +0800 Subject: [PATCH] fix(web): align language switcher fallback --- .../components/language-switcher.test.ts | 12 ---- .../components/language-switcher.test.tsx | 64 +++++++++++++++++++ .../shared/components/language-switcher.tsx | 5 +- 3 files changed, 67 insertions(+), 14 deletions(-) delete mode 100644 web/src/shared/components/language-switcher.test.ts create mode 100644 web/src/shared/components/language-switcher.test.tsx diff --git a/web/src/shared/components/language-switcher.test.ts b/web/src/shared/components/language-switcher.test.ts deleted file mode 100644 index 6408939c..00000000 --- a/web/src/shared/components/language-switcher.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { describe, expect, it } from 'vitest' -import * as mod from './language-switcher' - -/** - * LanguageSwitcher renders an in-tree locale menu (no Radix portal) driven by i18next. - * There are no exported pure helpers; verify the export contract only. - */ -describe('language-switcher module exports', () => { - it('exports the LanguageSwitcher component', () => { - expect(mod.LanguageSwitcher).toBeTypeOf('function') - }) -}) diff --git a/web/src/shared/components/language-switcher.test.tsx b/web/src/shared/components/language-switcher.test.tsx new file mode 100644 index 00000000..90862592 --- /dev/null +++ b/web/src/shared/components/language-switcher.test.tsx @@ -0,0 +1,64 @@ +// @vitest-environment jsdom + +import { cleanup, fireEvent, render, screen } from '@testing-library/react' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { LanguageSwitcher } from './language-switcher' + +const mockI18n = vi.hoisted(() => ({ + language: undefined as string | undefined, + resolvedLanguage: undefined as string | undefined, + changeLanguage: vi.fn(), +})) + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ i18n: mockI18n }), +})) + +describe('LanguageSwitcher', () => { + beforeEach(() => { + mockI18n.language = undefined + mockI18n.resolvedLanguage = undefined + mockI18n.changeLanguage.mockImplementation((language: string) => { + mockI18n.language = language + mockI18n.resolvedLanguage = language + return Promise.resolve() + }) + }) + + afterEach(() => { + cleanup() + vi.clearAllMocks() + }) + + it('exports the LanguageSwitcher component', () => { + expect(LanguageSwitcher).toBeTypeOf('function') + }) + + it('falls back to English when i18next has not resolved a language yet', () => { + render() + + expect(screen.getByRole('button', { name: /English/i })).toBeTruthy() + }) + + it('uses the resolved language before the raw detected language', () => { + mockI18n.language = 'zh-CN' + mockI18n.resolvedLanguage = 'en' + + render() + + expect(screen.getByRole('button', { name: /English/i })).toBeTruthy() + }) + + it('updates the displayed language after selecting English', () => { + mockI18n.language = 'zh' + mockI18n.resolvedLanguage = 'zh' + + render() + + fireEvent.click(screen.getByRole('button', { name: /中文/i })) + fireEvent.click(screen.getByRole('menuitem', { name: /English/i })) + + expect(mockI18n.changeLanguage).toHaveBeenCalledWith('en') + expect(screen.getByRole('button', { name: /English/i })).toBeTruthy() + }) +}) diff --git a/web/src/shared/components/language-switcher.tsx b/web/src/shared/components/language-switcher.tsx index 1b5756c9..1a298eec 100644 --- a/web/src/shared/components/language-switcher.tsx +++ b/web/src/shared/components/language-switcher.tsx @@ -27,9 +27,10 @@ export function LanguageSwitcher({ className }: LanguageSwitcherProps) { { code: 'ru', name: 'Русский' }, ] + const fallbackLanguage = languages.find((lang) => lang.code === 'en') ?? languages[0] // Primary language code only (strip region, e.g. ru-RU → ru). - const currentLangCode = i18n.language?.split('-')[0] || 'zh' - const currentLanguage = languages.find((lang) => lang.code === currentLangCode) || languages[0] + const currentLangCode = (i18n.resolvedLanguage || i18n.language || fallbackLanguage.code).split('-')[0] + const currentLanguage = languages.find((lang) => lang.code === currentLangCode) || fallbackLanguage useEffect(() => { if (!open) {