diff --git a/client/package.json b/client/package.json index edd24c3..1bb1782 100644 --- a/client/package.json +++ b/client/package.json @@ -17,6 +17,7 @@ "clipboard": "^2.0.4", "feather-icons": "^4.21.0", "jquery": "^3.4.1", + "js-cookie": "^2.2.0", "lodash": "^4.17.11", "moment": "^2.24.0", "node-sass": "^4.12.0", diff --git a/client/src/actions/app.js b/client/src/actions/app.js index 198dd8f..ec00465 100644 --- a/client/src/actions/app.js +++ b/client/src/actions/app.js @@ -26,3 +26,7 @@ export const createUser = payload => async (dispatch) => { export const clearActivities = () => async (dispatch) => { dispatch({ type: 'CLEAR_ACTIVITIES' }) } + +export const setLanguage = payload => async (dispatch) => { + dispatch({type: 'CHANGE_LANGUAGE', payload}); +} diff --git a/client/src/components/Chat/index.js b/client/src/components/Chat/index.js index 2e94008..62f7ed0 100644 --- a/client/src/components/Chat/index.js +++ b/client/src/components/Chat/index.js @@ -254,7 +254,7 @@ export class Chat extends Component { autoFocus className="chat" value={this.state.message} - placeholder="Type here" + placeholder={this.props.translations.typePlaceholder} onChange={this.handleInputChange.bind(this)} />
@@ -277,11 +277,13 @@ Chat.propTypes = { clearActivities: PropTypes.func.isRequired, focusChat: PropTypes.bool.isRequired, scrollToBottom: PropTypes.func.isRequired, + translations: PropTypes.object.isRequired, } const mapStateToProps = state => ({ username: state.user.username, userId: state.user.id, + translations: state.app.translations, }) const mapDispatchToProps = { diff --git a/client/src/components/Home/index.js b/client/src/components/Home/index.js index ce33d89..b4ee551 100644 --- a/client/src/components/Home/index.js +++ b/client/src/components/Home/index.js @@ -33,8 +33,10 @@ import { toggleSocketConnected, receiveUnencryptedMessage, sendUnencryptedMessage, - sendEncryptedMessage + sendEncryptedMessage, + setLanguage } from 'actions' +import T from 'components/T' import styles from './styles.module.scss' @@ -177,22 +179,41 @@ class Home extends Component { case 'USER_ENTER': return ( -
joined
+
+ + }} path='userJoined'/> +
) case 'USER_EXIT': return ( -
left
+
+ + }} path='userLeft'/> +
) case 'TOGGLE_LOCK_ROOM': - const lockedWord = activity.locked ? 'locked' : 'unlocked' - return ( - -
{lockedWord} the room
-
- ) + if (activity.locked) { + return ( + +
+ }} path='lockedRoom'/>
+
+ ) + } else { + return ( + +
+ }} path='unlockedRoom'/>
+
+ ) + } case 'NOTICE': return ( @@ -202,7 +223,11 @@ class Home extends Component { case 'CHANGE_USERNAME': return ( -
changed their name to
+
, + newUsername: + }} path='nameChange'/> +
) case 'USER_ACTION': @@ -215,7 +240,15 @@ class Home extends Component { const downloadUrl = getObjectUrl(activity.encodedFile, activity.fileType) return (
- sent you a file. Download {activity.fileName} + , + }} path='userSentFile'/>  + + + + {this.getFileDisplay(activity)}
) @@ -223,7 +256,11 @@ class Home extends Component { const url = getObjectUrl(activity.encodedFile, activity.fileType) return ( -
You sent {activity.fileName}
+
+ {activity.fileName}, + }} path='sentFile'/>  +
{this.getFileDisplay(activity)}
) @@ -243,22 +280,22 @@ class Home extends Component { case 'About': return { component: , - title: 'About', + title: this.props.translations.aboutHeader, } case 'Settings': return { - component: , - title: 'Settings & Help', + component: , + title: this.props.translations.settingsHeader, } case 'Welcome': return { - component: , - title: 'Welcome to Darkwire v2.0', + component: , + title: this.props.translations.welcomeHeader, } case 'Room Locked': return { - component: , - title: 'This room is locked', + component: , + title: this.props.translations.lockedRoomHeader, preventClose: true, } default: @@ -348,12 +385,13 @@ class Home extends Component { openModal={this.props.openModal} iAmOwner={this.props.iAmOwner} userId={this.props.userId} + translations={this.props.translations} />
this.messageStream = el}>
@@ -159,6 +160,7 @@ Nav.propTypes = { toggleLockRoom: PropTypes.func.isRequired, openModal: PropTypes.func.isRequired, iAmOwner: PropTypes.bool.isRequired, + translations: PropTypes.object.isRequired, } export default Nav diff --git a/client/src/components/RoomLink/index.js b/client/src/components/RoomLink/index.js index b5cd2b0..3b3c3af 100644 --- a/client/src/components/RoomLink/index.js +++ b/client/src/components/RoomLink/index.js @@ -46,7 +46,7 @@ class RoomLink extends Component { data-toggle="tooltip" data-placement="bottom" data-clipboard-text={this.state.roomUrl} - title="Copied!" + title={this.props.translations.copyButtonTooltip} > @@ -60,6 +60,7 @@ class RoomLink extends Component { RoomLink.propTypes = { roomId: PropTypes.string.isRequired, + translations: PropTypes.object.isRequired, } export default RoomLink diff --git a/client/src/components/RoomLocked/index.js b/client/src/components/RoomLocked/index.js index 17b39b5..6175e04 100644 --- a/client/src/components/RoomLocked/index.js +++ b/client/src/components/RoomLocked/index.js @@ -4,7 +4,7 @@ export default class RoomLocked extends Component { render() { return (
- This room is locked. + {this.props.modalContent}
) } diff --git a/client/src/components/Settings/index.js b/client/src/components/Settings/index.js index 0c7dc75..ea89b5c 100644 --- a/client/src/components/Settings/index.js +++ b/client/src/components/Settings/index.js @@ -2,49 +2,67 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import RoomLink from 'components/RoomLink' import {styles} from './styles.module.scss' +import Cookie from 'js-cookie'; +import T from 'components/T' class Settings extends Component { + handleSoundToggle() { this.props.toggleSoundEnabled(!this.props.soundIsEnabled) } + handleLanguageChange(evt) { + const language = evt.target.value; + Cookie.set('language', language); + this.props.setLanguage(language); + } + render() { return (
-

Sound

+

-

This room

- +

+ +
+ +
+

+

Help us translate Darkwire!

+
+ +
+
+ +
+

+

-

Room Ownership

-

The person who created the room is the room owner and has special privileges, like the ability to lock and unlock the room. - If the owner leaves the room, the second person to join assumes ownership. If they leave, the third person becomes owner, and so on. - The room owner has a star icon next to their username in the participants dropdown. -

+

+

-

Lock Room

-

If you are the room owner, you can lock and unlock the room by clicking the lock icon in the nav bar. When a room is locked, no other participants will be able to join.

-
-
-

Slash Commands

-

The following slash commands are available:

+

+

    -
  • /nick [username] changes username
  • -
  • /me [action] performs an action
  • -
  • /clear clears your message history
  • -
  • /help lists all commands
  • +
  • /nick [username]
  • +
  • /me [action]
  • +
  • /clear
  • +
  • /help
@@ -56,6 +74,8 @@ Settings.propTypes = { soundIsEnabled: PropTypes.bool.isRequired, toggleSoundEnabled: PropTypes.func.isRequired, roomId: PropTypes.string.isRequired, + setLanguage: PropTypes.func.isRequired, + translations: PropTypes.object.isRequired, } export default Settings diff --git a/client/src/components/T/index.js b/client/src/components/T/index.js new file mode 100644 index 0000000..6976973 --- /dev/null +++ b/client/src/components/T/index.js @@ -0,0 +1,39 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import {getTranslations} from 'i18n'; +import _ from 'lodash'; +import { connect } from 'react-redux' + +const regex = /{(.*?)}/g; + +class T extends Component { + render() { + const t = getTranslations(this.props.language); + let string = _.get(t, this.props.path, '').split(regex); + if (this.props.data) { + string = string.map(word => { + if (this.props.data[word]) { + return this.props.data[word]; + } + return word; + }); + return {string} + } + return string; + } + + t() { + const t = getTranslations(this.props.language); + return _.get(t, this.props.path); + } +} + +T.propTypes = { + path: PropTypes.string.isRequired, +} + +export default connect( + (state, ownProps) => ({ + language: state.app.language, + }), +)(T) \ No newline at end of file diff --git a/client/src/components/Welcome/index.js b/client/src/components/Welcome/index.js index 131c149..6a861f6 100644 --- a/client/src/components/Welcome/index.js +++ b/client/src/components/Welcome/index.js @@ -28,9 +28,9 @@ class Welcome extends Component {

Others can join this room using the following URL:

- +
- +
) @@ -40,6 +40,7 @@ class Welcome extends Component { Welcome.propTypes = { roomId: PropTypes.string.isRequired, close: PropTypes.func.isRequired, + translations: PropTypes.object.isRequired, } export default Welcome diff --git a/client/src/i18n/en.json b/client/src/i18n/en.json new file mode 100644 index 0000000..7fd36d1 --- /dev/null +++ b/client/src/i18n/en.json @@ -0,0 +1,37 @@ +{ + "newRoomButton": "New Room", + "lockedRoom": "{username} locked the room", + "unlockedRoom": "{username} unlocked the room", + "agreement": "By using Darkwire, you are agreeing to our Acceptable Use Policy and Terms of Service", + "typePlaceholder": "Type here", + "aboutButton": "About", + "settingsButton": "Settings", + "settings": "Settings", + "aboutHeader": "About", + "copyButtonTooltip": "Copied", + "welcomeHeader": "Welcome to Darkwire v2.0", + "sentFile": "You sent {filename}", + "userJoined": "{username} joined", + "userLeft": "{username} left", + "userSentFile": "{username} sent you a file.", + "downloadFile": "Download {filename}", + "nameChange": "{oldUsername} changed their name to {newUsername}", + "settingsHeader": "Settings & Help", + "copyRoomHeader": "This room", + "languageDropdownHeader": "Language", + "roomOwnerHeader": "Room Ownership", + "roomOwnerText": "The person who created the room is the room owner and has special privileges, like the ability to lock and unlock the room. If the owner leaves the room, the second person to join assumes ownership. If they leave, the third person becomes owner, and so on. The room owner has a star icon next to their username in the participants dropdown.", + "lockRoomHeader": "Lock Room", + "lockRoomText": "If you are the room owner, you can lock and unlock the room by clicking the lock icon in the nav bar. When a room is locked, no other participants will be able to join.", + "slashCommandsHeader": "Slash Commands", + "slashCommandsText": "The following slash commands are available:", + "slashCommandsBullets": [ + "changes username", + "performs an action", + "clears your message history", + "lists all commands" + ], + "sound": "Sound", + "welcomeModalCTA": "Ok", + "lockedRoomHeader": "This room is locked" +} \ No newline at end of file diff --git a/client/src/i18n/fr.json b/client/src/i18n/fr.json new file mode 100644 index 0000000..fdf2eaf --- /dev/null +++ b/client/src/i18n/fr.json @@ -0,0 +1,37 @@ +{ + "newRoomButton": "fr_New Room", + "lockedRoom": "fr_{username} locked the room", + "unlockedRoom": "fr_{username} unlocked the room", + "agreement": "fr_By using Darkwire, you are agreeing to our Acceptable Use Policy and Terms of Service", + "typePlaceholder": "fr_Type here", + "aboutButton": "fr_About", + "settingsButton": "fr_Settings", + "settings": "fr_Settings", + "aboutHeader": "fr_About", + "copyButtonTooltip": "fr_Copied", + "welcomeHeader": "fr_Welcome to Darkwire v2.0", + "sentFile": "fr_You sent {filename}", + "userJoined": "fr_{username} joined", + "userLeft": "fr_{username} left", + "userSentFile": "fr_{username} sent you a file.", + "downloadFile": "fr_Download {filename}", + "nameChange": "fr_{oldUsername} changed their name to {newUsername}", + "settingsHeader": "fr_Settings & Help", + "copyRoomHeader": "fr_This room", + "languageDropdownHeader": "fr_Language", + "roomOwnerHeader": "fr_Room Ownership", + "roomOwnerText": "fr_The person who created the room is the room owner and has special privileges, like the ability to lock and unlock the room. If the owner leaves the room, the second person to join assumes ownership. If they leave, the third person becomes owner, and so on. The room owner has a star icon next to their username in the participants dropdown.", + "lockRoomHeader": "fr_Lock Room", + "lockRoomText": "fr_If you are the room owner, you can lock and unlock the room by clicking the lock icon in the nav bar. When a room is locked, no other participants will be able to join.", + "slashCommandsHeader": "fr_Slash Commands", + "slashCommandsText": "fr_The following slash commands are available:", + "slashCommandsBullets": [ + "fr_changes username", + "fr_performs an action", + "fr_clears your message history", + "fr_lists all commands" + ], + "sound": "fr_Sound", + "welcomeModalCTA": "fr_Ok", + "lockedRoomHeader": "fr_This room is locked" +} \ No newline at end of file diff --git a/client/src/i18n/index.js b/client/src/i18n/index.js new file mode 100644 index 0000000..ab37ac2 --- /dev/null +++ b/client/src/i18n/index.js @@ -0,0 +1,11 @@ +import en from './en'; +import fr from './fr'; + +const languagesMap = { + en, + fr, +} + +export function getTranslations(language) { + return languagesMap[language]; +} \ No newline at end of file diff --git a/client/src/reducers/app.js b/client/src/reducers/app.js index 703e9b8..8823468 100644 --- a/client/src/reducers/app.js +++ b/client/src/reducers/app.js @@ -1,3 +1,8 @@ +import Cookie from 'js-cookie'; +import {getTranslations} from 'i18n'; + +const language = Cookie.get('language') || 'en'; + const initialState = { modalComponent: null, scrolledToBottom: true, @@ -5,6 +10,8 @@ const initialState = { unreadMessageCount: 0, soundIsEnabled: true, socketConnected: false, + language, + translations: getTranslations(language) } const app = (state = initialState, action) => { @@ -45,6 +52,12 @@ const app = (state = initialState, action) => { ...state, socketConnected: action.payload, } + case 'CHANGE_LANGUAGE': + return { + ...state, + language: action.payload, + translations: getTranslations(action.payload) + } default: return state } diff --git a/client/yarn.lock b/client/yarn.lock index 3a8fdb1..7d1a84d 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -6000,6 +6000,11 @@ js-base64@^2.1.8: resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.5.1.tgz#1efa39ef2c5f7980bb1784ade4a8af2de3291121" integrity sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw== +js-cookie@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.0.tgz#1b2c279a6eece380a12168b92485265b35b1effb" + integrity sha1-Gywnmm7s44ChIWi5JIUmWzWx7/s= + js-levenshtein@^1.1.3: version "1.1.6" resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d"