Add translations foundation (#86)

This commit is contained in:
Alan Friedman 2019-06-15 20:02:27 -04:00 committed by GitHub
parent 9b473e2f65
commit 81fb927df7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 265 additions and 51 deletions

View File

@ -17,6 +17,7 @@
"clipboard": "^2.0.4", "clipboard": "^2.0.4",
"feather-icons": "^4.21.0", "feather-icons": "^4.21.0",
"jquery": "^3.4.1", "jquery": "^3.4.1",
"js-cookie": "^2.2.0",
"lodash": "^4.17.11", "lodash": "^4.17.11",
"moment": "^2.24.0", "moment": "^2.24.0",
"node-sass": "^4.12.0", "node-sass": "^4.12.0",

View File

@ -26,3 +26,7 @@ export const createUser = payload => async (dispatch) => {
export const clearActivities = () => async (dispatch) => { export const clearActivities = () => async (dispatch) => {
dispatch({ type: 'CLEAR_ACTIVITIES' }) dispatch({ type: 'CLEAR_ACTIVITIES' })
} }
export const setLanguage = payload => async (dispatch) => {
dispatch({type: 'CHANGE_LANGUAGE', payload});
}

View File

@ -254,7 +254,7 @@ export class Chat extends Component {
autoFocus autoFocus
className="chat" className="chat"
value={this.state.message} value={this.state.message}
placeholder="Type here" placeholder={this.props.translations.typePlaceholder}
onChange={this.handleInputChange.bind(this)} /> onChange={this.handleInputChange.bind(this)} />
<div className="input-controls"> <div className="input-controls">
<FileTransfer sendEncryptedMessage={this.props.sendEncryptedMessage} /> <FileTransfer sendEncryptedMessage={this.props.sendEncryptedMessage} />
@ -277,11 +277,13 @@ Chat.propTypes = {
clearActivities: PropTypes.func.isRequired, clearActivities: PropTypes.func.isRequired,
focusChat: PropTypes.bool.isRequired, focusChat: PropTypes.bool.isRequired,
scrollToBottom: PropTypes.func.isRequired, scrollToBottom: PropTypes.func.isRequired,
translations: PropTypes.object.isRequired,
} }
const mapStateToProps = state => ({ const mapStateToProps = state => ({
username: state.user.username, username: state.user.username,
userId: state.user.id, userId: state.user.id,
translations: state.app.translations,
}) })
const mapDispatchToProps = { const mapDispatchToProps = {

View File

@ -33,8 +33,10 @@ import {
toggleSocketConnected, toggleSocketConnected,
receiveUnencryptedMessage, receiveUnencryptedMessage,
sendUnencryptedMessage, sendUnencryptedMessage,
sendEncryptedMessage sendEncryptedMessage,
setLanguage
} from 'actions' } from 'actions'
import T from 'components/T'
import styles from './styles.module.scss' import styles from './styles.module.scss'
@ -177,22 +179,41 @@ class Home extends Component {
case 'USER_ENTER': case 'USER_ENTER':
return ( return (
<Notice> <Notice>
<div><Username username={activity.username} /> joined</div> <div>
<T data={{
username: <Username key={0} username={activity.username} />
}} path='userJoined'/>
</div>
</Notice> </Notice>
) )
case 'USER_EXIT': case 'USER_EXIT':
return ( return (
<Notice> <Notice>
<div><Username username={activity.username} /> left</div> <div>
<T data={{
username: <Username key={0} username={activity.username} />
}} path='userLeft'/>
</div>
</Notice> </Notice>
) )
case 'TOGGLE_LOCK_ROOM': case 'TOGGLE_LOCK_ROOM':
const lockedWord = activity.locked ? 'locked' : 'unlocked' if (activity.locked) {
return ( return (
<Notice> <Notice>
<div><Username username={activity.username} /> {lockedWord} the room</div> <div><T data={{
</Notice> username: <Username key={0} username={activity.username} />
) }} path='lockedRoom'/></div>
</Notice>
)
} else {
return (
<Notice>
<div><T data={{
username: <Username key={0} username={activity.username} />
}} path='unlockedRoom'/></div>
</Notice>
)
}
case 'NOTICE': case 'NOTICE':
return ( return (
<Notice> <Notice>
@ -202,7 +223,11 @@ class Home extends Component {
case 'CHANGE_USERNAME': case 'CHANGE_USERNAME':
return ( return (
<Notice> <Notice>
<div><Username username={activity.currentUsername} /> changed their name to <Username username={activity.newUsername} /></div> <div><T data={{
oldUsername: <Username key={0} username={activity.currentUsername} />,
newUsername: <Username key={1} username={activity.newUsername} />
}} path='nameChange'/>
</div>
</Notice> </Notice>
) )
case 'USER_ACTION': case 'USER_ACTION':
@ -215,7 +240,15 @@ class Home extends Component {
const downloadUrl = getObjectUrl(activity.encodedFile, activity.fileType) const downloadUrl = getObjectUrl(activity.encodedFile, activity.fileType)
return ( return (
<div> <div>
<Username username={activity.username} /> sent you a file. <a target="_blank" href={downloadUrl} rel="noopener noreferrer">Download {activity.fileName}</a> <T data={{
username: <Username key={0} username={activity.username} />,
}} path='userSentFile'/>&nbsp;
<a target="_blank" href={downloadUrl} rel="noopener noreferrer">
<T data={{
filename: activity.fileName,
}} path='downloadFile'/>
</a>
{this.getFileDisplay(activity)} {this.getFileDisplay(activity)}
</div> </div>
) )
@ -223,7 +256,11 @@ class Home extends Component {
const url = getObjectUrl(activity.encodedFile, activity.fileType) const url = getObjectUrl(activity.encodedFile, activity.fileType)
return ( return (
<Notice> <Notice>
<div>You sent <a target="_blank" href={url} rel="noopener noreferrer">{activity.fileName}</a></div> <div>
<T data={{
filename: <a key={0} target="_blank" href={url} rel="noopener noreferrer">{activity.fileName}</a>,
}} path='sentFile'/>&nbsp;
</div>
{this.getFileDisplay(activity)} {this.getFileDisplay(activity)}
</Notice> </Notice>
) )
@ -243,22 +280,22 @@ class Home extends Component {
case 'About': case 'About':
return { return {
component: <About roomId={this.props.roomId} />, component: <About roomId={this.props.roomId} />,
title: 'About', title: this.props.translations.aboutHeader,
} }
case 'Settings': case 'Settings':
return { return {
component: <Settings roomId={this.props.roomId} toggleSoundEnabled={this.props.toggleSoundEnabled} soundIsEnabled={this.props.soundIsEnabled} />, component: <Settings roomId={this.props.roomId} toggleSoundEnabled={this.props.toggleSoundEnabled} soundIsEnabled={this.props.soundIsEnabled} setLanguage={this.props.setLanguage} language={this.props.language} translations={this.props.translations} />,
title: 'Settings & Help', title: this.props.translations.settingsHeader,
} }
case 'Welcome': case 'Welcome':
return { return {
component: <Welcome roomId={this.props.roomId} close={this.props.closeModal} />, component: <Welcome roomId={this.props.roomId} close={this.props.closeModal} translations={this.props.translations} />,
title: 'Welcome to Darkwire v2.0', title: this.props.translations.welcomeHeader,
} }
case 'Room Locked': case 'Room Locked':
return { return {
component: <RoomLocked />, component: <RoomLocked modalContent={this.props.translations.lockedRoomHeader} />,
title: 'This room is locked', title: this.props.translations.lockedRoomHeader,
preventClose: true, preventClose: true,
} }
default: default:
@ -348,12 +385,13 @@ class Home extends Component {
openModal={this.props.openModal} openModal={this.props.openModal}
iAmOwner={this.props.iAmOwner} iAmOwner={this.props.iAmOwner}
userId={this.props.userId} userId={this.props.userId}
translations={this.props.translations}
/> />
</div> </div>
<div className="main-chat"> <div className="main-chat">
<div onClick={this.handleChatClick.bind(this)} className="message-stream h-100" ref={el => this.messageStream = el}> <div onClick={this.handleChatClick.bind(this)} className="message-stream h-100" ref={el => this.messageStream = el}>
<ul className="plain" ref={el => this.activitiesList = el}> <ul className="plain" ref={el => this.activitiesList = el}>
<li><p className={styles.tos}><button className='btn btn-link' onClick={this.props.openModal.bind(this, 'About')}> By using Darkwire, you are agreeing to our Acceptable Use Policy and Terms of Service.</button></p></li> <li><p className={styles.tos}><button className='btn btn-link' onClick={this.props.openModal.bind(this, 'About')}> <T path='agreement'/></button></p></li>
{this.props.activities.map((activity, index) => ( {this.props.activities.map((activity, index) => (
<li key={index} className={`activity-item ${activity.type}`}> <li key={index} className={`activity-item ${activity.type}`}>
{this.getActivityComponent(activity)} {this.getActivityComponent(activity)}
@ -450,6 +488,8 @@ const mapStateToProps = (state) => {
faviconCount: state.app.unreadMessageCount, faviconCount: state.app.unreadMessageCount,
soundIsEnabled: state.app.soundIsEnabled, soundIsEnabled: state.app.soundIsEnabled,
socketConnected: state.app.socketConnected, socketConnected: state.app.socketConnected,
language: state.app.language,
translations: state.app.translations,
} }
} }
@ -464,7 +504,8 @@ const mapDispatchToProps = {
toggleSocketConnected, toggleSocketConnected,
receiveUnencryptedMessage, receiveUnencryptedMessage,
sendUnencryptedMessage, sendUnencryptedMessage,
sendEncryptedMessage sendEncryptedMessage,
setLanguage
} }
export default connect( export default connect(

View File

@ -69,7 +69,7 @@ class Nav extends Component {
<button <button
data-toggle="tooltip" data-toggle="tooltip"
data-placement="bottom" data-placement="bottom"
title="Copied" title={this.props.translations.copyButtonTooltip}
data-clipboard-text={`${window.location.origin}/${this.props.roomId}`} data-clipboard-text={`${window.location.origin}/${this.props.roomId}`}
className="btn btn-plain btn-link clipboard-trigger room-id ellipsis"> className="btn btn-plain btn-link clipboard-trigger room-id ellipsis">
{`/${this.props.roomId}`} {`/${this.props.roomId}`}
@ -136,13 +136,14 @@ class Nav extends Component {
<div className="collapse navbar-collapse" id="navbarSupportedContent"> <div className="collapse navbar-collapse" id="navbarSupportedContent">
<ul className="navbar-nav ml-auto"> <ul className="navbar-nav ml-auto">
<li className="nav-item"> <li className="nav-item">
<button className="btn btn-plain nav-link" onClick={this.newRoom.bind(this)}target="blank"><PlusCircle /> <span>New Room</span></button> <button className="btn btn-plain nav-link" onClick={this.newRoom.bind(this)}target="blank"><PlusCircle /> <span>{this.props.translations.newRoomButton}</span></button>
</li>
<li className="
nav-item">
<button onClick={this.handleSettingsClick.bind(this)} className="btn btn-plain nav-link"><Settings /> <span>{this.props.translations.settingsButton}</span></button>
</li> </li>
<li className="nav-item"> <li className="nav-item">
<button onClick={this.handleSettingsClick.bind(this)} className="btn btn-plain nav-link"><Settings /> <span>Settings</span></button> <button onClick={this.handleAboutClick.bind(this)} className="btn btn-plain nav-link"><Info /> <span>{this.props.translations.aboutButton}</span></button>
</li>
<li className="nav-item">
<button onClick={this.handleAboutClick.bind(this)} className="btn btn-plain nav-link"><Info /> <span>About</span></button>
</li> </li>
</ul> </ul>
</div> </div>
@ -159,6 +160,7 @@ Nav.propTypes = {
toggleLockRoom: PropTypes.func.isRequired, toggleLockRoom: PropTypes.func.isRequired,
openModal: PropTypes.func.isRequired, openModal: PropTypes.func.isRequired,
iAmOwner: PropTypes.bool.isRequired, iAmOwner: PropTypes.bool.isRequired,
translations: PropTypes.object.isRequired,
} }
export default Nav export default Nav

View File

@ -46,7 +46,7 @@ class RoomLink extends Component {
data-toggle="tooltip" data-toggle="tooltip"
data-placement="bottom" data-placement="bottom"
data-clipboard-text={this.state.roomUrl} data-clipboard-text={this.state.roomUrl}
title="Copied!" title={this.props.translations.copyButtonTooltip}
> >
<Copy /> <Copy />
</button> </button>
@ -60,6 +60,7 @@ class RoomLink extends Component {
RoomLink.propTypes = { RoomLink.propTypes = {
roomId: PropTypes.string.isRequired, roomId: PropTypes.string.isRequired,
translations: PropTypes.object.isRequired,
} }
export default RoomLink export default RoomLink

View File

@ -4,7 +4,7 @@ export default class RoomLocked extends Component {
render() { render() {
return ( return (
<div> <div>
This room is locked. {this.props.modalContent}
</div> </div>
) )
} }

View File

@ -2,49 +2,67 @@ import React, { Component } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import RoomLink from 'components/RoomLink' import RoomLink from 'components/RoomLink'
import {styles} from './styles.module.scss' import {styles} from './styles.module.scss'
import Cookie from 'js-cookie';
import T from 'components/T'
class Settings extends Component { class Settings extends Component {
handleSoundToggle() { handleSoundToggle() {
this.props.toggleSoundEnabled(!this.props.soundIsEnabled) this.props.toggleSoundEnabled(!this.props.soundIsEnabled)
} }
handleLanguageChange(evt) {
const language = evt.target.value;
Cookie.set('language', language);
this.props.setLanguage(language);
}
render() { render() {
return ( return (
<div className={styles}> <div className={styles}>
<section> <section>
<h4>Sound</h4> <h4><T path='sound'/></h4>
<form> <form>
<div className="form-check"> <div className="form-check">
<label className="form-check-label" htmlFor="sound-control"> <label className="form-check-label" htmlFor="sound-control">
<input id="sound-control" onChange={this.handleSoundToggle.bind(this)} className="form-check-input" type="checkbox" checked={this.props.soundIsEnabled} /> <input id="sound-control" onChange={this.handleSoundToggle.bind(this)} className="form-check-input" type="checkbox" checked={this.props.soundIsEnabled} />
Sound <T path='sound'/>
</label> </label>
</div> </div>
</form> </form>
</section> </section>
<section> <section>
<h4 className='mb-3'>This room</h4> <h4 className='mb-3'><T path='copyRoomHeader'/></h4>
<RoomLink roomId={this.props.roomId} /> <RoomLink roomId={this.props.roomId} translations={this.props.translations} />
</section>
<section>
<h4 className='mb-3'><T path='languageDropdownHeader'/></h4>
<p><a href="https://github.com/darkwire/darkwire.io/tree/master/client/src/i18n" target="_blank">Help us translate Darkwire!</a></p>
<div className="form-group">
<select value={this.props.language} className="form-control" onChange={this.handleLanguageChange.bind(this)}>
<option value="en">English</option>
{/*<option value="fr">Français</option>*/}
</select>
</div>
</section>
<section>
<h4><T path='roomOwnerHeader'/></h4>
<p><T path='roomOwnerText'/></p>
</section> </section>
<section> <section>
<h4>Room Ownership</h4> <h4><T path='lockRoomHeader'/></h4>
<p>The person who created the room is the room owner and has special privileges, like the ability to lock and unlock the room. <p><T path='lockRoomText'/></p>
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.
</p>
</section> </section>
<section> <section>
<h4>Lock Room</h4> <h4><T path='slashCommandsHeader'/></h4>
<p>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.</p> <p><T path='slashCommandsText'/></p>
</section>
<section>
<h4>Slash Commands</h4>
<p>The following slash commands are available:</p>
<ul> <ul>
<li>/nick [username] <span className="text-muted">changes username</span></li> <li>/nick [username] <span className="text-muted"><T path='slashCommandsBullets.0'/></span></li>
<li>/me [action] <span className="text-muted">performs an action</span></li> <li>/me [action] <span className="text-muted"><T path='slashCommandsBullets.1'/></span></li>
<li>/clear <span className="text-muted">clears your message history</span></li> <li>/clear <span className="text-muted"><T path='slashCommandsBullets.2'/></span></li>
<li>/help <span className="text-muted">lists all commands</span></li> <li>/help <span className="text-muted"><T path='slashCommandsBullets.3'/></span></li>
</ul> </ul>
</section> </section>
</div> </div>
@ -56,6 +74,8 @@ Settings.propTypes = {
soundIsEnabled: PropTypes.bool.isRequired, soundIsEnabled: PropTypes.bool.isRequired,
toggleSoundEnabled: PropTypes.func.isRequired, toggleSoundEnabled: PropTypes.func.isRequired,
roomId: PropTypes.string.isRequired, roomId: PropTypes.string.isRequired,
setLanguage: PropTypes.func.isRequired,
translations: PropTypes.object.isRequired,
} }
export default Settings export default Settings

View File

@ -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 <span>{string}</span>
}
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)

View File

@ -28,9 +28,9 @@ class Welcome extends Component {
</div> </div>
<br /> <br />
<p className='mb-2'>Others can join this room using the following URL:</p> <p className='mb-2'>Others can join this room using the following URL:</p>
<RoomLink roomId={this.props.roomId} /> <RoomLink roomId={this.props.roomId} translations={this.props.translations} />
<div className="react-modal-footer"> <div className="react-modal-footer">
<button className="btn btn-primary btn-lg" onClick={this.props.close}>Ok</button> <button className="btn btn-primary btn-lg" onClick={this.props.close}>{this.props.translations.welcomeModalCTA}</button>
</div> </div>
</div> </div>
) )
@ -40,6 +40,7 @@ class Welcome extends Component {
Welcome.propTypes = { Welcome.propTypes = {
roomId: PropTypes.string.isRequired, roomId: PropTypes.string.isRequired,
close: PropTypes.func.isRequired, close: PropTypes.func.isRequired,
translations: PropTypes.object.isRequired,
} }
export default Welcome export default Welcome

37
client/src/i18n/en.json Normal file
View File

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

37
client/src/i18n/fr.json Normal file
View File

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

11
client/src/i18n/index.js Normal file
View File

@ -0,0 +1,11 @@
import en from './en';
import fr from './fr';
const languagesMap = {
en,
fr,
}
export function getTranslations(language) {
return languagesMap[language];
}

View File

@ -1,3 +1,8 @@
import Cookie from 'js-cookie';
import {getTranslations} from 'i18n';
const language = Cookie.get('language') || 'en';
const initialState = { const initialState = {
modalComponent: null, modalComponent: null,
scrolledToBottom: true, scrolledToBottom: true,
@ -5,6 +10,8 @@ const initialState = {
unreadMessageCount: 0, unreadMessageCount: 0,
soundIsEnabled: true, soundIsEnabled: true,
socketConnected: false, socketConnected: false,
language,
translations: getTranslations(language)
} }
const app = (state = initialState, action) => { const app = (state = initialState, action) => {
@ -45,6 +52,12 @@ const app = (state = initialState, action) => {
...state, ...state,
socketConnected: action.payload, socketConnected: action.payload,
} }
case 'CHANGE_LANGUAGE':
return {
...state,
language: action.payload,
translations: getTranslations(action.payload)
}
default: default:
return state return state
} }

View File

@ -6000,6 +6000,11 @@ js-base64@^2.1.8:
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.5.1.tgz#1efa39ef2c5f7980bb1784ade4a8af2de3291121" resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.5.1.tgz#1efa39ef2c5f7980bb1784ade4a8af2de3291121"
integrity sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw== 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: js-levenshtein@^1.1.3:
version "1.1.6" version "1.1.6"
resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d" resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d"