DEV Community

Sébastien Belzile
Sébastien Belzile

Posted on • Updated 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) {
    i18n.initialize();
    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

Discussion (2)

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 Author

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