Per Locale
in React

Tyler Graf
Web Dev
Tree

π¨π»
Goals
- Help you understand our react stack
- Dip your toe into webpack
- Upgrade your app to per-locale

Current Issue
We load all locale strings on every page
zh
bg fr lt sk zh cs hr lv sl zh-hans da ht mk sm de hu mn sq el hy ms sr en id nl sv eo is no th es it pl to et ja pt tr fi km ro uk fj ko ru vi

@fs/zion-ui
en 1KB gzip
all
~45KB gzip
i18next
import i18n from "i18next";
import {initReactI18next} from 'react-i18next'
import localeStrings from './allLocaleStrings.json'
i18n
.use(initReactI18next)
.init({
lng: "zh",
fallbackLng: "en",
resources: localeStrings
});
import React from 'react'
import './i18n'
import { useTranslation } from "react-i18next";
export default function App(){
const { t } = useTranslation();
return <div>{t("title")}</div>
}
i18n.js
App.js
import i18n from "i18next";
export const addTranslations = (translations) => {
translations.forEach(locale=>{
i18n.addResources(locale, 'translation', translations[locale])
})
}
// ... lots of other stuff too
import React from 'react'
import { addTranslations } from '@fs/zion-locale'
import { useTranslation } from "react-i18next";
import translations from './locales'
addTranslations(translations)
export default function App(){
const { t } = useTranslation();
return <div>{t("title")}</div>
}
@fs/zion-locale
App.js
alienfast/i18next-loader
// App.js
import React from "react";
import translations from "./locales";
import { addTranslations } from "@fs/zion-locale";
import { useTranslation } from "react-i18next";
addTranslations(translations);
export default function App() {
const [t] = useTranslation();
return (
<div>
<h1>{t("overview")}</h1>
</div>
);
}
{
"overview": "Overview"
}
// intentionally left blank
.
βββ App.js
βββ node_modules
β βββ @fs
β βββ zion-locale
β βββ locales
β βββ de
β β βββ common-ui.json
β β βββ translation.json
β βββ en
β β βββ common-ui.json
β β βββ translation.json
β βββ es
β β βββ common-ui.json
β β βββ translation.json
β βββ index.js
βββ locales
βββ de
β βββ translation.json
βββ en
β βββ translation.json
βββ es
β βββ translation.json
βββ index.js
export default {
de: {
overview: "Γberblick"
},
en: {
overview: "Overview"
},
es: {
overview: "VisiΓ³n general"
}
};
// intentionally left blank

.
βββ App.js
βββ node_modules
β βββ @fs
β βββ zion-locale
β βββ locales
β βββ de
β β βββ common-ui.json
β β βββ translation.json
β βββ en
β β βββ common-ui.json
β β βββ translation.json
β βββ es
β β βββ common-ui.json
β β βββ translation.json
β βββ index.js
βββ locales
βββ de
β βββ translation.json
βββ en
β βββ translation.json
βββ es
β βββ translation.json
βββ index.js
export default {
de: {
overview: "Γberblick"
},
en: {
overview: "Overview"
},
es: {
overview: "VisiΓ³n general"
}
};
// App.js
import React from "react";
import translations from "./locales";
import { addTranslations } from "@fs/zion-locale";
import { useTranslation } from "react-i18next";
addTranslations(translations);
export default function App() {
const [t] = useTranslation();
return (
<div>
<h1>{t("overview")}</h1>
</div>
);
}
How do we solve this?

Community!!

http-backend

dynamic import

