Refactor i18n stuff a bit
This commit is contained in:
parent
b3ddec4131
commit
3afbaf61e7
17 changed files with 225 additions and 199 deletions
|
@ -35,12 +35,9 @@ export function _td(s) {
|
|||
return s;
|
||||
}
|
||||
|
||||
// The translation function. This is just a simple wrapper to counterpart,
|
||||
// but exists mostly because we must use the same counterpart instance
|
||||
// between modules (ie. here (react-sdk) and the app (riot-web), and if we
|
||||
// just import counterpart and use it directly, we end up using a different
|
||||
// instance.
|
||||
export function _t(...args) {
|
||||
// Wrapper for counterpart's translation function so that it handles nulls and undefineds properly
|
||||
//Takes the same arguments as counterpart.translate()
|
||||
function safe_counterpart_translate(...args) {
|
||||
// Horrible hack to avoid https://github.com/vector-im/riot-web/issues/4191
|
||||
// The interpolation library that counterpart uses does not support undefined/null
|
||||
// values and instead will throw an error. This is a problem since everywhere else
|
||||
|
@ -51,11 +48,11 @@ export function _t(...args) {
|
|||
if (args[1] && typeof args[1] === 'object') {
|
||||
Object.keys(args[1]).forEach((k) => {
|
||||
if (args[1][k] === undefined) {
|
||||
console.warn("_t called with undefined interpolation name: " + k);
|
||||
console.warn("safe_counterpart_translate called with undefined interpolation name: " + k);
|
||||
args[1][k] = 'undefined';
|
||||
}
|
||||
if (args[1][k] === null) {
|
||||
console.warn("_t called with null interpolation name: " + k);
|
||||
console.warn("safe_counterpart_translate called with null interpolation name: " + k);
|
||||
args[1][k] = 'null';
|
||||
}
|
||||
});
|
||||
|
@ -64,75 +61,112 @@ export function _t(...args) {
|
|||
}
|
||||
|
||||
/*
|
||||
* Translates stringified JSX into translated JSX. E.g
|
||||
* _tJsx(
|
||||
* "click <a href=''>here</a> now",
|
||||
* /<a href=''>(.*?)<\/a>/,
|
||||
* (sub) => { return <a href=''>{ sub }</a>; }
|
||||
* );
|
||||
* Translates text and optionally also replaces XML-ish elements in the text with e.g. React components
|
||||
* @param {string} text The untranslated text, e.g "click <a>here</a> now to %(foo)s".
|
||||
* @param {object} variables Variable substitutions, e.g { foo: 'bar' }
|
||||
* @param {object} tags Tag substitutions e.g. { 'a': (sub) => <a>{sub}</a> }
|
||||
*
|
||||
* @param {string} jsxText The untranslated stringified JSX e.g "click <a href=''>here</a> now".
|
||||
* This will be translated by passing the string through to _t(...)
|
||||
* The values to substitute with can be either simple strings, or functions that return the value to use in
|
||||
* the substitution (e.g. return a React component). In case of a tag replacement, the function receives as
|
||||
* the argument the text inside the element corresponding to the tag.
|
||||
*
|
||||
* @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 React <span> component containing the generated text
|
||||
* @return a React <span> component if any non-strings were used in substitutions, otherwise a string
|
||||
*/
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
export function _t(text, variables, tags) {
|
||||
// Don't do subsitutions in counterpart. We hanle it ourselves so we can replace with React components
|
||||
const args = Object.assign({ interpolate: false }, variables);
|
||||
|
||||
// The translation returns text so there's no XSS vector here (no unsafe HTML, no code execution)
|
||||
const tJsxText = _t(jsxText, {interpolate: false});
|
||||
const output = [tJsxText];
|
||||
const translated = safe_counterpart_translate(text, args);
|
||||
|
||||
return substitute(translated, variables, tags);
|
||||
}
|
||||
|
||||
/*
|
||||
* Similar to _t(), except only does substitutions, and no translation
|
||||
* @param {string} text The text, e.g "click <a>here</a> now to %(foo)s".
|
||||
* @param {object} variables Variable substitutions, e.g { foo: 'bar' }
|
||||
* @param {object} tags Tag substitutions e.g. { 'a': (sub) => <a>{sub}</a> }
|
||||
*
|
||||
* The values to substitute with can be either simple strings, or functions that return the value to use in
|
||||
* the substitution (e.g. return a React component). In case of a tag replacement, the function receives as
|
||||
* the argument the text inside the element corresponding to the tag.
|
||||
*
|
||||
* @return a React <span> component if any non-strings were used in substitutions, otherwise a string
|
||||
*/
|
||||
export function substitute(text, variables, tags) {
|
||||
const regexpMapping = {};
|
||||
|
||||
if(variables !== undefined) {
|
||||
for (const variable in variables) {
|
||||
regexpMapping[`%\\(${variable}\\)s`] = variables[variable];
|
||||
}
|
||||
}
|
||||
|
||||
if(tags !== undefined) {
|
||||
for (const tag in tags) {
|
||||
regexpMapping[`(<${tag}>(.*?)<\\/${tag}>|<${tag}>|<${tag}\\s*\\/>)`] = tags[tag];
|
||||
}
|
||||
}
|
||||
return replaceByRegexes(text, regexpMapping);
|
||||
}
|
||||
|
||||
/*
|
||||
* Replace parts of a text using regular expressions
|
||||
* @param {string} text The text on which to perform substitutions
|
||||
* @param {object} mapping A mapping from regular expressions in string form to replacement string or a
|
||||
* function which will receive as the argument the capture groups defined in the regexp. E.g.
|
||||
* { 'Hello (.?) World': (sub) => sub.toUpperCase() }
|
||||
*
|
||||
* @return a React <span> component if any non-strings were used in substitutions, otherwise a string
|
||||
*/
|
||||
export function replaceByRegexes(text, mapping) {
|
||||
const output = [text];
|
||||
|
||||
let wrap = false; // Remember if the output needs to be wrapped later
|
||||
for (const regexpString in mapping) {
|
||||
const regexp = new RegExp(regexpString);
|
||||
|
||||
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).
|
||||
const inputText = output.pop();
|
||||
const match = inputText.match(patterns[i]);
|
||||
if (!match) {
|
||||
throw new Error(`_tJsx: translator error. expected translation to match regexp: ${patterns[i]}`);
|
||||
const match = inputText.match(regexp);
|
||||
if(!match) {
|
||||
output.push(inputText); // Push back input
|
||||
continue; // Missing matches is entirely possible, because translation might change things
|
||||
}
|
||||
const capturedGroups = match.slice(1);
|
||||
const capturedGroups = match.slice(2);
|
||||
|
||||
// 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));
|
||||
|
||||
let toPush;
|
||||
// If substitution is a function, call it
|
||||
if(mapping[regexpString] instanceof Function) {
|
||||
toPush = mapping[regexpString].apply(null, capturedGroups);
|
||||
} else {
|
||||
toPush = mapping[regexpString];
|
||||
}
|
||||
|
||||
output.push(toPush);
|
||||
|
||||
// Check if we need to wrap the output into a span at the end
|
||||
if(typeof toPush === 'object') {
|
||||
wrap = true;
|
||||
}
|
||||
|
||||
output.push(inputText.substr(match.index + match[0].length));
|
||||
}
|
||||
|
||||
// this is a bit of a fudge to avoid the 'Each child in an array or iterator
|
||||
// should have a unique "key" prop' error: we explicitly pass the generated
|
||||
// nodes into React.createElement as children of a <span>.
|
||||
return React.createElement('span', null, ...output);
|
||||
if(wrap) {
|
||||
// this is a bit of a fudge to avoid the 'Each child in an array or iterator
|
||||
// should have a unique "key" prop' error: we explicitly pass the generated
|
||||
// nodes into React.createElement as children of a <span>.
|
||||
return React.createElement('span', null, ...output);
|
||||
} else {
|
||||
return output.join('');
|
||||
}
|
||||
}
|
||||
|
||||
// Allow overriding the text displayed when no translation exists
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue