mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 08:21:39 -06:00
9eff914aef
The locale validator demanded every CLDR plural category, so each new ICU plural key took the daily Crowdin sync red until a `many` branch was hand-added for Spanish and French. That branch can never render: `many` selects at a million and over in Spanish, French, Italian and Portuguese, and only for fractional counts in Czech and Slovak, while every catalog plural counts whole items well below a million and intl-messageformat falls back to `other` regardless. Require only the categories an integer count 0..1000 can select, which is the same set every integer below a million selects, and keep `other` always so the categories everyday counts reach stay mandatory. Drop a target message that still fails validation from the sparse catalog instead of failing the whole export, so one broken string can no longer block the other 23 languages for days. Report the rejections in the run log and the pull request body, and keep the retention guard fatal when a rejected message would replace a translation that is already in Git.
135 lines
6.2 KiB
JavaScript
135 lines
6.2 KiB
JavaScript
import { describe, expect, it } from 'vitest'
|
|
import { analyzeIcuMessage, isIcuPluralMessage, validateLocaleMessage, validateSlotCountMessage } from './locale-message-validation.mjs'
|
|
|
|
const validate = (locale, referenceMessage, message) =>
|
|
validateLocaleMessage({
|
|
key: 'books.count',
|
|
locale,
|
|
message,
|
|
referenceMessage,
|
|
})
|
|
|
|
describe('locale message validation', () => {
|
|
it('analyzes nested ICU arguments and plural selectors', () => {
|
|
const message = '{count, plural, =0 {No books for {name}} one {One book} other {# books}}'
|
|
|
|
expect(isIcuPluralMessage(message)).toBe(true)
|
|
expect(analyzeIcuMessage(message)).toEqual({
|
|
arguments: ['count', 'name'],
|
|
argumentSignatures: ['count:cardinal', 'name:argument'],
|
|
emptyOptions: [],
|
|
plurals: [
|
|
{
|
|
argument: 'count',
|
|
offset: 0,
|
|
type: 'cardinal',
|
|
selectors: ['=0', 'one', 'other'],
|
|
},
|
|
],
|
|
selects: [],
|
|
})
|
|
})
|
|
|
|
it('rejects malformed ordinary Vue I18n messages', () => {
|
|
expect(validate('de', 'Hello, {name}!', 'Hallo, {{name}}!')).toEqual(['de: invalid Vue I18n syntax in books.count: Not allowed nest placeholder'])
|
|
})
|
|
|
|
it('rejects legacy plural syntax', () => {
|
|
expect(validate('de', 'No books | One book | {count} books', 'Keine Bücher | Ein Buch | {count} Bücher')).toContain(
|
|
'de: legacy plural branches are not allowed for books.count',
|
|
)
|
|
expect(validate('de', 'No books|One book|{count} books', 'Keine Bücher|Ein Buch|{count} Bücher')).toContain(
|
|
'de: legacy plural branches are not allowed for books.count',
|
|
)
|
|
})
|
|
|
|
it('requires matching ICU arguments and exact selectors', () => {
|
|
const reference = '{count, plural, =0 {No books for {name}} one {One book for {name}} other {# books for {name}}}'
|
|
const message = '{count, plural, one {Ein Buch} other {# Bücher}}'
|
|
|
|
expect(validate('de', reference, message)).toEqual(['de: ICU arguments differ for books.count', 'de: ICU selector =0 missing for books.count'])
|
|
})
|
|
|
|
it('requires matching ICU argument types and styles', () => {
|
|
const reference = '{count, plural, one {One book for {name}} other {# books for {name}}}'
|
|
const changedType = '{count, plural, one {Ein Buch für {name, number}} other {# Bücher für {name, number}}}'
|
|
|
|
expect(validate('de', reference, changedType)).toContain('de: ICU argument types differ for books.count')
|
|
|
|
const styledReference = '{count, plural, one {{amount, number, integer} book} other {{amount, number, integer} books}}'
|
|
const changedStyle = '{count, plural, one {{amount, number, percent} Buch} other {{amount, number, percent} Bücher}}'
|
|
|
|
expect(validate('de', styledReference, changedStyle)).toContain('de: ICU argument types differ for books.count')
|
|
})
|
|
|
|
it('rejects empty ICU options', () => {
|
|
const reference = '{count, plural, one {One book} other {# books}}'
|
|
const message = '{count, plural, one {} other {# Bücher}}'
|
|
|
|
expect(validate('de', reference, message)).toContain('de: empty ICU option one for count in books.count')
|
|
})
|
|
|
|
it('requires matching plural offsets', () => {
|
|
const reference = '{count, plural, one {One book} other {# books}}'
|
|
const message = '{count, plural, offset:1 one {Ein Buch} other {# Bücher}}'
|
|
|
|
expect(validate('de', reference, message)).toContain('de: ICU plural expressions differ for books.count')
|
|
})
|
|
|
|
it('requires locale-specific cardinal categories', () => {
|
|
const reference = '{count, plural, one {# book} other {# books}}'
|
|
const message = '{count, plural, one {# knjiga} other {# knjig}}'
|
|
|
|
expect(validate('sl', reference, message)).toEqual([
|
|
'sl: ICU plural category two missing for books.count',
|
|
'sl: ICU plural category few missing for books.count',
|
|
])
|
|
})
|
|
|
|
it('accepts cardinal categories that only fractional or million-scale counts select', () => {
|
|
const reference = '{count, plural, one {# book} other {# books}}'
|
|
|
|
expect(validate('es', reference, '{count, plural, one {# libro} other {# libros}}')).toEqual([])
|
|
expect(validate('fr', reference, '{count, plural, one {# livre} other {# livres}}')).toEqual([])
|
|
expect(validate('cs', reference, '{count, plural, one {# kniha} few {# knihy} other {# knih}}')).toEqual([])
|
|
expect(validate('es', reference, '{count, plural, one {# libro} many {# de libros} other {# libros}}')).toEqual([])
|
|
})
|
|
|
|
it('still requires categories that everyday counts select', () => {
|
|
const reference = '{count, plural, one {# book} other {# books}}'
|
|
|
|
expect(validate('ru', reference, '{count, plural, one {# книга} other {# книги}}')).toEqual([
|
|
'ru: ICU plural category few missing for books.count',
|
|
'ru: ICU plural category many missing for books.count',
|
|
])
|
|
expect(validate('cs', reference, '{count, plural, one {# kniha} other {# knih}}')).toEqual([
|
|
'cs: ICU plural category few missing for books.count',
|
|
])
|
|
})
|
|
|
|
it('reports invalid ICU and rejects legacy branches for migrated keys', () => {
|
|
const reference = '{count, plural, one {One book} other {# books}}'
|
|
|
|
expect(validate('de', reference, '{count, plural, one {Ein Buch} other {# Bücher}')).toEqual([
|
|
'de: invalid ICU syntax in books.count: EXPECT_ARGUMENT_CLOSING_BRACE',
|
|
])
|
|
expect(validate('de', reference, 'Kein Buch | Ein Buch | {count} Bücher')).toEqual([
|
|
'de: legacy plural branches are not allowed for ICU message books.count',
|
|
'de: ICU plural syntax required for books.count',
|
|
])
|
|
})
|
|
|
|
it('requires slot count messages to render exactly one count', () => {
|
|
const slotCount = (message) => validateSlotCountMessage({ key: 'books.count', locale: 'de', message })
|
|
|
|
expect(slotCount('{count, plural, one {# Buch} other {# Bücher}}')).toEqual([])
|
|
expect(slotCount('{count, plural, one {Ein Buch} other {# Bücher}}')).toEqual([
|
|
'de: slot count message books.count must render exactly one # in every branch',
|
|
])
|
|
expect(slotCount('{count, plural, one {# von # Büchern} other {# Bücher}}')).toEqual([
|
|
'de: slot count message books.count must render exactly one # in every branch',
|
|
])
|
|
expect(slotCount('Keine Bücher')).toEqual(['de: ICU plural syntax required for slot count message books.count'])
|
|
})
|
|
})
|