i18n: implement dynamic language loading

This commit is contained in:
Jesse Chan
2020-09-17 19:40:45 +08:00
parent 901b5e0cc1
commit 485bb1ff34
8 changed files with 57 additions and 125 deletions
+1
View File
@@ -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'),
},
},
+7 -12
View File
@@ -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<FloodAppProps> {
}
public render(): React.ReactNode {
let {locale} = this.props;
if (locale == null || locale === 'auto' || !Object.prototype.hasOwnProperty.call(i18n.languages, locale)) {
locale = detectLocale();
}
return (
<IntlProvider locale={locale} messages={i18n.languages[locale]}>
{appRoutes}
</IntlProvider>
<React.Suspense fallback={<AsyncIntlProvider locale="en">{appRoutes}</AsyncIntlProvider>}>
<AsyncIntlProvider locale={this.props.locale}>{appRoutes}</AsyncIntlProvider>
</React.Suspense>
);
}
}
-104
View File
@@ -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,
},
};
+39
View File
@@ -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<
Exclude<keyof typeof Languages, 'auto'>,
Record<string, MessageFormatElement[]>
>> = {en: EN};
async function loadMessages(locale: Exclude<keyof typeof Languages, 'auto' | 'en'>) {
const messages: Record<string, MessageFormatElement[]> = await import(`./compiled/${locale}.json`);
messagesCache[locale] = messages;
return messages;
}
function getMessages(locale: Exclude<keyof typeof Languages, 'auto'>) {
if (messagesCache[locale]) {
return messagesCache[locale];
}
throw loadMessages(locale as Exclude<keyof typeof Languages, 'auto' | 'en'>);
}
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<keyof typeof Languages, 'auto'>);
return (
<IntlProvider locale={locale} messages={messages}>
{children}
</IntlProvider>
);
};
+7 -7
View File
@@ -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;
+2 -2
View File
@@ -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%"
]
}
+1
View File
@@ -5,6 +5,7 @@
"target": "esnext",
"moduleResolution": "node",
"allowJs": true,
"module": "esnext",
"noEmit": true,
"strict": true,
"isolatedModules": true,