Files
baya-monorepo/client/scripts/check-copy.mjs
T
2026-07-19 21:31:59 +03:30

94 lines
3.3 KiB
JavaScript

#!/usr/bin/env node
/**
* Lints client/messages/fa.json against the banned-orthography-variant rules in
* client/messages/STYLE.md — enforces the phase-12 sweep so it cannot silently regress.
* Exits non-zero (and prints every offending key) on any match.
*/
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const __dirname = dirname(fileURLToPath(import.meta.url));
const FA_MESSAGES_PATH = join(__dirname, '..', 'messages', 'fa.json');
// Each rule scans the *raw value* of every leaf string in fa.json (flattened key path → value).
// `pattern` is a plain substring (not regex) so ZWNJ/hamza characters stay literal and unambiguous.
const RULES = [
{
name: 'brand name must use ZWNJ (بالین‌یار), never a plain space',
pattern: 'بالین یار',
},
{
name: 'تأیید must use hamza — تایید (hamza-less) is banned',
pattern: 'تایید',
},
{
name: 'جستجو is the one standard form — جست‌وجو is banned',
pattern: 'جست‌وجو',
},
{
name: 'جستجو is the one standard form — جست و جو (spaced) is banned',
pattern: 'جست و جو',
},
{
name: 'the indefinite «ی» misattachment bug — «بازی» (game) instead of «ی» on the right word',
// Word-boundary trap: catches "... بازی " (space after) so it doesn't also flag words that
// legitimately contain "بازی" as a substring of something else — in this catalog there are none,
// but the trailing-space anchor keeps the check honest if one is ever added on purpose elsewhere.
pattern: 'بازی ',
},
{
// Leading space distinguishes the archaic passive auxiliary ("X می‌گردد" = "is X-ed") from the
// unrelated, entirely legitimate verb «برمی‌گردد» (returns/comes back), which fuses «بر» directly
// onto «می‌گردد» with no space and must never be flagged.
name: 'archaic passive می‌گردد is banned — use می‌شود',
pattern: ' می‌گردد',
},
];
function flattenStrings(value, path, out) {
if (typeof value === 'string') {
out.push([path, value]);
return;
}
if (Array.isArray(value)) {
value.forEach((item, index) => flattenStrings(item, `${path}[${index}]`, out));
return;
}
if (value && typeof value === 'object') {
for (const [key, child] of Object.entries(value)) {
flattenStrings(child, path ? `${path}.${key}` : key, out);
}
}
}
function main() {
const raw = readFileSync(FA_MESSAGES_PATH, 'utf8');
const messages = JSON.parse(raw);
const leaves = [];
flattenStrings(messages, '', leaves);
const failures = [];
for (const rule of RULES) {
for (const [path, value] of leaves) {
if (value.includes(rule.pattern)) {
failures.push({ rule: rule.name, path, value });
}
}
}
if (failures.length === 0) {
console.log(`check-copy: ${leaves.length} strings checked, 0 banned variants found.`);
return;
}
console.error(`check-copy: found ${failures.length} banned copy variant(s):\n`);
for (const failure of failures) {
console.error(` [${failure.rule}]`);
console.error(` fa.json → ${failure.path}: "${failure.value}"\n`);
}
process.exitCode = 1;
}
main();