Files
summercms/admin/tests/app/i18n.test.ts
Jakub Zych 1c2a66df45 test(10-05): bring every SPA module, composable and component under Vitest
- 41 unit and component suites under admin/tests/{app,state,shell,list,form,relation,views,ui}
  covering states and a11y roles; every src module is imported by a test
- typed fixture helper assigns each JSON fixture to its generated OpenAPI type
- fix: iconFor ignores inherited object members such as "constructor"
- fix: field controls import ./control instead of the registry (import cycle
  left a renderer unregistered depending on module load order)
- fix: dropdown shows the placeholder for an unknown stored value next to an emptyOption
- fix: list announces a failed schema load even when the rows arrive after it
- tailwind no longer scans admin/tests; boardwalk/dist rebuilt
2026-09-27 17:53:57 +02:00

138 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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')
})
})