Files
summercms/admin/tests/app/i18n.test.ts
Jakub Zych f0ab26b93f test(quick-261016-0fm): theme switch, language switcher and footer offset
- theme: light and dark ignore prefers-color-scheme, choices and guard
- i18n: ?locale= only with a preference, availableLocales from meta
- usePreferences: cache validation, cycle order and PUT bodies, revert
  with a danger toast, profile apply, setLocale save, cache and reload
- rail theme button, user menu language group, --shell-offset and the
  form save bar offset; fixture strings and resetState theme reset
2026-10-16 11:48:39 +02:00

162 lines
5.9 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 {
availableLocales,
currentLocale,
interpolate,
loadStrings,
message,
pluralCategory,
setBundle,
t,
tc,
} from '../../src/app/i18n'
import { API, mockApi, queryOf, 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('sends the preferred locale as ?locale= and keeps meta.locales', async () => {
const calls = mockApi({
[`GET ${API}/lang`]: { body: { data: {}, meta: { locale: 'en', locales: ['en', 'pl'] } } },
})
await loadStrings('en')
const [request] = requestsTo(calls, 'GET', `${API}/lang`)
expect(queryOf(request!).get('locale')).toBe('en')
expect(availableLocales.value).toEqual(['en', 'pl'])
expect(currentLocale.value).toBe('en')
})
it('sends no locale without a preference and follows a shorter meta.locales', async () => {
const calls = mockApi({
[`GET ${API}/lang`]: { body: { data: {}, meta: { locale: 'pl', locales: ['pl'] } } },
})
await loadStrings()
await loadStrings('')
for (const request of requestsTo(calls, 'GET', `${API}/lang`)) {
expect(queryOf(request).has('locale')).toBe(false)
}
expect(availableLocales.value).toEqual(['pl'])
})
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')
})
})