Added internationalization support, and translated to nb-NO.

master
Håvar Aambø Fosstveit 2019-11-06 23:11:42 +01:00
parent 23e02d4865
commit c12057e53c
23 changed files with 1214 additions and 375 deletions

View File

@ -20,6 +20,7 @@
"react": "^16.10.2",
"react-cookie-consent": "^2.5.0",
"react-dom": "^16.10.2",
"react-intl": "^3.4.0",
"react-redux": "^7.1.1",
"react-scripts": "3.2.0",
"redux": "^4.0.4",

View File

@ -81,15 +81,19 @@ const VIDEO_ENCODINGS =
let store;
let intl;
export default class RoomClient
{
/**
* @param {Object} data
* @param {Object} data.store - The Redux store.
* @param {Object} data.intl - react-intl object
*/
static init(data)
{
store = data.store;
intl = data.intl;
}
constructor(
@ -230,7 +234,10 @@ export default class RoomClient
store.dispatch(settingsActions.toggleAdvancedMode());
store.dispatch(requestActions.notify(
{
text : 'Toggled advanced mode.'
text : intl.formatMessage({
id : 'room.toggleAdvancedMode',
defaultMessage : 'Toggled advanced mode'
})
}));
break;
}
@ -240,7 +247,10 @@ export default class RoomClient
store.dispatch(roomActions.setDisplayMode('democratic'));
store.dispatch(requestActions.notify(
{
text : 'Changed layout to democratic view.'
text : intl.formatMessage({
id : 'room.setDemocraticView',
defaultMessage : 'Changed layout to democratic view'
})
}));
break;
}
@ -250,7 +260,10 @@ export default class RoomClient
store.dispatch(roomActions.setDisplayMode('filmstrip'));
store.dispatch(requestActions.notify(
{
text : 'Changed layout to filmstrip view.'
text : intl.formatMessage({
id : 'room.setFilmStripView',
defaultMessage : 'Changed layout to filmstrip view'
})
}));
break;
}
@ -265,7 +278,10 @@ export default class RoomClient
store.dispatch(requestActions.notify(
{
text : 'Muted your microphone.'
text : intl.formatMessage({
id : 'devices.microPhoneMute',
defaultMessage : 'Muted your microphone'
})
}));
}
else
@ -274,7 +290,10 @@ export default class RoomClient
store.dispatch(requestActions.notify(
{
text : 'Unmuted your microphone.'
text : intl.formatMessage({
id : 'devices.microPhoneUnMute',
defaultMessage : 'Unmuted your microphone'
})
}));
}
}
@ -284,7 +303,10 @@ export default class RoomClient
store.dispatch(requestActions.notify(
{
text : 'Enabled your microphone.'
text : intl.formatMessage({
id : 'devices.microphoneEnable',
defaultMessage : 'Enabled your microphone'
})
}));
}
@ -311,7 +333,10 @@ export default class RoomClient
store.dispatch(requestActions.notify(
{
text : 'Your devices changed, configure your devices in the settings dialog.'
text : intl.formatMessage({
id : 'devices.devicesChanged',
defaultMessage : 'Your devices changed, configure your devices in the settings dialog'
})
}));
});
}
@ -349,7 +374,10 @@ export default class RoomClient
store.dispatch(requestActions.notify(
{
text : 'You are logged in.'
text : intl.formatMessage({
id : 'room.loggedIn',
defaultMessage : 'You are logged in'
})
}));
}
@ -361,7 +389,10 @@ export default class RoomClient
store.dispatch(requestActions.notify(
{
text : 'You are logged out.'
text : intl.formatMessage({
id : 'room.loggedOut',
defaultMessage : 'You are logged out'
})
}));
}
@ -458,7 +489,13 @@ export default class RoomClient
store.dispatch(requestActions.notify(
{
text : `Your display name changed to ${displayName}.`
text : intl.formatMessage({
id : 'room.changedDisplayName',
defaultMessage : 'Your display name changed to {displayName}',
values : {
displayName
}
})
}));
}
catch (error)
@ -468,7 +505,10 @@ export default class RoomClient
store.dispatch(requestActions.notify(
{
type : 'error',
text : 'An error occured while changing your display name.'
text : intl.formatMessage({
id : 'room.changeDisplayNameError',
defaultMessage : 'An error occured while changing your display name'
})
}));
}
@ -508,7 +548,10 @@ export default class RoomClient
store.dispatch(requestActions.notify(
{
type : 'error',
text : 'Unable to send chat message.'
text : intl.formatMessage({
id : 'room.chatError',
defaultMessage : 'Unable to send chat message'
})
}));
}
}
@ -522,7 +565,10 @@ export default class RoomClient
return store.dispatch(requestActions.notify(
{
type : 'error',
text : 'Unable to save file.'
text : intl.formatMessage({
id : 'filesharing.saveFileError',
defaultMessage : 'Unable to save file'
})
}));
}
@ -589,7 +635,10 @@ export default class RoomClient
{
store.dispatch(requestActions.notify(
{
text : 'Starting file share.'
text : intl.formatMessage({
id : 'filesharing.startingFileShare',
defaultMessage : 'Attempting to share file'
})
}));
this._webTorrent.seed(files, (torrent) =>
@ -603,7 +652,10 @@ export default class RoomClient
store.dispatch(requestActions.notify(
{
text : 'File successfully shared.'
text : intl.formatMessage({
id : 'filesharing.successfulFileShare',
defaultMessage : 'File successfully shared'
})
}));
store.dispatch(fileActions.addFile(
@ -631,7 +683,10 @@ export default class RoomClient
store.dispatch(requestActions.notify(
{
type : 'error',
text : 'Unable to share file.'
text : intl.formatMessage({
id : 'filesharing.unableToShare',
defaultMessage : 'Unable to share file'
})
}));
}
}
@ -689,12 +744,6 @@ export default class RoomClient
catch (error)
{
logger.error('getServerHistory() | failed: %o', error);
store.dispatch(requestActions.notify(
{
type : 'error',
text : 'Unable to retrieve room history.'
}));
}
}
@ -719,7 +768,10 @@ export default class RoomClient
store.dispatch(requestActions.notify(
{
type : 'error',
text : 'Unable to access your microphone.'
text : intl.formatMessage({
id : 'devices.microphoneMuteError',
defaultMessage : 'Unable to mute your microphone'
})
}));
}
}
@ -751,7 +803,10 @@ export default class RoomClient
store.dispatch(requestActions.notify(
{
type : 'error',
text : 'An error occured while accessing your microphone.'
text : intl.formatMessage({
id : 'devices.microphoneUnMuteError',
defaultMessage : 'Unable to unmute your microphone'
})
}));
}
}
@ -1133,12 +1188,6 @@ export default class RoomClient
{
logger.error('sendRaiseHandState() | failed: %o', error);
store.dispatch(requestActions.notify(
{
type : 'error',
text : `An error occured while ${state ? 'raising' : 'lowering'} hand.`
}));
// We need to refresh the component for it to render changed state
store.dispatch(meActions.setMyRaiseHandState(!state));
}
@ -1213,7 +1262,7 @@ export default class RoomClient
store.dispatch(requestActions.notify(
{
type : 'error',
text : `An error occured with file sharing`
text : intl.formatMessage({ id: 'filesharing.error', defaultMessage: 'There was a filesharing error' })
}));
});
@ -1241,7 +1290,10 @@ export default class RoomClient
{
store.dispatch(requestActions.notify(
{
text : 'You are disconnected.'
text : intl.formatMessage({
id : 'socket.disconnected',
defaultMessage : 'You are disconnected'
})
}));
this.close();
@ -1249,7 +1301,10 @@ export default class RoomClient
store.dispatch(requestActions.notify(
{
text : 'You are disconnected, attempting to reconnect.'
text : intl.formatMessage({
id : 'socket.reconnecting',
defaultMessage : 'You are disconnected, attempting to reconnect'
})
}));
store.dispatch(roomActions.setRoomState('connecting'));
@ -1261,7 +1316,10 @@ export default class RoomClient
store.dispatch(requestActions.notify(
{
text : 'You are disconnected.'
text : intl.formatMessage({
id : 'socket.disconnected',
defaultMessage : 'You are disconnected'
})
}));
this.close();
@ -1273,7 +1331,10 @@ export default class RoomClient
store.dispatch(requestActions.notify(
{
text : 'You are reconnected.'
text : intl.formatMessage({
id : 'socket.reconnected',
defaultMessage : 'You are reconnected'
})
}));
store.dispatch(roomActions.setRoomState('connected'));
@ -1450,7 +1511,10 @@ export default class RoomClient
store.dispatch(requestActions.notify(
{
text : 'Room is now locked.'
text : intl.formatMessage({
id : 'room.locked',
defaultMessage : 'Room is now locked'
})
}));
break;
@ -1463,7 +1527,10 @@ export default class RoomClient
store.dispatch(requestActions.notify(
{
text : 'Room is now unlocked.'
text : intl.formatMessage({
id : 'room.unlocked',
defaultMessage : 'Room is now unlocked'
})
}));
break;
@ -1480,7 +1547,10 @@ export default class RoomClient
store.dispatch(requestActions.notify(
{
text : 'New participant entered the lobby.'
text : intl.formatMessage({
id : 'room.newLobbyPeer',
defaultMessage : 'New participant entered the lobby'
})
}));
break;
@ -1495,7 +1565,10 @@ export default class RoomClient
store.dispatch(requestActions.notify(
{
text : 'Participant in lobby left.'
text : intl.formatMessage({
id : 'room.lobbyPeerLeft',
defaultMessage : 'Participant in lobby left'
})
}));
break;
@ -1520,7 +1593,13 @@ export default class RoomClient
store.dispatch(requestActions.notify(
{
text : `Participant in lobby changed name to ${displayName}.`
text : intl.formatMessage({
id : 'room.lobbyPeerChangedDisplayName',
defaultMessage : 'Participant in lobby changed name to {displayName}',
values : {
displayName
}
})
}));
break;
@ -1535,7 +1614,10 @@ export default class RoomClient
store.dispatch(requestActions.notify(
{
text : 'Participant in lobby changed picture.'
text : intl.formatMessage({
id : 'room.lobbyPeerChangedPicture',
defaultMessage : 'Participant in lobby changed picture'
})
}));
break;
@ -1550,7 +1632,10 @@ export default class RoomClient
store.dispatch(requestActions.notify(
{
text : 'Access code for room updated'
text : intl.formatMessage({
id : 'room.setAccessCode',
defaultMessage : 'Access code for room updated'
})
}));
break;
@ -1567,14 +1652,20 @@ export default class RoomClient
{
store.dispatch(requestActions.notify(
{
text : 'Access code for room is now activated'
text : intl.formatMessage({
id : 'room.accessCodeOn',
defaultMessage : 'Access code for room is now activated'
})
}));
}
else
{
store.dispatch(requestActions.notify(
{
text : 'Access code for room is now deactivated'
text : intl.formatMessage({
id : 'room.accessCodeOff',
defaultMessage : 'Access code for room is now deactivated'
})
}));
}
@ -1603,7 +1694,14 @@ export default class RoomClient
store.dispatch(requestActions.notify(
{
text : `${oldDisplayName} is now ${displayName}`
text : intl.formatMessage({
id : 'room.peerChangedDisplayName',
defaultMessage : '{oldDisplayName} is now {displayName}',
values : {
oldDisplayName,
displayName
}
})
}));
break;
@ -1647,7 +1745,10 @@ export default class RoomClient
store.dispatch(requestActions.notify(
{
text : 'New file available.'
text : intl.formatMessage({
id : 'room.newFile',
defaultMessage : 'New file available'
})
}));
if (
@ -1683,7 +1784,13 @@ export default class RoomClient
store.dispatch(requestActions.notify(
{
text : `${displayName} joined the room.`
text : intl.formatMessage({
id : 'room.newPeer',
defaultMessage : '{displayName} joined the room',
values : {
displayName
}
})
}));
break;
@ -1788,7 +1895,10 @@ export default class RoomClient
store.dispatch(requestActions.notify(
{
type : 'error',
text : 'Error on server request.'
text : intl.formatMessage({
id : 'socket.requestError',
defaultMessage : 'Error on server request'
})
}));
}
@ -1957,7 +2067,10 @@ export default class RoomClient
store.dispatch(requestActions.notify(
{
text : 'You have joined the room.'
text : intl.formatMessage({
id : 'room.joined',
defaultMessage : 'You have joined the room'
})
}));
this._spotlights.start();
@ -1969,7 +2082,10 @@ export default class RoomClient
store.dispatch(requestActions.notify(
{
type : 'error',
text : 'Unable to join the room.'
text : intl.formatMessage({
id : 'room.cantJoin',
defaultMessage : 'Unable to join the room'
})
}));
this.close();
@ -1989,7 +2105,10 @@ export default class RoomClient
store.dispatch(requestActions.notify(
{
text : 'You locked the room.'
text : intl.formatMessage({
id : 'room.youLocked',
defaultMessage : 'You locked the room'
})
}));
}
catch (error)
@ -1997,7 +2116,10 @@ export default class RoomClient
store.dispatch(requestActions.notify(
{
type : 'error',
text : 'Unable to lock the room.'
text : intl.formatMessage({
id : 'room.cantLock',
defaultMessage : 'Unable to lock the room'
})
}));
logger.error('lockRoom() | failed: %o', error);
@ -2017,7 +2139,10 @@ export default class RoomClient
store.dispatch(requestActions.notify(
{
text : 'You unlocked the room.'
text : intl.formatMessage({
id : 'room.youUnLocked',
defaultMessage : 'You unlocked the room'
})
}));
}
catch (error)
@ -2025,7 +2150,10 @@ export default class RoomClient
store.dispatch(requestActions.notify(
{
type : 'error',
text : 'Unable to unlock the room.'
text : intl.formatMessage({
id : 'room.cantUnLock',
defaultMessage : 'Unable to unlock the room'
})
}));
logger.error('unlockRoom() | failed: %o', error);
@ -2164,7 +2292,10 @@ export default class RoomClient
store.dispatch(requestActions.notify(
{
type : 'error',
text : 'Microphone disconnected!'
text : intl.formatMessage({
id : 'devices.microphoneDisconnected',
defaultMessage : 'Microphone disconnected'
})
}));
this.disableMic()
@ -2215,7 +2346,10 @@ export default class RoomClient
store.dispatch(requestActions.notify(
{
type : 'error',
text : 'An error occured while accessing your microphone.'
text : intl.formatMessage({
id : 'devices.microphoneError',
defaultMessage : 'An error occured while accessing your microphone'
})
}));
if (track)
@ -2248,11 +2382,7 @@ export default class RoomClient
}
catch (error)
{
store.dispatch(requestActions.notify(
{
type : 'error',
text : `Error closing server-side mic Producer: ${error}`
}));
logger.error('disableMic() [error:"%o"]', error);
}
this._micProducer = null;
@ -2341,7 +2471,10 @@ export default class RoomClient
store.dispatch(requestActions.notify(
{
type : 'error',
text : 'Screen sharing disconnected!'
text : intl.formatMessage({
id : 'devices.screenSharingDisconnected',
defaultMessage : 'Screen sharing disconnected'
})
}));
this.disableScreenSharing()
@ -2357,7 +2490,10 @@ export default class RoomClient
store.dispatch(requestActions.notify(
{
type : 'error',
text : 'An error occured while accessing your camera.'
text : intl.formatMessage({
id : 'devices.screenSharingError',
defaultMessage : 'An error occured while accessing your screen'
})
}));
if (track)
@ -2388,11 +2524,7 @@ export default class RoomClient
}
catch (error)
{
store.dispatch(requestActions.notify(
{
type : 'error',
text : `Error closing server-side screen Producer: ${error}`
}));
logger.error('disableScreenSharing() [error:"%o"]', error);
}
this._screenSharingProducer = null;
@ -2497,7 +2629,10 @@ export default class RoomClient
store.dispatch(requestActions.notify(
{
type : 'error',
text : 'Webcam disconnected!'
text : intl.formatMessage({
id : 'devices.cameraDisconnected',
defaultMessage : 'Camera disconnected'
})
}));
this.disableWebcam()
@ -2513,7 +2648,10 @@ export default class RoomClient
store.dispatch(requestActions.notify(
{
type : 'error',
text : 'An error occured while accessing your camera.'
text : intl.formatMessage({
id : 'devices.cameraError',
defaultMessage : 'An error occured while accessing your camera'
})
}));
if (track)
@ -2545,11 +2683,7 @@ export default class RoomClient
}
catch (error)
{
store.dispatch(requestActions.notify(
{
type : 'error',
text : `Error closing server-side webcam Producer: ${error}`
}));
logger.error('disableWebcam() [error:"%o"]', error);
}
this._webcamProducer = null;

