diff --git a/src/languageHandler.js b/src/languageHandler.js
index af428d195f..961838b770 100644
--- a/src/languageHandler.js
+++ b/src/languageHandler.js
@@ -18,6 +18,7 @@ limitations under the License.
import request from 'browser-request';
import counterpart from 'counterpart';
import q from 'q';
+import sanitizeHtml from "sanitize-html";
import UserSettingsStore from './UserSettingsStore';
@@ -37,6 +38,80 @@ export function _t(...args) {
return counterpart.translate(...args);
}
+/*
+ * Translates stringified JSX into translated JSX. E.g
+ * _tJsx(
+ * "click here now",
+ * /(.*?)<\/a>/,
+ * (sub) => { return { sub }; }
+ * );
+ *
+ * @param {string} jsxText The untranslated stringified JSX e.g "click here now".
+ * This will be translated by passing the string through to _t(...)
+ *
+ * @param {RegExp|RegExp[]} patterns A regexp to match against the translated text.
+ * The captured groups from the regexp will be fed to 'sub'.
+ * Only the captured groups will be included in the output, the match itself is discarded.
+ * If multiple RegExps are provided, the function at the same position will be called. The
+ * match will always be done from left to right, so the 2nd RegExp will be matched against the
+ * remaining text from the first RegExp.
+ *
+ * @param {Function|Function[]} subs A function which will be called
+ * with multiple args, each arg representing a captured group of the matching regexp.
+ * This function must return a JSX node.
+ *
+ * @return A list of strings/JSX nodes.
+ */
+export function _tJsx(jsxText, patterns, subs) {
+ // convert everything to arrays
+ if (patterns instanceof RegExp) {
+ patterns = [patterns];
+ }
+ if (subs instanceof Function) {
+ subs = [subs];
+ }
+ // sanity checks
+ if (subs.length !== patterns.length || subs.length < 1) {
+ throw new Error(`_tJsx: programmer error. expected number of RegExps == number of Functions: ${subs.length} != ${patterns.length}`);
+ }
+ for (let i = 0; i < subs.length; i++) {
+ if (!patterns[i] instanceof RegExp) {
+ throw new Error(`_tJsx: programmer error. expected RegExp for text: ${jsxText}`);
+ }
+ if (!subs[i] instanceof Function) {
+ throw new Error(`_tJsx: programmer error. expected Function for text: ${jsxText}`);
+ }
+ }
+
+ // tJsxText may be unsafe if malicious translators try to inject HTML.
+ // Run this through sanitize-html and bail if the output isn't identical
+ const tJsxText = _t(jsxText);
+ const sanitized = sanitizeHtml(tJsxText, { allowedTags: sanitizeHtml.defaults.allowedTags.concat([ 'span' ]) });
+ if (tJsxText !== sanitized) {
+ throw new Error(`_tJsx: translator error. untrusted HTML supplied. '${tJsxText}' != '${sanitized}'`);
+ }
+
+ let output = [tJsxText];
+ for (let i = 0; i < patterns.length; i++) {
+ // convert the last element in 'output' into 3 elements (pre-text, sub function, post-text).
+ // Rinse and repeat for other patterns (using post-text).
+ let inputText = output.pop();
+ let match = inputText.match(patterns[i]);
+ if (!match) {
+ throw new Error(`_tJsx: translator error. expected translation to match regexp: ${patterns[i]}`);
+ }
+ let capturedGroups = match.slice(1);
+
+ // Return the raw translation before the *match* followed by the return value of sub() followed
+ // by the raw translation after the *match* (not captured group).
+ output.push(inputText.substr(0, match.index));
+ output.push(subs[i].apply(null, capturedGroups));
+ output.push(inputText.substr(match.index + match[0].length));
+ }
+
+ return output;
+}
+
// Allow overriding the text displayed when no translation exists
// Currently only use din unit tests to avoid having to load
// the translations in riot-web