Files
bookorbit/client/scripts/locale-catalog-validation.mjs
neon 9eff914aef fix(client): stop requiring plural categories no count can select
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.
2026-08-12 16:54:51 +00:00

63 lines
2.5 KiB
JavaScript

import { validateLocaleMessage, validateSlotCountMessage } from './locale-message-validation.mjs'
export function flattenCatalog(value, prefix = '', output = new Map()) {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new Error(`${prefix || 'catalog root'} must be a message object`)
}
for (const [key, child] of Object.entries(value)) {
const messageKey = prefix ? `${prefix}.${key}` : key
if (typeof child === 'string') output.set(messageKey, child)
else if (child && typeof child === 'object' && !Array.isArray(child)) flattenCatalog(child, messageKey, output)
else throw new Error(`${messageKey} must be a string or message object`)
}
return output
}
function messageErrors({ key, locale, message, referenceMessage, slotCountKeys }) {
if (referenceMessage === undefined) return [`${locale}: unexpected key ${key}`]
const errors = []
if (message.length === 0) errors.push(`${locale}: empty message ${key}`)
if (message.includes('\u2014')) errors.push(`${locale}: Unicode em dash is not allowed in ${key}`)
if (/<[^>]+>/.test(message)) errors.push(`${locale}: HTML is not allowed in ${key}`)
errors.push(...validateLocaleMessage({ key, locale, message, referenceMessage }))
if (slotCountKeys.has(key)) errors.push(...validateSlotCountMessage({ key, locale, message }))
return errors
}
export function validateCatalogs({ catalogs, referencedKeys = new Set(), slotCountKeys = new Set() }) {
const reference = catalogs.get('en')
if (!reference) throw new Error('English reference catalog is required')
const errors = []
for (const key of referencedKeys) {
if (!reference.has(key)) errors.push(`en: missing key referenced in source ${key}`)
}
for (const [locale, catalog] of catalogs) {
for (const [key, message] of catalog) {
errors.push(...messageErrors({ key, locale, message, referenceMessage: reference.get(key), slotCountKeys }))
}
}
return errors
}
export function findInvalidTargetMessages({ catalogs, slotCountKeys = new Set() }) {
const reference = catalogs.get('en')
if (!reference) throw new Error('English reference catalog is required')
const invalid = []
for (const [locale, catalog] of catalogs) {
if (locale === 'en') continue
for (const [key, message] of catalog) {
const errors = messageErrors({ key, locale, message, referenceMessage: reference.get(key), slotCountKeys })
if (errors.length > 0) invalid.push({ locale, key, errors })
}
}
return invalid
}