• Post category:IOS
  • Post comments:0 Comments
  • Post author:
  • Post published:15/12/2021
  • Post last modified:15/12/2021

Simple and performant, SolidJS is a declarative JavaScript library for creating user interfaces. Let’s see how to bootstrap a multilingual SolidJS project and localize it with the help of the popular I18next library. After completing this tutorial adding support for the localization of a typical SolidJS application, you should get a “solid” understanding of SolidJS i18n and be able to develop more complex applications with it.

🗒 Note » Get the source code for the demo app used in the tutorial on GitHub (with SolidJS v1.1.3 used at the moment of writing).

SolidJS library installation and setup

To start with, we’ll create a simple SPA application with the following components:

  • Locale switcher
  • Header component with a navbar
  • List of messages to translate and display

We’ll be translating the text elements of the application into either English or Greek. The user should be able to switch locales using the locale switcher. The preferred locale will be detected based on the current browser preference. Finally, the translations are requested on demand from the network. Using i18next, you can perform all those tasks by installing the relevant plugins.

SolidJS does not offer an i18next integration, but you can still add it by using the library component helpers that we’re going to show later on in this guide. Let’s start by installing a new SolidJS template project:

npx degit solidjs/templates/js solidjs-i18next

The command will only copy files into the solidjs-i18next folder so you will need to install the dependencies using yarn or npm:

$ cd solidjs-i18next
$ npm i

Once that’s done, you can inspect the folder tree:

❯ tree .
.
├── README.md
├── index.html
├── package.json
├── pnpm-lock.yaml
├── src
│   ├── App.jsx
│   ├── App.module.css
│   ├── assets
│   │   └── favicon.ico
│   ├── index.css
│   ├── index.jsx
│   └── logo.svg
└── vite.config.js

Now start the application to verify that it works:

$ npm run dev

vite v2.6.14 dev server running at:

> Local: http://localhost:3000/

The application is located at http://localhost:3000, and it installs the following dependencies:

  • Vite
  • SolidJS itself

The src folder contains the main source files and every time you edit one, the changes propagate to the browser. Open the App.jsx file to inspect the code:

src/App.jsx

import logo from "./logo.svg";
import styles from "./App.module.css";

function App() {
  return (
    <div class={styles.App}>
      <header class={styles.header}>
        <img src={logo} class={styles.logo} alt="logo" />
        <p>
          Edit <code>src/App.jsx</code> and save to reload.
        </p>
        <a
          class={styles.link}
          href="https://github.com/solidjs/solid"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn Solid
        </a>
      </header>
    </div>
  );
}

export default App;

We can now add the i18next dependency to the project.

Adding i18next to the project

Let’s add the following libraries to the project:

$ npm  i i18next i18next-xhr-backend  i18next-browser-languagedetector

It’s going to install the following packages:

  • I18next
  • http-backend to asynchronously load translations
  • LanguageDetector to automatically set a default language based on some criteria

We’ll need to create the i18 config object and initialize the i18next library. Create the following file:

src/i18n/config.js

import i18next from 'i18next';
import HttpApi from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';

const i18n = i18next
  .use(HttpApi)
  .use(LanguageDetector)
  .init({
    fallbackLng: 'en',
    whitelist: ['en', 'el'],
    preload: ['en', 'el'],
    ns: 'translations',
    defaultNS: 'translations',
    fallbackNS: false,
    debug: true,
    detection: {
      order: ['querystring', 'navigator', 'htmlTag'],
      lookupQuerystring: 'lang',
    },
    backend: {
      loadPath: '/i18n/{{lng}}/{{ns}}.json',
    }
  }, (err, t) => {
    if (err) return console.error(err)
  });

export default i18n;

What we’ll take care of here is the following:

  • Adding support for the LanguageDetector and HTTPBackend.
  • Specifying the preferred locales (English and Greek).
  • Specifying the default locale and the list of locales to preload.
  • Deciding on where the translation files should be located.
  • Specifying the location of the locale files to be requested from the backend.
  • Returning an error if something goes wrong.

Create two new folders and two new files under the public directory and place some simple messages:

i18n/el/translations.json

{
"title": "Καλώς ορίσατε στον πίνακα ελέγχου σας",
"messages_count_1": "Έχεται {{count}} μύνημα",
"messages_count_other": "Έχεται {{count}} μύνηματα"
}