Webpack refresher
(It's a module bundler)

What's a module bundler?
add.js
index.js
subtract.js

bundle.js
import add from './add.js';
import subtract from './subtract.js';
console.log(`Add: ${add(4,5)}`);
console.log(`Subtract: ${subtract(9,5)}`);
const add = (a, b) => {
return a + b;
}
export default add;
const subtract = (a, b) => {
return a - b;
}
export default subtract;
index.js
add.js
subtract.js
const add = (a, b) => {
return a + b;
}
const subtract = (a, b) => {
return a - b;
}
console.log(`Add: ${add(4,5)}`);
console.log(`Subtract: ${subtract(9,5)}`);
bundle.js
Well, kinda
/*
* ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
/******/ (() => { // webpackBootstrap
/******/ "use strict";
/******/ var __webpack_modules__ = ({
/***/ "./src/add.js":
/*!********************!*\
!*** ./src/add.js ***!
\********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => /* binding */ add\n/* harmony export */ });\nfunction add(a, b) {\n return a + b;\n}\n\n\n//# sourceURL=webpack://basic-webpack/./src/add.js?");
/***/ }),
/***/ "./src/index.js":
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _subtract__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./subtract */ \"./src/subtract.js\");\n/* harmony import */ var _add__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./add */ \"./src/add.js\");\n\n\n\nconsole.log((0,_subtract__WEBPACK_IMPORTED_MODULE_0__.default)(10,4))\nconsole.log((0,_add__WEBPACK_IMPORTED_MODULE_1__.default)(10,4))\n\n\n//# sourceURL=webpack://basic-webpack/./src/index.js?");
/***/ }),
/***/ "./src/subtract.js":
/*!*************************!*\
!*** ./src/subtract.js ***!
\*************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => /* binding */ subtract\n/* harmony export */ });\nfunction subtract(a, b) {\n return a - b;\n}\n\n\n//# sourceURL=webpack://basic-webpack/./src/subtract.js?");
/***/ })
/******/ });
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ if(__webpack_module_cache__[moduleId]) {
/******/ return __webpack_module_cache__[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ // no module.id needed
/******/ // no module.loaded needed
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/************************************************************************/
/******/ /* webpack/runtime/define property getters */
/******/ (() => {
/******/ // define getter functions for harmony exports
/******/ __webpack_require__.d = (exports, definition) => {
/******/ for(var key in definition) {
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ }
/******/ }
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ (() => {
/******/ __webpack_require__.o = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop)
/******/ })();
/******/
/******/ /* webpack/runtime/make namespace object */
/******/ (() => {
/******/ // define __esModule on exports
/******/ __webpack_require__.r = (exports) => {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/ })();
/******/
/************************************************************************/
/******/ // startup
/******/ // Load entry module
/******/ __webpack_require__("./src/index.js");
/******/ // This entry module used 'exports' so it can't be inlined
/******/ })()
;
bundle.js
module.exports = {
entry: './src/index.js',
output: {
path: '/dist',
filename: 'bundle.js'
}
}
Basic Config
Demo
Webpack Loaders
module.exports = {
entry: './src/index.js',
output: {
path: '/dist',
filename: 'bundle.js'
}
}
module.exports = {
entry: './src/index.js',
output: {
path: '/dist',
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'my-replace-loader'
}
}
]
}
}
Loaders take a string and return a string
module.exports = function(source) {
source = source.replace(/stuff/g, "things");
return source;
};
items.map(item=>{
return item.replace('stuff','thing');
});
Remind's me of a map function
my-replace-loader

export default function(){
const stuff = 'stuff'
console.log(stuff)
}
export default function(){
const things = 'things'
console.log(things)
}
add.js
index.js
subtract.js
bundle.js
my-replace-loader
once for each file
Demo
alienfast/i18next-loader
module.exports = {
entry: './src/index.js',
output: {
path: '/dist',
filename: 'bundle.js'
},
module: {
rules: [
{
test: /locales/,
use: {
loader: 'alienfast/i18next-loader'
}
}
]
}
}
export default {
de: {
overview: "Γberblick"
},
en: {
overview: "Overview"
},
es: {
overview: "VisiΓ³n general"
}
};
// intentionally left blank
.
βββ App.js
βββ node_modules
β βββ @fs
β βββ zion-locale
β βββ locales
β βββ de
β β βββ common-ui.json
β β βββ translation.json
β βββ en
β β βββ common-ui.json
β β βββ translation.json
β βββ es
β β βββ common-ui.json
β β βββ translation.json
β βββ index.js
βββ locales
βββ de
β βββ translation.json
βββ en
β βββ translation.json
βββ es
β βββ translation.json
βββ index.js
alienfast/i18next-loader

const jsonfile = require("jsonfile");
module.exports = function() {
const allLocales = getDirectoryNames();
const bundle = {};
//['en','de','es'].forEach
allLocales.forEach(locale => {
bundle[locale] = jsonfile.readFileSync(`./${locale}/translation.json`);
});
return "export default " + JSON.stringify(bundle);
};
.
βββ App.js
βββ node_modules
β βββ @fs
β βββ zion-locale
β βββ locales
β βββ de
β β βββ common-ui.json
β β βββ translation.json
β βββ en
β β βββ common-ui.json
β β βββ translation.json
β βββ es
β β βββ common-ui.json
β β βββ translation.json
β βββ index.js
βββ locales
βββ de
β βββ translation.json
βββ en
β βββ translation.json
βββ es
β βββ translation.json
βββ index.js
export default {
de: {
overview: "Γberblick"
},
en: {
overview: "Overview"
},
es: {
overview: "VisiΓ³n general"
}
};
alienfast/i18next-loader.js
Dynamic Import
export default {
de: {
overview: "Γberblick"
},
en: {
overview: "Overview"
},
es: {
overview: "VisiΓ³n general"
}
};
import i18n from 'i18next'
import(`./locales/${i18n.language}/translation.json`)
.then(localeStrings => {
i18n.addResources(i18n.language, 'translation', localeStrings)
})
new
old
import i18n from 'i18next'
import(`./locales/${i18n.language}/translation.json`)
.then(localeStrings => {
i18n.addResources(i18n.language, 'translation', localeStrings)
})
.
βββ App.js
βββ node_modules
β βββ @fs
β βββ zion-locale
β βββ locales
β βββ de
β β βββ common-ui.json
β β βββ translation.json
β βββ en
β β βββ common-ui.json
β β βββ translation.json
β βββ es
β β βββ common-ui.json
β β βββ translation.json
β βββ index.js
βββ locales
βββ de
β βββ translation.json
βββ en
β βββ translation.json
βββ es
β βββ translation.json
βββ index.js
Will this even work?
module.exports = function() {
return `
import i18n from 'i18next'
import(\`./locales/${i18n.language}/translation.json\`)
.then(localeStrings => {
i18n.addResources(i18n.language, 'translation', localeStrings)
})
`
};
Loader
// intentionally left blank
dynamic-import-loader

