diff --git a/frontend/src/__tests__/i18n.test.ts b/frontend/src/__tests__/i18n.test.ts
new file mode 100644
index 00000000..cac1d1b0
--- /dev/null
+++ b/frontend/src/__tests__/i18n.test.ts
@@ -0,0 +1,59 @@
+import { describe, it, expect, beforeEach } from 'vitest'
+import i18n from '../i18n'
+
+describe('i18n configuration', () => {
+ beforeEach(async () => {
+ await i18n.changeLanguage('en')
+ })
+
+ it('initializes with default language', () => {
+ expect(i18n.language).toBeDefined()
+ expect(i18n.isInitialized).toBe(true)
+ })
+
+ it('has all required language resources', () => {
+ const languages = ['en', 'es', 'fr', 'de', 'zh']
+ languages.forEach((lang) => {
+ expect(i18n.hasResourceBundle(lang, 'translation')).toBe(true)
+ })
+ })
+
+ it('translates common keys', () => {
+ expect(i18n.t('common.save')).toBe('Save')
+ expect(i18n.t('common.cancel')).toBe('Cancel')
+ expect(i18n.t('common.delete')).toBe('Delete')
+ })
+
+ it('translates navigation keys', () => {
+ expect(i18n.t('navigation.dashboard')).toBe('Dashboard')
+ expect(i18n.t('navigation.settings')).toBe('Settings')
+ })
+
+ it('changes language and translates correctly', async () => {
+ await i18n.changeLanguage('es')
+ expect(i18n.t('common.save')).toBe('Guardar')
+ expect(i18n.t('common.cancel')).toBe('Cancelar')
+
+ await i18n.changeLanguage('fr')
+ expect(i18n.t('common.save')).toBe('Enregistrer')
+ expect(i18n.t('common.cancel')).toBe('Annuler')
+
+ await i18n.changeLanguage('de')
+ expect(i18n.t('common.save')).toBe('Speichern')
+ expect(i18n.t('common.cancel')).toBe('Abbrechen')
+
+ await i18n.changeLanguage('zh')
+ expect(i18n.t('common.save')).toBe('保存')
+ expect(i18n.t('common.cancel')).toBe('取消')
+ })
+
+ it('falls back to English for missing translations', async () => {
+ await i18n.changeLanguage('en')
+ const key = 'nonexistent.key'
+ expect(i18n.t(key)).toBe(key) // Should return the key itself
+ })
+
+ it('supports interpolation', () => {
+ expect(i18n.t('dashboard.activeHosts', { count: 5 })).toBe('5 active')
+ })
+})
diff --git a/frontend/src/components/__tests__/LanguageSelector.test.tsx b/frontend/src/components/__tests__/LanguageSelector.test.tsx
new file mode 100644
index 00000000..26b3209c
--- /dev/null
+++ b/frontend/src/components/__tests__/LanguageSelector.test.tsx
@@ -0,0 +1,60 @@
+import { describe, it, expect, vi } from 'vitest'
+import { render, screen, fireEvent } from '@testing-library/react'
+import { LanguageSelector } from '../LanguageSelector'
+import { LanguageProvider } from '../../context/LanguageContext'
+
+// Mock i18next
+vi.mock('react-i18next', () => ({
+ useTranslation: () => ({
+ t: (key: string) => key,
+ i18n: {
+ changeLanguage: vi.fn(),
+ language: 'en',
+ },
+ }),
+}))
+
+describe('LanguageSelector', () => {
+ const renderWithProvider = () => {
+ return render(
+
+
+
+ )
+ }
+
+ it('renders language selector with all options', () => {
+ renderWithProvider()
+
+ const select = screen.getByRole('combobox')
+ expect(select).toBeInTheDocument()
+
+ // Check that all language options are available
+ const options = screen.getAllByRole('option')
+ expect(options).toHaveLength(5)
+ expect(options[0]).toHaveTextContent('English')
+ expect(options[1]).toHaveTextContent('Español')
+ expect(options[2]).toHaveTextContent('Français')
+ expect(options[3]).toHaveTextContent('Deutsch')
+ expect(options[4]).toHaveTextContent('中文')
+ })
+
+ it('displays globe icon', () => {
+ const { container } = renderWithProvider()
+ const svgElement = container.querySelector('svg')
+ expect(svgElement).toBeInTheDocument()
+ })
+
+ it('changes language when option is selected', () => {
+ renderWithProvider()
+
+ const select = screen.getByRole('combobox') as HTMLSelectElement
+ expect(select.value).toBe('en')
+
+ fireEvent.change(select, { target: { value: 'es' } })
+ expect(select.value).toBe('es')
+
+ fireEvent.change(select, { target: { value: 'fr' } })
+ expect(select.value).toBe('fr')
+ })
+})
diff --git a/frontend/src/hooks/__tests__/useLanguage.test.tsx b/frontend/src/hooks/__tests__/useLanguage.test.tsx
new file mode 100644
index 00000000..ff776965
--- /dev/null
+++ b/frontend/src/hooks/__tests__/useLanguage.test.tsx
@@ -0,0 +1,89 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest'
+import { renderHook, act } from '@testing-library/react'
+import { ReactNode } from 'react'
+import { useLanguage } from '../useLanguage'
+import { LanguageProvider } from '../../context/LanguageContext'
+
+// Mock i18next
+vi.mock('react-i18next', () => ({
+ useTranslation: () => ({
+ t: (key: string) => key,
+ i18n: {
+ changeLanguage: vi.fn(),
+ language: 'en',
+ },
+ }),
+}))
+
+describe('useLanguage', () => {
+ beforeEach(() => {
+ localStorage.clear()
+ vi.clearAllMocks()
+ })
+
+ it('throws error when used outside LanguageProvider', () => {
+ // Suppress console.error for this test as React logs the error
+ const consoleSpy = vi.spyOn(console, 'error')
+ consoleSpy.mockImplementation(() => {})
+
+ expect(() => {
+ renderHook(() => useLanguage())
+ }).toThrow('useLanguage must be used within a LanguageProvider')
+
+ consoleSpy.mockRestore()
+ })
+
+ it('provides default language', () => {
+ const wrapper = ({ children }: { children: ReactNode }) => (
+ {children}
+ )
+
+ const { result } = renderHook(() => useLanguage(), { wrapper })
+
+ expect(result.current.language).toBe('en')
+ })
+
+ it('changes language', () => {
+ const wrapper = ({ children }: { children: ReactNode }) => (
+ {children}
+ )
+
+ const { result } = renderHook(() => useLanguage(), { wrapper })
+
+ act(() => {
+ result.current.setLanguage('es')
+ })
+
+ expect(result.current.language).toBe('es')
+ expect(localStorage.getItem('charon-language')).toBe('es')
+ })
+
+ it('persists language selection', () => {
+ localStorage.setItem('charon-language', 'fr')
+
+ const wrapper = ({ children }: { children: ReactNode }) => (
+ {children}
+ )
+
+ const { result } = renderHook(() => useLanguage(), { wrapper })
+
+ expect(result.current.language).toBe('fr')
+ })
+
+ it('supports all configured languages', () => {
+ const wrapper = ({ children }: { children: ReactNode }) => (
+ {children}
+ )
+
+ const { result } = renderHook(() => useLanguage(), { wrapper })
+
+ const languages = ['en', 'es', 'fr', 'de', 'zh'] as const
+
+ languages.forEach((lang) => {
+ act(() => {
+ result.current.setLanguage(lang)
+ })
+ expect(result.current.language).toBe(lang)
+ })
+ })
+})