diff options
author | freddytuxworth <freddytuxworth@gmail.com> | 2020-06-26 12:05:42 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-06-26 13:05:42 +0200 |
commit | 2efa8b057e050e32eb2a45750ba5e6e7bd251fe7 (patch) | |
tree | 8b2cf5832aa9645bf49c9ed7219713c7f56e40df /app | |
parent | 1c4f76e3b84ae33823474f8c7970ff6f839f0b66 (diff) |
Introduce internationalisation
Diffstat (limited to 'app')
26 files changed, 357 insertions, 854 deletions
diff --git a/app/features/navbar/components/HelpButton.js b/app/features/navbar/components/HelpButton.js index 8df9a46..9adcbda 100644 --- a/app/features/navbar/components/HelpButton.js +++ b/app/features/navbar/components/HelpButton.js @@ -4,6 +4,7 @@ import Droplist, { Item, Group } from '@atlaskit/droplist'; import HelpIcon from '@atlaskit/icon/glyph/question-circle'; import React, { Component } from 'react'; +import { withTranslation } from 'react-i18next'; import config from '../../config'; import { openExternalLink } from '../../utils'; @@ -20,7 +21,7 @@ type State = { /** * Help button for Navigation Bar. */ -export default class HelpButton extends Component< *, State> { +class HelpButton extends Component<*, State> { /** * Initializes a new {@code HelpButton} instance. * @@ -87,6 +88,8 @@ export default class HelpButton extends Component< *, State> { * @returns {ReactElement} */ render() { + const { t } = this.props; + return ( <Droplist isOpen = { this.state.droplistOpen } @@ -94,27 +97,29 @@ export default class HelpButton extends Component< *, State> { onOpenChange = { this._onOpenChange } position = 'right bottom' trigger = { <HelpIcon /> }> - <Group heading = 'Help'> + <Group heading = { t('help') } > <Item onActivate = { this._onTermsClick }> - Terms + { t('termsLink') } </Item> <Item onActivate = { this._onPrivacyClick }> - Privacy + { t('privacyLink') } </Item> <Item onActivate = { this._onSendFeedbackClick }> - Send Feedback + { t('sendFeedbackLink') } </Item> <Item onActivate = { this._onAboutClick }> - About + { t('aboutLink') } </Item> <Item onActivate = { this._onSourceClick }> - Source + { t('sourceLink') } </Item> <Item> - Version: { version } + { t('versionLabel', { version }) } </Item> </Group> </Droplist> ); } } + +export default withTranslation()(HelpButton); diff --git a/app/features/onboarding/components/AlwaysOnTopWindowSpotlight.js b/app/features/onboarding/components/AlwaysOnTopWindowSpotlight.js deleted file mode 100644 index 53a8f2b..0000000 --- a/app/features/onboarding/components/AlwaysOnTopWindowSpotlight.js +++ /dev/null @@ -1,78 +0,0 @@ -// @flow - -import { Spotlight } from '@atlaskit/onboarding'; - -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import type { Dispatch } from 'redux'; - -import { closeDrawer } from '../../navbar'; - -import { continueOnboarding } from '../actions'; - -type Props = { - - /** - * Redux dispatch. - */ - dispatch: Dispatch<*>; -}; - -/** - * Always on Top Windows Spotlight Component. - */ -class AlwaysOnTopWindowSpotlight extends Component<Props, *> { - /** - * Initializes a new {@code StartMutedTogglesSpotlight} instance. - * - * @inheritdoc - */ - constructor(props: Props) { - super(props); - - this._next = this._next.bind(this); - } - - /** - * Render function of component. - * - * @returns {ReactElement} - */ - render() { - return ( - <Spotlight - actions = { [ - { - onClick: this._next, - text: 'Next' - } - ] } - dialogPlacement = 'top right' - target = { 'always-on-top-window' } > - You can toggle whether you want to enable the "always-on-top" window, - which is displayed when the main window loses focus. - This will be applied to all conferences. - </Spotlight> - ); - } - - _next: (*) => void; - - /** - * Close the spotlight component. - * - * @returns {void} - */ - _next() { - const { dispatch } = this.props; - - dispatch(continueOnboarding()); - - // FIXME: find a better way to do this. - setTimeout(() => { - dispatch(closeDrawer()); - }, 300); - } -} - -export default connect()(AlwaysOnTopWindowSpotlight); diff --git a/app/features/onboarding/components/ConferenceURLSpotlight.js b/app/features/onboarding/components/ConferenceURLSpotlight.js deleted file mode 100644 index 178e114..0000000 --- a/app/features/onboarding/components/ConferenceURLSpotlight.js +++ /dev/null @@ -1,70 +0,0 @@ -// @flow - -import { Spotlight } from '@atlaskit/onboarding'; - -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import type { Dispatch } from 'redux'; - -import { continueOnboarding } from '../actions'; - -type Props = { - - /** - * Redux dispatch. - */ - dispatch: Dispatch<*>; -}; - -/** - * Conference URL Spotlight Component. - */ -class ConferenceURLSpotlight extends Component<Props, *> { - /** - * Initializes a new {@code ComponentURLSpotlight} instance. - * - * @inheritdoc - */ - constructor(props: Props) { - super(props); - - this._next = this._next.bind(this); - } - - /** - * Render function of component. - * - * @returns {ReactElement} - */ - render() { - return ( - <Spotlight - actions = { [ - { - onClick: this._next, - text: 'Next' - } - ] } - dialogPlacement = 'bottom center' - target = { 'conference-url' } > - Enter the name (or full URL) of the room you want to join. You - may make a name up, just let others know so they enter the same - name. - </Spotlight> - ); - } - - _next: (*) => void; - - /** - * Close the spotlight component. - * - * @returns {void} - */ - _next() { - this.props.dispatch(continueOnboarding()); - } -} - -export default connect()(ConferenceURLSpotlight); - diff --git a/app/features/onboarding/components/EmailSettingSpotlight.js b/app/features/onboarding/components/EmailSettingSpotlight.js deleted file mode 100644 index d1bee0e..0000000 --- a/app/features/onboarding/components/EmailSettingSpotlight.js +++ /dev/null @@ -1,68 +0,0 @@ -// @flow - -import { Spotlight } from '@atlaskit/onboarding'; - -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import type { Dispatch } from 'redux'; - -import { continueOnboarding } from '../actions'; - -type Props = { - - /** - * Redux dispatch. - */ - dispatch: Dispatch<*>; -}; - -/** - * Email Setting Spotlight Component. - */ -class EmailSettingSpotlight extends Component<Props, *> { - /** - * Initializes a new {@code EmailSettingSpotlight} instance. - * - * @inheritdoc - */ - constructor(props: Props) { - super(props); - - this._next = this._next.bind(this); - } - - /** - * Render function of component. - * - * @returns {ReactElement} - */ - render() { - return ( - <Spotlight - actions = { [ - { - onClick: this._next, - text: 'Next' - } - ] } - dialogPlacement = 'top right' - target = { 'email-setting' } > - The email you enter here will be part of your user profile. - </Spotlight> - ); - } - - _next: (*) => void; - - /** - * Close the spotlight component. - * - * @returns {void} - */ - _next() { - this.props.dispatch(continueOnboarding()); - } -} - -export default connect()(EmailSettingSpotlight); - diff --git a/app/features/onboarding/components/NameSettingSpotlight.js b/app/features/onboarding/components/NameSettingSpotlight.js deleted file mode 100644 index cb5ce51..0000000 --- a/app/features/onboarding/components/NameSettingSpotlight.js +++ /dev/null @@ -1,69 +0,0 @@ -// @flow - -import { Spotlight } from '@atlaskit/onboarding'; - -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import type { Dispatch } from 'redux'; - -import { continueOnboarding } from '../actions'; - -type Props = { - - /** - * Redux dispatch. - */ - dispatch: Dispatch<*>; -}; - -/** - * Name Setting Spotlight Component. - */ -class NameSettingSpotlight extends Component<Props, *> { - /** - * Initializes a new {@code NameSettingSpotlight} instance. - * - * @inheritdoc - */ - constructor(props: Props) { - super(props); - - this._next = this._next.bind(this); - } - - /** - * Render function of component. - * - * @returns {ReactElement} - */ - render() { - return ( - <Spotlight - actions = { [ - { - onClick: this._next, - text: 'Next' - } - ] } - dialogPlacement = 'top right' - target = { 'name-setting' } > - This will be your display name, others will see you with this - name. - </Spotlight> - ); - } - - _next: (*) => void; - - /** - * Close the spotlight component. - * - * @returns {void} - */ - _next() { - this.props.dispatch(continueOnboarding()); - } -} - -export default connect()(NameSettingSpotlight); - diff --git a/app/features/onboarding/components/Onboarding.js b/app/features/onboarding/components/Onboarding.js index 31ea1d4..973beff 100644 --- a/app/features/onboarding/components/Onboarding.js +++ b/app/features/onboarding/components/Onboarding.js @@ -38,9 +38,9 @@ class Onboarding extends Component<Props, *> { const steps = onboardingSteps[section]; if (_activeOnboarding && steps.includes(_activeOnboarding)) { - const ActiveOnboarding = onboardingComponents[_activeOnboarding]; + const { type: ActiveOnboarding, ...props } = onboardingComponents[_activeOnboarding]; - return <ActiveOnboarding />; + return <ActiveOnboarding { ...props } />; } return null; diff --git a/app/features/onboarding/components/OnboardingModal.js b/app/features/onboarding/components/OnboardingModal.js index fe38c2d..57f129a 100644 --- a/app/features/onboarding/components/OnboardingModal.js +++ b/app/features/onboarding/components/OnboardingModal.js @@ -3,8 +3,10 @@ import { Modal } from '@atlaskit/onboarding'; import React, { Component } from 'react'; +import { withTranslation } from 'react-i18next'; import { connect } from 'react-redux'; import type { Dispatch } from 'redux'; +import { compose } from 'redux'; import OnboardingModalImage from '../../../images/onboarding.png'; @@ -18,6 +20,11 @@ type Props = { * Redux dispatch. */ dispatch: Dispatch<*>; + + /** + * I18next translation function. + */ + t: Function; }; /** @@ -43,21 +50,23 @@ class OnboardingModal extends Component<Props, *> { * @returns {ReactElement} */ render() { + const { t } = this.props; + return ( <Modal actions = { [ { onClick: this._next, - text: 'Start Tour' + text: t('onboarding.startTour') }, { onClick: this._skip, - text: 'Skip' + text: t('onboarding.skip') } ] } - heading = { `Welcome to ${config.appName}` } + heading = { t('onboarding.welcome', { appName: config.appName }) } image = { OnboardingModalImage } > - <p> Let us show you around!</p> + <p> { t('onboarding.letUsShowYouAround') }</p> </Modal> ); } @@ -86,4 +95,4 @@ class OnboardingModal extends Component<Props, *> { } -export default connect()(OnboardingModal); +export default compose(connect(), withTranslation())(OnboardingModal); diff --git a/app/features/onboarding/components/OnboardingSpotlight.js b/app/features/onboarding/components/OnboardingSpotlight.js new file mode 100644 index 0000000..9eccff2 --- /dev/null +++ b/app/features/onboarding/components/OnboardingSpotlight.js @@ -0,0 +1,68 @@ +// @flow + +import { Spotlight } from '@atlaskit/onboarding'; + +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { connect } from 'react-redux'; +import type { Dispatch } from 'redux'; + +import { continueOnboarding } from '../actions'; + +type Props = { + + /** + * Redux dispatch. + */ + dispatch: Dispatch<*>; + + /** + * Spotlight dialog placement. + */ + dialogPlacement: String; + + /** + * Callback when "next" clicked. + */ + onNext: Function; + + /** + * I18next translation function. + */ + t: Function; + + /** + * Spotlight target. + */ + target: String; + + /** + * Spotlight text. + */ + text: String; + +}; + +const OnboardingSpotlight = (props: Props) => { + const { t } = useTranslation(); + + return ( + <Spotlight + actions = { [ + { + onClick: () => { + props.dispatch(continueOnboarding()); + props.onNext && props.onNext(props); + }, + text: t('onboarding.next') + } + ] } + dialogPlacement = { props.dialogPlacement } + target = { props.target } > + { t(props.text) } + </Spotlight> + ); +}; + + +export default connect()(OnboardingSpotlight); diff --git a/app/features/onboarding/components/ServerSettingSpotlight.js b/app/features/onboarding/components/ServerSettingSpotlight.js deleted file mode 100644 index 84b4c4c..0000000 --- a/app/features/onboarding/components/ServerSettingSpotlight.js +++ /dev/null @@ -1,69 +0,0 @@ -// @flow - -import { Spotlight } from '@atlaskit/onboarding'; - -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import type { Dispatch } from 'redux'; - -import { continueOnboarding } from '../actions'; - -type Props = { - - /** - * Redux dispatch. - */ - dispatch: Dispatch<*>; -}; - -/** - * Server Setting Spotlight Component. - */ -class ServerSettingSpotlight extends Component<Props, *> { - /** - * Initializes a new {@code ServerSettingSpotlight} instance. - * - * @inheritdoc - */ - constructor(props: Props) { - super(props); - - this._next = this._next.bind(this); - } - - /** - * Render function of component. - * - * @returns {ReactElement} - */ - render() { - return ( - <Spotlight - actions = { [ - { - onClick: this._next, - text: 'Next' - } - ] } - dialogPlacement = 'top right' - target = { 'server-setting' } > - This will be the server where your conferences will take place. - You can use your own, but you don't need to! - </Spotlight> - ); - } - - _next: (*) => void; - - /** - * Close the spotlight component. - * - * @returns {void} - */ - _next() { - this.props.dispatch(continueOnboarding()); - } -} - -export default connect()(ServerSettingSpotlight); - diff --git a/app/features/onboarding/components/ServerTimeoutSpotlight.js b/app/features/onboarding/components/ServerTimeoutSpotlight.js deleted file mode 100644 index 9a016d9..0000000 --- a/app/features/onboarding/components/ServerTimeoutSpotlight.js +++ /dev/null @@ -1,67 +0,0 @@ -// @flow - -import { Spotlight } from '@atlaskit/onboarding'; - -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import type { Dispatch } from 'redux'; - -import { continueOnboarding } from '../actions'; - -type Props = { - - /** - * Redux dispatch. - */ - dispatch: Dispatch<*>; -}; - -/** - * Server Setting Spotlight Component. - */ -class ServerTimeoutSpotlight extends Component<Props, *> { - /** - * Initializes a new {@code ServerSettingSpotlight} instance. - * - * @inheritdoc - */ - constructor(props: Props) { - super(props); - - this._next = this._next.bind(this); - } - - /** - * Render function of component. - * - * @returns {ReactElement} - */ - render() { - return ( - <Spotlight - actions = { [ - { - onClick: this._next, - text: 'Next' - } - ] } - dialogPlacement = 'right top' - target = { 'server-timeout' } > - Timeout to join a meeting, if the meeting hasn't been joined before the timeout hits, it's cancelled. - </Spotlight> - ); - } - - _next: (*) => void; - - /** - * Close the spotlight component. - * - * @returns {void} - */ - _next() { - this.props.dispatch(continueOnboarding()); - } -} - -export default connect()(ServerTimeoutSpotlight); diff --git a/app/features/onboarding/components/SettingsDrawerSpotlight.js b/app/features/onboarding/components/SettingsDrawerSpotlight.js deleted file mode 100644 index f0bb0e8..0000000 --- a/app/features/onboarding/components/SettingsDrawerSpotlight.js +++ /dev/null @@ -1,68 +0,0 @@ -// @flow - -import { Spotlight } from '@atlaskit/onboarding'; - -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import type { Dispatch } from 'redux'; - -import { openDrawer } from '../../navbar'; -import { SettingsDrawer } from '../../settings'; - -import { continueOnboarding } from '../actions'; - -type Props = { - - /** - * Redux dispatch. - */ - dispatch: Dispatch<*>; -}; - -/** - * Settings Drawer Spotlight Component. - */ -class SettingsDrawerSpotlight extends Component<Props, *> { - /** - * Initializes a new {@code SettingsDrawerSpotlight} instance. - * - * @inheritdoc - */ - constructor(props: Props) { - super(props); - - this._next = this._next.bind(this); - } - - /** - * Render function of component. - * - * @returns {ReactElement} - */ - render() { - return ( - <Spotlight - dialogPlacement = 'top right' - target = { 'settings-drawer-button' } - targetOnClick = { this._next }> - Click here to open the settings drawer. - </Spotlight> - ); - } - - _next: (*) => void; - - /** - * Close the spotlight component and opens Settings Drawer and shows - * onboarding. - * - * @returns {void} - */ - _next() { - this.props.dispatch(openDrawer(SettingsDrawer)); - this.props.dispatch(continueOnboarding()); - } -} - -export default connect()(SettingsDrawerSpotlight); - diff --git a/app/features/onboarding/components/StartMutedTogglesSpotlight.js b/app/features/onboarding/components/StartMutedTogglesSpotlight.js deleted file mode 100644 index 4b4215f..0000000 --- a/app/features/onboarding/components/StartMutedTogglesSpotlight.js +++ /dev/null @@ -1,69 +0,0 @@ -// @flow - -import { Spotlight } from '@atlaskit/onboarding'; - -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import type { Dispatch } from 'redux'; - -import { continueOnboarding } from '../actions'; - -type Props = { - - /** - * Redux dispatch. - */ - dispatch: Dispatch<*>; -}; - -/** - * Start Muted Toggles Spotlight Component. - */ -class StartMutedTogglesSpotlight extends Component<Props, *> { - /** - * Initializes a new {@code StartMutedTogglesSpotlight} instance. - * - * @inheritdoc - */ - constructor(props: Props) { - super(props); - - this._next = this._next.bind(this); - } - - /** - * Render function of component. - * - * @returns {ReactElement} - */ - render() { - return ( - <Spotlight - actions = { [ - { - onClick: this._next, - text: 'Next' - } - ] } - dialogPlacement = 'top right' - target = { 'start-muted-toggles' } > - You can toggle if you want to start with your audio or video - muted here. This will be applied to all conferences. - </Spotlight> - ); - } - - _next: (*) => void; - - /** - * Close the spotlight component. - * - * @returns {void} - */ - _next() { - this.props.dispatch(continueOnboarding()); - } -} - -export default connect()(StartMutedTogglesSpotlight); - diff --git a/app/features/onboarding/components/index.js b/app/features/onboarding/components/index.js index 70e25e2..9119eba 100644 --- a/app/features/onboarding/components/index.js +++ b/app/features/onboarding/components/index.js @@ -1,10 +1,3 @@ -export { default as ConferenceURLSpotlight } from './ConferenceURLSpotlight'; -export { default as EmailSettingSpotlight } from './EmailSettingSpotlight'; -export { default as NameSettingSpotlight } from './NameSettingSpotlight'; +export { default as OnboardingSpotlight } from './OnboardingSpotlight'; export { default as Onboarding } from './Onboarding'; export { default as OnboardingModal } from './OnboardingModal'; -export { default as ServerSettingSpotlight } from './ServerSettingSpotlight'; -export { default as ServerTimeoutSpotlight } from './ServerTimeoutSpotlight'; -export { default as SettingsDrawerSpotlight } from './SettingsDrawerSpotlight'; -export { default as StartMutedTogglesSpotlight } from './StartMutedTogglesSpotlight'; -export { default as AlwaysOnTopWindowSpotlight } from './AlwaysOnTopWindowSpotlight'; diff --git a/app/features/onboarding/constants.js b/app/features/onboarding/constants.js index c6ac5ca..7b971f3 100644 --- a/app/features/onboarding/constants.js +++ b/app/features/onboarding/constants.js @@ -1,16 +1,7 @@ // @flow - -import { - OnboardingModal, - ConferenceURLSpotlight, - SettingsDrawerSpotlight, - NameSettingSpotlight, - EmailSettingSpotlight, - StartMutedTogglesSpotlight, - ServerSettingSpotlight, - ServerTimeoutSpotlight, - AlwaysOnTopWindowSpotlight -} from './components'; +import { OnboardingModal, OnboardingSpotlight } from './components'; +import { openDrawer, closeDrawer } from '../navbar'; +import { SettingsDrawer } from '../settings'; export const advenaceSettingsSteps = [ 'server-setting', @@ -33,13 +24,57 @@ export const onboardingSteps = { }; export const onboardingComponents = { - 'onboarding-modal': OnboardingModal, - 'conference-url': ConferenceURLSpotlight, - 'settings-drawer-button': SettingsDrawerSpotlight, - 'name-setting': NameSettingSpotlight, - 'email-setting': EmailSettingSpotlight, - 'start-muted-toggles': StartMutedTogglesSpotlight, - 'server-setting': ServerSettingSpotlight, - 'server-timeout': ServerTimeoutSpotlight, - 'always-on-top-window': AlwaysOnTopWindowSpotlight + 'onboarding-modal': { type: OnboardingModal }, + 'conference-url': { + type: OnboardingSpotlight, + dialogPlacement: 'bottom center', + target: 'conference-url', + text: 'onboarding.conferenceUrl' + }, + 'settings-drawer-button': { + type: OnboardingSpotlight, + dialogPlacement: 'top right', + target: 'settings-drawer-button', + text: 'onboarding.settingsDrawerButton', + onNext: (props: OnboardingSpotlight.props) => props.dispatch(openDrawer(SettingsDrawer)) + }, + 'name-setting': { + type: OnboardingSpotlight, + dialogPlacement: 'top right', + target: 'name-setting', + text: 'onboarding.nameSetting' + }, + 'email-setting': { + type: OnboardingSpotlight, + dialogPlacement: 'top right', + target: 'email-setting', + text: 'onboarding.emailSetting' + }, + 'start-muted-toggles': { + type: OnboardingSpotlight, + dialogPlacement: 'top right', + target: 'start-muted-toggles', + text: 'onboarding.startMutedToggles' + }, + 'server-setting': { + type: OnboardingSpotlight, + dialogPlacement: 'top right', + target: 'server-setting', + text: 'onboarding.serverSetting' + }, + 'server-timeout': { + type: OnboardingSpotlight, + dialogPlacement: 'top right', + target: 'server-timeout', + text: 'onboarding.serverTimeout' + }, + 'always-on-top-window': { + type: OnboardingSpotlight, + dialogPlacement: 'top right', + target: 'always-on-top-window', + text: 'onboarding.alwaysOnTop', + onNext: (props: OnboardingSpotlight.props) => setTimeout(() => { + props.dispatch(closeDrawer()); + }, 300) + } }; diff --git a/app/features/settings/components/AlwaysOnTopWindowToggle.js b/app/features/settings/components/AlwaysOnTopWindowToggle.js deleted file mode 100644 index dd77e60..0000000 --- a/app/features/settings/components/AlwaysOnTopWindowToggle.js +++ /dev/null @@ -1,84 +0,0 @@ -// @flow - -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import type { Dispatch } from 'redux'; - -import { setWindowAlwaysOnTop } from '../actions'; - -import ToggleWithLabel from './ToggleWithLabel'; - -type Props = { - - /** - * Redux dispatch. - */ - dispatch: Dispatch<*>; - - /** - * Window Always on Top value in (redux) state. - */ - _alwaysOnTopWindowEnabled: boolean; -}; - -/** - * Window always open on top placed in Settings Drawer. - */ -class AlwaysOnTopWindowToggle extends Component<Props> { - /** - * Initializes a new {@code AlwaysOnTopWindowToggle} instance. - * - * @inheritdoc - */ - constructor(props) { - super(props); - - this._onAlwaysOnTopWindowToggleChange - = this._onAlwaysOnTopWindowToggleChange.bind(this); - } - - /** - * Render function of component. - * - * @returns {ReactElement} - */ - render() { - return ( - <ToggleWithLabel - isDefaultChecked = { this.props._alwaysOnTopWindowEnabled } - label = 'Always on Top Window' - onChange = { this._onAlwaysOnTopWindowToggleChange } - value = { this.props._alwaysOnTopWindowEnabled } /> - ); - } - - _onAlwaysOnTopWindowToggleChange: (*) => void; - - /** - * Toggles alwaysOnTopWindowEnabled. - * - * @returns {void} - */ - _onAlwaysOnTopWindowToggleChange() { - const { _alwaysOnTopWindowEnabled } = this.props; - const newState = !_alwaysOnTopWindowEnabled; - - this.props.dispatch(setWindowAlwaysOnTop(newState)); - } -} - -/** - * Maps (parts of) the redux state to the React props. - * - * @param {Object} state - The redux state. - * @returns {{ - * _alwaysOnTopWindowEnabled: boolean, - * }} - */ -function _mapStateToProps(state: Object) { - return { - _alwaysOnTopWindowEnabled: state.settings.alwaysOnTopWindowEnabled - }; -} - -export default connect(_mapStateToProps)(AlwaysOnTopWindowToggle); diff --git a/app/features/settings/components/ServerTimeoutField.js b/app/features/settings/components/ServerTimeoutField.js index 742f49f..3e2d50a 100644 --- a/app/features/settings/components/ServerTimeoutField.js +++ b/app/features/settings/components/ServerTimeoutField.js @@ -3,7 +3,9 @@ import { FieldTextStateless } from '@atlaskit/field-text'; import React, { Component } from 'react'; +import { withTranslation } from 'react-i18next'; import { connect } from 'react-redux'; +import { compose } from 'redux'; import type { Dispatch } from 'redux'; import config from '../../config'; @@ -22,6 +24,11 @@ type Props = { * Default Jitsi Meet Server Timeout in (redux) store. */ _serverTimeout: number; + + /** + * I18next translation function. + */ + t: Function; }; type State = { @@ -64,6 +71,8 @@ class ServerTimeoutField extends Component<Props, State> { * @returns {ReactElement} */ render() { + const { t } = this.props; + return ( <Form onSubmit = { this._onServerTimeoutSubmit }> <FieldTextStateless @@ -71,7 +80,7 @@ class ServerTimeoutField extends Component<Props, State> { = { 'Invalid Timeout' } isInvalid = { !this.state.isValid } isValidationHidden = { this.state.isValid } - label = 'Server Timeout (in seconds)' + label = { t('settings.serverTimeout') } onBlur = { this._onServerTimeoutSubmit } onChange = { this._onServerTimeoutChange } placeholder = { config.defaultServerTimeout } @@ -138,4 +147,4 @@ function _mapStateToProps(state: Object) { }; } -export default connect(_mapStateToProps)(ServerTimeoutField); +export default compose(connect(_mapStateToProps), withTranslation())(ServerTimeoutField); diff --git a/app/features/settings/components/ServerURLField.js b/app/features/settings/components/ServerURLField.js index b85ba4a..09ae945 100644 --- a/app/features/settings/components/ServerURLField.js +++ b/app/features/settings/components/ServerURLField.js @@ -3,8 +3,10 @@ import { FieldTextStateless } from '@atlaskit/field-text'; import React, { Component } from 'react'; +import { withTranslation } from 'react-i18next'; import { connect } from 'react-redux'; import type { Dispatch } from 'redux'; +import { compose } from 'redux'; import config from '../../config'; import { getExternalApiURL } from '../../utils'; @@ -23,6 +25,11 @@ type Props = { * Default Jitsi Meet Server URL in (redux) store. */ _serverURL: string; + + /** + * I18next translation function. + */ + t: Function; }; type State = { @@ -65,14 +72,15 @@ class ServerURLField extends Component<Props, State> { * @returns {ReactElement} */ render() { + const { t } = this.props; + return ( <Form onSubmit = { this._onServerURLSubmit }> <FieldTextStateless - invalidMessage - = { 'Invalid Server URL or external API not enabled' } + invalidMessage = { t('settings.invalidServer') } isInvalid = { !this.state.isValid } isValidationHidden = { this.state.isValid } - label = 'Server URL' + label = { t('settings.serverUrl') } onBlur = { this._onServerURLSubmit } onChange = { this._onServerURLChange } placeholder = { config.defaultServerURL } @@ -150,4 +158,4 @@ function _mapStateToProps(state: Object) { }; } -export default connect(_mapStateToProps)(ServerURLField); +export default compose(connect(_mapStateToProps), withTranslation())(ServerURLField); diff --git a/app/features/settings/components/SettingToggle.js b/app/features/settings/components/SettingToggle.js new file mode 100644 index 0000000..ac02326 --- /dev/null +++ b/app/features/settings/components/SettingToggle.js @@ -0,0 +1,66 @@ +// @flow + +import React, { useCallback } from 'react'; +import { connect } from 'react-redux'; +import type { Dispatch } from 'redux'; + +import ToggleWithLabel from './ToggleWithLabel'; + +type Props = { + + /** + * Redux dispatch. + */ + dispatch: Dispatch<*>; + + /** + * The label for the toggle. + */ + label: String; + + /** + * The name of the setting. + */ + settingName: String; + + /** + * A function to produce setting change events. + */ + settingChangeEvent: Function; + +}; + +/** + * Maps (parts of) the redux state to the React props. + * + * @param {Object} state - The redux state. + * @param {Object} ownProps - The props of the redux wrapper component. + * @returns {Object} A props object including the current value of the setting. + */ +const mapStateToProps = (state, ownProps: Props) => { + return { + value: state.settings[ownProps.settingName], + ...ownProps + }; +}; + +/** + * A component to control a single boolean redux setting. + * + * @param {Object} props - The props provided by mapStateToProps. + * @returns {Object} A rendered toggle component with correct state. + */ +function SettingToggle(props: Object) { + const onChange = useCallback( + () => props.dispatch(props.settingChangeEvent(!props.value))); + + return ( + <ToggleWithLabel + isDefaultChecked = { props.value } + label = { props.label } + onChange = { onChange } + value = { props.value } /> + ); +} + +export default connect(mapStateToProps)(SettingToggle); diff --git a/app/features/settings/components/SettingsDrawer.js b/app/features/settings/components/SettingsDrawer.js index 970963d..985123d 100644 --- a/app/features/settings/components/SettingsDrawer.js +++ b/app/features/settings/components/SettingsDrawer.js @@ -7,18 +7,22 @@ import { SpotlightTarget } from '@atlaskit/onboarding'; import Panel from '@atlaskit/panel'; import React, { Component } from 'react'; +import { withTranslation } from 'react-i18next'; import { connect } from 'react-redux'; import type { Dispatch } from 'redux'; +import { compose } from 'redux'; import { closeDrawer, DrawerContainer, Logo } from '../../navbar'; import { Onboarding, advenaceSettingsSteps, startOnboarding } from '../../onboarding'; import { Form, SettingsContainer, TogglesContainer } from '../styled'; -import { setEmail, setName } from '../actions'; +import { + setEmail, setName, setWindowAlwaysOnTop, + setStartWithAudioMuted, setStartWithVideoMuted +} from '../actions'; -import AlwaysOnTopWindowToggle from './AlwaysOnTopWindowToggle'; +import SettingToggle from './SettingToggle'; import ServerURLField from './ServerURLField'; import ServerTimeoutField from './ServerTimeoutField'; -import StartMutedToggles from './StartMutedToggles'; type Props = { @@ -46,6 +50,11 @@ type Props = { * Name of the user. */ _name: string; + + /** + * I18next translation function. + */ + t: Function; }; /** @@ -92,9 +101,11 @@ class SettingsDrawer extends Component<Props, *> { * @returns {ReactElement} */ render() { + const { t } = this.props; + return ( <AkCustomDrawer - backIcon = { <ArrowLeft label = 'Back' /> } + backIcon = { <ArrowLeft label = { t('settings.back') } /> } isOpen = { this.props.isOpen } onBackButton = { this._onBackButton } primaryIcon = { <Logo /> } > @@ -104,7 +115,7 @@ class SettingsDrawer extends Component<Props, *> { name = 'name-setting'> <Form onSubmit = { this._onNameFormSubmit }> <FieldText - label = 'Name' + label = { t('settings.name') } onBlur = { this._onNameBlur } shouldFitContainer = { true } type = 'text' @@ -115,7 +126,7 @@ class SettingsDrawer extends Component<Props, *> { name = 'email-setting'> <Form onSubmit = { this._onEmailFormSubmit }> <FieldText - label = 'Email' + label = { t('settings.email') } onBlur = { this._onEmailBlur } shouldFitContainer = { true } type = 'text' @@ -125,11 +136,18 @@ class SettingsDrawer extends Component<Props, *> { <TogglesContainer> <SpotlightTarget name = 'start-muted-toggles'> - <StartMutedToggles /> + <SettingToggle + label = { t('settings.startWithAudioMuted') } + settingChangeEvent = { setStartWithAudioMuted } + settingName = 'startWithAudioMuted' /> + <SettingToggle + label = { t('settings.startWithVideoMuted') } + settingChangeEvent = { setStartWithVideoMuted } + settingName = 'startWithVideoMuted' /> </SpotlightTarget> </TogglesContainer> <Panel - header = 'Advanced Settings' + header = { t('settings.advancedSettings') } isDefaultExpanded = { this.props._isOnboardingAdvancedSettings }> <SpotlightTarget name = 'server-setting'> <ServerURLField /> @@ -140,7 +158,10 @@ class SettingsDrawer extends Component<Props, *> { <TogglesContainer> <SpotlightTarget name = 'always-on-top-window'> - <AlwaysOnTopWindowToggle /> + <SettingToggle + label = { t('settings.alwaysOnTopWindow') } + settingChangeEvent = { setWindowAlwaysOnTop } + settingName = 'alwaysOnTopWindowEnabled' /> </SpotlightTarget> </TogglesContainer> </Panel> @@ -236,4 +257,4 @@ function _mapStateToProps(state: Object) { }; } -export default connect(_mapStateToProps)(SettingsDrawer); +export default compose(connect(_mapStateToProps), withTranslation())(SettingsDrawer); diff --git a/app/features/settings/components/StartMutedToggles.js b/app/features/settings/components/StartMutedToggles.js deleted file mode 100644 index 3cee823..0000000 --- a/app/features/settings/components/StartMutedToggles.js +++ /dev/null @@ -1,145 +0,0 @@ -// @flow - -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import type { Dispatch } from 'redux'; - -import { - setStartWithAudioMuted, - setStartWithVideoMuted -} from '../actions'; - -import ToggleWithLabel from './ToggleWithLabel'; - -type Props = { - - /** - * Redux dispatch. - */ - dispatch: Dispatch<*>; - - /** - * Start with Audio Muted value in (redux) state. - */ - _startWithAudioMuted: boolean; - - /** - * Start with Video Muted value in (redux) state. - */ - _startWithVideoMuted: boolean; -}; - -type State = { - - /** - * Start with Audio Muted value in (local) state. - */ - startWithAudioMuted: boolean; - - /** - * Start with Video Muted value in (local) state. - */ - startWithVideoMuted: boolean; -}; - -/** - * Start Muted toggles for audio and video placed in Settings Drawer. - */ -class StartMutedToggles extends Component<Props, State> { - /** - * Initializes a new {@code StartMutedToggles} instance. - * - * @inheritdoc - */ - constructor(props) { - super(props); - - this.state = { - startWithAudioMuted: false, - startWithVideoMuted: false - }; - - this._onAudioToggleChange = this._onAudioToggleChange.bind(this); - this._onVideoToggleChange = this._onVideoToggleChange.bind(this); - } - - /** - * This updates the startWithAudioMuted and startWithVideoMuted in (local) - * state when there is a change in redux store. - * - * @param {Props} props - New props of the component. - * @returns {State} - New state of the component. - */ - static getDerivedStateFromProps(props) { - return { - startWithAudioMuted: props._startWithAudioMuted, - startWithVideoMuted: props._startWithVideoMuted - }; - } - - /** - * Render function of component. - * - * @returns {ReactElement} - */ - render() { - return ( - <> - <ToggleWithLabel - isDefaultChecked = { this.props._startWithAudioMuted } - label = 'Start with Audio muted' - onChange = { this._onAudioToggleChange } - value = { this.state.startWithAudioMuted } /> - <ToggleWithLabel - isDefaultChecked = { this.props._startWithVideoMuted } - label = 'Start with Video muted' - onChange = { this._onVideoToggleChange } - value = { this.state.startWithVideoMuted } /> - </> - ); - } - - _onAudioToggleChange: (*) => void; - - /** - * Toggles startWithAudioMuted. - * - * @returns {void} - */ - _onAudioToggleChange() { - const { startWithAudioMuted } = this.state; - - this.props.dispatch(setStartWithAudioMuted(!startWithAudioMuted)); - } - - _onVideoToggleChange: (*) => void; - - /** - * Toggles startWithVideoMuted. - * - * @returns {void} - */ - _onVideoToggleChange() { - const { startWithVideoMuted } = this.state; - - this.props.dispatch(setStartWithVideoMuted(!startWithVideoMuted)); - } -} - -/** - * Maps (parts of) the redux state to the React props. - * - * @param {Object} state - The redux state. - * @returns {{ - * _startWithAudioMuted: boolean, - * _startWithVideoMuted: boolean - * }} - */ -function _mapStateToProps(state: Object) { - return { - _startWithAudioMuted: state.settings.startWithAudioMuted, - _startWithVideoMuted: state.settings.startWithVideoMuted - }; -} - -export default connect(_mapStateToProps)(StartMutedToggles); diff --git a/app/features/welcome/components/Welcome.js b/app/features/welcome/components/Welcome.js index f0bb138..7a7b7a4 100644 --- a/app/features/welcome/components/Welcome.js +++ b/app/features/welcome/components/Welcome.js @@ -8,6 +8,8 @@ import { AtlasKitThemeProvider } from '@atlaskit/theme'; import { generateRoomWithoutSeparator } from 'js-utils/random'; import React, { Component } from 'react'; +import { withTranslation } from 'react-i18next'; +import { compose } from 'redux'; import type { Dispatch } from 'redux'; import { connect } from 'react-redux'; import { push } from 'react-router-redux'; @@ -19,7 +21,6 @@ import { createConferenceObjectFromURL } from '../../utils'; import { Body, FieldWrapper, Form, Header, Label, Wrapper } from '../styled'; - type Props = { /** @@ -31,6 +32,11 @@ type Props = { * React Router location object. */ location: Object; + + /** + * I18next translate function. + */ + t: Function; }; type State = { @@ -252,12 +258,13 @@ class Welcome extends Component<Props, State> { _renderHeader() { const locationState = this.props.location.state; const locationError = locationState && locationState.error; + const { t } = this.props; return ( <Header> <SpotlightTarget name = 'conference-url'> <Form onSubmit = { this._onFormSubmit }> - <Label>{ 'Enter a name for your conference or a Jitsi URL' } </Label> + <Label>{ t('enterConferenceNameOrUrl') } </Label> <FieldWrapper> <FieldTextStateless autoFocus = { true } @@ -272,7 +279,7 @@ class Welcome extends Component<Props, State> { appearance = 'primary' onClick = { this._onJoin } type = 'button'> - GO + { t('go') } </Button> </FieldWrapper> </Form> @@ -306,4 +313,4 @@ class Welcome extends Component<Props, State> { } } -export default connect()(Welcome); +export default compose(connect(), withTranslation())(Welcome); diff --git a/app/i18n/index.js b/app/i18n/index.js new file mode 100644 index 0000000..4f08390 --- /dev/null +++ b/app/i18n/index.js @@ -0,0 +1,24 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import moment from 'moment'; + +const languages = { + en: { translation: require('./lang/en.json') } +}; + +const detectedLocale = window.jitsiNodeAPI.getLocale(); + +i18n + .use(initReactI18next) + .init({ + resources: languages, + lng: detectedLocale, + fallbackLng: 'en', + interpolation: { + escapeValue: false // not needed for react as it escapes by default + } + }); + +moment.locale(detectedLocale); + +export default i18n; diff --git a/app/i18n/lang/en.json b/app/i18n/lang/en.json new file mode 100644 index 0000000..08a3632 --- /dev/null +++ b/app/i18n/lang/en.json @@ -0,0 +1,38 @@ +{ + "enterConferenceNameOrUrl": "Enter a name for your conference or a Jitsi URL", + "go": "GO", + "help": "Help", + "termsLink": "Terms", + "privacyLink": "Privacy", + "sendFeedbackLink": "Send Feedback", + "aboutLink": "About", + "sourceLink": "Source Code", + "versionLabel": "Version: {{version}}", + "onboarding": { + "startTour": "Start Tour", + "skip": "Skip", + "welcome": "Welcome to {{appName}}", + "letUsShowYouAround": "Let us show you around!", + "next": "Next", + "conferenceUrl": "Enter the name (or full URL) of the room you want to join. You may make a name up, just let others know so they enter the same name.", + "settingsDrawerButton": "Click here to open the settings drawer.", + "nameSetting": "This will be your display name, others will see you with this name.", + "emailSetting": "The email you enter here will be part of your user profile.", + "startMutedToggles": "You can toggle if you want to start with your audio or video muted here. This will be applied to all conferences.", + "serverSetting": "This will be the server where your conferences will take place. You can use your own, but you don't need to!", + "serverTimeout": "Timeout to join a meeting, if the meeting hasn't been joined before the timeout hits, it's cancelled.", + "alwaysOnTop": "You can toggle whether you want to enable the \"always-on-top\" window, which is displayed when the main window loses focus. This will be applied to all conferences." + }, + "settings": { + "back": "Back", + "name": "Name", + "email": "Email", + "advancedSettings": "Advanced Settings", + "alwaysOnTopWindow": "Always on Top Window", + "startWithAudioMuted": "Start with Audio muted", + "startWithVideoMuted": "Start with Video muted", + "invalidServer": "Invalid Server URL or external API not enabled", + "serverUrl": "Server URL", + "serverTimeout": "Server Timeout (in seconds)" + } +} diff --git a/app/index.html b/app/index.html index 405d3ac..68706f4 100644 --- a/app/index.html +++ b/app/index.html @@ -1,6 +1,7 @@ <!DOCTYPE html> <html> <head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <style> body, html { overflow: hidden; diff --git a/app/index.js b/app/index.js index 3a22f18..0fe93cb 100644 --- a/app/index.js +++ b/app/index.js @@ -5,9 +5,10 @@ */ import '@atlaskit/css-reset'; +import Spinner from '@atlaskit/spinner'; import { SpotlightManager } from '@atlaskit/onboarding'; -import React, { Component } from 'react'; +import React, { Component, Suspense } from 'react'; import { render } from 'react-dom'; import { Provider } from 'react-redux'; import { PersistGate } from 'redux-persist/integration/react'; @@ -15,6 +16,8 @@ import { PersistGate } from 'redux-persist/integration/react'; import { App } from './features/app'; import { persistor, store } from './features/redux'; +import './i18n'; + /** * Component encapsulating App component with redux store using provider. */ @@ -31,7 +34,9 @@ class Root extends Component<*> { loading = { null } persistor = { persistor }> <SpotlightManager> - <App /> + <Suspense fallback = { <Spinner /> } > + <App /> + </Suspense> </SpotlightManager> </PersistGate> </Provider> diff --git a/app/preload/preload.js b/app/preload/preload.js index 85064ea..538b82f 100644 --- a/app/preload/preload.js +++ b/app/preload/preload.js @@ -1,5 +1,5 @@ const createElectronStorage = require('redux-persist-electron-storage'); -const { ipcRenderer, shell } = require('electron'); +const { ipcRenderer, shell, remote } = require('electron'); const os = require('os'); const url = require('url'); @@ -35,6 +35,7 @@ window.jitsiNodeAPI = { openExternalLink, jitsiMeetElectronUtils, shellOpenExternal: shell.openExternal, + getLocale: remote.app.getLocale, ipc: { on: (channel, listener) => { if (!whitelistedIpcChannels.includes(channel)) { |