From 3afbaf61e7e52052ed9c6f0fd6a75e0025e4e13d Mon Sep 17 00:00:00 2001 From: Stefan Parviainen Date: Mon, 13 Nov 2017 20:19:33 +0100 Subject: [PATCH 01/13] Refactor i18n stuff a bit --- src/components/structures/FilePanel.js | 7 +- src/components/structures/GroupView.js | 10 +- src/components/structures/MyGroups.js | 10 +- src/components/structures/RoomStatusBar.js | 34 ++-- src/components/structures/login/Login.js | 25 +-- .../dialogs/SessionRestoreErrorDialog.js | 7 +- src/components/views/dialogs/SetMxIdDialog.js | 23 ++- src/components/views/login/CaptchaForm.js | 8 +- .../login/InteractiveAuthEntryComponents.js | 12 +- .../views/messages/RoomAvatarEvent.js | 27 ++- .../views/messages/SenderProfile.js | 19 +-- .../views/room_settings/UrlPreviewSettings.js | 13 +- src/components/views/rooms/AuxPanel.js | 16 +- src/components/views/rooms/RoomList.js | 25 ++- src/components/views/rooms/RoomPreviewBar.js | 20 +-- src/components/views/rooms/RoomSettings.js | 14 +- src/languageHandler.js | 154 +++++++++++------- 17 files changed, 225 insertions(+), 199 deletions(-) diff --git a/src/components/structures/FilePanel.js b/src/components/structures/FilePanel.js index 23feb4cf30..ffa5e45249 100644 --- a/src/components/structures/FilePanel.js +++ b/src/components/structures/FilePanel.js @@ -19,7 +19,7 @@ import React from 'react'; import Matrix from 'matrix-js-sdk'; import sdk from '../../index'; import MatrixClientPeg from '../../MatrixClientPeg'; -import { _t, _tJsx } from '../../languageHandler'; +import { _t } from '../../languageHandler'; /* * Component which shows the filtered file using a TimelinePanel @@ -92,7 +92,10 @@ const FilePanel = React.createClass({ if (MatrixClientPeg.get().isGuest()) { return
- { _tJsx("You must register to use this functionality", /(.*?)<\/a>/, (sub) => { sub }) } + { _t("You must register to use this functionality", + {}, + { 'a': (sub) => { sub } }) + }
; } else if (this.noRoom) { diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 1b5ebb6b36..cba030c1cc 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -22,7 +22,7 @@ import MatrixClientPeg from '../../MatrixClientPeg'; import sdk from '../../index'; import dis from '../../dispatcher'; import { sanitizedHtmlNode } from '../../HtmlUtils'; -import { _t, _td, _tJsx } from '../../languageHandler'; +import { _t, _td } from '../../languageHandler'; import AccessibleButton from '../views/elements/AccessibleButton'; import Modal from '../../Modal'; import classnames from 'classnames'; @@ -932,12 +932,12 @@ export default React.createClass({ className="mx_GroupView_groupDesc_placeholder" onClick={this._onEditClick} > - { _tJsx( + { _t( 'Your community hasn\'t got a Long Description, a HTML page to show to community members.
' + 'Click here to open settings and give it one!', - [/
/], - [(sub) =>
]) - } + {}, + { 'br': () =>
}, + ) } ; } const groupDescEditingClasses = classnames({ diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js index cc4783fdac..c669d7dd73 100644 --- a/src/components/structures/MyGroups.js +++ b/src/components/structures/MyGroups.js @@ -19,7 +19,7 @@ import PropTypes from 'prop-types'; import GeminiScrollbar from 'react-gemini-scrollbar'; import {MatrixClient} from 'matrix-js-sdk'; import sdk from '../../index'; -import { _t, _tJsx } from '../../languageHandler'; +import { _t } from '../../languageHandler'; import withMatrixClient from '../../wrappers/withMatrixClient'; import AccessibleButton from '../views/elements/AccessibleButton'; import dis from '../../dispatcher'; @@ -165,13 +165,13 @@ export default withMatrixClient(React.createClass({
{ _t('Join an existing community') }
- { _tJsx( + { _t( 'To join an existing community you\'ll have to '+ 'know its community identifier; this will look '+ 'something like +example:matrix.org.', - /(.*)<\/i>/, - (sub) => { sub }, - ) } + {}, + { 'i': (sub) => { sub } }) + } diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index cad55351d1..03859f522e 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -15,13 +15,12 @@ limitations under the License. */ import React from 'react'; -import { _t, _tJsx } from '../../languageHandler'; +import { _t } from '../../languageHandler'; import sdk from '../../index'; import WhoIsTyping from '../../WhoIsTyping'; import MatrixClientPeg from '../../MatrixClientPeg'; import MemberAvatar from '../views/avatars/MemberAvatar'; -const HIDE_DEBOUNCE_MS = 10000; const STATUS_BAR_HIDDEN = 0; const STATUS_BAR_EXPANDED = 1; const STATUS_BAR_EXPANDED_LARGE = 2; @@ -272,12 +271,16 @@ module.exports = React.createClass({ { this.props.unsentMessageError }
- { _tJsx("Resend all or cancel all now. You can also select individual messages to resend or cancel.", - [/(.*?)<\/a>/, /(.*?)<\/a>/], - [ - (sub) => { sub }, - (sub) => { sub }, - ], + { + _t("Resend all or cancel all now. " + + "You can also select individual messages to resend or cancel.", + {}, + { + 'resendText': (sub) => + { sub }, + 'cancelText': (sub) => + { sub }, + }, ) }
@@ -322,12 +325,15 @@ module.exports = React.createClass({ if (this.props.sentMessageAndIsAlone) { return (
- { _tJsx("There's no one else here! Would you like to invite others or stop warning about the empty room?", - [/(.*?)<\/a>/, /(.*?)<\/a>/], - [ - (sub) => { sub }, - (sub) => { sub }, - ], + { _t("There's no one else here! Would you like to invite others " + + "or stop warning about the empty room?", + {}, + { + 'inviteText': (sub) => + { sub }, + 'nowarnText': (sub) => + { sub }, + }, ) }
); diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index 8ee6eafad4..3b68234abd 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -18,7 +18,7 @@ limitations under the License. 'use strict'; import React from 'react'; -import { _t, _tJsx } from '../../../languageHandler'; +import { _t } from '../../../languageHandler'; import * as languageHandler from '../../../languageHandler'; import sdk from '../../../index'; import Login from '../../../Login'; @@ -256,17 +256,19 @@ module.exports = React.createClass({ !this.state.enteredHomeserverUrl.startsWith("http")) ) { errorText = - { _tJsx("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " + + { + _t("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " + "Either use HTTPS or enable unsafe scripts.", - /(.*?)<\/a>/, - (sub) => { return { sub }; }, + {}, + { 'a': (sub) => { return { sub }; } }, ) } ; } else { errorText = - { _tJsx("Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.", - /(.*?)<\/a>/, - (sub) => { return { sub }; }, + { + _t("Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.", + {}, + { 'a': (sub) => { return { sub }; } }, ) } ; } @@ -277,7 +279,7 @@ module.exports = React.createClass({ componentForStep: function(step) { switch (step) { - case 'm.login.password': + case 'm.login.password': { const PasswordLogin = sdk.getComponent('login.PasswordLogin'); return ( ); - case 'm.login.cas': + } + case 'm.login.cas': { const CasLogin = sdk.getComponent('login.CasLogin'); return ( ); - default: + } + default: { if (!step) { return; } @@ -307,6 +311,7 @@ module.exports = React.createClass({ { _t('Sorry, this homeserver is using a login which is not recognised ') }({ step }) ); + } } }, diff --git a/src/components/views/dialogs/SessionRestoreErrorDialog.js b/src/components/views/dialogs/SessionRestoreErrorDialog.js index f404bdd975..75ae0eda17 100644 --- a/src/components/views/dialogs/SessionRestoreErrorDialog.js +++ b/src/components/views/dialogs/SessionRestoreErrorDialog.js @@ -18,7 +18,7 @@ import React from 'react'; import sdk from '../../../index'; import SdkConfig from '../../../SdkConfig'; import Modal from '../../../Modal'; -import { _t, _tJsx } from '../../../languageHandler'; +import { _t } from '../../../languageHandler'; export default React.createClass({ @@ -45,9 +45,10 @@ export default React.createClass({ if (SdkConfig.get().bug_report_endpoint_url) { bugreport = (

- { _tJsx( + { _t( "Otherwise, click here to send a bug report.", - /(.*?)<\/a>/, (sub) => { sub }, + {}, + { 'a': (sub) => { sub } }, ) }

); diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js index 057609b344..53fdee20ff 100644 --- a/src/components/views/dialogs/SetMxIdDialog.js +++ b/src/components/views/dialogs/SetMxIdDialog.js @@ -21,7 +21,7 @@ import sdk from '../../../index'; import MatrixClientPeg from '../../../MatrixClientPeg'; import classnames from 'classnames'; import KeyCode from '../../../KeyCode'; -import { _t, _tJsx } from '../../../languageHandler'; +import { _t } from '../../../languageHandler'; // The amount of time to wait for further changes to the input username before // sending a request to the server @@ -267,24 +267,21 @@ export default React.createClass({ { usernameIndicator }

- { _tJsx( + { _t( 'This will be your account name on the ' + 'homeserver, or you can pick a different server.', - [ - /<\/span>/, - /(.*?)<\/a>/, - ], - [ - (sub) => { this.props.homeserverUrl }, - (sub) => { sub }, - ], + {}, + { + 'span': () => { this.props.homeserverUrl }, + 'a': (sub) => { sub }, + }, ) }

- { _tJsx( + { _t( 'If you already have a Matrix account you can log in instead.', - /(.*?)<\/a>/, - [(sub) => { sub }], + {}, + { 'a': (sub) => { sub } }, ) }

{ auth } diff --git a/src/components/views/login/CaptchaForm.js b/src/components/views/login/CaptchaForm.js index cf814b0a6e..21e5094b28 100644 --- a/src/components/views/login/CaptchaForm.js +++ b/src/components/views/login/CaptchaForm.js @@ -18,7 +18,7 @@ limitations under the License. import React from 'react'; import ReactDOM from 'react-dom'; -import { _t, _tJsx } from '../../../languageHandler'; +import { _t } from '../../../languageHandler'; const DIV_ID = 'mx_recaptcha'; @@ -67,10 +67,10 @@ module.exports = React.createClass({ // * jumping straight to a hosted captcha page (but we don't support that yet) // * embedding the captcha in an iframe (if that works) // * using a better captcha lib - ReactDOM.render(_tJsx( + ReactDOM.render(_t( "Robot check is currently unavailable on desktop - please use a web browser", - /(.*?)<\/a>/, - (sub) => { return { sub }; }), warning); + {}, + { 'a': (sub) => { return { sub }; }}), warning); this.refs.recaptchaContainer.appendChild(warning); } else { const scriptTag = document.createElement('script'); diff --git a/src/components/views/login/InteractiveAuthEntryComponents.js b/src/components/views/login/InteractiveAuthEntryComponents.js index 5f5a74ccd1..de8746230c 100644 --- a/src/components/views/login/InteractiveAuthEntryComponents.js +++ b/src/components/views/login/InteractiveAuthEntryComponents.js @@ -20,7 +20,7 @@ import url from 'url'; import classnames from 'classnames'; import sdk from '../../../index'; -import { _t, _tJsx } from '../../../languageHandler'; +import { _t } from '../../../languageHandler'; /* This file contains a collection of components which are used by the * InteractiveAuth to prompt the user to enter the information needed @@ -256,7 +256,10 @@ export const EmailIdentityAuthEntry = React.createClass({ } else { return (
-

{ _tJsx("An email has been sent to %(emailAddress)s", /%\(emailAddress\)s/, (sub) => {this.props.inputs.emailAddress}) }

+

{ _t("An email has been sent to %(emailAddress)s", + { emailAddress: (sub) => { this.props.inputs.emailAddress } }, + ) } +

{ _t("Please check your email to continue registration.") }

); @@ -370,7 +373,10 @@ export const MsisdnAuthEntry = React.createClass({ }); return (
-

{ _tJsx("A text message has been sent to %(msisdn)s", /%\(msisdn\)s/, (sub) => {this._msisdn}) }

+

{ _t("A text message has been sent to %(msisdn)s", + { msisdn: () => this._msisdn }, + ) } +

{ _t("Please enter the code it contains:") }

diff --git a/src/components/views/messages/RoomAvatarEvent.js b/src/components/views/messages/RoomAvatarEvent.js index 995d5f8531..3a572d0cbe 100644 --- a/src/components/views/messages/RoomAvatarEvent.js +++ b/src/components/views/messages/RoomAvatarEvent.js @@ -17,7 +17,7 @@ limitations under the License. import React from 'react'; import MatrixClientPeg from '../../../MatrixClientPeg'; import { ContentRepo } from 'matrix-js-sdk'; -import { _t, _tJsx } from '../../../languageHandler'; +import { _t } from '../../../languageHandler'; import sdk from '../../../index'; import Modal from '../../../Modal'; import AccessibleButton from '../elements/AccessibleButton'; @@ -67,24 +67,17 @@ module.exports = React.createClass({ 'crop', ); - // it sucks that _tJsx doesn't support normal _t substitutions :(( return (
- { _tJsx('%(senderDisplayName)s changed the room avatar to ', - [ - /%\(senderDisplayName\)s/, - //, - ], - [ - (sub) => senderDisplayName, - (sub) => - - - , - ], - ) + { _t('%(senderDisplayName)s changed the room avatar to ', + { senderDisplayName: senderDisplayName }, + { + 'img': () => + + + , + }) }
); diff --git a/src/components/views/messages/SenderProfile.js b/src/components/views/messages/SenderProfile.js index afdb97272f..d5f78fe252 100644 --- a/src/components/views/messages/SenderProfile.js +++ b/src/components/views/messages/SenderProfile.js @@ -19,7 +19,7 @@ import React from 'react'; import sdk from '../../../index'; import Flair from '../elements/Flair.js'; -import { _tJsx } from '../../../languageHandler'; +import { _t, substitute } from '../../../languageHandler'; export default function SenderProfile(props) { const EmojiText = sdk.getComponent('elements.EmojiText'); @@ -42,22 +42,19 @@ export default function SenderProfile(props) { : null, ]; - let content = ''; - + let content; if(props.text) { - // Replace senderName, and wrap surrounding text in spans with the right class - content = _tJsx(props.text, /^(.*)\%\(senderName\)s(.*)$/m, (p1, p2) => [ - p1 ? { p1 } : null, - nameElem, - p2 ? { p2 } : null, - ]); + content = _t(props.text, { senderName: () => nameElem }); } else { - content = nameElem; + // There is nothing to translate here, so call substitute() instead + content = substitute('%(senderName)s', { senderName: () => nameElem }); } return (
- { content } + { content.props.children[0] ? { content.props.children[0] } : '' } + { content.props.children[1] } + { content.props.children[2] ? { content.props.children[2] } : '' }
); } diff --git a/src/components/views/room_settings/UrlPreviewSettings.js b/src/components/views/room_settings/UrlPreviewSettings.js index 56ae24e2f8..b9bf997009 100644 --- a/src/components/views/room_settings/UrlPreviewSettings.js +++ b/src/components/views/room_settings/UrlPreviewSettings.js @@ -14,13 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Promise from 'bluebird'; const React = require('react'); const MatrixClientPeg = require('../../../MatrixClientPeg'); -const sdk = require("../../../index"); -const Modal = require("../../../Modal"); const UserSettingsStore = require('../../../UserSettingsStore'); -import { _t, _tJsx } from '../../../languageHandler'; +import { _t } from '../../../languageHandler'; module.exports = React.createClass({ @@ -31,9 +28,6 @@ module.exports = React.createClass({ }, getInitialState: function() { - const cli = MatrixClientPeg.get(); - const roomState = this.props.room.currentState; - const roomPreviewUrls = this.props.room.currentState.getStateEvents('org.matrix.room.preview_urls', ''); const userPreviewUrls = this.props.room.getAccountData("org.matrix.room.preview_urls"); @@ -109,7 +103,6 @@ module.exports = React.createClass({ }, render: function() { - const self = this; const roomState = this.props.room.currentState; const cli = MatrixClientPeg.get(); @@ -133,11 +126,11 @@ module.exports = React.createClass({ let urlPreviewText = null; if (UserSettingsStore.getUrlPreviewsDisabled()) { urlPreviewText = ( - _tJsx("You have disabled URL previews by default.", /(.*?)<\/a>/, (sub)=>{ sub }) + _t("You have disabled URL previews by default.", {}, { 'a': (sub)=>{ sub } }) ); } else { urlPreviewText = ( - _tJsx("You have enabled URL previews by default.", /(.*?)<\/a>/, (sub)=>{ sub }) + _t("You have enabled URL previews by default.", {}, { 'a': (sub)=>{ sub } }) ); } diff --git a/src/components/views/rooms/AuxPanel.js b/src/components/views/rooms/AuxPanel.js index 271b0e48db..b8f31ef896 100644 --- a/src/components/views/rooms/AuxPanel.js +++ b/src/components/views/rooms/AuxPanel.js @@ -21,9 +21,7 @@ import sdk from '../../../index'; import dis from "../../../dispatcher"; import ObjectUtils from '../../../ObjectUtils'; import AppsDrawer from './AppsDrawer'; -import { _t, _tJsx} from '../../../languageHandler'; -import UserSettingsStore from '../../../UserSettingsStore'; - +import { _t } from '../../../languageHandler'; module.exports = React.createClass({ displayName: 'AuxPanel', @@ -100,13 +98,13 @@ module.exports = React.createClass({ supportedText = _t(" (unsupported)"); } else { joinNode = ( - { _tJsx( + { _t( "Join as voice or video.", - [/(.*?)<\/voiceText>/, /(.*?)<\/videoText>/], - [ - (sub) => { this.onConferenceNotificationClick(event, 'voice');}} href="#">{ sub }, - (sub) => { this.onConferenceNotificationClick(event, 'video');}} href="#">{ sub }, - ], + {}, + { + 'voiceText': (sub) => { this.onConferenceNotificationClick(event, 'voice');}} href="#">{ sub }, + 'videoText': (sub) => { this.onConferenceNotificationClick(event, 'video');}} href="#">{ sub }, + }, ) } ); } diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 1a9fa5d4e9..c1e1ce2cb2 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -18,12 +18,10 @@ limitations under the License. 'use strict'; const React = require("react"); const ReactDOM = require("react-dom"); -import { _t, _tJsx } from '../../../languageHandler'; +import { _t } from '../../../languageHandler'; const GeminiScrollbar = require('react-gemini-scrollbar'); const MatrixClientPeg = require("../../../MatrixClientPeg"); const CallHandler = require('../../../CallHandler'); -const RoomListSorter = require("../../../RoomListSorter"); -const Unread = require('../../../Unread'); const dis = require("../../../dispatcher"); const sdk = require('../../../index'); const rate_limited_func = require('../../../ratelimitedfunc'); @@ -486,28 +484,25 @@ module.exports = React.createClass({ const RoomDirectoryButton = sdk.getComponent('elements.RoomDirectoryButton'); const CreateRoomButton = sdk.getComponent('elements.CreateRoomButton'); - const TintableSvg = sdk.getComponent('elements.TintableSvg'); switch (section) { case 'im.vector.fake.direct': return
- { _tJsx( + { _t( "Press to start a chat with someone", - [//], - [ - (sub) => , - ], + {}, + { 'StartChatButton': () => }, ) }
; case 'im.vector.fake.recent': return
- { _tJsx( + { _t( "You're not in any rooms yet! Press to make a room or"+ " to browse the directory", - [//, //], - [ - (sub) => , - (sub) => , - ], + {}, + { + 'CreateRoomButton': () => , + 'RoomDirectoryButton': () => , + }, ) }
; } diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js index 0c0601a504..fe7948aeb3 100644 --- a/src/components/views/rooms/RoomPreviewBar.js +++ b/src/components/views/rooms/RoomPreviewBar.js @@ -21,7 +21,7 @@ const React = require('react'); const sdk = require('../../../index'); const MatrixClientPeg = require('../../../MatrixClientPeg'); -import { _t, _tJsx } from '../../../languageHandler'; +import { _t } from '../../../languageHandler'; module.exports = React.createClass({ displayName: 'RoomPreviewBar', @@ -135,13 +135,13 @@ module.exports = React.createClass({ { _t('You have been invited to join this room by %(inviterName)s', {inviterName: this.props.inviterName}) }
- { _tJsx( + { _t( 'Would you like to accept or decline this invitation?', - [/(.*?)<\/acceptText>/, /(.*?)<\/declineText>/], - [ - (sub) => { sub }, - (sub) => { sub }, - ], + {}, + { + 'acceptText': (sub) => { sub }, + 'declineText': (sub) => { sub }, + }, ) }
{ emailMatchBlock } @@ -211,9 +211,9 @@ module.exports = React.createClass({
{ name ? _t('You are trying to access %(roomName)s.', {roomName: name}) : _t('You are trying to access a room.') }
- { _tJsx("Click here to join the discussion!", - /(.*?)<\/a>/, - (sub) => { sub }, + { _t("Click here to join the discussion!", + {}, + { 'a': (sub) => { sub } }, ) }
diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index c7e839ab40..be5fb0fe2f 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -17,7 +17,7 @@ limitations under the License. import Promise from 'bluebird'; import React from 'react'; -import { _t, _tJsx, _td } from '../../../languageHandler'; +import { _t, _td } from '../../../languageHandler'; import MatrixClientPeg from '../../../MatrixClientPeg'; import sdk from '../../../index'; import Modal from '../../../Modal'; @@ -637,9 +637,7 @@ module.exports = React.createClass({ const ColorSettings = sdk.getComponent("room_settings.ColorSettings"); const UrlPreviewSettings = sdk.getComponent("room_settings.UrlPreviewSettings"); const RelatedGroupSettings = sdk.getComponent("room_settings.RelatedGroupSettings"); - const EditableText = sdk.getComponent('elements.EditableText'); const PowerSelector = sdk.getComponent('elements.PowerSelector'); - const Loader = sdk.getComponent("elements.Spinner"); const cli = MatrixClientPeg.get(); const roomState = this.props.room.currentState; @@ -760,7 +758,7 @@ module.exports = React.createClass({ var tagsSection = null; if (canSetTag || self.state.tags) { - var tagsSection = + tagsSection =
{ _t("Tagged as: ") }{ canSetTag ? (tags.map(function(tag, i) { @@ -790,10 +788,10 @@ module.exports = React.createClass({ if (this.state.join_rule === "public" && aliasCount == 0) { addressWarning =
- { _tJsx( + { _t( 'To link to a room it must have an address.', - /(.*?)<\/a>/, - (sub) => { sub }, + {}, + { 'a': (sub) => { sub } }, ) }
; } @@ -940,7 +938,7 @@ module.exports = React.createClass({ { Object.keys(events_levels).map(function(event_type, i) { let label = plEventsToLabels[event_type]; if (label) label = _t(label); - else label = _tJsx("To send events of type , you must be a", //, () => { event_type }); + else label = _t("To send events of type , you must be a", {}, { 'eventType': () => { event_type } }); return (
{ label } diff --git a/src/languageHandler.js b/src/languageHandler.js index da62bfee56..33ae229185 100644 --- a/src/languageHandler.js +++ b/src/languageHandler.js @@ -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 here now", - * /(.*?)<\/a>/, - * (sub) => { return { sub }; } - * ); + * 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 here now to %(foo)s". + * @param {object} variables Variable substitutions, e.g { foo: 'bar' } + * @param {object} tags Tag substitutions e.g. { 'a': (sub) => {sub} } * - * @param {string} jsxText The untranslated stringified JSX e.g "click here 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 component containing the generated text + * @return a React 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 here now to %(foo)s". + * @param {object} variables Variable substitutions, e.g { foo: 'bar' } + * @param {object} tags Tag substitutions e.g. { 'a': (sub) => {sub} } + * + * 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 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 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 . - 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 . + return React.createElement('span', null, ...output); + } else { + return output.join(''); + } } // Allow overriding the text displayed when no translation exists From 9cf7e1b4808f144eb3f3c544812702d686c0144d Mon Sep 17 00:00:00 2001 From: Stefan Parviainen Date: Mon, 13 Nov 2017 20:20:14 +0100 Subject: [PATCH 02/13] Validate tag replacements in gen-i18n --- scripts/gen-i18n.js | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/scripts/gen-i18n.js b/scripts/gen-i18n.js index dd990b5210..fa9ccc8ed7 100755 --- a/scripts/gen-i18n.js +++ b/scripts/gen-i18n.js @@ -32,7 +32,7 @@ const walk = require('walk'); const flowParser = require('flow-parser'); const estreeWalker = require('estree-walker'); -const TRANSLATIONS_FUNCS = ['_t', '_td', '_tJsx']; +const TRANSLATIONS_FUNCS = ['_t', '_td']; const INPUT_TRANSLATIONS_FILE = 'src/i18n/strings/en_EN.json'; const OUTPUT_FILE = 'src/i18n/strings/en_EN.json'; @@ -126,7 +126,7 @@ function getTranslationsJs(file) { 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 + // We only check _t: _td has no args if (node.callee.name === '_t') { try { const placeholders = getFormatStrings(tKey); @@ -139,6 +139,22 @@ function getTranslationsJs(file) { throw new Error(`No value found for placeholder '${placeholder}'`); } } + + // Validate tag replacements + if (node.arguments.length > 2) { + const tagMap = node.arguments[2]; + for (const prop of tagMap.properties) { + if (prop.key.type === 'Literal') { + const tag = prop.key.value; + // RegExp same as in src/languageHandler.js + const regexp = new RegExp(`(<${tag}>(.*?)<\\/${tag}>|<${tag}>|<${tag}\\s*\\/>)`); + if (!tKey.match(regexp)) { + throw new Error(`No match for ${regexp} in ${tKey}`); + } + } + } + } + } catch (e) { console.log(); console.error(`ERROR: ${file}:${node.loc.start.line} ${tKey}`); From 672d5080adae87f5269a4fcf2407f513a43852a9 Mon Sep 17 00:00:00 2001 From: Stefan Parviainen Date: Mon, 13 Nov 2017 20:20:41 +0100 Subject: [PATCH 03/13] Add unit tests for translation --- test/i18n-test/languageHandler-test.js | 60 ++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 test/i18n-test/languageHandler-test.js diff --git a/test/i18n-test/languageHandler-test.js b/test/i18n-test/languageHandler-test.js new file mode 100644 index 0000000000..2a94768092 --- /dev/null +++ b/test/i18n-test/languageHandler-test.js @@ -0,0 +1,60 @@ +const React = require('react'); +const expect = require('expect'); +import * as languageHandler from '../../src/languageHandler'; + +const testUtils = require('../test-utils'); + +describe('languageHandler', function() { + let sandbox; + + beforeEach(function(done) { + testUtils.beforeEach(this); + sandbox = testUtils.stubClient(); + + languageHandler.setLanguage('en').done(done); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('translates a string to german', function() { + languageHandler.setLanguage('de').then(function() { + const translated = languageHandler._t('Rooms'); + expect(translated).toBe('Räume'); + }); + }); + + it('handles plurals', function() { + var text = 'and %(count)s others...'; + expect(languageHandler._t(text, { count: 1 })).toBe('and one other...'); + expect(languageHandler._t(text, { count: 2 })).toBe('and 2 others...'); + }); + + it('handles simple variable subsitutions', function() { + var text = 'You are now ignoring %(userId)s'; + expect(languageHandler._t(text, { userId: 'foo' })).toBe('You are now ignoring foo'); + }); + + it('handles simple tag substitution', function() { + var text = 'Press to start a chat with someone'; + expect(languageHandler._t(text, {}, { 'StartChatButton': () => 'foo' })).toBe('Press foo to start a chat with someone'); + }); + + it('handles text in tags', function() { + var text = 'Click here to join the discussion!'; + expect(languageHandler._t(text, {}, { 'a': (sub) => `x${sub}x` })).toBe('xClick herex to join the discussion!'); + }); + + 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 + var text = 'You are now ignoring %(userId)s '; + expect(JSON.stringify(languageHandler._t(text, { userId: () => foo }))).toBe(JSON.stringify((You are now ignoring foo ))); + }); + + it('tag substitution with React component', function() { + var text = 'Press to start a chat with someone'; + expect(JSON.stringify(languageHandler._t(text, {}, { 'StartChatButton': () => foo }))).toBe(JSON.stringify(Press foo to start a chat with someone)); + + }); +}); From 2acd42e7c55df9941c578558dded05b641fb4916 Mon Sep 17 00:00:00 2001 From: Stefan Parviainen Date: Mon, 13 Nov 2017 21:10:08 +0100 Subject: [PATCH 04/13] Make eslint happy --- .../views/messages/SenderProfile.js | 8 ++++-- test/i18n-test/languageHandler-test.js | 25 +++++++++++-------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/components/views/messages/SenderProfile.js b/src/components/views/messages/SenderProfile.js index d5f78fe252..d64c5fe651 100644 --- a/src/components/views/messages/SenderProfile.js +++ b/src/components/views/messages/SenderProfile.js @@ -52,9 +52,13 @@ export default function SenderProfile(props) { return (
- { content.props.children[0] ? { content.props.children[0] } : '' } + { content.props.children[0] ? + { content.props.children[0] } : '' + } { content.props.children[1] } - { content.props.children[2] ? { content.props.children[2] } : '' } + { content.props.children[2] ? + { content.props.children[2] } : '' + }
); } diff --git a/test/i18n-test/languageHandler-test.js b/test/i18n-test/languageHandler-test.js index 2a94768092..f3c2e135d5 100644 --- a/test/i18n-test/languageHandler-test.js +++ b/test/i18n-test/languageHandler-test.js @@ -26,35 +26,38 @@ describe('languageHandler', function() { }); it('handles plurals', function() { - var text = 'and %(count)s others...'; + const text = 'and %(count)s others...'; expect(languageHandler._t(text, { count: 1 })).toBe('and one other...'); expect(languageHandler._t(text, { count: 2 })).toBe('and 2 others...'); }); it('handles simple variable subsitutions', function() { - var text = 'You are now ignoring %(userId)s'; + const text = 'You are now ignoring %(userId)s'; expect(languageHandler._t(text, { userId: 'foo' })).toBe('You are now ignoring foo'); }); it('handles simple tag substitution', function() { - var text = 'Press to start a chat with someone'; - expect(languageHandler._t(text, {}, { 'StartChatButton': () => 'foo' })).toBe('Press foo to start a chat with someone'); + const text = 'Press to start a chat with someone'; + expect(languageHandler._t(text, {}, { 'StartChatButton': () => 'foo' })) + .toBe('Press foo to start a chat with someone'); }); it('handles text in tags', function() { - var text = 'Click here to join the discussion!'; - expect(languageHandler._t(text, {}, { 'a': (sub) => `x${sub}x` })).toBe('xClick herex to join the discussion!'); + const text = 'Click here to join the discussion!'; + expect(languageHandler._t(text, {}, { 'a': (sub) => `x${sub}x` })) + .toBe('xClick herex to join the discussion!'); }); 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 - var text = 'You are now ignoring %(userId)s '; - expect(JSON.stringify(languageHandler._t(text, { userId: () => foo }))).toBe(JSON.stringify((You are now ignoring foo ))); + const text = 'You are now ignoring %(userId)s '; + expect(JSON.stringify(languageHandler._t(text, { userId: () => foo }))) + .toBe(JSON.stringify((You are now ignoring foo ))); }); it('tag substitution with React component', function() { - var text = 'Press to start a chat with someone'; - expect(JSON.stringify(languageHandler._t(text, {}, { 'StartChatButton': () => foo }))).toBe(JSON.stringify(Press foo to start a chat with someone)); - + const text = 'Press to start a chat with someone'; + expect(JSON.stringify(languageHandler._t(text, {}, { 'StartChatButton': () => foo }))) + .toBe(JSON.stringify(Press foo to start a chat with someone)); }); }); From 772550a24a51e7a23492c4fbed81438d14d00ec8 Mon Sep 17 00:00:00 2001 From: Stefan Parviainen Date: Tue, 14 Nov 2017 19:33:12 +0100 Subject: [PATCH 05/13] Dont't add empty nodes --- src/languageHandler.js | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/languageHandler.js b/src/languageHandler.js index 33ae229185..272b0a4848 100644 --- a/src/languageHandler.js +++ b/src/languageHandler.js @@ -139,24 +139,34 @@ export function replaceByRegexes(text, mapping) { // 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)); - let toPush; - // If substitution is a function, call it - if(mapping[regexpString] instanceof Function) { - toPush = mapping[regexpString].apply(null, capturedGroups); - } else { - toPush = mapping[regexpString]; + const head = inputText.substr(0, match.index); + if (head !== '') { // Don't push empty nodes, they are of no use + output.push(head); } - output.push(toPush); + let replaced; + // If substitution is a function, call it + if(mapping[regexpString] instanceof Function) { + replaced = mapping[regexpString].apply(null, capturedGroups); + } else { + replaced = mapping[regexpString]; + } - // Check if we need to wrap the output into a span at the end - if(typeof toPush === 'object') { + // Here we also need to check that it actually is a string before comparing against one + // The head and tail are always strings + if (typeof replaced !== 'string' || replaced !== '') { + output.push(replaced); + } + + if(typeof replaced === 'object') { wrap = true; } - output.push(inputText.substr(match.index + match[0].length)); + const tail = inputText.substr(match.index + match[0].length); + if (tail !== '') { + output.push(tail); + } } if(wrap) { From cdd03dd1c59e24423c2bd02a2419841ef73ba205 Mon Sep 17 00:00:00 2001 From: Stefan Parviainen Date: Tue, 14 Nov 2017 19:34:47 +0100 Subject: [PATCH 06/13] Use toEqual instead of toBe --- test/i18n-test/languageHandler-test.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/test/i18n-test/languageHandler-test.js b/test/i18n-test/languageHandler-test.js index f3c2e135d5..bdaa431e0a 100644 --- a/test/i18n-test/languageHandler-test.js +++ b/test/i18n-test/languageHandler-test.js @@ -49,15 +49,14 @@ describe('languageHandler', function() { }); 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: () => foo }))) - .toBe(JSON.stringify((You are now ignoring foo ))); + const text = 'You are now ignoring %(userId)s'; + expect(languageHandler._t(text, { userId: () => foo })) + .toEqual((You are now ignoring foo)); }); it('tag substitution with React component', function() { const text = 'Press to start a chat with someone'; - expect(JSON.stringify(languageHandler._t(text, {}, { 'StartChatButton': () => foo }))) - .toBe(JSON.stringify(Press foo to start a chat with someone)); + expect(languageHandler._t(text, {}, { 'StartChatButton': () => foo })) + .toEqual(Press foo to start a chat with someone); }); }); From 788be67c75e67f5f69c46e2e645d06b39744a5f5 Mon Sep 17 00:00:00 2001 From: Stefan Parviainen Date: Tue, 14 Nov 2017 19:55:47 +0100 Subject: [PATCH 07/13] Clarifications --- .../views/messages/SenderProfile.js | 4 +++ src/languageHandler.js | 30 +++++++++++-------- test/i18n-test/languageHandler-test.js | 6 ++++ 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/components/views/messages/SenderProfile.js b/src/components/views/messages/SenderProfile.js index d64c5fe651..2c557bebe2 100644 --- a/src/components/views/messages/SenderProfile.js +++ b/src/components/views/messages/SenderProfile.js @@ -52,6 +52,10 @@ export default function SenderProfile(props) { return (
+ // The text surrounding the user name must be wrapped in order for it to have the correct opacity. + // It is not possible to wrap the whole thing, because the user name might contain flair which should + // be shown at full opacity. Sadly CSS does not make it possible to "reset" opacity so we have to do it + // in parts like this. Sometimes CSS makes me a sad panda :-( { content.props.children[0] ? { content.props.children[0] } : '' } diff --git a/src/languageHandler.js b/src/languageHandler.js index 272b0a4848..d6660be283 100644 --- a/src/languageHandler.js +++ b/src/languageHandler.js @@ -36,7 +36,7 @@ export function _td(s) { } // Wrapper for counterpart's translation function so that it handles nulls and undefineds properly -//Takes the same arguments as counterpart.translate() +// 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 @@ -66,14 +66,20 @@ function safe_counterpart_translate(...args) { * @param {object} variables Variable substitutions, e.g { foo: 'bar' } * @param {object} tags Tag substitutions e.g. { 'a': (sub) => {sub} } * - * 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. + * In both variables and tags, the values to substitute with can be either simple strings, React components, + * 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. + * + * Use tag substitutions if you need to translate text between tags (e.g. "Click here!"), otherwise + * you will end up with literal "" in your output, rather than HTML. Note that you can also use variable + * substitution to insert React components, but you can't use it to translate text between tags. * * @return a React 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 + // Don't do subsitutions in counterpart. We handle it ourselves so we can replace with React components + // However, still pass the variables to counterpart so that it can choose the correct plural if count is given + // It is enough to pass the count variable, but in the future counterpart might make use of other information too const args = Object.assign({ interpolate: false }, variables); // The translation returns text so there's no XSS vector here (no unsafe HTML, no code execution) @@ -123,15 +129,18 @@ export function substitute(text, variables, tags) { export function replaceByRegexes(text, mapping) { const output = [text]; - let wrap = false; // Remember if the output needs to be wrapped later + // If we insert any components we need to wrap the output in a span. React doesn't like just an array of components. + let shouldWrapInSpan = false; + for (const regexpString in mapping) { + // TODO: Cache regexps const regexp = new RegExp(regexpString); // 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(regexp); - if(!match) { + if (!match) { output.push(inputText); // Push back input continue; // Missing matches is entirely possible, because translation might change things } @@ -160,7 +169,7 @@ export function replaceByRegexes(text, mapping) { } if(typeof replaced === 'object') { - wrap = true; + shouldWrapInSpan = true; } const tail = inputText.substr(match.index + match[0].length); @@ -169,10 +178,7 @@ export function replaceByRegexes(text, mapping) { } } - 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 . + if(shouldWrapInSpan) { return React.createElement('span', null, ...output); } else { return output.join(''); diff --git a/test/i18n-test/languageHandler-test.js b/test/i18n-test/languageHandler-test.js index bdaa431e0a..9c08916235 100644 --- a/test/i18n-test/languageHandler-test.js +++ b/test/i18n-test/languageHandler-test.js @@ -54,6 +54,12 @@ describe('languageHandler', function() { .toEqual((You are now ignoring foo)); }); + it('variable substitution with plain React component', function() { + const text = 'You are now ignoring %(userId)s'; + expect(languageHandler._t(text, { userId: foo })) + .toEqual((You are now ignoring foo)); + }); + it('tag substitution with React component', function() { const text = 'Press to start a chat with someone'; expect(languageHandler._t(text, {}, { 'StartChatButton': () => foo })) From df6d5cc2b4ae11adae1a079ca0ecac996834a802 Mon Sep 17 00:00:00 2001 From: Stefan Parviainen Date: Tue, 14 Nov 2017 20:09:52 +0100 Subject: [PATCH 08/13] Pass plain components, rather than functions returning them --- src/components/structures/GroupView.js | 2 +- src/components/views/dialogs/SetMxIdDialog.js | 2 +- .../views/login/InteractiveAuthEntryComponents.js | 2 +- src/components/views/rooms/RoomList.js | 6 +++--- src/components/views/rooms/RoomSettings.js | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index cba030c1cc..b137893bde 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -936,7 +936,7 @@ export default React.createClass({ 'Your community hasn\'t got a Long Description, a HTML page to show to community members.
' + 'Click here to open settings and give it one!', {}, - { 'br': () =>
}, + { 'br':
}, ) }
; } diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js index 53fdee20ff..6fc1d77682 100644 --- a/src/components/views/dialogs/SetMxIdDialog.js +++ b/src/components/views/dialogs/SetMxIdDialog.js @@ -272,7 +272,7 @@ export default React.createClass({ 'homeserver, or you can pick a different server.', {}, { - 'span': () => { this.props.homeserverUrl }, + 'span': { this.props.homeserverUrl }, 'a': (sub) => { sub }, }, ) } diff --git a/src/components/views/login/InteractiveAuthEntryComponents.js b/src/components/views/login/InteractiveAuthEntryComponents.js index de8746230c..d0b6c8decb 100644 --- a/src/components/views/login/InteractiveAuthEntryComponents.js +++ b/src/components/views/login/InteractiveAuthEntryComponents.js @@ -374,7 +374,7 @@ export const MsisdnAuthEntry = React.createClass({ return (

{ _t("A text message has been sent to %(msisdn)s", - { msisdn: () => this._msisdn }, + { msisdn: this._msisdn }, ) }

{ _t("Please enter the code it contains:") }

diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index c1e1ce2cb2..ebe0bdb03f 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -490,7 +490,7 @@ module.exports = React.createClass({ { _t( "Press to start a chat with someone", {}, - { 'StartChatButton': () => }, + { 'StartChatButton': }, ) }
; case 'im.vector.fake.recent': @@ -500,8 +500,8 @@ module.exports = React.createClass({ " to browse the directory", {}, { - 'CreateRoomButton': () => , - 'RoomDirectoryButton': () => , + 'CreateRoomButton': , + 'RoomDirectoryButton': , }, ) }
; diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index be5fb0fe2f..d11a45732e 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -938,7 +938,7 @@ module.exports = React.createClass({ { Object.keys(events_levels).map(function(event_type, i) { let label = plEventsToLabels[event_type]; if (label) label = _t(label); - else label = _t("To send events of type , you must be a", {}, { 'eventType': () => { event_type } }); + else label = _t("To send events of type , you must be a", {}, { 'eventType': { event_type } }); return (
{ label } From f8660de52d75bcd9471eb87edf86a8c652d9d1a0 Mon Sep 17 00:00:00 2001 From: Stefan Parviainen Date: Tue, 14 Nov 2017 20:13:00 +0100 Subject: [PATCH 09/13] Add note about alternative to opacity --- src/components/views/messages/SenderProfile.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/messages/SenderProfile.js b/src/components/views/messages/SenderProfile.js index 2c557bebe2..04e7832493 100644 --- a/src/components/views/messages/SenderProfile.js +++ b/src/components/views/messages/SenderProfile.js @@ -56,6 +56,7 @@ export default function SenderProfile(props) { // It is not possible to wrap the whole thing, because the user name might contain flair which should // be shown at full opacity. Sadly CSS does not make it possible to "reset" opacity so we have to do it // in parts like this. Sometimes CSS makes me a sad panda :-( + // XXX: This could be avoided if the actual colour is set, rather than faking it with opacity { content.props.children[0] ? { content.props.children[0] } : '' } From ae252f7e59cdbce5851d43675b7842cf90a40bbd Mon Sep 17 00:00:00 2001 From: Stefan Parviainen Date: Tue, 14 Nov 2017 21:34:20 +0100 Subject: [PATCH 10/13] Log if no match is found --- src/languageHandler.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/languageHandler.js b/src/languageHandler.js index d6660be283..7f23f6e4b6 100644 --- a/src/languageHandler.js +++ b/src/languageHandler.js @@ -37,7 +37,7 @@ export function _td(s) { // 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) { +function safeCounterpartTranslate(...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 @@ -48,11 +48,11 @@ function safe_counterpart_translate(...args) { if (args[1] && typeof args[1] === 'object') { Object.keys(args[1]).forEach((k) => { if (args[1][k] === undefined) { - console.warn("safe_counterpart_translate called with undefined interpolation name: " + k); + console.warn("safeCounterpartTranslate called with undefined interpolation name: " + k); args[1][k] = 'undefined'; } if (args[1][k] === null) { - console.warn("safe_counterpart_translate called with null interpolation name: " + k); + console.warn("safeCounterpartTranslate called with null interpolation name: " + k); args[1][k] = 'null'; } }); @@ -83,7 +83,7 @@ export function _t(text, variables, tags) { 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 translated = safe_counterpart_translate(text, args); + const translated = safeCounterpartTranslate(text, args); return substitute(translated, variables, tags); } @@ -142,7 +142,15 @@ export function replaceByRegexes(text, mapping) { const match = inputText.match(regexp); if (!match) { output.push(inputText); // Push back input - continue; // Missing matches is entirely possible, because translation might change things + + // Missing matches is entirely possible because you might choose to show some variables only in the case + // of e.g. plurals. It's still a bit suspicious, and could be due to an error, so log it. + // However, not showing count is so common that it's not worth logging. And other commonly unused variables + // here, if there are any. + if (regexpString !== '%\\(count\\)s') { + console.log(`Could not find ${regexp} in ${inputText}`); + } + continue; } const capturedGroups = match.slice(2); From 5563b71c47d4e9b07f2f926efa63683117a4616f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 15 Nov 2017 15:56:54 +0000 Subject: [PATCH 11/13] differentiate between state events and message events so that people can't fake state event types and have them rendered. Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/EventTile.js | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 499d0ec09a..d7d40d1e6e 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -33,22 +33,30 @@ const ObjectUtils = require('../../../ObjectUtils'); const eventTileTypes = { 'm.room.message': 'messages.MessageEvent', - 'm.room.member': 'messages.TextualEvent', 'm.call.invite': 'messages.TextualEvent', 'm.call.answer': 'messages.TextualEvent', 'm.call.hangup': 'messages.TextualEvent', +}; + +const stateEventTileTypes = { + 'm.room.member': 'messages.TextualEvent', 'm.room.name': 'messages.TextualEvent', 'm.room.avatar': 'messages.RoomAvatarEvent', - 'm.room.topic': 'messages.TextualEvent', 'm.room.third_party_invite': 'messages.TextualEvent', 'm.room.history_visibility': 'messages.TextualEvent', 'm.room.encryption': 'messages.TextualEvent', + 'm.room.topic': 'messages.TextualEvent', 'm.room.power_levels': 'messages.TextualEvent', - 'm.room.pinned_events' : 'messages.TextualEvent', + 'm.room.pinned_events': 'messages.TextualEvent', 'im.vector.modular.widgets': 'messages.TextualEvent', }; +function getHandlerTile(ev) { + const type = ev.getType(); + return ev.isState() ? stateEventTileTypes[type] : eventTileTypes[type]; +} + const MAX_READ_AVATARS = 5; // Our component structure for EventTiles on the timeline is: @@ -433,7 +441,7 @@ module.exports = withMatrixClient(React.createClass({ // Info messages are basically information about commands processed on a room const isInfoMessage = (eventType !== 'm.room.message'); - const EventTileType = sdk.getComponent(eventTileTypes[eventType]); + const EventTileType = sdk.getComponent(getHandlerTile(this.props.mxEvent)); // This shouldn't happen: the caller should check we support this type // before trying to instantiate us if (!EventTileType) { @@ -600,8 +608,10 @@ module.exports = withMatrixClient(React.createClass({ module.exports.haveTileForEvent = function(e) { // Only messages have a tile (black-rectangle) if redacted if (e.isRedacted() && e.getType() !== 'm.room.message') return false; - if (eventTileTypes[e.getType()] == undefined) return false; - if (eventTileTypes[e.getType()] == 'messages.TextualEvent') { + + const handler = getHandlerTile(e); + if (handler === undefined) return false; + if (handler === 'messages.TextualEvent') { return TextForEvent.textForEvent(e) !== ''; } else { return true; From 342378f48a1c304dd562f92c4c7b73e644502f98 Mon Sep 17 00:00:00 2001 From: pafcu Date: Thu, 16 Nov 2017 12:19:56 +0100 Subject: [PATCH 12/13] Add space after if --- src/languageHandler.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/languageHandler.js b/src/languageHandler.js index 9495cc758e..b9b5371022 100644 --- a/src/languageHandler.js +++ b/src/languageHandler.js @@ -102,13 +102,13 @@ export function _t(text, variables, tags) { export function substitute(text, variables, tags) { const regexpMapping = {}; - if(variables !== undefined) { + if (variables !== undefined) { for (const variable in variables) { regexpMapping[`%\\(${variable}\\)s`] = variables[variable]; } } - if(tags !== undefined) { + if (tags !== undefined) { for (const tag in tags) { regexpMapping[`(<${tag}>(.*?)<\\/${tag}>|<${tag}>|<${tag}\\s*\\/>)`] = tags[tag]; } @@ -163,7 +163,7 @@ export function replaceByRegexes(text, mapping) { let replaced; // If substitution is a function, call it - if(mapping[regexpString] instanceof Function) { + if (mapping[regexpString] instanceof Function) { replaced = mapping[regexpString].apply(null, capturedGroups); } else { replaced = mapping[regexpString]; @@ -175,7 +175,7 @@ export function replaceByRegexes(text, mapping) { output.push(replaced); } - if(typeof replaced === 'object') { + if (typeof replaced === 'object') { shouldWrapInSpan = true; } @@ -185,7 +185,7 @@ export function replaceByRegexes(text, mapping) { } } - if(shouldWrapInSpan) { + if (shouldWrapInSpan) { return React.createElement('span', null, ...output); } else { return output.join(''); From a80935e181da404e64643d4619d150872da3db75 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 16 Nov 2017 11:45:40 +0000 Subject: [PATCH 13/13] JSX does not do comments in a way one might expect --- src/components/views/messages/SenderProfile.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/views/messages/SenderProfile.js b/src/components/views/messages/SenderProfile.js index 04e7832493..a8116b1e8a 100644 --- a/src/components/views/messages/SenderProfile.js +++ b/src/components/views/messages/SenderProfile.js @@ -50,13 +50,13 @@ export default function SenderProfile(props) { content = substitute('%(senderName)s', { senderName: () => nameElem }); } + // The text surrounding the user name must be wrapped in order for it to have the correct opacity. + // It is not possible to wrap the whole thing, because the user name might contain flair which should + // be shown at full opacity. Sadly CSS does not make it possible to "reset" opacity so we have to do it + // in parts like this. Sometimes CSS makes me a sad panda :-( + // XXX: This could be avoided if the actual colour is set, rather than faking it with opacity return (
- // The text surrounding the user name must be wrapped in order for it to have the correct opacity. - // It is not possible to wrap the whole thing, because the user name might contain flair which should - // be shown at full opacity. Sadly CSS does not make it possible to "reset" opacity so we have to do it - // in parts like this. Sometimes CSS makes me a sad panda :-( - // XXX: This could be avoided if the actual colour is set, rather than faking it with opacity { content.props.children[0] ? { content.props.children[0] } : '' }