i18n/en/translations.json

{
  "title": "Welcome to your dashboard",
  "messages_count_1": "You have {{count}} message",
  "messages_count_other": "You have {{count}} messages"
}

We’ve included some basic messages that we’re going to render into the application. However, we first need to plumb i18next config into the SolidJS environment.

Rendering translation messages using i18next

To use i18next with SolidJS, you need to load it before the application starts and expose it as a context parameter. You can start by creating this context provider:

src/i18n/context.js

import { createContext, useContext } from 'solid-js';

export const I18nContext = createContext();

export function useI18n() {
  const context = useContext(I18nContext);
  if (!context) throw new ReferenceError('I18nContext');

  return context;
}

We use the createContext and useContext functions to expose a context value with a hook-like function. Now we need to create the I18nProvider component in the same manner:

src/components/I18nProvider.jsx

import { I18nContext } from '../i18n/context';

export function I18nProvider(props) {
  return (
    <I18nContext.Provider value={props.i18n}>
      {props.children}
    </I18nContext.Provider>
  );
}

We’re taking care of all those aspects so we can expose the i18next instance to the whole application. Modify the App.jsx to consume this context:

src/App.jsx

import i18n from './i18n/config';
import {onMount, createSignal} from "solid-js";
import {Show} from "solid-js";
import {I18nProvider} from "./components/I18nProvider";
import i18next from "i18next";
import Header from "./components/Header";
import Messages from "./components/Messages";
const msgList = [
  {
    topic: "Event Cancelled",
    body: "The Fundraising event was cancelled"
  },
  {
    topic: "Notification send",
    body: "Check your email box for more information"
  }
]

function App() {
  const [loaded, setLoaded] = createSignal(false);
  onMount(async () => {
    await i18n;
    setLoaded(true);
  });
  return (
    <Show
      when={loaded()}
    >
      <I18nProvider i18n={i18next}>
        <Header />
        <Messages messages={msgList}/>
      </I18nProvider>

    </Show>
  );
}

Here’s what we’re doing here:

  • We use a createSignal hook, which is similar to React useState, for signaling when the i18next config instance is loaded; using the Show component from SolidJS, we can perform conditional rendering à la Suspense.
  • We use the onMount hook, which gets triggered once when the component is mounted; we resolve the i18n promise object that will trigger the http-backend and other plugins to load the translation messages; we, then, set the loaded signal to true; note that we call it loaded() so it retrieves the current value.
  • Inside the Show component, we use the I18nProvider,passing the i18next object; we know at this point that it’ll be initialized correctly and it’s ready to render translated messages.
  • We place two children components, Header and Messages, inside.

Inside the Header and Messages components, we render the translated messages using the useI18n hook:

src/components/Header.jsx

import styles from "../App.module.css";
import {useI18n} from "../i18n/context";

function Header(props) {
  const i18n = useI18n();
  return (
    <div className={styles.App}>
      <header className={styles.header}>
        <a
          className={styles.link}
          href="https://github.com/solidjs/solid"
          target="_blank"
          rel="noopener noreferrer"
        >
          {i18n.t('title')}
        </a>
      </header>
    </div>
  )
}

export default Header;

src/components/Messages.jsx

import styles from "../App.module.css";
import {useI18n} from "../i18n/context";

function Messages(props) {
  const i18n = useI18n();
  return (
    <div className={styles.Messages}>
      <p>{i18n.t('messages_count', {count: props.messages.length})}</p>
      {props.messages.map((msg ) => (
        <div className="message">
          <strong>{msg.topic}</strong>
          <br/>
          {msg.body}
        </div>
      ))}
    </div>
  )
}

export default Messages;

What we’ve done so far: We invoked the useI18n() hook that returns the i18next object offered by the API for translating messages. We used the “t” method to render any translated strings by providing a unique key. In the Messages component, we used the Plural call for the messages_count key, which will render the appropriate translation for plural values.

Please note that we didn’t use a destructuring assignment in the useI18n hook because that would break the reactivity detection. If we were to use const {t} = useI18n();, we would only be able to render the component message once and any subsequent updates wouldn’t trigger an update. This would create problems if we were to add a language switcher component, which would require an update of the translation values.

If you reload the application, you should see the following page in English:

