From 980e9026f3b6186bd82ef178ea3dc0eb02c01e74 Mon Sep 17 00:00:00 2001
From: Peter Jaszkowiak
Date: Wed, 12 Apr 2017 20:34:31 -0600
Subject: [PATCH] Fix #5588, translator ignores unclosed tokens
---
public/src/modules/translator.js | 70 +++++++++++++++++++++-----------
test/translator.js | 21 ++++++++++
2 files changed, 67 insertions(+), 24 deletions(-)
diff --git a/public/src/modules/translator.js b/public/src/modules/translator.js
index ce49f222f5..eea3f7495b 100644
--- a/public/src/modules/translator.js
+++ b/public/src/modules/translator.js
@@ -97,6 +97,9 @@
// and the strings of untranslated text in between
var toTranslate = [];
+ // to store the state of if we're currently in a top-level token for later
+ var inToken = false;
+
// split a translator string into an array of tokens
// but don't split by commas inside other translator strings
function split(text) {
@@ -141,6 +144,8 @@
// set the last break to our current
// spot since we just broke the string
lastBreak = cursor;
+ // we're in a token now
+ inToken = true;
// the current level of nesting of the translation strings
var level = 0;
@@ -176,6 +181,8 @@
invalidTextRegex.test(sliced[0])) {
cursor += 1;
lastBreak -= 2;
+ // no longer in a token
+ inToken = false;
if (level > 0) {
level -= 1;
} else {
@@ -191,18 +198,26 @@
// if we're at the base level, then this is the end
if (level === 0) {
// so grab the name and args
- var result = split(str.slice(lastBreak, cursor));
+ var currentSlice = str.slice(lastBreak, cursor);
+ var result = split(currentSlice);
var name = result[0];
var args = result.slice(1);
+ // make a backup based on the raw string of the token
+ // if there are arguments to the token
+ var backup = '';
+ if (args && args.length) {
+ backup = this.translate('[[' + currentSlice + '[[');
+ }
// add the translation promise to the array
- toTranslate.push(this.translateKey(name, args));
+ toTranslate.push(this.translateKey(name, args, backup));
// skip past the ending brackets
cursor += 2;
// set this as our last break
lastBreak = cursor;
// and we're no longer in a translation string,
// so continue with the main loop
+ inToken = false;
break;
}
// otherwise we lower the level
@@ -219,8 +234,16 @@
cursor += 1;
}
+ // ending string of source
+ var last = str.slice(lastBreak);
+
+ // if we were mid-token, treat it as invalid
+ if (inToken) {
+ last = this.translate('[[' + last);
+ }
+
// add the remaining text after the last translation string
- toTranslate.push(str.slice(lastBreak, cursor + 2));
+ toTranslate.push(last);
// and return a promise for the concatenated translated string
return Promise.all(toTranslate).then(function (translated) {
@@ -232,9 +255,10 @@
* Translates a specific key and array of arguments
* @param {string} name - Translation key (ex. 'global:home')
* @param {string[]} args - Arguments for `%1`, `%2`, etc
+ * @param {string|Promise} backup - Text to use in case the key can't be found
* @returns {Promise}
*/
- Translator.prototype.translateKey = function translateKey(name, args) {
+ Translator.prototype.translateKey = function translateKey(name, args, backup) {
var self = this;
var result = name.split(':', 2);
@@ -251,29 +275,27 @@
}
var translation = this.getTranslation(namespace, key);
- var argsToTranslate = args.map(function (arg) {
- return string(arg).collapseWhitespace().decodeHTMLEntities().escapeHTML().s;
- }).map(function (arg) {
- return self.translate(arg);
- });
-
- // so we can await all promises at once
- argsToTranslate.unshift(translation);
-
- return Promise.all(argsToTranslate).then(function (result) {
- var translated = result[0];
- var translatedArgs = result.slice(1);
-
+ return translation.then(function (translated) {
+ // check if the translation is missing first
if (!translated) {
warn('Missing translation "' + name + '"');
- return key;
+ return backup || key;
}
- var out = translated;
- translatedArgs.forEach(function (arg, i) {
- var escaped = arg.replace(/%/g, '%').replace(/\\,/g, ',');
- out = out.replace(new RegExp('%' + (i + 1), 'g'), escaped);
+
+ var argsToTranslate = args.map(function (arg) {
+ return string(arg).collapseWhitespace().decodeHTMLEntities().escapeHTML().s;
+ }).map(function (arg) {
+ return self.translate(arg);
+ });
+
+ return Promise.all(argsToTranslate).then(function (translatedArgs) {
+ var out = translated;
+ translatedArgs.forEach(function (arg, i) {
+ var escaped = arg.replace(/%/g, '%').replace(/\\,/g, ',');
+ out = out.replace(new RegExp('%' + (i + 1), 'g'), escaped);
+ });
+ return out;
});
- return out;
});
};
@@ -281,7 +303,7 @@
* Load translation file (or use a cached version), and optionally return the translation of a certain key
* @param {string} namespace - The file name of the translation namespace
* @param {string} [key] - The key of the specific translation to getJSON
- * @returns {Promise