Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
80 lines
3.0 KiB
TypeScript
80 lines
3.0 KiB
TypeScript
import { createHash } from 'node:crypto';
|
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
import path from 'node:path';
|
|
import { flatten, LOCALES, type Locale, type Messages } from '../src/index.ts';
|
|
|
|
export const ROOT = path.resolve(import.meta.dirname, '..');
|
|
export const TARGETS = LOCALES.filter((l) => l !== 'ru') as Exclude<Locale, 'ru'>[];
|
|
|
|
type Tree = { [k: string]: string | Tree };
|
|
|
|
export function readLocale(locale: Locale): Messages {
|
|
return flatten(JSON.parse(readFileSync(path.join(ROOT, 'locales', `${locale}.json`), 'utf8')) as Tree);
|
|
}
|
|
|
|
export function writeLocale(locale: Locale, flat: Messages, order: string[]) {
|
|
const tree: Tree = {};
|
|
for (const key of order) {
|
|
if (flat[key] === undefined) continue;
|
|
const parts = key.split('.');
|
|
let node = tree;
|
|
for (const p of parts.slice(0, -1)) node = (node[p] ??= {}) as Tree;
|
|
node[parts.at(-1)!] = flat[key]!;
|
|
}
|
|
writeFileSync(path.join(ROOT, 'locales', `${locale}.json`), JSON.stringify(tree, null, 2) + '\n');
|
|
}
|
|
|
|
/**
|
|
* review.json хранит для каждого языка { ключ: хеш русского текста на момент проверки }.
|
|
* Если русский текст изменился, хеш не совпадёт — ключ снова needs_review.
|
|
*/
|
|
export type Review = Record<Exclude<Locale, 'ru'>, Record<string, string>>;
|
|
export const readReview = (): Review => JSON.parse(readFileSync(path.join(ROOT, 'review.json'), 'utf8'));
|
|
export const writeReview = (r: Review) => writeFileSync(path.join(ROOT, 'review.json'), JSON.stringify(r, null, 2) + '\n');
|
|
|
|
export const sourceHash = (s: string) => createHash('sha256').update(s).digest('hex').slice(0, 12);
|
|
|
|
export type Status = 'approved' | 'needs_review' | 'missing';
|
|
export function statusOf(review: Review, locale: Exclude<Locale, 'ru'>, key: string, ru: string, value: string | undefined): Status {
|
|
if (value === undefined || value === '') return 'missing';
|
|
return review[locale][key] === sourceHash(ru) ? 'approved' : 'needs_review';
|
|
}
|
|
|
|
export const placeholders = (s: string) => [...s.matchAll(/\{\{\s*(\w+)\s*\}\}/g)].map((m) => m[1]).sort().join(',');
|
|
|
|
export function csvEscape(v: string) {
|
|
return /[",\n]/.test(v) ? `"${v.replace(/"/g, '""')}"` : v;
|
|
}
|
|
|
|
export function parseCsv(text: string): string[][] {
|
|
const rows: string[][] = [];
|
|
let row: string[] = [];
|
|
let cell = '';
|
|
let quoted = false;
|
|
for (let i = 0; i < text.length; i++) {
|
|
const ch = text[i]!;
|
|
if (quoted) {
|
|
if (ch === '"' && text[i + 1] === '"') {
|
|
cell += '"';
|
|
i++;
|
|
} else if (ch === '"') quoted = false;
|
|
else cell += ch;
|
|
} else if (ch === '"') quoted = true;
|
|
else if (ch === ',') {
|
|
row.push(cell);
|
|
cell = '';
|
|
} else if (ch === '\n' || ch === '\r') {
|
|
if (ch === '\r' && text[i + 1] === '\n') i++;
|
|
row.push(cell);
|
|
rows.push(row);
|
|
row = [];
|
|
cell = '';
|
|
} else cell += ch;
|
|
}
|
|
if (cell || row.length) {
|
|
row.push(cell);
|
|
rows.push(row);
|
|
}
|
|
return rows.filter((r) => r.some((c) => c !== ''));
|
|
}
|