UNPKG

@shelchin/svelte-i18n

Version:

The last Svelte i18n library you'll ever need. Type-safe, AI-powered, zero-config.

218 lines (217 loc) • 8.45 kB
#!/usr/bin/env node /** * Validate translation files * Checks for missing keys, type mismatches, and consistency */ import { readFileSync, readdirSync, statSync } from 'fs'; import { join, basename } from 'path'; import { validateSchema } from '../utils/translation-utils.js'; function loadTranslationFile(filePath) { try { const content = readFileSync(filePath, 'utf-8'); return JSON.parse(content); } catch (error) { throw new Error(`Failed to load ${filePath}: ${error}`); } } function getTranslationFiles(dir) { const translations = new Map(); const files = readdirSync(dir); for (const file of files) { const filePath = join(dir, file); const stats = statSync(filePath); if (stats.isFile() && file.endsWith('.json')) { const locale = basename(file, '.json'); const content = loadTranslationFile(filePath); // Remove _meta field for validation // eslint-disable-next-line @typescript-eslint/no-unused-vars const { _meta, ...translationContent } = content; translations.set(locale, translationContent); } } return translations; } function validateTranslations(translations, options) { const results = []; // Determine base locale const baseLocale = options.baseLocale || 'en'; const baseTranslations = translations.get(baseLocale); if (!baseTranslations) { throw new Error(`Base locale "${baseLocale}" not found`); } // Validate each locale against the base for (const [locale, content] of translations) { if (locale === baseLocale) continue; const result = { locale, errors: [], warnings: [] }; // Use the validateSchema utility const schemaErrors = validateSchema(content, baseTranslations); result.errors.push(...schemaErrors); // Check for placeholder mismatches (always check, not just in strict mode) const placeholderErrors = checkPlaceholders(content, baseTranslations); result.errors.push(...placeholderErrors); // Additional validations for strict mode if (options.strict) { // Check for extra keys not in base const extraKeys = findExtraKeys(content, baseTranslations); extraKeys.forEach((key) => { result.warnings.push(`Extra key not in base locale: ${key}`); }); } results.push(result); } return results; } function findExtraKeys(obj, base, prefix = '') { const extra = []; for (const key in obj) { const fullKey = prefix ? `${prefix}.${key}` : key; if (!(key in base)) { extra.push(fullKey); } else if (typeof obj[key] === 'object' && obj[key] !== null && typeof base[key] === 'object' && base[key] !== null) { extra.push(...findExtraKeys(obj[key], base[key], fullKey)); } } return extra; } function checkPlaceholders(obj, base) { const errors = []; function extractPlaceholders(str) { const placeholders = new Set(); const regex = /\{([^}]+)\}/g; let match; while ((match = regex.exec(str)) !== null) { placeholders.add(match[1]); } return placeholders; } function check(current, reference, path = '') { for (const key in reference) { const fullPath = path ? `${path}.${key}` : key; if (typeof reference[key] === 'string' && typeof current[key] === 'string') { const refPlaceholders = extractPlaceholders(reference[key]); const curPlaceholders = extractPlaceholders(current[key]); // Check if all reference placeholders exist in current refPlaceholders.forEach((placeholder) => { if (!curPlaceholders.has(placeholder)) { errors.push(`Missing placeholder {${placeholder}} in ${fullPath}`); } }); // Check for extra placeholders curPlaceholders.forEach((placeholder) => { if (!refPlaceholders.has(placeholder)) { errors.push(`Extra placeholder {${placeholder}} in ${fullPath}`); } }); } else if (typeof reference[key] === 'object' && reference[key] !== null && typeof current[key] === 'object' && current[key] !== null) { check(current[key], reference[key], fullPath); } } } check(obj, base); return errors; } export function validate(options) { console.log('šŸ” Validating translations...'); console.log(` Directory: ${options.translationsDir}`); console.log(` Base locale: ${options.baseLocale || 'en'}`); console.log(` Strict mode: ${options.strict ? 'Yes' : 'No'}`); const translations = getTranslationFiles(options.translationsDir); console.log(` Found ${translations.size} translation files`); const results = validateTranslations(translations, options); let hasErrors = false; let hasWarnings = false; results.forEach((result) => { if (result.errors.length > 0 || result.warnings.length > 0) { console.log(`\nšŸ“‹ ${result.locale}:`); if (result.errors.length > 0) { hasErrors = true; console.log(' āŒ Errors:'); result.errors.forEach((error) => { console.log(` • ${error}`); }); } if (result.warnings.length > 0) { hasWarnings = true; console.log(' āš ļø Warnings:'); result.warnings.forEach((warning) => { console.log(` • ${warning}`); }); } } else { console.log(`āœ… ${result.locale}: No issues found`); } }); // In strict mode, warnings also cause validation to fail const validationFailed = hasErrors || (options.strict && hasWarnings); if (!validationFailed) { console.log('\nāœ… All translations are valid!'); return true; } else { console.log('\nāŒ Validation failed!'); return false; } } // CLI interface // Check if this file is being run directly (ES module style) if (import.meta.url === `file://${process.argv[1]}`) { const args = process.argv.slice(2); // Filter out any "--" separators from pnpm/npm const filteredArgs = args.filter((arg) => arg !== '--'); // Parse arguments let translationsDir = './src/translations'; let baseLocale = 'en'; let strict = false; for (let i = 0; i < filteredArgs.length; i++) { if (filteredArgs[i] === '--dir' && filteredArgs[i + 1]) { translationsDir = filteredArgs[i + 1]; i++; } else if (filteredArgs[i] === '--base' && filteredArgs[i + 1]) { baseLocale = filteredArgs[i + 1]; i++; } else if (filteredArgs[i] === '--strict') { strict = true; } else if (filteredArgs[i] === '--help') { console.log('Usage: validate [options]'); console.log('Options:'); console.log(' --dir <path> Directory containing translation files (default: ./src/translations)'); console.log(' --base <locale> Base locale for comparison (default: en)'); console.log(' --strict Enable strict validation'); console.log(' --help Show this help message'); console.log(''); console.log('Examples:'); console.log(' npm run cli:validate'); console.log(' npm run cli:validate -- --dir ./static/translations'); console.log(' npm run cli:validate -- --dir ./src/translations --base en --strict'); process.exit(0); } else if (!filteredArgs[i].startsWith('--')) { // If it's not a flag, assume it's the directory (for backward compatibility) translationsDir = filteredArgs[i]; } } const isValid = validate({ translationsDir, baseLocale, strict }); process.exit(isValid ? 0 : 1); }