Internationalization in SvelteKit with svelte-i18n

This is the first article of three-part series to demonstrate i18n in SvelteKit. In this article, we will work on integrating svelte-i18n with SvelteKit.

svelte-i18n helps you localize your app using the reactive tools Svelte provides. By using stores to keep track of the current locale, dictionary of messages and format messages, it keeps everything neat, in sync and easy to use on svelte files.

Under the hood, svelte-i18n uses formatjs for localizing messages. It allows svelte-i18n to support the ICU message syntax.

Application Structure

Before we begin, I want to give you a detailed walkthrough of the application that we will be working on. You can find the code in the GitHub repository.

As we can see from the image, this will be a simple single-page application demonstrating i18n features like locale switching, pluralization and formatting.

In the next section, we will work on integrating the svelte-i18n library with SvelteKit.

Integration with SvelteKit

  1. Install svelte-i18n library

    • This first step is to install this library as a dependency in our SvelteKit project:

        pnpm i svelte-i18n
      
    • [OPTIONAL] If you're using VSCode and want to have your messages previewed alongside your components, check out the i18n-ally and their FAQ to see how to set it up.

  2. Define Locale Dictionary

    • Now that we have installed this library, it's time to identify the areas in our application that need localization. We then define the locale dictionaries within the src/lib/lang folder. A locale dictionary is a regular JSON object which contains message definitions for a certain language.

    • In our example application, I want five fields to be localized: The main heading, Lable for locale switching, Button label text, Body text (the paragraph) and Text for pluralization.

    • We also have the requirement for formatting date, time and currency but for that, we don't need a special entry in the locale dictionary. We will implement the same using inbuild methods provided by the svelte-i18n for formatting.

    • For the sake of this application, I will define dictionaries in three languages - English (en.json), Hindi (hi.json) and French (fr.json) within the src/lib/lang folder.

        // en.json
        {
          "heading": "Internationalization in SvelteKit",
          "toggle_label": "Select Locale",
          "button_label": "Generate Awards",
          "body_text": "This is a small example to demonstrate i18n functionality in SvelteKit using svelte-i18n library. svelte-i18n helps you localize your app using the reactive tools Svelte provides. By using stores to keep track of the current locale, dictionary of messages and to format messages, we keep everything neat, in sync and easy to use on your svelte files. Total number of npm downloads per week as of {date} are {download}.",
          "awards": "You have {n, plural, =0 { not won any awards } one { won exactly # award } other { won # awards }}!"
        }
      
        // hi.json
        {
          "heading": "SvelteKit में अंतर्राष्ट्रीयकरण",
          "toggle_label": "भाषा चुने",
          "button_label": "पुरस्कार उत्पन्न करें",
          "body_text": "svelte-i18n लाइब्रेरी का उपयोग करके SvelteKit में i18n कार्यक्षमता प्रदर्शित करने के लिए यह एक छोटा सा उदाहरण है। svelte-i18n, Svelte द्वारा प्रदान किए गए प्रतिक्रियाशील टूल का उपयोग करके आपके ऐप को स्थानीयकृत करने में आपकी सहायता करता है। वर्तमान स्थान, संदेशों के शब्दकोश पर नज़र रखने और संदेशों को प्रारूपित करने के लिए स्टोर का उपयोग करके, हम सब कुछ साफ-सुथरा, सिंक में रखते हैं और आपकी विस्तृत फ़ाइलों पर उपयोग में आसान रखते हैं। {date} तक प्रति सप्ताह NPM डाउनलोड की कुल संख्या {download} है|",
          "awards": "आपने {n, plural, =0 { कोई पुरस्कार नहीं जीता } one { बिल्कुल # पुरस्कार जीता } other { # पुरस्कार जीते }} है|"
        }
      
        // etc... all other languages that you wish to support
      
    • Pay attention to the syntax {value} Here {value} is a placeholder that will be populated with a value of a particular locale during runtime. As the locale changes, this field will be recomputed.

  3. Defining entry point and mode for initializing svelte-i18n library

    • Now that our library is installed and the locale dictionary is ready, it is time to create an entry point that will load the assets based on the user locale and initialize the library with a specific locale.

    • This entry point will be invoked as soon as the application bootstraps - once on the client side and once on the server side.

    • We will create the helper methods in the src/lib/i18n.ts file.

        import { browser } from '$app/environment';
        import { init, register } from 'svelte-i18n';
      
        const defaultLocale = 'en';
      
        register('en', () => import('./lang/en.json'));
        register('hi', () => import('./lang/hi.json'));
        register('fr', () => import('./lang/fr.json'));
      
        init({
          fallbackLocale: defaultLocale,
          initialLocale: browser ? window.navigator.language : defaultLocale
        });
      
    • The above code snippet loads the locale files (en.json, hi.json and fr.json) and registers them with the library. Now when the user switches the locale, the corresponding translation file will be used. We also provide initialLocale and a fallbackLocale. Please note that the fallbackLocale is always loaded, independent of the current locale, since only some messages can be missing. Let's go through each method in detail.

    • init() is responsible for configuring some of the library behaviors such as the global fallback and initial locales. Must be called before setting a locale and displaying your view.

    • register() adds a new dictionary of messages to a certain locale i.e. it will register your local dictionaries (en.json etc.) and get all translation keys ready for you.

    • Now there are two ways to register/add dictionaries to your locale:

      • the synchronous way using addMessages() that imports your locale JSON files

      • the asynchronous way using register() The asynchronous way is a more performant way to load your dictionaries. This way, only the files registered to the current locale will be loaded. As the locale value changes, it will automatically load the registered loaders for the new locale.

  4. Initialize the library on the server side

    • The rule is to initialize the svelte-i18n library with a locale as soon as the application boots up. For SSR we need to tell the server what language is being used. This could use cookies or the accept-language header for example. The easiest way to set the locale is in the server hook.

        import type { Handle } from '@sveltejs/kit';
        import { locale } from 'svelte-i18n';
      
        export const handle: Handle = async ({ event, resolve }) => {
          const lang = event.request.headers.get('accept-language')?.split(',')[0];
          if (lang) {
            locale.set(lang);
          }
          return resolve(event);
        };
      
    • In the above example, we intercept every request and look for a header with the key "accept-language" and set it as the current locale.

      For the sake of explanation, I am keeping things simple and limiting them to query headers only but you can extend this logic to query cookies and URL parameters to compute the locale and set it accordingly!

  5. Initialize the library on the client side

    • Once the backend is all wired up, we will now proceed with client-side initialization in the +layout.ts

        import { browser } from '$app/environment';
        import '$lib/i18n'; // Import to initialize. Very Important!
        import { locale, waitLocale } from 'svelte-i18n';
        import type { LayoutLoad } from './$types';
      
        export const load: LayoutLoad = async () => {
          if (browser) {
            locale.set(window.navigator.language);
          }
          await waitLocale();
        };
      
    • In the above code example, as soon as the browser is initialized, we set locale to the default language configured in the user's browser.

    • After that we invoke waitLocale() This method executes the queue of the locale. If the queue isn't resolved yet, the same promise is returned.

      For the sake of explanation, I am keeping things simple and limiting them to use the default language configured in the user's browser but you can extend this logic to set locale returned by parent layout or by any other means and set it accordingly!

