42 lines
2.4 KiB
TypeScript
42 lines
2.4 KiB
TypeScript
#!/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<string, string>;
|
|
const placeholders = (s: string): string[] => [...s.matchAll(/\{(\w+)\}/g)].map(m => m[1]!).sort();
|
|
|
|
export function checkCatalogs(source: Catalog, others: Record<string, Catalog>): 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`);
|
|
}
|