If you use an internationalization framework with React, I am ready to bet it’s i18Next. It works well, is flexible, and has a React library to help you using it.
Although i18Next has a very long list libraries to support various frameworks, it does not have one for Svelte. This article explains how to wire up i18Next with Svelte. The code in this article is written using Typescript.
Setup
First, let’s add i18Next to our Svelte project.
yarn add i18next
or
npm install --save i18next
i18Next Configuration
In your project folder dedicated to localization (src/i18n
if you need a suggestion), add a file called i18n-service.ts
and add the following code:
import i18next, { i18n, Resource } from 'i18next';
import translations from './translations';
const INITIAL_LANGUAGE = 'fr';
export class I18nService {
// expose i18next
i18n: i18n;
constructor() {
this.i18n = i18next;
this.initialize();
this.changeLanguage(INITIAL_LANGUAGE);
}
// Our translation function
t(key: string, replacements?: Record<string, unknown>): string {
return this.i18n.t(key, replacements);
}
// Initializing i18n
initialize() {
this.i18n.init({
lng: INITIAL_LANGUAGE,
fallbackLng: 'en',
debug: false,
defaultNS: 'common',
fallbackNS: 'common',
resources: translations as Resource,
interpolation: {
escapeValue: false,
},
});
}
changeLanguage(language: string) {
this.i18n.changeLanguage(language);
}
}
Here we have a class that initialize i18Next and exposes a translation function, and a method to change the language of the application.
Do the same with a translations.ts
file and add the minimum to it:
export default {
en: {
common: {
'Hello': 'Hello',
},
},
};
Here I am loading a typescript file, but in reality you can load JSON files, or load your translations from the server, it’s up to you. This other article explains how to implement other features which would be nice to add. Even though it uses another library, the same principles apply.
Setup Svelte
To use this with Svelte, we will create a translation service. Beside your i18n-service file, add a translation-service.ts
. Add the following code to it:
import type { I18nService } from './i18n-service';
import { derived, Readable, Writable, writable } from 'svelte/store';
export type TType = (text: string, replacements?: Record<string, unknown>) => string;
export interface TranslationService {
locale: Writable<string>;
translate: Readable<TType>;
}
export class I18NextTranslationService implements TranslationService {
public locale: Writable<string>;
public translate: Readable<TType>;
constructor(i18n: I18nService) {
this.locale = this.createLocale(i18n);
this.translate = this.createTranslate(i18n);
}
// Locale initialization.
// 1. Create a writable store
// 2. Create a new set function that changes the i18n locale.
// 3. Create a new update function that changes the i18n locale.
// 4. Return modified writable.
private createLocale(i18n: I18nService) {
const { subscribe, set, update } = writable<string>(i18n.i18n.language);
const setLocale = (newLocale: string) => {
i18n.changeLanguage(newLocale);
set(newLocale);
};
const updateLocale = (updater: (value: string) => string) => {
update(currentValue => {
const nextLocale = updater(currentValue);
i18n.changeLanguage(nextLocale);
return nextLocale;
});
};
return {
subscribe,
update: updateLocale,
set: setLocale,
};
}
// Create a translate function.
// It is derived from the "locale" writable.
// This means it will be updated every time the locale changes.
private createTranslate(i18n: I18nService) {
return derived([this.locale], () => {
return (key: string, replacements?: Record<string, unknown>) => i18n.t(key, replacements);
});
}
}
This code contains a class that exposes 2 observables:
A locale writable allowing you to read and modify the translation locale, and a translate
readable that wraps i18Next’s t
function.
The locale writable is constructed so that it updates the i18n-service
before updating it’s value.
The translate
readable is derived from the locale. This means every time the locale value changes, the translate readable will get notified and our Svelte template will be updated to the new language.
Wire it up
To wire that in our code, we will add an index.ts
placed beside our 2 other files. This file will contain an initialization method:
export const initLocalizationContext = () => {
// Initialize our services
const i18n = new I18nService();
const tranlator = new I18NextTranslationService(i18n);
// Setting the Svelte context
setLocalization({
t: tranlator.translate,
currentLanguage: tranlator.locale,
});
return {
i18n,
};
};
And 2 methods to set and get our observables from the Svelte context.
const CONTEXT_KEY = 't';
export type I18nContext = {
t: Readable<TType>;
currentLanguage: Writable<string>;
};
export const setLocalization = (context: I18nContext) => {
return setContext<I18nContext>(CONTEXT_KEY, context);
};
// To make retrieving the t function easier.
export const getLocalization = () => {
return getContext<I18nContext>(CONTEXT_KEY);
};
To wire it up, you need to call the initLocalizationContext
function from the root of your application (usually App.svelte
).
Using it
In your components, in your script tags, import and call the getLocalization
method that we created earlier.
import { getLocalization } from '../i18n';
const {t} = getLocalization();
Then, in your templates, you can call:
{$t('Hello')}
You can also change the value of the local by calling the locale
observable.
const {locale} = getLocalization();
const onClick = () => locale.update(current => current === 'en' ? 'fr' : 'en');
Unit testing
This would not be complete without an explanation about how to properly test a component that uses our module. In this section, we will be using the Svelte testing library.
Since we added our methods to the Svelte context API, they are in some way decoupled from our code. There is an easy way to inject mocks in Svelte’s context, using the method described here.
I usually go by having a test wrapper component that takes care of properly injecting my mocks into the context. Such a component can look like this:
<script lang="ts">
// [Insert imports here]
// Property to inject a mock, or a default t function
export let t: TType = (key: string) => key;
// The component to wrap.
export let Component: SvelteComponent;
// Why not go further with something to inject any kind of context property.
export let contexts: Record<string, any> | undefined = undefined;
// Initializing and injecting our mocks.
const tReadable = readable<TType>(t, () => undefined);
const currentLanguage = writable('fr');
setLocalization({
t: tReadable,
currentLanguage,
});
// Injecting our optional contexts.
if (!!contexts) {
Object.entries(contexts)
.forEach(entry => {
setContext(entry[0], entry[1]);
});
}
</script>
<!-- Template, rendering the wrapped component. -->
<svelte:component this={Component} />
And then a unit test would look like this:
const { getByText } = render(TestWrapper, {
Component: MyComponent,
});
expect(getByText('my localization key')).toBeTruthy();
Top comments (9)
I like the amount very much and that's why I wanted to try the program right away. I tried it and spent a few hours in the process. Unfortunately, I could not solve the following problems:
(1) I get:
getContext is not defined
(index.ts:31) Where shouldgetContext
be defined?(2) "To wire it up, you need to call the initLocalizationContext function from the root of your application (usually App.svelte)." I put this in the index.svelte file:
Does this fit?
(3) Is the complete program as described in the post available somewhere to try?
(1) Where should getContext be defined?
getContext
is provided by Svelte.Link to documentation: svelte.dev/tutorial/context-api
So,
import { getContext } from 'svelte';
(2) Does it fit?
index.svelte
is fine, though I don't think it should go inside a<svelte:head>
tag.I would place that in the Svelte script section of your file, not in the markup. (add the
script
tag directly to the root)(3) Is the complete program as described in the post available somewhere to try?
Unfortunately it is not, all the snippets are adapted copies of a non-public codebase I worked on.
I encourage you to go through the tutorial on the Svelte site: svelte.dev/tutorial/basics . It does not take long to complete and shows very well most of Svelte features. I feel like this page svelte.dev/tutorial/adding-data and this page svelte.dev/tutorial/context-api could have provided answers your questions.
As I could see, the initialisation of i18next happens twice. One in the constructor of
i18n-service.ts
and next in the constructor oftranslation-service.ts
. I would assume this is not desired and causes loading of the translation file twice.Good catch. I removed one of them from the snippets.
Hey! Well put, how do I specify the namespace in consuming space and get the translated string from the namespace?
(before you read: I did not test this answer nor did I spend a lot of time thinking about it, but this is the direction I would take)
Either directly:
Or you could expose (instead of
t
) a function that creates a derived store using i18next'sgetFixedT
:You would then use it this way:
How do I auto-detect the language of the user and set it to this language?
There are multiple ways to do this:
You may use i18next language detector, this will probably meet your requirements and will take 5 mins to leverage.
If you are looking for a more browser native solution, have a look at the
navigator.language
API. You could easily script something from there.