From 485bb1ff347d369cca73d3efc1bea06e0c5b904d Mon Sep 17 00:00:00 2001 From: Jesse Chan Date: Thu, 17 Sep 2020 19:40:45 +0800 Subject: [PATCH] i18n: implement dynamic language loading --- client/config/webpack.config.prod.js | 1 + client/src/javascript/app.tsx | 19 ++-- client/src/javascript/i18n/languages.ts | 104 ------------------ client/src/javascript/i18n/languages.tsx | 39 +++++++ .../strings.json => strings.compiled.json} | 0 client/src/javascript/util/detectLocale.tsx | 14 +-- package.json | 4 +- tsconfig.json | 1 + 8 files changed, 57 insertions(+), 125 deletions(-) delete mode 100644 client/src/javascript/i18n/languages.ts create mode 100644 client/src/javascript/i18n/languages.tsx rename client/src/javascript/i18n/{compiled/strings.json => strings.compiled.json} (100%) diff --git a/client/config/webpack.config.prod.js b/client/config/webpack.config.prod.js index 825d390b..dd5ac78b 100644 --- a/client/config/webpack.config.prod.js +++ b/client/config/webpack.config.prod.js @@ -139,6 +139,7 @@ module.exports = { resolve: { extensions: ['*', '.js', '.jsx', '.ts', '.tsx', '.json'], alias: { + 'react-intl': 'react-intl/react-intl-no-parser.umd.min.js', '@shared': path.resolve('./shared'), }, }, diff --git a/client/src/javascript/app.tsx b/client/src/javascript/app.tsx index 957b0391..bdb8c50f 100644 --- a/client/src/javascript/app.tsx +++ b/client/src/javascript/app.tsx @@ -1,16 +1,16 @@ import {Router} from 'react-router-dom'; -import {FormattedMessage, IntlProvider} from 'react-intl'; +import {FormattedMessage} from 'react-intl'; import {Route} from 'react-router'; import React from 'react'; import ReactDOM from 'react-dom'; -import detectLocale from './util/detectLocale'; -import * as i18n from './i18n/languages'; +import {AsyncIntlProvider} from './i18n/languages'; import connectStores from './util/connectStores'; import AppWrapper from './components/AppWrapper'; import AuthActions from './actions/AuthActions'; import FloodActions from './actions/FloodActions'; import history from './util/history'; +import Languages from './constants/Languages'; import Login from './components/views/Login'; import Register from './components/views/Register'; import SettingsStore from './stores/SettingsStore'; @@ -20,7 +20,7 @@ import UIStore from './stores/UIStore'; import '../sass/style.scss'; interface FloodAppProps { - locale?: keyof typeof i18n.languages; + locale?: keyof typeof Languages; } const initialize = (): void => { @@ -81,15 +81,10 @@ class FloodApp extends React.Component { } public render(): React.ReactNode { - let {locale} = this.props; - if (locale == null || locale === 'auto' || !Object.prototype.hasOwnProperty.call(i18n.languages, locale)) { - locale = detectLocale(); - } - return ( - - {appRoutes} - + {appRoutes}}> + {appRoutes} + ); } } diff --git a/client/src/javascript/i18n/languages.ts b/client/src/javascript/i18n/languages.ts deleted file mode 100644 index bb1be82b..00000000 --- a/client/src/javascript/i18n/languages.ts +++ /dev/null @@ -1,104 +0,0 @@ -import EN from './compiled/strings.json'; - -import CS from './compiled/cs.json'; -import DE from './compiled/de.json'; -import ES from './compiled/es.json'; -import FR from './compiled/fr.json'; -import IT from './compiled/it.json'; -import HU from './compiled/hu.json'; -import NL from './compiled/nl.json'; -import NO from './compiled/no.json'; -import PL from './compiled/pl.json'; -import PT from './compiled/pt.json'; -import RU from './compiled/ru.json'; -import RO from './compiled/ro.json'; -import SV from './compiled/sv.json'; -import UK from './compiled/uk.json'; -import KO from './compiled/ko.json'; -import JA from './compiled/ja.json'; -import ZH_HANS from './compiled/zh-Hans.json'; -import ZH_HANT from './compiled/zh-Hant.json'; -import AR from './compiled/ar.json'; - -export const languages = { - auto: EN, - en: EN, - cs: { - ...EN, - ...CS, - }, - de: { - ...EN, - ...DE, - }, - es: { - ...EN, - ...ES, - }, - fr: { - ...EN, - ...FR, - }, - it: { - ...EN, - ...IT, - }, - hu: { - ...EN, - ...HU, - }, - nl: { - ...EN, - ...NL, - }, - no: { - ...EN, - ...NO, - }, - pl: { - ...EN, - ...PL, - }, - pt: { - ...EN, - ...PT, - }, - ru: { - ...EN, - ...RU, - }, - ro: { - ...EN, - ...RO, - }, - sv: { - ...EN, - ...SV, - }, - uk: { - ...EN, - ...UK, - }, - ko: { - ...EN, - ...KO, - }, - ja: { - ...EN, - ...JA, - }, - 'zh-Hans': { - ...EN, - ...ZH_HANT, - ...ZH_HANS, - }, - 'zh-Hant': { - ...EN, - ...ZH_HANS, - ...ZH_HANT, - }, - ar: { - ...EN, - ...AR, - }, -}; diff --git a/client/src/javascript/i18n/languages.tsx b/client/src/javascript/i18n/languages.tsx new file mode 100644 index 00000000..68df768c --- /dev/null +++ b/client/src/javascript/i18n/languages.tsx @@ -0,0 +1,39 @@ +import {IntlProvider} from 'react-intl'; +import React from 'react'; + +import type {MessageFormatElement} from 'intl-messageformat-parser'; + +import detectLocale from '../util/detectLocale'; +import EN from './strings.compiled.json'; +import Languages from '../constants/Languages'; + +const messagesCache: Partial, + Record +>> = {en: EN}; + +async function loadMessages(locale: Exclude) { + const messages: Record = await import(`./compiled/${locale}.json`); + messagesCache[locale] = messages; + return messages; +} + +function getMessages(locale: Exclude) { + if (messagesCache[locale]) { + return messagesCache[locale]; + } + throw loadMessages(locale as Exclude); +} + +export const AsyncIntlProvider = ({locale, children}: {locale?: keyof typeof Languages; children: React.ReactNode}) => { + if (locale == null || locale === 'auto') { + locale = detectLocale(); + } + + const messages = getMessages(locale as Exclude); + return ( + + {children} + + ); +}; diff --git a/client/src/javascript/i18n/compiled/strings.json b/client/src/javascript/i18n/strings.compiled.json similarity index 100% rename from client/src/javascript/i18n/compiled/strings.json rename to client/src/javascript/i18n/strings.compiled.json diff --git a/client/src/javascript/util/detectLocale.tsx b/client/src/javascript/util/detectLocale.tsx index d40db7f0..a32ffeb8 100644 --- a/client/src/javascript/util/detectLocale.tsx +++ b/client/src/javascript/util/detectLocale.tsx @@ -1,10 +1,10 @@ import {getUserLocales} from 'get-user-locale'; -import * as i18n from '../i18n/languages'; +import Languages from '../constants/Languages'; -let detectedLocale: keyof typeof i18n.languages = 'en'; +let detectedLocale: keyof typeof Languages = 'en'; let localeDetected = false; -export default function (): keyof typeof i18n.languages { +export default function (): keyof typeof Languages { if (localeDetected) { return detectedLocale; } @@ -28,11 +28,11 @@ export default function (): keyof typeof i18n.languages { default: break; } - if (Object.prototype.hasOwnProperty.call(i18n.languages, userLocale)) { - detectedLocale = userLocale as keyof typeof i18n.languages; - } else if (Object.prototype.hasOwnProperty.call(i18n.languages, userLocale.substr(0, 2))) { + if (Object.prototype.hasOwnProperty.call(Languages, userLocale)) { + detectedLocale = userLocale as keyof typeof Languages; + } else if (Object.prototype.hasOwnProperty.call(Languages, userLocale.substr(0, 2))) { // In rare cases, user provides a locale (eg. en-US) without fallback (eg. en) - detectedLocale = userLocale.substr(0, 2) as keyof typeof i18n.languages; + detectedLocale = userLocale.substr(0, 2) as keyof typeof Languages; } }); localeDetected = true; diff --git a/package.json b/package.json index 7399799d..96be03c0 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "build-assets": "node client/scripts/build.js", "build-ts": "tsc-transpile-only -p server/tsconfig.json && cat config.js > dist/config.js && chmod 755 dist/server/bin/start.js", "build-docs": "jsdoc -c ./.jsdoc.json", - "build-i18n": "formatjs compile-folder --ast --format simple client/src/javascript/i18n client/src/javascript/i18n/compiled && formatjs compile-folder --ast --format simple client/src/javascript/i18n/translations client/src/javascript/i18n/compiled", + "build-i18n": "formatjs compile --ast --format simple client/src/javascript/i18n/strings.json --out-file client/src/javascript/i18n/strings.compiled.json && formatjs compile-folder --ast --format simple client/src/javascript/i18n/translations client/src/javascript/i18n/compiled", "deprecated-warning": "node client/scripts/deprecated-warning.js && sleep 10", "format-source": "node scripts/prettier.js formatSource", "check-compiled-i18n": "npm run build-i18n && npm run format-source && git diff --quiet client/src/javascript/i18n/compiled", @@ -164,6 +164,6 @@ "npm": ">=6.0.0" }, "browserslist": [ - "> 1%" + "> 5%" ] } diff --git a/tsconfig.json b/tsconfig.json index ded6e1d9..a00fec48 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,7 @@ "target": "esnext", "moduleResolution": "node", "allowJs": true, + "module": "esnext", "noEmit": true, "strict": true, "isolatedModules": true,