Files
AWatch-rus/i18n/scripts/validate-locales.js
T
qwen.ai[bot] dc422e2e2e Title: Add i18n module with Russian localization and validation tools
Key features implemented:
- New i18n module with JSON-based message catalogs for multi-language support
- Added Russian (ru-RU) and English (en-US) localization files with 100+ messages
- PowerShell module ActivityWatch.Windows.I18n.psm1 with fallback mechanism
- Node.js validation and coverage analysis scripts for locale management
- Package.json configuration with semantic versioning and build tools
- README_RU.md documentation with integration examples and best practices

The implementation provides comprehensive internationalization support with automated validation, versioning, and fallback capabilities for robust multilingual deployments.
2026-04-27 06:55:20 +00:00

226 lines
7.6 KiB
JavaScript

#!/usr/bin/env node
/**
* Locale validation script for i18n JSON files
* Validates structure, required fields, and message format placeholders
*/
import { readFileSync, readdirSync } from 'fs';
import { join, basename } from 'path';
const I18N_DIR = join(new URL('.', import.meta.url).pathname, '..');
const REQUIRED_FIELDS = ['version', 'language', 'fallback', 'messages'];
const MESSAGE_CATEGORIES = ['errors', 'info', 'warnings', 'prompts', 'status', 'choices'];
function validateLocaleFile(filePath) {
const errors = [];
const warnings = [];
let content;
try {
content = readFileSync(filePath, 'utf-8');
} catch (error) {
return {
valid: false,
errors: [`Cannot read file: ${error.message}`],
warnings: []
};
}
let catalog;
try {
catalog = JSON.parse(content);
} catch (error) {
return {
valid: false,
errors: [`Invalid JSON: ${error.message}`],
warnings: []
};
}
// Check required top-level fields
for (const field of REQUIRED_FIELDS) {
if (!(field in catalog)) {
errors.push(`Missing required field: "${field}"`);
}
}
if (!catalog.version) {
warnings.push('Version is empty or missing');
} else if (!/^\d+\.\d+\.\d+$/.test(catalog.version)) {
warnings.push(`Version "${catalog.version}" does not follow semver (X.Y.Z)`);
}
if (!catalog.language || typeof catalog.language !== 'string') {
errors.push('Field "language" must be a non-empty string');
}
if (catalog.fallback !== null && typeof catalog.fallback !== 'string') {
warnings.push('Field "fallback" should be null or a locale string');
}
// Validate messages structure
if (!catalog.messages || typeof catalog.messages !== 'object') {
errors.push('Field "messages" must be an object');
return { valid: false, errors, warnings };
}
const messageKeys = Object.keys(catalog.messages);
// Check for proper key naming convention (category.key)
const invalidKeys = messageKeys.filter(key => !key.includes('.'));
if (invalidKeys.length > 0) {
warnings.push(`Keys without category prefix: ${invalidKeys.slice(0, 5).join(', ')}`);
}
// Check message categories coverage
const foundCategories = new Set(messageKeys.map(k => k.split('.')[0]));
const missingCategories = MESSAGE_CATEGORIES.filter(cat => !foundCategories.has(cat));
if (missingCategories.length > 0) {
warnings.push(`Missing message categories: ${missingCategories.join(', ')}`);
}
// Validate message format placeholders consistency
const placeholderPattern = /\{(\d+)\}/g;
const messagesWithPlaceholders = {};
for (const [key, value] of Object.entries(catalog.messages)) {
if (typeof value !== 'string') {
errors.push(`Message "${key}" must be a string, got ${typeof value}`);
continue;
}
if (value.trim() === '') {
warnings.push(`Message "${key}" is empty`);
}
const matches = value.match(placeholderPattern);
if (matches) {
const indices = matches.map(m => parseInt(m.slice(1, -1)));
const maxIndex = Math.max(...indices);
const minIndex = Math.min(...indices);
if (minIndex !== 0) {
warnings.push(`Message "${key}": placeholder indices should start at 0`);
}
const expectedCount = maxIndex + 1;
const uniqueIndices = new Set(indices);
if (uniqueIndices.size !== expectedCount) {
warnings.push(`Message "${key}": potentially missing placeholder indices (0-${maxIndex})`);
}
messagesWithPlaceholders[key] = {
count: uniqueIndices.size,
maxIndex
};
}
}
// Check for consistent placeholder usage across locales (if reference exists)
const refLocalePath = join(I18N_DIR, 'en-US.json');
if (basename(filePath) !== 'en-US.json' && refLocalePath !== filePath) {
try {
const refContent = JSON.parse(readFileSync(refLocalePath, 'utf-8'));
const refMessages = refContent.messages || {};
for (const [key, data] of Object.entries(messagesWithPlaceholders)) {
if (refMessages[key]) {
const refMatches = refMessages[key].match(placeholderPattern);
if (refMatches) {
const refIndices = new Set(refMatches.map(m => parseInt(m.slice(1, -1))));
if (refIndices.size !== data.count) {
warnings.push(`Message "${key}": placeholder count differs from en-US (${data.count} vs ${refIndices.size})`);
}
}
}
}
} catch (e) {
// Reference locale may not exist
}
}
return {
valid: errors.length === 0,
errors,
warnings,
stats: {
totalMessages: messageKeys.length,
messagesWithPlaceholders: Object.keys(messagesWithPlaceholders).length,
categories: Array.from(foundCategories)
}
};
}
function main() {
const args = process.argv.slice(2);
const specificFile = args[0];
let filesToValidate = [];
if (specificFile) {
filesToValidate = [join(I18N_DIR, specificFile)];
} else {
const files = readdirSync(I18N_DIR);
filesToValidate = files
.filter(f => f.endsWith('.json') && !f.startsWith('package'))
.map(f => join(I18N_DIR, f));
}
if (filesToValidate.length === 0) {
console.error('No locale files found to validate');
process.exit(1);
}
let allValid = true;
let totalErrors = 0;
let totalWarnings = 0;
console.log('🔍 Validating locale files...\n');
for (const filePath of filesToValidate) {
const fileName = basename(filePath);
console.log(`📄 ${fileName}`);
const result = validateLocaleFile(filePath);
if (result.stats) {
console.log(` Messages: ${result.stats.totalMessages}`);
console.log(` Categories: ${result.stats.categories.join(', ')}`);
console.log(` With placeholders: ${result.stats.messagesWithPlaceholders}`);
}
if (result.errors.length > 0) {
console.log(` ❌ Errors (${result.errors.length}):`);
result.errors.forEach(err => console.log(` - ${err}`));
totalErrors += result.errors.length;
allValid = false;
} else {
console.log(` ✅ No errors`);
}
if (result.warnings.length > 0) {
console.log(` ⚠️ Warnings (${result.warnings.length}):`);
result.warnings.slice(0, 5).forEach(warn => console.log(` - ${warn}`));
if (result.warnings.length > 5) {
console.log(` ... and ${result.warnings.length - 5} more`);
}
totalWarnings += result.warnings.length;
}
console.log('');
}
console.log('─'.repeat(50));
console.log(`Summary: ${totalErrors} errors, ${totalWarnings} warnings`);
if (allValid) {
console.log('✅ All locale files are valid!');
process.exit(0);
} else {
console.log('❌ Validation failed. Please fix the errors above.');
process.exit(1);
}
}
main();