Software localization

How to Localize a Svelte App with svelte-i18n

Svelte-i18n is a light wrapper around FormatJS that uses Svelte stores to provide a no-frills i18n solution. Learn how to use it.
Software localization blog category featured image | Phrase

I'll be honest with you, when I came upon Svelte, I thought to myself: "Oh come on, not another JavaScript framework. I'm just about to get proficient with React. I can't take another one of these." But I jumped in and used Svelte for a while, and I'm happy that I did. Of course, if you use Svelte, you already know that it's a compiler, not a framework that ships with your production code. This makes your apps smaller and faster than "traditional" JavaScript frameworks and libraries like React, Angular, and Vue. But that's just the tip of the iceberg. Svelte has learned from its predecessors and provides an exceptionally elegant development experience. It's telling that the entire Svelte API documentation is effectively one page: the framework is as minimal in its design as its output, and that's a good thing. But if you're here, you probably already know that.

So what about internationalizing our Svelte apps? Other JS frameworks have a slew of i18n library options to choose from. What do we have to use for Svelte? Well, one option is svelte-i18n by Christian Kaisermann. It's a light wrapper around FormatJS that uses Svelte stores to provide a no-frills i18n solution with ICU message support. That's a good start, and we can build on top of that. In fact, in this article we'll do just that, internationalizing a small demo Svelte app with svelte-i18n.

Note » If you're not familiar with FormatJS or ICU messages, don't worry. It's not necessary to know about them to follow along here.

Our Demo app

Alright, enough with the chit-chat. Let's get to the code. We have a starter project so that we can get right to the i18n work. Here's what it looks like so far:

Demo app | Phrase

Our demo app, Flimic, is a list of films

You can download all the code for our demo on Github. If you want to work along with us here, just download the repo and checkout the start branch.

Our app, Filmic, is a curated list of eighties Hollywood movies. Our goal is to make the app's UI available both in English, our source locale, and Arabic. You can use any locales here, of course. By the time we're done, we will have accomplished the following:

  • Walkthrough of the starter project code
  • Installing and setting up the svelte-i18n library
  • Dynamic, per-locale translation file loading
  • Handling locale direction: left-to-right VS right-to-left
  • Using basic translation messages, interpolation, plurals, and date formatting
  • Building a simple locale switching UI for our users

When we're done, our app will look like this:

Localized demo app | Phrase

Our completed app will be localizable to any locale of our choosing

Btw who else owned both the Ghostbusters II and Batman soundtracks? No? No one? Ok, awkward. Let's start.

The Starter Project Code

Our starter project isn't terribly complex. However, let's quickly go over how it's put together so that we're comfortable with our starting point.

The starter project's component hierarchy looks like the following.

App

|

+---Header

|

+---MovieGrid

|   |

|   +---Movie

|   |

|   +---Movie

|   |

|   +---...

|

+---Footer

Header and Footer are just bread-and-butter layout components that contain text. MovieGrid is a bit more interesting: it's a stateful component that loads movie data from a JSON file.

<script>

    import Movie from './Movie.svelte';

    function fetchMovies() {

        return fetch('/data/movies.json')

                .then(response => response.json());

    }

</script>

