diff --git a/lang/main.json b/lang/main.json index 78a8249d1a..14254122dd 100644 --- a/lang/main.json +++ b/lang/main.json @@ -40,7 +40,9 @@ }, "welcomepage":{ "go": "GO", + "join": "JOIN", "roomname": "Enter room name", + "roomnamePlaceHolder": "room name", "disable": "Don't show this page again", "feature1": { "title": "Simple to use", @@ -73,7 +75,10 @@ "feature8": { "title": "Usage statistics", "content": "Learn about your users through easy integration with Piwik, Google Analytics, and other usage monitoring and statistics systems." - } + }, + "privacy": "Privacy", + "sendFeedback": "Send feedback", + "terms": "Terms" }, "startupoverlay": { "policyText": " ", @@ -110,6 +115,15 @@ "profile": "Edit your profile", "raiseHand": "Raise / Lower your hand" }, + "unsupportedPage": { + "onlySupportedBy": "This application is currently only supported by", + "download": "DOWNLOAD", + "joinConversation": "Join the conversation", + "startConference": "Start a conference", + "joinConversationMobile": "You need __app__ to join a conversation on your mobile", + "downloadApp": "Download the App", + "availableApp": "or if you already have it
then" + }, "bottomtoolbar": { "chat": "Open / close chat", "filmstrip": "Show / hide videos", diff --git a/package.json b/package.json index 9d7e0840d0..71b71308df 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "autosize": "^1.18.13", "bootstrap": "3.1.1", "i18next": "7.0.0", - "i18next-browser-languagedetector": "*", + "i18next-browser-languagedetector": "1.0.1", "i18next-xhr-backend": "1.3.0", "jitsi-meet-logger": "jitsi/jitsi-meet-logger", "jquery": "~2.1.1", @@ -40,6 +40,7 @@ "react-native-background-timer": "1.0.0", "react-native-immersive": "0.0.4", "react-native-keep-awake": "^2.0.2", + "react-native-locale-detector": "1.0.1 ", "react-native-prompt": "^1.0.0", "react-native-vector-icons": "^4.0.0", "react-native-webrtc": "jitsi/react-native-webrtc", diff --git a/react/features/base/react/components/Watermarks.web.js b/react/features/base/react/components/Watermarks.web.js index 60b674e455..9f18a764d4 100644 --- a/react/features/base/react/components/Watermarks.web.js +++ b/react/features/base/react/components/Watermarks.web.js @@ -1,6 +1,7 @@ /* @flow */ import React, { Component } from 'react'; +import { translate } from '../../translation'; declare var APP: Object; declare var interfaceConfig: Object; @@ -18,7 +19,7 @@ const _RIGHT_WATERMARK_STYLE = { * A Web Component which renders watermarks such as Jits, brand, powered by, * etc. */ -export class Watermarks extends Component { +class WatermarksComponent extends Component { state = { brandWatermarkLink: String, jitsiWatermarkLink: String, @@ -139,12 +140,14 @@ export class Watermarks extends Component { */ _renderPoweredBy() { if (this.state.showPoweredBy) { + const { t } = this.props; + return ( - jitsi.org + {t('poweredby')} jitsi.org ); } @@ -152,3 +155,5 @@ export class Watermarks extends Component { return null; } } + +export const Watermarks = translate(WatermarksComponent); diff --git a/react/features/base/translation/ConfigLanguageDetector.js b/react/features/base/translation/ConfigLanguageDetector.js index 427cf0850b..2369ed8305 100644 --- a/react/features/base/translation/ConfigLanguageDetector.js +++ b/react/features/base/translation/ConfigLanguageDetector.js @@ -12,7 +12,7 @@ export default { /** * The actual lookup. * - * @returns {string} the default language if any. + * @returns {string} The default language if any. */ lookup() { return config.defaultLanguage; diff --git a/react/features/base/translation/LanguageDetector.native.js b/react/features/base/translation/LanguageDetector.native.js new file mode 100644 index 0000000000..e30fffeb4e --- /dev/null +++ b/react/features/base/translation/LanguageDetector.native.js @@ -0,0 +1,11 @@ +import locale from 'react-native-locale-detector'; + +/** + * A language detector that uses native locale. + */ +export default { + init: Function.prototype, + type: 'languageDetector', + detect: () => locale, + cacheUserLanguage: Function.prototype +}; diff --git a/react/features/base/translation/LanguageDetector.web.js b/react/features/base/translation/LanguageDetector.web.js new file mode 100644 index 0000000000..c45eebcd5d --- /dev/null +++ b/react/features/base/translation/LanguageDetector.web.js @@ -0,0 +1,34 @@ +/* global interfaceConfig */ +import Browser from 'i18next-browser-languagedetector'; +import ConfigLanguageDetector from './ConfigLanguageDetector'; + +/** + * List of detectors to use in their order. + * + * @type {[*]} + */ +const detectors = [ 'querystring', 'localStorage', 'configLanguageDetector' ]; + +/** + * Allow i18n to detect the system language from the browser. + */ +if (interfaceConfig.LANG_DETECTION) { + detectors.push('navigator'); +} + +/** + * The language detectors. + */ +const browser = new Browser(null, { + order: detectors, + lookupQuerystring: 'lang', + lookupLocalStorage: 'language', + caches: [ 'localStorage' ] +}); + +/** + * adds a language detector that just checks the config + */ +browser.addDetector(ConfigLanguageDetector); + +export default browser; diff --git a/react/features/base/translation/Translation.js b/react/features/base/translation/Translation.js index 3af4b17456..4ec22b8fb5 100644 --- a/react/features/base/translation/Translation.js +++ b/react/features/base/translation/Translation.js @@ -4,8 +4,8 @@ import XHR from 'i18next-xhr-backend'; import { DEFAULT_LANG, languages } from './constants'; import languagesR from '../../../../lang/languages.json'; import mainR from '../../../../lang/main.json'; -import Browser from 'i18next-browser-languagedetector'; -import ConfigLanguageDetector from './ConfigLanguageDetector'; + +import LanguageDetector from './LanguageDetector'; /** * Default options to initialize i18next. @@ -26,38 +26,12 @@ const defaultOptions = { fallbackOnNull: true, fallbackOnEmpty: true, useDataAttrOptions: true, - app: interfaceConfig.APP_NAME + app: typeof interfaceConfig === 'undefined' + ? 'Jitsi Meet' : interfaceConfig.APP_NAME }; -/** - * List of detectors to use in their order. - * - * @type {[*]} - */ -const detectors = [ 'querystring', 'localStorage', 'configLanguageDetector' ]; - -/** - * Allow i18n to detect the system language from the browser. - */ -if (interfaceConfig.LANG_DETECTION) { - detectors.push('navigator'); -} - -/** - * The language detectors. - */ -const browser = new Browser(null, { - order: detectors, - lookupQuerystring: 'lang', - lookupLocalStorage: 'language', - caches: [ 'localStorage' ] -}); - -// adds a language detector that just checks the config -browser.addDetector(ConfigLanguageDetector); - i18n.use(XHR) - .use(browser) + .use(LanguageDetector) .use({ type: 'postProcessor', name: 'resolveAppName', diff --git a/react/features/base/translation/functions.js b/react/features/base/translation/functions.js index 25bee46068..023b6e7e42 100644 --- a/react/features/base/translation/functions.js +++ b/react/features/base/translation/functions.js @@ -1,12 +1,32 @@ import { translate as reactTranslate } from 'react-i18next'; +import React from 'react'; /** * Wrap a translatable component. * - * @param {Component} component - the component to wrap - * @returns {Component} the wrapped component. + * @param {Component} component - The component to wrap. + * @returns {Component} The wrapped component. */ export function translate(component) { // use the default list of namespaces return reactTranslate([ 'main', 'languages' ], { wait: true })(component); } + +/** + * Translates key and prepares data to be passed to dangerouslySetInnerHTML. + * Used when translation text contains html. + * + * @param {func} t - Translate function. + * @param {string} key - The key to translate. + * @param {Array} options - Optional options. + * @returns {XML} A span using dangerouslySetInnerHTML to insert html text. + */ +export function translateToHTML(t, key, options = {}) { + /* eslint-disable react/no-danger */ + return ( + + ); + + /* eslint-enable react/no-danger */ +} diff --git a/react/features/conference/components/Conference.web.js b/react/features/conference/components/Conference.web.js index 1175d477b1..be7b366e7e 100644 --- a/react/features/conference/components/Conference.web.js +++ b/react/features/conference/components/Conference.web.js @@ -51,9 +51,6 @@ class Conference extends Component { APP.UI.registerListeners(); APP.UI.bindEvents(); - // XXX Temporary solution until we add React translation. - APP.translation.translateElement($('#videoconference_page')); - this.props.dispatch(connect()); } diff --git a/react/features/conference/components/PasswordRequiredPrompt.native.js b/react/features/conference/components/PasswordRequiredPrompt.native.js index 7944cb5131..7ad25da2a2 100644 --- a/react/features/conference/components/PasswordRequiredPrompt.native.js +++ b/react/features/conference/components/PasswordRequiredPrompt.native.js @@ -4,6 +4,8 @@ import { connect } from 'react-redux'; import { setPassword } from '../../base/conference'; +import { translate } from '../../base/translation'; + /** * Implements a React Component which prompts the user when a password is * required to join a conference. @@ -21,7 +23,8 @@ class PasswordRequiredPrompt extends Component { * @type {JitsiConference} */ conference: React.PropTypes.object, - dispatch: React.PropTypes.func + dispatch: React.PropTypes.func, + t: React.PropTypes.func } /** @@ -45,12 +48,14 @@ class PasswordRequiredPrompt extends Component { * @returns {ReactElement} */ render() { + const { t } = this.props; + return ( ); } @@ -84,4 +89,4 @@ class PasswordRequiredPrompt extends Component { } } -export default connect()(PasswordRequiredPrompt); +export default translate(connect()(PasswordRequiredPrompt)); diff --git a/react/features/overlay/components/AbstractOverlay.js b/react/features/overlay/components/AbstractOverlay.js index b10df51009..1c60c317a8 100644 --- a/react/features/overlay/components/AbstractOverlay.js +++ b/react/features/overlay/components/AbstractOverlay.js @@ -1,4 +1,4 @@ -/* global $, APP */ +/* global APP */ import React, { Component } from 'react'; @@ -30,17 +30,6 @@ export default class AbstractOverlay extends Component { }; } - /** - * This method is executed when comonent is mounted. - * - * @inheritdoc - * @returns {void} - */ - componentDidMount() { - // XXX Temporary solution until we add React translation. - APP.translation.translateElement($('#overlay')); - } - /** * Implements React's {@link Component#render()}. * diff --git a/react/features/overlay/components/PageReloadOverlay.js b/react/features/overlay/components/PageReloadOverlay.js index 12598be5db..fb84607552 100644 --- a/react/features/overlay/components/PageReloadOverlay.js +++ b/react/features/overlay/components/PageReloadOverlay.js @@ -5,6 +5,8 @@ import { randomInt } from '../../base/util'; import AbstractOverlay from './AbstractOverlay'; import ReloadTimer from './ReloadTimer'; +import { translate } from '../../base/translation'; + declare var APP: Object; const logger = require('jitsi-meet-logger').getLogger(__filename); @@ -14,7 +16,7 @@ const logger = require('jitsi-meet-logger').getLogger(__filename); * conference is reloaded. Shows a warning message and counts down towards the * reload. */ -export default class PageReloadOverlay extends AbstractOverlay { +class PageReloadOverlay extends AbstractOverlay { /** * PageReloadOverlay component's property types. * @@ -134,15 +136,17 @@ export default class PageReloadOverlay extends AbstractOverlay { if (this.props.isNetworkFailure) { const className = 'button-control button-control_primary button-control_center'; + const { t } = this.props; /* eslint-disable react/jsx-handler-names */ return ( ); @@ -161,17 +165,20 @@ export default class PageReloadOverlay extends AbstractOverlay { * @protected */ _renderOverlayContent() { + const { t } = this.props; /* eslint-disable react/jsx-handler-names */ return (
+ className = 'reload_overlay_title'> + { t(this.state.title) } + + className = 'reload_overlay_text'> + { t(this.state.message) } +
+ + { t('dialog.conferenceReloadTimeLeft') } +
); } } + +export default translate(ReloadTimer); diff --git a/react/features/overlay/components/SuspendedOverlay.js b/react/features/overlay/components/SuspendedOverlay.js index 7a129e99af..bbc8948583 100644 --- a/react/features/overlay/components/SuspendedOverlay.js +++ b/react/features/overlay/components/SuspendedOverlay.js @@ -2,11 +2,13 @@ import React from 'react'; import AbstractOverlay from './AbstractOverlay'; +import { translate } from '../../base/translation'; + /** * Implements a React Component for suspended overlay. Shown when a suspend is * detected. */ -export default class SuspendedOverlay extends AbstractOverlay { +class SuspendedOverlay extends AbstractOverlay { /** * Constructs overlay body with the message and a button to rejoin. * @@ -16,6 +18,7 @@ export default class SuspendedOverlay extends AbstractOverlay { */ _renderOverlayContent() { const btnClass = 'inlay__button button-control button-control_primary'; + const { t } = this.props; /* eslint-disable react/jsx-handler-names */ @@ -24,15 +27,19 @@ export default class SuspendedOverlay extends AbstractOverlay {

+ className = 'inlay__title'> + { t('suspendedoverlay.title') } +

); /* eslint-enable react/jsx-handler-names */ } } + +export default translate(SuspendedOverlay); diff --git a/react/features/overlay/components/UserMediaPermissionsOverlay.js b/react/features/overlay/components/UserMediaPermissionsOverlay.js index ef9f2e3878..b304a600ca 100644 --- a/react/features/overlay/components/UserMediaPermissionsOverlay.js +++ b/react/features/overlay/components/UserMediaPermissionsOverlay.js @@ -4,11 +4,13 @@ import React from 'react'; import AbstractOverlay from './AbstractOverlay'; +import { translate, translateToHTML } from '../../base/translation'; + /** * Implements a React Component for overlay with guidance how to proceed with * gUM prompt. */ -export default class UserMediaPermissionsOverlay extends AbstractOverlay { +class UserMediaPermissionsOverlay extends AbstractOverlay { /** * UserMediaPermissionsOverlay component's property types. * @@ -54,26 +56,26 @@ export default class UserMediaPermissionsOverlay extends AbstractOverlay { * @protected */ _renderOverlayContent() { - const textKey = `userMedia.${this.props.browser}GrantPermissions`; + const { t } = this.props; return (
-

- +

+ { t('startupoverlay.title', + { postProcess: 'resolveAppName' }) } +

+ + { translateToHTML(t, + `userMedia.${this.props.browser}GrantPermissions`)} +

-

+

+ { t('startupoverlay.policyText') } +

{ this._renderPolicyLogo() } @@ -102,3 +104,5 @@ export default class UserMediaPermissionsOverlay extends AbstractOverlay { return null; } } + +export default translate(UserMediaPermissionsOverlay); diff --git a/react/features/room-lock/components/RoomLockPrompt.native.js b/react/features/room-lock/components/RoomLockPrompt.native.js index b2c5e73d96..7a4a7ba734 100644 --- a/react/features/room-lock/components/RoomLockPrompt.native.js +++ b/react/features/room-lock/components/RoomLockPrompt.native.js @@ -4,6 +4,8 @@ import { connect } from 'react-redux'; import { endRoomLockRequest } from '../actions'; +import { translate } from '../../base/translation'; + /** * Implements a React Component which prompts the user for a password to lock a * conference/room. @@ -21,7 +23,8 @@ class RoomLockPrompt extends Component { * @type {JitsiConference} */ conference: React.PropTypes.object, - dispatch: React.PropTypes.func + dispatch: React.PropTypes.func, + t: React.PropTypes.func } /** @@ -45,12 +48,14 @@ class RoomLockPrompt extends Component { * @returns {ReactElement} */ render() { + const { t } = this.props; + return ( ); } @@ -80,4 +85,4 @@ class RoomLockPrompt extends Component { } } -export default connect()(RoomLockPrompt); +export default translate(connect()(RoomLockPrompt)); diff --git a/react/features/unsupported-browser/components/UnsupportedDesktopBrowser.js b/react/features/unsupported-browser/components/UnsupportedDesktopBrowser.js index 291953530d..1d94601705 100644 --- a/react/features/unsupported-browser/components/UnsupportedDesktopBrowser.js +++ b/react/features/unsupported-browser/components/UnsupportedDesktopBrowser.js @@ -3,6 +3,7 @@ import React, { Component } from 'react'; import { Platform } from '../../base/react'; +import { translate } from '../../base/translation'; import { CHROME, FIREFOX, IE, SAFARI } from './browserLinks'; import HideNotificationBarStyle from './HideNotificationBarStyle'; @@ -20,7 +21,16 @@ const _NS = 'unsupported-desktop-browser'; * * @class UnsupportedDesktopBrowser */ -export default class UnsupportedDesktopBrowser extends Component { +class UnsupportedDesktopBrowser extends Component { + /** + * UnsupportedDesktopBrowser component's property types. + * + * @static + */ + static propTypes = { + t: React.PropTypes.func + } + /** * Renders the component. * @@ -87,3 +97,4 @@ export default class UnsupportedDesktopBrowser extends Component { } } +export default translate(UnsupportedDesktopBrowser); diff --git a/react/features/unsupported-browser/components/UnsupportedMobileBrowser.js b/react/features/unsupported-browser/components/UnsupportedMobileBrowser.js index 6c276f2ab7..e0e45300bf 100644 --- a/react/features/unsupported-browser/components/UnsupportedMobileBrowser.js +++ b/react/features/unsupported-browser/components/UnsupportedMobileBrowser.js @@ -4,6 +4,7 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { Platform } from '../../base/react'; +import { translate, translateToHTML } from '../../base/translation'; import HideNotificationBarStyle from './HideNotificationBarStyle'; @@ -39,7 +40,8 @@ class UnsupportedMobileBrowser extends Component { * @private * @type {string} */ - _room: React.PropTypes.string + _room: React.PropTypes.string, + t: React.PropTypes.func } /** @@ -50,7 +52,8 @@ class UnsupportedMobileBrowser extends Component { */ componentWillMount() { const joinText - = this.props._room ? 'Join the conversation' : 'Start a conference'; + = this.props._room ? 'unsupportedPage.joinConversation' + : 'unsupportedPage.startConference'; // If the user installed the app while this Component was displayed // (e.g. the user clicked the Download the App button), then we would @@ -74,6 +77,7 @@ class UnsupportedMobileBrowser extends Component { render() { const ns = 'unsupported-mobile-browser'; const downloadButtonClassName = `${ns}__button ${ns}__button_primary`; + const { t } = this.props; return (
@@ -82,24 +86,21 @@ class UnsupportedMobileBrowser extends Component { className = { `${ns}__logo` } src = 'images/logo-blue.svg' />

- You need Jitsi Meet to join a - conversation on mobile + { translateToHTML(t, + 'unsupportedPage.joinConversationMobile', + { postProcess: 'resolveAppName' }) }

- or if you already have it -
- then + { translateToHTML(t, 'unsupportedPage.availableApp') }

@@ -133,4 +134,4 @@ function _mapStateToProps(state) { }; } -export default connect(_mapStateToProps)(UnsupportedMobileBrowser); +export default translate(connect(_mapStateToProps)(UnsupportedMobileBrowser)); diff --git a/react/features/welcome/components/WelcomePage.native.js b/react/features/welcome/components/WelcomePage.native.js index 54f8bd7d14..258aa14244 100644 --- a/react/features/welcome/components/WelcomePage.native.js +++ b/react/features/welcome/components/WelcomePage.native.js @@ -8,6 +8,8 @@ import { ColorPalette } from '../../base/styles'; import { AbstractWelcomePage, _mapStateToProps } from './AbstractWelcomePage'; import { styles } from './styles'; +import { translate } from '../../base/translation'; + /** * The URL at which the privacy policy is available to the user. */ @@ -62,22 +64,24 @@ class WelcomePage extends AbstractWelcomePage { * @returns {ReactElement} */ _renderLegalese() { + const { t } = this.props; + return ( - Terms + { t('welcomepage.terms') } - Privacy + { t('welcomepage.privacy') } - Send feedback + { t('welcomepage.sendFeedback') } ); @@ -93,10 +97,14 @@ class WelcomePage extends AbstractWelcomePage { * @returns {ReactElement} */ _renderLocalVideoOverlay() { + const { t } = this.props; + return ( - Enter room name + + { t('welcomepage.roomname') } + @@ -114,7 +122,9 @@ class WelcomePage extends AbstractWelcomePage { onPress = { this._onJoin } style = { styles.button } underlayColor = { ColorPalette.white }> - JOIN + + { t('welcomepage.join') } + { @@ -125,4 +135,4 @@ class WelcomePage extends AbstractWelcomePage { } } -export default connect(_mapStateToProps)(WelcomePage); +export default translate(connect(_mapStateToProps)(WelcomePage)); diff --git a/react/features/welcome/components/WelcomePage.web.js b/react/features/welcome/components/WelcomePage.web.js index 541b357e6e..8fea1460ae 100644 --- a/react/features/welcome/components/WelcomePage.web.js +++ b/react/features/welcome/components/WelcomePage.web.js @@ -1,4 +1,4 @@ -/* global $, APP, interfaceConfig */ +/* global APP, interfaceConfig */ import React from 'react'; import { connect } from 'react-redux'; @@ -7,6 +7,8 @@ import { Watermarks } from '../../base/react'; import { AbstractWelcomePage, _mapStateToProps } from './AbstractWelcomePage'; +import { translate } from '../../base/translation'; + /* eslint-disable require-jsdoc */ /** @@ -51,9 +53,6 @@ class WelcomePage extends AbstractWelcomePage { if (this.state.generateRoomnames) { this._updateRoomname(); } - - // XXX Temporary solution until we add React translation. - APP.translation.translateElement($('#welcome_page')); } /** @@ -142,19 +141,21 @@ class WelcomePage extends AbstractWelcomePage { * @returns {ReactElement} */ _renderFeature(index) { + const { t } = this.props; + return (
+ className = 'feature_icon'> + { t(`welcomepage.feature${index}.title`) } +
+ className = 'feature_description'> + { t(`welcomepage.feature${index}.content`, + { postProcess: 'resolveAppName' }) } +
); } @@ -196,6 +197,7 @@ class WelcomePage extends AbstractWelcomePage { _renderHeader() { /* eslint-enable require-jsdoc */ + const { t } = this.props; return (
@@ -229,10 +231,11 @@ class WelcomePage extends AbstractWelcomePage {
@@ -245,8 +248,9 @@ class WelcomePage extends AbstractWelcomePage { type = 'checkbox' />
); @@ -274,4 +278,4 @@ class WelcomePage extends AbstractWelcomePage { } } -export default connect(_mapStateToProps)(WelcomePage); +export default translate(connect(_mapStateToProps)(WelcomePage));