Using i18Next with Svelte
Tags: svelte, i18next, testing
Published: 2020-10-24 21:53:00

If you use an internationalization framework with React, I am ready to bet it’s [i18Next](https://www.i18next.com/). It works well, is flexible, and has a [React library](https://github.com/i18next/react-i18next) to help you using it.

Although i18Next has a very long list libraries to [support various frameworks](https://www.i18next.com/overview/supported-frameworks), it does not have one for [Svelte](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:

```ts
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:

```ts
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](https://medium.com/i18n-and-l10n-resources-for-developers/a-step-by-step-guide-to-svelte-localization-with-svelte-i18n-v3-2c3ff0d645b8) 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:

```ts
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:

```ts
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.

```ts
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.

```ts
import { getLocalization } from '../i18n';
const {t} = getLocalization();
```

Then, in your templates, you can call:

```ts
{$t('Hello')}
```

You can also change the value of the local by calling the `locale` observable.

```ts
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](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](https://svelte-recipes.netlify.app/testing/#testing-the-context-api).

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:

```ts
<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:

```ts
const { getByText } = render(TestWrapper, {
  Component: MyComponent,
});
expect(getByText('my localization key')).toBeTruthy();
```