Introduce internationalisation

This commit is contained in:
freddytuxworth 2020-06-26 12:05:42 +01:00 committed by GitHub
parent 1c4f76e3b8
commit 2efa8b057e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 402 additions and 867 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

24
app/i18n/index.js Normal file
View File

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

38
app/i18n/lang/en.json Normal file
View File

@ -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)"
}
}

View File

@ -1,6 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<style>
body, html {
overflow: hidden;

View File

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

View File

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

52
package-lock.json generated
View File

@ -9624,6 +9624,14 @@
"terser": "^4.6.3"
}
},
"html-parse-stringify2": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/html-parse-stringify2/-/html-parse-stringify2-2.0.1.tgz",
"integrity": "sha1-3FZwtyksoVi3vJFsmmc1rIhyg0o=",
"requires": {
"void-elements": "^2.0.1"
}
},
"html-webpack-plugin": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-4.0.4.tgz",
@ -9692,6 +9700,14 @@
"integrity": "sha1-pls0RZrWNnrbs3B6gqPJ+RYWcDA=",
"dev": true
},
"i18next": {
"version": "19.4.5",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-19.4.5.tgz",
"integrity": "sha512-aLvSsURoupi3x9IndmV6+m3IGhzLzhYv7Gw+//K3ovdliyGcFRV0I1MuddI0Bk/zR7BG1U+kJOjeHFUcUIdEgg==",
"requires": {
"@babel/runtime": "^7.3.1"
}
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@ -12170,14 +12186,14 @@
}
},
"react": {
"version": "16.6.3",
"resolved": "https://registry.npmjs.org/react/-/react-16.6.3.tgz",
"integrity": "sha512-zCvmH2vbEolgKxtqXL2wmGCUxUyNheYn/C+PD1YAjfxHC54+MhdruyhO7QieQrYsYeTxrn93PM2y0jRH1zEExw==",
"version": "16.8.0",
"resolved": "https://registry.npmjs.org/react/-/react-16.8.0.tgz",
"integrity": "sha512-g+nikW2D48kqgWSPwNo0NH9tIGG3DsQFlrtrQ1kj6W77z5ahyIHG0w8kPpz4Sdj6gyLnz0lEd/xsjOoGge2MYQ==",
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2",
"scheduler": "^0.11.2"
"scheduler": "^0.13.0"
}
},
"react-addons-text-content": {
@ -12203,14 +12219,14 @@
}
},
"react-dom": {
"version": "16.6.3",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.6.3.tgz",
"integrity": "sha512-8ugJWRCWLGXy+7PmNh8WJz3g1TaTUt1XyoIcFN+x0Zbkoz+KKdUyx1AQLYJdbFXjuF41Nmjn5+j//rxvhFjgSQ==",
"version": "16.8.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.8.0.tgz",
"integrity": "sha512-dBzoAGYZpW9Yggp+CzBPC7q1HmWSeRc93DWrwbskmG1eHJWznZB/p0l/Sm+69leIGUS91AXPB/qB3WcPnKx8Sw==",
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2",
"scheduler": "^0.11.2"
"scheduler": "^0.13.0"
}
},
"react-focus-lock": {
@ -12233,6 +12249,15 @@
"prop-types": "^15.6.1"
}
},
"react-i18next": {
"version": "11.5.1",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.5.1.tgz",
"integrity": "sha512-2VSx+dClvmCJTgrw9Nrof4EkggMlzo4s/YZPEUAdI7gU2HTksapkGtFPARzpeTk+3k1zY+Xl6pLHc0LYvsRWAQ==",
"requires": {
"@babel/runtime": "^7.3.1",
"html-parse-stringify2": "2.0.1"
}
},
"react-is": {
"version": "16.10.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.10.2.tgz",
@ -12863,9 +12888,9 @@
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"scheduler": {
"version": "0.11.3",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.11.3.tgz",
"integrity": "sha512-i9X9VRRVZDd3xZw10NY5Z2cVMbdYg6gqFecfj79USv1CFN+YrJ3gIPRKf1qlY+Sxly4djoKdfx1T+m9dnRB8kQ==",
"version": "0.13.6",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.6.tgz",
"integrity": "sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==",
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1"
@ -14382,6 +14407,11 @@
"integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==",
"dev": true
},
"void-elements": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz",
"integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w="
},
"warning": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz",

View File

@ -116,12 +116,14 @@
"electron-updater": "4.2.5",
"electron-window-state": "5.0.3",
"history": "4.10.1",
"i18next": "19.4.5",
"jitsi-meet-electron-utils": "github:jitsi/jitsi-meet-electron-utils#v2.0.7",
"js-utils": "github:jitsi/js-utils#cf11996bd866fdb47326c59a5d3bc24be17282d4",
"moment": "2.23.0",
"mousetrap": "1.6.2",
"react": "16.6.3",
"react-dom": "16.6.3",
"react": "16.8.0",
"react-dom": "16.8.0",
"react-i18next": "11.5.1",
"react-redux": "5.1.1",
"react-router-redux": "5.0.0-alpha.9",
"redux": "4.0.1",