// FE-COMP-I18NCTX-001 to FE-COMP-I18NCTX-018 (plus -003b / -013b) import { act, render, screen, waitFor } from '@testing-library/react'; import { SUPPORTED_LANGUAGES, TranslationProvider, useTranslation } from '../../../src/i18n/TranslationContext'; import { TransHtml } from '../../../src/i18n/TransHtml'; import { useSettingsStore } from '../../../src/store/settingsStore'; import { resetAllStores, seedStore } from '../../helpers/store'; // The provider swaps the whole string table via a dynamic import per locale, so // these tests assert on what the consumer actually sees rather than on the // loader plumbing: English is synchronous, everything else arrives one tick later. function Probe({ tKey, params }: { tKey: string; params?: Record }) { const { t, language, locale } = useTranslation(); return (
{t(tKey, params)} {language} {locale}
); } function HtmlProbe({ tKey, params }: { tKey: string; params?: Record }) { const { tHtml } = useTranslation(); return {tHtml(tKey, params)}; } function withLanguage(language: string) { seedStore(useSettingsStore, { settings: { language } }); } beforeEach(() => { resetAllStores(); document.documentElement.lang = ''; document.documentElement.dir = ''; }); describe('TranslationProvider', () => { it('FE-COMP-I18NCTX-001: resolves a key from the English table', () => { render(); expect(screen.getByTestId('value').textContent).toBe('Save'); }); it('FE-COMP-I18NCTX-002: returns the key itself when nothing matches', () => { render(); expect(screen.getByTestId('value').textContent).toBe('does.not.exist'); }); it('FE-COMP-I18NCTX-003: substitutes every occurrence of a placeholder', () => { render(); expect(screen.getByTestId('value').textContent).toContain('5'); }); it('FE-COMP-I18NCTX-003b: inserts a value containing $ patterns literally', () => { render( , ); // A trip really can be named this, and `$&` as a replacement string would // paste the matched placeholder back in. expect(screen.getByTestId('value').textContent).toContain('A$&B $1 $`'); }); it('FE-COMP-I18NCTX-004: defaults to English with the matching locale', () => { render(); expect(screen.getByTestId('language').textContent).toBe('en'); expect(screen.getByTestId('locale').textContent).toBe('en-US'); }); it('FE-COMP-I18NCTX-005: loads another locale and exposes its language and locale', async () => { withLanguage('de'); render(); expect(screen.getByTestId('language').textContent).toBe('de'); await waitFor(() => expect(screen.getByTestId('value').textContent).toBe('Speichern')); expect(screen.getByTestId('locale').textContent).toBe('de-DE'); }); it('FE-COMP-I18NCTX-006: falls back to the English string for a key a locale is missing', async () => { withLanguage('de'); render(); await waitFor(() => expect(screen.getByTestId('value').textContent).toBe('does.not.exist')); }); it('FE-COMP-I18NCTX-007: writes lang and ltr direction onto the html element', () => { withLanguage('de'); render(); expect(document.documentElement.lang).toBe('de'); expect(document.documentElement.dir).toBe('ltr'); }); it('FE-COMP-I18NCTX-008: switches the html element to rtl for Arabic', async () => { withLanguage('ar'); render(); expect(document.documentElement.lang).toBe('ar'); expect(document.documentElement.dir).toBe('rtl'); await waitFor(() => expect(screen.getByTestId('value').textContent).not.toBe('Save')); }); it('FE-COMP-I18NCTX-009: keeps the English table for an unknown language code', () => { withLanguage('klingon'); render(); expect(screen.getByTestId('value').textContent).toBe('Save'); expect(screen.getByTestId('language').textContent).toBe('klingon'); }); it('FE-COMP-I18NCTX-010: treats an empty language setting as English', () => { withLanguage(''); render(); expect(screen.getByTestId('language').textContent).toBe('en'); }); it('FE-COMP-I18NCTX-011: a locale arriving after a second switch does not win', async () => { withLanguage('de'); render(); // Switch back before the German chunk lands — the cancelled loader must not // overwrite the English table when its promise finally resolves. act(() => withLanguage('en')); await act(async () => { await Promise.resolve(); }); expect(screen.getByTestId('language').textContent).toBe('en'); expect(screen.getByTestId('value').textContent).toBe('Save'); }); it('FE-COMP-I18NCTX-011a: every supported language resolves through its own chunk', async () => { for (const { value } of SUPPORTED_LANGUAGES) { withLanguage(value); const view = render(); await act(async () => { await Promise.resolve(); }); expect(screen.getByTestId('language').textContent).toBe(value); // Whatever the locale ships, the key must resolve to something other than itself. expect(screen.getByTestId('value').textContent).not.toBe('common.save'); view.unmount(); } }); it('FE-COMP-I18NCTX-018: plural forms follow the language of the strings on screen, not the one still loading', async () => { withLanguage('ru'); render(); // English strings until the Russian chunk arrives, read with the English // rule: 21 is "other" there, while Russian's rule would pick English's .one. expect(screen.getByTestId('value').textContent).toBe('21 places'); await waitFor(() => expect(screen.getByTestId('value').textContent).toBe('21 место')); }); it('FE-COMP-I18NCTX-012: the default context outside a provider echoes the key', () => { render(); expect(screen.getByTestId('value').textContent).toBe('common.save'); expect(screen.getByTestId('locale').textContent).toBe('en-US'); }); }); describe('tHtml', () => { it('FE-COMP-I18NCTX-013: escapes an interpolated value before substitution', () => { render( ' }} /> , ); const raw = screen.getByTestId('raw').textContent ?? ''; expect(raw).not.toContain(' { render( , ); expect(screen.getByTestId('raw').textContent).toContain('A$&B'); }); it('FE-COMP-I18NCTX-014: returns the key unchanged when it is unknown', () => { render(); expect(screen.getByTestId('raw').textContent).toBe('nope.nope'); }); }); describe('TransHtml', () => { it('FE-COMP-I18NCTX-015: renders as a span by default and keeps allowed inline markup', () => { const { container } = render( , ); const el = container.firstElementChild!; expect(el.tagName).toBe('SPAN'); expect(el.textContent).toContain('Japan 2026'); expect(el.querySelector('script')).toBeNull(); }); it('FE-COMP-I18NCTX-016: honours the as, className and id props', () => { const { container } = render( , ); const el = container.firstElementChild!; expect(el.tagName).toBe('P'); expect(el).toHaveClass('text-body'); expect(el.id).toBe('banner'); }); it('FE-COMP-I18NCTX-017: neutralises a script tag smuggled in through a param', () => { const { container } = render( alert(1)' }} /> , ); expect(container.querySelector('script')).toBeNull(); expect(container.textContent).toContain('alert(1)'); }); });