Translate react strings.

Split language detectors to be web/native dependent. Take care of strings that contain html.
This commit is contained in:
damencho 2017-02-23 10:56:25 -06:00 committed by Lyubo Marinov
parent e3d4152e32
commit c361e1e31a
20 changed files with 242 additions and 125 deletions

View File

@ -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 <strong>__app__</strong> to join a conversation on your mobile",
"downloadApp": "Download the App",
"availableApp": "or if you already have it<br /><strong>then</strong>"
},
"bottomtoolbar": {
"chat": "Open / close chat",
"filmstrip": "Show / hide videos",

View File

@ -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",

View File

@ -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 (
<a
className = 'poweredby'
href = 'http://jitsi.org'
target = '_new'>
<span data-i18n = 'poweredby' /> jitsi.org
<span>{t('poweredby')} jitsi.org</span>
</a>
);
}
@ -152,3 +155,5 @@ export class Watermarks extends Component {
return null;
}
}
export const Watermarks = translate(WatermarksComponent);

View File

@ -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;

View File

@ -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
};

View File

@ -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;

View File

@ -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',

View File

@ -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 (
<span
dangerouslySetInnerHTML = {{ __html: t(key, options) }} />
);
/* eslint-enable react/no-danger */
}

View File

@ -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());
}

View File

@ -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 (
<Prompt
onCancel = { this._onCancel }
onSubmit = { this._onSubmit }
placeholder = 'Password'
title = 'Password required'
placeholder = { t('dialog.passwordLabel') }
title = { t('dialog.passwordRequired') }
visible = { true } />
);
}
@ -84,4 +89,4 @@ class PasswordRequiredPrompt extends Component {
}
}
export default connect()(PasswordRequiredPrompt);
export default translate(connect()(PasswordRequiredPrompt));

View File

@ -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()}.
*

View File

@ -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 (
<button
className = { className }
data-i18n = 'dialog.reconnectNow'
id = 'reconnectNow'
onClick = { this._reconnectNow } />
onClick = { this._reconnectNow }>
{ t('dialog.reconnectNow') }
</button>
);
@ -161,17 +165,20 @@ export default class PageReloadOverlay extends AbstractOverlay {
* @protected
*/
_renderOverlayContent() {
const { t } = this.props;
/* eslint-disable react/jsx-handler-names */
return (
<div className = 'inlay'>
<span
className = 'reload_overlay_title'
data-i18n = { this.state.title } />
className = 'reload_overlay_title'>
{ t(this.state.title) }
</span>
<span
className = 'reload_overlay_text'
data-i18n = { this.state.message } />
className = 'reload_overlay_text'>
{ t(this.state.message) }
</span>
<ReloadTimer
end = { 0 }
interval = { 1 }
@ -185,3 +192,5 @@ export default class PageReloadOverlay extends AbstractOverlay {
/* eslint-enable react/jsx-handler-names */
}
}
export default translate(PageReloadOverlay);

View File

@ -1,5 +1,7 @@
import React, { Component } from 'react';
import { translate } from '../../base/translation';
declare var AJS: Object;
/**
@ -8,7 +10,7 @@ declare var AJS: Object;
* seconds until the current value reaches props.end. Also displays progress
* bar.
*/
export default class ReloadTimer extends Component {
class ReloadTimer extends Component {
/**
* ReloadTimer component's property types.
*
@ -52,7 +54,15 @@ export default class ReloadTimer extends Component {
* @public
* @type {number}
*/
step: React.PropTypes.number
step: React.PropTypes.number,
/**
* The function used to translate strings.
*
* @public
* @type {func}
*/
t: React.PropTypes.func
}
/**
@ -132,6 +142,8 @@ export default class ReloadTimer extends Component {
* @public
*/
render() {
const { t } = this.props;
return (
<div>
<div
@ -143,9 +155,13 @@ export default class ReloadTimer extends Component {
{
this.state.current
}
<span data-i18n = 'dialog.conferenceReloadTimeLeft' />
<span>
{ t('dialog.conferenceReloadTimeLeft') }
</span>
</span>
</div>
);
}
}
export default translate(ReloadTimer);

View File

@ -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 {
<span className = 'inlay__icon icon-microphone' />
<span className = 'inlay__icon icon-camera' />
<h3
className = 'inlay__title'
data-i18n = 'suspendedoverlay.title' />
className = 'inlay__title'>
{ t('suspendedoverlay.title') }
</h3>
<button
className = { btnClass }
data-i18n = 'suspendedoverlay.rejoinKeyTitle'
onClick = { this._reconnectNow } />
onClick = { this._reconnectNow }>
{ t('suspendedoverlay.rejoinKeyTitle') }
</button>
</div>
);
/* eslint-enable react/jsx-handler-names */
}
}
export default translate(SuspendedOverlay);

View File

@ -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 (
<div>
<div className = 'inlay'>
<span className = 'inlay__icon icon-microphone' />
<span className = 'inlay__icon icon-camera' />
<h3
className = 'inlay__title'
data-i18n = 'startupoverlay.title'
data-i18n-options
= '{"postProcess": "resolveAppName"}' />
<span
className = 'inlay__text'
data-i18n = { `[html]${textKey}` } />
<h3 className = 'inlay__title'>
{ t('startupoverlay.title',
{ postProcess: 'resolveAppName' }) }
</h3>
<span className = 'inlay__text'>
{ translateToHTML(t,
`userMedia.${this.props.browser}GrantPermissions`)}
</span>
</div>
<div className = 'policy overlay__policy'>
<p
className = 'policy__text'
data-i18n = '[html]startupoverlay.policyText' />
<p className = 'policy__text'>
{ t('startupoverlay.policyText') }
</p>
{
this._renderPolicyLogo()
}
@ -102,3 +104,5 @@ export default class UserMediaPermissionsOverlay extends AbstractOverlay {
return null;
}
}
export default translate(UserMediaPermissionsOverlay);

View File

@ -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 (
<Prompt
onCancel = { this._onCancel }
onSubmit = { this._onSubmit }
placeholder = 'Password'
title = 'Lock / Unlock room'
placeholder = { t('dialog.passwordLabel') }
title = { t('toolbar.lock') }
visible = { true } />
);
}
@ -80,4 +85,4 @@ class RoomLockPrompt extends Component {
}
}
export default connect()(RoomLockPrompt);
export default translate(connect()(RoomLockPrompt));

