Skip to content

Experiment with an alternative approach to our i18n

Notifications You must be signed in to change notification settings

Janpot/i18n-test

Repository files navigation

i18n experiment

Precompiles our messages format (sprintf) to react components. Uses webpack aliases to create multiple bundles, one for each locale.

Commands:

To run the dev server

npm start

To run the dev server in a specific locale

npm start -- --env.locale=nl

To make a build:

npm run build

To make a production build:

npm run build -- --env.production

How it works:

a special loader for client/locales/*.json which transforms the contained tags to react components.

for instance:

{
  "interpolated_tag": "Here's some substitution: %(sub1)s, and another: %(sub2)s"
}

becomes:

// react < 16.0.0
import React from 'react';
export const interpolated_tag = ({sub1, sub2}) => <span>Here's some substitution: {sub1}, and another: {sub2}</span>;

// react >= 16.0.0
import React from 'react';
const ensureKey = (elm, key) => React.isValidElement(elm) ? React.cloneElement(elm,{key}) : elm;
export const interpolated_tag = ({sub1, sub2}) => ['Here\'s some substitution: ',ensureKey(sub1,'0-1'),', and another: ',ensureKey(sub2,'0-3')]

It renders fragments when used in react 16. On lower versions it will automatically wrap with a <span>

Using webpack aliases we can resolve the correct locale to a certain module identifier (e.g. i18n) then, to use this component, just do like any other:

import React from 'react';
import * as I18n from 'i18n';
class MyComponent extends React.Component {
  render () {
    return (
      <div>
        <I18n.interpolated_tag sub1="hello" sub2="world" />
      </div>
    );
  }
}

project configuration:

loader:

To enable the i18n loader on the locale files:

// webpack.config.js
const path = require('path');
module.exports = {
  // ...
  module: {
    loaders: [
      // ...
      {
        test: /\.json$/,
        include: path.resolve(__dirname, '../client/locales'),
        use: [ { loader: 'babel-loader' }, { loader: 'i18n' } ]
      }
    ]
  },
  resolveLoader: {
    modules: ['node_modules', path.resolve(__dirname, '../loaders')]
  }
};

localized bundles:

To create a bundle for a specific locale:

// webpack.config.js
const path = require('path');
module.exports = ({ locale = 'en' } = {}) => ({
  // ...
  output: {
    path: path.resolve(__dirname, `../dist/${locale}`)
  },
  resolve: {
    alias: {
      i18n: path.resolve(__dirname, `../client/locales/${locale}.json`)
    }
  }
})

Then it can be run as:

webpack --env.locale=nl

tree shaking:

Turn off modules in .babelrc:

{
    "presets":[ ["env", { "modules": false }], "react" ]
}

use uglifyjs-webpack-plugin:

// webpack.config.js
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
module.exports = ({ production } = {}) => {
  // ...
  plugins: production ? [ new UglifyJSPlugin() ] : []
}

Then call webpack with

webpack --env.production

Conclusion:

advantages:

  • Performant: no extra processing of messages when included in the markup
  • part of the bundle, so no extra requests required
  • Size: only import the tags you want. tree-shake what's not imported
  • Highly compatible with our current messageformat
  • Supports interpolation of components
  • Safe: relies on react escaping
  • Missing/invalid tags caught at compile time
  • Since these are react components, it's possible to add extra behaviour to them for our translation tooling, like a debug mode, or maybe even live editing

disadvantages:

  • Very naive sprinf parser. It only recognizes %(...)s for now. But afaik that's the only thing we use.
  • Extra compile time, needs optimization
  • Need to rebuild when translations change in production

About

Experiment with an alternative approach to our i18n

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published