97 lines
3.5 KiB
JavaScript
97 lines
3.5 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Lints client/messages/fa.json against the banned-orthography-variant rules in
|
|
* docs/rules/client/i18n.md §4 — enforces the phase-12 copy sweep so it cannot silently regress.
|
|
* Exits non-zero (and prints every offending key) on any match.
|
|
*
|
|
* The rules below are the machine-checkable subset. The full Persian style guide (glossary, register,
|
|
* shell naming, the ZWNJ cases a grep can't express) lives in that doc; keep the two in step.
|
|
*/
|
|
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();
|