Implementing localization in Svelte

📅 2 years ago 🕒 12 min read 🙎‍♂️ by Madza

Implementing localization in Svelte

In this tutorial, we will implement the localization in Svelte application. The app will include routing, so you can divide the content across multiple pages of the app.

We will first set up a new project, then create the dictionary for the localized content, configure the localization packages, create the layout and finally style the app.

In the end, we will have a localized app, where the default language is detected by the browser. Also, users will be able to switch between the languages manually using the dropdown.

For reference, the source code and full demo app are available here.

Why Svelte?

The main advantages of the Svelte are its reactivity and lightweight.

Svelte is pretty radical on its way to building user interfaces compared to existing solutions like React and Vue. Instead of using the browser to do most of the heavy lifting, Svelte transfer this step to the compiling, meaning the content can be displayed much faster.

According to the StackOverflow's survey in 2021, Svelte has been voted the most loved framework from over 66k respondents.

Also, if we see the npm trend's graph and its GitHub repository, it gets clear that this framework is worth keeping an eye on and definitely holds great potential for the future.

NPM graph

Setting up the Project

We will use the Svelte official template to set up the project boilerplate via degit. To do that, open your terminal and run the following command:

npx degit sveltejs/template svelte-localization

Wait for the setup to finish and change directory into the newly created project folder by running cd svelte-localization. Then run npm install to install all the necessary dependencies for the project to work.

Once it is done, you can run npm run dev, which will start up Rollup.

Finally, open your browser and navigate to http://localhost:5000/ which should then present a fully functional Svelte app, which looks like this:

Svelte default app

The default boilerplate comes with some extra code we won't need. To clean it up, navigate to the src folder, open App.svelte file, and remove all the contents inside it. We will write everything from scratch later on.

Creating the Dictionaries

We will create a separate locale dictionary for each language - English, Spanish, and French. Each locale will include translation for the navigation items (Home, Features, and About), as well as the included content (Title and Description) for each page.

To do that, create a new folder called langs and create three files inside it en.json, es.json, and fr.json. You can do it manually or using this command in the terminal: mkdir langs && cd langs && touch en.json es.json fr.json.

To create a locale for English, open the file en.json and include the following code:

{
  "nav": {
    "home": "Home",
    "features": "Features",
    "about": "About"
  },
  "home": {
    "title": "Welcome, Everyone!",
    "description": "Switch between different tabs and languages to see the action."
  },
  "features": {
    "title": "Main Features",
    "description": "The default language on the launch is detected by the user's browser. If it is not supported, English is used. If the user selects the language manually from the menu, that particular language is used."
  },
  "about": {
    "title": "Stack of Technologies",
    "description": "This demo was built by Madza. I used 'Svelte', 'svelte-routing' and 'svelte-i18n' as the stack of technologies."
  }
}

Next, to create a locale for Spanish, open the file es.json and include the following code:

{
  "nav": {
    "home": "Hogar",
    "features": "Características",
    "about": "Sobre"
  },
  "home": {
    "title": "¡Todos bienvenidos!",
    "description": "Cambie entre diferentes pestañas e idiomas para ver la acción."
  },
  "features": {
    "title": "Principales características",
    "description": "El navegador del usuario detecta el idioma predeterminado en el lanzamiento. Si no es compatible, se utiliza el inglés. Si el usuario selecciona el idioma manualmente en el menú, se utiliza ese idioma en particular."
  },
  "about": {
    "title": "Pila de tecnologías",
    "description": "Esta demostración fue construida por Madza. Usé 'Svelte', 'svelte-routing' y 'svelte-i18n' como pila de tecnologías."
  }
}

Finally, to create a locale for French, open the file fr.json and include the following code:

{
  "nav": {
    "home": "Domicile",
    "features": "Caractéristiques",
    "about": "À propos"
  },
  "home": {
    "title": "Bienvenue tout le monde!",
    "description": "Basculez entre différents onglets et langues pour voir l'action."
  },
  "features": {
    "title": "Caractéristiques principales",
    "description": "La langue par défaut au lancement est détectée par le navigateur de l'utilisateur. S'il n'est pas pris en charge, l'anglais est utilisé. Si l'utilisateur sélectionne la langue manuellement dans le menu, cette langue particulière est utilisée."
  },
  "about": {
    "title": "Pile de technologies",
    "description": "Cette démo a été construite par Madza. J'ai utilisé 'Svelte', 'svelte-routing' et 'svelte-i18n' comme pile de technologies."
  }
}

Installing the Packages

Next, we will set up some external packages, so we can access and use previously created dictionaries. For that we will be using svelte-i18n and @rollup/plugin-json.

To install both packages, open the terminal and run the command:

npm i svelte-i18n @rollup/plugin-json

The svelte-i18n is a pretty straightforward package that uses stores to keep track of current locale, dictionary, helps to format the files, and so on. The @rollup/plugin-json is a helper package that allows us to import JSON files to work with Rollup.

We will also implement a simple routing so we can use localization across multiple pages. For that, we will use svelte-routing, which simplifies the routing process, thanks to built-in Router, Link and Route components.

To install the package, open the terminal and run the command:

npm i svelte-routing

Initializing the Localization

At this point we have all the necessary packages, we just need to configure them.

Open the App.svelte file and add the following code:

<script>
  import { Router, Route, Link } from "svelte-routing";
  import {
    _,
    getLocaleFromNavigator,
    isLoading,
    register,
    init,
    locale
  } from "svelte-i18n";

  register("en", () => import("./langs/en.json"));
  register("es", () => import("./langs/es.json"));
  register("fr", () => import("./langs/fr.json"));

  init({
    fallbackLocale: "en",
    initialLocale: getLocaleFromNavigator()
  });

  const handleLocaleChange = e => {
    e.preventDefault();
    locale.set(e.target.value);
  };
