Jump to final component.
I18nProvider
doesn't assume anything about your app and it's your responsibility to load messages based on active language. Here's an example of I18nLoader
component which is connected to Redux store and loads message catalogs using dynamic import in Webpack.
- babel
- webpack 2.x
- redux
- lingui-react
We are using the Dynamic Import() Proposal to ECMAScript. We need to install babel-plugin-syntax-dynamic-import
and babel-plugin-dynamic-import-node
to make it work. Also, the code examples given here make use of babel-plugin-transform-class-properties
yarn add --dev babel-plugin-syntax-dynamic-import babel-plugin-dynamic-import-node babel-plugin-transform-class-properties
Warning
babel-plugin-dynamic-import-node is required when running tests in Jest.
// .babelrc
{
"plugins": [
"syntax-dynamic-import",
"transform-class-properties"
],
"env": {
"test": {
"plugins": [
"dynamic-import-node"
]
}
}
Let's start with the component. We're going to wrap I18nProvider
from lingui-react
. Active language is loaded from redux store, while messages are dynamically loaded and stored in local state.
The render()
method looks like this:
render () {
const { children, language } = this.props
const { catalogs } = this.state
// Skip rendering when catalog isn't loaded.
if (!catalogs[language]) return
return (
<I18nProvider language={language} catalogs={catalogs}>
{children}
</I18nProvider>
)
}
Next, we need to load message catalog when language changes which isn't done yet. Dynamic import returns a promise and we don't want to re-render our component until the message catalog is loaded. Let's add a shouldComponentUpdate()
method:
shouldComponentUpdate(nextProps, nextState) {
const { language } = nextProps
const { catalogs } = nextState
if (language !== this.props.language && !catalogs[language]) {
// Start loading message catalog and skip update
this.loadCatalog(language)
return false
}
return true
}
shouldComponentUpdate
isn't called during the first render, so we need trigger loading of catalog manually in componentDidMount
:
componentDidMount() {
this.loadCatalog(this.props.language)
}
The most important piece in this story is loadCatalog()
method. It's necessary to load compiled message catalogs in production (messages.js
instead of messages.json
) and unpack them using unpackCatalog()
function. Here we use the dynamic import syntax to load the message catalog:
loadCatalog = async (language) => {
const catalog = await import(
/* webpackMode: "lazy", webpackChunkName: "i18n-[index]" */
`locale/${language}/messages.js`)
this.setState(state => ({
catalogs: {
...state.catalogs,
[language]: unpackCatalog(catalog)
}
}))
}
Dynamic import returns a promise, so we can either use async/await keywords or good old promises:
loadCatalog = (language) => {
import(
/* webpackMode: "lazy", webpackChunkName: "i18n-[index]" */
`locale/${language}/messages.js`)
.then(catalog =>
this.setState(state => ({
catalogs: {
...state.catalogs,
[language]: unpackCatalog(catalog)
}
}))
)
}
The comment before message catalog path is webpack's magic comment. webpackMode: lazy
means, that chunks are loaded as requested. webpackChunkName: "i18n-[index]"
overrides default chunk name for this import.
Here's the full source of I18nLoader
component:
import React from 'react'
import { connect } from 'react-redux'
import { I18nProvider } from 'lingui-react'
import { unpackCatalog } from 'lingui-i18n'
export class I18nLoader extends React.Component {
state = {
catalogs: {},
}
loadCatalog = async (language) => {
const catalog = await import(
/* webpackMode: "lazy", webpackChunkName: "i18n-[index]" */
`locale/${language}/messages.js`)
this.setState(state => ({
catalogs: {
...state.catalogs,
[language]: unpackCatalog(catalog)
}
}))
}
componentDidMount() {
this.loadCatalog(this.props.language)
}
shouldComponentUpdate(nextProps, nextState) {
const { language } = nextProps
const { catalogs } = nextState
if (language !== this.props.language && !catalogs[language]) {
this.loadCatalog(language)
return false
}
return true
}
render () {
const { children, language } = this.props
const { catalogs } = this.state
// Skip rendering when catalog isn't loaded.
if (!catalogs[language]) return
return (
<I18nProvider language={language} catalogs={catalogs}>
{children}
</I18nProvider>
)
}
}
// Example: depends on implementation of reducer
const getLanguage = state => state.locale.language
export default connect(state => ({
language: getLanguage(state)
}))(I18nLoader)
Looking at the content of build dir, we see one chunk per language:
i18n-0.c433b3bd.chunk.js
i18n-1.f0cf2e3d.chunk.js
main.ab4626ef.js
When page is loaded initially, only main bundle and bundle for the first language are loaded:
After changing language in UI, the second language bundle is loaded:
And that's it! 馃帀