@userfrosting/sprinkle-core
Version:
Core Sprinkle for UserFrosting
374 lines (327 loc) • 15.2 kB
text/typescript
import { describe, expect, beforeEach, test, vi } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { DateTime, Settings } from 'luxon'
import axios from 'axios'
import { useTranslator } from '../../stores/useTranslator'
import type { DictionaryConfig, DictionaryEntries, DictionaryResponse } from '../../interfaces'
const testDictionaryConfig: DictionaryConfig = {
name: 'English',
regional: 'English',
authors: ['UserFrosting'],
plural_rule: 1,
dates: 'en-US'
}
const testDictionaryConfigFr: DictionaryConfig = {
name: 'French',
regional: 'Français',
authors: ['Malou'],
plural_rule: 1,
dates: 'fr-FR'
}
const testDictionaryEntries: DictionaryEntries = {
YES: 'Yes',
NO: 'No',
WELCOME_TO: 'Welcome to {{title}}!',
'ERROR.@TRANSLATION': 'The Error',
'ERROR.TITLE': "We've sensed a great disturbance in the Force.",
USERNAME: 'Username',
'COLOR.BLACK': 'black',
'COLOR.RED': 'red',
'COLOR.WHITE': 'white',
'COLOR.0': 'colors',
'COLOR.1': 'color',
'COLOR.2': 'colors',
'X_CARS.0': 'no cars',
'X_CARS.1': 'a car',
'X_CARS.2': '{{plural}} cars',
'X_HUNGRY_CATS.@PLURAL': 'num',
'X_HUNGRY_CATS.1': '{{num}} hungry cat',
'X_HUNGRY_CATS.2': '{{num}} hungry cats',
X_FOO: '{{plural}}x foos',
'CAR.1': 'car',
'CAR.2': 'cars',
'CAR.GAS': 'gas',
'CAR.EV.@TRANSLATION': 'electric',
'CAR.EV.FULL': 'full electric',
'CAR.FULL_MODEL': '{{make}} {{model}} {{year}}',
'MY_CARS.@TRANSLATION': 'My cars',
'MY_CARS.1': 'I have a {{type}} {{&CAR}}',
'MY_CARS.2': 'I have {{plural}} {{type}} {{&CAR}}',
TEST_LIMIT: 'Your test must be between {{min}} and {{max}} potatoes.',
MIN: 'minimum',
'X_RULES.0': 'no rules',
'X_RULES.1': '{{plural}} rule', // No plural form for 2
'X_BANANAS.0': 'no bananas', // No plural form for 1, 2, etc.
'X_DOGS.5': 'five dogs',
'X_DOGS.101': '101 Dalmatians',
'X_DOGS.1000': 'An island of dogs'
}
const testDictionary: DictionaryResponse = {
identifier: 'en_US',
config: testDictionaryConfig,
dictionary: testDictionaryEntries
}
const testDictionaryFr: DictionaryResponse = {
identifier: 'fr_FR',
config: testDictionaryConfigFr,
dictionary: testDictionaryEntries
}
describe('API Tests', async () => {
test('API should be saved', async () => {
// Arrange
setActivePinia(createPinia())
const translator = useTranslator()
const { translate, load } = translator
const response = { data: testDictionary }
vi.spyOn(axios, 'get').mockResolvedValue(response as any)
// Assert initial state
expect(translator.identifier).toBe('')
expect(translator.config).toEqual({
name: '',
regional: '',
authors: [],
plural_rule: 0,
dates: ''
})
// Act
await load()
// Assert config data
expect(axios.get).toHaveBeenCalledWith('/api/dictionary')
expect(translator.config).toEqual(testDictionaryConfig)
expect(translator.identifier).toBe('en_US')
// Assert basic translate method
expect(translate('YES')).toBe('Yes')
expect(translate('NO')).toBe('No')
expect(translate('WELCOME_TO')).toBe('Welcome to {{title}}!')
})
})
describe('Translator Tests', () => {
beforeEach(async () => {
// Arrange
setActivePinia(createPinia())
const translator = useTranslator()
const { load } = translator
const response = { data: testDictionary }
vi.spyOn(axios, 'get').mockResolvedValue(response as any)
// Act
await load()
})
test('Should handle basic translation', () => {
const { translate } = useTranslator()
expect(translate('USERNAME')).toBe('Username')
})
test('Should handle key not existing', () => {
const { translate } = useTranslator()
expect(translate('NOT_EXIST')).toBe('NOT_EXIST')
expect(translate('NOT EXIST')).toBe('NOT EXIST')
})
test('Should handle @TRANSLATION', () => {
const { translate } = useTranslator()
expect(translate('ERROR')).toBe('The Error')
expect(translate('ERROR.TITLE')).toBe("We've sensed a great disturbance in the Force.")
})
test('Should handle placeholders', () => {
const { translate } = useTranslator()
expect(translate('WELCOME_TO')).toBe('Welcome to {{title}}!') // Placeholder not replaced
expect(translate('WELCOME_TO', { title: 'UserFrosting' })).toBe('Welcome to UserFrosting!')
expect(translate('WELCOME_TO', { bar: 'UserFrosting' })).toBe('Welcome to {{title}}!') // Wrong key
expect(translate('WELCOME_TO', 'UserFrosting')).toBe('Welcome to {{title}}!') // Key not provided
expect(translate('WELCOME_TO', { title: 'UserFrosting', foo: 'bar' })).toBe(
'Welcome to UserFrosting!'
) // Extra key not used
})
test('Should handle basic plurals', () => {
const { translate } = useTranslator()
expect(translate('X_CARS', 0)).toBe('no cars')
expect(translate('X_CARS', 1)).toBe('a car')
expect(translate('X_CARS', 2)).toBe('2 cars')
expect(translate('X_CARS', 5)).toBe('5 cars')
})
test('Should handle pluralization with custom plural key', () => {
const { translate } = useTranslator()
expect(translate('X_HUNGRY_CATS', { num: 0 })).toBe('0 hungry cats')
expect(translate('X_HUNGRY_CATS', { num: 1 })).toBe('1 hungry cat')
expect(translate('X_HUNGRY_CATS', { num: 2 })).toBe('2 hungry cats')
expect(translate('X_HUNGRY_CATS', { num: 5 })).toBe('5 hungry cats')
// Custom key can also be omitted in the placeholder if it's the only
// placeholder even with custom plural key
expect(translate('X_HUNGRY_CATS', 5)).toBe('5 hungry cats')
// Test missing pluralization and placeholder (default to 1)
expect(translate('X_HUNGRY_CATS')).toBe('1 hungry cat')
})
test('Should handle plurals default, when no placeholder', () => {
const { translate } = useTranslator()
expect(translate('X_CARS')).toBe('a car')
})
test('Should handle Key With No Plural', () => {
const { translate } = useTranslator()
expect(translate('USERNAME', 123)).toBe('Username') // USERNAME has no placeholders
expect(translate('X_FOO')).toBe('{{plural}}x foos') // 'X_FOO' doesn't have children, so it's not treated as a "pluralize-able" string
expect(translate('X_FOO', { plural: 1 })).toBe('1x foos') // Replace {{plural}} with 1
expect(translate('X_FOO', 1)).toBe('1x foos') // Replace {{plural}} with 1, without specifying the key
expect(translate('X_FOO', 123)).toBe('123x foos') // Replace {{plural}} with 123
})
test('Should handle plurals for different plural rules', () => {
const { translate } = useTranslator()
// English plural rule is 1
expect(translate('COLOR', 0)).toBe('colors')
expect(translate('COLOR', 1)).toBe('color')
expect(translate('COLOR', 2)).toBe('colors')
expect(translate('COLOR', 3)).toBe('colors')
// Same as above, but with a custom plural key (french)
// Note "0" is plural (colors) in english, singular (couleur) in french !
// TODO : Implement plurals & load custom locale
// expect(translate('COLOR', 0)).toBe('colors')
// expect(translate('COLOR', 1)).toBe('color')
// expect(translate('COLOR', 2)).toBe('colors')
// expect(translate('COLOR', 3)).toBe('colors')
})
test('Should handle a simple replacement when the key is not defined in dictionary', () => {
const { translate } = useTranslator()
expect(translate('You are {{status}}', { status: 'dumb' })).toBe('You are dumb')
})
test('Should handle @TRANSLATION when the key have plural rules', () => {
const { translate } = useTranslator()
expect(translate('MY_CARS')).toBe('My cars') // @TRANSLATION is used, not the 1 rule
expect(translate('MY_CARS', 1)).toBe('I have a {{type}} car')
expect(translate('MY_CARS', 2)).toBe('I have 2 {{type}} cars')
})
test('Should handle complex placeholders', () => {
const { translate } = useTranslator()
expect(translate('MY_CARS', { type: 'car' })).toBe('My cars')
expect(translate('MY_CARS', { type: 'gaz', plural: 1 })).toBe('I have a gaz car')
expect(translate('MY_CARS', { type: 'gaz', plural: 2 })).toBe('I have 2 gaz cars')
expect(translate('MY_CARS', { type: '&CAR.GAS', plural: 2 })).toBe('I have 2 gas cars')
expect(translate('MY_CARS', { type: '&CAR.EV', plural: 2 })).toBe('I have 2 electric cars')
expect(translate('MY_CARS', { type: '&CAR.EV.FULL', plural: 1 })).toBe(
'I have a full electric car'
)
expect(
translate('MY_CARS', {
type: '&CAR.FULL_MODEL',
plural: 5,
make: 'Toyota',
model: 'Camry',
year: 2022
})
).toBe('I have 5 Toyota Camry 2022 cars')
})
// Test basic placeholder replacement using int as placeholder value (So they don't try to translate "min" and "max")
// We don't want to end up with "Your test must be between _minimum_ and 200 potatoes"
test('Should handle placeholder not overwritten by other key', () => {
const { translate } = useTranslator()
expect(translate('TEST_LIMIT', { min: 4, max: 200 })).toBe(
'Your test must be between 4 and 200 potatoes.'
)
})
// 2 will return singular as the plural is not defined
test('Should handle pluralization with no rules', () => {
const { translate } = useTranslator()
expect(translate('X_RULES', 0)).toBe('no rules')
expect(translate('X_RULES', 1)).toBe('1 rule')
expect(translate('X_RULES', 2)).toBe('2 rule')
// X_BANANAS
expect(translate('X_BANANAS', 0)).toBe('no bananas')
expect(translate('X_BANANAS', 1)).toBe('no bananas')
expect(translate('X_BANANAS', 2)).toBe('no bananas')
expect(translate('X_BANANAS', 5)).toBe('no bananas')
})
// The keys are int, but don't follow the rules. It will fallback to the literal key
test("Should handle plurals who doesn't follow the rules", () => {
const { translate } = useTranslator()
expect(translate('X_DOGS')).toBe('X_DOGS')
expect(translate('X_DOGS', 0)).toBe('X_DOGS')
expect(translate('X_DOGS', 1)).toBe('X_DOGS')
expect(translate('X_DOGS', 2)).toBe('X_DOGS') // No plural rules found
expect(translate('X_DOGS', 5)).toBe('five dogs') // This one is hardcoded and will fallback as normal string key
expect(translate('X_DOGS', 101)).toBe('101 Dalmatians') // Same here
expect(translate('X_DOGS', 102)).toBe('X_DOGS') // This one is not hardcoded
expect(translate('X_DOGS', 1000)).toBe('An island of dogs') // Still fallback, if the key is a string representing and INT
})
})
describe('Date Tests', async () => {
test('translateDate should return the correct values in English', async () => {
// Arrange
setActivePinia(createPinia())
const translator = useTranslator()
const { translateDate, load, getDateTime } = translator
const response = { data: testDictionary }
vi.spyOn(axios, 'get').mockResolvedValue(response as any)
await load()
// Assert basic translate method
// Force the default timezone, so the test is consistent regardless of
// the timezone of the runner
Settings.defaultZone = 'America/New_York'
expect(translateDate('2025-02-02T14:42:12.000000Z')).toBe('Sun, Feb 2, 2025, 9:42 AM')
expect(translateDate('2025-02-02T14:42:12.000000Z', 'DDD')).toBe('February 2, 2025')
expect(translateDate('2025-02-02T14:42:12.000000Z', DateTime.DATETIME_MED)).toBe(
'Feb 2, 2025, 9:42 AM'
)
// Test get the Datetime object
expect(getDateTime('2025-02-02T14:42:12.000000Z').monthLong).toBe('February')
})
test('translateDate should return the correct values in French', async () => {
// Arrange
setActivePinia(createPinia())
const translator = useTranslator()
const { translateDate, load, getDateTime } = translator
const response = { data: testDictionaryFr }
vi.spyOn(axios, 'get').mockResolvedValue(response as any)
await load()
// Assert basic translate method
// Force the default timezone, so the test is consistent regardless of
// the timezone of the runner
Settings.defaultZone = 'America/New_York'
expect(translateDate('2025-02-02T14:42:12.000000Z')).toBe('dim. 2 févr. 2025, 09:42')
expect(translateDate('2025-02-02T14:42:12.000000Z', 'DDD')).toBe('2 février 2025')
expect(translateDate('2025-02-02T14:42:12.000000Z', DateTime.DATETIME_MED)).toBe(
'2 févr. 2025, 09:42'
)
// Test get the Datetime object
expect(getDateTime('2025-02-02T14:42:12.000000Z').monthLong).toBe('février')
})
})
describe('Plural Form Tests', () => {
beforeEach(async () => {
// Arrange
setActivePinia(createPinia())
const translator = useTranslator()
const { load } = translator
const response = { data: testDictionary }
vi.spyOn(axios, 'get').mockResolvedValue(response as any)
// Act
await load()
})
test('Should return correct plural form for rule 1', () => {
const { getPluralForm } = useTranslator()
expect(getPluralForm(0, 1)).toBe(2)
expect(getPluralForm(1, 1)).toBe(1)
expect(getPluralForm(2, 1)).toBe(2)
expect(getPluralForm(20, 1)).toBe(2)
})
test('Should return correct plural form for rule 2', () => {
const { getPluralForm } = useTranslator()
expect(getPluralForm(0, 2)).toBe(1) // French = zero is singular
expect(getPluralForm(1, 2)).toBe(1)
expect(getPluralForm(2, 2)).toBe(2)
expect(getPluralForm(20, 2)).toBe(2)
})
test('Should throw error for invalid rule', () => {
const { getPluralForm } = useTranslator()
expect(() => getPluralForm(1, 16)).toThrow('The rule number 16 must be between 0 and 15')
})
test('Should use default rule if forceRule is not provided', () => {
const { getPluralForm } = useTranslator()
expect(getPluralForm(0)).toBe(2)
expect(getPluralForm(1)).toBe(1)
expect(getPluralForm(2)).toBe(2)
})
test('Should used the specified rule if forceRule is provided', () => {
const { getPluralForm } = useTranslator()
expect(getPluralForm(0, 1)).toBe(2) // English
expect(getPluralForm(0, 2)).toBe(1) // French
expect(getPluralForm(5, 1)).toBe(2) // English
expect(getPluralForm(5, 8)).toBe(3) // Slavic
})
})
// TODO : Test each plural rules