110 lines
4.7 KiB
JavaScript
110 lines
4.7 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const root = path.resolve(__dirname, '..');
|
|
const srcDir = path.join(root, 'src');
|
|
const localeDir = path.join(srcDir, 'i18n', 'locales');
|
|
const uiTextFile = path.join(srcDir, 'i18n', 'uiText.ts');
|
|
const langs = ['ar', 'fr', 'hi', 'ms'];
|
|
|
|
const read = (file) => fs.readFileSync(file, 'utf8');
|
|
const readJson = (file) => JSON.parse(read(file));
|
|
|
|
const walk = (dir, out = []) => {
|
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
const full = path.join(dir, entry.name);
|
|
if (entry.isDirectory()) walk(full, out);
|
|
else if (/\.(tsx?|jsx?)$/.test(entry.name)) out.push(full);
|
|
}
|
|
return out;
|
|
};
|
|
|
|
const flatten = (source, prefix = '', out = {}) => {
|
|
for (const [key, value] of Object.entries(source)) {
|
|
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
if (value && typeof value === 'object' && !Array.isArray(value)) flatten(value, fullKey, out);
|
|
else out[fullKey] = value;
|
|
}
|
|
return out;
|
|
};
|
|
|
|
const normalize = (value) => value.trim().replace(/\s+/g, ' ');
|
|
const isUiPhrase = (value) =>
|
|
/[A-Za-z]/.test(value) &&
|
|
!/[{}()[\]=<>]/.test(value) &&
|
|
!/^(https?:|\/api\/|[.#]?[a-z0-9_-]+$)/i.test(value) &&
|
|
!/\.(tsx?|jsx?|css|png|jpg|svg|xlsx?)$/i.test(value);
|
|
|
|
const allowedSame = new Set([
|
|
'API', 'CSV', 'DN', 'GRN', 'GST', 'GSTIN', 'HSN', 'HSN/SAC', 'IFSC / SWIFT', 'PDF', 'PO', 'POS', 'QR', 'RFQ', 'SKU', 'SMS', 'SSO', 'UPI', 'VAT',
|
|
'CGST', 'SGST', 'IGST', 'UPI ID', 'IFSC Code:', 'SWIFT Code:', 'UPI ID:', 'CGST (+)', 'SGST (+)', 'IGST (+)', 'CGST (+):', 'SGST (+):', 'IGST (+):',
|
|
'Status:', 'Item:', 'Unit *', 'Ref #', 'ⓘ info', '→ stock',
|
|
]);
|
|
|
|
const isCodeLikeLocaleValue = (value) =>
|
|
!isUiPhrase(value) ||
|
|
allowedSame.has(value.trim()) ||
|
|
/[{}()[\]=<>]/.test(value) ||
|
|
/\b[a-z]+[A-Z][A-Za-z]*\b/.test(value) ||
|
|
/[&=<>]/.test(value) ||
|
|
/\b(currentData|totalRow|enableDelete|pageSize|filteredProducts|sessionSales|grandTotal|canCompare|onPayDue|soList|stats)\b/.test(value) ||
|
|
/^e\.g\./i.test(value) ||
|
|
/^https?:/i.test(value) ||
|
|
/^[A-Z0-9_ /:+#.-]+$/.test(value);
|
|
|
|
const en = readJson(path.join(localeDir, 'en.json'));
|
|
const enFlat = flatten(en);
|
|
const enValues = new Set(Object.values(enFlat).filter((value) => typeof value === 'string').map(normalize));
|
|
const uiText = read(uiTextFile);
|
|
const uiTextValues = new Set([...uiText.matchAll(/'([^']+)':\s*'[^']+'/g)].map((match) => normalize(match[1])));
|
|
|
|
const headerCandidates = new Map();
|
|
const addCandidate = (value, file) => {
|
|
const text = normalize(value);
|
|
if (!isUiPhrase(text)) return;
|
|
if (!headerCandidates.has(text)) headerCandidates.set(text, new Set());
|
|
headerCandidates.get(text).add(path.relative(root, file));
|
|
};
|
|
|
|
for (const file of walk(srcDir)) {
|
|
const source = read(file);
|
|
for (const match of source.matchAll(/\bheaders\s*=\s*\{?\s*\[([\s\S]*?)\]\s*\}?/g)) {
|
|
const body = match[1];
|
|
for (const stringMatch of body.matchAll(/['"`]([^'"`\n]+)['"`]/g)) addCandidate(stringMatch[1], file);
|
|
}
|
|
for (const match of source.matchAll(/\b(?:headers|columns)\s*=\s*\[([\s\S]*?)\]/g)) {
|
|
const body = match[1];
|
|
for (const stringMatch of body.matchAll(/['"`]([^'"`\n]+)['"`]/g)) addCandidate(stringMatch[1], file);
|
|
}
|
|
}
|
|
|
|
const missingHeaderMappings = [...headerCandidates.entries()]
|
|
.filter(([text]) => !uiTextValues.has(text) && !enValues.has(text))
|
|
.sort((a, b) => b[1].size - a[1].size || a[0].localeCompare(b[0]))
|
|
.map(([text, files]) => ({ text, files: [...files] }));
|
|
|
|
const untranslatedByLang = {};
|
|
const translatableUntranslatedByLang = {};
|
|
for (const lang of langs) {
|
|
const flat = flatten(readJson(path.join(localeDir, `${lang}.json`)));
|
|
untranslatedByLang[lang] = [];
|
|
translatableUntranslatedByLang[lang] = [];
|
|
for (const [key, enValue] of Object.entries(enFlat)) {
|
|
if (typeof enValue !== 'string') continue;
|
|
if (flat[key] === enValue && /[A-Za-z]/.test(enValue)) {
|
|
const item = { key, value: enValue };
|
|
untranslatedByLang[lang].push(item);
|
|
if (!isCodeLikeLocaleValue(enValue)) translatableUntranslatedByLang[lang].push(item);
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log(JSON.stringify({
|
|
missingHeaderMappingsCount: missingHeaderMappings.length,
|
|
missingHeaderMappings: missingHeaderMappings.slice(0, 250),
|
|
untranslatedCounts: Object.fromEntries(Object.entries(untranslatedByLang).map(([lang, items]) => [lang, items.length])),
|
|
translatableUntranslatedCounts: Object.fromEntries(Object.entries(translatableUntranslatedByLang).map(([lang, items]) => [lang, items.length])),
|
|
untranslatedSamples: Object.fromEntries(Object.entries(untranslatedByLang).map(([lang, items]) => [lang, items.slice(0, 120)])),
|
|
translatableUntranslatedSamples: Object.fromEntries(Object.entries(translatableUntranslatedByLang).map(([lang, items]) => [lang, items.slice(0, 160)])),
|
|
}, null, 2));
|