</script>

First, we imported all the necessary components (Router, Route and Link ) from svelte-routing, so we can later implement the fully functional routing mechanism for the pages.

Then we imported the necessary methods from the svelte-i18n, so we can later use them to localize the app. _ will allow us to access the dictionaries, getLocaleFromNavigator will get the default locale from the browser, isLoading will help us control the loading state of the locales, init will let us initialize them, and locale will allow us to set them manually.

After that, we registered each language and set the default (initial) language as well as fallback language if the default one is not supported.

Finally, we created the handleLocaleChange function to set the language via the dropdown menu, which we will implement in the next section.

Building the Layout

In order to use the localization features we just initialized, we must create a layout to display the contents of the dictionaries and allow us to switch the locales.

Under the script tags in the App.svelte add the following code:

// script tags..

{#if $isLoading}
  <p>Loading</p>
{:else}
  <main>
    <Router>
      <select on:change={handleLocaleChange}>
        <option value="en">en</option>
        <option value="es">es</option>
        <option value="fr">fr</option>
      </select>
      <header >
        <nav>
          <Link to="/">{$_('nav.home')}</Link>
          <Link to="features">{$_('nav.features')}</Link>
          <Link to="about">{$_('nav.about')}</Link>
        </nav>
      </header>
      <section>
        <Route path="/">
          <h3>{$_('home.title')}</h3>
          <p>{$_('home.description')}</p>
        </Route>
        <Route path="features">
          <h3>{$_('features.title')}</h3>
          <p>{$_('features.description')}</p>
        </Route>
        <Route path="about">
        <h3>{$_('about.title')}</h3>
          <p>{$_('about.description')}</p>
        </Route>
      </section>
    </Router>
  </main>
{/if}

First, we used if and else statements to detect if the dictionary has been loaded. For that, we used $isLoading store.

If the dictionaries have not been loaded we return a message to inform the user about that (shown only after longer loading times than 200ms, which is rare). When the dictionary has been loaded, the Svelte renders the App.

The whole app resides in the main wrapper. Inside it there is a Router component, that is a wrapper for the routing mechanism.

Inside we have a select dropdown allowing us to select the language of the App. Also, we used the input event on:change and passed in the previously created handleLocaleChange function to get the selected language and set it as the active locale.

Inside the header we have out navigation and each nav element receives an input from the language dictionaries via the $_ method, which is a shorthand alias of the $format from the svelte-i18n.

Each section element include the Route component which uses the specific path for it and includes the title and description of the page via the $_ method from the svelte-i18n.

Let's test what we get so far. Open your terminal and run npm run dev to start up Rollup and then navigate to http://localhost:5000/ in your browser.

You should see a very basic layout structure of the elements:

Layout

Styling the Application

To make our app pretty we will add some style rules for each of the elements.

Open App.svelte file and add the following style rules:

// script tags..

// elements..

<style>
  @import url("https://fonts.googleapis.com/css2?family=Montserrat&display=swap");

  * {
    padding: 0;
    margin: 0;
    box-sizing: border-box;
    font-family: "Montserrat", sans-serif;
  }

  :global(body) {
    background-image: linear-gradient(120deg, #d4fc79 0%, #96e6a1 100%);
    min-height: 100vh;
    color: black;
    padding: 10px;
  }

  main {
    max-width: 600px;
    margin: 0 auto;
  }

  select {
    border: none;
    padding: 10px;
    margin-bottom: 20px;
    border-radius: 5px;
  }

  nav {
    margin-bottom: 20px;
    display: grid;
    grid-template-columns: 1fr 1fr 1fr;
    text-align: center;
    gap: 20px;
  }

  nav > :global(a) {
    background-color: white;
    padding: 10px 20px;
    border-radius: 5px;
    color: black;
    font-weight: bold;
    text-decoration: none;
  }

  section {
    background-color: white;
    padding: 20px;
    border-radius: 5px;
  }

  h3 {
    margin-bottom: 10px;
  }

  @media screen and (max-width: 500px) {
    nav {
      grid-template-columns: 1fr;
    }
  }
</style>

We first imported the Montserrat font and set it up to be used in all elements. We also reset the rules for margin, padding, and box-sizing so they do not differ across the different browsers due to the default values used.

Then we styled the body by setting a green gradient as the background, set the height to use at least the whole viewport, set the text color to be black, and added some padding, so the app looks great on the responsive screens.

For the main wrapper we defined specific width that can never be exceeded and centered it to the viewport horizontally.

We removed the default border to the select dropdown, added some radius to it, set some padding, and set some bottom margin, so there is some space between the direct element below.

For the nav we used a grid layout with three columns and a 20px gap between them. We centered the included text and added a bottom margin. For the included links we removed the text decoration, set the font color to be black, bolded them, set background-color to be white, added some padding and border-radius.

We styled the section element by adding the white background, some padding, and a border radius so it fits better in the overall styling of the app.

We also added some bottom margin for the h3 elements, so there is some space between section titles and the description.

Finally, we added a media rule for responsive screens, so the nav element switches to one column layout for the screens smaller than 500px in width, meaning each of the navigation items will be then shown directly below each other.

If we check the browser now, the output should be a fully functional app.

Demo

Conclusion

If you know that your target audience is international clients whose native language differs from the default language used in your app, the localization feature should be considered for any app. This way users know that you care about them and it improves the overall UI/UX.

If you want you can add as many locales as you need. Also, feel free to update this demo app by adding more pages and content. Maybe you can even add a backend functionality and support private routes, so users must log in to see them.

Thanks for reading and I hope you will find a practical use for this demo.