Files
Claude-Code-Workflow/ccw/frontend/scripts/validate-translations.ts
catlog22 715ef12c92 feat(a2ui): Implement A2UI backend with question handling and WebSocket support
- Added A2UITypes for defining question structures and answers.
- Created A2UIWebSocketHandler for managing WebSocket connections and message handling.
- Developed ask-question tool for interactive user questions via A2UI.
- Introduced platformUtils for platform detection and shell command handling.
- Centralized TypeScript types in index.ts for better organization.
- Implemented compatibility checks for hook templates based on platform requirements.
2026-01-31 15:27:12 +08:00

232 lines
6.3 KiB
TypeScript

// ========================================
// Translation Validation Script
// ========================================
// Checks that en/ and zh/ translation files have matching keys
import { readFileSync, readdirSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
interface TranslationEntry {
key: string;
path: string[];
}
interface ValidationResult {
isValid: boolean;
errors: string[];
warnings: string[];
missingKeys: {
en: string[];
zh: string[];
};
extraKeys: {
en: string[];
zh: string[];
};
}
// ES module equivalent of __dirname
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const LOCALES_DIR = join(__dirname, '../src/locales');
const SUPPORTED_LOCALES = ['en', 'zh'] as const;
/**
* Recursively get all translation keys and values from a nested object
*/
function flattenObject(obj: Record<string, unknown>, prefix = ''): Map<string, string> {
const map = new Map<string, string>();
for (const key in obj) {
const fullKey = prefix ? `${prefix}.${key}` : key;
const value = obj[key];
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
const nestedMap = flattenObject(value as Record<string, unknown>, fullKey);
nestedMap.forEach((v, k) => map.set(k, v));
} else if (typeof value === 'string') {
map.set(fullKey, value);
}
}
return map;
}
/**
* Load and parse a JSON file
*/
function loadJsonFile(filePath: string): Record<string, unknown> {
try {
const content = readFileSync(filePath, 'utf-8');
return JSON.parse(content);
} catch (error) {
console.error(`Error loading ${filePath}:`, error);
return {};
}
}
/**
* Get all translation keys and values for a locale
*/
function getLocaleKeys(locale: string): Map<string, string> {
const localeDir = join(LOCALES_DIR, locale);
const map = new Map<string, string>();
try {
const files = readdirSync(localeDir).filter((f) => f.endsWith('.json'));
for (const file of files) {
const filePath = join(localeDir, file);
const content = loadJsonFile(filePath);
const flatMap = flattenObject(content);
flatMap.forEach((v, k) => map.set(k, v));
}
} catch (error) {
console.error(`Error reading locale directory for ${locale}:`, error);
}
return map;
}
/**
* Check if a value is a non-translatable (numbers, symbols, placeholders only)
*/
function isNonTranslatable(value: string): boolean {
// Check if it's just numbers, symbols, or contains only placeholders like {count}, {name}, etc.
const nonTranslatablePattern = /^[0-9%\$#\-\+\=\[\]{}()\/\\.,:;!?<>|"'\s_@*~`^&]*$/;
return nonTranslatablePattern.test(value) && !/[a-zA-Z\u4e00-\u9fa5]/.test(value);
}
/**
* Compare translation keys and values between locales
*/
function compareTranslations(): ValidationResult {
const result: ValidationResult = {
isValid: true,
errors: [],
warnings: [],
missingKeys: { en: [], zh: [] },
extraKeys: { en: [], zh: [] },
};
// Get keys and values for each locale
const enMap = getLocaleKeys('en');
const zhMap = getLocaleKeys('zh');
// Get all unique keys
const allKeys = new Set([...enMap.keys(), ...zhMap.keys()]);
// Find keys missing in Chinese
for (const key of enMap.keys()) {
if (!zhMap.has(key)) {
result.missingKeys.zh.push(key);
result.isValid = false;
}
}
// Find keys missing in English
for (const key of zhMap.keys()) {
if (!enMap.has(key)) {
result.missingKeys.en.push(key);
result.isValid = false;
}
}
// Check for untranslated values (identical en and zh values)
for (const key of allKeys) {
const enValue = enMap.get(key);
const zhValue = zhMap.get(key);
if (enValue && zhValue && enValue === zhValue) {
// Skip if the value is non-translatable (numbers, symbols, etc.)
if (!isNonTranslatable(enValue)) {
result.warnings.push(
`Untranslated value in zh/ for key "${key}": en="${enValue}" == zh="${zhValue}"`
);
}
}
}
return result;
}
/**
* Display validation results
*/
function displayResults(result: ValidationResult): void {
console.log('\n=== Translation Validation Report ===\n');
if (result.isValid) {
console.log('Status: PASSED');
console.log('All translation keys are synchronized between en/ and zh/ locales.\n');
} else {
console.log('Status: FAILED');
console.log('Translation keys are not synchronized.\n');
}
// Display missing keys
if (result.missingKeys.zh.length > 0) {
console.log(`Keys missing in zh/ (${result.missingKeys.zh.length}):`);
result.missingKeys.zh.forEach((key) => console.log(` - ${key}`));
console.log('');
}
if (result.missingKeys.en.length > 0) {
console.log(`Keys missing in en/ (${result.missingKeys.en.length}):`);
result.missingKeys.en.forEach((key) => console.log(` - ${key}`));
console.log('');
}
// Display untranslated values warnings
const untranslatedWarnings = result.warnings.filter(w => w.startsWith('Untranslated value'));
if (untranslatedWarnings.length > 0) {
console.log(`Untranslated values in zh/ (${untranslatedWarnings.length}):`);
untranslatedWarnings.forEach((warning) => console.log(` ⚠️ ${warning}`));
console.log('');
}
// Display other warnings
const otherWarnings = result.warnings.filter(w => !w.startsWith('Untranslated value'));
if (otherWarnings.length > 0) {
console.log('Warnings:');
otherWarnings.forEach((warning) => console.log(` ⚠️ ${warning}`));
console.log('');
}
// Display errors
if (result.errors.length > 0) {
console.log('Errors:');
result.errors.forEach((error) => console.log(`${error}`));
console.log('');
}
console.log('=====================================\n');
}
/**
* Main validation function
*/
function main(): void {
console.log('Validating translations...\n');
// Check if locale directories exist
for (const locale of SUPPORTED_LOCALES) {
const localePath = join(LOCALES_DIR, locale);
// Note: In a real script, you'd use fs.existsSync here
// For now, we'll let the error be caught in getLocaleKeys
}
// Compare translations
const result = compareTranslations();
// Display results
displayResults(result);
// Exit with appropriate code
process.exit(result.isValid ? 0 : 1);
}
// Run the validation
main();