Snapshot of uncommitted work: landing, bots, prod infra, partner cabinet, backoffice stage 1
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
45d7512d2b
commit
d452824c62
314 files changed
+44394
-121
No files matched your search
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Проверка переводов (критерий приёмки «нет ключей без перевода»):
|
||||
* - в каждом языке есть все ключи ru, без пустых значений и лишних ключей;
|
||||
* - плейсхолдеры {{x}} совпадают с исходником;
|
||||
* - отчёт по статусам needs_review / approved.
|
||||
* Код выхода 1 при любой ошибке — для CI.
|
||||
*/
|
||||
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { dynamicKeys } from './dynamic-keys.ts';
|
||||
import { placeholders, readLocale, readReview, ROOT, statusOf, TARGETS } from './lib.ts';
|
||||
|
||||
const ru = readLocale('ru');
|
||||
const review = readReview();
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const [key, value] of Object.entries(ru)) {
|
||||
if (!value.trim()) errors.push(`ru: пустое значение ${key}`);
|
||||
}
|
||||
|
||||
// ключи-литералы в коде (вида t(«a.b»)) должны существовать в ru
|
||||
const NAMESPACES = Object.keys(JSON.parse(readFileSync(path.join(ROOT, 'locales/ru.json'), 'utf8')) as object);
|
||||
const KEY_RE = new RegExp(`['\`](?:${NAMESPACES.join('|')})\\.[a-zA-Z0-9_.]+(?=['\`])`, 'g');
|
||||
function walk(dir: string, out: string[] = []) {
|
||||
for (const name of readdirSync(dir)) {
|
||||
if (['node_modules', 'generated', '.next', 'dist', '.turbo'].includes(name)) continue;
|
||||
const p = path.join(dir, name);
|
||||
if (statSync(p).isDirectory()) walk(p, out);
|
||||
else if (/\.(ts|tsx)$/.test(name) && !/\.test\.ts$/.test(name)) out.push(p);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
const repo = path.resolve(ROOT, '../..');
|
||||
const pluralBase = (k: string) => k.replace(/_(one|few|many|other)$/, '');
|
||||
const known = new Set(Object.keys(ru).flatMap((k) => [k, pluralBase(k)]));
|
||||
for (const file of [...walk(path.join(repo, 'apps')), ...walk(path.join(repo, 'packages'))]) {
|
||||
for (const m of readFileSync(file, 'utf8').matchAll(KEY_RE)) {
|
||||
const key = m[0].slice(1);
|
||||
if (!known.has(key) && !key.endsWith('.')) errors.push(`ключ из кода отсутствует в ru: ${key} (${path.relative(repo, file)})`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of dynamicKeys()) if (!known.has(key)) errors.push(`динамический ключ отсутствует в ru: ${key}`);
|
||||
|
||||
for (const locale of TARGETS) {
|
||||
const msgs = readLocale(locale);
|
||||
const counts = { approved: 0, needs_review: 0, missing: 0 };
|
||||
for (const [key, source] of Object.entries(ru)) {
|
||||
const status = statusOf(review, locale, key, source, msgs[key]);
|
||||
counts[status]++;
|
||||
if (status === 'missing') errors.push(`${locale}: нет перевода ${key}`);
|
||||
else if (placeholders(source) !== placeholders(msgs[key]!)) errors.push(`${locale}: плейсхолдеры не совпадают в ${key}`);
|
||||
}
|
||||
for (const key of Object.keys(msgs)) if (!(key in ru)) errors.push(`${locale}: лишний ключ ${key}`);
|
||||
console.log(`${locale}: ${Object.keys(ru).length} ключей · approved ${counts.approved} · needs_review ${counts.needs_review} · missing ${counts.missing}`);
|
||||
}
|
||||
|
||||
if (errors.length) {
|
||||
console.error(`\n${errors.length} ошибок:\n` + errors.slice(0, 50).join('\n'));
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('i18n: все ключи переведены.');
|
||||
Reference in new issue
Block a user