Add translation with react-i18next
authorymh <ymh.work@gmail.com>
Thu, 08 Nov 2018 16:03:28 +0100
changeset 171 03334a31130a
parent 170 7da1d5137b0b
child 172 4b780ebbedc6
Add translation with react-i18next
client/package.json
client/src/components/CreateGroup.js
client/src/components/CreateSession.js
client/src/components/Login.js
client/src/components/Navbar.js
client/src/components/NavbarLogin.js
client/src/components/Note.js
client/src/components/NoteInput.js
client/src/components/NotesList.js
client/src/components/Session.js
client/src/components/SessionList.js
client/src/components/SlateEditor/index.js
client/src/i18n.js
client/src/index.js
client/src/locales/en/translation.json
client/src/locales/fr/translation.json
client/yarn.lock
--- a/client/package.json	Tue Nov 06 16:19:26 2018 +0100
+++ b/client/package.json	Thu Nov 08 16:03:28 2018 +0100
@@ -11,6 +11,7 @@
     "bootstrap": "^4.1.3",
     "connected-react-router": "^4.5.0",
     "i18next": "^11.6.0",
+    "i18next-browser-languagedetector": "^2.2.3",
     "immutable": "^3.8.2",
     "jquery": "^3.3.1",
     "jwt-decode": "^2.2.0",
@@ -19,10 +20,12 @@
     "moment": "^2.18.1",
     "npm": "^6.4.1",
     "popper.js": "^1.14.4",
+    "prop-types": "^15.6.2",
     "qs": "^6.5.0",
     "ramda": "^0.25.0",
     "react": "^16.5.2",
     "react-dom": "^16.5.2",
+    "react-i18next": "^8.3.6",
     "react-modal": "^3.5.1",
     "react-overlays": "^0.8.3",
     "react-portal": "^4.1.5",
--- a/client/src/components/CreateGroup.js	Tue Nov 06 16:19:26 2018 +0100
+++ b/client/src/components/CreateGroup.js	Thu Nov 08 16:03:28 2018 +0100
@@ -1,6 +1,7 @@
 import React, { Component } from 'react';
 import { connect } from 'react-redux';
 import { bindActionCreators } from 'redux';
+import { Trans } from 'react-i18next';
 import '../App.css';
 import Navbar from './Navbar';
 import * as authActions from '../actions/authActions';
@@ -79,22 +80,22 @@
               <div className="col-lg-6 offset-md-5">
                 <div className="panel-login panel panel-default d-flex justify-content-end">
                   <div className="card-header bg-primary w-50">
-                  <h5 className="text-center text-secondary font-weight-bold">Nouveau groupe</h5>
+                  <h5 className="text-center text-secondary font-weight-bold text-capitalize"><Trans i18nKey="create_group.new_group">nouveau groupe</Trans></h5>
                   <form className="mt-3" onSubmit={this.submit.bind(this)}>
                     <div className="form-group mb-2" /*validationState={ errorMessages && ('name' in errorMessages) ? 'error' : null }*/>
-                      <label className="col-from-label text-secondary font-weight-bold mt-2">Nom</label>
+                      <label className="col-from-label text-secondary font-weight-bold mt-2 text-capitalize"><Trans i18nKey="common.name">nom</Trans></label>
                       <input className="form-control bg-secondary text-primary border-0 w-100" type="text" onChange={this.handleInputChange} name="name" placeholder="Entrez un nom de groupe"/>
                       {/* { this.renderErrorMessage(errorMessages, 'name') } */}
                     </div>
                     <div className="form-group mb-2" /*validationState={ errorMessages && ('description' in errorMessages) ? 'error' : null }*/>
-                      <label className="col-form-label text-secondary font-weight-bold mt-2">Description</label>
+                      <label className="col-form-label text-secondary font-weight-bold mt-2 text-capitalize"><Trans i18nKey="common.description">description</Trans></label>
                       <textarea className="form-control bg-secondary text-primary border-0 w-100" type="textarea" onChange={this.handleInputChange} name="description" placeholder="Entrez une description de groupe"></textarea>
                       {/* { this.renderErrorMessage(errorMessages, 'description') } */}
                     </div>
                     {/* { this.renderNonFieldErrors(errorMessages) } */}
                     <div className="text-center">
-                    <button type="submit" value="Submit" className="btn btn-secondary btn-lg text-primary font-weight-bold m-3" disabled={okDisabled} onClick={this.submit}>Créer</button>
-                    <button type="button" className="btn btn-irinotes-form text-muted btn-lg font-weight-bold" onClick={this.cancel}>Annuler</button>
+                    <button type="submit" value="Submit" className="btn btn-secondary btn-lg text-primary font-weight-bold m-3 text-capitalize" disabled={okDisabled} onClick={this.submit}><Trans i18nKey="common.create">Créer</Trans></button>
+                    <button type="button" className="btn btn-irinotes-form text-muted btn-lg font-weight-bold text-capitalize" onClick={this.cancel}><Trans i18nKey="common.cancel">annuler</Trans></button>
                     </div>
                   </form>
                 </div>
--- a/client/src/components/CreateSession.js	Tue Nov 06 16:19:26 2018 +0100
+++ b/client/src/components/CreateSession.js	Thu Nov 08 16:03:28 2018 +0100
@@ -2,10 +2,11 @@
 import Modal from 'react-modal';
 import PropTypes from 'prop-types';
 import uuidV1 from 'uuid/v1';
