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
Install svelte-i18n library
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.
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 afallbackLocale.
Please note that thefallbackLocale
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 filesthe 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.
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!
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 categoryone
we can output custom messages within{...}
Here#
represents the value ofn
which will be1
in this case. Example:one { won exactly # award }
n=any-number
for plural categoryother
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
List of Methods in svelte-i18n
CLI commands reference