#!/usr/bin/env bun // Checks the desktop app's UI catalogs (apps/desktop/src/i18n): every language has exactly the English keys, no empty // message, the same {placeholders} as English, plural "_one" entries only next to their base key and with {count} or // a literal 1, no em dash (the house style uses "·"), no stray markup. Part of `bun run ci`. // bun tools/i18n-check.ts import { en } from '../apps/desktop/src/i18n/en'; import { tr } from '../apps/desktop/src/i18n/tr'; export type Catalog = Record; const placeholders = (s: string): string[] => [...s.matchAll(/\{(\w+)\}/g)].map(m => m[1]!).sort(); export function checkCatalogs(source: Catalog, others: Record): string[] { const errors: string[] = []; const keys = Object.keys(source); for (const [k, v] of Object.entries(source)) { if (!v.trim()) errors.push(`en: ${k} is empty`); if (k.endsWith('_one') && !(k.slice(0, -4) in source)) errors.push(`en: ${k} has no base key`); } for (const [lang, cat] of Object.entries({ en: source, ...others })) { for (const k of keys) if (!(k in cat)) errors.push(`${lang}: missing ${k}`); for (const [k, v] of Object.entries(cat)) { if (!(k in source)) { errors.push(`${lang}: unknown key ${k}`); continue; } if (!v.trim()) errors.push(`${lang}: ${k} is empty`); if (/—/.test(v)) errors.push(`${lang}: ${k} has an em dash (use "·")`); if (/<[a-z/][^>]*>/i.test(v)) errors.push(`${lang}: ${k} has markup`); if (/[\u0000-\u0008\u000b-\u001f]/.test(v)) errors.push(`${lang}: ${k} has control characters`); const want = placeholders(source[k]!), got = placeholders(v); if (k.endsWith('_one')) { // a "one" form may spell the count out ("1 app") instead of using {count} const base = placeholders(source[k.slice(0, -4)]!).filter(p => p !== 'count'); if (got.filter(p => p !== 'count').join() !== base.join()) errors.push(`${lang}: ${k} placeholders ${got.join(',')} differ from ${base.join(',')}`); } else if (got.join() !== want.join()) errors.push(`${lang}: ${k} placeholders {${got.join('},{')}} differ from English {${want.join('},{')}}`); } } return errors; } if (import.meta.main) { const errors = checkCatalogs(en, { tr }); if (errors.length) { console.error(errors.join('\n')); console.error(`i18n: ${errors.length} problem(s)`); process.exit(1); } console.log(`i18n: en + tr, ${Object.keys(en).length} messages, placeholders match`); }