@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
JavaScript
/**
* 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);
}