mirror of https://github.com/vector-im/riot-web
Make the gen-i18n script validate _t calls
And throw a massive tantrum if you've messed up your format strings. Because broken format strings making their way into the app cause it to throw exceptions.pull/21833/head
parent
d859504276
commit
317ad64ae6
|
@ -35,6 +35,7 @@ const estreeWalker = require('estree-walker');
|
||||||
const TRANSLATIONS_FUNCS = ['_t', '_td', '_tJsx'];
|
const TRANSLATIONS_FUNCS = ['_t', '_td', '_tJsx'];
|
||||||
|
|
||||||
const INPUT_TRANSLATIONS_FILE = 'src/i18n/strings/en_EN.json';
|
const INPUT_TRANSLATIONS_FILE = 'src/i18n/strings/en_EN.json';
|
||||||
|
const OUTPUT_FILE = 'src/i18n/strings/en_EN.json';
|
||||||
|
|
||||||
// NB. The sync version of walk is broken for single files so we walk
|
// NB. The sync version of walk is broken for single files so we walk
|
||||||
// all of res rather than just res/home.html.
|
// all of res rather than just res/home.html.
|
||||||
|
@ -73,6 +74,41 @@ function getTKey(arg) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getFormatStrings(str) {
|
||||||
|
// Match anything that starts with %
|
||||||
|
// We could make a regex that matched the full placeholder, but this
|
||||||
|
// would just not match invalid placeholders and so wouldn't help us
|
||||||
|
// detect the invalid ones.
|
||||||
|
// Also note that for simplicity, this just matches a % character and then
|
||||||
|
// anything up to the next % character (or a single %, or end of string).
|
||||||
|
const formatStringRe = /%([^%]+|%|$)/g;
|
||||||
|
const formatStrings = new Set();
|
||||||
|
|
||||||
|
let match;
|
||||||
|
while ( (match = formatStringRe.exec(str)) !== null) {
|
||||||
|
const placeholder = match[1]; // Minus the leading '%'
|
||||||
|
if (placeholder === '%') continue; // Literal % is %%
|
||||||
|
|
||||||
|
const placeholderMatch = placeholder.match(/^\((.*?)\)(.)/);
|
||||||
|
if (placeholderMatch === null) {
|
||||||
|
throw new Error("Invalid format specifier: '"+match[0]+"'");
|
||||||
|
}
|
||||||
|
if (placeholderMatch.length < 3) {
|
||||||
|
throw new Error("Malformed format specifier");
|
||||||
|
}
|
||||||
|
const placeHolderName = placeholderMatch[1];
|
||||||
|
const placeHolderFormat = placeholderMatch[2];
|
||||||
|
|
||||||
|
if (placeHolderFormat !== 's') {
|
||||||
|
throw new Error(`'${placeHolderFormat}' used as format character: you probably didn't mean this`);
|
||||||
|
}
|
||||||
|
|
||||||
|
formatStrings.add(placeHolderName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatStrings;
|
||||||
|
}
|
||||||
|
|
||||||
function getTranslationsJs(file) {
|
function getTranslationsJs(file) {
|
||||||
const tree = flowParser.parse(fs.readFileSync(file, { encoding: 'utf8' }), FLOW_PARSER_OPTS);
|
const tree = flowParser.parse(fs.readFileSync(file, { encoding: 'utf8' }), FLOW_PARSER_OPTS);
|
||||||
|
|
||||||
|
@ -89,6 +125,28 @@ function getTranslationsJs(file) {
|
||||||
// had to use a _td to compensate) so is expected.
|
// had to use a _td to compensate) so is expected.
|
||||||
if (tKey === null) return;
|
if (tKey === null) return;
|
||||||
|
|
||||||
|
// check the format string against the args
|
||||||
|
// We only check _t: _tJsx is much more complex and _td has no args
|
||||||
|
if (node.callee.name === '_t') {
|
||||||
|
try {
|
||||||
|
const placeholders = getFormatStrings(tKey);
|
||||||
|
for (const placeholder of placeholders) {
|
||||||
|
if (node.arguments.length < 2) {
|
||||||
|
throw new Error(`Placeholder found ('${placeholder}') but no substitutions given`);
|
||||||
|
}
|
||||||
|
const value = getObjectValue(node.arguments[1], placeholder);
|
||||||
|
if (value === null) {
|
||||||
|
throw new Error(`No value found for placeholder '${placeholder}'`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log();
|
||||||
|
console.error(`ERROR: ${file}:${node.loc.start.line} ${tKey}`);
|
||||||
|
console.error(e);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let isPlural = false;
|
let isPlural = false;
|
||||||
if (node.arguments.length > 1 && node.arguments[1].type == 'ObjectExpression') {
|
if (node.arguments.length > 1 && node.arguments[1].type == 'ObjectExpression') {
|
||||||
const countVal = getObjectValue(node.arguments[1], 'count');
|
const countVal = getObjectValue(node.arguments[1], 'count');
|
||||||
|
@ -182,7 +240,9 @@ for (const tr of translatables) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
"src/i18n/strings/en_EN.json",
|
OUTPUT_FILE,
|
||||||
JSON.stringify(trObj, translatables.values(), 4) + "\n"
|
JSON.stringify(trObj, translatables.values(), 4) + "\n"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log();
|
||||||
|
console.log(`Wrote ${translatables.size} strings to ${OUTPUT_FILE}`);
|
||||||
|
|
Loading…
Reference in New Issue