Skip to content

Commit

Permalink
feat: add internationalization support
Browse files Browse the repository at this point in the history
  • Loading branch information
mistakia committed Feb 18, 2024
1 parent fa11e6e commit 14bae33
Show file tree
Hide file tree
Showing 19 changed files with 510 additions and 183 deletions.
3 changes: 3 additions & 0 deletions api/server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ api.use((req, res, next) => {
const resourcesPath = path.join(__dirname, '..', 'resources')
api.use('/resources', serveStatic(resourcesPath))

const localesPath = path.join(__dirname, '..', 'locales')
api.use('/locales', serveStatic(localesPath))

const dataPath = path.join(__dirname, '..', 'data')
api.use('/data', serveStatic(dataPath))

Expand Down
5 changes: 5 additions & 0 deletions locales/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"menu": {
"introduction": "Introduction"
}
}
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@
"fetch-cheerio-object": "^1.3.0",
"front-matter": "^4.0.2",
"fs-extra": "^11.1.1",
"i18next": "^23.8.2",
"i18next-http-backend": "^2.4.3",
"jsonwebtoken": "^9.0.1",
"knex": "^0.95.15",
"markdown-it": "^12.3.2",
Expand All @@ -114,6 +116,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",
Expand All @@ -140,6 +143,7 @@
"compression-webpack-plugin": "^10.0.0",
"concurrently": "^8.2.0",
"copy-text-to-clipboard": "^3.2.0",
"copy-webpack-plugin": "^12.0.2",
"cross-env": "^7.0.3",
"css-loader": "6.8.1",
"deepmerge": "4.3.1",
Expand Down
5 changes: 3 additions & 2 deletions src/core/app/actions.js
Original file line number Diff line number Diff line change
@@ -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
}
})
}
10 changes: 10 additions & 0 deletions src/core/i18n/actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const i18nActions = {
CHANGE_LOCALE: 'CHANGE_LOCALE',

change_locale: (locale) => ({
type: i18nActions.CHANGE_LOCALE,
payload: {
locale
}
})
}
24 changes: 24 additions & 0 deletions src/core/i18n/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { initReactI18next } from 'react-i18next'
import i18n from 'i18next'
import HttpBackend from 'i18next-http-backend'

export { i18nActions } from './actions'
export { i18nReducer } from './reducer'
export { i18nSagas } from './sagas'

i18n
.use(HttpBackend)
.use(initReactI18next)
.init({
// detection
debug: true,
backend: {
// Configuration options for the backend plugin
loadPath: '/locales/{{lng}}.json' // Path to the translation files
},
lng: 'en',
fallbackLng: 'en'
// supportedLngs
})

export default i18n
17 changes: 17 additions & 0 deletions src/core/i18n/reducer.js
Original file line number Diff line number Diff line change
@@ -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
}
}
37 changes: 37 additions & 0 deletions src/core/i18n/sagas.js
Original file line number Diff line number Diff line change
@@ -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)]
4 changes: 3 additions & 1 deletion src/core/reducers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -28,7 +29,8 @@ const rootReducer = (history) =>
network: networkReducer,
notification: notificationReducer,
posts: postsReducer,
postlists: postlistsReducer
postlists: postlistsReducer,
i18n: i18nReducer
})

export default rootReducer
4 changes: 3 additions & 1 deletion src/core/sagas.js
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand All @@ -22,6 +23,7 @@ export default function* rootSage() {
...githubIssuesSagas,
...ledgerSagas,
...networkSagas,
...postlistSagas
...postlistSagas,
...i18nSagas
])
}
3 changes: 2 additions & 1 deletion src/views/components/app/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
32 changes: 32 additions & 0 deletions src/views/components/change-locale/change-locale.js
Original file line number Diff line number Diff line change
@@ -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 (
<FormControl className='change-locale'>
<Select
labelId='change-locale'
id='change-locale'
value={locale}
onChange={(event) => change_locale(event.target.value)}>
<MenuItem value='en'>English</MenuItem>
<MenuItem value='es'>Español</MenuItem>
<MenuItem value='fr'>Français</MenuItem>
<MenuItem value='it'>Italiano</MenuItem>
<MenuItem value='de'>Deutsch</MenuItem>
<MenuItem value='nl'>Nederlands</MenuItem>
<MenuItem value='ru'>Русский</MenuItem>
</Select>
</FormControl>
)
}

ChangeLocale.propTypes = {
change_locale: PropTypes.func.isRequired,
locale: PropTypes.string.isRequired
}
Empty file.
17 changes: 17 additions & 0 deletions src/views/components/change-locale/index.js
Original file line number Diff line number Diff line change
@@ -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)
54 changes: 28 additions & 26 deletions src/views/components/menu/menu.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
import React from 'react'
import React, { useState, Suspense } from 'react'
import { NavLink } from 'react-router-dom'
import PropTypes from 'prop-types'
import SwipeableDrawer from '@material-ui/core/SwipeableDrawer'
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 Skeleton from '@material-ui/lab/Skeleton'
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 (
<div className='menu__sections'>
<div className='menu__section'>
<div className='menu__heading'>Introduction</div>
<div className='menu__heading'>
{t('menu.introduction', 'Introduction')}
</div>
<div className='menu__links'>
<NavLink to='/introduction/basics'>Overview</NavLink>
<NavLink to='/introduction/advantages'>Advantages</NavLink>
Expand Down Expand Up @@ -115,43 +121,38 @@ function MenuSections() {
)
}

export default class Menu extends React.Component {
constructor(props) {
super(props)
this.state = {
open: false
}
}
export default function Menu({ hide, hideSearch, hide_speed_dial }) {
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 isHome = history.location.pathname === '/'
const isMobile = window.innerWidth < 750

return (
return (
<Suspense fallback={<Skeleton variant='rect' height={100} />}>
<div className='menu__container'>
<SwipeableDrawer
open={this.state.open}
onOpen={this.handleOpen}
onClose={this.handleClose}
open={open}
onOpen={handleOpen}
onClose={handleClose}
disableBackdropTransition={!iOS}
disableDiscovery={iOS}
anchor='top'>
<MenuSections />
<ChangeLocale />
</SwipeableDrawer>
{!hide_speed_dial && (
<SpeedDial
className='menu__dial'
ariaLabel='menu dial'
transitionDuration={0}
direction={isMobile ? 'up' : 'down'}
onClick={this.handleClick}
open={this.state.open}
onClick={handleClick}
open={open}
icon={
<img
alt='Nano is feeless, instant, and green / energy efficient digital money (cryptocurrency)'
Expand All @@ -164,7 +165,7 @@ export default class Menu extends React.Component {
icon={<HomeIcon />}
tooltipTitle='Home'
tooltipPlacement={isMobile ? 'left' : 'right'}
onClick={this.handleHomeClick}
onClick={handleHomeClick}
/>
)}
</SpeedDial>
Expand All @@ -179,10 +180,11 @@ export default class Menu extends React.Component {
)}
{!hideSearch && <SearchBar />}
{!hide && <MenuSections />}
<ChangeLocale />
</div>
</div>
)
}
</Suspense>
)
}

Menu.propTypes = {
Expand Down
Loading

0 comments on commit 14bae33

Please sign in to comment.