Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to add "Locale" toolbar and integrate it with i18next (or other internationalization lib) #13

Open
neatbyte-vnobis opened this issue Apr 16, 2023 · 0 comments

Comments

@neatbyte-vnobis
Copy link

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:

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant