Search code examples
javascriptreactjsinternationalizationreact-hookslinguijs

Creating a language switcher for i18n app


I am learning how to implement Lingui(i18n) on apps. Everything is setup, but I wanted to know how I should create a language swticher to change between language catalogs on my app.

This is my index.js file

import React, { useEffect } from "react";
import { render } from "react-dom";
import App from "./App";

import { I18nProvider } from "@lingui/react";
import { i18n } from "@lingui/core";
import { defaultLocale, dynamicActivate } from "./i18n";

const Translation = () => {
    useEffect(() => {
        dynamicActivate(defaultLocale);
    }, []);

    return (
        <I18nProvider i18n={i18n}>
            <App />
        </I18nProvider>
    );
};

render(<Translation />, document.getElementById("root"));

My App.js file

import "./App.css";
import { Trans } from "@lingui/macro";

function App() {
    return (
        <div className="App">
            <header className="App-header">
                <h1>
                    <Trans>HELLOO</Trans>
                </h1>
                <p>
                    <Trans>it's me.</Trans>

                </p>
            </header>
        </div>
    );
}

export default App;

and i18n.ts file

import { i18n } from '@lingui/core';

export const locales = {
  en: "English",
  es: "Spanish",
  fr: "French",
};

export const defaultLocale = "fr";

/**
* We do a dynamic import of just the catalog that we need
* @param locale any locale string
*/

export async function dynamicActivate(locale: string) {
  const { messages } = await import(`./locales/${locale}/messages`)
  i18n.load(locale, messages)
  i18n.activate(locale)
}

everytime I specify a es,en or fr defaultLocale the language changes, but I would like to have a language button to change this automatically on the page with select.

ex: "export const defaultLocale = "fr";" (in i18n.ts)


Solution

  • You can use i18n.activate() method to switch to needed locale.

    i18n object API is defined in @js-lingui/core.

    You also need to load the locale if it was not loaded before.

    In case of your project you can use handy dynamicActivate() function you've already created.

    Your component output will look like this:

    <div>
        <Trans>Switch to:</Trans>
        <button
            onClick={() => dynamicActivate('en')}>
            English
        </button>
        <button
            onClick={() => dynamicActivate('fr')}>
            Français
        </button>
        <button
            onClick={() => dynamicActivate('es')}>
            Espanol
        </button>
    </div>
    

    It will render 3 buttons [English] [Français] [Espanol] each one will load and activate needed locale.

    It is a best practice to keep the button captions in their own languages, so users can find a language they understand.

    As an addition to the above it probably makes sense to highlight currently-selected language and disable the button.

    I'm using useLingui() to get i18n.locale which indicates current language and set disabled flag on one of the buttons bellow.

    Here is the full code of LanguageSelector.js component for you, you can use it in App.js as <LanguageSelector />. Good luck with your project/learnings.

    import React from "react"
    import { useLingui } from "@lingui/react"
    import { Trans } from "@lingui/macro";
    import { dynamicActivate } from "./i18n";
    
    const LanguageSelector = () => {
        const { i18n } = useLingui();
    
        return <div>
                <Trans>Switch to:</Trans>
                <button
                    onClick={() => dynamicActivate('en')}
                    disabled={i18n.locale === 'en'}>
                    English
                </button>
                <button
                    onClick={() => dynamicActivate('fr')}
                    disabled={i18n.locale === 'fr'}>
                    Français
                </button>
                <button
                    onClick={() => dynamicActivate('es')}
                    disabled={i18n.locale === 'es'}>
                    Espanol
                </button>
        </div>
    };
    
    export default LanguageSelector
    
    

    UPDATED:

    Additionally you can persist selected locale to browser's LocalStorage

    We should save locale each time the dynamicActivate() gets called:

    const LOCAL_STORAGE_KEY = 'lang';
    
    function dynamicActivate(locale: string) {
      // existing code here
      window.localStorage.setItem(LOCAL_STORAGE_KEY, locale);
    }
    

    Apparently the @lingui/detect-locale library has very good coverage for detecting locale from many sources, including LocalStorage.

    Here's how it can be applied here:

    import { detect, fromUrl, fromStorage, fromNavigator } from '@lingui/detect-locale';
    
    // existing code from i18n.ts
    
    export const locales = {
      en: "English",
      es: "Spanish",
      fr: "French",
    };
    
    export const defaultLocale = "en";
    
    const LOCAL_STORAGE_KEY = 'lang';
    
    // checks that detected locale is available
    const isLocaleValid = (locale: string | null) => `${locale}` in locales;
    
    // returns locale 
    export function getLocale() {
      const detectedLocale = detect(
        fromUrl("lang"), // for example http://localhost:3000/?lang=es
        fromStorage(LOCAL_STORAGE_KEY),
        fromNavigator(), // from system settings
        () => defaultLocale,
      );
    
      return isLocaleValid(detectedLocale) ? detectedLocale : defaultLocale;
    }
    
    

    The last step is to call getLocale() instead of using defaultLocale all the time.

        useEffect(() => {
            // With this method we dynamically load the catalogs
            dynamicActivate(getLocale());
        }, []);