View File

@ -4,6 +4,7 @@ import { withStyles } from '@material-ui/core/styles';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { withRoomContext } from '../../../RoomContext';
import { useIntl } from 'react-intl';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
import ListItemIcon from '@material-ui/core/ListItemIcon';
@ -85,6 +86,8 @@ const ListLobbyPeer = (props) =>
classes
} = props;
const intl = useIntl();
const picture = peer.picture || EmptyAvatar;
return (
@ -100,7 +103,12 @@ const ListLobbyPeer = (props) =>
<ListItemText
primary={peer.displayName}
/>
<Tooltip title='Click to let them in'>
<Tooltip
title={intl.formatMessage({
id : 'tooltip.admitFromLobby',
defaultMessage : 'Click to let them in'
})}
>
<ListItemIcon
className={classnames(classes.button, 'promote', {
disabled : peer.promotionInProgress

View File

@ -8,6 +8,7 @@ import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../../../RoomContext';
import * as roomActions from '../../../actions/roomActions';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import Dialog from '@material-ui/core/Dialog';
import DialogTitle from '@material-ui/core/DialogTitle';
import DialogActions from '@material-ui/core/DialogActions';
@ -75,7 +76,12 @@ const LockDialog = ({
paper : classes.dialogPaper
}}
>
<DialogTitle id='form-dialog-title'>Lobby administration</DialogTitle>
<DialogTitle id='form-dialog-title'>
<FormattedMessage
id='room.lobbyAdministration'
defaultMessage='Lobby administration'
/>
</DialogTitle>
{/*
<FormControl component='fieldset' className={classes.formControl}>
<FormLabel component='legend'>Room lock</FormLabel>
@ -129,7 +135,10 @@ const LockDialog = ({
dense
subheader={
<ListSubheader component='div'>
Participants in Lobby
<FormattedMessage
id='room.peersInLobby'
defaultMessage='Participants in Lobby'
/>
</ListSubheader>
}
>
@ -143,13 +152,19 @@ const LockDialog = ({
:
<DialogContent>
<DialogContentText gutterBottom>
There are currently no one in the lobby.
<FormattedMessage
id='room.lobbyEmpty'
defaultMessage='There are currently no one in the lobby'
/>
</DialogContentText>
</DialogContent>
}
<DialogActions>
<Button onClick={() => handleCloseLockDialog({ lockDialogOpen: false })} color='primary'>
Close
<FormattedMessage
id='label.close'
defaultMessage='Close'
/>
</Button>
</DialogActions>
</Dialog>

View File

@ -3,6 +3,7 @@ import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { withStyles } from '@material-ui/core/styles';
import { FormattedMessage } from 'react-intl';
import * as toolareaActions from '../../actions/toolareaActions';
import BuddyImage from '../../images/buddy.svg';
@ -95,8 +96,19 @@ class HiddenPeers extends React.PureComponent
className={classnames(classes.root, this.state.className)}
onClick={() => openUsersTab()}
>
<p>+{hiddenPeersCount} <br /> participant
{(hiddenPeersCount > 1) && 's'}
<p>
+{hiddenPeersCount} <br />
<FormattedMessage
id='room.hiddenPeers'
defaultMessage={
`{hiddenPeersCount, plural,
one {participant}
other {participants}}`
}
values={{
hiddenPeersCount
}}
/>
</p>
</div>
);

View File

@ -7,6 +7,7 @@ import useMediaQuery from '@material-ui/core/useMediaQuery';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import * as appPropTypes from '../appPropTypes';
import { useIntl, FormattedMessage } from 'react-intl';
import VideoView from '../VideoContainers/VideoView';
import Volume from './Volume';
import Fab from '@material-ui/core/Fab';
@ -96,6 +97,8 @@ const Me = (props) =>
{
const [ hover, setHover ] = useState(false);
const intl = useIntl();
let touchTimeout = null;
const {
@ -133,22 +136,34 @@ const Me = (props) =>
if (!me.canSendMic)
{
micState = 'unsupported';
micTip = 'Audio unsupported';
micTip = intl.formatMessage({
id : 'device.audioUnsupported',
defaultMessage : 'Audio unsupported'
});
}
else if (!micProducer)
{
micState = 'off';
micTip = 'Activate audio';
micTip = intl.formatMessage({
id : 'device.activateAudio',
defaultMessage : 'Activate audio'
});
}
else if (!micProducer.locallyPaused && !micProducer.remotelyPaused)
{
micState = 'on';
micTip = 'Mute audio';
micTip = intl.formatMessage({
id : 'device.muteAudio',
defaultMessage : 'Mute audio'
});
}
else
{
micState = 'muted';
micTip = 'Unmute audio';
micTip = intl.formatMessage({
id : 'device.unMuteAudio',
defaultMessage : 'Unmute audio'
});
}
let webcamState;
@ -158,17 +173,26 @@ const Me = (props) =>
if (!me.canSendWebcam)
{
webcamState = 'unsupported';
webcamTip = 'Video unsupported';
webcamTip = intl.formatMessage({
id : 'device.videoUnsupported',
defaultMessage : 'Video unsupported'
});
}
else if (webcamProducer)
{
webcamState = 'on';
webcamTip = 'Stop video';
webcamTip = intl.formatMessage({
id : 'device.stopVideo',
defaultMessage : 'Stop video'
});
}
else
{
webcamState = 'off';
webcamTip = 'Start video';
webcamTip = intl.formatMessage({
id : 'device.startVideo',
defaultMessage : 'Start video'
});
}
let screenState;
@ -178,17 +202,26 @@ const Me = (props) =>
if (!me.canShareScreen)
{
screenState = 'unsupported';
screenTip = 'Screen sharing not supported';
screenTip = intl.formatMessage({
id : 'device.screenSharingUnsupported',
defaultMessage : 'Screen sharing not supported'
});
}
else if (screenProducer)
{
screenState = 'on';
screenTip = 'Stop screen sharing';
screenTip = intl.formatMessage({
id : 'device.stopScreenSharing',
defaultMessage : 'Stop screen sharing'
});
}
else
{
screenState = 'off';
screenTip = 'Start screen sharing';
screenTip = intl.formatMessage({
id : 'device.startScreenSharing',
defaultMessage : 'Start screen sharing'
});
}
const spacingStyle =
@ -253,11 +286,19 @@ const Me = (props) =>
}, 2000);
}}
>
<p>ME</p>
<Tooltip title={micTip} placement={smallScreen ? 'top' : 'right'}>
<p>
<FormattedMessage
id='room.me'
defaultMessage='ME'
/>
</p>
<Tooltip title={micTip} placement={smallScreen ? 'top' : 'left'}>
<div>
<Fab
aria-label='Mute mic'
aria-label={intl.formatMessage({
id : 'device.muteAudio',
defaultMessage : 'Mute audio'
})}
className={classes.fab}
disabled={!me.canSendMic || me.audioInProgress}
color={micState === 'on' ? 'default' : 'secondary'}
@ -280,10 +321,13 @@ const Me = (props) =>
</Fab>
</div>
</Tooltip>
<Tooltip title={webcamTip} placement={smallScreen ? 'top' : 'right'}>
<Tooltip title={webcamTip} placement={smallScreen ? 'top' : 'left'}>
<div>
<Fab
aria-label='Mute video'
aria-label={intl.formatMessage({
id : 'device.startVideo',
defaultMessage : 'Start video'
})}
className={classes.fab}
disabled={!me.canSendWebcam || me.webcamInProgress}
color={webcamState === 'on' ? 'default' : 'secondary'}
@ -303,10 +347,13 @@ const Me = (props) =>
</Fab>
</div>
</Tooltip>
<Tooltip title={screenTip} placement={smallScreen ? 'top' : 'right'}>
<Tooltip title={screenTip} placement={smallScreen ? 'top' : 'left'}>
<div>
<Fab
aria-label='Share screen'
aria-label={intl.formatMessage({
id : 'device.startScreenSharing',
defaultMessage : 'Start screen sharing'
})}
className={classes.fab}
disabled={!me.canShareScreen || me.screenShareInProgress}
color={screenState === 'on' ? 'primary' : 'default'}
@ -410,7 +457,12 @@ const Me = (props) =>
}, 2000);
}}
>
<p>ME</p>
<p>
<FormattedMessage
id='room.me'
defaultMessage='ME'
/>
</p>
</div>
<VideoView

View File

@ -8,7 +8,9 @@ import { withRoomContext } from '../../RoomContext';
import { withStyles } from '@material-ui/core/styles';
import useMediaQuery from '@material-ui/core/useMediaQuery';
import * as roomActions from '../../actions/roomActions';
import { useIntl, FormattedMessage } from 'react-intl';
import VideoView from '../VideoContainers/VideoView';
import Tooltip from '@material-ui/core/Tooltip';
import Fab from '@material-ui/core/Fab';
import MicIcon from '@material-ui/icons/Mic';
import MicOffIcon from '@material-ui/icons/MicOff';
@ -103,6 +105,8 @@ const Peer = (props) =>
{
const [ hover, setHover ] = useState(false);
const intl = useIntl();
let touchTimeout = null;
const {
@ -194,7 +198,12 @@ const Peer = (props) =>
<div className={classnames(classes.viewContainer)}>
{ !videoVisible &&
<div className={classes.videoInfo}>
<p>this video is paused</p>
<p>
<FormattedMessage
id='room.videoPaused'
defaultMessage='This video is paused'
/>
</p>
</div>
}
@ -220,56 +229,95 @@ const Peer = (props) =>
}, 2000);
}}
>
<Fab
aria-label='Mute mic'
className={classes.fab}
disabled={!micConsumer}
color={micEnabled ? 'default' : 'secondary'}
size={smallButtons ? 'small' : 'large'}
onClick={() =>
{
micEnabled ?
roomClient.modifyPeerConsumer(peer.id, 'mic', true) :
roomClient.modifyPeerConsumer(peer.id, 'mic', false);
}}
<Tooltip
title={intl.formatMessage({
id : 'device.muteAudio',
defaultMessage : 'Mute audio'
})}
placement={smallScreen ? 'top' : 'left'}
>
{ micEnabled ?
<MicIcon />
:
<MicOffIcon />
}
</Fab>
<div>
<Fab
aria-label={intl.formatMessage({
id : 'device.muteAudio',
defaultMessage : 'Mute audio'
})}
className={classes.fab}
disabled={!micConsumer}
color={micEnabled ? 'default' : 'secondary'}
size={smallButtons ? 'small' : 'large'}
onClick={() =>
{
micEnabled ?
roomClient.modifyPeerConsumer(peer.id, 'mic', true) :
roomClient.modifyPeerConsumer(peer.id, 'mic', false);
}}
>
{ micEnabled ?
<MicIcon />
:
<MicOffIcon />
}
</Fab>
</div>
</Tooltip>
{ !smallScreen &&
<Fab
aria-label='New window'
className={classes.fab}
disabled={
!videoVisible ||
(windowConsumer === webcamConsumer.id)
}
size={smallButtons ? 'small' : 'large'}
onClick={() =>
{
toggleConsumerWindow(webcamConsumer);
}}
<Tooltip
title={intl.formatMessage({
id : 'label.newWindow',
defaultMessage : 'New window'
})}
placement={smallScreen ? 'top' : 'left'}
>
<NewWindowIcon />
</Fab>
<div>
<Fab
aria-label={intl.formatMessage({
id : 'label.newWindow',
defaultMessage : 'New window'
})}
className={classes.fab}
disabled={
!videoVisible ||
(windowConsumer === webcamConsumer.id)
}
size={smallButtons ? 'small' : 'large'}
onClick={() =>
{
toggleConsumerWindow(webcamConsumer);
}}
>
<NewWindowIcon />
</Fab>
</div>
</Tooltip>
}
<Fab
aria-label='Fullscreen'
className={classes.fab}
disabled={!videoVisible}
size={smallButtons ? 'small' : 'large'}
onClick={() =>
{
toggleConsumerFullscreen(webcamConsumer);
}}
<Tooltip
title={intl.formatMessage({
id : 'label.fullscreen',
defaultMessage : 'Fullscreen'
})}
placement={smallScreen ? 'top' : 'left'}
>
<FullScreenIcon />
</Fab>
<div>
<Fab
aria-label={intl.formatMessage({
id : 'label.fullscreen',
defaultMessage : 'Fullscreen'
})}
className={classes.fab}
disabled={!videoVisible}
size={smallButtons ? 'small' : 'large'}
onClick={() =>
{
toggleConsumerFullscreen(webcamConsumer);
}}
>
<FullScreenIcon />
</Fab>
</div>
</Tooltip>
</div>
<VideoView
@ -314,7 +362,12 @@ const Peer = (props) =>
>
{ !screenVisible &&
<div className={classes.videoInfo}>
<p>this video is paused</p>
<p>
<FormattedMessage
id='room.videoPaused'
defaultMessage='This video is paused'
/>
</p>
</div>
}
@ -344,35 +397,61 @@ const Peer = (props) =>
}}
>
{ !smallScreen &&
<Fab
aria-label='New window'
className={classes.fab}
disabled={
!screenVisible ||
(windowConsumer === screenConsumer.id)
}
size={smallButtons ? 'small' : 'large'}
onClick={() =>
{
toggleConsumerWindow(screenConsumer);
}}
<Tooltip
title={intl.formatMessage({
id : 'label.newWindow',
defaultMessage : 'New window'
})}
placement={smallScreen ? 'top' : 'left'}
>
<NewWindowIcon />
</Fab>
<div>
<Fab
aria-label={intl.formatMessage({
id : 'label.newWindow',
defaultMessage : 'New window'
})}
className={classes.fab}
disabled={
!screenVisible ||
(windowConsumer === screenConsumer.id)
}
size={smallButtons ? 'small' : 'large'}
onClick={() =>
{
toggleConsumerWindow(screenConsumer);
}}
>
<NewWindowIcon />
</Fab>
</div>
</Tooltip>
}
<Fab
aria-label='Fullscreen'
className={classes.fab}
disabled={!screenVisible}
size={smallButtons ? 'small' : 'large'}
onClick={() =>
{
toggleConsumerFullscreen(screenConsumer);
}}
<Tooltip
title={intl.formatMessage({
id : 'label.fullscreen',
defaultMessage : 'Fullscreen'
})}
placement={smallScreen ? 'top' : 'left'}
>
<FullScreenIcon />
</Fab>
<div>
<Fab
aria-label={intl.formatMessage({
id : 'label.fullscreen',
defaultMessage : 'Fullscreen'
})}
className={classes.fab}
disabled={!screenVisible}
size={smallButtons ? 'small' : 'large'}
onClick={() =>
{
toggleConsumerFullscreen(screenConsumer);
}}
>
<FullScreenIcon />
</Fab>
</div>
</Tooltip>
</div>
<VideoView
advancedMode={advancedMode}

View File

@ -5,6 +5,7 @@ import PropTypes from 'prop-types';
import classnames from 'classnames';
import * as appPropTypes from '../appPropTypes';
import { withStyles } from '@material-ui/core/styles';
import { FormattedMessage } from 'react-intl';
import VideoView from '../VideoContainers/VideoView';
import Volume from './Volume';
@ -119,7 +120,12 @@ const SpeakerPeer = (props) =>
<div className={classnames(classes.viewContainer)} style={style}>
{ !videoVisible &&
<div className={classes.videoInfo}>
<p>this video is paused</p>
<p>
<FormattedMessage
id='room.videoPaused'
defaultMessage='This video is paused'
/>
</p>
</div>
}
@ -145,7 +151,12 @@ const SpeakerPeer = (props) =>
>
{ !screenVisible &&
<div className={classes.videoInfo} style={style}>
<p>this video is paused</p>
<p>
<FormattedMessage
id='room.videoPaused'
defaultMessage='This video is paused'
/>
</p>
</div>
}

View File

@ -9,6 +9,7 @@ import { withRoomContext } from '../../RoomContext';
import { withStyles } from '@material-ui/core/styles';
import * as roomActions from '../../actions/roomActions';
import * as toolareaActions from '../../actions/toolareaActions';
import { useIntl, FormattedMessage } from 'react-intl';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography';
@ -109,6 +110,8 @@ const PulsingBadge = withStyles((theme) =>
const TopBar = (props) =>
{
const intl = useIntl();
const {
roomClient,
room,
@ -126,6 +129,39 @@ const TopBar = (props) =>
classes
} = props;
const lockTooltip = room.locked ?
intl.formatMessage({
id : 'tooltip.unLockRoom',
defaultMessage : 'Unlock room'
})
:
intl.formatMessage({
id : 'tooltip.lockRoom',
defaultMessage : 'Lock room'
});
const fullscreenTooltip = fullscreen ?
intl.formatMessage({
id : 'tooltip.leaveFullscreen',
defaultMessage : 'Leave fullscreen'
})
:
intl.formatMessage({
id : 'tooltip.enterFullscreen',
defaultMessage : 'Enter fullscreen'
});
const loginTooltip = loggedIn ?
intl.formatMessage({
id : 'tooltip.logout',
defaultMessage : 'Log out'
})
:
intl.formatMessage({
id : 'tooltip.login',
defaultMessage : 'Log in'
});
return (
<AppBar
position='fixed'
@ -138,7 +174,10 @@ const TopBar = (props) =>
>
<IconButton
color='inherit'
aria-label='Open drawer'
aria-label={intl.formatMessage({
id : 'label.openDrawer',
defaultMessage : 'Open drawer'
})}
onClick={() => toggleToolArea()}
className={classes.menuButton}
>
@ -156,9 +195,12 @@ const TopBar = (props) =>
</Typography>
<div className={classes.grow} />
<div className={classes.actionButtons}>
<Tooltip title={`${room.locked ? 'Unlock' : 'Lock'} room`}>
<Tooltip title={lockTooltip}>
<IconButton
aria-label='Lock room'
aria-label={intl.formatMessage({
id : 'tooltip.lockRoom',
defaultMessage : 'Lock room'
})}
className={classes.actionButton}
color='inherit'
onClick={() =>
@ -181,9 +223,17 @@ const TopBar = (props) =>
</IconButton>
</Tooltip>
{ lobbyPeers.length > 0 &&
<Tooltip title='Show lobby'>
<Tooltip
title={intl.formatMessage({
id : 'tooltip.lobby',
defaultMessage : 'Show lobby'
})}
>
<IconButton
aria-label='Lobby'
aria-label={intl.formatMessage({
id : 'tooltip.lobby',
defaultMessage : 'Show lobby'
})}
color='inherit'
onClick={() => setLockDialogOpen(!room.lockDialogOpen)}
>
@ -197,9 +247,12 @@ const TopBar = (props) =>
</Tooltip>
}
{ fullscreenEnabled &&
<Tooltip title={`${fullscreen ? 'Leave' : 'Enter'} fullscreen`}>
<Tooltip title={fullscreenTooltip}>
<IconButton
aria-label='Fullscreen'
aria-label={intl.formatMessage({
id : 'tooltip.enterFullscreen',
defaultMessage : 'Enter fullscreen'
})}
className={classes.actionButton}
color='inherit'
onClick={onFullscreen}
@ -212,9 +265,17 @@ const TopBar = (props) =>
</IconButton>
</Tooltip>
}
<Tooltip title='Show settings'>
<Tooltip
title={intl.formatMessage({
id : 'tooltip.settings',
defaultMessage : 'Show settings'
})}
>
<IconButton
aria-label='Settings'
aria-label={intl.formatMessage({
id : 'tooltip.settings',
defaultMessage : 'Show settings'
})}
className={classes.actionButton}
color='inherit'
onClick={() => setSettingsOpen(!room.settingsOpen)}
@ -223,9 +284,12 @@ const TopBar = (props) =>
</IconButton>
</Tooltip>
{ loginEnabled &&
<Tooltip title={`Log ${loggedIn ? 'out' : 'in'}`}>
<Tooltip title={loginTooltip}>
<IconButton
aria-label='Account'
aria-label={intl.formatMessage({
id : 'tooltip.login',
defaultMessage : 'Log in'
})}
className={classes.actionButton}
color='inherit'
onClick={() =>
@ -242,13 +306,19 @@ const TopBar = (props) =>
</Tooltip>
}
<Button
aria-label='Leave meeting'
aria-label={intl.formatMessage({
id : 'label.leave',
defaultMessage : 'Leave'
})}
className={classes.actionButton}
variant='contained'
color='secondary'
onClick={() => roomClient.close()}
>
Leave
<FormattedMessage
id='label.leave'
defaultMessage='Leave'
/>
</Button>
</div>
</Toolbar>

View File

@ -4,6 +4,7 @@ import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../RoomContext';
import * as settingsActions from '../actions/settingsActions';
import PropTypes from 'prop-types';
import { useIntl, FormattedMessage } from 'react-intl';
import Dialog from '@material-ui/core/Dialog';
import DialogContentText from '@material-ui/core/DialogContentText';
import IconButton from '@material-ui/core/IconButton';
@ -87,6 +88,8 @@ const DialogTitle = withStyles(styles)((props) =>
{
const [ open, setOpen ] = useState(false);
const intl = useIntl();
useEffect(() =>
{
const openTimer = setTimeout(() => setOpen(true), 1000);
@ -120,7 +123,10 @@ const DialogTitle = withStyles(styles)((props) =>
onClose={handleTooltipClose}
onOpen={handleTooltipOpen}
open={open}
title='Click to log in'
title={intl.formatMessage({
id : 'tooltip.login',
defaultMessage : 'Click to log in'
})}
placement='left'
>
<IconButton
@ -167,6 +173,8 @@ const JoinDialog = ({
classes
}) =>
{
const intl = useIntl();
const handleKeyDown = (event) =>
{
const { key } = event;
@ -207,21 +215,38 @@ const JoinDialog = ({
</DialogTitle>
<DialogContent>
<DialogContentText gutterBottom>
You are about to join a meeting.
<FormattedMessage
id='room.aboutToJoin'
defaultMessage='You are about to join a meeting'
/>
</DialogContentText>
<DialogContentText variant='h6' gutterBottom align='center'>
Room ID: { room.name }
<FormattedMessage
id='room.roomId'
defaultMessage='Room ID: {roomName}'
values={{
roomName : room.name
}}
/>
</DialogContentText>
<DialogContentText gutterBottom>
Set your name for participation,
and choose how you want to join:
<FormattedMessage
id='room.setYourName'
defaultMessage={
`Set your name for participation,
and choose how you want to join:`
}
/>
</DialogContentText>
<TextField
id='displayname'
label='Your name'
label={intl.formatMessage({
id : 'label.yourName',
defaultMessage : 'Your name'
})}
value={displayName}
variant='outlined'
margin='normal'
@ -255,7 +280,10 @@ const JoinDialog = ({
variant='contained'
color='secondary'
>
Audio only
<FormattedMessage
id='room.audioOnly'
defaultMessage='Audio only'
/>
</Button>
<Button
onClick={() =>
@ -265,7 +293,10 @@ const JoinDialog = ({
variant='contained'
color='secondary'
>
Audio and Video
<FormattedMessage
id='room.audioVideo'
defaultMessage='Audio and Video'
/>
</Button>
</DialogActions>
:
@ -276,23 +307,37 @@ const JoinDialog = ({
variant='h6'
align='center'
>
Ok, you are ready
<FormattedMessage
id='room.youAreReady'
defaultMessage='Ok, you are ready'
/>
</DialogContentText>
{ room.signInRequired ?
<DialogContentText gutterBottom>
The room is empty!
You can Log In to start the meeting or wait until the host joins.
<FormattedMessage
id='room.emptyRequireLogin'
defaultMessage={
`The room is empty! You can Log In to start
the meeting or wait until the host joins`
}
/>
</DialogContentText>
:
<DialogContentText gutterBottom>
The room is locked - hang on until somebody lets you in ...
<FormattedMessage
id='room.locketWait'
defaultMessage='The room is locked - hang on until somebody lets you in ...'
/>
</DialogContentText>
}
</DialogContent>
}
<CookieConsent>
This website uses cookies to enhance the user experience.
<FormattedMessage
id='room.cookieConsent'
defaultMessage='This website uses cookies to enhance the user experience'
/>
</CookieConsent>
</Dialog>
</div>

View File

@ -1,8 +1,9 @@
import React from 'react';
import React, { useState } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../../../RoomContext';
import { useIntl } from 'react-intl';
import Paper from '@material-ui/core/Paper';
import InputBase from '@material-ui/core/InputBase';
import IconButton from '@material-ui/core/IconButton';
@ -28,19 +29,13 @@ const styles = (theme) =>
}
});
class ChatInput extends React.PureComponent
const ChatInput = (props) =>
{
constructor(props)
{
super(props);
const [ message, setMessage ] = useState('');
this.state =
{
message : ''
};
}
const intl = useIntl();
createNewMessage = (text, sender, name, picture) =>
const createNewMessage = (text, sender, name, picture) =>
({
type : 'message',
text,
@ -50,67 +45,67 @@ class ChatInput extends React.PureComponent
picture
});
handleChange = (e) =>
const handleChange = (e) =>
{
this.setState({ message: e.target.value });
}
setMessage(e.target.value);
};
render()
{
const {
roomClient,
displayName,
picture,
classes
} = this.props;
const {
roomClient,
displayName,
picture,
classes
} = props;
return (
<Paper className={classes.root}>
<InputBase
className={classes.input}
placeholder='Enter chat message...'
value={this.state.message || ''}
onChange={this.handleChange}
onKeyPress={(ev) =>
return (
<Paper className={classes.root}>
<InputBase
className={classes.input}
placeholder={intl.formatMessage({
id : 'label.chatInput',
defaultMessage : 'Enter chat message...'
})}
value={message || ''}
onChange={handleChange}
onKeyPress={(ev) =>
{
if (ev.key === 'Enter')
{
if (ev.key === 'Enter')
ev.preventDefault();
if (message && message !== '')
{
ev.preventDefault();
const sendMessage = createNewMessage(message, 'response', displayName, picture);
if (this.state.message && this.state.message !== '')
{
const message = this.createNewMessage(this.state.message, 'response', displayName, picture);
roomClient.sendChatMessage(sendMessage);
roomClient.sendChatMessage(message);
this.setState({ message: '' });
}
setMessage('');
}
}}
autoFocus
/>
<IconButton
color='primary'
className={classes.iconButton}
aria-label='Send'
onClick={() =>
}
}}
autoFocus
/>
<IconButton
color='primary'
className={classes.iconButton}
aria-label='Send'
onClick={() =>
{
if (message && message !== '')
{
if (this.state.message && this.state.message !== '')
{
const message = this.createNewMessage(this.state.message, 'response', displayName, picture);
const sendMessage = this.createNewMessage(message, 'response', displayName, picture);
roomClient.sendChatMessage(message);
roomClient.sendChatMessage(sendMessage);
this.setState({ message: '' });
}
}}
>
<SendIcon />
</IconButton>
</Paper>
);
}
}
setMessage('');
}
}}
>
<SendIcon />
</IconButton>
</Paper>
);
};
ChatInput.propTypes =
{

View File

@ -92,7 +92,7 @@ Message.propTypes =
self : PropTypes.bool,
picture : PropTypes.string,
text : PropTypes.string,
time : PropTypes.string,
time : PropTypes.object,
name : PropTypes.string,
classes : PropTypes.object.isRequired
};

View File

@ -2,6 +2,7 @@ import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import { FormattedTime } from 'react-intl';
import Message from './Message';
import EmptyAvatar from '../../../images/avatar-empty.jpeg';
@ -49,7 +50,7 @@ class MessageList extends React.Component
getTimeString(time)
{
return `${(time.getHours() < 10 ? '0' : '')}${time.getHours()}:${(time.getMinutes() < 10 ? '0' : '')}${time.getMinutes()}`;
return (<FormattedTime value={new Date(time)} />);
}
render()
@ -65,8 +66,6 @@ class MessageList extends React.Component
{
chat.map((message, index) =>
{
const messageTime = new Date(message.time);
const picture = (message.sender === 'response' ?
message.picture : myPicture) || EmptyAvatar;
@ -76,7 +75,7 @@ class MessageList extends React.Component
self={message.sender === 'client'}
picture={picture}
text={message.text}
time={this.getTimeString(messageTime)}
time={this.getTimeString(message.time)}
name={message.name}
/>
);
@ -89,9 +88,9 @@ class MessageList extends React.Component
MessageList.propTypes =
{
chat : PropTypes.array,
myPicture : PropTypes.string,
classes : PropTypes.object.isRequired
chat : PropTypes.array,
myPicture : PropTypes.string,
classes : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { withRoomContext } from '../../../RoomContext';
import { withStyles } from '@material-ui/core/styles';
import { FormattedMessage } from 'react-intl';
import magnet from 'magnet-uri';
import Typography from '@material-ui/core/Typography';
import Button from '@material-ui/core/Button';
@ -70,7 +71,10 @@ class File extends React.PureComponent
{ file.files &&
<Fragment>
<Typography className={classes.text}>
File finished downloading
<FormattedMessage
id='filesharing.finished'
defaultMessage='File finished downloading'
/>
</Typography>
{ file.files.map((sharedFile, i) => (
@ -87,14 +91,23 @@ class File extends React.PureComponent
roomClient.saveFile(sharedFile);
}}
>
Save
<FormattedMessage
id='filesharing.save'
defaultMessage='Save'
/>
</Button>
</div>
))}
</Fragment>
}
<Typography className={classes.text}>
{ `${displayName} shared a file` }
<FormattedMessage
id='filesharing.sharedFile'
defaultMessage='{displayName} shared a file'
values={{
displayName
}}
/>
</Typography>
{ (!file.active && !file.files) &&
@ -112,11 +125,17 @@ class File extends React.PureComponent
roomClient.handleDownload(magnetUri);
}}
>
Download
<FormattedMessage
id='filesharing.download'
defaultMessage='Download'
/>
</Button>
:
<Typography className={classes.text}>
Your browser does not support downloading files using WebTorrent.
<FormattedMessage
id='label.fileSharingUnsupported'
defaultMessage='File sharing not supported'
/>
</Typography>
}
</div>
@ -124,8 +143,14 @@ class File extends React.PureComponent
{ file.timeout &&
<Typography className={classes.text}>
If this process takes a long time, there might not be anyone seeding
this torrent. Try asking someone to reupload the file that you want.
<FormattedMessage
id='filesharing.missingSeeds'
defaultMessage={
`If this process takes a long time, there might not
be anyone seeding this torrent. Try asking someone to
reupload the file that you want.`
}
/>
</Typography>
}

View File

@ -3,6 +3,7 @@ import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import * as appPropTypes from '../../appPropTypes';
import { withStyles } from '@material-ui/core/styles';
import { injectIntl } from 'react-intl';
import File from './File';
import EmptyAvatar from '../../../images/avatar-empty.jpeg';
@ -46,6 +47,7 @@ class FileList extends React.PureComponent
files,
me,
peers,
intl,
classes
} = this.props;
@ -59,7 +61,10 @@ class FileList extends React.PureComponent
if (me.id === file.peerId)
{
displayName = 'You';
displayName = intl.formatMessage({
id : 'room.me',
defaultMessage : 'Me'
});
filePicture = me.picture;
}
else if (peers[file.peerId])
@ -69,7 +74,10 @@ class FileList extends React.PureComponent
}
else
{
displayName = 'Unknown';
displayName = intl.formatMessage({
id : 'label.unknown',
defaultMessage : 'Unknown'
});
}
return (
@ -91,6 +99,7 @@ FileList.propTypes =
files : PropTypes.object.isRequired,
me : appPropTypes.Me.isRequired,
peers : PropTypes.object.isRequired,
intl : PropTypes.object.isRequired,
classes : PropTypes.object.isRequired
};
@ -117,4 +126,4 @@ export default connect(
);
}
}
)(withStyles(styles)(FileList));
)(withStyles(styles)(injectIntl(FileList)));

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../../../RoomContext';
import { useIntl } from 'react-intl';
import FileList from './FileList';
import Paper from '@material-ui/core/Paper';
import Button from '@material-ui/core/Button';
@ -26,58 +27,57 @@ const styles = (theme) =>
}
});
class FileSharing extends React.PureComponent
const FileSharing = (props) =>
{
constructor(props)
{
super(props);
const intl = useIntl();
this._fileInput = React.createRef();
}
handleFileChange = async (event) =>
const handleFileChange = async (event) =>
{
if (event.target.files.length > 0)
{
this.props.roomClient.shareFiles(event.target.files);
props.roomClient.shareFiles(event.target.files);
}
};
render()
{
const {
canShareFiles,
classes
} = this.props;
const {
canShareFiles,
classes
} = props;
const buttonDescription = canShareFiles ?
'Share file' : 'File sharing not supported';
const buttonDescription = canShareFiles ?
intl.formatMessage({
id : 'label.shareFile',
defaultMessage : 'Share file'
})
:
intl.formatMessage({
id : 'label.fileSharingUnsupported',
defaultMessage : 'File sharing not supported'
});
return (
<Paper className={classes.root}>
<input
ref={this._fileInput}
className={classes.input}
type='file'
onChange={this.handleFileChange}
id='share-files-button'
/>
<label htmlFor='share-files-button'>
<Button
variant='contained'
component='span'
className={classes.button}
disabled={!canShareFiles}
>
{buttonDescription}
</Button>
</label>
return (
<Paper className={classes.root}>
<input
className={classes.input}
type='file'
onChange={handleFileChange}
id='share-files-button'
/>
<label htmlFor='share-files-button'>
<Button
variant='contained'
component='span'
className={classes.button}
disabled={!canShareFiles}
>
{buttonDescription}
</Button>
</label>
<FileList />
</Paper>
);
}
}
<FileList />
</Paper>
);
};
FileSharing.propTypes = {
roomClient : PropTypes.any.isRequired,

View File

@ -3,6 +3,7 @@ import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import * as toolareaActions from '../../actions/toolareaActions';
import { useIntl } from 'react-intl';
import AppBar from '@material-ui/core/AppBar';
import Tabs from '@material-ui/core/Tabs';
import Tab from '@material-ui/core/Tab';
@ -44,6 +45,8 @@ const styles = (theme) =>
const MeetingDrawer = (props) =>
{
const intl = useIntl();
const {
currentToolTab,
unreadMessages,
@ -72,18 +75,29 @@ const MeetingDrawer = (props) =>
<Tab
label={
<Badge color='secondary' badgeContent={unreadMessages}>
Chat
{intl.formatMessage({
id : 'label.chat',
defaultMessage : 'Chat'
})}
</Badge>
}
/>
<Tab
label={
<Badge color='secondary' badgeContent={unreadFiles}>
File sharing
{intl.formatMessage({
id : 'label.filesharing',
defaultMessage : 'File sharing'
})}
</Badge>
}
/>
<Tab label='Participants' />
<Tab
label={intl.formatMessage({
id : 'label.participants',
defaultMessage : 'Participants'
})}
/>
</Tabs>
<IconButton onClick={closeDrawer}>
{theme.direction === 'ltr' ? <ChevronLeftIcon /> : <ChevronRightIcon />}

View File

@ -8,6 +8,7 @@ import classNames from 'classnames';
import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../../../RoomContext';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import ListPeer from './ListPeer';
import ListMe from './ListMe';
import Volume from '../../Containers/Volume';
@ -84,11 +85,21 @@ class ParticipantList extends React.PureComponent
return (
<div className={classes.root} ref={(node) => { this.node = node; }}>
<ul className={classes.list}>
<li className={classes.listheader}>Me:</li>
<li className={classes.listheader}>
<FormattedMessage
id='room.me'
defaultMessage='Me'
/>
</li>
<ListMe />
</ul>
<ul className={classes.list}>
<li className={classes.listheader}>Participants in Spotlight:</li>
<li className={classes.listheader}>
<FormattedMessage
id='room.spotlights'
defaultMessage='Participants in Spotlight'
/>
</li>
{ spotlightPeers.map((peer) => (
<li
key={peer.id}
@ -104,7 +115,12 @@ class ParticipantList extends React.PureComponent
))}
</ul>
<ul className={classes.list}>
<li className={classes.listheader}>Passive Participants:</li>
<li className={classes.listheader}>
<FormattedMessage
id='room.passive'
defaultMessage='Passive Participants'
/>
</li>
{ passivePeers.map((peerId) => (
<li
key={peerId}

View File

@ -7,6 +7,7 @@ import * as roomActions from '../actions/roomActions';
import * as toolareaActions from '../actions/toolareaActions';
import { idle } from '../utils';
import FullScreen from './FullScreen';
import { FormattedMessage } from 'react-intl';
import CookieConsent from 'react-cookie-consent';
import CssBaseline from '@material-ui/core/CssBaseline';
import SwipeableDrawer from '@material-ui/core/SwipeableDrawer';
@ -152,7 +153,10 @@ class Room extends React.PureComponent
return (
<div className={classes.root}>
<CookieConsent>
This website uses cookies to enhance the user experience.
<FormattedMessage
id='room.cookieConsent'
defaultMessage='This website uses cookies to enhance the user experience'
/>
</CookieConsent>
<FullScreenView advancedMode={advancedMode} />
@ -237,6 +241,7 @@ export default connect(
{
return (
prev.room === next.room &&
prev.settings.advancedMode === next.settings.advancedMode &&
prev.toolarea.toolAreaOpen === next.toolarea.toolAreaOpen
);
}

View File

@ -6,6 +6,7 @@ import { withRoomContext } from '../../RoomContext';
import * as roomActions from '../../actions/roomActions';
import * as settingsActions from '../../actions/settingsActions';
import PropTypes from 'prop-types';
import { useIntl, FormattedMessage } from 'react-intl';
import Dialog from '@material-ui/core/Dialog';
import DialogTitle from '@material-ui/core/DialogTitle';
import DialogActions from '@material-ui/core/DialogActions';
@ -52,35 +53,6 @@ const styles = (theme) =>
}
});
const modes = [ {
value : 'democratic',
label : 'Democratic view'
}, {
value : 'filmstrip',
label : 'Filmstrip view'
} ];
const resolutions = [ {
value : 'low',
label : 'Low'
},
{
value : 'medium',
label : 'Medium'
},
{
value : 'high',
label : 'High (HD)'
},
{
value : 'veryhigh',
label : 'Very high (FHD)'
},
{
value : 'ultra',
label : 'Ultra (UHD)'
} ];
const Settings = ({
roomClient,
room,
@ -92,6 +64,58 @@ const Settings = ({
classes
}) =>
{
const intl = useIntl();
const modes = [ {
value : 'democratic',
label : intl.formatMessage({
id : 'label.democratic',
defaultMessage : 'Democratic view'
})
}, {
value : 'filmstrip',
label : intl.formatMessage({
id : 'label.filmstrip',
defaultMessage : 'Filmstrip view'
})
} ];
const resolutions = [ {
value : 'low',
label : intl.formatMessage({
id : 'label.low',
defaultMessage : 'Low'
})
},
{
value : 'medium',
label : intl.formatMessage({
id : 'label.medium',
defaultMessage : 'Medium'
})
},
{
value : 'high',
label : intl.formatMessage({
id : 'label.high',
defaultMessage : 'High (HD)'
})
},
{
value : 'veryhigh',
label : intl.formatMessage({
id : 'label.veryHigh',
defaultMessage : 'Very high (FHD)'
})
},
{
value : 'ultra',
label : intl.formatMessage({
id : 'label.ultra',
defaultMessage : 'Ultra (UHD)'
})
} ];
let webcams;
if (me.webcamDevices)
@ -115,7 +139,12 @@ const Settings = ({
paper : classes.dialogPaper
}}
>
<DialogTitle id='form-dialog-title'>Settings</DialogTitle>
<DialogTitle id='form-dialog-title'>
<FormattedMessage
id='settings.settings'
defaultMessage='Settings'
/>
</DialogTitle>
<form className={classes.setting} autoComplete='off'>
<FormControl className={classes.formControl}>
<Select
@ -126,7 +155,10 @@ const Settings = ({
roomClient.changeWebcam(event.target.value);
}}
displayEmpty
name='Camera'
name={intl.formatMessage({
id : 'settings.camera',
defaultMessage : 'Camera'
})}
autoWidth
className={classes.selectEmpty}
disabled={webcams.length === 0 || me.webcamInProgress}
@ -140,9 +172,15 @@ const Settings = ({
</Select>
<FormHelperText>
{ webcams.length > 0 ?
'Select video device'
intl.formatMessage({
id : 'settings.selectCamera',
defaultMessage : 'Select video device'
})
:
'Unable to select video device'
intl.formatMessage({
id : 'settings.cantSelectCamera',
defaultMessage : 'Unable to select video device'
})
}
</FormHelperText>
</FormControl>
@ -157,7 +195,10 @@ const Settings = ({
roomClient.changeAudioDevice(event.target.value);
}}
displayEmpty
name='Audio device'
name={intl.formatMessage({
id : 'settings.audio',
defaultMessage : 'Audio device'
})}
autoWidth
className={classes.selectEmpty}
disabled={audioDevices.length === 0 || me.audioInProgress}
@ -171,9 +212,15 @@ const Settings = ({
</Select>
<FormHelperText>
{ audioDevices.length > 0 ?
'Select audio device'
intl.formatMessage({
id : 'settings.selectAudio',
defaultMessage : 'Select audio device'
})
:
'Unable to select audio device'
intl.formatMessage({
id : 'settings.cantSelectAudio',
defaultMessage : 'Unable to select audio device'
})
}
</FormHelperText>
</FormControl>
@ -201,7 +248,10 @@ const Settings = ({
})}
</Select>
<FormHelperText>
Select your video resolution
<FormattedMessage
id='settings.resolution'
defaultMessage='Select your video resolution'
/>
</FormHelperText>
</FormControl>
</form>
@ -214,7 +264,10 @@ const Settings = ({
if (event.target.value)
handleChangeMode(event.target.value);
}}
name='Room layout'
name={intl.formatMessage({
id : 'settings.layout',
defaultMessage : 'Room layout'
})}
autoWidth
className={classes.selectEmpty}
>
@ -228,18 +281,27 @@ const Settings = ({
})}
</Select>
<FormHelperText>
Select room layout
<FormattedMessage
id='settings.selectRoomLayout'
defaultMessage='Select room layout'
/>
</FormHelperText>
</FormControl>
</form>
<FormControlLabel
className={classes.setting}
control={<Checkbox checked={settings.advancedMode} onChange={onToggleAdvancedMode} value='advancedMode' />}
label='Advanced mode'
label={intl.formatMessage({
id : 'settings.advancedMode',
defaultMessage : 'Advanced mode'
})}
/>
<DialogActions>
<Button onClick={() => handleCloseSettings({ settingsOpen: false })} color='primary'>
Close
<FormattedMessage
id='label.close'
defaultMessage='Close'
/>
</Button>
</DialogActions>
</Dialog>

View File

@ -2,6 +2,7 @@ import domready from 'domready';
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { createIntl, createIntlCache, RawIntlProvider } from 'react-intl';
import randomString from 'random-string';
import Logger from './Logger';
import debug from 'debug';
@ -18,8 +19,26 @@ import { persistor, store } from './store';
import { SnackbarProvider } from 'notistack';
import * as serviceWorker from './serviceWorker';
import messagesEnglish from './translations/en';
import messagesNorwegian from './translations/nb';
import './index.css';
const cache = createIntlCache();
const messages =
{
'en' : messagesEnglish,
'nb' : messagesNorwegian
};
const locale = navigator.language.split(/[-_]/)[0]; // language without region code
const intl = createIntl({
locale,
messages : messages[locale]
}, cache);
if (process.env.REACT_APP_DEBUG === '*' || process.env.NODE_ENV !== 'production')
{
debug.enable('* -engine* -socket* -RIE* *WARN* *ERROR*');
@ -29,7 +48,7 @@ const logger = new Logger();
let roomClient;
RoomClient.init({ store });
RoomClient.init({ store, intl });
const theme = createMuiTheme(window.config.theme);
@ -92,13 +111,15 @@ function run()
render(
<Provider store={store}>
<MuiThemeProvider theme={theme}>
<PersistGate loading={<LoadingView />} persistor={persistor}>
<RoomContext.Provider value={roomClient}>
<SnackbarProvider>
<App />
</SnackbarProvider>
</RoomContext.Provider>
</PersistGate>
<RawIntlProvider value={intl}>
<PersistGate loading={<LoadingView />} persistor={persistor}>
<RoomContext.Provider value={roomClient}>
<SnackbarProvider>
<App />
</SnackbarProvider>
</RoomContext.Provider>
</PersistGate>
</RawIntlProvider>
</MuiThemeProvider>
</Provider>,
document.getElementById('multiparty-meeting')

View File

@ -0,0 +1,133 @@
{
"socket.disconnected": "You are disconnected",
"socket.reconnecting": "You are disconnected, attempting to reconnect",
"socket.reconnected": "You are reconnected",
"socket.requestError": "Error on server request",
"room.cookieConsent": "This website uses cookies to enhance the user experience",
"room.joined": "You have joined the room",
"room.cantJoin": "Unable to join the room",
"room.youLocked": "You locked the room",
"room.cantLock": "Unable to lock the room",
"room.youUnLocked": "You unlocked the room",
"room.cantUnLock": "Unable to unlock the room",
"room.locked": "Room is now locked",
"room.unlocked": "Room is now unlocked",
"room.newLobbyPeer": "New participant entered the lobby",
"room.lobbyPeerLeft": "Participant in lobby left",
"room.lobbyPeerChangedDisplayName": "Participant in lobby changed name to {displayName}",
"room.lobbyPeerChangedPicture": "Participant in lobby changed picture",
"room.setAccessCode": "Access code for room updated",
"room.accessCodeOn": "Access code for room is now activated",
"room.accessCodeOff": "Access code for room is now deactivated",
"room.peerChangedDisplayName": "{oldDisplayName} is now {displayName}",
"room.newPeer": "{displayName} joined the room",
"room.newFile": "New file available",
"room.toggleAdvancedMode": "Toggled advanced mode",
"room.setDemocraticView": "Changed layout to democratic view",
"room.setFilmStripView": "Changed layout to filmstrip view",
"room.loggedIn": "You are logged in",
"room.loggedOut": "You are logged out",
"room.changedDisplayName": "Your display name changed to {displayName}",
"room.changeDisplayNameError": "An error occured while changing your display name",
"room.chatError": "Unable to send chat message",
"room.aboutToJoin": "You are about to join a meeting",
"room.roomId": "Room ID: {roomName}",
"room.setYourName": "Set your name for participation, and choose how you want to join:",
"room.audioOnly": "Audio only",
"room.audioVideo": "Audio and Video",
"room.youAreReady": "Ok, you are ready",
"room.emptyRequireLogin": "The room is empty! You can Log In to start the meeting or wait until the host joins",
"room.locketWait": "The room is locked - hang on until somebody lets you in ...",
"room.lobbyAdministration": "Lobby administration",
"room.peersInLobby": "Participants in Lobby",
"room.lobbyEmpty": "There are currently no one in the lobby",
"room.hiddenPeers": "{hiddenPeersCount, plural, one {participant} other {participants}}",
"room.me": "Me",
"room.spotlights": "Participants in Spotlight",
"room.passive": "Passive Participants",
"room.videoPaused": "This video is paused",
"tooltip.login": "Log in",
"tooltip.logout": "Log out",
"tooltip.admitFromLobby": "Admit from lobby",
"tooltip.lockRoom": "Lock room",
"tooltip.unLockRoom": "Unlock room",
"tooltip.enterFullscreen": "Enter fullscreen",
"tooltip.leaveFullscreen": "Leave fullscreen",
"tooltip.lobby": "Show lobby",
"tooltip.settings": "Show settings",
"label.yourName": "Your name",
"label.newWindow": "New window",
"label.fullscreen": "Fullscreen",
"label.openDrawer": "Open drawer",
"label.leave": "Leave",
"label.chatInput": "Enter chat message...",
"label.chat": "Chat",
"label.filesharing": "File sharing",
"label.participants": "Participants",
"label.shareFile": "Share file",
"label.fileSharingUnsupported": "File sharing not supported",
"label.unknown": "Unknown",
"label.democratic": "Democratic view",
"label.filmstrip": "Filmstrip view",
"label.low": "Low",
"label.medium": "Medium",
"label.high": "High (HD)",
"label.veryHigh": "Very high (FHD)",
"label.ultra": "Ultra (UHD)",
"label.close": "Close",
"settings.settings": "Settings",
"settings.camera": "Camera",
"settings.selectCamera": "Select video device",
"settings.cantSelectCamera": "Unable to select video device",
"settings.audio": "Audio device",
"settings.selectAudio": "Select audio device",
"settings.cantSelectAudio": "Unable to select audio device",
"settings.resolution": "Select your video resolution",
"settings.layout": "Room layout",
"settings.selectRoomLayout": "Select room layout",
"settings.advancedMode": "Advanced mode",
"filesharing.saveFileError": "Unable to save file",
"filesharing.startingFileShare": "Attempting to share file",
"filesharing.successfulFileShare": "File successfully shared",
"filesharing.unableToShare": "Unable to share file",
"filesharing.error": "There was a filesharing error",
"filesharing.finished": "File finished downloading",
"filesharing.save": "Save",
"filesharing.sharedFile": "{displayName} shared a file",
"filesharing.download": "Download",
"filesharing.missingSeeds": "If this process takes a long time, there might not be anyone seeding this torrent. Try asking someone to reupload the file that you want.",
"devices.devicesChanged": "Your devices changed, configure your devices in the settings dialog",
"device.audioUnsupported": "Audio unsupported",
"device.activateAudio": "Activate audio",
"device.muteAudio": "Mute audio",
"device.unMuteAudio": "Unmute audio",
"device.videoUnsupported": "Video unsupported",
"device.startVideo": "Start video",
"device.stopVideo": "Stop video",
"device.screenSharingUnsupported": "Screen sharing not supported",
"device.startScreenSharing": "Start screen sharing",
"device.stopScreenSharing": "Stop screen sharing",
"devices.microphoneDisconnected": "Microphone disconnected",
"devices.microphoneError": "An error occured while accessing your microphone",
"devices.microPhoneMute": "Muted your microphone",
"devices.micophoneUnMute": "Unmuted your microphone",
"devices.microphoneEnable": "Enabled your microphone",
"devices.microphoneMuteError": "Unable to mute your microphone",
"devices.microphoneUnMuteError": "Unable to unmute your microphone",
"devices.screenSharingDisconnected" : "Screen sharing disconnected",
"devices.screenSharingError": "An error occured while accessing your screen",
"devices.cameraDisconnected": "Camera disconnected",
"devices.cameraError": "An error occured while accessing your camera"
}

View File

@ -0,0 +1,133 @@
{
"socket.disconnected": "Du er frakoblet",
"socket.reconnecting": "Du er frakoblet, forsøker å koble til på nytt",
"socket.reconnected": "Du er koblet til igjen",
"socket.requestError": "Feil på server melding",
"room.cookieConsent": "Denne siden bruker cookies for å forbedre brukeropplevelsen",
"room.joined": "Du ble med i møtet",
"room.cantJoin": "Kunne ikke bli med i møtet",
"room.youLocked": "Du låste møtet",
"room.cantLock": "Klarte ikke å låse møtet",
"room.youUnLocked": "Du låste opp møtet",
"room.cantUnLock": "Klarte ikke å låse opp møtet",
"room.locked": "Møtet er låst",
"room.unlocked": "Møtet er låst opp",
"room.newLobbyPeer": "Ny deltaker i lobbyen",
"room.lobbyPeerLeft": "Deltaker i lobbyen forsvant",
"room.lobbyPeerChangedDisplayName": "Deltaker i lobbyen endret navn til {displayName}",
"room.lobbyPeerChangedPicture": "Deltaker i lobbyen endret bilde",
"room.setAccessCode": "Tilgangskode for møtet er oppdatert",
"room.accessCodeOn": "Tilgangskode for møtet er aktivert",
"room.accessCodeOff": "Tilgangskode for møtet er deaktivert",
"room.peerChangedDisplayName": "{oldDisplayName} heter nå {displayName}",
"room.newPeer": "{displayName} ble med i møtet",
"room.newFile": "Ny fil tilgjengelig",
"room.toggleAdvancedMode": "Aktiver avansert modus",
"room.setDemocraticView": "Endret layout til demokratisk",
"room.setFilmStripView": "Endret layout til filmstripe",
"room.loggedIn": "Du er logget inn",
"room.loggedOut": "Du er logget ut",
"room.changedDisplayName": "Navnet ditt er nå {displayName}",
"room.changeDisplayNameError": "Det skjedde en feil ved endring av navnet ditt",
"room.chatError": "Klarte ikke sende melding",
"room.aboutToJoin": "Du er i ferd med å bli med i et møte",
"room.roomId": "Møte ID: {roomName}",
"room.setYourName": "Skriv inn navnet ditt, og velg hvordan du vil bli med i møtet",
"room.audioOnly": "Kun lyd",
"room.audioVideo": "Lyd og bilde",
"room.youAreReady": "Ok, du er klar",
"room.emptyRequireLogin": "Møtet er tomt. Du kan logge inn for å starte møtet, eller vente til verten kommer",
"room.locketWait": "Møtet er låst, vent til noen slipper deg inn",
"room.lobbyAdministration": "Lobby administrasjon",
"room.peersInLobby": "Deltakere i lobbyen",
"room.lobbyEmpty": "Det er for øyeblikket ingen deltakere i lobbyen",
"room.hiddenPeers": "{hiddenPeersCount, plural, one {deltaker} other {deltakere}}",
"room.me": "Meg",
"room.spotlights": "Deltakere i fokus",
"room.passive": "Passive deltakere",
"room.videoPaused": "Denne videoen er inaktiv",
"tooltip.login": "Logg in",
"tooltip.logout": "Logg ut",
"tooltip.admitFromLobby": "Slipp inn fra lobby",
"tooltip.lockRoom": "Lås møtet",
"tooltip.unLockRoom": "Lås opp møtet",
"tooltip.enterFullscreen": "Gå til fullskjerm",
"tooltip.leaveFullscreen": "Forlat fullskjerm",
"tooltip.lobby": "Vis lobby",
"tooltip.settings": "Vis innstillinger",
"label.yourName": "Ditt navn",
"label.newWindow": "Flytt til separat vindu",
"label.fullscreen": "Fullskjerm",
"label.openDrawer": "Åpne meny",
"label.leave": "Avslutt",
"label.chatInput": "Skriv melding...",
"label.chat": "Chat",
"label.filesharing": "Fildeling",
"label.participants": "Deltakere",
"label.shareFile": "Del fil",
"label.fileSharingUnsupported": "Fildeling ikke støttet",
"label.unknown": "Ukjent",
"label.democratic": "Demokratisk",
"label.filmstrip": "Filmstripe",
"label.low": "Lav",
"label.medium": "Medium",
"label.high": "Høy (HD)",
"label.veryHigh": "Veldig høy (FHD)",
"label.ultra": "Ultra (UHD)",
"label.close": "Lukk",
"settings.settings": "Innstillinger",
"settings.camera": "Kamera",
"settings.selectCamera": "Velg videoenhet",
"settings.cantSelectCamera": "Kan ikke velge videoenhet",
"settings.audio": "Lydenhet",
"settings.selectAudio": "Velg lydenhet",
"settings.cantSelectAudio": "Kan ikke velge lydenhet",
"settings.resolution": "Velg oppløsning",
"settings.layout": "Møtelayout",
"settings.selectRoomLayout": "Velg møtelayout",
"settings.advancedMode": "Avansert modus",
"filesharing.saveFileError": "Klarte ikke å lagre fil",
"filesharing.startingFileShare": "Starter fildeling",
"filesharing.successfulFileShare": "Filen ble delt",
"filesharing.unableToShare": "Klarte ikke å dele fil",
"filesharing.error": "Det skjedde noe feil med fildeling",
"filesharing.finished": "Fil ferdig lastet ned",
"filesharing.save": "Lagre",
"filesharing.sharedFile": "{displayName} delte en fil",
"filesharing.download": "Last ned",
"filesharing.missingSeeds": "Dersom dette tar lang til mangler det kanskje noen som kan dele denne filen. Prøv å spørre noen om å laste opp filen på nytt.",
"devices.devicesChanged": "Medieenhetene dine endret seg, du kan konfigurere enheter i innstillinger",
"device.audioUnsupported": "Lyd ikke støttet",
"device.activateAudio": "Aktiver lyd",
"device.muteAudio": "Demp lyd",
"device.unMuteAudio": "Aktiver lyd",
"device.videoUnsupported": "Video ikke støttet",
"device.startVideo": "Start video",
"device.stopVideo": "Stopp video",
"device.screenSharingUnsupported": "Skjermdeling ikke støttet",
"device.startScreenSharing": "Start skjermdeling",
"device.stopScreenSharing": "Stopp skjermdeling",
"devices.microphoneDisconnected": "Mikrofon koblet fra",
"devices.microphoneError": "Det skjedde noe feil med mikrofonen din",
"devices.microPhoneMute": "Dempet mikrofonen",
"devices.micophoneUnMute": "Aktiverte mikrofonen",
"devices.microphoneEnable": "Aktiverte mikrofonen",
"devices.microphoneMuteError": "Klarte ikke å dempe mikrofonen",
"devices.microphoneUnMuteError": "Klarte ikke å aktivere mikrofonen",
"devices.screenSharingDisconnected" : "Skjermdelingen forsvant",
"devices.screenSharingError": "Det skjedde noe feil med skjermdelingen din",
"devices.cameraDisconnected": "Kamera koblet fra",
"devices.cameraError": "Det skjedde noe feil med kameraet ditt"
}