{#await fetchMovies()}

    <p>Loading...</p>

{:then movies}

    <div class="columns is-mobile is-multiline">

        {#each movies as movie}

            <div class="column is-one-third-desktop is-half-tablet is-full-mobile">

                <Movie {movie} />

            </div>

        {/each}

    </div>

{:catch error}

    <p>There was a problem loading movies.</p>

Each movie item is passed to a presentational Movie component that displays the item.

<script>

    export let movie;

    const {

        title,

        imageUrl: src,

        awardWinCount,

        awardNominationCount,

        releaseDate: released,

    } = movie;

</script>

<style>

    /* Style code removed for brevity. Check out the full repo if

       you want to get at the CSS. */

</style>

<div class="box">

    <div class="columns is-mobile">

        <div class="column is-one-quarter img-container">

            <img {src} alt="Poster for {title}">

        </div>

        <div class="column">

            <h3 class="is-size-5 is-uppercase movie-title">{title}</h3>

            <p class="release-date">Released {released}</p>

            <p class="awards">Won {awardWinCount} awards</p>

            <p class="nominations">Nominated for {awardNominationCount} awards</p>

        </div>

    </div>

</div>

Here's a sample of our data from movies.json:

[

    {

        "id": 1,

        "title": "Ghostbusters",

        "releaseDate": "1984-06-08",

        "awardWinCount": 6,

        "awardNominationCount": 8,

        "imageUrl": "https://upload.wikimedia.org/wikipedia/en/2/2f/Ghostbusters_%281984%29_theatrical_poster.png"

    },

    {

        "id": 2,

        "title": "Batman",

        "releaseDate": "1989-06-23",

        "awardWinCount": 9,

        "awardNominationCount": 26,

        "imageUrl": "https://upload.wikimedia.org/wikipedia/en/5/5a/Batman_%281989%29_theatrical_poster.jpg"

    },

    // ...

]

At the moment, our UI text is all hard-coded, and our data is only presented in English. Let's address this and internationalize our app with svelte-i18n.

Installation & Setup

We'll start by pulling svelte-i18n into our project through NPM.

# With NPM

npm install svelte-i18n

# With Yarn

yarn add svelte-i18n

✋🏽 Heads up » After installing svelte-i18n and running your development server, you may get warnings that say, "(!) this has been rewritten to undefined". This is a known problem when using Rollup with some modules. The code the warnings are refering to seems to work just fine in our case, and Rollup is probably being a bit overzealous with its warnings. However, if you want to address the issue and get rid of the warnings, you can modify the project's Rollup configuration to pass the modules the value of this that they expect. We've done the work for you already in our Github repo, so check it out there.

That's all we need to install the library. Now let's use it to internationalize our app. We'll cut our teeth with the Header component since it's nice and simple. Here's the current code for Header:

<header class="hero">

    <div class="hero-body">

        <div class="contiainer">

            <h1 class="title">Filmic</h1>

            <h2 class="subtitle">A curated collection of eighties movies</h2>

        </div>

    </div>

</header>

We'll want to update our .title and .subtitle so that they display dynamic, translated messages. Let's head over to our root App component and set up those messages.

<script>

    import { dictionary, locale } from 'svelte-i18n';

    // ...

    dictionary.set({

        en: {

            app: {

                title: 'Filmic',

                subtitle: 'A curated collection of eighties movies',

            },

        },

        ar: {

            app: {

                title: 'فيلميك',

                subtitle: 'مجموعة أفلام مختارة من الثمنينات',

            },

        },

    });

    locale.set('en');

</script>

<!-- ... -->

<Header />

<main role="main">

    <MovieGrid />

</main>

<Footer />

svelte-i18n provides regular Svelte stores for setting its messages dictionary, and the active locale.

✋🏽 Heads up » We need to set our messages dictionary before we set our active locale or svelte-i18n will throw an error.

The Messages Dictionary

The library uses a simple key-value map for its messages. Top-level keys are locale codes like en, ar, or fr-CA. Underneath each locale code is the locale's translated messages. The messages themselves are keyed by IDs that we can use elsewhere in our app. Let's use these messages in our Header component now.

<script>

    import { _ } from 'svelte-i18n';

</script>

<header class="hero">

    <div class="hero-body">

        <div class="contiainer">

            <h1 class="title">{$_('app.title')}</h1>

            <h2 class="subtitle">{$_('app.subtitle')}</h2>

        </div>

    </div>

</header>

Another store, _() (underscore), is provided by svelte-i18n for retrieving our messages for the current locale. We just need to provide a message's ID to the function, like _('app.title').

🔗 Resource » You can use format() instead of _() if you want, as the latter is just an alias of the former. Check out the svelte-i18n documentation for more info.

Notice that we can nest our messages in our dictionary, and retrieve deeply nested messages with dot notation. Here, we've nested both our title and subtitle messages under app, and we can access them with something like "app.title". This isn't a requirement, of course, and we can use a flat list of messages under each locale, or mix and match flat and nested messages.

Also notice that we're using the $ shortcut notation that Svelte provides us. The $ tells Svelte to subscribe to the _ store for us, and to unsubscribe when the current component is destroyed. This avoids memory leaks while keeping our component reactive to the store: when either the current locale or dictionary change, our Header component will re-render with updated message values.

Refactoring to a Custom i18n Adapter

We probably don't want to leave our setup code in App.svelte, as that can get messy as our app grows. Let's clean things up and create a little /src/services/i18n.js wrapper module that houses our custom i18n logic.

import { dictionary, locale, _ } from 'svelte-i18n';

function setupI18n({ withLocale: _locale } = { withLocale: 'en' }) {

    dictionary.set({

        en: {

            app: {

                title: 'Filmic',

                subtitle: 'A curated collection of eighties movies',

            },

        },

        ar: {

            app: {

                title: 'فيلميك',

                subtitle: 'مجموعة أفلام مختارة من الثمنينات',

            },

        },

    });

    locale.set(_locale);

}

export { _, setupI18n };

We wrap our initialization logic in a setupI18n function, which we expose along with svelte-i18n's _() . Our App component can now be a lot cleaner.

<script>

    import { setupI18n } from './services/i18n';

    // ...

    setupI18n({ withLocale: 'en' });

</script>

<!-- ... -->

We can now import our forwarded _() from our custom wrapper whenever we want to display translated messages in our components.

<script>

    import { _ } from '../../services/i18n';

</script>

<header class="hero">

    <div class="hero-body">

        <div class="contiainer">

            <h1 class="title">{$_('app.title')}</h1>

            <h2 class="subtitle">{$_('app.subtitle')}</h2>

        </div>

    </div>

</header>

All we had to change in our Header component was our import statement.

This refactor makes our code more flexible. We can potentially swap another library in place of svelte-i18n in the future. The new code design also allows us to add custom i18n logic without dramatically affecting the rest of our app.

A Note on Advanced Locale Detection

According to the svelte-i18n documentation, the library provides locale detection through the browser's window.navigator.language, a URL query param, or the URL hash/anchor. This is provided via a getClientLocale() function. I havent's used getClientLocale() myself, but you can find out more in the docs. If you do give the library's locale detection a try, please let us know how that worked out for you in the comments below.

Dynamic Loading of Translation Files via HTTP

We're currently hard-coding our translation messages in our i18n setup code. While this might be OK for very small projects, we often want to break out our translations into one file per locale. Let's do that now.

First, we'll move our current messages to files under /public/lang. This allows these files to be served directly by the web server we're working with.

/public/lang/en.json

{

    "app": {

        "title": "Filmic",

        "subtitle": "A curated collection of eighties movies"

    }

}

/public/lang/ar.json

{

    "app": {

        "title": "فيلميك",

        "subtitle": "مجموعة أفلام مختارة من الثمنينات"

    }

}

We'll have a /public/lang/{locale-code}.json file for each locale our app supports. With those files in place, we can update our setup logic to load them in when needed.

✋🏽 Heads up » When moving our messages to our new files, we need to make sure that we're always using double quotes around string values, and avoiding trailing commas, to adhere to JSON syntax.

import { dictionary, locale, _ } from 'svelte-i18n';

const MESSAGE_FILE_URL_TEMPLATE = '/lang/{locale}.json';

function setupI18n({ withLocale: _locale } = { withLocale: 'en' }) {

    const messsagesFileUrl = MESSAGE_FILE_URL_TEMPLATE.replace('{locale}', _locale);

    return fetch(messsagesFileUrl)

        .then(response => response.json())

        .then((messages) => {

            dictionary.set({ [_locale]: messages });

            locale.set(_locale);

        });

}

export { _, setupI18n };

We use the standard fetch API to load the message file for the given _locale. We also return the result of the fetch promise chain, itself a promise. This allows calling code to be notified when our i18n setup is complete: the message file has been loaded, its contents placed in the dictionary store, and the current locale is set.

Rendering Only After Our Locale Has Loaded

If we were to refresh our app in the browser right now, we'd get an error and our app wouldn't load. This is because our current app logic doesn't wait for our translation messages to load from the network. Our $_() calls in Header, for example, won't have any messages to work with on first render. Let's correct this.

We'll add a helper store, isLocaleLoaded, which will let us know whether translation messages are ready.

import { derived } from 'svelte/store';

import { dictionary, locale, _ } from 'svelte-i18n';

// ...

const isLocaleLoaded = derived(locale, $locale => typeof $locale === 'string');

export { _, setupI18n, isLocaleLoaded };

isLocaleLoaded is just a derived store: whenever the active locale is updated, so is isLocaleLoaded. Before any locale is set, svelte-i18n will give locale an object type. Once it is correctly set, the libray will set locale to the code of the active locale, e.g. "en", a string type. We check for this in our devired store, and make sure that isLocaleLoaded's value is true only after i18n initialization is successful.

We can now use this derived store in our App component to fix our error.

<script>

    import { setupI18n, isLocaleLoaded } from './services/i18n';

    // ...

    $: if (!$isLocaleLoaded) {

        setupI18n({ withLocale: 'en' });

    }

</script>

<!-- ... -->

{#if $isLocaleLoaded}

    <Header />

    <main role="main">

        <MovieGrid />

    </main>

    <Footer />

{:else}

    <p>Loading...</p>

{/if}

With the new $isLocaleLoaded store state, we show a loading indicator while our message file is coming down the pipe. Once the file is loaded and svelte-i18n has been set up, $isLocaleLoaded becomes true, which causes the app UI to render. Bye bye, error.

🔗 Resource » In case you're wondering, the $: syntax above makes a statement reactive in Svelte components. These reactive statements can make for very consice code, and pair really well with Svelte stores, so check them out in the Svelte documentation if you haven't come across them before.

Handling Locale Direction: Right-to-Left VS Left-to-Right

Currently, when we select Arabic, our layout direction remains left-to-right. Arabic is a right-to-left language, and the UI just looks awkward in that locale now. Let's fix this by adding another derived store to our i18n library.

import { derived } from 'svelte/store';

import { dictionary, locale, _ } from 'svelte-i18n';

// ...

const dir = derived(locale, $locale => $locale === 'ar' ? 'rtl' : 'ltr');

export { _, locale, dir, setupI18n, isLocaleLoaded };

The dir store's value is "rtl" if the active locale is Arabic, and "ltr" otherwise. This works fine for our little demo app with two locales. Of course, in a bigger app we would need some more complex logic and perhaps a map of locales to directions. Let's update our App component to use our new store.

<script>

    import { setupI18n, isLocaleLoaded, locale, dir } from './services/i18n';

    // ...

    $: if (!$isLocaleLoaded) {

        setupI18n({ withLocale: 'en' });

    }

    $: { document.dir = $dir; }

</script>

<!-- ... -->

We simply react to any changes in $dir, which itself is updated when the underlying active locale is updated, and manually set the direction of the document DOM element. This makes the browser show our app's page in a right-to-left orientation when we set our locale to Arabic, and left-to-right otherwise.

Working with Translation Messages

At this point we've seen how to work with basic translation messages. We have keyed messages in our translation files that we access via the $_('myMessageId') store function in our components. Let's take a look at more complex examples of translation messages as we explore interpolation, plurals, and date formatting.

Interpolation

Under the hood, svelte-i18n uses the FormatJS library collection, which itself uses the ICU message defacto standard. ICU messages use {curly braces} for interpolated values. Let's update the Footer of our demo to get a taste of ICU interpolation. The current footer is a presentational component that has a simple implementation.

Demo app footer | Phrase

The end of all things: our footer

<style>

    .footer {

        margin-top: 2rem;

        background-color: #4a4a4a;

        color: #f7f7f7;

    }

    a, a:active, a:hover, a:visited {

        color: #65b6e3;

    }

</style>

<footer class="footer">

    <div class="content has-text-centered">

        <p>

            Companion to a <a href="https://phrase.com/blog">Phrase blog</a> article.

            Made with <a href="https://svelte.dev/">Svelte</a> &amp;

            <a href="https://bulma.io/">Bulma</a>.

        </p>

    </div>

</footer>

While the text of our footer paragraph will change per locale, the URLs of the links within the paragraph won't. We can pass those URLs to our translated messages as named variables. We use {curly braces} to denote those variables in our translation files.

{

    // ...

    "footer": {

        "line1": "Companion to a <a href=\"{phraseUrl}\">Phrase blog</a> article.",

        "line2": "Made with <a href=\"{svelteUrl}\">Svelte</a> &amp; <a href=\"{bulmaUrl}\">Bulma</a>."

    }

}

The svelte-i18n $_() function takes a second parameter after the message ID: a map of keyed values to swap into our messages dynamically. So our updated Footer component code can look like this:

<script>

    import { _ } from '../../services/i18n';

</script>

// ...

<footer class="footer">

    <div class="content has-text-centered">

        <p>

            {@html $_('footer.line1', { phraseUrl: 'https://phrase.com/blog' })}

            {@html $_('footer.line2', {

                svelteUrl: 'https://svelte.dev/',

                bulmaUrl: 'https://bulma.io',

            })}

        </p>

    </div>

</footer>

✋🏽 Heads up » Note that we're using the potentially unsafe @html modifier in our Svelte template here. @html will tell Svelte not escape any HTML code before outputting our dynamic values. This works for us here, since we want to output the <a> tags in our translations messages as unescaped HTML. However, we should always make sure that we trust the source of any data we output with @html, since Svelte won't sanitize the data for potential XSS injection attacks.

When passing our map to $_() we just make sure to match the keys in our object map to the keys in our messages. svelte-i18n and its underlying plumbing will take care of the rest.

Working with Tags & Style Scope in Our Messages

By embedding the <a> tag in our translation messages, we don't have Svelte applying its component-scoped styles to our link text anymore. This is due to the extra, hashed CSS class that Svelte adds to our component HTML when it renders it, or the lack of it in our case.

Our component's parent footer tag, for example, is rendered to the browser as something like <footer class="footer svelte-vwdjom">. The svelte-{hash} CSS class allows the footer's corresponding styles to be scoped to our component, and not leak out to other components, when they're rendered out to our /public/bundle.css:

/* ... */

.footer.svelte-vwdjom {

  margin-top: 2rem;

  background-color: #4a4a4a;

  color: #f7f7f7;

}

This often helps us work with styles in a nice, modular way. However, Svelte needs our HTML to be directly in our .svelte files to be able to add its hashed CSS classes to it. When we moved our <a> tags outside to our message .json files, Svelte lost sight of them.

We can work around this issue by using the special :global() syntax that Svelte provides. Here's the updated Footer.svelte code using :global().

<script>

    import { _ } from '../../services/i18n';

</script>

<style>

    .footer {

        margin-top: 2rem;

        background-color: #4a4a4a;

        color: #f7f7f7;

    }

    .footer :global(a),

    .footer :global(a:active),

    .footer :global(a:hover),

    .footer :global(a:visited) {

        color: #65b6e3;

    }

</style>

<footer class="footer">

    <div class="content has-text-centered">

        <p>

            {@html $_('footer.line1', { phraseUrl: 'https://phrase.com/blog' })}

            {@html $_('footer.line2', {

                svelteUrl: 'https://svelte.dev/',

                bulmaUrl: 'https://bulma.io',

            })}

        </p>

    </div>

</footer>

view rawFooter.svelte hosted with

The .footer :global(a) selector will scope its style rules to any <a> element under this component's .footer, whether that <a> is explicitly defined in the component's template or not. This effectively fixes our styling problem, and returns our link styles to where they were before we moved them to our JSON files.

Plurals

Right now our movies' stats are being displayed without regard for plurality.

Demo app without plurals | Phrase

"1 awards" is just wrong, isn't it? - Also GB won 6 awards and was nominated for 8

Alright let's correct this by using the ICU plural messages that come with svelte-i18n. We'll move our relevant messages to our language files while we're at it.

{

    // ...

    "movie": {

        "won_awards": "Won {n, plural, =0 {no awards} one {# award} other {# awards}}",

        "nominated_for_awards": "Nominated for {n, plural, =0 {no awards} one {# award} other {# awards}}"

    },

    // ...

}
{

    // ...

    "movie": {

        "won_awards": "{n, plural, =0 {لم يحوز على جوائز} one {حاز على جائزة #} two {حاز على جائزتين} few {حاز على # جوائز} other {حاز على # جائزة}}",

        "nominated_for_awards": "{n, plural, =0 {لم يرشح لجوائز} one {رشح لجائزة #} two {رشح لجائزتان} few {رشح لـ# جوائز} other {رشح لـ# جائزة}}"

    },

    // ...

}

The {key, plural, matches} syntax is standard ICU stuff. If it looks weird to you, check out the FormatJS documentation for a nice, short intro. Also worth noting is that Arabic has more plural forms than English. ICU messages accommodate this and allow us to specify different plural forms/categories for each locale. Now let's use our new messages in our Movie component.

<script>

    import { _ } from '../../services/i18n';

    export let movie;

    const {

        title,

        imageUrl: src,

        awardWinCount,

        releaseDate: released,

        awardNominationCount: nominationCount,

    } = movie;

</script>

// ...

<div class="box">

    <div class="columns is-mobile">

        <div class="column is-one-quarter img-container">

            <img {src} alt="Poster for {title}">

        </div>

        <div class="column">

            <h3 class="is-size-5 is-uppercase movie-title">{title}</h3>

            <p class="release-date">Released {released}</p>

            <p class="awards">

                {$_('movie.won_awards', { n: awardWinCount })}

            </p>

            <p class="nominations">

                {$_('movie.nominated_for_awards', { n: nominationCount })}

            </p>

        </div>

    </div>

</div>

We use the $_() function as normal to access our plural messages. We also pass in a variable that gives svelte-i18n the number it uses to select the appropriate plural form from the ones we defined in our message files. We call this variable n here as a matter of convention. Its name doesn't matter, however, as long as it matches the name in our plural messages. And that's our plurals taken care of.

Demo app with right plural form | Phrase

Now this adds up: we're presenting proper plurals

Date Formatting

We're currently displaying the release date of each movie as it is in our JSON data.

Demo app without date localization | Phrase

How do we format our dates while respecting locales?

We'd like to show the date in a natural way for the active locale. This is easy enough to accomplish with svelte-i18n. Let's first move our whole "Released 1984-06-08" string to our language files so that we can translate it.

{

    // ...

    "movie": {

        // ...

        "released_at": "Released {date}"

    },

    // ...

}
{

    // ...

    "movie": {

        // ...

        "released_at": "صدر {date}"

    },

    // ...

}
// ...

<div class="box">

    <div class="columns is-mobile">

        <!-- ... -->

        <div class="column">

            <p class="release-date">

                {$_('movie.released_at', { date: released })}

            </p>

            <!-- ... -->

        </div>

    </div>

</div>

Nothing new here. In fact, our updated code won't handle localized date formatting yet. We can use svelte-i18n's $_.date() function to localize our date.

ie.svelte

// ...

<div class="box">

    <div class="columns is-mobile">

        <!-- ... -->

        <div class="column">

            <p class="release-date">

                {$_('movie.released_at', { date: $_.date(new Date(released), 'long')})}

            </p>

            <!-- ... -->

        </div>

    </div>

</div>

$_date() requires a Date object as its first parameter, so we convert our released string to a Date before we pass it to the function. An optional second parameter can be "short" | "medium" | "long" and is used to display dates in preset formats.

🔗 Resource » See the svelte-i18n documentation for more info about date formatting, number formatting, and more.

Localized dates in demo app | Phrase

Localized dates with very little code

While the svelte-i18n options work fine for us if we want predefined short, medium, or long dates, they don't give us exactly what we want here. We only want to show each movie's release year, and we want that localized. Unfortunately, at the time of writing this is not possible with svelte-i18n. The infrastructure looks to be there: svelte-i81n is built on top of FormatJS, which itself uses the standard Intl.DateTimeFormat for its date formatting. Intl.DateTimeFormat has extensive options for date formatting. But since svelte-i18n doesn't expose these options to us, we have to use Intl.DateFormat directly. We can add a little function to our custom i18n library that does what we want.

import { derived } from 'svelte/store';

import { dictionary, locale, _ } from 'svelte-i18n';

const MESSAGE_FILE_URL_TEMPLATE = '/lang/{locale}.json';

// For use in our own custom functions

let cachedLocale;

function setupI18n({ withLocale: _locale } = { withLocale: 'en' }) {

    const messsagesFileUrl = MESSAGE_FILE_URL_TEMPLATE.replace('{locale}', _locale);

    return fetch(messsagesFileUrl)

        .then(response => response.json())

        .then((messages) => {

            dictionary.set({ [_locale]: messages });

            // Keep a cached copy of the locale

            cachedLocale = _locale;

            locale.set(_locale);

        });

}

function formatDate(date, options) {

    return new Intl.DateTimeFormat(cachedLocale, options)

        .format(new Date(date));

}

// ...

export { _, locale, dir, setupI18n, formatDate, isLocaleLoaded };

We cache a copy of the active locale in cachedLocale whenever we set the locale in setupI8n. This cachedLocale is used in our custom function, formatDate, which is a simple convenience function that wraps Intl.DateTimeFormat. We can now call formatDate from our Movie component to get exactly the localized formatting we want.

<script>

    import { _, formatDate } from '../../services/i18n';

    // ...

</script>

<div class="box">

    <div class="columns is-mobile">

        <!-- ... -->

            <p class="release-date">

                {$_('movie.released_at', { date: formatDate(released, { year: 'numeric' }) })}

            </p>

       <!-- ... -->

    </div>

</div>

The options object we pass to formatDate gets forwarded to the Intl.DateTimeFormat constructor, so we can use any formatting option the standard constructor accepts. By passing { year: 'numeric' } to Intl.DateTimeFormat we tell it to produce date strings that only contain the given date's four-digit year. Done and dusted.

Localized dates without months and days in demo app | Phrase

Our dates are custom-formatted to our exact specs

A Quick Locale Switcher

Let's round out our app with a locale switcher so that our user can consume our app's content in the language of their choosing.

<script>

    import { createEventDispatcher } from 'svelte';

    export let value = 'en';

    const dispatch = createEventDispatcher();

    function handleLocaleChange(event) {

        event.preventDefault();

        dispatch('locale-changed', event.target.value);

    }

</script>

<style>

    /* Styles omitted for brevity. For full code see Github repo. */

</style>

<div class="locale-selector">

    <div class="select">

        <select value={value} on:change={handleLocaleChange}>

            <option value="en">English</option>

            <option value="ar">عربي</option>

        </select>

    </div>

</div>

LocaleSwitcher is a plain old Svelte component that is meant to be controlled by its parent. It wraps an HTML <select> element and exposes its set value and a custom locale-changed event. locale-changed is fired when the user selects an option from the <select> dropdown and the event provides the newly selected value to its listeners.

🔗 Resource » Read more about Svelte's custom component events in the documentation.

We can now wire up our LocaleSwicther in our App root component to allow locale switching.

<script>

    import { setupI18n, isLocaleLoaded, locale, dir } from './services/i18n';

    // ...

    import LocaleSelector from './components/UI/LocaleSelector.svelte';

    // ...

</script>

<!-- ... -->

{#if $isLocaleLoaded}

    <Header />

    <LocaleSelector

        value={$locale}

        on:locale-changed={e => setupI18n({ withLocale: e.detail }) }

    />

    <!-- ... -->

That's it, really. Now, whenever our user selects a new locale from our switcher we load that locale's message file and switch svelte-i18n's locale behind the scenes. This causes our app to re-render with the new locale's messages.

And that about does it for our demo. Here's what the final form of our app looks like.

Localized demo app | Phrase

Our completed demo app in all its glory

🔗 Resource » You can get all the code for the completed demo on Github.

Related Articles

If you want to go deeper into JavaScript i18n/l10n, check out these juicy reads:

We hope you enjoyed this tumble into localizing Svelte apps with svelte-i18n. If you're looking to scale your localized app, look no further than Phrase for a professional, robust localization platform. Phrase helps you automate your i18n tasks as a developer, and provides a powerful translation UI for your translators. Translations sync to your development environment(s) and work with your app seamlessly. Phrase also provides a flexible API, powerful web console, and many advanced i18n features: OTA translations, branching, machine translation, and more. Take a look at all of Phrase's products, and sign up for a free 14-day trial.