+import { withNamespaces } from 'react-i18next';
 import '../App.css';
 import './CreateSession.css';
 
-export default class CreateSession extends Component {
+class CreateSession extends Component {
 
   static propTypes = {
     history: PropTypes.object.isRequired,
@@ -69,9 +70,10 @@
 
   render() {
 
+    const t = this.props.t;
     return (
       <div className="container-fluid">
-      <a className="nav-link" onClick={this.openSessionModal}>Nouvelle session</a>
+      <a className="nav-link" onClick={this.openSessionModal}>{t('create_session.new_session')}</a>
       <Modal
       className="Modal__Bootstrap modal-dialog ml-5 mt-5 fixed-top w-100"
       isOpen={this.state.modalIsOpen}
@@ -81,7 +83,7 @@
           <div className="modal-body mt-3">
             <form onSubmit={ e => { e.preventDefault() } }>
               <div className="form-group">
-                <label className="col-form-label text-secondary font-weight-bold pt-3">Titre</label>
+                <label className="col-form-label text-secondary font-weight-bold pt-3 text-capitalize">{t('common.title')}</label>
                 <input className="form-control text-primary w-100"
                   name="title"
                   onChange={ this.onChange }
@@ -90,7 +92,7 @@
                 />
               </div>
               <div className="form-group">
-                <label className="col-form-label text-secondary font-weight-bold pt-3 mt-3">Description</label>
+                <label className="col-form-label text-secondary font-weight-bold pt-3 mt-3 text-capitalize">{t('common.description')}</label>
                 <textarea className="form-control text-primary w-100"
                   type="textarea"
                   name="description"
@@ -99,14 +101,14 @@
                   ></textarea>
               </div>
               <div className="form-group">
-                <label className="col-form-label text-secondary font-weight-bold mt-5 ml-5" onClick={this.toggleProtocol}>Protocole de la prise de note {this.state.protocolOpen?<span className="material-icons protocol-toggle">&#xE313;</span>:<span className="material-icons protocol-toggle">&#xE315;</span>}</label>
+                <label className="col-form-label text-secondary font-weight-bold mt-5 ml-5" onClick={this.toggleProtocol}>{t('create_session.protocol')} {this.state.protocolOpen?<span className="material-icons protocol-toggle">&#xE313;</span>:<span className="material-icons protocol-toggle">&#xE315;</span>}</label>
                 <div className={ "collapse" + (this.state.protocolOpen?'in':'')} >
                   {/* <pre>{JSON.stringify(this.props.currentSession.protocol, null, 2)}</pre> */}
                   <pre className=" protocol text-secondary">{JSON.stringify(this.getGroupProtocol(), null, 2)}</pre>
                 </div>
               </div>
               <div className="text-center">
-              <button id="create-button" type="submit" className="btn btn-secondary btn-lg text-primary font-weight-bold m-3" onClick={this.createSession}>Commencer</button>
+              <button id="create-button" type="submit" className="btn btn-secondary btn-lg text-primary font-weight-bold m-3 text-capitalize" onClick={this.createSession}>{t('common.begin')}</button>
               </div>
             </form>
           </div>
@@ -116,3 +118,5 @@
     );
   }
 }
+
+export default withNamespaces()(CreateSession);
--- a/client/src/components/Login.js	Tue Nov 06 16:19:26 2018 +0100
+++ b/client/src/components/Login.js	Thu Nov 08 16:03:28 2018 +0100
@@ -1,6 +1,8 @@
 import React, { Component } from 'react';
 import { connect } from 'react-redux';
 import { bindActionCreators } from 'redux';
+import { Trans } from 'react-i18next';
+
 import '../App.css';
 import './Login.css';
 // import Navbar from './Navbar';
@@ -51,17 +53,26 @@
   }
 
   renderNonFieldErrors(errorMessages) {
-    console.log(errorMessages);
+
+    if (errorMessages && errorMessages.error) {
+      return (
+        <div className="alert alert-danger mt-4" role="alert">
+          <Trans i18nKey="login.login_error">Unable to log in.</Trans>
+        </div>
+      )
+    }
+
     const errors = R.path(['data','non_field_errors'], errorMessages);
     if (errors) {
       return (
-        <div className="alert alert-danger" role="alert">
+        <div className="alert alert-danger mt-4" role="alert">
         { errors.map((message, key) =>
-          <p key={ key }>{ message }</p>
+          <p key={ key }><Trans i18nKey="login.credentials_error">{ message }</Trans></p>
         ) }
         </div>
       )
     }
+
   }
 
   render() {
@@ -79,24 +90,24 @@
                   <h4 className="text-center card-title font-weight-bold text-lg">IRI Notes</h4>
                   <form className="pt-3 ml-5 pl-5" onSubmit={this.submit}>
                     <div className="form-group mb-2 ml-3 w-75" >
-                      <label className="col-form-label text-primary font-weight-bold mt-2">Nom d'utilisateur</label>
+                      <label className="col-form-label text-primary font-weight-bold mt-2 text-capitalize"><Trans i18nKey="common.username">Nom d'utilisateur</Trans></label>
                       <input className="form-control bg-irinotes-form border-0 text-muted" type="text" onChange={this.handleInputChange} name="username" />
                       {/* { this.renderErrorMessage(errorMessages, 'username') } */}
                     </div>
                     <div className="form-group mb-2 ml-3 w-75">
-                      <label className="col-form-label text-primary font-weight-bold mt-2">Mot de passe</label>
+                      <label className="col-form-label text-primary font-weight-bold mt-2 text-capitalize"><Trans i18nKey="common.password">Mot de passe</Trans></label>
                       <input className="form-control bg-irinotes-form border-0 text-muted" type="password" onChange={this.handleInputChange} name="password" />
                       {/* { this.renderErrorMessage(errorMessages, 'password') } */}
                     </div>
                     { this.renderNonFieldErrors(errorMessages) }
                     <div className="text-center mr-5 pr-5">
-                    <button type="submit" className="btn btn-primary btn-lg text-secondary font-weight-bold mt-3">Se connecter</button>
+                    <button type="submit" className="btn btn-primary btn-lg text-secondary font-weight-bold mt-3 text-capitalize"><Trans i18nKey='common.connect'>Se connecter</Trans></button>
                     </div>
                   </form>
                 </div>
               </div>
               <p className="text-center">
-                <a className="text-muted" href="/register" onClick={ this.onClickRegister }>Pas encore inscrit ? Créer un compte.</a>
+                <a className="text-muted" href="/register" onClick={ this.onClickRegister }><Trans i18nKey='login.registration_message'>Pas encore inscrit ? Créer un compte.</Trans></a>
               </p>
             </div>
           </div>
--- a/client/src/components/Navbar.js	Tue Nov 06 16:19:26 2018 +0100
+++ b/client/src/components/Navbar.js	Thu Nov 08 16:03:28 2018 +0100
@@ -5,6 +5,8 @@
 import { connect } from 'react-redux';
 import { withRouter } from 'react-router';
 import { bindActionCreators } from 'redux';
+import { withNamespaces, Trans } from 'react-i18next';
+
 // import logo from './logo.svg';
 import Modal from 'react-modal';
 import * as authActions from '../actions/authActions';
@@ -60,8 +62,7 @@
 const OffLineMessage = ({isAuthenticated}) => {
   if (!isAuthenticated) {
     return (
-        <span className="sticky-top text-warning text-right float-right mr-4 offline-message">Vous êtes en mode Offline. N'oubliez pas de
-        vous connecter ou de créer un compte pour sauvegarder vos sessions</span>
+        <span className="sticky-top text-warning text-right float-right mr-4 offline-message"><Trans i18nKey="navbar.offline_message"></Trans></span>
     );
   }
     return (
@@ -146,6 +147,7 @@
   }
 
   render() {
+    const t = this.props.t;
     return (
       <div>
       <nav className="navbar navbar-expand-lg navbar-light bg-primary sticky-top">
@@ -162,7 +164,7 @@
           <div className="collapse navbar-collapse text-center" id="navbarSupportedContent">
             <ul className="navbar-nav mr-auto">
               <li className="nav-item text-secondary">
-                <a className="nav-link " onClick={this.onClickSessions} href="/sessions">Sessions</a>
+                <a className="nav-link text-capitalize" onClick={this.onClickSessions} href="/sessions">{t('common.session', {count: 2})}</a>
                 {/* <CreateSession
                     history={this.props.history}
                     group={this.props.currentGroup}
@@ -197,12 +199,12 @@
               <span id="logout-close-modal-button" className="material-icons p-0 text-right" onClick={ this.handleModalCloseRequest }>close</span>
               <div className="modal-body text-center">
               <span className="material-icons modal-warning text-info pb-5">warning</span>
-                <p className="modal-text">
+                <p className="modal-text"><Trans i18nKey="navbar.logout_modal">
                   Certaines sessions n'ont pas encore été sauvegardées.
                   <br />
                   Si vous continuez, elles seront perdues.
-                </p>
-              <button type="button" className="btn btn-danger text-secondary font-weight-bold py-1 px-2 mb-3" id="logout-modal-button" onClick={ this.confirmLogout }>Confirmer</button>
+                  </Trans></p>
+              <button type="button" className="btn btn-danger text-secondary font-weight-bold py-1 px-2 mb-3 text-capitalize" id="logout-modal-button" onClick={ this.confirmLogout }>{t('common.confirm')}</button>
               </div>
             </div>
           </Modal>
@@ -239,4 +241,4 @@
   }
 }
 
-export default connect(mapStateToProps, mapDispatchToProps)(withRouter(AppNavbar));
+export default withNamespaces()(connect(mapStateToProps, mapDispatchToProps)(withRouter(AppNavbar)));
--- a/client/src/components/NavbarLogin.js	Tue Nov 06 16:19:26 2018 +0100
+++ b/client/src/components/NavbarLogin.js	Thu Nov 08 16:03:28 2018 +0100
@@ -1,5 +1,6 @@
 import React, { Component } from 'react';
 import ReactDOM from 'react-dom';
+import { Trans } from 'react-i18next';
 
 export default class LoginNav extends Component {
 
@@ -54,15 +55,15 @@
           &nbsp;<span className="caret"></span>
           </a>
           <div className={`dropdown-menu dropdown-menu-right bg-primary border-0 ${this.state.showDropdown?'show':''}`} aria-labelledby="navbarDropdown">
-            <a className="dropdown-item bg-primary text-secondary font-weight-bold" onClick={this.onClickSettings}>Paramètres</a>
-            <a className="dropdown-item bg-primary text-secondary font-weight-bold" onClick={onLogout}>Se déconnecter</a>
+            <a className="dropdown-item bg-primary text-secondary font-weight-bold text-capitalize" onClick={this.onClickSettings}><Trans i18nKey='common.parameters'>Paramètres</Trans></a>
+            <a className="dropdown-item bg-primary text-secondary font-weight-bold text-capitalize" onClick={onLogout}><Trans i18nKey='common.disconnect'>Se déconnecter</Trans></a>
           </div>
         </li>
       );
     } else {
       return (
           <li className="nav-item">
-          <a className="nav-link" onClick={this.onClickLogin} href="/login">Se connecter</a>
+          <a className="nav-link text-capitalize" onClick={this.onClickLogin} href="/login"><Trans i18nKey='common.connect'>Se connecter</Trans></a>
           </li>
       );
     }
--- a/client/src/components/Note.js	Tue Nov 06 16:19:26 2018 +0100
+++ b/client/src/components/Note.js	Thu Nov 08 16:03:28 2018 +0100
@@ -6,6 +6,18 @@
 
 class Note extends Component {
 
+  constructor(props) {
+    super(props);
+    this.editorInst = React.createRef();
+  }
+
+  get editor() {
+    if(this.editorInst && this.editorInst.current) {
+      return this.editorInst.current;
+    }
+    return null;
+  }
+
   onClickDelete = (e) => {
     e.preventDefault();
     e.stopPropagation();
@@ -15,10 +27,10 @@
 
   onClickButton = (e) => {
 
-    const plain = this.refs.editor.asPlain();
-    const raw = this.refs.editor.asRaw();
-    const html = this.refs.editor.asHtml();
-    const categories = this.refs.editor.asCategories();
+    const plain = this.editor.asPlain();
+    const raw = this.editor.asRaw();
+    const html = this.editor.asHtml();
+    const categories = this.editor.asCategories();
     // const marginComment = this.marginComment.value;
 
     const data = {
@@ -43,7 +55,7 @@
     if (this.props.isEditing) {
       return (
         <div className="note-content w-100 pl-2 pt-2">
-          <SlateEditor ref="editor"
+          <SlateEditor editorRef={this.editorInst}
             onButtonClick={ this.onClickButton }
             note={ this.props.note }
             annotationCategories={ this.props.annotationCategories } />
--- a/client/src/components/NoteInput.js	Tue Nov 06 16:19:26 2018 +0100
+++ b/client/src/components/NoteInput.js	Thu Nov 08 16:03:28 2018 +0100
@@ -6,6 +6,14 @@
 
 class NoteInput extends Component {
 
+  constructor(props) {
+    super(props);
+    this.editorInst = React.createRef();
+  }
+
+  get editor() {
+    return this.editorInst.current;
+  }
   state = {
     buttonDisabled: false,
     startedAt: null,
@@ -22,10 +30,10 @@
 
   onAddNoteClick = () => {
 
-    const plain = this.refs.editor.asPlain();
-    const raw = this.refs.editor.asRaw();
-    const html = this.refs.editor.asHtml();
-    const categories = this.refs.editor.asCategories();
+    const plain = this.editor.asPlain();
+    const raw = this.editor.asRaw();
+    const html = this.editor.asHtml();
+    const categories = this.editor.asCategories();
     // const marginComment = this.marginComment.value;
 
     this.props.addNote(this.props.session, {
@@ -40,8 +48,8 @@
     });
 
 
-    this.refs.editor.clear();
-    setTimeout(() => this.refs.editor.focus(), 250);
+    this.editor.clear();
+    setTimeout(() => this.editor.focus(), 250);
   }
 
   onCheckboxChange = (e) => {
@@ -49,8 +57,10 @@
   }
 
   componentDidMount() {
-    const text = this.refs.editor.asPlain();
-    this.setState({ buttonDisabled: text.length === 0 });
+    if(this.editor) {
+      const text = this.editor.asPlain();
+      this.setState({ buttonDisabled: text.length === 0 });
+    }
   }
 
   render() {
@@ -58,7 +68,7 @@
       <form>
         <div className="editor mb-3">
           <div className="editor-left sticky-bottom px-2">
-            <SlateEditor ref="editor"
+            <SlateEditor editorRef={this.editorInst}
               onChange={this.onEditorChange}
               onEnterKeyDown={this.onAddNoteClick}
               onButtonClick={this.onAddNoteClick}
--- a/client/src/components/NotesList.js	Tue Nov 06 16:19:26 2018 +0100
+++ b/client/src/components/NotesList.js	Thu Nov 08 16:03:28 2018 +0100
@@ -3,6 +3,7 @@
 import Modal  from 'react-modal';
 import Note from './Note';
 import './NoteList.css';
+import { Trans } from 'react-i18next';
 
 class NotesList extends Component {
   constructor(props) {
@@ -103,8 +104,8 @@
           <div id="delete-note-modal" className="modal-content">
             <span id="delete-note-close-modal-button" className="material-icons text-right" onClick={ this.handleModalCloseRequest }>close</span>
             <div className="modal-body text-center">
-            <span className="modal-text">Supprimer cette note ?</span>
-              <button type="button" className="btn btn-danger text-secondary font-weight-bold py-1 px-2 ml-3" id="delete-note-modal-button" onClick={ this.deleteNote }>Supprimer</button>
+            <span className="modal-text"><Trans i18nKey='notes_list.delete_note_msg'>Supprimer cette note ?</Trans></span>
+              <button type="button" className="btn btn-danger text-secondary font-weight-bold py-1 px-2 ml-3" id="delete-note-modal-button" onClick={ this.deleteNote }><Trans i18nKey='common.delete'>Supprimer</Trans></button>
             </div>
           </div>
         </Modal>
--- a/client/src/components/Session.js	Tue Nov 06 16:19:26 2018 +0100
+++ b/client/src/components/Session.js	Thu Nov 08 16:03:28 2018 +0100
@@ -1,6 +1,7 @@
 import React, { Component } from 'react';
 import { connect } from 'react-redux';
 import { bindActionCreators } from 'redux';
+import { Trans } from 'react-i18next';
 import '../App.css';
 import './Session.css';
 import Navbar from './Navbar';
@@ -44,7 +45,7 @@
     if (this.state.screenSummary === 0) {
       return (
         <div>
-        <a onClick={this.toggleScreenSummary} className ="text-primary">Afficher le protocole d'annotation</a>
+        <a onClick={this.toggleScreenSummary} className ="text-primary"><Trans i18nKey="session.protocol_display">Afficher le protocole d'annotation</Trans></a>
        <SessionSummary notes={this.props.notes} />
        </div>
       );
@@ -53,7 +54,7 @@
     if (this.state.screenSummary === 1) {
       return (
       <div>
-      <a onClick={this.toggleScreenSummary} className ="text-primary">Afficher le résumé de la session</a>
+      <a onClick={this.toggleScreenSummary} className ="text-primary"><Trans i18nKey="session.summary_display">Afficher le résumé de la session</Trans></a>
       <ProtocolSummary />
       </div>
       );
--- a/client/src/components/SessionList.js	Tue Nov 06 16:19:26 2018 +0100
+++ b/client/src/components/SessionList.js	Thu Nov 08 16:03:28 2018 +0100
@@ -1,6 +1,7 @@
 import React, { Component } from 'react';
 import { connect } from 'react-redux';
 import { bindActionCreators } from 'redux';
+import { Trans } from 'react-i18next';
 import Modal from 'react-modal';
 import moment from 'moment';
 import '../App.css';
@@ -67,7 +68,7 @@
 
       if (this.props.sessions.length === 0) {
       return (
-       <h1 className="text-primary text-center mt-5 pt-5">vous n'avez créé aucune session pour le moment</h1>
+       <h1 className="text-primary text-center mt-5 pt-5"><Trans i18nKey="session_list.no_session">vous n'avez créé aucune session pour le moment</Trans></h1>
       );
     }
   }
@@ -121,8 +122,8 @@
           <div id="delete-session-modal" className="modal-content">
             <span id="delete-session-close-modal-button" className="material-icons text-right" onClick={ this.handleModalCloseRequest }>close</span>
             <div className="modal-body text-center">
-            <span className="modal-text">Supprimer cette session ?</span>
-              <button type="button" className="btn btn-danger text-secondary font-weight-bold py-1 px-2 ml-3" id="delete-session-modal-button" onClick={ this.deleteSession }>Supprimer</button>
+            <span className="modal-text"><Trans i18nKey="session_list.delete_modal_message">Supprimer cette session ?</Trans></span>
+            <button type="button" className="btn btn-danger text-secondary font-weight-bold py-1 px-2 ml-3 text-capitalize" id="delete-session-modal-button" onClick={ this.deleteSession }><Trans i18nKey="common.delete">Supprimer</Trans></button>
             </div>
           </div>
         </Modal>
--- a/client/src/components/SlateEditor/index.js	Tue Nov 06 16:19:26 2018 +0100
+++ b/client/src/components/SlateEditor/index.js	Thu Nov 08 16:03:28 2018 +0100
@@ -1,11 +1,13 @@
-import { Value } from 'slate'
-import Plain from 'slate-plain-serializer'
-import { Editor } from 'slate-react'
-import React from 'react'
-import { Portal } from 'react-portal'
-import HtmlSerializer from './HtmlSerializer'
-import AnnotationPlugin from './AnnotationPlugin'
-import CategoriesTooltip from './CategoriesTooltip'
+import { Value } from 'slate';
+import Plain from 'slate-plain-serializer';
+import { Editor } from 'slate-react';
+import React from 'react';
+import { Portal } from 'react-portal';
+import { Trans, withNamespaces } from 'react-i18next';
+import * as R from 'ramda';
+import HtmlSerializer from './HtmlSerializer';
+import AnnotationPlugin from './AnnotationPlugin';
+import CategoriesTooltip from './CategoriesTooltip';
 import './SlateEditor.css';
 import { now } from '../../utils';
 import { defaultAnnotationsCategories } from '../../constants';
@@ -114,6 +116,8 @@
       isCheckboxChecked: false,
       enterKeyValue: 0,
     };
+
+    this.editor = React.createRef();
   }
 
   componentDidMount = () => {
@@ -155,10 +159,11 @@
       Object.assign(newState, { startedAt: now() });
     }
 
+    const oldState = R.clone(this.state);
     this.setState(newState)
 
     if (typeof this.props.onChange === 'function') {
-      this.props.onChange(newState);
+      this.props.onChange(R.clone(this.state), oldState, newState);
     }
   }
 
@@ -172,7 +177,7 @@
   hasMark = type => {
     const { value } = this.state
     return value.activeMarks.some(mark => mark.type === type)
-}
+  }
 
   /**
    * Check if the any of the currently selected blocks are of `type`.
@@ -184,7 +189,7 @@
   hasBlock = type => {
     const { value } = this.state
     return value.blocks.some(node => node.type === type)
-}
+  }
 
   asPlain = () => {
     return Plain.serialize(this.state.value);
@@ -213,10 +218,12 @@
   }
 
   focus = () => {
-    this.refs.editor.focus();
+    if(this.editor.current) {
+      this.editor.current.focus();
+    }
   }
 
-      /**
+  /**
    * When a mark button is clicked, toggle the current mark.
    *
    * @param {Event} e
@@ -516,17 +523,18 @@
     return (
       <div className="checkbox float-right">
         <label className="mr-2">
-          <input type="checkbox" checked={this.props.isChecked} onChange={this.onCheckboxChange} /><small className="text-muted ml-1"> Appuyer sur <kbd className="bg-irinotes-form text-muted ml-1">Entrée</kbd> pour ajouter une note</small>
+          <input type="checkbox" checked={this.props.isChecked} onChange={this.onCheckboxChange} /><small className="text-muted ml-1"><Trans i18nKey="slate_editor.press_enter_msg">Appuyer sur <kbd className="bg-irinotes-form text-muted ml-1">Entrée</kbd> pour ajouter une note</Trans></small>
         </label>
       </div>
     )
   }
 
   renderToolbarButtons = () => {
+    const t = this.props.t;
     return (
       <div>
-        <button type="button" id="btn-editor" className="btn btn-primary btn-sm text-secondary font-weight-bold float-right" disabled={this.props.isButtonDisabled} onClick={this.onButtonClick}>
-          { this.props.note ? 'Sauvegarder' : 'Ajouter' }
+        <button type="button" id="btn-editor" className="btn btn-primary btn-sm text-secondary font-weight-bold float-right text-capitalize" disabled={this.props.isButtonDisabled} onClick={this.onButtonClick}>
+          { this.props.note ? t('common.save') : t('common.add') }
         </button>
         { !this.props.note && this.renderToolbarCheckbox() }
       </div>
@@ -627,13 +635,14 @@
    */
 
   renderEditor = () => {
+    const t = this.props.t;
     return (
       <div className="editor-slatejs p-2">
         {this.renderHoveringMenu()}
         <Editor
-          ref="editor"
+          ref={this.editor}
           spellCheck
-          placeholder={'Votre espace de prise de note...'}
+          placeholder={t('slate_editor.placeholder')}
           schema={schema}
           plugins={plugins}
           value={this.state.value}
@@ -691,4 +700,12 @@
  * Export.
  */
 
-export default SlateEditor
+export default withNamespaces("", {
+  innerRef: (ref) => {
+    const editorRef = (ref && ref.props) ? ref.props.editorRef : null;
+    if(editorRef && editorRef.hasOwnProperty('current')) {
+      editorRef.current = ref;
+    }
+  }
+})(SlateEditor);
+// export default SlateEditor;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/i18n.js	Thu Nov 08 16:03:28 2018 +0100
@@ -0,0 +1,31 @@
+import i18n from "i18next";
+import { reactI18nextModule } from "react-i18next";
+import detector from "i18next-browser-languagedetector";
+
+import translationFR from "./locales/fr/translation.json";
+import translationEN from "./locales/en/translation.json";
+
+// the translations
+const resources = {
+  fr: {
+    translation: translationFR
+  },
+  en: {
+    translation: translationEN
+  }
+};
+
+i18n
+  .use(detector)
+  .use(reactI18nextModule) // passes i18n down to react-i18next
+  .init({
+    resources,
+    debug: true,
+    fallbackLng: "fr",
+
+    interpolation: {
+      escapeValue: false // react already safes from xss
+    }
+  });
+
+export default i18n;
--- a/client/src/index.js	Tue Nov 06 16:19:26 2018 +0100
+++ b/client/src/index.js	Thu Nov 08 16:03:28 2018 +0100
@@ -20,6 +20,8 @@
 import config from './config';
 import AuthenticatedRoute from './misc/AuthenticatedRoute';
 
+import './i18n';
+
 const history = createHistory({
     basename: config.basename
 });
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/locales/en/translation.json	Thu Nov 08 16:03:28 2018 +0100
@@ -0,0 +1,52 @@
+{
+    "create_session": {
+        "new_session": "New session",
+        "protocol": "Session protocol"
+    },
+    "create_group": {
+        "new_group": "new group"
+    },
+    "navbar": {
+        "offline_message": "you are offline. To save your sessions, do not forget to connect or to create a new account.",
+        "logout_modal": "Some sessions were not saved.<1></1>If you continue, they will be lost."
+    },
+    "login": {
+        "registration_message": "No account yet? Please register.",
+        "credentials_error": "Unable to log in with provided credentials.",
+        "login_error": "Unable to log in."
+    },
+    "session": {
+        "protocol_display": "Display the annotation protocol",
+        "summary_display": "Display the session summary"
+    },
+    "session_list": {
+        "no_session": "you didn't create any session.",
+        "delete_modal_message": "Delete this session?"
+    },
+    "notes_list": {
+        "delete_note_msg": "Delete this note?"
+    },
+    "slate_editor": {
+        "press_enter_msg": "Press <1>Enter</1> to add a note",
+        "placeholder": "Type your note here..."
+    },
+    "common": {
+        "title": "title",
+        "description": "description",
+        "begin": "begin",
+        "session": "session",
+        "session_plural": "sessions",
+        "confirm": "confirm",
+        "connect": "connect",
+        "disconnect": "disconnect",
+        "parameters": "parameters",
+        "username": "username",
+        "password": "password",
+        "delete": "delete",
+        "name": "name",
+        "create": "create",
+        "cancel": "cancel",
+        "add": "add",
+        "save": "save"
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/locales/fr/translation.json	Thu Nov 08 16:03:28 2018 +0100
@@ -0,0 +1,52 @@
+{
+    "create_session": {
+        "new_session": "Nouvelle session",
+        "protocol": "Protocole de la prise de note"
+    },
+    "create_group": {
+        "new_group": "nouveau groupe"
+    },
+    "navbar": {
+        "offline_message": "Vous êtes en mode Offline. N'oubliez pas de vous connecter ou de créer un compte pour sauvegarder vos sessions",
+        "logout_modal": "Certaines sessions n'ont pas encore été sauvegardées.<1></1>Si vous continuez, elles seront perdues."
+    },
+    "login": {
+        "registration_message": "Pas encore inscrit ? Créer un compte.",
+        "credentials_error": "Impossible de se connecter avec les identifiants fournis.",
+        "login_error": "Impossible de se connecter."
+    },
+    "notes_list": {
+        "delete_note_msg": "Supprimer cette note ?"
+    },
+    "session": {
+        "protocol_display": "Afficher le protocole d'annotation",
+        "summary_display": "Afficher le résumé de la session"
+    },
+    "session_list": {
+        "no_session": "vous n'avez créé aucune session pour le moment",
+        "delete_modal_message": "Supprimer cette session ?"
+    },
+    "slate_editor": {
+        "press_enter_msg": "Appuyer sur <1>Entrée</1> pour ajouter une note",
+        "placeholder": "Votre espace de prise de note..."
+    },
+    "common": {
+        "title": "titre",
+        "description": "description",
+        "begin" : "commencer",
+        "session": "session",
+        "session_plural": "sessions",
+        "confirm": "confirmer",
+        "connect": "se connecter",
+        "disconnect": "se déconnecter",
+        "parameters": "paramètres",
+        "username": "nom d'utilisateur",
+        "password": "mot de passe",
+        "delete": "supprimer",
+        "name": "nom",
+        "create": "créer",
+        "cancel": "annuler",
+        "add": "ajouter",
+        "save": "sauvegarder"
+    }
+}
--- a/client/yarn.lock	Tue Nov 06 16:19:26 2018 +0100
+++ b/client/yarn.lock	Thu Nov 08 16:03:28 2018 +0100
@@ -1957,6 +1957,11 @@
   resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
   integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
 
+core-js@^1.0.0:
+  version "1.2.7"
+  resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
+  integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=
+
 core-js@^2.4.0, core-js@^2.5.0:
   version "2.5.7"
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e"
@@ -2012,6 +2017,14 @@
     safe-buffer "^5.0.1"
     sha.js "^2.4.8"
 
+create-react-context@0.2.3:
+  version "0.2.3"
+  resolved "https://registry.yarnpkg.com/create-react-context/-/create-react-context-0.2.3.tgz#9ec140a6914a22ef04b8b09b7771de89567cb6f3"
+  integrity sha512-CQBmD0+QGgTaxDL3OX1IDXYqjkp2It4RIbcb99jS6AEg27Ga+a9G3JtK6SIu0HBwPLZlmwt9F7UwWA4Bn92Rag==
+  dependencies:
+    fbjs "^0.8.0"
+    gud "^1.0.0"
+
 cross-spawn@5.1.0, cross-spawn@^5.0.1, cross-spawn@^5.1.0:
   version "5.1.0"
   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
@@ -3202,6 +3215,19 @@
   dependencies:
     bser "^2.0.0"
 
+fbjs@^0.8.0:
+  version "0.8.17"
+  resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd"
+  integrity sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=
+  dependencies:
+    core-js "^1.0.0"
+    isomorphic-fetch "^2.1.1"
+    loose-envify "^1.0.0"
+    object-assign "^4.1.0"
+    promise "^7.1.1"
+    setimmediate "^1.0.5"
+    ua-parser-js "^0.7.18"
+
 figgy-pudding@^3.0.0, figgy-pudding@^3.1.0, figgy-pudding@^3.4.1, figgy-pudding@^3.5.1:
   version "3.5.1"
   resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790"
@@ -3666,6 +3692,11 @@
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
 
+gud@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/gud/-/gud-1.0.0.tgz#a489581b17e6a70beca9abe3ae57de7a499852c0"
+  integrity sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==
+
 gzip-size@3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-3.0.0.tgz#546188e9bdc337f673772f81660464b389dce520"
@@ -3801,6 +3832,13 @@
     minimalistic-assert "^1.0.0"
     minimalistic-crypto-utils "^1.0.1"
 
+hoist-non-react-statics@3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.0.1.tgz#fba3e7df0210eb9447757ca1a7cb607162f0a364"
+  integrity sha512-1kXwPsOi0OGQIZNVMPvgWJ9tSnGMiMfJdihqEzrPEXlHOBh9AAHXX/QYmAJTXztnz/K+PQ8ryCb4eGaN6HlGbQ==
+  dependencies:
+    react-is "^16.3.2"
+
 hoist-non-react-statics@^2.5.0:
   version "2.5.5"
   resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47"
@@ -3865,6 +3903,13 @@
     relateurl "0.2.x"
     uglify-js "3.4.x"
 
+html-parse-stringify2@2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/html-parse-stringify2/-/html-parse-stringify2-2.0.1.tgz#dc5670b7292ca158b7bc916c9a6735ac8872834a"
+  integrity sha1-3FZwtyksoVi3vJFsmmc1rIhyg0o=
+  dependencies:
+    void-elements "^2.0.1"
+
 html-webpack-plugin@2.29.0:
   version "2.29.0"
   resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-2.29.0.tgz#e987f421853d3b6938c8c4c8171842e5fd17af23"
@@ -3977,6 +4022,11 @@
   dependencies:
     ms "^2.0.0"
 
+i18next-browser-languagedetector@^2.2.3:
+  version "2.2.3"
+  resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-2.2.3.tgz#4196a9964b6d51b76254706a267ba746c9ca19de"
+  integrity sha512-sJZ2n9Vgax0vGer23hJMwyO3FRO7P0dq2DXZPXWE329g3snfJUcw+S24Mp3lqJaxL/0McDu4BD75ds6pzIfhhw==
+
 i18next@^11.6.0:
   version "11.10.2"
   resolved "https://registry.yarnpkg.com/i18next/-/i18next-11.10.2.tgz#e5f10346f6320ecf15595419926c25255381a56c"
@@ -4443,7 +4493,7 @@
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/is-root/-/is-root-1.0.0.tgz#07b6c233bc394cd9d02ba15c966bd6660d6342d5"
 
-is-stream@^1.0.0, is-stream@^1.1.0:
+is-stream@^1.0.0, is-stream@^1.0.1, is-stream@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
 
@@ -4524,6 +4574,14 @@
   resolved "https://registry.yarnpkg.com/isomorphic-base64/-/isomorphic-base64-1.0.2.tgz#f426aae82569ba8a4ec5ca73ad21a44ab1ee7803"
   integrity sha1-9Caq6CVpuopOxcpzrSGkSrHueAM=
 
+isomorphic-fetch@^2.1.1:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9"
+  integrity sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=
+  dependencies:
+    node-fetch "^1.0.1"
+    whatwg-fetch ">=0.10.0"
+
 isstream@~0.1.2:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
@@ -5721,6 +5779,14 @@
     json-parse-better-errors "^1.0.0"
     safe-buffer "^5.1.1"
 
+node-fetch@^1.0.1:
+  version "1.7.3"
+  resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
+  integrity sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==
+  dependencies:
+    encoding "^0.1.11"
+    is-stream "^1.0.1"
+
 node-forge@0.7.5:
   version "0.7.5"
   resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.5.tgz#6c152c345ce11c52f465c2abd957e8639cd674df"
@@ -6991,6 +7057,13 @@
   dependencies:
     asap "~2.0.3"
 
+promise@^7.1.1:
+  version "7.3.1"
+  resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
+  integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==
+  dependencies:
+    asap "~2.0.3"
+
 promzard@^0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/promzard/-/promzard-0.3.0.tgz#26a5d6ee8c7dee4cb12208305acfb93ba382a9ee"
@@ -7244,6 +7317,16 @@
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-4.0.1.tgz#417addb0814a90f3a7082eacba7cee588d00da89"
 
+react-i18next@^8.3.6:
+  version "8.3.6"
+  resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-8.3.6.tgz#0e36f0b3906ddaa3002dfabc24b047caf6b7fb88"
+  integrity sha512-yhEYij0ssBG+m9n8l3ir8mHq4y+IsGN6Nd2CHg0XRFxF+Jj/Vje4Vjm2j2MReAncgs4i7uTnCKWVMUZAuWmX+g==
+  dependencies:
+    "@babel/runtime" "^7.1.2"
+    create-react-context "0.2.3"
+    hoist-non-react-statics "3.0.1"
+    html-parse-stringify2 "2.0.1"
+
 react-immutable-proptypes@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/react-immutable-proptypes/-/react-immutable-proptypes-2.1.0.tgz#023d6f39bb15c97c071e9e60d00d136eac5fa0b4"
@@ -8041,7 +8124,7 @@
     is-plain-object "^2.0.3"
     split-string "^3.0.1"
 
-setimmediate@^1.0.4:
+setimmediate@^1.0.4, setimmediate@^1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
 
@@ -8875,6 +8958,11 @@
   version "0.0.6"
   resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
 
+ua-parser-js@^0.7.18:
+  version "0.7.19"
+  resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.19.tgz#94151be4c0a7fb1d001af7022fdaca4642659e4b"
+  integrity sha512-T3PVJ6uz8i0HzPxOF9SWzWAlfN/DavlpQqepn22xgve/5QecC+XMCAtmUNnY7C9StehaV6exjUCI801lOI7QlQ==
+
 uglify-js@3.4.x, uglify-js@^3.0.13, uglify-js@^3.1.4:
   version "3.4.9"
   resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3"
@@ -9122,6 +9210,11 @@
   dependencies:
     indexof "0.0.1"
 
+void-elements@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
+  integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=
+
 walker@~1.0.5:
   version "1.0.7"
   resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb"
@@ -9277,6 +9370,11 @@
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84"
 
+whatwg-fetch@>=0.10.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb"
+  integrity sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==
+
 whatwg-url@^4.3.0:
   version "4.8.0"
   resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-4.8.0.tgz#d2981aa9148c1e00a41c5a6131166ab4683bbcc0"