Skip to content
This repository was archived by the owner on Nov 17, 2024. It is now read-only.

Commit 05b5bac

Browse files
⚡ Add home-brewed i18n coverage reports and language file checkers to CI
1 parent 67f7595 commit 05b5bac

File tree

2 files changed

+121
-1
lines changed

2 files changed

+121
-1
lines changed

gulpfile.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,9 @@ const lintJS = () => {
204204
.pipe(eslint.failAfterError());
205205
};
206206

207-
const lint = gulp.series(lintJS, lintStylus);
207+
const lintI18n = () => require('./node_requires/i18n')().then(console.log);
208+
209+
const lint = gulp.series(lintJS, lintStylus, lintI18n);
208210

209211
const launchApp = () => {
210212
spawnise.spawn(npm, ['run', 'start'], {

node_requires/i18n/index.js

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
const i18nDir = './../../app/data/i18n';
2+
const path = require('path'),
3+
fs = require('fs-extra');
4+
const referenceLanguage = require(path.join(i18nDir, 'English.json'));
5+
6+
const recursiveCountRemainingFields = node => {
7+
if (typeof node === 'string') {
8+
return 1;
9+
}
10+
let counted = 0;
11+
for (const i in node) {
12+
counted += recursiveCountRemainingFields(node[i]);
13+
}
14+
return counted;
15+
};
16+
17+
// Recursively counts keys in English.json, translated keys,
18+
// and collects paths to untranslated and excess keys
19+
const recursiveCheck = function(partial, langNode, referenceNode) {
20+
const untranslated = [],
21+
excess = [];
22+
let counted = 0,
23+
needed = 0;
24+
for (const i in referenceNode) {
25+
if (typeof referenceNode[i] === 'string') {
26+
needed++;
27+
if (!(i in langNode) || langNode[i].trim() === '') {
28+
untranslated.push(partial? `${partial}.${i}` : i);
29+
} else {
30+
counted++;
31+
}
32+
} else if (i in langNode) {
33+
const subresults = recursiveCheck(partial? `${partial}.${i}` : i, langNode[i], referenceNode[i]);
34+
if (subresults.untranslated.length) {
35+
untranslated.push(...subresults.untranslated);
36+
}
37+
if (subresults.excess.length) {
38+
excess.push(...subresults.excess);
39+
}
40+
counted += subresults.counted;
41+
needed += subresults.needed;
42+
} else {
43+
untranslated.push(partial? `${partial}.${i}` : i);
44+
needed += recursiveCountRemainingFields(referenceNode[i]);
45+
}
46+
}
47+
// Reverse check to catch excess keys
48+
// that are not present in English.json but are in another language file
49+
for (const i in langNode) {
50+
if (!(i in referenceNode) || (typeof referenceNode[i] === 'string' && referenceNode[i].trim() === '')) {
51+
excess.push(partial? `${partial}.${i}` : i);
52+
}
53+
}
54+
return {
55+
untranslated,
56+
excess,
57+
counted,
58+
needed
59+
};
60+
};
61+
62+
const breakpoints = [
63+
[95, '👏 Fabulous!'],
64+
[85, '✅ Good!'],
65+
[70, '😕 A decent coverage, but it could be better!'],
66+
[50, '👎 Meh'],
67+
[0, '🆘 Someone help, please!']
68+
];
69+
const getSuitableBreakpoint = percent => {
70+
for (const point of breakpoints) {
71+
if (percent >= point[0]) {
72+
return point[1];
73+
}
74+
}
75+
return 'WTF?';
76+
};
77+
78+
module.exports = async () => {
79+
const fileNames = (await fs.readdir('./app/data/i18n')).filter(file =>
80+
path.extname(file) === '.json' &&
81+
file !== 'Comments.json' &&
82+
file !== 'English.json' &&
83+
file !== 'Debug.json'
84+
);
85+
const report = {
86+
untranslated: {},
87+
excess: {},
88+
stats: {}
89+
};
90+
for (const lang of fileNames) {
91+
const rootNode = require(path.join(i18nDir, lang));
92+
const result = recursiveCheck('', rootNode, referenceLanguage);
93+
report.untranslated[lang] = result.untranslated;
94+
report.excess[lang] = result.excess;
95+
report.stats[lang] = Math.floor(result.counted / result.needed * 100);
96+
}
97+
const reportText =
98+
99+
`\nTranslation report:
100+
===================\n\n` + fileNames.map(lang =>
101+
`Translation file ${lang}
102+
-----------------${'-'.repeat(lang.length)}\n` +
103+
// eslint-disable-next-line no-nested-ternary
104+
`Coverage: ${report.stats[lang]}% ${getSuitableBreakpoint(report.stats[lang])}
105+
Not translated: ${report.untranslated[lang].length}` +
106+
(report.untranslated[lang].length > 0? '\n'+report.untranslated[lang].map(key => ` ${key}`).join('\n') : '') +
107+
`\nExcess: ${report.excess[lang].length} ${report.excess[lang].length? '⚠️ '.repeat(Math.min(10, report.excess[lang].length)) : '✅'}\n` +
108+
(report.excess[lang].length > 0? report.excess[lang].map(key => ` ${key}`).join('\n') : '')
109+
110+
).join('\n\n');
111+
112+
for (const lang in report.excess) {
113+
if (report.excess[lang].length) {
114+
throw new Error(reportText);
115+
}
116+
}
117+
return reportText;
118+
};

0 commit comments

Comments
 (0)