diff --git a/src/utils/validate-translations.spec.js b/src/utils/validate-translations.spec.js new file mode 100644 index 000000000..c46af6f7b --- /dev/null +++ b/src/utils/validate-translations.spec.js @@ -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(/]*>([\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 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); + }); + }); + }); +});