// App.js
import React from "react";
import "./locales";
import { useTranslation } from "react-i18next";
export default function App() {
const [t] = useTranslation();
return (
<div>
<h1>{t("overview")}</h1>
</div>
);
}
// App.js
import React from "react";
import /*translations from*/ "./locales";
// import { addTranslations } from "@fs/zion-locale";
import { useTranslation } from "react-i18next";
// addTranslations(translations);
export default function App() {
const [t] = useTranslation();
return (
<div>
<h1>{t("overview")}</h1>
</div>
);
}
// App.js
import React from "react";
import translations from "./locales";
import { addTranslations } from "@fs/zion-locale";
import { useTranslation } from "react-i18next";
addTranslations(translations);
export default function App() {
const [t] = useTranslation();
return (
<div>
<h1>{t("overview")}</h1>
</div>
);
}
i18next-http-backend
.
βββ App.js
βββ locales
Β Β βββ de
Β Β β βββ translation.json
Β Β βββ en
Β Β β βββ translation.json
Β Β βββ es
Β Β β βββ translation.json
Β Β βββ index.js
// App.js
import React from "react";
import /*translations from*/ "./locales";
// import { addTranslations } from "@fs/zion-locale";
import { useTranslation } from "react-i18next";
// addTranslations(translations);
export default function App() {
const [t] = useTranslation();
return (
<div>
<h1>{t("overview")}</h1>
</div>
);
}
{
"overview": "Overview"
}
// intentionally left blank
// App.js
import React from "react";
import translations from "./locales";
import { addTranslations } from "@fs/zion-locale";
import { useTranslation } from "react-i18next";
addTranslations(translations);
export default function App() {
const [t] = useTranslation();
return (
<div>
<h1>{t("overview")}</h1>
</div>
);
}
// https://edge.fscdn.org/assets/static/locales/en/translation-1h4l12412m.json
{
"overview": "Overview",
"ok": "OK",
"stuff": "Stuff",
}
.
βββ App.js
βββ node_modules
β βββ @fs
β βββ zion-locale
β βββ locales
β βββ de
β β βββ common-ui.json
β β βββ translation.json
β βββ en
β β βββ common-ui.json
β β βββ translation.json
β βββ es
β β βββ common-ui.json
β β βββ translation.json
β βββ index.js
βββ locales
βββ de
β βββ translation.json
βββ en
β βββ translation.json
βββ es
β βββ translation.json
βββ index.js
// intentionally left blank

// https://edge.fscdn.org/assets/static/locales/en/common-ui-123irnwelk.json
{
"common.things": "Common Things",
"cool": "Super cool"
}
module.exports = {}
// index.html
<script>
window.localeManifest = {
'en-translation': 'https://edge.fscdn.org/assets/static/locales/en/translation-1h4l12412m.json',
'en-common-ui': 'https://edge.fscdn.org/assets/static/locales/en/common-io-mmklk3i23.json',
}
</script>
<script src="https://edge.fscdn.org/assets/static/js/main-sdfisdg9e.js">
// @fs/zion-locale
import i18n from "i18next"
import Backend from "i18next-http-backend";
i18n
.use(Backend)
.init({
backend: {
loadPath: (locale, ns) => window.localeManifest?.[`${locale}-${ns}`]
}
});
Pros/Cons
http-backend
Pros
- Used by the community
- fewer requests
- Shared common-ui cache across apps
Cons
-
Major bump to zion-locale
-
need to figure out a way to load locales in storybook
-
could be loading unused locale strings
- issue with json file loading slowly/uncompressed from cdn
- Not backward compatible
Pros
- 100% backward compatible
- smaller payload size
- loads faster on slower networks
- webpack handles loading
Cons
-
our own implementation
- more requests
dynamic import
Adoption
- major bump @fs/react-scripts
- major bump @fs/zion-locale
- Add a <Suspense fallback="" /> as top level component
Β
- zion storybook will have to inject the manifest somehow
- zion tests may need to change
http-backend
- major bump @fs/react-scripts@6.0.0
- be on @fs/zion-locale@3.4.0
dynamic-import
Perfomance
Temple R9
alienfast/i18next-loader
i18next-http-backendΒ Β Β Β 788KB (27% smaller)
dynamic importΒ Β 618KB (43% smaller)
1086KB
Changelog
21% smaller
1284KB
1012KB
Merge
22% smaller
1240KB
967KB
Overview
11% smaller
1247KB
1106KB
Code
How can I test it?
npm i @fs/react-scripts@6.0.0-beta.6
npm i @fs/zion-locale@3.4.0-beta.0

Thanks guys
Per-locale presentation
By Tyler Graf
Per-locale presentation
- 728