morphit/apps/web/scripts/payment-method-i18n-parity-smoke.ts

151 lines
4.7 KiB
TypeScript

/**
* payment-method-i18n-parity-smoke.ts
*
* Pre-launch invariant: every entry in `PAYMENT_METHODS`
* (apps/web/src/lib/payments/registry.ts) must have a
* corresponding i18n key at `payment_method.<key>.description`
* in EVERY locale (en, es, fr, de, it, pl, ru, fa, zh-CN, zh-HK).
*
* WHY THIS SMOKE EXISTS (Part 122 cp32 deep-deep CODE-2 finding):
*
* Cp32 deep-deep surfaced that `pay_usdt` (Part 121 cp3),
* `pay_usdc` (Part 122 cp30), and `pay_dai` (Part 122 cp31) had
* all been added to the registry + indexer's
* RESERVED_CANONICAL_KEYS WITHOUT their i18n description keys
* being added to ANY locale. The picker still rendered (the
* description lookup falls back to the key text when missing),
* but rendered "pay_dai" instead of "Dai: USD-pegged stablecoin
* on EVM networks." 3-checkpoint drift across cp3/cp30/cp31.
*
* The existing `reserved-keys-parity-smoke.ts` enforces SET
* parity between registry and RESERVED_CANONICAL_KEYS — but not
* i18n coverage. This smoke closes that gap.
*
* Locale parity is mandatory per Memory #8. This smoke fires
* the moment a future asset addition extends the registry
* without translating its description across all 10 locales.
*
* Self-test on tamper: remove pay_dai from one locale → smoke
* MUST fail before tarball.
*/
import { readFileSync } from 'node:fs';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { SUPPORTED_LOCALES } from '../src/lib/i18n/locales';
const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = resolve(__dirname, '..', '..', '..');
const REGISTRY_PATH = resolve(
REPO_ROOT,
'apps/web/src/lib/payments/registry.ts'
);
const LOCALES_DIR = resolve(REPO_ROOT, 'apps/web/src/lib/i18n/locales');
const LOCALES = SUPPORTED_LOCALES.map((l) => l.code);
let scenarios = 0;
let failures = 0;
function scenario(name: string, fn: () => void): void {
scenarios++;
try {
fn();
} catch (err) {
failures++;
console.error(
`${name}: ${err instanceof Error ? err.message : String(err)}`
);
}
}
// Extract every key: '...' from registry source. String-extract
// to keep the smoke standalone — no @morphit/asset-registry import
// dep, runs without npm install.
const registrySrc = readFileSync(REGISTRY_PATH, 'utf8');
const registryKeys = new Set<string>();
for (const m of registrySrc.matchAll(
/key:\s*'([a-z][a-z0-9_]+)'/g
)) {
registryKeys.add(m[1]!);
}
scenario('registry source readable', () => {
if (registryKeys.size === 0) {
throw new Error('extracted zero keys from registry — regex may be broken');
}
});
scenario('at least 10 crypto + 3 in-person + 30 online payment methods (sanity)', () => {
// Currently shipped: 10 crypto (btc/xmr/blurt/usdt/usdc/dai/bch/ltc/dash/doge)
// + 3 in-person (barter/cash/precious_metals) + ~30 online rails.
if (registryKeys.size < 40) {
throw new Error(
`expected ≥40 registry entries (cp33 baseline), found ${registryKeys.size}`
);
}
});
scenario('pay_dai present in registry (cp32 CODE-1 closure)', () => {
if (!registryKeys.has('pay_dai')) {
throw new Error(
'pay_dai missing from registry — cp31 DAI addition + cp32 CODE-1 closure should have added it'
);
}
});
// Per-locale i18n coverage check
for (const locale of LOCALES) {
scenario(`locale '${locale}' covers every registry key`, () => {
const localeData = JSON.parse(
readFileSync(resolve(LOCALES_DIR, `${locale}.json`), 'utf8')
);
const pm = localeData.payment_method;
if (!pm) {
throw new Error(`payment_method root missing in ${locale}.json`);
}
const missing: string[] = [];
for (const key of registryKeys) {
const entry = pm[key];
if (!entry || typeof entry !== 'object' || !entry.description) {
missing.push(key);
}
}
if (missing.length > 0) {
throw new Error(
`${locale}.json missing payment_method.<key>.description for: ${missing.join(', ')}`
);
}
});
}
scenario('crypto pay_<asset> ordering matches registry section grouping', () => {
// Sanity: ensure pay_btc/pay_xmr/pay_blurt/pay_usdt/pay_usdc/pay_dai/
// pay_bch/pay_ltc/pay_dash all in en.json (subset check).
const en = JSON.parse(
readFileSync(resolve(LOCALES_DIR, 'en.json'), 'utf8')
);
const pm = en.payment_method;
const expected = [
'pay_btc',
'pay_blurt',
'pay_xmr',
'pay_usdt',
'pay_usdc',
'pay_dai',
'pay_bch',
'pay_ltc',
'pay_dash'
];
const missing = expected.filter((k) => !pm[k]);
if (missing.length > 0) {
throw new Error(`en.json missing crypto pay_*: ${missing.join(', ')}`);
}
});
console.log(`\n${'─'.repeat(54)}`);
if (failures === 0) {
console.log(`✓ all ${scenarios} scenarios passed`);
process.exit(0);
} else {
console.error(`\n ${failures}/${scenarios} scenarios FAILED`);
process.exit(1);
}