zlocalz
Version:
ZLocalz - TUI Locale Guardian for Flutter ARB l10n/i18n validation and translation with AI-powered fixes
259 lines • 9.62 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Validator = void 0;
const parser_1 = require("@messageformat/parser");
class Validator {
sourceFile;
targetFiles;
constructor(sourceFile, targetFiles) {
this.sourceFile = sourceFile;
this.targetFiles = targetFiles;
}
validate() {
const reports = new Map();
for (const targetFile of this.targetFiles) {
const issues = this.validateTarget(targetFile);
const stats = this.calculateStats(issues);
reports.set(targetFile.locale, {
locale: targetFile.locale,
format: targetFile.format,
issues,
stats
});
}
return reports;
}
validateTarget(targetFile) {
const issues = [];
issues.push(...this.checkMissingKeys(targetFile));
issues.push(...this.checkExtraKeys(targetFile));
issues.push(...this.checkDuplicates(targetFile));
issues.push(...this.checkPlaceholders(targetFile));
issues.push(...this.checkICUMessages(targetFile));
issues.push(...this.checkFormatting(targetFile));
return issues;
}
checkMissingKeys(targetFile) {
const issues = [];
const sourceKeys = Object.keys(this.sourceFile.entries);
const targetKeys = new Set(Object.keys(targetFile.entries));
for (const key of sourceKeys) {
if (!targetKeys.has(key)) {
issues.push({
type: 'missing',
locale: targetFile.locale,
key,
message: `Key "${key}" is missing`,
severity: 'error',
sourceValue: this.sourceFile.entries[key].value
});
}
}
return issues;
}
checkExtraKeys(targetFile) {
const issues = [];
const sourceKeys = new Set(Object.keys(this.sourceFile.entries));
const targetKeys = Object.keys(targetFile.entries);
for (const key of targetKeys) {
if (!sourceKeys.has(key)) {
issues.push({
type: 'extra',
locale: targetFile.locale,
key,
message: `Key "${key}" not in source locale`,
severity: 'warning',
targetValue: targetFile.entries[key].value
});
}
}
return issues;
}
checkDuplicates(targetFile) {
const issues = [];
const seenValues = new Map();
for (const [key, entry] of Object.entries(targetFile.entries)) {
const value = entry.value.trim();
if (!seenValues.has(value)) {
seenValues.set(value, []);
}
seenValues.get(value).push(key);
}
for (const [value, keys] of seenValues.entries()) {
if (keys.length > 1) {
for (let i = 1; i < keys.length; i++) {
issues.push({
type: 'duplicate',
locale: targetFile.locale,
key: keys[i],
message: `Duplicate value with key "${keys[0]}"`,
severity: 'warning',
targetValue: value,
suggestion: `Remove duplicate or differentiate from "${keys[0]}"`
});
}
}
}
return issues;
}
checkPlaceholders(targetFile) {
const issues = [];
for (const key of Object.keys(this.sourceFile.entries)) {
if (!targetFile.entries[key])
continue;
const sourcePlaceholders = this.extractPlaceholders(this.sourceFile.entries[key].value);
const targetPlaceholders = this.extractPlaceholders(targetFile.entries[key].value);
const targetPSet = new Set(targetPlaceholders);
if (sourcePlaceholders.length !== targetPlaceholders.length ||
!sourcePlaceholders.every(p => targetPSet.has(p))) {
issues.push({
type: 'placeholderMismatch',
locale: targetFile.locale,
key,
message: `Placeholder mismatch: expected [${sourcePlaceholders.join(', ')}], found [${targetPlaceholders.join(', ')}]`,
severity: 'error',
sourceValue: this.sourceFile.entries[key].value,
targetValue: targetFile.entries[key].value
});
}
}
return issues;
}
checkICUMessages(targetFile) {
const issues = [];
for (const key of Object.keys(this.sourceFile.entries)) {
if (!targetFile.entries[key])
continue;
const sourceValue = this.sourceFile.entries[key].value;
const targetValue = targetFile.entries[key].value;
if (this.isICUMessage(sourceValue)) {
try {
const sourceAST = (0, parser_1.parse)(sourceValue);
const targetAST = (0, parser_1.parse)(targetValue);
if (!this.compareICUStructure(sourceAST, targetAST)) {
issues.push({
type: 'icuError',
locale: targetFile.locale,
key,
message: 'ICU structure mismatch with source',
severity: 'error',
sourceValue,
targetValue
});
}
}
catch (error) {
issues.push({
type: 'icuError',
locale: targetFile.locale,
key,
message: `Invalid ICU message: ${error}`,
severity: 'error',
targetValue
});
}
}
}
return issues;
}
checkFormatting(targetFile) {
const issues = [];
for (const [key, entry] of Object.entries(targetFile.entries)) {
const value = entry.value;
if (value !== value.trim()) {
issues.push({
type: 'formatting',
locale: targetFile.locale,
key,
message: 'Value has leading or trailing whitespace',
severity: 'warning',
targetValue: value,
suggestion: value.trim()
});
}
if (value.includes(' ')) {
issues.push({
type: 'formatting',
locale: targetFile.locale,
key,
message: 'Value contains multiple consecutive spaces',
severity: 'warning',
targetValue: value,
suggestion: value.replace(/\s+/g, ' ')
});
}
}
return issues;
}
isICUMessage(value) {
return value.includes('{') && (value.includes(', plural,') ||
value.includes(', select,') ||
value.includes(', selectordinal,'));
}
compareICUStructure(ast1, ast2) {
return JSON.stringify(this.extractICUStructure(ast1)) ===
JSON.stringify(this.extractICUStructure(ast2));
}
extractICUStructure(ast) {
if (Array.isArray(ast)) {
return ast.map(node => this.extractICUStructure(node));
}
if (ast && typeof ast === 'object') {
const structure = { type: ast.type };
if (ast.argument)
structure.argument = ast.argument;
if (ast.pluralType)
structure.pluralType = ast.pluralType;
if (ast.options) {
structure.options = Object.keys(ast.options).sort();
}
return structure;
}
return null;
}
calculateStats(issues) {
const stats = {
totalKeys: Object.keys(this.sourceFile.entries).length,
missingKeys: 0,
extraKeys: 0,
duplicates: 0,
icuErrors: 0,
placeholderMismatches: 0,
formattingWarnings: 0
};
for (const issue of issues) {
switch (issue.type) {
case 'missing':
stats.missingKeys++;
break;
case 'extra':
stats.extraKeys++;
break;
case 'duplicate':
stats.duplicates++;
break;
case 'icuError':
stats.icuErrors++;
break;
case 'placeholderMismatch':
stats.placeholderMismatches++;
break;
case 'formatting':
stats.formattingWarnings++;
break;
}
}
return stats;
}
extractPlaceholders(value) {
const placeholderRegex = /\{([^}]+)\}/g;
const placeholders = [];
let match;
while ((match = placeholderRegex.exec(value)) !== null) {
placeholders.push(match[1]);
}
return placeholders;
}
}
exports.Validator = Validator;
//# sourceMappingURL=validator.js.map