At this point, you can also test the Greek translation as well by navigating to the http://localhost:3000/?lang=el endpoint. The way it works is that the LanguageDetector plugin, which we included earlier, parses the lang parameter and switches the locale to Greek once the page is loaded. However, we can do better by implementing a language switcher component without passing a parameter to the page. Here’s how to implement it step by step.

Switching between locales

Creating a locale switcher is one of the first tasks that you’d like to offer to the users of your application. Everyone should be able to switch to a language using a dropdown out of your list of supported locales. i18next offers a method called i18next.changeLanguage that can be used to change the current language being translated.

We’ll start by adding the language switcher component within the Header component.

Header.jsx

...
import {createSelector} from "solid-js";

function Header(props) {
  const i18n = useI18n();
  const isSelected = createSelector(() => i18n.language);
  const availableLocales = () => [
    {title: 'English', code: 'en'},
    {title: 'Greek', code: 'el'}
  ];
  return (
    <div className={styles.App}>
      <header className={styles.header}>
        ...
        <select onChange={(e) => props.onChange(e.target.value)}>
          <For each={availableLocales()}>
            {(item) => <option 
                         value={item.code} 
                         selected={isSelected(item.code)} 
                         classList={{ active: isSelected(item.code) }}>
                         {item.title}
                       </option>}
          </For>
        </select>
      </header>
    </div>
  )
}

Here’s an overview of the steps that:

  • We use a local createSelector variable to keep track of what language is currently selected from the list of availableLocales.
  • We use a For component offered by SolidJS to render a list of items that responds to reactive changes.
  • We use a onChange callback when the user selects a language from the list.

This will just display a select dropdown component. Now, we need to provide the onChange callback in the parent component:

App.jsx

...

function App() {
  const [loaded, setLoaded] = createSignal(false);
  const [translationChanged, updateTranslationChanged] = createSignal({});
  ...

  const handleOnChange = (lang) => {
    i18next.changeLanguage(lang).then(() => {
      updateTranslationChanged(...translationChanged()); // Re-render maybe?
    })
  }
  return (
    <Show
      when={loaded()}
    >
      <I18nProvider i18n={i18next}>
        <Header onChange={handleOnChange}/>
        <Messages messages={msgList}/>
      </I18nProvider>

    </Show>
  );
}

export default App;

However, you will soon find out that this strategy doesn’t work in SolidJS due to how its reactivity detection feature renders components. You won’t be able to see the updated i18next messages because we need to explicitly tell the I18nProvider that the current value of the i18next instance has changed. This means that we’ll have to wrap the i18next instance in a reactive wrapper and update it whenever we change the locale.

Let’s see how you can do it using a createStore hook:

/src/i18n/context.js

import { createContext, useContext } from 'solid-js';
import {createStore} from "solid-js/store";

export function createI18n(i18n) {
  const [store, setStore] = createStore({
    ...i18n,
    t: i18n.t.bind({}),
  });
  const updateStore = (i18n) => {
    setStore({
      ...i18n,
      t: i18n.t.bind({}),
    });
  }
  return [store, updateStore];
}

Here, we copied the i18n parameter object into another object we explicitly define properties for. Next, we’re exposing the updateStore callback so that the client code can update the store. This way, SolidJS can trigger the reactive callbacks whenever the store changes.

Here’s how to use it in your application:

src/App.jsx

function App() {
  const [loaded, setLoaded] = createSignal(false);{});
  const [i18nState, updatei18nState] = createI18n(i18next);
  onMount(async () => {
    await i18n;
    updateStore(i18next);
    setLoaded(true);
  });

  const handleOnChangeLanguage = (lang) => {
    i18next.changeLanguage(lang).then(() => {
      updatei18nState(i18next);
    })
  }
  return (
    <Show
      when={loaded()}
    >
      <I18nProvider i18n={i18nState}>
        <Header onChange={handleOnChangeLanguage}/>
        <Messages messages={msgList}/>
      </I18nProvider>
...

Now, when we call the changeLanguage method to change the current language, we first call the updateStore method we exposed earlier while creating the store. This will propagate through the I18nProvider value and render the updated messages. Here’s a demo interaction:

If you’ve worked with SolidJS for a while, you may recognize that libraries like i18next work outside the reactive model of this library so you’ll need to capture their internal state updates and trigger store updates whenever they get updated.

Dynamic loading of languages

Using i18next http-backend, you can control which languages load when you initiate the i18n instance. The config object we used in the init method accepts a preload parameter we can use for that purpose. Currently, it’s set to load all locales, but you can change that to load only English first:

preload: ['en']

If you inspect the network tab, you’ll see it loads only the English translation.json file:

When you change the language using the dropdown, it’ll request the new language translations file on demand and then update the messages as usual, without needing to change any logic in our code.

Fallback and supported locales

In case you have an app that attempts to use a locale we don’t have translations for, you should ideally serve the fallback ones. In our i18next configuration, we specified English as the fallback locale:

fallbackLng: 'en',

This means that the application will fallback to English when we request a locale that isn’t available—unless there’s a different order the user has configured in the browser. How do we know which ones are available you may ask? Well, we can have a whitelist of allowed locales we support using the supportedLngs parameter so we better add it there:

src/i18n/config.js

...
export const supportedLocales = ['en', 'el'];

const i18n = i18next
  .use(HttpApi)
  .use(LanguageDetector)
  .init({
     fallbackLng: 'en',
     supportedLngs: supportedLocales,
     whitelist: supportedLocales,
...

Now, if we were to load the page with the Spanish parameter, http://localhost:3000/?lang=es, our default English locale would be loaded instead. Note that (because we use the LanguageDetector plugin) this can only work with the following browser preference settings:

Here, English is preferred over Greek, so it will fallback to it if we request a non-supported locale. It would fallback to Greek, if we were to place it in a higher order than English. If you aren’t satisfied with this behavior, you can remove the plugin from the middleware list.

Basic translation messages

The structure of translation messages using i18next follows a simple key-value format. You assign message keys that correspond to the i18next.t(‘key’) parameter. For example:

{
    "first": "First message",
    "second": {
        "example": "second message"
    }
}

You can also provide a default value for a key if that doesn’t exist in the list of translations:

i18next.t('this_key_does_not_exist', 'Hidden message');

In our i18next config, we specified a single namespace called translations. This corresponds to the file name of the library to search for in the filesystem. You can have multiple namespaces if you want to split translation files among departments or app features.

src/i18n/config.js

...
ns: ['translations', 'common'],
defaultNS: 'translations'
...

The list of all options is specified in the official documentation.

Interpolation in messages

With interpolation, you can inject dynamic values into your translations when data comes from a remote service but you still want to provide a decently translated message. You just add curly braces surrounding a variable name you want to substitute at runtime with a value:

{
"is_making_something": "{{who}} is making {{what}}"
}

Then, in the application, you want to supply the variables by name:

i18next.t('is_making_something', {who: 'Alex', what: 'tea' });
// -> "Alex is making tea"

The full format on how to structure messages in i18next is referenced here.

Plurals

You can use interpolation to display messages that involve plural cases using a special format. To define plural messages, you’ll need to add certain postfix values to the keys denoting the count of elements for each individual cardinality case. We’ve already shown an example of the message list translations:

"messages_count_1": "Έχεται {{count}} μύνημα",
"messages_count_other": "Έχεται {{count}} μύνηματα"

The “_1” postfix string corresponds to a message for a single value. You can also use “_one” as well. Some languages handle different counts based on the meaning of the sentence and also offer different translations for different counts: “_2” or “_two” for 2 values, “_3” or “_three” for 3, and so on. The count interpolation parameter is something you can change and doesn’t need to have that name when you provide the values in the application:

i18next.t('messages_count_1', {count: 1}); // Έχεται 1 μύνημα

i18next.t('messages_count_other', {count: 2}); Έχεται 2  μύνηματα

With all those utilities and plugins, i18next is a great choice for localizing SolidJS applications even when there is no official plugin available.

Conclusion

If you’re building multilingual websites with SolidJS, you can choose from several popular libraries like i18next or FormatJS. To keep exploring, we suggest having a look at the following tutorials:

  • A React i18n Tutorial with FormatJS
  • Full-Stack JavaScript I18n Step by Step
  • Localizing StimulusJS Applications With I18next

If you’re looking to take your localization workflow to the next level, give Phrase a try. The fastest, leanest, and most reliable localization management platform worldwide will help you streamline the i18n process with everything you need to:

  • Build production-ready integrations with your development workflow
  • Invite as many users as you wish to collaborate on your projects
  • Edit and convert localization files with more context for higher translation quality

Sign up for a free 14-day trial, and see for yourself how it can make your life as a developer easier.

Leave a Reply