mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 15:51:00 +00:00
70f27085b9
Translate the 55 keys dev added since the branch was cut (TREK services allowance, Valhalla routing service, PDF routing progress, overview distances, Unsplash key hint, tour day limit) and drop the two reservation toasts dev removed, so he matches en key for key again. Refresh the two admin strings and the Valhalla hint whose English changed. Check he in the placeholder spec and the RTL test, and count 28 languages with Hebrew in the README and the Languages, Environment Variables, Contributing and Help Center wiki pages.
270 lines
10 KiB
TypeScript
270 lines
10 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
import { render } from '@testing-library/react'
|
|
import React from 'react'
|
|
import {
|
|
TranslationProvider,
|
|
useTranslation,
|
|
getLocaleForLanguage,
|
|
getIntlLanguage,
|
|
isRtlLanguage,
|
|
SUPPORTED_LANGUAGES,
|
|
detectBrowserLanguage,
|
|
} from '../../../src/i18n'
|
|
import { resetAllStores, seedStore } from '../../helpers/store'
|
|
import { useSettingsStore } from '../../../src/store/settingsStore'
|
|
import { buildSettings } from '../../helpers/factories'
|
|
|
|
beforeEach(() => {
|
|
resetAllStores()
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
// ── FE-COMP-I18N-001: Barrel re-exports ───────────────────────────────────────
|
|
|
|
describe('barrel re-exports', () => {
|
|
it('FE-COMP-I18N-001: all named exports are defined with expected types', () => {
|
|
expect(TranslationProvider).toBeDefined()
|
|
expect(typeof TranslationProvider).toBe('function')
|
|
expect(useTranslation).toBeDefined()
|
|
expect(typeof useTranslation).toBe('function')
|
|
expect(getLocaleForLanguage).toBeDefined()
|
|
expect(typeof getLocaleForLanguage).toBe('function')
|
|
expect(getIntlLanguage).toBeDefined()
|
|
expect(typeof getIntlLanguage).toBe('function')
|
|
expect(isRtlLanguage).toBeDefined()
|
|
expect(typeof isRtlLanguage).toBe('function')
|
|
expect(SUPPORTED_LANGUAGES).toBeDefined()
|
|
expect(Array.isArray(SUPPORTED_LANGUAGES)).toBe(true)
|
|
})
|
|
})
|
|
|
|
// ── FE-COMP-I18N-002/003: getLocaleForLanguage ────────────────────────────────
|
|
|
|
describe('getLocaleForLanguage', () => {
|
|
it('FE-COMP-I18N-002: returns correct locale for known languages', () => {
|
|
expect(getLocaleForLanguage('en')).toBe('en-US')
|
|
expect(getLocaleForLanguage('de')).toBe('de-DE')
|
|
expect(getLocaleForLanguage('zh-TW')).toBe('zh-TW')
|
|
expect(getLocaleForLanguage('ar')).toBe('ar-SA')
|
|
expect(getLocaleForLanguage('br')).toBe('pt-BR')
|
|
})
|
|
|
|
it('FE-COMP-I18N-003: falls back to en-US for unknown language codes', () => {
|
|
expect(getLocaleForLanguage('xx')).toBe('en-US')
|
|
})
|
|
})
|
|
|
|
// ── FE-COMP-I18N-004/005/006: getIntlLanguage ─────────────────────────────────
|
|
|
|
describe('getIntlLanguage', () => {
|
|
it('FE-COMP-I18N-004: returns language code for known supported languages', () => {
|
|
expect(getIntlLanguage('de')).toBe('de')
|
|
expect(getIntlLanguage('fr')).toBe('fr')
|
|
expect(getIntlLanguage('zh-TW')).toBe('zh-TW')
|
|
})
|
|
|
|
it('FE-COMP-I18N-005: maps br to pt-BR', () => {
|
|
expect(getIntlLanguage('br')).toBe('pt-BR')
|
|
})
|
|
|
|
it('FE-COMP-I18N-006: falls back to en for unknown codes', () => {
|
|
expect(getIntlLanguage('xx')).toBe('en')
|
|
})
|
|
})
|
|
|
|
// ── FE-COMP-I18N-007/008: isRtlLanguage ──────────────────────────────────────
|
|
|
|
describe('isRtlLanguage', () => {
|
|
it('FE-COMP-I18N-007: returns true for Arabic and Hebrew', () => {
|
|
expect(isRtlLanguage('ar')).toBe(true)
|
|
expect(isRtlLanguage('he')).toBe(true)
|
|
})
|
|
|
|
it('FE-COMP-I18N-008: returns false for all other supported languages', () => {
|
|
expect(isRtlLanguage('en')).toBe(false)
|
|
expect(isRtlLanguage('de')).toBe(false)
|
|
expect(isRtlLanguage('zh-TW')).toBe(false)
|
|
})
|
|
})
|
|
|
|
// ── FE-COMP-I18N-009: SUPPORTED_LANGUAGES ────────────────────────────────────
|
|
|
|
describe('SUPPORTED_LANGUAGES', () => {
|
|
it('FE-COMP-I18N-009: contains expected entries with value/label shape', () => {
|
|
expect(Array.isArray(SUPPORTED_LANGUAGES)).toBe(true)
|
|
expect(new Set(SUPPORTED_LANGUAGES.map(l => l.value)).size).toBe(SUPPORTED_LANGUAGES.length)
|
|
expect(SUPPORTED_LANGUAGES).toContainEqual(expect.objectContaining({ value: 'en', label: 'English' }))
|
|
expect(SUPPORTED_LANGUAGES).toContainEqual(expect.objectContaining({ value: 'tr', label: 'Türkçe' }))
|
|
expect(SUPPORTED_LANGUAGES).toContainEqual(expect.objectContaining({ value: 'ja', label: '日本語' }))
|
|
expect(SUPPORTED_LANGUAGES).toContainEqual(expect.objectContaining({ value: 'ko', label: '한국어' }))
|
|
expect(SUPPORTED_LANGUAGES).toContainEqual(expect.objectContaining({ value: 'uk', label: 'Українська' }))
|
|
expect(SUPPORTED_LANGUAGES).toContainEqual(expect.objectContaining({ value: 'sv', label: 'Svenska' }))
|
|
expect(SUPPORTED_LANGUAGES).toContainEqual(expect.objectContaining({ value: 'ar', label: 'العربية' }))
|
|
expect(SUPPORTED_LANGUAGES).toContainEqual(expect.objectContaining({ value: 'vi', label: 'Tiếng Việt' }))
|
|
expect(SUPPORTED_LANGUAGES).toContainEqual(expect.objectContaining({ value: 'ca', label: 'Català' }))
|
|
})
|
|
})
|
|
|
|
// ── FE-COMP-I18N-016 to 023: detectBrowserLanguage ───────────────────────────
|
|
|
|
describe('detectBrowserLanguage', () => {
|
|
afterEach(() => {
|
|
Object.defineProperty(navigator, 'languages', { value: [], configurable: true })
|
|
Object.defineProperty(navigator, 'language', { value: '', configurable: true })
|
|
})
|
|
|
|
it('FE-COMP-I18N-016: exact match returns the matched code', () => {
|
|
Object.defineProperty(navigator, 'languages', { value: ['de'], configurable: true })
|
|
expect(detectBrowserLanguage()).toBe('de')
|
|
})
|
|
|
|
it('FE-COMP-I18N-017: region-tagged exact match (zh-TW) returns zh-TW', () => {
|
|
Object.defineProperty(navigator, 'languages', { value: ['zh-TW'], configurable: true })
|
|
expect(detectBrowserLanguage()).toBe('zh-TW')
|
|
})
|
|
|
|
it('FE-COMP-I18N-018: prefix match (de-AT → de)', () => {
|
|
Object.defineProperty(navigator, 'languages', { value: ['de-AT'], configurable: true })
|
|
expect(detectBrowserLanguage()).toBe('de')
|
|
})
|
|
|
|
it('FE-COMP-I18N-019: pt-PT returns null (European Portuguese is a distinct language)', () => {
|
|
Object.defineProperty(navigator, 'languages', { value: ['pt-PT'], configurable: true })
|
|
expect(detectBrowserLanguage()).toBeNull()
|
|
})
|
|
|
|
it('FE-COMP-I18N-020: pt-BR maps to br', () => {
|
|
Object.defineProperty(navigator, 'languages', { value: ['pt-BR'], configurable: true })
|
|
expect(detectBrowserLanguage()).toBe('br')
|
|
})
|
|
|
|
it('FE-COMP-I18N-021: first-match-wins across multiple entries', () => {
|
|
Object.defineProperty(navigator, 'languages', { value: ['xx-XX', 'fr'], configurable: true })
|
|
expect(detectBrowserLanguage()).toBe('fr')
|
|
})
|
|
|
|
it('FE-COMP-I18N-022: unknown language returns null', () => {
|
|
Object.defineProperty(navigator, 'languages', { value: ['xx'], configurable: true })
|
|
expect(detectBrowserLanguage()).toBeNull()
|
|
})
|
|
|
|
it('FE-COMP-I18N-023: falls back to navigator.language when navigator.languages is empty', () => {
|
|
Object.defineProperty(navigator, 'languages', { value: [], configurable: true })
|
|
Object.defineProperty(navigator, 'language', { value: 'es', configurable: true })
|
|
expect(detectBrowserLanguage()).toBe('es')
|
|
})
|
|
})
|
|
|
|
// ── FE-COMP-I18N-010 to 015: TranslationProvider + useTranslation ─────────────
|
|
|
|
describe('TranslationProvider + useTranslation integration', () => {
|
|
it('FE-COMP-I18N-010: useTranslation returns t, language, and locale', () => {
|
|
seedStore(useSettingsStore, { settings: buildSettings({ language: 'en' }) })
|
|
|
|
let result: { language: string; locale: string; tResult: string } | null = null
|
|
|
|
function TestComponent() {
|
|
const { t, language, locale } = useTranslation()
|
|
result = { language, locale, tResult: t('common.loading') }
|
|
return null
|
|
}
|
|
|
|
render(
|
|
React.createElement(TranslationProvider, null, React.createElement(TestComponent))
|
|
)
|
|
|
|
expect(result).not.toBeNull()
|
|
expect(result!.language).toBe('en')
|
|
expect(result!.locale).toBe('en-US')
|
|
expect(result!.tResult).toBeTruthy()
|
|
expect(typeof result!.tResult).toBe('string')
|
|
})
|
|
|
|
it('FE-COMP-I18N-011: t() with params substitutes {count} placeholders', () => {
|
|
seedStore(useSettingsStore, { settings: buildSettings({ language: 'en' }) })
|
|
|
|
let translated = ''
|
|
|
|
function TestComponent() {
|
|
const { t } = useTranslation()
|
|
translated = t('dashboard.subtitle.trips', { count: 5, archived: 2 })
|
|
return null
|
|
}
|
|
|
|
render(
|
|
React.createElement(TranslationProvider, null, React.createElement(TestComponent))
|
|
)
|
|
|
|
expect(translated).toContain('5')
|
|
expect(translated).toContain('2')
|
|
expect(translated).not.toContain('{count}')
|
|
expect(translated).not.toContain('{archived}')
|
|
})
|
|
|
|
it('FE-COMP-I18N-012: TranslationProvider sets document.documentElement.lang', () => {
|
|
seedStore(useSettingsStore, { settings: buildSettings({ language: 'de' }) })
|
|
|
|
function TestComponent() {
|
|
useTranslation()
|
|
return null
|
|
}
|
|
|
|
render(
|
|
React.createElement(TranslationProvider, null, React.createElement(TestComponent))
|
|
)
|
|
|
|
expect(document.documentElement.lang).toBe('de')
|
|
})
|
|
|
|
it('FE-COMP-I18N-013: TranslationProvider sets dir=rtl for Arabic', () => {
|
|
seedStore(useSettingsStore, { settings: buildSettings({ language: 'ar' }) })
|
|
|
|
function TestComponent() {
|
|
useTranslation()
|
|
return null
|
|
}
|
|
|
|
render(
|
|
React.createElement(TranslationProvider, null, React.createElement(TestComponent))
|
|
)
|
|
|
|
expect(document.documentElement.dir).toBe('rtl')
|
|
})
|
|
|
|
it('FE-COMP-I18N-014: TranslationProvider sets dir=ltr for non-RTL language', () => {
|
|
seedStore(useSettingsStore, { settings: buildSettings({ language: 'en' }) })
|
|
|
|
function TestComponent() {
|
|
useTranslation()
|
|
return null
|
|
}
|
|
|
|
render(
|
|
React.createElement(TranslationProvider, null, React.createElement(TestComponent))
|
|
)
|
|
|
|
expect(document.documentElement.dir).toBe('ltr')
|
|
})
|
|
|
|
it('FE-COMP-I18N-015: t() falls back to English for unknown language', () => {
|
|
// Seed with a non-existent language to trigger fallback to English translations
|
|
seedStore(useSettingsStore, { settings: buildSettings({ language: 'xx' as any }) })
|
|
|
|
let translated = ''
|
|
|
|
function TestComponent() {
|
|
const { t } = useTranslation()
|
|
translated = t('common.loading')
|
|
return null
|
|
}
|
|
|
|
render(
|
|
React.createElement(TranslationProvider, null, React.createElement(TestComponent))
|
|
)
|
|
|
|
// Should fall back to English translation (non-empty, not the key itself if key exists in en)
|
|
expect(typeof translated).toBe('string')
|
|
expect(translated.length).toBeGreaterThan(0)
|
|
})
|
|
})
|