WIP: #7324 - validate_translations #1134

Draft
jsegarra wants to merge 5 commits from validate_translations into dev
1 changed files with 118 additions and 0 deletions

View File

@ -0,0 +1,118 @@
import { describe, it, expect } from 'vitest';
import fs from 'fs';
import path from 'path';
import { glob } from 'glob';
import yaml from 'js-yaml';
import assert from 'assert';
const YML = '.yml';
const UTF8 = 'utf8';
const LOCALES_FILES = [`src/i18n/locale/**${YML}`, `src/pages/**/locale/*${YML}`];
const VUE_FILES = ['src/pages/*.vue', 'src/pages/**/*.vue', 'src/components/**/*.vue'];
const CURRENT_LOCALES = ['es'];
let locales = {};
let i18n = {};
async function init() {
const files = await glob(LOCALES_FILES);
locales = files.reduce((acc, file) => {
const locale = path.basename(file, YML);
acc[locale] = { ...acc[locale], ...yaml.load(fs.readFileSync(file, UTF8)) };
return acc;
}, {});
}
function validateKeys(keys, locale) {
const missingKeys = validateLocale(keys, locales[locale]);
const missingKeys2 = validateLocale(missingKeys, i18n[locale]);
return missingKeys2;
}
function validateLocale(keys, translations) {
let missingKeys = [];
if (translations === undefined) return missingKeys;
keys.forEach((key) => {
const parts = key.split('.');
let current = translations;
for (const part of parts) {
if (current[part] !== undefined) {
current = current[part];
} else {
missingKeys.push(key);
break;
}
}
});
return missingKeys;
}
describe('🔍 Translation Keys Validation', async () => {
await init();
const vueFiles = await glob(VUE_FILES);
const regex = /="\$t\(['"`]([\w.]+)['"`]\)|="t\(['"`]([\w.]+)['"`]\)/g;
vueFiles.forEach(async (file) => {
const keys = new Set();
const content = fs.readFileSync(file, UTF8);
let match;
while ((match = regex.exec(content)) !== null) {
keys.add(match[1] || match[2]);
}
const parts = file.split(path.sep);
const cardIndex = parts.indexOf('Card');
let previousElement = '';
if (cardIndex > -1) {
previousElement = parts[cardIndex - 1];
previousElement =
previousElement.charAt(0).toLowerCase() + previousElement.slice(1);
}
const i18nMatch = content.match(/<i18n[^>]*>([\s\S]*?)<\/i18n>/);
if (i18nMatch) {
try {
const i18nContent = yaml.load(i18nMatch[1]);
if (Object.keys(i18nContent).length < Object.keys(locales).length) {
const langs = Object.keys(locales);
const current = Object.keys(i18nContent);
langs
.filter((x) => !current.includes(x))
.forEach((lang) => {
i18nContent[lang] = i18nContent[current[0]];
});
}
i18n = i18nContent;
} catch (err) {
console.warn(`⚠️ Error parsing <i18n> block in ${file}:`, err.message);
}
}
CURRENT_LOCALES.forEach((locale) => {
let missingKeys = validateKeys(keys, locale);
if (missingKeys.length > 0) {
const updatedKeys = new Set();
missingKeys.forEach((key) => {
if (!key.startsWith(`${previousElement}.`))
updatedKeys.add(`${previousElement}.${key}`);
});
missingKeys = validateKeys(updatedKeys, locale);
}
assert(
missingKeys.length === 0,
`Missing keys in ${locale}.${file}:\n${missingKeys.join('\n')}`
);
it(`should have all translation keys in ${locale}.${file}`, () => {
expect(missingKeys, `Missing keys in ${file}`).toHaveLength(0);
});
});
});
});