DEV Community

Sébastien Belzile
Sébastien Belzile

Posted on • Edited on

Using i18Next with Svelte

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
Enter fullscreen mode Exit fullscreen mode

or

npm install --save i18next
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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',
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

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);
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

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,
  };
};
Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

Then, in your templates, you can call:

{$t('Hello')}
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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} />
Enter fullscreen mode Exit fullscreen mode

And then a unit test would look like this:

const { getByText } = render(TestWrapper, {
  Component: MyComponent,
});
expect(getByText('my localization key')).toBeTruthy();
Enter fullscreen mode Exit fullscreen mode

Top comments (9)

Collapse
 
mweissen profile image
mweissen

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 should getContext 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:

<svelte:head>
    <script>
        import { initLocalizationContext } from "../i18n/index.ts";
        initLocalizationContext();      
    </script>
</svelte:head>
Enter fullscreen mode Exit fullscreen mode

Does this fit?

(3) Is the complete program as described in the post available somewhere to try?

Collapse
 
sbelzile profile image
Sébastien Belzile

(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)

<script>
    import { initLocalizationContext } from "../i18n/index.ts";
    initLocalizationContext();      
</script>

<svelte:head>
 [...]
Enter fullscreen mode Exit fullscreen mode

(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.

Collapse
 
varghesethomase profile image
varghese thomas e

As I could see, the initialisation of i18next happens twice. One in the constructor of i18n-service.ts and next in the constructor of translation-service.ts. I would assume this is not desired and causes loading of the translation file twice.

Collapse
 
sbelzile profile image
Sébastien Belzile

Good catch. I removed one of them from the snippets.

Collapse
 
nishugoel profile image
Nishu Goel

Hey! Well put, how do I specify the namespace in consuming space and get the translated string from the namespace?

Collapse
 
sbelzile profile image
Sébastien Belzile

(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:

t("namespace:key")
Enter fullscreen mode Exit fullscreen mode

Or you could expose (instead of t) a function that creates a derived store using i18next's getFixedT:

const getT = (ns: string | string[]) => derived([languageStore], $language => (key) => i18n.getFixedT($language, ns)(key));
Enter fullscreen mode Exit fullscreen mode
Collapse
 
sbelzile profile image
Sébastien Belzile

You would then use it this way:

<script>
/// ...
const t = getT('myNamespace')
</script>

{$t("my-key")}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
mawoka profile image
Mawoka

How do I auto-detect the language of the user and set it to this language?

Collapse
 
sbelzile profile image
Sébastien Belzile

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.