From 549f3eb320192278de6080518b1800cc133c6b62 Mon Sep 17 00:00:00 2001 From: Philipp Wambach Date: Tue, 8 Oct 2019 15:47:14 +0200 Subject: [PATCH] feat(i18n): add react-intl and locale state (#130) * feat(i18n): add react-intl and local state * feat(i18n): refetch layers when locale changes (#131) --- .eslintrc | 1 + i18n/de.json | 4 + i18n/en.json | 4 + package-lock.json | 81 +++++++++++++++++-- package.json | 1 + src/scripts/actions/fetch-layers.ts | 13 ++- src/scripts/actions/set-locale.ts | 31 +++++++ src/scripts/api/fetch-layers.ts | 7 ++ src/scripts/components/app/app.tsx | 46 +++++++---- .../layer-selector/layer-selector.tsx | 7 +- src/scripts/components/menu/menu.tsx | 4 +- src/scripts/config/main.ts | 2 +- src/scripts/i18n.ts | 7 ++ src/scripts/reducers/index.ts | 2 + src/scripts/reducers/locale.ts | 22 +++++ tsconfig.json | 3 +- 16 files changed, 205 insertions(+), 30 deletions(-) create mode 100644 i18n/de.json create mode 100644 i18n/en.json create mode 100644 src/scripts/actions/set-locale.ts create mode 100644 src/scripts/api/fetch-layers.ts create mode 100644 src/scripts/i18n.ts create mode 100644 src/scripts/reducers/locale.ts diff --git a/.eslintrc b/.eslintrc index 934e3104f..d40b49e5c 100644 --- a/.eslintrc +++ b/.eslintrc @@ -332,6 +332,7 @@ "@typescript-eslint/explicit-function-return-type": 0, "@typescript-eslint/no-var-requires": 0, "@typescript-eslint/ban-ts-ignore": 0, + "@typescript-eslint/no-unused-vars": 2, "react/prop-types": 0 } } diff --git a/i18n/de.json b/i18n/de.json new file mode 100644 index 000000000..a68012db6 --- /dev/null +++ b/i18n/de.json @@ -0,0 +1,4 @@ +{ + "layerSelector.main": "Haupt", + "layerSelector.compare": "Vergleich" +} diff --git a/i18n/en.json b/i18n/en.json new file mode 100644 index 000000000..fadb59209 --- /dev/null +++ b/i18n/en.json @@ -0,0 +1,4 @@ +{ + "layerSelector.main": "Main", + "layerSelector.compare": "Compare" +} diff --git a/package-lock.json b/package-lock.json index d5c970757..bcbc52361 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,6 +48,27 @@ "ajv-keywords": "^3.1.0" } }, + "@formatjs/intl-relativetimeformat": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@formatjs/intl-relativetimeformat/-/intl-relativetimeformat-4.2.0.tgz", + "integrity": "sha512-0NQnixfRIdwRMagVR0CmqfKaI8xCtT1oZ0tAU7zrsch0k6N2wJFUYBsOtlgSqRR4hz2gAbVc7Rv5tdzkWW5fgg==", + "requires": { + "@formatjs/intl-utils": "^1.4.0" + } + }, + "@formatjs/intl-unified-numberformat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@formatjs/intl-unified-numberformat/-/intl-unified-numberformat-1.0.1.tgz", + "integrity": "sha512-nlHCmisXCzCOloy+My1PCGkjfrkFeHwDSq2IVnt8OFwDbljXX+atGg32T+w3nvRpbMWBJ7GsYIEeaZq+UFMomw==", + "requires": { + "@formatjs/intl-utils": "^1.3.0" + } + }, + "@formatjs/intl-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@formatjs/intl-utils/-/intl-utils-1.4.0.tgz", + "integrity": "sha512-z5HyJumGzORM+5SpvkAlp/hu0AHDeZcUNKSmj9NjS7kWxOGZMuAdS3X1K5XiE0j5I8r8s8SIaz0IQOdMA1WFeA==" + }, "@sindresorhus/is": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", @@ -108,12 +129,16 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", - "dev": true, "requires": { "@types/react": "*", "hoist-non-react-statics": "^3.3.0" } }, + "@types/invariant": { + "version": "2.2.30", + "resolved": "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.30.tgz", + "integrity": "sha512-98fB+yo7imSD2F7PF7GIpELNgtLNgo5wjivu0W5V4jx+KVVJxo6p/qN4zdzSTBWy4/sN3pPyXwnhRSD28QX+ag==" + }, "@types/json-schema": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.3.tgz", @@ -135,8 +160,7 @@ "@types/prop-types": { "version": "15.7.2", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.2.tgz", - "integrity": "sha512-f8JzJNWVhKtc9dg/dyDNfliTKNOJSLa7Oht/ElZdF/UbMUmAH3rLmAk3ODNjw0mZajDEgatA03tRjB4+Dp/tzA==", - "dev": true + "integrity": "sha512-f8JzJNWVhKtc9dg/dyDNfliTKNOJSLa7Oht/ElZdF/UbMUmAH3rLmAk3ODNjw0mZajDEgatA03tRjB4+Dp/tzA==" }, "@types/q": { "version": "1.5.2", @@ -148,7 +172,6 @@ "version": "16.9.2", "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.2.tgz", "integrity": "sha512-jYP2LWwlh+FTqGd9v7ynUKZzjj98T8x7Yclz479QdRhHfuW9yQ+0jjnD31eXSXutmBpppj5PYNLYLRfnZJvcfg==", - "dev": true, "requires": { "@types/prop-types": "*", "csstype": "^2.2.0" @@ -2536,8 +2559,7 @@ "csstype": { "version": "2.6.6", "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.6.tgz", - "integrity": "sha512-RpFbQGUE74iyPgvr46U9t1xoQBM8T4BL8SxrN66Le2xYAPSaDJJKeztV3awugusb3g3G9iL8StmkBBXhcbbXhg==", - "dev": true + "integrity": "sha512-RpFbQGUE74iyPgvr46U9t1xoQBM8T4BL8SxrN66Le2xYAPSaDJJKeztV3awugusb3g3G9iL8StmkBBXhcbbXhg==" }, "currently-unhandled": { "version": "0.4.1", @@ -5636,6 +5658,30 @@ "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==", "dev": true }, + "intl-format-cache": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/intl-format-cache/-/intl-format-cache-4.2.2.tgz", + "integrity": "sha512-7tY3XadLn8rMHiYVUzH/6NmOe944nJ59LdAWuFm64/m2OfFAEkZTtTHxrEtoxq7HWFddX4aRwDb9P8KB5Z2AvQ==" + }, + "intl-locales-supported": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/intl-locales-supported/-/intl-locales-supported-1.6.0.tgz", + "integrity": "sha512-n8J5v2oBjaOu065/HXeDFU3huv76Ehwj6YovPI7IJ3DCf0EvvwL1lncRj/qobmlyDh0LumwxpU+pVhFR34xjEA==" + }, + "intl-messageformat": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-7.3.2.tgz", + "integrity": "sha512-1hSgNhnpQqNrr09lFiz/oA3jX+REBuSyXh/ePvSncUicMsREtH3j2X1tDTTFHbK5kHjI+9vcwGpDSZpP8CM/uQ==", + "requires": { + "intl-format-cache": "^4.2.2", + "intl-messageformat-parser": "^3.2.1" + } + }, + "intl-messageformat-parser": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/intl-messageformat-parser/-/intl-messageformat-parser-3.2.1.tgz", + "integrity": "sha512-ajCL1k1ha0mUrutlBTo5vcTzyfdH2OoghUu8SmR7tJ1D0uifZh9Hqd3ZC2SYVv/GTfTdW//rgKonMgAhZWmwZg==" + }, "invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -8673,6 +8719,24 @@ "scheduler": "^0.15.0" } }, + "react-intl": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-3.3.2.tgz", + "integrity": "sha512-Y2QMrcVxkVSzuTD/3+wkbJOV+vYcu60KsKY5XqJ6IkzseFY68myk7ijJ1UHXOP/xBdJtwiXVOCG10EDwDGj/xQ==", + "requires": { + "@formatjs/intl-relativetimeformat": "^4.1.1", + "@formatjs/intl-unified-numberformat": "^1.0.0", + "@types/hoist-non-react-statics": "^3.3.1", + "@types/invariant": "^2.2.30", + "hoist-non-react-statics": "^3.3.0", + "intl-format-cache": "^4.2.1", + "intl-locales-supported": "^1.5.0", + "intl-messageformat": "^7.3.1", + "intl-messageformat-parser": "^3.2.0", + "invariant": "^2.1.1", + "shallow-equal": "^1.1.0" + } + }, "react-is": { "version": "16.9.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.9.0.tgz", @@ -9429,6 +9493,11 @@ "safe-buffer": "^5.0.1" } }, + "shallow-equal": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.0.tgz", + "integrity": "sha512-Z21pVxR4cXsfwpMKMhCEIO1PCi5sp7KEp+CmOpBQ+E8GpHwKOw2sEzk7sgblM3d/j4z4gakoWEoPcjK0VJQogA==" + }, "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", diff --git a/package.json b/package.json index 6e06b414f..ad5c4ca4e 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "cesium": "^1.61.0", "react": "^16.9.0", "react-dom": "^16.9.0", + "react-intl": "^3.3.2", "react-redux": "^7.1.1", "redux": "^4.0.4", "redux-logger": "^3.0.6", diff --git a/src/scripts/actions/fetch-layers.ts b/src/scripts/actions/fetch-layers.ts index 8c7202cfb..36f0bc77e 100644 --- a/src/scripts/actions/fetch-layers.ts +++ b/src/scripts/actions/fetch-layers.ts @@ -1,5 +1,8 @@ import {Dispatch} from 'redux'; -import config from '../config/main'; + +import fetchLayersApi from '../api/fetch-layers'; +import {localeSelector} from '../reducers/locale'; +import {State} from '../reducers/index'; export const FETCH_LAYERS_SUCCESS = 'FETCH_LAYERS_SUCCESS'; export const FETCH_LAYERS_ERROR = 'FETCH_LAYERS_ERROR'; @@ -40,10 +43,12 @@ function fetchLayersErrorAction(message: string) { }; } -const fetchLayers = () => (dispatch: Dispatch) => - fetch(config.api.layers) - .then(res => res.json()) +const fetchLayers = () => (dispatch: Dispatch, getState: () => State) => { + const locale = localeSelector(getState()); + + return fetchLayersApi(locale) .then(layers => dispatch(fetchLayersSuccessAction(layers))) .catch(error => dispatch(fetchLayersErrorAction(error.message))); +}; export default fetchLayers; diff --git a/src/scripts/actions/set-locale.ts b/src/scripts/actions/set-locale.ts new file mode 100644 index 000000000..bb6e4f54b --- /dev/null +++ b/src/scripts/actions/set-locale.ts @@ -0,0 +1,31 @@ +import {ThunkDispatch} from 'redux-thunk'; + +import {State} from '../reducers/index'; +import fetchLayers, {FetchLayersActions} from './fetch-layers'; + +export const SET_LOCALE = 'SET_LOCALE'; + +export enum Locale { + EN = 'en', + DE = 'de' +} + +export interface SetLocaleAction { + type: typeof SET_LOCALE; + locale: Locale; +} + +type AllThunkActions = SetLocaleAction | FetchLayersActions; + +const setLocaleAction = (locale: Locale) => ( + dispatch: ThunkDispatch +) => { + dispatch({ + type: SET_LOCALE, + locale + }); + + dispatch(fetchLayers()); +}; + +export default setLocaleAction; diff --git a/src/scripts/api/fetch-layers.ts b/src/scripts/api/fetch-layers.ts new file mode 100644 index 000000000..edec90081 --- /dev/null +++ b/src/scripts/api/fetch-layers.ts @@ -0,0 +1,7 @@ +import {Locale} from '../actions/set-locale'; +import config from '../config/main'; + +export default function fetchLayers(locale: Locale) { + const url = `${config.api.layers}-${locale.toLowerCase()}.json`; + return fetch(url).then(res => res.json()); +} diff --git a/src/scripts/components/app/app.tsx b/src/scripts/components/app/app.tsx index a1106e727..941635075 100644 --- a/src/scripts/components/app/app.tsx +++ b/src/scripts/components/app/app.tsx @@ -1,29 +1,47 @@ import React, {FunctionComponent} from 'react'; import {createStore, applyMiddleware} from 'redux'; -import {Provider} from 'react-redux'; +import {Provider as StoreProvider, useSelector} from 'react-redux'; import thunk from 'redux-thunk'; -import logger from 'redux-logger'; +import {createLogger} from 'redux-logger'; +import {IntlProvider} from 'react-intl'; + import rootReducer from '../../reducers/index'; +import {localeSelector} from '../../reducers/locale'; import LayerSelector from '../layer-selector/layer-selector'; import Globe from '../globe/globe'; import Menu from '../menu/menu'; import ProjectionMenu from '../projection-menu/projection-menu'; + +import translations from '../../i18n'; import styles from './app.styl'; -const store = createStore(rootReducer, applyMiddleware(thunk, logger)); +const store = createStore( + rootReducer, + applyMiddleware(thunk, createLogger({collapsed: true})) +); const App: FunctionComponent<{}> = () => ( - -
- -
- -
- - -
-
- + + + ); +const TranslatedApp: FunctionComponent<{}> = () => { + const locale = useSelector(localeSelector); + + return ( + +
+ +
+ +
+ + +
+
+ + ); +}; + export default App; diff --git a/src/scripts/components/layer-selector/layer-selector.tsx b/src/scripts/components/layer-selector/layer-selector.tsx index 06d29c518..d3b82e5bc 100644 --- a/src/scripts/components/layer-selector/layer-selector.tsx +++ b/src/scripts/components/layer-selector/layer-selector.tsx @@ -1,5 +1,7 @@ import React, {FunctionComponent, useEffect, useState} from 'react'; import {useSelector, useDispatch} from 'react-redux'; +import {useIntl} from 'react-intl'; + import {layersSelector} from '../../reducers/layers'; import {selectedLayerIdSelector} from '../../reducers/selected-layer-id'; import fetchLayers from '../../actions/fetch-layers'; @@ -9,17 +11,18 @@ import Tabs from '../tabs/tabs'; import styles from './layer-selector.styl'; const LayerSelector: FunctionComponent<{}> = () => { + const intl = useIntl(); const layers = useSelector(layersSelector); const layerIds = useSelector(selectedLayerIdSelector); const dispatch = useDispatch(); const tabs = [ { id: 'main', - label: 'Main' + label: intl.formatMessage({id: 'layerSelector.main'}) }, { id: 'compare', - label: 'Compare' + label: intl.formatMessage({id: 'layerSelector.compare'}) } ]; diff --git a/src/scripts/components/menu/menu.tsx b/src/scripts/components/menu/menu.tsx index 0174ab651..23d7a6567 100644 --- a/src/scripts/components/menu/menu.tsx +++ b/src/scripts/components/menu/menu.tsx @@ -8,7 +8,7 @@ interface MenuItem { } const Menu: FunctionComponent<{}> = () => { - const menuItems = [ + const menuItems: MenuItem[] = [ { id: 'presenter-mode', name: 'Presenter Mode', @@ -23,7 +23,7 @@ const Menu: FunctionComponent<{}> = () => { {id: 'share', name: 'Share Content'}, {id: 'export', name: 'Export Data'}, {id: 'info', name: 'More Information'} - ] as MenuItem[]; + ]; const [isOpen, setIsOpen] = useState(false); diff --git a/src/scripts/config/main.ts b/src/scripts/config/main.ts index 352b1ed3f..e0987da52 100644 --- a/src/scripts/config/main.ts +++ b/src/scripts/config/main.ts @@ -1,5 +1,5 @@ export default { api: { - layers: 'https://storage.googleapis.com/esa-cfs-storage/layers.json' + layers: 'https://storage.googleapis.com/esa-cfs-storage/layers' } }; diff --git a/src/scripts/i18n.ts b/src/scripts/i18n.ts new file mode 100644 index 000000000..784d3fbf1 --- /dev/null +++ b/src/scripts/i18n.ts @@ -0,0 +1,7 @@ +import en from '../../i18n/en.json'; +import de from '../../i18n/de.json'; + +export default { + en, + de +}; diff --git a/src/scripts/reducers/index.ts b/src/scripts/reducers/index.ts index 56d4ea635..f5a435154 100644 --- a/src/scripts/reducers/index.ts +++ b/src/scripts/reducers/index.ts @@ -1,10 +1,12 @@ import {combineReducers} from 'redux'; +import localeReducer from './locale'; import layersReducer from './layers'; import selectedLayerReducer from './selected-layer-id'; import projectionReducer from './projection'; const rootReducer = combineReducers({ + locale: localeReducer, layers: layersReducer, selectedLayer: selectedLayerReducer, projection: projectionReducer diff --git a/src/scripts/reducers/locale.ts b/src/scripts/reducers/locale.ts new file mode 100644 index 000000000..9fd717602 --- /dev/null +++ b/src/scripts/reducers/locale.ts @@ -0,0 +1,22 @@ +import {State} from './index'; +import {SET_LOCALE, Locale, SetLocaleAction} from '../actions/set-locale'; + +const initialState: Locale = Locale.EN; + +function localeReducer( + localeState: Locale = initialState, + action: SetLocaleAction +): Locale { + switch (action.type) { + case SET_LOCALE: + return action.locale; + default: + return localeState; + } +} + +export function localeSelector(state: State): Locale { + return state.locale; +} + +export default localeReducer; diff --git a/tsconfig.json b/tsconfig.json index b461bdfa2..aa7acd9b8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,6 +9,7 @@ "allowSyntheticDefaultImports": true, "moduleResolution": "node", "target": "es5", - "jsx": "react" + "jsx": "react", + "resolveJsonModule": true } }