Apps, web, i18n, UI, partners, payouts, backoffice, bariki, prod infra
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
45d7512d2b
commit
3489e0b816
319 files changed
+45483
-121
No files matched your search
@@ -0,0 +1,79 @@
|
||||
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 !== ''));
|
||||
}
|
||||
Reference in new issue
Block a user