add translation process badge
This commit is contained in:
@@ -0,0 +1,322 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const FRONTEND_LOCALES_DIR = path.join(__dirname, '..', '..', 'src', 'locales');
|
||||
const BACKEND_LOCALES_DIR = path.join(__dirname, '..', '..', 'pkg', 'locales');
|
||||
const OUTPUT_DIR = process.argv[2] || path.join(__dirname, '..', '..', 'i18n-badge');
|
||||
|
||||
const DEFAULT_LANGUAGE_TAG = 'en';
|
||||
|
||||
const BACKEND_SKIP_STRUCTS = new Set([
|
||||
'GlobalTextItems',
|
||||
'DefaultTypes',
|
||||
'DataConverterTextItems',
|
||||
]);
|
||||
|
||||
function discoverFrontendLanguages() {
|
||||
const indexPath = path.join(FRONTEND_LOCALES_DIR, 'index.ts');
|
||||
const content = fs.readFileSync(indexPath, 'utf-8');
|
||||
|
||||
const importMap = {};
|
||||
const importRegex = /import\s+(\w+)\s+from\s+['"]\.\/([\w_]+\.json)['"]/g;
|
||||
let match;
|
||||
|
||||
while ((match = importRegex.exec(content)) !== null) {
|
||||
importMap[match[1]] = match[2];
|
||||
}
|
||||
|
||||
const result = {};
|
||||
const langRegex = /['"]([^'"]+)['"]\s*:\s*\{[^}]*content\s*:\s*(\w+)/g;
|
||||
|
||||
while ((match = langRegex.exec(content)) !== null) {
|
||||
const tag = match[1];
|
||||
const varName = match[2];
|
||||
|
||||
if (importMap[varName]) {
|
||||
result[tag] = importMap[varName];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function discoverBackendLanguages() {
|
||||
const allLocalesPath = path.join(BACKEND_LOCALES_DIR, 'all_locales.go');
|
||||
const content = fs.readFileSync(allLocalesPath, 'utf-8');
|
||||
|
||||
const result = {};
|
||||
const entryRegex = /"([^"]+)"\s*:\s*\{[^}]*Content\s*:\s*(\w+)/g;
|
||||
let match;
|
||||
|
||||
while ((match = entryRegex.exec(content)) !== null) {
|
||||
const tag = match[1];
|
||||
const fileName = tag.toLowerCase().replace(/-/g, '_') + '.go';
|
||||
const filePath = path.join(BACKEND_LOCALES_DIR, fileName);
|
||||
|
||||
if (fs.existsSync(filePath)) {
|
||||
result[tag] = fileName;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function flattenJSON(obj, prefix) {
|
||||
const result = {};
|
||||
|
||||
for (const key of Object.keys(obj)) {
|
||||
const fullKey = prefix ? prefix + '.' + key : key;
|
||||
|
||||
if (typeof obj[key] === 'object' && obj[key] !== null) {
|
||||
Object.assign(result, flattenJSON(obj[key], fullKey));
|
||||
} else {
|
||||
result[fullKey] = obj[key];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function shouldSkipFrontendKey(key) {
|
||||
if (key.startsWith('global.')) {
|
||||
return true;
|
||||
} else if (key.startsWith('default.')) {
|
||||
return true;
|
||||
} else if (key.startsWith('format.')) {
|
||||
if (key.startsWith('format.misc.')) {
|
||||
if (key === 'format.misc.multiTextJoinSeparator') {
|
||||
return true;
|
||||
} else if (key === 'format.misc.eachMonthDayInMonthDays') {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} else if (key.startsWith('datetime.')) {
|
||||
return true;
|
||||
} else if (key.startsWith('timezone.')) {
|
||||
return true;
|
||||
} else if (key.startsWith('currency.')) {
|
||||
return true;
|
||||
} else if (key.startsWith('mapprovider.')) {
|
||||
return true;
|
||||
} else if (key.startsWith('parameter.id')) {
|
||||
return true;
|
||||
} else if (key.startsWith('encoding.')) {
|
||||
return true;
|
||||
} else if (key.startsWith('document.anchor.')) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function extractGoStringFields(content) {
|
||||
const fields = [];
|
||||
const structBlockRegex = /(\w+):\s*&\w+\{([^}]*)\}/gs;
|
||||
let blockMatch;
|
||||
|
||||
while ((blockMatch = structBlockRegex.exec(content)) !== null) {
|
||||
const structName = blockMatch[1];
|
||||
const blockBody = blockMatch[2];
|
||||
const fieldRegex = /(\w+):\s+"((?:[^"\\]|\\.)*)"/g;
|
||||
let fieldMatch;
|
||||
|
||||
while ((fieldMatch = fieldRegex.exec(blockBody)) !== null) {
|
||||
fields.push({
|
||||
struct: structName,
|
||||
name: fieldMatch[1],
|
||||
value: fieldMatch[2],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
function getProgressColor(progress) {
|
||||
if (progress >= 95) {
|
||||
return 'brightgreen';
|
||||
} else if (progress >= 90) {
|
||||
return 'green';
|
||||
} else if (progress >= 70) {
|
||||
return 'yellowgreen';
|
||||
} else if (progress >= 50) {
|
||||
return 'yellow';
|
||||
} else if (progress >= 20) {
|
||||
return 'orange';
|
||||
} else {
|
||||
return 'red';
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
const frontendLangs = discoverFrontendLanguages();
|
||||
const backendLangs = discoverBackendLanguages();
|
||||
const allTags = new Set([...Object.keys(frontendLangs), ...Object.keys(backendLangs)]);
|
||||
|
||||
console.log('Discovered ' + allTags.size + ' languages: ' + [...allTags].sort().join(', '));
|
||||
|
||||
const defaultFrontendJSON = JSON.parse(fs.readFileSync(path.join(FRONTEND_LOCALES_DIR, `${DEFAULT_LANGUAGE_TAG}.json`), 'utf-8'));
|
||||
const defaultFrontendItemsMap = flattenJSON(defaultFrontendJSON, '');
|
||||
const defaultFrontendKeys = Object.keys(defaultFrontendItemsMap);
|
||||
const frontendTranslatableKeys = defaultFrontendKeys.filter(function (k) {
|
||||
return !shouldSkipFrontendKey(k);
|
||||
});
|
||||
const frontendSkippedCount = defaultFrontendKeys.length - frontendTranslatableKeys.length;
|
||||
const frontendTotal = frontendTranslatableKeys.length;
|
||||
|
||||
const defaultBackendContent = fs.readFileSync(path.join(BACKEND_LOCALES_DIR, `${DEFAULT_LANGUAGE_TAG}.go`), 'utf-8');
|
||||
const defaultBackendItems = extractGoStringFields(defaultBackendContent);
|
||||
const defaultBackendTranslatableItems = defaultBackendItems.filter(function (f) {
|
||||
return !BACKEND_SKIP_STRUCTS.has(f.struct);
|
||||
});
|
||||
const backendSkippedCount = defaultBackendItems.length - defaultBackendTranslatableItems.length;
|
||||
const backendTotal = defaultBackendTranslatableItems.length;
|
||||
|
||||
console.log('Frontend: ' + frontendTotal + ' translatable keys (' + frontendSkippedCount + ' excluded)');
|
||||
console.log('Backend: ' + backendTotal + ' translatable fields (' + backendSkippedCount + ' excluded)');
|
||||
|
||||
const results = {};
|
||||
const untranslatedKeys = {};
|
||||
|
||||
for (const tag of allTags) {
|
||||
results[tag] = {
|
||||
languageTag: tag,
|
||||
frontendTranslated: 0,
|
||||
frontendTotal: frontendTotal,
|
||||
backendTranslated: 0,
|
||||
backendTotal: backendTotal
|
||||
};
|
||||
untranslatedKeys[tag] = [];
|
||||
}
|
||||
|
||||
for (const tag of Object.keys(frontendLangs)) {
|
||||
if (tag === DEFAULT_LANGUAGE_TAG) {
|
||||
results[tag].frontendTranslated = frontendTotal;
|
||||
continue;
|
||||
}
|
||||
|
||||
const file = frontendLangs[tag];
|
||||
const filePath = path.join(FRONTEND_LOCALES_DIR, file);
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const json = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
||||
const kv = flattenJSON(json, '');
|
||||
let translated = 0;
|
||||
|
||||
for (const key of frontendTranslatableKeys) {
|
||||
if (kv[key] !== undefined && kv[key] !== '' && kv[key] !== defaultFrontendItemsMap[key]) {
|
||||
translated++;
|
||||
} else {
|
||||
untranslatedKeys[tag].push({ source: 'frontend', key: key, defaultValue: defaultFrontendItemsMap[key], value: kv[key] });
|
||||
}
|
||||
}
|
||||
|
||||
results[tag].frontendTranslated = translated;
|
||||
}
|
||||
|
||||
for (const tag of Object.keys(backendLangs)) {
|
||||
if (tag === DEFAULT_LANGUAGE_TAG) {
|
||||
results[tag].backendTranslated = backendTotal;
|
||||
continue;
|
||||
}
|
||||
|
||||
const file = backendLangs[tag];
|
||||
const filePath = path.join(BACKEND_LOCALES_DIR, file);
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const fields = extractGoStringFields(content).filter(function (f) {
|
||||
return !BACKEND_SKIP_STRUCTS.has(f.struct);
|
||||
});
|
||||
let translated = 0;
|
||||
|
||||
for (let i = 0; i < defaultBackendTranslatableItems.length; i++) {
|
||||
if (i < fields.length && fields[i].value !== defaultBackendTranslatableItems[i].value) {
|
||||
translated++;
|
||||
} else {
|
||||
untranslatedKeys[tag].push({ source: 'backend', key: defaultBackendTranslatableItems[i].struct + '.' + defaultBackendTranslatableItems[i].name, defaultValue: defaultBackendTranslatableItems[i].value, value: (i < fields.length) ? fields[i].value : null });
|
||||
}
|
||||
}
|
||||
|
||||
results[tag].backendTranslated = translated;
|
||||
}
|
||||
|
||||
for (const tag of Object.keys(results)) {
|
||||
const r = results[tag];
|
||||
const totalTranslated = r.frontendTranslated + r.backendTranslated;
|
||||
const totalItems = r.frontendTotal + r.backendTotal;
|
||||
r.totalProgress = Math.round((totalTranslated / totalItems) * 10000) / 100;
|
||||
}
|
||||
|
||||
const sortedResults = {};
|
||||
var sortedTags = Object.keys(results).sort();
|
||||
|
||||
for (const tag of sortedTags) {
|
||||
sortedResults[tag] = results[tag];
|
||||
}
|
||||
|
||||
if (!fs.existsSync(OUTPUT_DIR)) {
|
||||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
var badgesDir = path.join(OUTPUT_DIR, 'badges');
|
||||
|
||||
if (!fs.existsSync(badgesDir)) {
|
||||
fs.mkdirSync(badgesDir, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(OUTPUT_DIR, 'i18n-progress.json'),
|
||||
JSON.stringify(sortedResults, null, 4) + '\n'
|
||||
);
|
||||
|
||||
for (const tag of sortedTags) {
|
||||
const data = sortedResults[tag];
|
||||
const badge = {
|
||||
schemaVersion: 1,
|
||||
label: 'translation',
|
||||
message: data.totalProgress + '%',
|
||||
color: getProgressColor(data.totalProgress)
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(badgesDir, tag + '.json'),
|
||||
JSON.stringify(badge, null, 4) + '\n'
|
||||
);
|
||||
}
|
||||
|
||||
var untranslatedDir = path.join(OUTPUT_DIR, 'untranslated');
|
||||
|
||||
if (!fs.existsSync(untranslatedDir)) {
|
||||
fs.mkdirSync(untranslatedDir, { recursive: true });
|
||||
}
|
||||
|
||||
for (const tag of sortedTags) {
|
||||
const items = untranslatedKeys[tag] || [];
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(untranslatedDir, tag + '.json'),
|
||||
JSON.stringify(items, null, 4) + '\n'
|
||||
);
|
||||
}
|
||||
|
||||
for (const tag of sortedTags) {
|
||||
const data = sortedResults[tag];
|
||||
const missingCount = (untranslatedKeys[tag] || []).length;
|
||||
console.log(tag + ': ' + data.totalProgress + '% (frontend: ' + data.frontendTranslated + '/' + data.frontendTotal + ', backend: ' + data.backendTranslated + '/' + data.backendTotal + ', untranslated: ' + missingCount + ')');
|
||||
}
|
||||
|
||||
console.log('\nResults written to ' + OUTPUT_DIR);
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user