358 lines
14 KiB
JavaScript
358 lines
14 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", "Tools",
|
|
"Messages", "Sequence Editor", "Commands", "Regimen Editor", "Scheduler",
|
|
"Farmware List", "SYNC NOW", "Test", "SYNCING", "SYNCED", "UNKNOWN",
|
|
"Else Execute", "Connecting FarmBot to the Internet", "move to home",
|
|
"emergency stop", "SYNC ERROR", "inactive", "error", "No messages.",
|
|
"back to regimens", "back to sequences", "back to farmware list",
|
|
];
|
|
|
|
/**
|
|
* 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);
|
|
|
|
// flatten list of list in a simple list
|
|
var flatAllTags = [].concat.apply([], allTags);
|
|
var flatConstantsTags = [].concat.apply([], constantsTags);
|
|
var flatExtraTags = [].concat.apply([], EXTRA_TAGS);
|
|
var flattenedTags = [].concat.apply([],
|
|
[flatAllTags, flatConstantsTags, 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 characters except whitespace and {{ words }} */
|
|
function repl(string, character) {
|
|
var parts = string.split("{{");
|
|
if (parts.length < 2) { return string.replace(/\S/g, character); }
|
|
var insideAndAfter = parts[1].split("}}");
|
|
var before = parts[0].replace(/\S/g, character);
|
|
var inside = insideAndAfter[0];
|
|
var after = insideAndAfter[1].replace(/\S/g, character);
|
|
var firstPart = [before, inside].join("{{");
|
|
return [firstPart, after].join("}}");
|
|
}
|
|
|
|
/** 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 repl(key, 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';
|
|
});
|
|
markdown += '\n**Percent translated** refers to the percent of phrases';
|
|
markdown += ' identified by the\nlanguage helper that have been';
|
|
markdown += ' translated. Additional phrases not identified\n';
|
|
markdown += 'by the language helper may exist in the Web App.\n\n';
|
|
markdown += '\n**Untranslated** includes phrases not yet translated';
|
|
markdown += ' or phrases that do not\nneed translation. Phrases that are';
|
|
markdown += ' identical before and after translation\ncan be moved to';
|
|
markdown += ' `translated` to indicate translation status to the language';
|
|
markdown += '\nhelper.\n\n';
|
|
markdown += '**Other Translations** include translated phrases';
|
|
markdown += ' that do not match any of\nthe phrases identified by the';
|
|
markdown += ' language helper. These are usually phrases\nnot identified';
|
|
markdown += ' by the language helper or phrases that have been changed';
|
|
markdown += '\nor removed from the Web App.';
|
|
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 translatedKeys = [];
|
|
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);
|
|
translatedKeys.push.apply(
|
|
translatedKeys, Object.keys(JSON.parse(fileContent).translated));
|
|
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(localeSort).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]) || translatedKeys.includes(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);
|