import { beforeEach, describe, expect, it } from 'vitest' import { currentLocale, interpolate, loadStrings, message, pluralCategory, setBundle, t, tc, } from '../../src/app/i18n' import { API, mockApi, requestsTo } from '../helpers' import { langFixture } from '../fixtures/typed' beforeEach(() => { setBundle(langFixture.data, 'pl') }) describe('interpolate (parity with phrasebook.interpolate)', () => { it('replaces :name, :Name and :NAME with the plain, first-upper and upper value', () => { expect(interpolate(':name / :Name / :NAME', { name: 'ada' })).toBe('ada / Ada / ADA') }) it('replaces the longer placeholder first so :names never loses to :name', () => { expect(interpolate(':names and :name', { name: 'one', names: 'many' })).toBe('many and one') expect(interpolate(':name and :names', { names: 'many', name: 'one' })).toBe('one and many') }) it('accepts a leading colon on the parameter name and skips an empty name', () => { expect(interpolate('Hello :who', { ':who': 'world' })).toBe('Hello world') expect(interpolate('Hello :', { ':': 'x', '': 'y' })).toBe('Hello :') }) it('upper-cases the first code point, not the first UTF-16 unit', () => { expect(interpolate(':Name', { name: 'łódź' })).toBe('Łódź') expect(interpolate(':Name', { name: '𝒶bc' })).toBe('𝒶bc') }) it('stringifies numbers and leaves text without placeholders or params alone', () => { expect(interpolate(':count items', { count: 3 })).toBe('3 items') expect(interpolate('plain', { name: 'x' })).toBe('plain') expect(interpolate(':name')).toBe(':name') expect(interpolate('', { name: 'x' })).toBe('') }) }) describe('plural selection', () => { it.each([ [1, 'one'], [2, 'few'], [5, 'many'], [22, 'few'], [25, 'many'], ])('pl %i is %s', (count, category) => { expect(pluralCategory(count, 'pl')).toBe(category) }) it.each([ [1, 'one'], [2, 'other'], ])('en %i is %s', (count, category) => { expect(pluralCategory(count, 'en')).toBe(category) }) it('answers other for a locale Intl cannot parse', () => { expect(pluralCategory(1, '!!invalid!!')).toBe('other') }) it('uses the bundle locale by default', () => { setBundle(langFixture.data, 'en') expect(pluralCategory(2)).toBe('other') setBundle(langFixture.data, 'pl') expect(pluralCategory(2)).toBe('few') }) it('picks the form for the count and falls back to other when that form is missing', () => { const forms = { one: ':count plik', other: ':count pliku' } expect(message(forms, 1)).toBe('1 plik') expect(message(forms, 5)).toBe('5 pliku') expect(message(forms, 2, { count: 'dwa' })).toBe('dwa pliku') }) it('renders other without a count and nothing without forms', () => { expect(message({ other: 'Hi :name', one: 'x' }, undefined, { name: 'Ada' })).toBe('Hi Ada') expect(message(null)).toBe('') expect(message(undefined, 3)).toBe('') expect(message({ one: 'only one' }, 5)).toBe('') }) }) describe('bundle lookups', () => { it('t returns the key itself when the key is not in the bundle', () => { expect(t('backend::lang.missing.key')).toBe('backend::lang.missing.key') expect(tc('backend::lang.missing.key', 3)).toBe('backend::lang.missing.key') }) it('t interpolates the other form and tc the plural form', () => { expect(t('backend::lang.form.remove_item', { name: 'Ada' })).toBe('Usuń: Ada') expect(tc('backend::lang.list.results', 1)).toBe('1 wynik') expect(tc('backend::lang.list.results', 3)).toBe('3 wyniki') expect(tc('backend::lang.list.results', 7)).toBe('7 wyników') }) it('setBundle replaces the strings and sets the document language', () => { setBundle({ 'backend::lang.auth.title': { other: 'Welcome back' } }, 'en') expect(t('backend::lang.auth.title')).toBe('Welcome back') expect(t('backend::lang.auth.submit')).toBe('backend::lang.auth.submit') expect(document.documentElement.lang).toBe('en') expect(currentLocale.value).toBe('en') }) it('setBundle without a locale keeps the current locale', () => { setBundle({}, 'pl') setBundle({ a: { other: 'b' } }) expect(currentLocale.value).toBe('pl') expect(t('a')).toBe('b') }) }) describe('loadStrings', () => { it('loads GET /lang and adopts meta.locale', async () => { const calls = mockApi({ [`GET ${API}/lang`]: { body: { data: { 'backend::lang.auth.title': { other: 'Hello' } }, meta: { locale: 'en' } } }, }) await loadStrings() expect(requestsTo(calls, 'GET', `${API}/lang`)).toHaveLength(1) expect(t('backend::lang.auth.title')).toBe('Hello') expect(currentLocale.value).toBe('en') }) it('keeps the current strings when the bundle cannot be loaded', async () => { mockApi({ [`GET ${API}/lang`]: { status: 500, body: { error: { code: 'server', message: 'x', details: {} } } } }) await loadStrings() expect(t('backend::lang.auth.title')).toBe('Witaj ponownie') expect(currentLocale.value).toBe('pl') }) })