diff --git a/babel.config.js b/babel.config.js index fbe35bd0..a1ebc1b4 100644 --- a/babel.config.js +++ b/babel.config.js @@ -19,7 +19,8 @@ module.exports = { '@pages': './src/views/pages', '@core': './src/core', '@components': './src/views/components', - '@styles': './src/styles' + '@styles': './src/styles', + '@i18n': './src/i18n' } } ], diff --git a/package.json b/package.json index 28b85565..897d0435 100644 --- a/package.json +++ b/package.json @@ -96,6 +96,7 @@ "fetch-cheerio-object": "^1.3.0", "front-matter": "^4.0.2", "fs-extra": "^11.1.1", + "i18next": "^23.8.2", "jsonwebtoken": "^9.0.1", "knex": "^0.95.15", "markdown-it": "^12.3.2", @@ -114,6 +115,7 @@ "react": "^17.0.2", "react-dom": "^17.0.2", "react-helmet": "^6.1.0", + "react-i18next": "^14.0.5", "react-redux": "^7.2.9", "react-router": "^5.3.4", "redux-saga": "^1.2.3", diff --git a/src/core/app/actions.js b/src/core/app/actions.js index e178832a..c671f3ee 100644 --- a/src/core/app/actions.js +++ b/src/core/app/actions.js @@ -1,11 +1,12 @@ export const appActions = { INIT_APP: 'INIT_APP', - init: ({ token, key }) => ({ + init: ({ token, key, locale }) => ({ type: appActions.INIT_APP, payload: { token, - key + key, + locale } }) } diff --git a/src/core/i18n/actions.js b/src/core/i18n/actions.js new file mode 100644 index 00000000..4e968253 --- /dev/null +++ b/src/core/i18n/actions.js @@ -0,0 +1,10 @@ +export const i18nActions = { + CHANGE_LOCALE: 'CHANGE_LOCALE', + + change_locale: (locale) => ({ + type: i18nActions.CHANGE_LOCALE, + payload: { + locale + } + }) +} diff --git a/src/core/i18n/index.js b/src/core/i18n/index.js new file mode 100644 index 00000000..9cb917b1 --- /dev/null +++ b/src/core/i18n/index.js @@ -0,0 +1,3 @@ +export { i18nActions } from './actions' +export { i18nReducer } from './reducer' +export { i18nSagas } from './sagas' diff --git a/src/core/i18n/reducer.js b/src/core/i18n/reducer.js new file mode 100644 index 00000000..1891169f --- /dev/null +++ b/src/core/i18n/reducer.js @@ -0,0 +1,17 @@ +import { Record } from 'immutable' + +import { i18nActions } from './actions' + +const initialState = new Record({ + locale: 'en' +}) + +export function i18nReducer(state = initialState(), { payload, type }) { + switch (type) { + case i18nActions.CHANGE_LOCALE: + return state.set('locale', payload.locale) + + default: + return state + } +} diff --git a/src/core/i18n/sagas.js b/src/core/i18n/sagas.js new file mode 100644 index 00000000..06135e04 --- /dev/null +++ b/src/core/i18n/sagas.js @@ -0,0 +1,37 @@ +import { takeLatest, put, fork } from 'redux-saga/effects' +import i18n from 'i18next' + +import { localStorageAdapter } from '@core/utils' +import { appActions } from '@core/app/actions' +import { i18nActions } from './actions' + +export function* init({ payload }) { + if (payload.locale) { + yield put(i18nActions.change_locale(payload.locale)) + } + + // TODO detect user locale +} + +export function ChangeLocale({ payload }) { + localStorageAdapter.setItem('locale', payload.locale) + i18n.changeLanguage(payload.locale) +} + +//= ==================================== +// WATCHERS +// ------------------------------------- + +export function* watchInitApp() { + yield takeLatest(appActions.INIT_APP, init) +} + +export function* watchChangeLocale() { + yield takeLatest(i18nActions.CHANGE_LOCALE, ChangeLocale) +} + +//= ==================================== +// ROOT +// ------------------------------------- + +export const i18nSagas = [fork(watchInitApp), fork(watchChangeLocale)] diff --git a/src/core/reducers.js b/src/core/reducers.js index 6c7a6b4e..6304fc70 100644 --- a/src/core/reducers.js +++ b/src/core/reducers.js @@ -13,6 +13,7 @@ import { networkReducer } from './network' import { notificationReducer } from './notifications' import { postsReducer } from './posts' import { postlistsReducer } from './postlists' +import { i18nReducer } from './i18n' const rootReducer = (history) => combineReducers({ @@ -28,7 +29,8 @@ const rootReducer = (history) => network: networkReducer, notification: notificationReducer, posts: postsReducer, - postlists: postlistsReducer + postlists: postlistsReducer, + i18n: i18nReducer }) export default rootReducer diff --git a/src/core/sagas.js b/src/core/sagas.js index 237db53f..ebaf63c7 100644 --- a/src/core/sagas.js +++ b/src/core/sagas.js @@ -10,6 +10,7 @@ import { githubIssuesSagas } from './github-issues' import { ledgerSagas } from './ledger' import { networkSagas } from './network' import { postlistSagas } from './postlists' +import { i18nSagas } from './i18n' export default function* rootSage() { yield all([ @@ -22,6 +23,7 @@ export default function* rootSage() { ...githubIssuesSagas, ...ledgerSagas, ...networkSagas, - ...postlistSagas + ...postlistSagas, + ...i18nSagas ]) } diff --git a/src/i18n/index.js b/src/i18n/index.js new file mode 100644 index 00000000..6fa8f984 --- /dev/null +++ b/src/i18n/index.js @@ -0,0 +1,28 @@ +import { initReactI18next } from 'react-i18next' +import i18n from 'i18next' + +i18n.use(initReactI18next).init({ + // detection + debug: true, + resources: { + en: { + translation: { + menu: { + introduction: 'Introduction' + } + } + }, + es: { + translation: { + menu: { + introduction: 'Introducción' + } + } + } + }, + lng: 'en', + fallbackLng: 'en' + // supportedLngs +}) + +export default i18n diff --git a/src/index.js b/src/index.js index a587c113..28baef2f 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,7 @@ // Needed for redux-saga es6 generator support import '@babel/polyfill' +import '@i18n' import React from 'react' import { render } from 'react-dom' diff --git a/src/views/components/app/app.js b/src/views/components/app/app.js index 5ae1193f..61685dc5 100644 --- a/src/views/components/app/app.js +++ b/src/views/components/app/app.js @@ -15,7 +15,8 @@ export default class App extends React.Component { async componentDidMount() { const token = await localStorageAdapter.getItem('token') const key = await localStorageAdapter.getItem('key') - this.props.init({ token, key }) + const locale = await localStorageAdapter.getItem('locale') + this.props.init({ token, key, locale }) this.props.getRepresentatives() this.props.getNetworkStats() this.props.getGithubEvents() diff --git a/src/views/components/change-locale/change-locale.js b/src/views/components/change-locale/change-locale.js new file mode 100644 index 00000000..cf9b3f29 --- /dev/null +++ b/src/views/components/change-locale/change-locale.js @@ -0,0 +1,32 @@ +import React from 'react' +import PropTypes from 'prop-types' +import FormControl from '@material-ui/core/FormControl' +import Select from '@material-ui/core/Select' +import MenuItem from '@material-ui/core/MenuItem' + +import './change-locale.styl' + +export default function ChangeLocale({ change_locale, locale }) { + return ( + + + + ) +} + +ChangeLocale.propTypes = { + change_locale: PropTypes.func.isRequired, + locale: PropTypes.string.isRequired +} diff --git a/src/views/components/change-locale/change-locale.styl b/src/views/components/change-locale/change-locale.styl new file mode 100644 index 00000000..e69de29b diff --git a/src/views/components/change-locale/index.js b/src/views/components/change-locale/index.js new file mode 100644 index 00000000..5d78615d --- /dev/null +++ b/src/views/components/change-locale/index.js @@ -0,0 +1,17 @@ +import { connect } from 'react-redux' +import { createSelector } from 'reselect' + +import { i18nActions } from '@core/i18n' + +import ChangeLocale from './change-locale' + +const mapStateToProps = createSelector( + (state) => state.getIn(['i18n', 'locale']), + (locale) => ({ locale }) +) + +const mapDispatchToProps = { + change_locale: i18nActions.change_locale +} + +export default connect(mapStateToProps, mapDispatchToProps)(ChangeLocale) diff --git a/src/views/components/menu/menu.js b/src/views/components/menu/menu.js index d597a19d..7ef291c5 100644 --- a/src/views/components/menu/menu.js +++ b/src/views/components/menu/menu.js @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useState } from 'react' import { NavLink } from 'react-router-dom' import PropTypes from 'prop-types' import SwipeableDrawer from '@material-ui/core/SwipeableDrawer' @@ -6,19 +6,22 @@ import CloseIcon from '@material-ui/icons/Close' import SpeedDial from '@material-ui/lab/SpeedDial' import SpeedDialAction from '@material-ui/lab/SpeedDialAction' import HomeIcon from '@material-ui/icons/Home' +import { useTranslation } from 'react-i18next' import SearchBar from '@components/search-bar' import history from '@core/history' +import ChangeLocale from '@components/change-locale' import './menu.styl' const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) function MenuSections() { + const { t } = useTranslation() return (
-
Introduction
+
{t('menu.introduction')}
Overview Advantages @@ -115,74 +118,69 @@ function MenuSections() { ) } -export default class Menu extends React.Component { - constructor(props) { - super(props) - this.state = { - open: false - } - } +export default function Menu(props) { + const [open, setOpen] = useState(false) - handleOpen = () => this.setState({ open: true }) - handleClose = () => this.setState({ open: false }) - handleClick = () => this.setState({ open: !this.state.open }) - handleHomeClick = () => history.push('/') + const handleOpen = () => setOpen(true) + const handleClose = () => setOpen(false) + const handleClick = () => setOpen(!open) + const handleHomeClick = () => history.push('/') - render() { - const { hide, hideSearch, hide_speed_dial } = this.props - const isHome = history.location.pathname === '/' - const isMobile = window.innerWidth < 750 + const { hide, hideSearch, hide_speed_dial } = props + const isHome = history.location.pathname === '/' + const isMobile = window.innerWidth < 750 - return ( -
- - - - {!hide_speed_dial && ( - - } - openIcon={}> - {!isHome && ( - } - tooltipTitle='Home' - tooltipPlacement={isMobile ? 'left' : 'right'} - onClick={this.handleHomeClick} - /> - )} - - )} -
- {isHome ? ( -
NANO
- ) : ( - - NANO - + return ( +
+ + + + + {!hide_speed_dial && ( + + } + openIcon={}> + {!isHome && ( + } + tooltipTitle='Home' + tooltipPlacement={isMobile ? 'left' : 'right'} + onClick={handleHomeClick} + /> )} - {!hideSearch && } - {!hide && } -
+ + )} +
+ {isHome ? ( +
NANO
+ ) : ( + + NANO + + )} + {!hideSearch && } + {!hide && } +
- ) - } +
+ ) } Menu.propTypes = { diff --git a/yarn.lock b/yarn.lock index 4be8228a..c52b47b6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2451,6 +2451,15 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9": + version: 7.23.9 + resolution: "@babel/runtime@npm:7.23.9" + dependencies: + regenerator-runtime: ^0.14.0 + checksum: 6bbebe8d27c0c2dd275d1ac197fc1a6c00e18dab68cc7aaff0adc3195b45862bae9c4cc58975629004b0213955b2ed91e99eccb3d9b39cabea246c657323d667 + languageName: node + linkType: hard + "@babel/template@npm:^7.22.15, @babel/template@npm:^7.23.9": version: 7.23.9 resolution: "@babel/template@npm:7.23.9" @@ -9646,6 +9655,15 @@ __metadata: languageName: node linkType: hard +"html-parse-stringify@npm:^3.0.1": + version: 3.0.1 + resolution: "html-parse-stringify@npm:3.0.1" + dependencies: + void-elements: 3.1.0 + checksum: 334fdebd4b5c355dba8e95284cead6f62bf865a2359da2759b039db58c805646350016d2017875718bc3c4b9bf81a0d11be5ee0cf4774a3a5a7b97cde21cfd67 + languageName: node + linkType: hard + "html-webpack-plugin@npm:^5.5.3": version: 5.5.3 resolution: "html-webpack-plugin@npm:5.5.3" @@ -9866,6 +9884,15 @@ __metadata: languageName: node linkType: hard +"i18next@npm:^23.8.2": + version: 23.8.2 + resolution: "i18next@npm:23.8.2" + dependencies: + "@babel/runtime": ^7.23.2 + checksum: c20e68c6c216bfcedc16d8d8b1ee545423e26e84ace36b699f936ec8cf1b4df8ee2ae093e7a3e444a9cb5931ca76698ae1a80d31691aa4153bcc804394e0019e + languageName: node + linkType: hard + "iconv-lite@npm:0.4.24": version: 0.4.24 resolution: "iconv-lite@npm:0.4.24" @@ -14641,6 +14668,24 @@ __metadata: languageName: node linkType: hard +"react-i18next@npm:^14.0.5": + version: 14.0.5 + resolution: "react-i18next@npm:14.0.5" + dependencies: + "@babel/runtime": ^7.23.9 + html-parse-stringify: ^3.0.1 + peerDependencies: + i18next: ">= 23.2.3" + react: ">= 16.8.0" + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + checksum: 54fe5ffd887d633852ea7e82e98b6ef057facf45dec512469dd0e43ae64d9450d2bafe7c85c87fd650cf6dad1bf81727a89fba23af0508b7a806153d459fc5cc + languageName: node + linkType: hard + "react-immutable-proptypes@npm:^2.2.0": version: 2.2.0 resolution: "react-immutable-proptypes@npm:2.2.0" @@ -15013,6 +15058,13 @@ __metadata: languageName: node linkType: hard +"regenerator-runtime@npm:^0.14.0": + version: 0.14.1 + resolution: "regenerator-runtime@npm:0.14.1" + checksum: 9f57c93277b5585d3c83b0cf76be47b473ae8c6d9142a46ce8b0291a04bb2cf902059f0f8445dcabb3fb7378e5fe4bb4ea1e008876343d42e46d3b484534ce38 + languageName: node + linkType: hard + "regenerator-transform@npm:^0.15.2": version: 0.15.2 resolution: "regenerator-transform@npm:0.15.2" @@ -15421,6 +15473,7 @@ __metadata: html-inline-script-webpack-plugin: ^2.0.3 html-loader: ^2.1.2 html-webpack-plugin: ^5.5.3 + i18next: ^23.8.2 image-webpack-loader: ^7.0.1 ipfs-deploy: ^12.0.1 jsonwebtoken: ^9.0.1 @@ -15446,6 +15499,7 @@ __metadata: react: ^17.0.2 react-dom: ^17.0.2 react-helmet: ^6.1.0 + react-i18next: ^14.0.5 react-immutable-proptypes: ^2.2.0 react-redux: ^7.2.9 react-router: ^5.3.4 @@ -17529,6 +17583,13 @@ __metadata: languageName: node linkType: hard +"void-elements@npm:3.1.0": + version: 3.1.0 + resolution: "void-elements@npm:3.1.0" + checksum: 0390f818107fa8fce55bb0a5c3f661056001c1d5a2a48c28d582d4d847347c2ab5b7f8272314cac58acf62345126b6b09bea623a185935f6b1c3bbce0dfd7f7f + languageName: node + linkType: hard + "watchpack@npm:^2.4.0": version: 2.4.0 resolution: "watchpack@npm:2.4.0"