import { describe, it, expect, afterEach, beforeEach, vi } from 'vitest'
import { nextTick } from 'vue'
import {
LOCALE_STORAGE_KEY,
SUPPORTED_LOCALES,
locale,
normalizeTag,
pluralCategory,
rememberLocale,
setLocale,
storedLocale,
t,
} from '@/i18n/index.js'
import en from '@/i18n/messages/en.js'
import ru from '@/i18n/messages/ru.js'
import uk from '@/i18n/messages/uk.js'
// A slug added to one dictionary and forgotten in another falls back to
// English silently, so the screen just comes out half-translated. Compare the
// key sets instead of trusting everyone to remember.
describe('the dictionaries', () => {
it('carry the same slugs', () => {
const reference = Object.keys(en).sort()
expect(Object.keys(uk).sort()).toEqual(reference)
expect(Object.keys(ru).sort()).toEqual(reference)
})
})
describe('language tags', () => {
it('ships exactly the three languages of the handbook', () => {
expect(SUPPORTED_LOCALES).toEqual(['en', 'uk', 'ru'])
})
it('reads the language prefix out of a tag', () => {
expect(normalizeTag('en-US')).toBe('en')
expect(normalizeTag('uk_UA')).toBe('uk')
expect(normalizeTag(' RU ')).toBe('ru')
})
it('refuses a language it has no dictionary for', () => {
expect(normalizeTag('de')).toBeNull()
expect(normalizeTag('')).toBeNull()
expect(normalizeTag(null)).toBeNull()
})
it('falls back to English for a tag it cannot use', () => {
setLocale('de')
expect(locale.value).toBe('en')
})
it('follows the chosen language into the document', async () => {
setLocale('uk')
await nextTick()
expect(document.documentElement.lang).toBe('uk')
setLocale('en')
await nextTick()
expect(document.documentElement.lang).toBe('en')
})
})
describe('translating', () => {
beforeEach(() => setLocale('en'))
it('answers in the chosen language', () => {
setLocale('uk')
expect(t('toast.languageSaved')).toBe('Мову збережено')
setLocale('ru')
expect(t('toast.languageSaved')).toBe('Язык сохранён')
})
// The dictionaries are filled in area by area, so a language is expected to
// be missing slugs for a while — the interface must still be readable.
describe('when a dictionary is not complete yet', () => {
afterEach(() => {
ru['toast.languageSaved'] = 'Язык сохранён'
})
it('falls back to English for a slug the language does not carry', () => {
delete ru['toast.languageSaved']
setLocale('ru')
expect(t('toast.languageSaved')).toBe('Language saved')
})
it('shows the slug itself rather than nothing when nobody has it', () => {
expect(t('nope.not.a.slug')).toBe('nope.not.a.slug')
})
})
it('interpolates parameters', () => {
en['test.greeting'] = 'Hello {name}, {n} new'
try {
expect(t('test.greeting', { name: 'Ada', n: 3 })).toBe('Hello Ada, 3 new')
// A parameter nobody passed stays visible instead of turning into "undefined"
expect(t('test.greeting', { name: 'Ada' })).toBe('Hello Ada, {n} new')
} finally {
delete en['test.greeting']
}
})
it('picks the plural form from n', () => {
en['test.items'] = { one: '{n} item', other: '{n} items' }
ru['test.items'] = { one: '{n} вещь', few: '{n} вещи', many: '{n} вещей' }
try {
setLocale('en')
expect(t('test.items', { n: 1 })).toBe('1 item')
expect(t('test.items', { n: 2 })).toBe('2 items')
setLocale('ru')
expect(t('test.items', { n: 1 })).toBe('1 вещь')
expect(t('test.items', { n: 3 })).toBe('3 вещи')
expect(t('test.items', { n: 5 })).toBe('5 вещей')
expect(t('test.items', { n: 21 })).toBe('21 вещь')
expect(t('test.items', { n: 12 })).toBe('12 вещей')
} finally {
delete en['test.items']
delete ru['test.items']
}
})
})
// The Slavic categories are the part of pluralisation that is easy to get
// subtly wrong, so they are checked on their own.
describe('plural categories', () => {
it('uses one/few/many for ru and uk', () => {
for (const lang of ['ru', 'uk']) {
expect([1, 21, 101].map(n => pluralCategory(lang, n))).toEqual(['one', 'one', 'one'])
expect([2, 3, 4, 22].map(n => pluralCategory(lang, n))).toEqual(['few', 'few', 'few', 'few'])
expect([0, 5, 11, 12, 14, 25].map(n => pluralCategory(lang, n)))
.toEqual(['many', 'many', 'many', 'many', 'many', 'many'])
}
})
it('uses one/other for English', () => {
expect(pluralCategory('en', 1)).toBe('one')
expect(pluralCategory('en', 0)).toBe('other')
expect(pluralCategory('en', 7)).toBe('other')
})
})
// With sign-in switched off there is no account to follow, and the choice has
// to survive a reload in this browser.
describe('the language of a browser without an account', () => {
function fakeStorage(initial = {}) {
const entries = { ...initial }
return {
getItem: key => (key in entries ? entries[key] : null),
setItem: (key, value) => { entries[key] = String(value) },
}
}
afterEach(() => {
vi.unstubAllGlobals()
setLocale('en')
})
it('keeps the choice for the next visit', () => {
const storage = fakeStorage()
vi.stubGlobal('localStorage', storage)
rememberLocale('ru')
expect(storage.getItem(LOCALE_STORAGE_KEY)).toBe('ru')
expect(storedLocale()).toBe('ru')
})
it('reads nothing out of storage that it cannot speak', () => {
vi.stubGlobal('localStorage', fakeStorage({ [LOCALE_STORAGE_KEY]: 'kl-GL' }))
expect(storedLocale()).toBeNull()
})
it('survives storage being unavailable', () => {
// A private window: every access throws, and neither reading nor writing
// may take the interface down with it.
vi.stubGlobal('localStorage', {
getItem: () => { throw new Error('blocked') },
setItem: () => { throw new Error('blocked') },
})
expect(storedLocale()).toBeNull()
expect(() => rememberLocale('uk')).not.toThrow()
})
})