Farmbot-Web-App/public/app-resources/languages/_helper.js

326 lines
12 KiB
JavaScript

var HelperNamespace = (function () {
/**
* @desc Build a list of all the files that are children of a directory
* @param {string} dir The directory to search
* @param {list} filelist The list of the directories/files already detected
* @param {string} ext The extension to filter for the files
*/
function walkSync(dir, filelist, ext) {
var path = path || require('path');
var fs = fs || require('fs'),
files = fs.readdirSync(dir);
filelist = filelist || [];
files.forEach(function (file) {
if (fs.statSync(path.join(dir, file)).isDirectory()) {
filelist = walkSync(path.join(dir, file), filelist, ext);
}
else {
if (file.indexOf(ext) > 0)
filelist.push(path.join(dir, file));
}
});
return filelist;
}
/**
* @desc search in the file in parameter to detect the tags
*/
function searchInFile(path, regex) {
var fs = fs || require('fs');
// load the file
var fileContent = fs.readFileSync(path, 'utf8');
var strArray = [];
// match all the groups
var match = regex.exec(fileContent);
while (match != null) {
strArray.push(match[1].replace(/\s+/g, ' '));
match = regex.exec(fileContent);
}
return strArray;
}
/** Locale-aware sort */
function localeSort(a, b) { return a.localeCompare(b); }
// '.t("")' or '{t("")' or ' t("")' or '(t("")' or '[t("")'
// '.t(``)' or '{t(``)' or ' t(``)' or '(t(``)' or '[t(``)'
// Also finds ' t("some {{data}}", \n {data})'
var T_REGEX = /[.{[(\s]t\(["`]([\w\s{}().,:'\-=\\?\/%!]*)["`],*\s*.*\)/g;
// '``'
var C_REGEX = /[`]([\w\s{}().,:'\-=\\?"+!]*)[`].*/g;
/** Some additional phrases the regex can't find. */
var EXTRA_TAGS = [
"Fun", "Warn", "Controls", "Device", "Farm Designer", "on",
"Map Points", "Spread", "Row Spacing", "Height", "Taxon",
"Growing Degree Days", "Svg Icon", "Invalid date", "yes"
];
/**
* Get all the tags in the files with extension .ts of the current project
*/
function getAllTags() {
var srcPath = __dirname + '/../../../frontend';
var listFilteredFiles = walkSync(srcPath, [], '.ts');
var allTags = listFilteredFiles.map(function (x) {
return searchInFile(x, T_REGEX);
});
var constantsTags = searchInFile(srcPath + '/constants.ts', C_REGEX);
var DIAG_MESSAGE_FILE = '/devices/connectivity/diagnostic_messages.ts';
var diagnosticTags = searchInFile(srcPath + DIAG_MESSAGE_FILE, C_REGEX);
// flatten list of list in a simple list
var flatAllTags = [].concat.apply([], allTags);
var flatConstantsTags = [].concat.apply([], constantsTags);
var flatDiagnosticTags = [].concat.apply([], diagnosticTags);
var flatExtraTags = [].concat.apply([], EXTRA_TAGS);
var flattenedTags = [].concat.apply([],
[flatAllTags, flatConstantsTags, flatDiagnosticTags, flatExtraTags]);
// distinct
var uniq = Array.from(new Set(flattenedTags));
var sorted = uniq.sort(localeSort);
return sorted;
}
/**
* For debugging
*/
function logAllTags() {
console.dir(getAllTags());
}
/** For debugging. Replace all translations with a debug string. */
function replaceWithDebugString(key, debugString, debugStringOption) {
var debugChar = debugString[0];
switch (debugStringOption) {
case 'r': return debugString; // replace with: string as provided
case 's': return debugChar; // single character
case 'n': return key.replace(/\S/g, debugChar); // maintain whitespace
case 'l': return debugChar.repeat(key.length); // replace whitespace
default: return key;
}
}
var metrics = [];
/** Generate translation summary data for all languages. */
function generateMetrics() {
var languageCodes = walkSync(__dirname, [], '.json')
.filter(function (s) { return !s.includes('en.js'); })
.filter(function (s) { return !(s.includes('metrics') || s.includes('.md')); })
.map(function (s) { return s.slice(-'en.json'.length, -'.json'.length); });
var fs = fs || require('fs');
var markdown = '';
languageCodes.map(function (lang) {
return createOrUpdateTranslationFile(lang, true);
});
// console.log(metrics);
// var jsonMetrics = JSON.stringify(metrics, undefined, 2);
// fs.writeFileSync(__dirname + '/metrics.json', jsonMetrics);
markdown += '# Translation summary\n\n';
markdown += '_This summary was automatically generated by running the';
markdown += ' language helper._\n\n';
markdown += 'Auto-sort and generate translation file contents using:\n\n';
markdown += '```bash\nnode public/app-resources/languages/_helper.js en\n```\n\n';
markdown += 'Where `en` is your language code.\n\n';
markdown += 'Translation file format can be checked using:\n\n';
markdown += '```bash\nnpm run translation-check\n```\n\n';
markdown += 'See the [README](https://github.com/FarmBot/Farmbot-Web-App';
markdown += '#translating-the-web-app-into-your-language) for contribution';
markdown += ' instructions.\n\n';
markdown += 'Total number of phrases identified by the language helper';
markdown += ' for translation: __' + metrics[0].current + '__\n\n';
markdown += '|Language|Percent translated';
markdown += '|Translated|Untranslated|Other Translations|\n';
markdown += '|:---:|---:|---:|---:|---:|\n';
metrics.map(function (langMetrics) {
markdown += '|' + langMetrics.language;
markdown += '|' + langMetrics.percent + '%';
markdown += '|' + langMetrics.translated;
markdown += '|' + langMetrics.untranslated;
markdown += '|' + langMetrics.orphans;
markdown += '|\n';
});
fs.writeFileSync(__dirname + '/translation_metrics.md', markdown);
}
/** Print some translation file status metrics. */
function generateSummary(args) {
// {foundTags, unmatchedTags, untranslated, translated, countExisting, langCode}
var current = Object.keys(args.foundTags).length;
var orphans = Object.keys(args.unmatchedTags).length;
var untranslated = Object.keys(args.untranslated).length;
var translated = Object.keys(args.translated).length;
var total = untranslated + translated + orphans;
var percent = Math.round(translated / current * 100);
var existingUntranslated = args.countExisting - translated;
if (!args.metricsOnly) {
console.log(current + ' strings found.');
console.log(' ' + args.countExisting + ' existing items match.');
console.log(' ' + translated + ' existing translations match.');
console.log(' ' + existingUntranslated + ' existing untranslated items.');
console.log(' ' + (current - args.countExisting) + ' new items added.');
console.log(percent + '% of found strings translated.');
console.log(orphans + ' unused, outdated, or extra items.');
console.log('Updated file (' + args.langCode + '.js) with ' + total + ' items.');
}
return {
percent: percent, orphans: orphans, total: total, untranslated: untranslated,
current: current, translated: translated, language: args.langCode
};
}
/**
* Create the translation file or update it with new tags
* The tags are in the following order:
* 1. New tags in English that need to be translated (ASC)
* 2. Tags already translated that match an existing tag in src (ASC)
* 3. Tags already in the file before but not found at the moment in src (ASC)
* @param {string} lang The short name of the language.
*/
function createOrUpdateTranslationFile(lang, metricsOnly) {
lang = lang || 'en';
// check current file entry
var langFilePath = __dirname + '/' + lang + '.json';
var fs = fs || require('fs');
try {
var columnsResult = HelperNamespace.getAllTags();
var jsonCurrentTagData = {};
columnsResult.forEach(function (column) {
jsonCurrentTagData[column] = column;
});
var ordered = {};
var fileContent;
try {
// check the file can be opened
var stats = fs.statSync(langFilePath);
// load the file
fileContent = fs.readFileSync(langFilePath, 'utf8');
if (lang == 'en') {
console.log('Current file (' + lang + '.json) content: ');
console.log(fileContent);
console.log('Try entering a language code.');
console.log('For example: node _helper.js en');
if (!metricsOnly) { generateMetrics(); }
return;
}
}
catch (e) {
if (!metricsOnly) {
console.log('we will create the file: ' + langFilePath);
}
// If there is no current file, we will create it
}
try {
if (fileContent != undefined) {
var jsonParsed = JSON.parse(fileContent);
var combinedContent = jsonParsed.translated;
if ('untranslated' in jsonParsed) {
for (var untranslated_key in jsonParsed.untranslated) {
combinedContent[untranslated_key] = jsonParsed.untranslated[untranslated_key];
}
}
if ('other_translations' in jsonParsed) {
for (var other_key in jsonParsed.other_translations) {
combinedContent[other_key] = jsonParsed.other_translations[other_key];
}
}
var count = Object.keys(combinedContent).length;
if (!metricsOnly) {
console.log('Loaded file ' + lang + '.json with ' + count + ' items.');
}
Object.keys(combinedContent).sort().forEach(function (key) {
ordered[key] = combinedContent[key];
});
}
} catch (e) {
if (!metricsOnly) {
console.log('file: ' + langFilePath + ' contains an error: ' + e);
}
// If there is an error with the current file content, abort
return;
}
// For debugging
var debug = process.argv[3];
var debugOption = process.argv[4];
// merge new tags with existing translation
var untranslated = {};
var translated = {};
var other_translations = {};
var existing = 0;
// all current tags in English
Object.keys(jsonCurrentTagData).sort(localeSort).map(function (key) {
untranslated[key] = jsonCurrentTagData[key];
if (debug) {
untranslated[key] = replaceWithDebugString(key, debug, debugOption);
}
});
for (var key in ordered) {
// replace current tag with an existing translation
if (untranslated.hasOwnProperty(key)) {
existing++;
if (key !== ordered[key]) {
delete untranslated[key];
translated[key] = ordered[key];
if (debug) {
translated[key] = replaceWithDebugString(key, debug, debugOption);
}
}
}
// if the tag doesn't exist but a translation exists,
// put the key/value at the end of the json
else {
other_translations[key] = ordered[key];
}
}
var summaryData = generateSummary({
langCode: lang, untranslated: untranslated,
foundTags: jsonCurrentTagData, unmatchedTags: other_translations,
translated: translated, countExisting: existing,
metricsOnly: metricsOnly
});
if (metricsOnly) { metrics.push(summaryData); }
var jsonContent = {
translated: translated,
untranslated: untranslated,
other_translations: other_translations,
};
var stringJson = JSON.stringify(jsonContent, null, 2);
stringJson += '\n';
if (!metricsOnly) { fs.writeFileSync(langFilePath, stringJson); }
} catch (e) {
if (!metricsOnly) {
console.log('file: ' + langFilePath + '. error append: ' + e);
}
}
if (!metricsOnly) { generateMetrics(); }
}
// public functions
return {
logAllTags: logAllTags,
getAllTags: getAllTags,
createOrUpdateTranslationFile: createOrUpdateTranslationFile
};
})();
// Need to run this cmd in this folder: node _helper.js
var language = process.argv[2];
HelperNamespace.createOrUpdateTranslationFile(language, false);