diff --git a/gulpfile.js b/gulpfile.js index 241ed350c..d8ebc6327 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -204,7 +204,9 @@ const lintJS = () => { .pipe(eslint.failAfterError()); }; -const lint = gulp.series(lintJS, lintStylus); +const lintI18n = () => require('./node_requires/i18n')().then(console.log); + +const lint = gulp.series(lintJS, lintStylus, lintI18n); const launchApp = () => { spawnise.spawn(npm, ['run', 'start'], { diff --git a/node_requires/i18n/index.js b/node_requires/i18n/index.js new file mode 100644 index 000000000..a628a6087 --- /dev/null +++ b/node_requires/i18n/index.js @@ -0,0 +1,118 @@ +const i18nDir = './../../app/data/i18n'; +const path = require('path'), + fs = require('fs-extra'); +const referenceLanguage = require(path.join(i18nDir, 'English.json')); + +const recursiveCountRemainingFields = node => { + if (typeof node === 'string') { + return 1; + } + let counted = 0; + for (const i in node) { + counted += recursiveCountRemainingFields(node[i]); + } + return counted; +}; + +// Recursively counts keys in English.json, translated keys, +// and collects paths to untranslated and excess keys +const recursiveCheck = function(partial, langNode, referenceNode) { + const untranslated = [], + excess = []; + let counted = 0, + needed = 0; + for (const i in referenceNode) { + if (typeof referenceNode[i] === 'string') { + needed++; + if (!(i in langNode) || langNode[i].trim() === '') { + untranslated.push(partial? `${partial}.${i}` : i); + } else { + counted++; + } + } else if (i in langNode) { + const subresults = recursiveCheck(partial? `${partial}.${i}` : i, langNode[i], referenceNode[i]); + if (subresults.untranslated.length) { + untranslated.push(...subresults.untranslated); + } + if (subresults.excess.length) { + excess.push(...subresults.excess); + } + counted += subresults.counted; + needed += subresults.needed; + } else { + untranslated.push(partial? `${partial}.${i}` : i); + needed += recursiveCountRemainingFields(referenceNode[i]); + } + } + // Reverse check to catch excess keys + // that are not present in English.json but are in another language file + for (const i in langNode) { + if (!(i in referenceNode) || (typeof referenceNode[i] === 'string' && referenceNode[i].trim() === '')) { + excess.push(partial? `${partial}.${i}` : i); + } + } + return { + untranslated, + excess, + counted, + needed + }; +}; + +const breakpoints = [ + [95, '👏 Fabulous!'], + [85, '✅ Good!'], + [70, '😕 A decent coverage, but it could be better!'], + [50, '👎 Meh'], + [0, '🆘 Someone help, please!'] +]; +const getSuitableBreakpoint = percent => { + for (const point of breakpoints) { + if (percent >= point[0]) { + return point[1]; + } + } + return 'WTF?'; +}; + +module.exports = async () => { + const fileNames = (await fs.readdir('./app/data/i18n')).filter(file => + path.extname(file) === '.json' && + file !== 'Comments.json' && + file !== 'English.json' && + file !== 'Debug.json' + ); + const report = { + untranslated: {}, + excess: {}, + stats: {} + }; + for (const lang of fileNames) { + const rootNode = require(path.join(i18nDir, lang)); + const result = recursiveCheck('', rootNode, referenceLanguage); + report.untranslated[lang] = result.untranslated; + report.excess[lang] = result.excess; + report.stats[lang] = Math.floor(result.counted / result.needed * 100); + } + const reportText = + +`\nTranslation report: +===================\n\n` + fileNames.map(lang => +`Translation file ${lang} +-----------------${'-'.repeat(lang.length)}\n` + +// eslint-disable-next-line no-nested-ternary +`Coverage: ${report.stats[lang]}% ${getSuitableBreakpoint(report.stats[lang])} +Not translated: ${report.untranslated[lang].length}` + +(report.untranslated[lang].length > 0? '\n'+report.untranslated[lang].map(key => ` ${key}`).join('\n') : '') + +`\nExcess: ${report.excess[lang].length} ${report.excess[lang].length? '⚠️ '.repeat(Math.min(10, report.excess[lang].length)) : '✅'}\n` + +(report.excess[lang].length > 0? report.excess[lang].map(key => ` ${key}`).join('\n') : '') + +).join('\n\n'); + + for (const lang in report.excess) { + if (report.excess[lang].length) { + throw new Error(reportText); + } + } + return reportText; +};