Localizing Svelte Applications with Svelte-i18n and Crowdin

29 mins read
Svelte Localization with Crowdin

The Internet has made the world a global, interconnected village, and the content we produce should reflect this reality. Using internationalization (i18n) and localization (l10n), software developers and digital content creators can translate their content to fit different languages and cultures around the world.

For developers using JavaScript to build web applications, they have plenty of options to choose from. Popular frameworks like React, Angular, Vue, and Svelte, and even newer ones like Solid and Qwik, all have dedicated i18n libraries. In this article, however, we’ll be looking at i18n with Svelte.

What is Svelte?

Svelte is similar to popular frameworks in terms of syntax and functionality. They share a lot of concepts in common, like components, props, and events. Svelte has a dedicated meta-framework, called Sveltekit, for building web applications. Sveltekit offers server-side rendering, routing, and offline capabilities. It’s similar to Next.js and Nuxt.js for React and Vue.

Svelte is unique compared to the other frameworks in that it’s technically a compiler and doesn’t use a virtual DOM like some of the others. After developers write code, Svelte compiles the code into JavaScript. The compilation step allows Svelte apps to be performant enough to handle reactivity and the dynamic nature of modern web applications without the complexity of a virtual DOM.

Svelte has a few dedicated i18n libraries, like Svelte-i18n, Sveltekit-i18n and Svelte-i18n-lingui. Svelte-i18n is the most mature library and can be used to localize apps built with Svelte and Sveltekit. For the rest of this article, we’ll internationalize a Svelte app with svelte-i18n, localize some parts of the app manually, and then automate the localization process with Crowdin.

Sample App: Animal Quiz

Do you know a lot about animals? Then I think you’ll enjoy this game.

A screenshot of one question

In this animal quiz, you answer five general questions about animals. Each question has four options you can choose from. If you get a question correctly, you get one point. This means you get five points in total if you answer all the questions correctly. In this article, we’ll localize the game to French.

By the way, this is not a Sveltekit application. I built this application using the Svelte boilerplate. You can find out how to create an application with the boilerplate in Svelte’s documentation. You can check the game’s GitHub repository for the complete source code. It would be much easier to follow along if you clone the repository.

Setting up Svelte-i18n

To install svelte-i18n, open your terminal and run the following command:

npm install svelte-i18n

Once the library is installed, we’ll configure it to handle all the files and locales.

Create a file src/lib/i18n.js and fill it with the following code:

import { register, init } from  'svelte-i18n';

register('en', ()  =>  import('../locales/en.json'));
register('fr', ()  =>  import('../locales/fr.json'));

init({
	fallbackLocale: 'en',
	initialLocale: 'en',
});

Using the register() function, we’ll fetch the translation files (dictionaries) for the locales we’ll use in this app. These files don’t exist yet, and we’ll create them in a second. The important thing to note here is that register() is asynchronous, and it only loads one locale (the active locale) at a time to keep things performant. You can use the synchronous addMessage() function instead if you want.

We’ll then initialize the library with English as the initial locale. In case the user’s browser uses a locale that we didn’t specify above (Arabic, for example), the app will fall back to English.

To use svelte-i18n in the app, we’ll have to import it in the app’s entry point main.js:

import './lib/i18n'

With svelte-i18n setup, we can start localizing the app.

Translating Simple Strings

Before we can translate the strings, we’ll need to extract them using svelte-i18n. Once we extract the strings, we can include them in the locale’s dictionaries.

We’ll get started with the app’s title and the start button.

Title screen showing the app's title and the start button

