From 0bb60e46aa987c9b60220ee98d8a45a5ed4ca21d Mon Sep 17 00:00:00 2001 From: mistakia <1823355+mistakia@users.noreply.github.com> Date: Sat, 17 Feb 2024 08:29:35 -0500 Subject: [PATCH] feat: add internationalization support --- babel.config.js | 3 +- package.json | 2 + src/core/i18n/actions.js | 10 +++ src/core/i18n/index.js | 3 + src/core/i18n/reducer.js | 17 ++++++ src/core/i18n/sagas.js | 38 ++++++++++++ src/core/reducers.js | 4 +- src/core/sagas.js | 4 +- src/i18n/index.js | 19 ++++++ src/index.js | 1 + .../components/change-locale/change-locale.js | 32 ++++++++++ .../change-locale/change-locale.styl | 0 src/views/components/change-locale/index.js | 17 ++++++ src/views/components/menu/menu.js | 3 + yarn.lock | 61 +++++++++++++++++++ 15 files changed, 211 insertions(+), 3 deletions(-) create mode 100644 src/core/i18n/actions.js create mode 100644 src/core/i18n/index.js create mode 100644 src/core/i18n/reducer.js create mode 100644 src/core/i18n/sagas.js create mode 100644 src/i18n/index.js create mode 100644 src/views/components/change-locale/change-locale.js create mode 100644 src/views/components/change-locale/change-locale.styl create mode 100644 src/views/components/change-locale/index.js 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/i18n/actions.js b/src/core/i18n/actions.js new file mode 100644 index 00000000..aa2ad688 --- /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..adf89774 --- /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_LANGUAGE: + 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..e1d52703 --- /dev/null +++ b/src/core/i18n/sagas.js @@ -0,0 +1,38 @@ +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() { + const locale = localStorageAdapter.getItem('locale') + if (locale) { + yield put(i18nActions.change_locale(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..c43e8286 --- /dev/null +++ b/src/i18n/index.js @@ -0,0 +1,19 @@ +import { initReactI18next } from 'react-i18next' +import i18n from 'i18next' + +i18n.use(initReactI18next).init({ + // detection + debug: true, + resources: { + en: { + translation: { + 'Welcome to React': 'Welcome to React and react-i18next' + } + } + }, + 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/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..f4d8e945 100644 --- a/src/views/components/menu/menu.js +++ b/src/views/components/menu/menu.js @@ -9,6 +9,7 @@ import HomeIcon from '@material-ui/icons/Home' import SearchBar from '@components/search-bar' import history from '@core/history' +import ChangeLocale from '@components/change-locale' import './menu.styl' @@ -143,6 +144,7 @@ export default class Menu extends React.Component { disableDiscovery={iOS} anchor='top'> + {!hide_speed_dial && ( } {!hide && } + ) 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"