aboutsummaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorfreddytuxworth <freddytuxworth@gmail.com>2020-06-26 12:05:42 +0100
committerGitHub <noreply@github.com>2020-06-26 13:05:42 +0200
commit2efa8b057e050e32eb2a45750ba5e6e7bd251fe7 (patch)
tree8b2cf5832aa9645bf49c9ed7219713c7f56e40df /app
parent1c4f76e3b84ae33823474f8c7970ff6f839f0b66 (diff)
Introduce internationalisation
Diffstat (limited to 'app')
-rw-r--r--app/features/navbar/components/HelpButton.js21
-rw-r--r--app/features/onboarding/components/AlwaysOnTopWindowSpotlight.js78
-rw-r--r--app/features/onboarding/components/ConferenceURLSpotlight.js70
-rw-r--r--app/features/onboarding/components/EmailSettingSpotlight.js68
-rw-r--r--app/features/onboarding/components/NameSettingSpotlight.js69
-rw-r--r--app/features/onboarding/components/Onboarding.js4
-rw-r--r--app/features/onboarding/components/OnboardingModal.js19
-rw-r--r--app/features/onboarding/components/OnboardingSpotlight.js68
-rw-r--r--app/features/onboarding/components/ServerSettingSpotlight.js69
-rw-r--r--app/features/onboarding/components/ServerTimeoutSpotlight.js67
-rw-r--r--app/features/onboarding/components/SettingsDrawerSpotlight.js68
-rw-r--r--app/features/onboarding/components/StartMutedTogglesSpotlight.js69
-rw-r--r--app/features/onboarding/components/index.js9
-rw-r--r--app/features/onboarding/constants.js77
-rw-r--r--app/features/settings/components/AlwaysOnTopWindowToggle.js84
-rw-r--r--app/features/settings/components/ServerTimeoutField.js13
-rw-r--r--app/features/settings/components/ServerURLField.js16
-rw-r--r--app/features/settings/components/SettingToggle.js66
-rw-r--r--app/features/settings/components/SettingsDrawer.js41
-rw-r--r--app/features/settings/components/StartMutedToggles.js145
-rw-r--r--app/features/welcome/components/Welcome.js15
-rw-r--r--app/i18n/index.js24
-rw-r--r--app/i18n/lang/en.json38
-rw-r--r--app/index.html1
-rw-r--r--app/index.js9
-rw-r--r--app/preload/preload.js3
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)) {