Follow these steps to translate the strings:

  1. Open App.svelte and import these variables from svelte-i18n:
     import { _, isLoading } from  "svelte-i18n";
    
  2. Wrap all the markup in an {#if} block so we can wait for svelte-i18n to load messages.
     {#if $isLoading}
         <p>Loading...</p>
     {:else}
         <header>
             <p id="title">Animals Quiz</p>
         </header>
         <main>
             {#if gameStarted}
                 <QuizContainer/>
             {:else}
                 <button on:click={() => gameStarted = true} id="start__button">Start</button>
             {/if}
         </main>
     {/if}
    

    We’re using the $isLoading store to only display our app after the locale has loaded. This only happens because we’re using the asynchronous register() function. If you use addMessages(), you won’t need $isLoading.

  3. Replace the string literals with i18n keys. So the header and start button text change from this:

     <p id="title">Animals Quiz</p>
     <button  on:click={() => gameStarted = true}  id="start__button">Start</button>
    

    To this:

     <p id="title">{$_('title')}</p>
     <button  on:click={() => gameStarted = true}  id="start__button">{$_('start_button')}</button>
    

The _ function formats strings and allows us to extract them for translation. Svelte-i18n uses the FormatJS library under the hood, so you might get the most of out of it by understanding how FormatJS works.

Now that we’ve replaced the texts with translation keys, svelte-i18n will insert the correct strings for us based on the current locale. But we’ll have to create dictionaries to use them first. We can create the dictionaries manually, but doing that is error-prone. Thankfully, svelte-i18n’s CLI tool makes this process easy.

Extracting Strings with Svelte-i18n CLI

To use svelte-i18n’s CLI, we’ll have to add the command in package.json. Add this line of code in the scripts object:

"scripts": {
	"dev": "vite",
	"build": "vite build",
	"preview": "vite preview",
	"extract": "svelte-i18n extract \"src/**/*.svelte\" src/locales/en.json"
}

Running the extract command will search for translatable strings in .svelte files in subfolders of src (src/components in this case) and extract these strings to locales/en.json. You won’t need to manually create a locales folder; the command will do that for you.

Let’s see the command in action. Open your terminal and run:

npm run extract

Check locales/en.json and you should see the message keys:

{
	"title": "",
	"start_button": ""
}

Put in the text for the title and start button:

{
	"title": "Animals Quiz",
	"start_button": "Start"
}

Check the app and the strings should be the same as before.

But we want to see some French, so we’ll have to create a dictionary for it.

To localize the strings to French, follow these steps:

  1. Create fr.json in the locales folder.
  2. Fill in the message keys and their translations:
     {
         "title": "Quiz sur les animaux",
         "start_button": "Commencer"
     }
    
  3. Change the initial locale to French in i18n.js:
     init({
         fallbackLocale: 'en',
         initialLocale: 'fr',
     });
    

    Header and Start button text translated

Congratulations! You just localized your first strings using svelte-i18n following these steps:

  1. Marked strings for translation
  2. Extracted strings using the CLI.
  3. Created a dictionary for another locale and translated the strings.

Let’s look at translating more complex strings.

Localizing Strings Containing Variables

We’ll translate the text on the “Next” button and the question counter.

Question counter and next untranslated

  1. Open components/QuizContainer.svelte and import the _ function:
     import { _ } from  "svelte-i18n";
    
  2. Take note of the next button and question strings. We’re going to replace them with translation keys.
     <p>Question {currentQuestions}/{NUMBER_OF_QUESTIONS}</p>
     // ... other code
     {#if currentQuestion <  NUMBER_OF_QUESTIONS}
         <button on:click={gotoNextQuestion}  id="next__button">Next</button>
     {/if}
    
  3. Replace the Question counter and the next button strings with translation keys:
     <p>{$_('question_counter',
     {
         values: {
             current_question: currentQuestion,
             number_of_questions: NUMBER_OF_QUESTIONS
         }
     })}</p>
     // ... other code
     {#if currentQuestion <  NUMBER_OF_QUESTIONS}
         <button on:click={gotoNextQuestion}  id="next__button">{$_('next_button')}</button>
     {/if}
    

    Notice the question_counter key. The _ function takes an extra parameter where we can pass variables. In this case, we pass the variables currentQuestion and NUMBER_OF_QUESTIONS to the values object, and we’ll use the keys current_question and number_of_questions to reference the values in the translation file.

  4. Run the extract command.
  5. Open the en.json file to see the new strings:
     {
         "title": "Animals Quiz",
         "start_button": "Start",
         "question_counter": "",
         "next_button": ""
     }
    
  6. Fill in the strings:
     {
         "title": "Animals Quiz",
         "start_button": "Start",
         "question_counter": "Question {current_question}/{number_of_questions}",
         "next_button": "Next"
     }
    
  7. Add the new keys in fr.json and fill in the translations:
     {
         "title": "Quiz sur les animaux",
         "start_button": "Commencer",
         "question_counter": "Question {current_question}/{number_of_questions}",
         "next_button": "Suivante"
     }
    

    You should see the translations once you click the start button and start the quiz. Next and Question counter

Now, here’s a problem. Anytime we want to add new strings to the French dictionary, we have to copy the keys to the file. Also, when we want to add a new locale, we’ll have to create it ourselves and fill in the translations. Imagine doing this process when your app has many strings and locales. Sure, using ChatGPT and other AI tools can help you generate the keys for every locale, but things become harder to change and track if you add more strings.

Automating some of these processes will make things faster, and this is where Crowdin comes in.

Automating Localization with Crowdin

Crowdin is a cloud-based localization management software. Crowdin uses continuous localization and automation to enable developers and translators to localize software with less friction, allowing translation and development to go on at the same time. Crowdin’s editor is user-friendly and it allows developers to give translators context so they have all the information they need to put in the right translations.

With Crowdin, developers can upload their translations and invite translators to collaborate on a project. Once the translators are done, they can use Crowdin’s QA checking features to verify if the translations are correct and then synchronize with the codebase.

So how exactly will Crowdin help us? Well, instead of manually creating files for our locales, we can let Crowdin do that for us. Let’s add Crowdin to the project.

Schedule a free demo with our manager to learn how to streamline your content localization

Setting up Crowdin

Follow these steps to set up Crowdin:

  1. Create a free Crowdin account.
  2. Click the + button to create a new project.
  3. Give your project a name, and make your project “Private” or “Public”.
  4. Set English as the source language and French as the target language, then click “Create Project”.

We’ll use Crowdin’s GitHub integration to upload our source file (en.json) for translation so Crowdin can stay in sync with our project. Crowdin supports other VCS (version control systems) and has many integrations. You can check them out in the Crowdin store.

To connect Crowdin to GitHub:

  1. Go to the Integrations tab. Integrations tab
  2. Select GitHub.
  3. Select the “Set up integration” button and click “Source and translation files mode”
  4. Give Crowdin access to your GitHub account.

Once you grant access to Crowdin, you’ll need to setup the app’s repository for localization. Follow these steps:

  1. Select the app’s repository.
  2. Select the branch you want to translate, main in this case. Crowdin will create a branch called “l10n_main” in your repository to store the translations. You can give the branch a different name if you like.

    Branch configuration

  3. Click the pencil icon next to “l10n_main” to edit the configuration.
  4. Confirm crowdin.yml as the configuration file.
  5. Enter /src/locales/en.json in the Source files path field.
  6. Enter /src/locales/%two_letters_code%.json in the Translated files path field. Crowdin uses configuration placeholders like %two_letters_code% to make it easier to match files having locales in their names. In this case, Crowdin will create fr.json in the locales folder.

    File filters for app strings

  7. Save all the settings.

If you go the dashboard and open en.json in the editor, you’ll see the files and strings we translated earlier. Application strings

Now that we’ve connected Crowdin to the app, let’s continue with our translations.

Dealing with Plurals

Let’s localize the score counter. It contains the user’s score and the string “points”. Plurals showing incorrectly

The score currently displays “1 points”, which isn’t correct in English. We could use JavaScript logic to handle the plurals problem, but it won’t scale as we add more locales to the app. Plural handling is a common localization feature and it comes in-built with svelte-i18n.

Follow these steps to localize the score:

  1. Mark the score counter for extraction in QuizContainer.svelte: Changing it from this:
     <p>Score: {score} points</p>
    

    To this:

     <p>{$_('score', { values: { score } })}</p>
    
  2. Run the extract command.
  3. Open en.json and fill in the score key with this code:
     "score": "Score: {score, plural, =0 {0 points} one {1 point} other {# points}}"
    
    

    We’re using the ICU Message Format to display the plurals. Using the score variable, we set the strings we’ll display for 0 (0 points), 1 (1 point), and other (2 or more points). These are the plural forms of English. Other languages, such as Arabic, have more plural forms than English.

We’ll now head over to Crowdin to translate the plurals. Follow these steps:

  1. Commit and push your code to GitHub.
  2. Open en.json in your Crowdin dashboard.

    Once you start translating, you’ll see a preview of how the plural translation looks.

    Plurals translation in the Crowdin editor

    The words for “Score” and “point(s)” are the same in English as in French. Also, French and English have the same plural form, so we’ll just copy and paste the translation. After you’re done translating, click “Save” and click the checkmark below the preview to approve the translations.

  3. Click the “Sync” button in the Integrations tab to synchronise Crowdin with your GitHub repository.

  4. Go to your GitHub repository and you should see the pull request containing the translations. Crowdin pull request

  5. Merge the pull request and pull in the changes to your local branch. Everything should work fine if you change the locale in the app.

    Plurals displaying correctly in the application for English

    Plurals displaying correctly in the application for English

TIP: Svelte-i18n has methods for handling dates, time, and currencies. Check out the documentation for examples on these topics.

Locale Switcher

I’m sure you’re tired of changing the locale through code. I am too. We’ll build a locale switcher to make this task easy for us and our users.

To accomplish this, we’ll need a locale switcher component.

  1. Create the locale switcher component src/components/LocaleSwitcher.svelte.
     <script>
     import { _, locale } from "svelte-i18n";
    
     export let currentLocale = "en";
    
     function changeLocale(event) {
         event.preventDefault();
         $locale = event.target.value;
     }
     </script>
    
     <div>
         <select value={currentLocale}  on:change={changeLocale}>
             <option value="en">{$_('locale_switcher_english')}</option>
             <option value="fr">{$_('locale_switcher_french')}</option>
         </select>
     </div>
    

    Notice the $locale variable we’re using. Svelte-i18n provides us with this store so we can access the current locale.

  2. Import LocaleSwitcher in App.svelte:
     <script>
     import { _, isLoading, locale } from  "svelte-i18n";
     import QuizContainer from "./components/QuizContainer.svelte";
     import LocaleSwitcher from "./components/LocaleSwitcher.svelte";
    
     let gameStarted = false;
     </script>
    
     // other markup
    
     {#if !gameStarted}
     <LocaleSwitcher  currentLocale={$locale}/>
     {/if}
    
    

    I put the LocaleSwitcher in an if block because I want it to be visible only when the game starts. It would be a bit weird to change the locale in the middle of the game.

  3. Run the extract command.
  4. Fill in the strings in en.json.
     {
         // ... other keys
         "locale_switcher_english": "English",
         "locale_switcher_french": "French"
     }
    

    Since these new keys are short, we can skip using Crowdin to make this quick change.

  5. Add the keys in fr.json and fill in the translations:
     "locale_switcher_english": "Anglais",
     "locale_switcher_french": "Français"
    

    You should see the locale switcher in action.

    Using locale switcher to change between French and English

Remember to commit and push your changes to GitHub.

Localize the Questions and Options with Crowdin

The French version of the app currently looks weird because the questions and options are still in English. So let’s change that.

I stored the questions and answers in a JSON file, questions.json:

Screenshot of the questions.json file

Since we want to translate this file into another locale and to future-proof it for translation into other locales, we should rename the file to questions.en.json. When we upload this file to Crowdin, Crowdin will create the files for other locales using this format (the language in the filename).

  1. Rename questions.json to questions.en.json.
  2. Import the locale variable in QuizContainer.svelte.
     import { _, locale } from  "svelte-i18n";
    
  3. Modify the fetch() call to include the current locale when fetching JSON files.
     function getQuestions() {
         let currentLocale = $locale;
         return fetch(`src/assets/data/questions.${currentLocale}.json`).then(response => response.json());
     }
    

Follow these steps to upload questions.en.json to Crowdin:

  1. Go to the Integrations tab.
  2. Click the “Edit” button above your repository
  3. Click the Edit icon in the repository settings
  4. Click “Add a new file filter”.
  5. Enter /src/assets/data/questions.en.json in the Source files path.
  6. Enter /src/assets/data/questions.%two_letters_code%.json in the Translated files path.

    File filter for questions

  7. Save all changes.

Navigate to the editor and start translating:

NOTE: The editor might not display the strings at first. If that happens, click "Disable All filters".
  1. Translate the questions and the image’s alt texts as well.

    Translating questions and alt texts

  2. For strings you don’t want to translate, like imageUrl, use the “Hide string” option.

    Using the Hide string option

  3. Save your translations.
  4. Synchronize your changes in the Integrations tab.
  5. Merge the pull request in GitHub.

And there you go! A fully localized quiz app.

Playing the quiz with the questions localized

Conclusion

Svelte-i18n is a simple i18n library, and with it, we were able to start localizing the quiz for a French audience. Using Crowdin, we removed some of the manual processes that caused friction and finished localizing the app.

Here are some resources you might find helpful:

[Free E-book]Localize your content with Crowdin

Learn how to set up a continuous localization workflow to grow with multiple languages faster than with one. Experience and tips from 10+ localization experts.
Zayyad Muhammad Sani