storybook
storybook copied to clipboard
How to add "Locale" toolbar and integrate it with i18next (or other internationalization lib)
There are different ways to integrate localization into Pixi app/game, so usage of i18next is not required.
But most tutorials and docs are jumping around react-i18next
and its integration.
So if you are not using pixi-react
it may be tricky to attach all louse ends.
Bellow is my code implementation for "Locale" picker for a the Storybook toolbar.
First of all we need a global arg for a locale picker (Storybook supports nice selector definition from a toolbar
property):
/* .storybook/preview.js */
import { localeDecorator } from './localeDecorator';
// Create a global variable called locale in storybook
// and add a menu in the toolbar to change your locale
export const globalTypes = {
locale: {
name: 'Locale',
description: 'Internationalization locale',
toolbar: {
icon: 'globe',
items: [
{ value: 'en', title: 'English' },
{ value: 'ua', title: 'Ukrainian' },
],
dynamicTitle: true,
},
},
};
/* Other preview.js code */
// Add decorator to react for a locale change
export const decorators = [localeDecorator, /* ... other decorators */];
Next part was done following next tutorials:
- https://storybook.js.org/recipes/react-i18next
- https://storybook.js.org/docs/react/essentials/toolbars-and-globals (see top video)
Main part that is different from them is how to define a decorator that can rerender the story/componet. React trigger component rerender due to build in watching over the i18next state, but Pixi cannot do it. So we need to trigger component rerender manually (so our locale change is applied to the story/component). It can be done inside the decorator:
/* .storybook/localeDecorator.js */
import { getCurrentLang, setCurrentLang } from "../src/core/Translations";
import { FORCE_RE_RENDER } from '@storybook/core-events';
import { addons as previewAddons } from '@storybook/preview-api';
export const localeDecorator = (story, context) => {
// storyResult is the value returned from your Story render
const storyResult = story();
// store story result into context.canvasElement so it can be get from there in the interaction
const { locale } = context.globals;
const currentTranslationLang = getCurrentLang();
if (locale !== currentTranslationLang) {
setCurrentLang(locale);
previewAddons.getChannel().emit(FORCE_RE_RENDER);
}
// return story result as is
return storyResult;
};
Actually decorator (according to its name) should not trigger the rerender and such logic is more suitable to be done inside locale selector addon... (you may take a look on https://storybook.js.org/docs/react/essentials/toolbars-and-globals#updating-globals-from-within-an-addon ) But it is more easy to use decorator compared to creating a separate addon manually.
In code above src/core/Translations.js
is a wrapper over the i18next:
/* src/core/Translations.js */
import i18next from 'i18next';
import * as resources from "../locales";
i18next.init({
lng: 'en',
supportedLngs: ['en', 'ua'],
fallbackLng: 'en',
debug: true,
ns: Object.keys(resources),
resources
});
export const t = i18next.t;
// exports per namespaces
export const t_common = i18next.getFixedT(null, "common");
export const t_mech = i18next.getFixedT(null, "mech");
// locale picker utils
export const getCurrentLang = () => {
return i18next.resolvedLanguage;
};
export const setCurrentLang = (lang: string) => {
return i18next.changeLanguage(lang);
};
In this file i18next can be replaced with other internationalization lib or your own implementation.
IMPORTANT NOTE:
i18next lib needs help with resources loading (it is not doing it itself).
So I am using a @alienfast/i18next-loader
package to do it for me.
To do so I need a webpack rule:
...
module: {
rules: [
// important to be before ts-loader
{
test: /locales/,
loader: "@alienfast/i18next-loader",
options: {
basenameAsNamespace: true,
}
},
{
test: /\.ts(x)?$/,
loader: 'ts-loader',
exclude: /node_modules/
}
]
},
Same rule SHOULD be added to a Storybook too. To do it you need to modife .storybook/main.js
:
...
webpackFinal: async (config) => {
// add "@alienfast/i18next-loader" to a webpack to enable locales loading
config.module.rules = [
//should be in first position to be executed before Typescript loader
{
test: /locales/,
loader: "@alienfast/i18next-loader",
options: {
basenameAsNamespace: true,
}
},
...config.module.rules
];
// Return the altered config
return config;
},
So for me I have next file structure (IMPORTANT: see index.ts
- it is required for proper build process):
└── app
└── src
│ └── app.js
└── locales
├── index.ts <--- IMPORTANT: file that returns empty object {} as default export
├── de
│ ├── foo.json
│ └── bar.yaml
└── en
├── foo.json
└── bar.yaml