Browse Source

added i18n for translation

Skylsmoi 6 years ago
parent
commit
a291aa3366

+ 2 - 0
jsonserver/server.js View File

@@ -39,6 +39,8 @@ server.patch('/user', (req, res) => res.jsonp({lang: 'fr'}))
39 39
 //   res.jsonp('gg')
40 40
 // })
41 41
 
42
+server.get('/lang', (req, res) => res.jsonp(jsonDb.lang))
43
+
42 44
 server.post('/user/login', (req, res) => {
43 45
   if (req.body.login !== '' && req.body.password !== '') return res.jsonp(jsonDb.user_logged)
44 46
   else return res.jsonp('error')

+ 11 - 0
jsonserver/static_db.json View File

@@ -1,4 +1,15 @@
1 1
 {
2
+  "lang": [{
3
+    "id": "fr",
4
+    "name": "Français",
5
+    "src": "https://upload.wikimedia.org/wikipedia/commons/c/c3/Flag_of_France.svg",
6
+    "active": true
7
+  }, {
8
+    "id": "en",
9
+    "name": "English",
10
+    "src": "https://upload.wikimedia.org/wikipedia/commons/a/ae/Flag_of_the_United_Kingdom.svg",
11
+    "active": false
12
+  }],
2 13
   "user_logged": {
3 14
     "logged": true,
4 15
     "user": {

+ 2 - 0
package.json View File

@@ -26,10 +26,12 @@
26 26
     "classnames": "^2.2.5",
27 27
     "css-loader": "^0.28.7",
28 28
     "file-loader": "^1.1.5",
29
+    "i18next": "^10.5.0",
29 30
     "prop-types": "^15.6.0",
30 31
     "react": "^16.0.0",
31 32
     "react-animate-height": "^0.10.10",
32 33
     "react-dom": "^16.0.0",
34
+    "react-i18next": "^7.4.0",
33 35
     "react-redux": "^5.0.6",
34 36
     "react-router-dom": "^4.2.2",
35 37
     "redux": "^3.7.2",

+ 13 - 1
src/action-creator.async.js View File

@@ -1,5 +1,7 @@
1 1
 import { FETCH_CONFIG } from './helper.js'
2 2
 import {
3
+  LANG,
4
+  updateLangList,
3 5
   USER_LOGIN,
4 6
   USER_DATA,
5 7
   USER_CONNECTED,
@@ -59,6 +61,16 @@ const fetchWrapper = async ({url, param, actionName, dispatch, debug = false}) =
59 61
   return fetchResult
60 62
 }
61 63
 
64
+export const getLangList = () => async dispatch => {
65
+  const fetchGetLangList = await fetchWrapper({
66
+    url: `${FETCH_CONFIG.apiUrl}/lang`,
67
+    param: {...FETCH_CONFIG.header, method: 'GET'},
68
+    actionName: LANG,
69
+    dispatch
70
+  })
71
+  if (fetchGetLangList.status === 200) dispatch(updateLangList(fetchGetLangList.json))
72
+}
73
+
62 74
 export const userLogin = (login, password, rememberMe) => async dispatch => {
63 75
   const fetchUserLogin = await fetchWrapper({
64 76
     url: `${FETCH_CONFIG.apiUrl}/user/login`,
@@ -87,7 +99,7 @@ export const getIsUserConnected = () => async dispatch => {
87 99
   if (fetchUserLogged.status === 200) dispatch(updateUserConnected(fetchUserLogged.json))
88 100
 }
89 101
 
90
-export const updateUserLang = newLang => async dispatch => {
102
+export const updateUserLang = newLang => async dispatch => { // unused
91 103
   const fetchUpdateUserLang = await fetchWrapper({
92 104
     url: `${FETCH_CONFIG.apiUrl}/user`,
93 105
     param: {...FETCH_CONFIG.header, method: 'PATCH', body: JSON.stringify({lang: newLang})},

+ 5 - 1
src/action-creator.sync.js View File

@@ -10,7 +10,7 @@ export const updateWorkspaceData = workspace => ({ type: `Update/${WORKSPACE}`,
10 10
 
11 11
 export const WORKSPACE_LIST = 'WorkspaceList'
12 12
 export const updateWorkspaceListData = workspaceList => ({ type: `Update/${WORKSPACE_LIST}`, workspaceList })
13
-export const updateWorkspaceListIsOpen = (workspaceId, isOpen) => ({ type: `Update/${WORKSPACE_LIST}/isOpen`, workspaceId, isOpen })
13
+export const setWorkspaceListIsOpen = (workspaceId, isOpen) => ({ type: `Set/${WORKSPACE_LIST}/isOpen`, workspaceId, isOpen })
14 14
 
15 15
 export const FILE_CONTENT = 'FileContent'
16 16
 export const setActiveFileContent = file => ({ type: `Set/${FILE_CONTENT}/Active`, file })
@@ -18,3 +18,7 @@ export const hideActiveFileContent = () => ({ type: `Set/${FILE_CONTENT}/Hide` }
18 18
 
19 19
 export const APP_LIST = 'App/List'
20 20
 export const setAppList = appList => ({ type: `Set/${APP_LIST}`, appList })
21
+
22
+export const LANG = 'Lang'
23
+export const updateLangList = langList => ({ type: `Update/${LANG}`, langList })
24
+export const setLangActive = langId => ({ type: `Set/${LANG}/Active`, langId })

+ 1 - 2
src/component/Header/MenuActionListItem/DropdownLang.jsx View File

@@ -1,9 +1,8 @@
1 1
 import React from 'react'
2 2
 import PropTypes from 'prop-types'
3
-import flagFr from '../../../img/flagFr.png' // for default lang
4 3
 
5 4
 const DropdownLang = props => {
6
-  const activeLang = props.langList.find(l => l.active) || {id: 'fr', name: 'Français', src: flagFr, active: true}
5
+  const activeLang = props.langList.find(l => l.active) || {id: 'fr', name: 'Français', src: '', active: true}
7 6
   return (
8 7
     <li className='header__menu__rightside__itemlanguage'>
9 8
       <div className='header__menu__rightside__itemlanguage__languagedropdown dropdown'>

+ 3 - 2
src/component/Header/MenuActionListItem/Search.jsx View File

@@ -1,5 +1,6 @@
1 1
 import React from 'react'
2 2
 import PropTypes from 'prop-types'
3
+import { translate } from 'react-i18next'
3 4
 
4 5
 const Search = props => {
5 6
   return (
@@ -8,7 +9,7 @@ const Search = props => {
8 9
         <input
9 10
           type='text'
10 11
           className='search__input form-control'
11
-          placeholder='Rechercher un dossier ..'
12
+          placeholder={`${props.t('Header.Search')}...`}
12 13
           aria-describedby='headerInputSearch'
13 14
           onChange={props.onChangeInput}
14 15
         />
@@ -23,7 +24,7 @@ const Search = props => {
23 24
     </li>
24 25
   )
25 26
 }
26
-export default Search
27
+export default translate()(Search)
27 28
 
28 29
 Search.propTypes = {
29 30
   onChangeInput: PropTypes.func.isRequired,

+ 4 - 3
src/component/Workspace/FileItemHeader.jsx View File

@@ -1,4 +1,5 @@
1 1
 import React from 'react'
2
+import { translate } from 'react-i18next'
2 3
 
3 4
 const FileItemHeader = props => {
4 5
   return (
@@ -10,16 +11,16 @@ const FileItemHeader = props => {
10 11
       </div>
11 12
       <div className='col-8 col-sm-8 col-md-8 col-lg-8 col-xl-10'>
12 13
         <div className='file__header__name'>
13
-          Nom du document ou fichier
14
+          {props.t('FileItemHeader.document_name')}
14 15
         </div>
15 16
       </div>
16 17
       <div className='col-2 col-sm-2 col-md-2 col-lg-2 col-xl-1'>
17 18
         <div className='file__header__status'>
18
-          Statut
19
+          {props.t('FileItemHeader.status')}
19 20
         </div>
20 21
       </div>
21 22
     </div>
22 23
   )
23 24
 }
24 25
 
25
-export default FileItemHeader
26
+export default translate()(FileItemHeader)

+ 7 - 6
src/component/Workspace/Folder.jsx View File

@@ -1,27 +1,28 @@
1
-import React, { Component } from 'react'
1
+import React from 'react'
2
+import { translate } from 'react-i18next'
2 3
 import PropTypes from 'prop-types'
3 4
 import classnames from 'classnames'
4 5
 import FileItem from './FileItem.jsx'
5 6
 
6 7
 // @TODO set Folder as a component, state open will come from parent container (which will come from redux) // update: or not ?
7 8
 
8
-class Folder extends Component {
9
+class Folder extends React.Component {
9 10
   constructor (props) {
10 11
     super(props)
11 12
     this.state = {
12 13
       open: false
13 14
     }
14
-    this.handleClickToggleFolder = this.handleClickToggleFolder.bind(this)
15 15
   }
16 16
 
17 17
   handleClickToggleFolder = () => this.setState({open: !this.state.open})
18
+
18 19
   handleClickNewFile = e => {
19 20
     e.stopPropagation() // because we have a link inside a link (togler and newFile)
20 21
     console.log('new file') // @TODO
21 22
   }
22 23
 
23 24
   render () {
24
-    const { app, folderData: { title, content }, isLast } = this.props
25
+    const { app, folderData: { title, content }, isLast, t } = this.props
25 26
     return (
26 27
       <div className={classnames('folder', {'active': this.state.open, 'item-last': isLast})}>
27 28
         <div className='folder__header' onClick={this.handleClickToggleFolder}>
@@ -37,7 +38,7 @@ class Folder extends Component {
37 38
             </div>
38 39
             <div className='folder__header__name__addbtn' onClick={this.handleClickNewFile}>
39 40
               <div className='folder__header__name__addbtn__text btn btn-primary'>
40
-                créer ...
41
+                {t('Folder.create')} ...
41 42
               </div>
42 43
             </div>
43 44
           </div>
@@ -72,7 +73,7 @@ class Folder extends Component {
72 73
   }
73 74
 }
74 75
 
75
-export default Folder
76
+export default translate()(Folder)
76 77
 
77 78
 Folder.propTypes = {
78 79
   folderData: PropTypes.shape({

+ 12 - 27
src/container/Header.jsx View File

@@ -1,5 +1,6 @@
1 1
 import React from 'react'
2 2
 import { connect } from 'react-redux'
3
+import i18n from '../i18n.js'
3 4
 import Logo from '../component/Header/Logo.jsx'
4 5
 import NavbarToggler from '../component/Header/NavbarToggler.jsx'
5 6
 import MenuLinkList from '../component/Header/MenuLinkList.jsx'
@@ -9,14 +10,9 @@ import MenuActionListItemDropdownLang from '../component/Header/MenuActionListIt
9 10
 import MenuActionListItemHelp from '../component/Header/MenuActionListItem/Help.jsx'
10 11
 import MenuActionListItemMenuProfil from '../component/Header/MenuActionListItem/MenuProfil.jsx'
11 12
 import logoHeader from '../img/logoHeader.svg'
12
-import flagFr from '../img/flagFr.png'
13
-import flagEn from '../img/flagEn.png'
14
-// import { updateUserLang } from '../action-creator.async.js'
13
+import { setLangActive } from '../action-creator.sync.js'
15 14
 
16 15
 class Header extends React.Component {
17
-  // handleChangeLang = newLang => this.props.dispatch(updateUserLang(newLang))
18
-  // handleSubmitSearch = search => console.log('search submited : ', search)
19
-
20 16
   handleClickLogo = () => {}
21 17
 
22 18
   handleClickFeature = () => {}
@@ -26,7 +22,10 @@ class Header extends React.Component {
26 22
   handleChangeInput = e => this.setState({inputSearchValue: e.target.value})
27 23
   handleClickSubmit = () => {}
28 24
 
29
-  handleChangeLang = langId => {}
25
+  handleChangeLang = langId => {
26
+    this.props.dispatch(setLangActive(langId))
27
+    i18n.changeLanguage(langId)
28
+  }
30 29
 
31 30
   handleClickHelp = () => {}
32 31
 
@@ -34,28 +33,12 @@ class Header extends React.Component {
34 33
   handleClickLogout = () => {}
35 34
 
36 35
   render () {
37
-    const { user } = this.props
38
-    const langList = [{ // @TODO this should come from API
39
-      id: 'fr',
40
-      name: 'Français',
41
-      src: flagFr,
42
-      active: true
43
-    }, {
44
-      id: 'en',
45
-      name: 'English',
46
-      src: flagEn,
47
-      active: false
48
-    }]
49
-    // const userLogged = {
50
-    //   name: 'MrLapin',
51
-    //   avatar: 'https://photos-5.dropbox.com/t/2/AABmH38-9J0wawQdkSbEd757WfRA411L4h1eGtK0MLbWhA/' +
52
-    //   '12/14775898/png/32x32/1/_/1/2/avatar.png/ELyA_woY8SUgBygH/f3VzTEnU5OWqjkWwGyHOrJA2vYQKb3j' +
53
-    //   'S_zkvxpAJO0g?size=800x600&size_mode=3'
54
-    // }
36
+    const { lang, user } = this.props
55 37
 
56 38
     const HeaderWrapper = props => <header><nav className='header navbar navbar-expand-md navbar-light bg-light'>{props.children}</nav></header>
57 39
     const HeaderMenuRightWrapper = props => <div className='header__menu collapse navbar-collapse justify-content-end' id='navbarSupportedContent'>{props.children}</div>
58 40
     const MenuActionListWrapper = props => <ul className='header__menu__rightside'>{ props.children }</ul>
41
+
59 42
     return (
60 43
       <HeaderWrapper>
61 44
         <Logo logoSrc={logoHeader} onClickImg={this.handleClickLogo} />
@@ -73,7 +56,7 @@ class Header extends React.Component {
73 56
               onClickSubmit={this.handleClickSubmit}
74 57
             />
75 58
             <MenuActionListItemDropdownLang
76
-              langList={langList}
59
+              langList={lang}
77 60
               onChangeLang={this.handleChangeLang}
78 61
             />
79 62
             <MenuActionListItemHelp
@@ -90,4 +73,6 @@ class Header extends React.Component {
90 73
     )
91 74
   }
92 75
 }
93
-export default connect(({ user }) => ({ user }))(Header)
76
+
77
+const mapStateToProps = ({ lang, user }) => ({ lang, user })
78
+export default connect(mapStateToProps)(Header)

+ 2 - 2
src/container/Sidebar.jsx View File

@@ -3,7 +3,7 @@ import { connect } from 'react-redux'
3 3
 import { withRouter } from 'react-router'
4 4
 import WorkspaceListItem from '../component/Sidebar/WorkspaceListItem.jsx'
5 5
 import { getWorkspaceList } from '../action-creator.async.js'
6
-import { updateWorkspaceListIsOpen } from '../action-creator.sync.js'
6
+import { setWorkspaceListIsOpen } from '../action-creator.sync.js'
7 7
 import { PAGE_NAME } from '../helper.js'
8 8
 
9 9
 class Sidebar extends React.Component {
@@ -24,7 +24,7 @@ class Sidebar extends React.Component {
24 24
     user.id !== 0 && prevProps.user.id !== user.id && dispatch(getWorkspaceList(user.id))
25 25
   }
26 26
 
27
-  handleClickWorkspace = (wsId, newIsOpen) => this.props.dispatch(updateWorkspaceListIsOpen(wsId, newIsOpen))
27
+  handleClickWorkspace = (wsId, newIsOpen) => this.props.dispatch(setWorkspaceListIsOpen(wsId, newIsOpen))
28 28
 
29 29
   handleClickAllContent = wsId => {
30 30
     this.props.history.push(`${PAGE_NAME.WS_CONTENT}/${wsId}`)

+ 5 - 1
src/container/Tracim.jsx View File

@@ -14,11 +14,15 @@ import {
14 14
 } from 'react-router-dom'
15 15
 import PrivateRoute from './PrivateRoute.jsx'
16 16
 import { PAGE_NAME } from '../helper.js'
17
-import { getIsUserConnected } from '../action-creator.async.js'
17
+import {
18
+  getLangList,
19
+  getIsUserConnected
20
+} from '../action-creator.async.js'
18 21
 
19 22
 class Tracim extends React.Component {
20 23
   componentDidMount = () => {
21 24
     this.props.dispatch(getIsUserConnected())
25
+    this.props.dispatch(getLangList())
22 26
   }
23 27
 
24 28
   render () {

+ 2 - 0
src/css/Header.styl View File

@@ -53,6 +53,8 @@
53 53
               padding 0
54 54
               min-width 0
55 55
               .subdropdown
56
+                &__link
57
+                  cursor pointer
56 58
                 &__flag
57 59
                   width 25px
58 60
                   height 18px

+ 26 - 0
src/i18n.js View File

@@ -0,0 +1,26 @@
1
+import i18n from 'i18next'
2
+import { reactI18nextModule } from 'react-i18next'
3
+import fr from './translate/fr.js'
4
+import en from './translate/en.js'
5
+
6
+i18n
7
+  .use(reactI18nextModule)
8
+  .init({
9
+    fallbackLng: 'fr',
10
+    // have a common namespace used around the full app
11
+    ns: ['translation'], // namespace
12
+    defaultNS: 'translation',
13
+    debug: true,
14
+    // interpolation: {
15
+    //   escapeValue: false, // not needed for react!!
16
+    // },
17
+    react: {
18
+      wait: true
19
+    },
20
+    resources: {
21
+      en,
22
+      fr
23
+    }
24
+  })
25
+
26
+export default i18n

BIN
src/img/flagEn.png View File


BIN
src/img/flagFr.png View File


+ 5 - 1
src/index.js View File

@@ -4,13 +4,17 @@ import { Provider } from 'react-redux'
4 4
 import { store } from './store.js'
5 5
 import Tracim from './container/Tracim.jsx'
6 6
 import { BrowserRouter } from 'react-router-dom'
7
+import { I18nextProvider } from 'react-i18next'
8
+import i18n from './i18n.js'
7 9
 
8 10
 require('./css/index.styl')
9 11
 
10 12
 ReactDOM.render(
11 13
   <Provider store={store}>
12 14
     <BrowserRouter>
13
-      <Tracim />
15
+      <I18nextProvider i18n={i18n}>
16
+        <Tracim />
17
+      </I18nextProvider>
14 18
     </BrowserRouter>
15 19
   </Provider>
16 20
   , document.getElementById('content')

+ 16 - 0
src/reducer/lang.js View File

@@ -0,0 +1,16 @@
1
+import { LANG } from '../action-creator.sync.js'
2
+
3
+export function lang (state = [], action) {
4
+  switch (action.type) {
5
+    case `Update/${LANG}`:
6
+      return action.langList
7
+
8
+    case `Set/${LANG}/Active`:
9
+      return state.map(l => ({...l, active: l.id === action.langId}))
10
+
11
+    default:
12
+      return state
13
+  }
14
+}
15
+
16
+export default lang

+ 2 - 1
src/reducer/root.js View File

@@ -1,10 +1,11 @@
1 1
 import { combineReducers } from 'redux'
2
+import lang from './lang.js'
2 3
 import user from './user.js'
3 4
 import workspace from './workspace.js'
4 5
 import workspaceList from './workspaceList.js'
5 6
 import activeFileContent from './activeFileContent.js'
6 7
 import app from './app.js'
7 8
 
8
-const rootReducer = combineReducers({ user, workspace, workspaceList, activeFileContent, app })
9
+const rootReducer = combineReducers({ lang, user, workspace, workspaceList, activeFileContent, app })
9 10
 
10 11
 export default rootReducer

+ 1 - 1
src/reducer/workspaceList.js View File

@@ -10,7 +10,7 @@ export function workspaceList (state = [], action) {
10 10
         isOpen: false
11 11
       }))
12 12
 
13
-    case `Update/${WORKSPACE_LIST}/isOpen`:
13
+    case `Set/${WORKSPACE_LIST}/isOpen`:
14 14
       return state.map(ws => ws.id === action.workspaceId
15 15
         ? {...ws, isOpen: action.isOpen}
16 16
         : ws

+ 16 - 0
src/translate/en.js View File

@@ -0,0 +1,16 @@
1
+const en = {
2
+  translation: { // 'en' in the namespace 'translation'
3
+    Header: {
4
+      Search: 'Search'
5
+    },
6
+    FileItemHeader: {
7
+      document_name: "Document's name",
8
+      status: 'Status'
9
+    },
10
+    Folder: {
11
+      create: 'Create'
12
+    }
13
+  }
14
+}
15
+
16
+export default en

+ 16 - 0
src/translate/fr.js View File

@@ -0,0 +1,16 @@
1
+const fr = {
2
+  translation: { // 'fr' in the namespace 'translation'
3
+    Header: {
4
+      Search: 'Rechercher'
5
+    },
6
+    FileItemHeader: {
7
+      document_name: 'Nom du document',
8
+      status: 'Status'
9
+    },
10
+    Folder: {
11
+      create: 'Créer'
12
+    }
13
+  }
14
+}
15
+
16
+export default fr