zlocalz
Version:
ZLocalz - TUI Locale Guardian for Flutter ARB l10n/i18n validation and translation with AI-powered fixes
367 lines • 14.1 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.UniversalParser = void 0;
const fs = __importStar(require("fs/promises"));
const path = __importStar(require("path"));
const fast_glob_1 = __importDefault(require("fast-glob"));
const YAML = __importStar(require("yaml"));
const sync_1 = require("csv-parse/sync");
const sync_2 = require("csv-stringify/sync");
class UniversalParser {
config;
constructor(config) {
this.config = config;
}
static detectFormat(filePath) {
const ext = path.extname(filePath).toLowerCase();
switch (ext) {
case '.arb':
return 'arb';
case '.json':
return 'json';
case '.yaml':
case '.yml':
return 'yaml';
case '.csv':
return 'csv';
case '.tsv':
return 'tsv';
default:
return 'json';
}
}
static getFilePatterns(format) {
switch (format) {
case 'arb':
return ['**/*.arb'];
case 'json':
return ['**/*.json'];
case 'yaml':
return ['**/*.yaml', '**/*.yml'];
case 'csv':
return ['**/*.csv'];
case 'tsv':
return ['**/*.tsv'];
case 'auto':
default:
return ['**/*.arb', '**/*.json', '**/*.yaml', '**/*.yml', '**/*.csv', '**/*.tsv'];
}
}
async discoverFiles(basePath) {
const format = this.config.fileFormat || 'auto';
const patterns = this.config.filePattern
? [this.config.filePattern]
: UniversalParser.getFilePatterns(format);
const files = await (0, fast_glob_1.default)(patterns, {
cwd: basePath,
absolute: true,
ignore: ['**/node_modules/**', '**/build/**', '**/.*/**']
});
return files.sort();
}
async parseAllLocalesFromFile(filePath) {
const format = UniversalParser.detectFormat(filePath);
if (format === 'csv' || format === 'tsv') {
const content = await fs.readFile(filePath, 'utf-8');
const delimiter = format === 'tsv' ? '\t' : (this.config.csvOptions?.delimiter || ',');
const records = (0, sync_1.parse)(content, {
delimiter,
columns: true,
skip_empty_lines: true
});
const locales = [];
const allLocales = new Set();
if (records.length > 0) {
const columns = Object.keys(records[0]);
for (const column of columns) {
if (column !== (this.config.csvOptions?.keyColumn || 'key') &&
column !== 'description' &&
column !== 'context') {
allLocales.add(column);
}
}
}
for (const locale of allLocales) {
if (this.config.sourceLocale === locale || this.config.targetLocales.includes(locale)) {
const entries = this.parseCsvEntries(records, locale);
const stats = await fs.stat(filePath);
locales.push({
locale,
path: filePath,
format,
entries,
raw: { records, locale },
lastModified: stats.mtime
});
}
}
return locales;
}
else {
const localeFile = await this.parseFile(filePath);
return [localeFile];
}
}
async parseFile(filePath, targetLocale) {
const format = UniversalParser.detectFormat(filePath);
const content = await fs.readFile(filePath, 'utf-8');
const locale = targetLocale || this.extractLocale(filePath, format);
let raw;
let entries = {};
switch (format) {
case 'arb':
case 'json':
raw = JSON.parse(content);
entries = this.parseJsonEntries(raw);
break;
case 'yaml':
raw = YAML.parse(content) || {};
entries = this.parseYamlEntries(raw, locale);
break;
case 'csv':
case 'tsv':
const delimiter = format === 'tsv' ? '\t' : (this.config.csvOptions?.delimiter || ',');
const records = (0, sync_1.parse)(content, {
delimiter,
columns: true,
skip_empty_lines: true
});
raw = { records, locale };
entries = this.parseCsvEntries(records, locale);
break;
default:
throw new Error(`Unsupported file format: ${format}`);
}
const stats = await fs.stat(filePath);
return {
locale,
path: filePath,
format,
entries,
raw,
lastModified: stats.mtime
};
}
parseJsonEntries(raw) {
const entries = {};
const metadataPrefix = '@';
for (const [key, value] of Object.entries(raw)) {
if (key.startsWith(metadataPrefix))
continue;
if (key.startsWith('@@'))
continue;
const entry = {
key,
value: String(value)
};
const metadataKey = `${metadataPrefix}${key}`;
if (raw[metadataKey]) {
const metadata = raw[metadataKey];
entry.metadata = metadata;
entry.description = metadata.description;
entry.placeholders = metadata.placeholders;
}
entries[key] = entry;
}
return entries;
}
parseYamlEntries(raw, _locale) {
const entries = {};
const flatten = (obj, prefix = '') => {
for (const [key, value] of Object.entries(obj)) {
const fullKey = prefix ? `${prefix}.${key}` : key;
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
const valueObj = value;
if (valueObj.value !== undefined) {
entries[fullKey] = {
key: fullKey,
value: String(valueObj.value),
description: valueObj.description,
context: valueObj.context,
tags: valueObj.tags,
metadata: valueObj
};
}
else {
flatten(value, fullKey);
}
}
else {
entries[fullKey] = {
key: fullKey,
value: String(value)
};
}
}
};
flatten(raw);
return entries;
}
parseCsvEntries(records, locale) {
const entries = {};
const keyColumn = this.config.csvOptions?.keyColumn || 'key';
const valueColumns = this.config.csvOptions?.valueColumns || { [locale]: locale };
const valueColumn = valueColumns[locale] || locale;
for (const record of records) {
const key = record[keyColumn];
const value = record[valueColumn];
if (key && value) {
entries[key] = {
key,
value: String(value),
description: record.description,
context: record.context,
tags: record.tags ? record.tags.split(',').map((t) => t.trim()) : undefined,
metadata: record
};
}
}
return entries;
}
async writeFile(localeFile) {
let content;
switch (localeFile.format) {
case 'arb':
case 'json':
content = this.generateJsonContent(localeFile);
break;
case 'yaml':
content = this.generateYamlContent(localeFile);
break;
case 'csv':
case 'tsv':
content = this.generateCsvContent(localeFile);
break;
default:
throw new Error(`Unsupported file format: ${localeFile.format}`);
}
await fs.writeFile(localeFile.path, content, 'utf-8');
}
generateJsonContent(localeFile) {
const output = {};
if (localeFile.format === 'arb' && localeFile.raw['@@locale']) {
output['@@locale'] = localeFile.raw['@@locale'];
}
const keys = Object.keys(localeFile.entries);
if (this.config.preferOrder === 'alphabetical') {
keys.sort();
}
for (const key of keys) {
const entry = localeFile.entries[key];
output[key] = entry.value;
if (localeFile.format === 'arb' && (entry.metadata || entry.description || entry.placeholders)) {
const metadata = {};
if (entry.description)
metadata.description = entry.description;
if (entry.placeholders)
metadata.placeholders = entry.placeholders;
if (entry.metadata)
Object.assign(metadata, entry.metadata);
if (Object.keys(metadata).length > 0) {
output[`@${key}`] = metadata;
}
}
}
return JSON.stringify(output, null, 2) + '\n';
}
generateYamlContent(localeFile) {
const output = {};
for (const [key, entry] of Object.entries(localeFile.entries)) {
const keyParts = key.split('.');
let current = output;
for (let i = 0; i < keyParts.length - 1; i++) {
if (!current[keyParts[i]]) {
current[keyParts[i]] = {};
}
current = current[keyParts[i]];
}
const finalKey = keyParts[keyParts.length - 1];
if (entry.description || entry.context || entry.tags || entry.metadata) {
current[finalKey] = {
value: entry.value,
...(entry.description && { description: entry.description }),
...(entry.context && { context: entry.context }),
...(entry.tags && { tags: entry.tags })
};
}
else {
current[finalKey] = entry.value;
}
}
return YAML.stringify(output, { indent: 2 });
}
generateCsvContent(localeFile) {
const delimiter = localeFile.format === 'tsv' ? '\t' : (this.config.csvOptions?.delimiter || ',');
const keyColumn = this.config.csvOptions?.keyColumn || 'key';
const valueColumn = this.config.csvOptions?.valueColumns?.[localeFile.locale] || localeFile.locale;
const records = Object.values(localeFile.entries).map(entry => ({
[keyColumn]: entry.key,
[valueColumn]: entry.value,
...(entry.description && { description: entry.description }),
...(entry.context && { context: entry.context }),
...(entry.tags && { tags: entry.tags.join(', ') })
}));
return (0, sync_2.stringify)(records, {
delimiter,
header: true,
quoted_string: true
});
}
extractLocale(filePath, _format) {
const basename = path.basename(filePath, path.extname(filePath));
const patterns = [
/^(.+)_([a-z]{2}(?:_[A-Z]{2})?)$/,
/^([a-z]{2}(?:[-_][A-Z]{2})?)$/,
/^([a-z]{2}(?:[-_][A-Z]{2})?)[\\/](.+)$/
];
for (const pattern of patterns) {
const match = basename.match(pattern);
if (match) {
return match[match.length - 1].replace('-', '_');
}
}
const parentDir = path.basename(path.dirname(filePath));
if (/^[a-z]{2}(?:[-_][A-Z]{2})?$/.test(parentDir)) {
return parentDir.replace('-', '_');
}
return 'unknown';
}
}
exports.UniversalParser = UniversalParser;
//# sourceMappingURL=universal-parser.js.map