Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Refactor translations #1608

Merged
merged 12 commits into from
Nov 16, 2017
Merged

Refactor translations #1608

merged 12 commits into from
Nov 16, 2017

Conversation

pafcu
Copy link
Contributor

@pafcu pafcu commented Nov 13, 2017

Also see element-hq/element-web#5613

Get rid of _tJsx and offer all features in the same function:

  • Plurals
  • Replacement with React
  • %(foo)s AND <a>foo</a> replacements mixed together

Also, no need anymore to play around with regular expressions.

Before: Now:
_t('Hello %(var)s', { var: 'foo' }) Same
_tJsx('sent to %(emailAddress)s', /%\(emailAddress\)s/, (sub) => <i>{emailAddress}</i>) _t('sent to %(emailAddress)s', { emailAddress: (sub) => <i>{emailAddress}</i> })
_tJsx('Click <a>here</a>', [/<a>(.*?)<\/a>/], [(sub) => <a>{ sub }</a>]) _t('Click <a>here</a>', {}, {'a': (sub) => <a>{ sub }</a>)
_tJsx('Click <a>here</a> or <a>there</a>', [/<a>(.*?)<\/a>/,/<a>(.*?)<\/a>/], [(sub) => <a>{ sub }</a>,(sub) => <a>{ sub }</a>]) _t('Click <hereLink>here</hereLink> or <thereLink>there</thereLink>', {}, {'hereLink': (sub) => <a>{ sub }</a>,'thereLink': (sub) => <a>{ sub }</a>})

If this looks OK I'll update riot-web as well, and update the documentation.

@lukebarnard1 lukebarnard1 self-assigned this Nov 14, 2017
Copy link
Contributor

@lukebarnard1 lukebarnard1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Largely coherent, yey for refactoring things 👍 😄

],
{},
{
'span': () => <span>{ this.props.homeserverUrl }</span>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like it can be replaced with a normal substitution,

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, but I think it might be better to save for later. Changing it to a normal variable would require changing it in every translation as well. which would make this PR balloon even more (or force every translator to re-translate the string).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough

}

return (
<div className="mx_SenderProfile" dir="auto" onClick={props.onClick}>
{ content }
{ content.props.children[0] ?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a little bit sad, especially because we're only doing this so that Flair can appear with a different opacity to the sender profile text and yet we don't show Flair (currently) when the aux text is actually used.

One way to do this is to not use opacity as a cheap way to get the text to be the colour we want. This feels out-of-scope, but maybe leave a "// XXX:" comment explaining this.

// 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()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing space at start of comment

* @return a React <span> component if any non-strings were used in substitutions, otherwise a string
*/
export function _t(text, variables, tags) {
// Don't do subsitutions in counterpart. We hanle it ourselves so we can replace with React components
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spelling

if (!match) {
throw new Error(`_tJsx: translator error. expected translation to match regexp: ${patterns[i]}`);
const match = inputText.match(regexp);
if(!match) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally, the style is if (

const match = inputText.match(regexp);
if(!match) {
output.push(inputText); // Push back input
continue; // Missing matches is entirely possible, because translation might change things
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought the point was that the translations should contain the things to be replaced? Should we not still be throwing an error here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not true. You can pass in e.g. count, but not display it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If count is the exception, then maybe we should handle it as one and treat missing matches as errors?

Copy link
Contributor Author

@pafcu pafcu Nov 14, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's also possible for others, but probably less common. E.g. in the "plural branch" you might display other variables than in the "singular branch".

Might make sense to log a warning though?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning would be fine, but probably want to ignore missing count because otherwise we log pointlessly. In doing so we might lose some warnings we do want for plural branches, as you've described, but that feels like an okay tradeoff.


output.push(toPush);

// Check if we need to wrap the output into a span at the end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would change this comment to be along the lines of "wrap the output in a React component if any items are React components"

it('variable substitution with React component', function() {
// Need an extra space at the end because the result of _t() has an extra empty node at the end
const text = 'You are now ignoring %(userId)s ';
expect(JSON.stringify(languageHandler._t(text, { userId: () => <i>foo</i> })))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason for JSON.stringify? This should probably be commented.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a better way to compare objects by content rather than identity?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expect supports toEqual: https://github.com/mjackson/expect#toequal, which can be used in place of toBe (which uses ===).

});

it('variable substitution with React component', function() {
// Need an extra space at the end because the result of _t() has an extra empty node at the end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way for it not to have an extra empty node? 😇

*/
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So why is variables passed in here?

Copy link
Member

@dbkr dbkr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few comments (and ftr, have been chatting on riot dev too (https://matrix.to/#/!DdJkzRliezrwpNebLk:matrix.org/$1510674234381wUoHD:ldbco.de) about things like doing our own sprintf-ing rather than using counterparts, but I think this is probably fine.

Thanks for this, I appreciate it's a bit of a mission!


let wrap = false; // Remember if the output needs to be wrapped later
for (const regexpString in mapping) {
const regexp = new RegExp(regexpString);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might find we need to look at caching these regexes, but I guess premature optimisation...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had the same thought. I concluded that seemingly our major perf issues are elsewhere, but could be wrong.

@@ -370,7 +373,10 @@ export const MsisdnAuthEntry = React.createClass({
});
return (
<div>
<p>{ _tJsx("A text message has been sent to %(msisdn)s", /%\(msisdn\)s/, (sub) => <i>{this._msisdn}</i>) }</p>
<p>{ _t("A text message has been sent to %(msisdn)s",
{ msisdn: () => <i>this._msisdn</i> },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this not just be the value rather than a function?

{},
{
'CreateRoomButton': () => <CreateRoomButton size="16" callout={true} />,
'RoomDirectoryButton': () => <RoomDirectoryButton size="16" callout={true} />,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More bits that don't need to be functions?

*
* @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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the code, looks like they could probably be react components too? So you could just pass them in rather than functions that return them if it's a node that doesn't contain text? In fact, presumably you could even do these by passing it into vars rather than tags?

* 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> }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be worth spelling out when one would use vars vs tags here (ie. tags is necessary whenever you need i18ned text inside an HTML tag like <a> or <i>?) With _tJsx you also needed to use _tJsx if you wanted any custom markup in the i18ned text (eg. an img) but since your new _t uses the same code paths for both, presumably tags like <img> could equally be done with a regular vars substitution?

Signed-off-by: Stefan Parviainen <pafcu@iki.fi>
@pafcu pafcu changed the title [WIP] Refactor translations Refactor translations Nov 15, 2017
Copy link
Contributor

@lukebarnard1 lukebarnard1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tiny thing, but LGTM otherwise 😄


let replaced;
// If substitution is a function, call it
if(mapping[regexpString] instanceof Function) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The style we use is if (, not sure why we haven't added this to the linting.

@lukebarnard1 lukebarnard1 merged commit 29e6627 into matrix-org:develop Nov 16, 2017
@pafcu pafcu deleted the simplify-translation branch November 29, 2017 19:25
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants