
Localization strategy
Software localization
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).
🔗 Resource » Check out our guide for a deep dive into everything you need to know about localization in JavaScript.
To start with, we'll create a simple SPA application with the following components:
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:
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.
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:
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:
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.
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:
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.
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:
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.
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.
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.
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.
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.
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.
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:
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:
Check out all Phrase features for developers, and see for yourself how it can make your life easier.
Last updated on September 23, 2022.