View File

@ -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);

View File

@ -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 (
<div className = { ns }>
@ -82,24 +86,21 @@ class UnsupportedMobileBrowser extends Component {
className = { `${ns}__logo` }
src = 'images/logo-blue.svg' />
<p className = { `${ns}__text` }>
You need <strong>Jitsi Meet</strong> to join a
conversation on mobile
{ translateToHTML(t,
'unsupportedPage.joinConversationMobile',
{ postProcess: 'resolveAppName' }) }
</p>
<a href = { _URLS[Platform.OS] }>
<button className = { downloadButtonClassName }>
Download the App
{ t('unsupportedPage.downloadApp') }
</button>
</a>
<p className = { `${ns}__text ${ns}__text_small` }>
or if you already have it
<br />
<strong>then</strong>
{ translateToHTML(t, 'unsupportedPage.availableApp') }
</p>
<a href = { this.state.joinURL }>
<button className = { `${ns}__button` }>
{
this.state.joinText
}
{ t(this.state.joinText) }
</button>
</a>
</div>
@ -133,4 +134,4 @@ function _mapStateToProps(state) {
};
}
export default connect(_mapStateToProps)(UnsupportedMobileBrowser);
export default translate(connect(_mapStateToProps)(UnsupportedMobileBrowser));

View File

@ -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 (
<View style = { styles.legaleseContainer }>
<Link
style = { styles.legaleseItem }
url = { TERMS_URL }>
Terms
{ t('welcomepage.terms') }
</Link>
<Link
style = { styles.legaleseItem }
url = { PRIVACY_URL }>
Privacy
{ t('welcomepage.privacy') }
</Link>
<Link
style = { styles.legaleseItem }
url = { SEND_FEEDBACK_URL }>
Send feedback
{ t('welcomepage.sendFeedback') }
</Link>
</View>
);
@ -93,10 +97,14 @@ class WelcomePage extends AbstractWelcomePage {
* @returns {ReactElement}
*/
_renderLocalVideoOverlay() {
const { t } = this.props;
return (
<View style = { styles.localVideoOverlay }>
<View style = { styles.roomContainer }>
<Text style = { styles.title }>Enter room name</Text>
<Text style = { styles.title }>
{ t('welcomepage.roomname') }
</Text>
<TextInput
accessibilityLabel = { 'Input room name.' }
autoCapitalize = 'none'
@ -104,7 +112,7 @@ class WelcomePage extends AbstractWelcomePage {
autoCorrect = { false }
autoFocus = { false }
onChangeText = { this._onRoomChange }
placeholder = 'room name'
placeholder = { t('welcomepage.roomnamePlaceHolder') }
style = { styles.textInput }
underlineColorAndroid = 'transparent'
value = { this.state.room } />
@ -114,7 +122,9 @@ class WelcomePage extends AbstractWelcomePage {
onPress = { this._onJoin }
style = { styles.button }
underlayColor = { ColorPalette.white }>
<Text style = { styles.buttonText }>JOIN</Text>
<Text style = { styles.buttonText }>
{ t('welcomepage.join') }
</Text>
</TouchableHighlight>
</View>
{
@ -125,4 +135,4 @@ class WelcomePage extends AbstractWelcomePage {
}
}
export default connect(_mapStateToProps)(WelcomePage);
export default translate(connect(_mapStateToProps)(WelcomePage));

View File

@ -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 (
<div
className = 'feature_holder'
key = { index } >
<div
className = 'feature_icon'
data-i18n = { `welcomepage.feature${index}.title` } />
className = 'feature_icon'>
{ t(`welcomepage.feature${index}.title`) }
</div>
<div
className = 'feature_description'
data-i18n = { `welcomepage.feature${index}.content` }
data-i18n-options = { JSON.stringify({
postProcess: 'resolveAppName'
}) } />
className = 'feature_description'>
{ t(`welcomepage.feature${index}.content`,
{ postProcess: 'resolveAppName' }) }
</div>
</div>
);
}
@ -196,6 +197,7 @@ class WelcomePage extends AbstractWelcomePage {
_renderHeader() {
/* eslint-enable require-jsdoc */
const { t } = this.props;
return (
<div id = 'welcome_page_header'>
@ -229,10 +231,11 @@ class WelcomePage extends AbstractWelcomePage {
<button
className = 'enter-room__button'
data-i18n = 'welcomepage.go'
id = 'enter_room_button'
onClick = { this._onJoin }
type = 'button' />
type = 'button'>
{ t('welcomepage.go') }
</button>
</div>
</div>
</div>
@ -245,8 +248,9 @@ class WelcomePage extends AbstractWelcomePage {
type = 'checkbox' />
<label
className = 'disable_welcome_position'
data-i18n = 'welcomepage.disable'
htmlFor = 'disable_welcome' />
htmlFor = 'disable_welcome'>
{ t('welcomepage.disable') }
</label>
<div id = 'header_text' />
</div>
);
@ -274,4 +278,4 @@ class WelcomePage extends AbstractWelcomePage {
}
}
export default connect(_mapStateToProps)(WelcomePage);
export default translate(connect(_mapStateToProps)(WelcomePage));