Localizing Application

Before we start with localization, it is important to ensure the default locale is set up and that we do have an initial set of key-value message pairs for translations. To do so we can implement a derived store variable in the src/lib/i18n.ts file and react to it accordingly:

import { derived } from 'svelte/store';
import { locale } from 'svelte-i18n';

export const isLocaleLoaded = derived(locale, ($locale) => typeof $locale === 'string');
<div class="content">
  {#if $isLocaleLoaded}
    <h1>{$_('heading')}</h1>
  {:else}
    <div>Locale initializing...</div>
  {/if}
</div>

Now that we have configured the library and have the initial locale set, we're ready to start localizing our app. To do that we simply import $_ and pass message id in any component that needs to be translated.

The $_ is the implementation of format() from formnatjs. To format a message is as simple as executing the $_ method and passing the message-id:

<script>
  import { _ } from 'svelte-i18n';
</script>

<h1>{$_('page_title')}</h1>

It has two aliases $t and $format. So we can do any of the following:

<h1>{$_('page_title')}</h1>
<h1>{$t('page_title')}</h1>
<h1>{$format('page_title')}</h1>

From the context of our application, let us revisit our translation file:

{
  "heading": "Internationalization in SvelteKit",
  "toggle_label": "Select Locale",
  "button_label": "Generate Awards",
  "body_text": "This is a small example to demonstrate i18n functionality in SvelteKit using svelte-i18n library. svelte-i18n helps you localize your app using the reactive tools Svelte provides. By using stores to keep track of the current locale, dictionary of messages and to format messages, we keep everything neat, in sync and easy to use on your svelte files. Total number of npm downloads per week as of {date} are {download}.",
  "awards": "..."
}

To set the main heading, the label for locale switching and the button label text, we simply invoke $_() and pass in the message-id as explained above:

<h1>{$_('heading')}</h1>
<span>{$_('toggle_label')}: </span>
<button>{$_('button_label')}</button>

We can also pass additional parameters to $_() and the syntax is:

$_(messageId: string, options?: MessageObject): string
$_(options: MessageObject): string

interface MessageObject {
  id?: string;
  locale?: string;
  format?: string;
  default?: string;
  values?: Record<string, string | number | Date>;
}

In the message-id, we had something like:

"body_text": "...  {date} are {download}."

We can use the new syntax that we just saw and fill in the values of the date and the download placeholders ({ ... }) dynamically:

<p>{$_('body_text', {
  values: {
    download: $number(30242),
    date: $date(Date.UTC(2023, 6, 14, 0, 0, 0, 0), { year: "numeric", month: "long", day: "numeric" })
  }
})}</p>

For now, ignore $number() and $date() I'll discuss them in detail, in the following section about the Formatters. For now, switching to the next section where we will learn how to switch between locales!

Locale Switching

To switch between locales, we make use of the $locale store variable.

The locale store defines what is the current locale. When its value is changed, before updating the actual stored value, svelte-i18n sees if there are any message loaders registered for the new locale:

  • If yes, changing the locale is an async operation.

  • If no, the locale's dictionary is fully loaded and changing the locale is a sync operation.

<script lang="ts">
  import { locale } from 'svelte-i18n';

  let value: string = 'en';

  function handleLocaleChange(event: Event) {
    event.preventDefault();
    value = event?.target?.value;
    $locale = value;
  }
</script>

<select {value} on:change={handleLocaleChange}>
  <option value="en" selected>English</option>
  <option value="hi">Hindi</option>
  <option value="fr">French</option>
</select>

We can get the list of locales available in our application using the $locales

{#each $locales as locale, i}
  <option value={locale}>{locale.toUpperCase()}</option>
{/each}
<!-- $locales = ['en', 'hi', 'fr'] -->

While changing the locales, we can also make use of the $loading store that can detect if the app is currently fetching any enqueued message definitions.

<script>
  import { isLoading } from 'svelte-i18n'
</script>

{#if $isLoading}
  Please wait...
{:else}
  <Nav />
  <Main />
{/if}

We can also check the list of all messages with a particular locale using $dictionary The $dictionary store is responsible for holding all loaded message definitions for each locale.

<script>
  import { dictionary } from 'svelte-i18n'

  console.log($dictionary); // { en: {...}, hi: {...}, fr: {...} }
</script>

Pluralization

As I have mentioned before that svelte-i18n uses ICU syntax, and we can leverage that to implement Pluralization.

Consider the following message-id:

"awards": "You have {n, plural, =0 { not won any awards } one { won exactly # award } other { won # awards }}!"

If we break that into a simple format and compare it with ICU syntax { key, plural, matches }

{ n, plural, =0 { some-message } one { some-message # } other { some-message # } }

Here n is the key that will be passed via $_() followed by a plural keyword. matches resemble the rest of the conditions where if:

  • n=0 we can output custom messages within {...} Example: =0 { not won any awards }

  • n=1 for plural category one we can output custom messages within {...} Here # represents the value of n which will be 1 in this case. Example: one { won exactly # award }

  • n=any-number for plural category other we can output custom messages within {...} Example: other { won # awards }

Coming back to our example:

<span>{$_('awards', { values: { n: randomNumber } })}</span>
  • if n = 0, output = You have not won any awards!

  • if n = 1, output = You have won exactly 1 award!

  • if n = 10, output = You have won 10 awards!

Formatting

Coming to the last section of the article where I will discuss formatting Date, Time, Number and Currency using inbuild Formatters.

svelte-i18n provides inbuild formatters as well as provisions to create a custom formatter. For the sake of simplicity, I will only be explaining how to use inbuild formatters. You can refer to this guide on how to create custom formatters.

svelte-i18n provides $time(), $date() and $number() formatters that we can use to format data accordingly.

The following are the available Formats:

Number:

  • currency: { style: 'currency' }

  • percent: { style: 'percent' }

  • scientific: { notation: 'scientific' }

  • engineering: { notation: 'engineering' }

  • compactLong: { notation: 'compact', compactDisplay: 'long' }

  • compactShort: { notation: 'compact', compactDisplay: 'short' }

Date:

  • short: { month: 'numeric', day: 'numeric', year: '2-digit' }

  • medium: { month: 'short', day: 'numeric', year: 'numeric' }

  • long: { month: 'long', day: 'numeric', year: 'numeric' }

  • full: { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' }

Time:

  • short: { hour: 'numeric', minute: 'numeric' }

  • medium: { hour: 'numeric', minute: 'numeric', second: 'numeric' }

  • long: { hour: 'numeric', minute: 'numeric', second: 'numeric', timeZoneName: 'short' }

  • full: { hour: 'numeric', minute: 'numeric', second: 'numeric', timeZoneName: 'short' }

Coming back to our example application, we can now format date, time and currency:

<script lang="ts">
  import { time, date, number } from 'svelte-i18n';
</script>

<span>Time: { $time(new Date(), { hour: "numeric", minute: "numeric", second: "numeric" }) }</span>
<span>Date: { $date(new Date(), { year: "numeric", month: "long", day: "numeric" }) }</span>
<span>Currency: { $number(2, { style: "currency", currency: "INR" }) }</span>
<span>Number: { $number(2) }</span>

Conclusion

Finally, we were able to localize our application using svelte-i18n. You can find the code in the GitHub repo and link to the live demo.

This was the first article of three-part series to demonstrate i18n in SvelteKit. In the next article, I'll be explaining i18n in SvelteKit with sveltekit-i18n library.

References

Did you find this article valuable?

Support Aakash Goplani by becoming a sponsor. Any amount is appreciated!