Loading...

How to Load i18n Locales Asynchronously in Vue 3 + Vite

Last update: 1/14/2024
Title image from Nick Fewings

Intro

In my previous story How to Build a Multi-Language Vue 3 + Vite App we built a multi-language app with the Vue I18n plugin, where we stored all locale messages in external json files. The files were then pre-compiled and bundled during the build process. This way, all locales are included in the main bundle and are available immediately when the application is loaded. However, you may want to include only a few locales in the main bundle while serving others asynchronously as they are needed. This can reduce bundle size in projects with many locales.

In This Guide

We extend the Vue 3 + Vite app from the last story, in order to bundle only a few locales in the main chunk, while others are loaded asynchronously when they are needed.

There are several ways to implement asynchronously loading the locales. In this guide, the locale message files which are not bundled in the main chunk are provided as static json files. The client-side Vue 3 app can then load a locale resource file when it is needed by utilizing the fetch API. Once the locale is loaded, the locale messages must be compiled and added to the global I18n instance.

This approach requires to host the locale resources files in two different directories.

Prerequisites

  • Basic understanding of how to work with Vue 3 + Vite, the Composition API and the <script setup> syntax
  • node.js is installed on your machine
  • You have read and understood my previous story as it serves as the basis

Base App

Clone the Vue 3 + Vite app with the Vue I18n plugin from the last story by opening your terminal at the desired working directory and executing the following command:

git clone https://github.com/robinkloeckner/vue3_vite_i18n

Alternatively you can download the source code from GitHub.

Then navigate to the project root directory, install all modules and start the dev server:

cd vue_vite_i18n
npm install
npm run dev

If you now browse to http://localhost:5173/ you see that the app looks similar to the app that comes with a standard Vue 3 + Vite installation. However, the app is extended by the Vue I18n plugin which provides you with many features to localize your app, as well as the unplugin-vue-i18n plugin which provides a compiler to convert the locale messages from dedicated json files into JavaScript functions during the build process. Further, a LocaleSelect.vue component is added to the HelloWorld.vue component in order to change the locale. The locale resource files are located in src/locales/ which are then loaded in the main.js file before creating the Vue I18n instance. For a detailed description of the source code please read my previous story.

The base project has the following folder structure:

public/
  favicon.ico
src/
  assets/
    ...
  components/
    icons/
      ...
    HelloWorld.vue
    LocaleSeclect.vue
    TheWelcome.vue
    WelcomeItem.vue
  locales/
    de.json
    en.json
  App.vue
  main.js
.gitignore
index.html
package.json
package-lock.json
README.md
vite.config.js

Illustration of the running app: Base Vue 3 + Vite app with the Vue I18n plugin

Add New Locale

Next we add a new locale which should be loaded asynchronously later on. Since we want to serve it as a static file, the easiest way is to place it in the public/ directory, because the folder is copied to dist/ as is during build. Therefore create a new file es.json in a new directory public/locales/ with the following content:

{
  "title": "¡Lo lograste!",
  "description": "Ha creado con éxito un proyecto con\n<a target=\"_blank\" href=\"{viteRef}\">Vite</a> +\n<a target=\"_blank\" href=\"{vueRef}\">Vue 3</a>."
}

Vite Configuration

In the vite.config.js configuration file of the base app, VueI18nPlugin is already added and contains the path to the locale resource files which will be bundled during the build process. The strictMessage option is required since version 0.10.0 of unplugin-vue-i18n plugin and must be set to false, if the locale messages contain HTML.

In addition to that, we need to set the runtimeOnly option false to include the message compiler in the bundle. The message compiler in turn is needed to convert the locale messages from the JSON file to JavaScript functions (see the documentation here and here).

The final vite.config.js looks as follows:

import { fileURLToPath, URL } from "node:url";
import { resolve, dirname } from "node:path";
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import VueI18nPlugin from "@intlify/unplugin-vue-i18n/vite";

export default defineConfig({
  plugins: [
    vue(),
    VueI18nPlugin({
      include: resolve(dirname(fileURLToPath(import.meta.url)), './src/locales/**'),
      runtimeOnly: false,
      strictMessage: false
    })
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
});

i18n Setup

To keep things more organized we will put everything related to the Vue I18n plugin in a separate file. Therefore, create a i18n.js file in the src/ directory with the following content:

import {createI18n} from "vue-i18n";
import messages from "@intlify/unplugin-vue-i18n/messages";

// List of all locales.
export const allLocales = ["en", "de", "es"];

// Create Vue I18n instance.
export const i18n = createI18n({
  legacy: false,
  globalInjection: true,
  locale: "en",
  fallbackLocale: "en",
  messages: messages,
});

// Set new locale.
export async function setLocale (locale) {
  // Load locale if not available yet.
  if (!i18n.global.availableLocales.includes(locale)) {
    const messages = await loadLocale(locale);

    // fetch() error occurred.
    if (messages === undefined) {
      return;
    }

    // Add locale.
    i18n.global.setLocaleMessage(locale, messages);
  }

  // Set locale.
  i18n.global.locale.value = locale;
}

// Fetch locale.
function loadLocale(locale) {
  return fetch(`./locales/${locale}.json`)
    .then((response) => {
      if (response.ok) {
        return response.json();
      }
      throw new Error("Something went wrong!");
    })
    .catch((error) => {
      console.error(error);
    });
}

We first import the locale messages from the src/locales/ directory by utilizing the unplugin-vue-i18n plugin (line 2). After that, we define an array containing all locales we want to offer in line 5. The array will be used later to create the dropdown menu for changing the locale. In line 8 to 14 we create a Vue I18n instance. Since we want to use the Composition API with the <script setup> syntax the legacy variable must be set to false. We also set the default locale to English. For a detailed explanation of the options please see my previous story.

The setLocale() function will be called when a user selects a new locale, whereas the locale parameter will hold the selected locale. All currently available locales are listed under the availableLocales property of the Vue I18n instance. If the selected locale is not included in that list, it must be fetched from the server by using the fetch() method. This is done in a separate loadLocale() function which returns a Promise. If the request is successful the locale messages are assigned to the messages constant. If an error occurs, messages will be undefined. In that case we return from the function without changing the locale (line 24). If the request is successful, the new locale is added to the global I18n instance in line 28 (i18n.global.setLocaleMessage(locale, messages);). In the background the locale is automatically compiled to JavaScript. At the end, the locale is changed in line 32 (i18n.global.locale.value = locale;).

Locale Switch

Next, the <select> element from the LocaleSwitch.vue component must be modified. We still want to sync the value of the <select> element with the locale set in the Vue I18n instance. However, in contrast to the current implementation we don’t want to change the locale directly. Instead we want to use the custom setLocale() function from the i18n.js file, because that is where we fetch locales if they are not yet available. Further, the options of the <select> element should be created according to the list of all available locales rather than those that are bundled in the main chunk.

Therefore, modify the LocaleSelect.vue component as follows:


<script setup>
  import { allLocales, setLocale } from "../i18n";
</script>

<template>
  <label for="locale">Locale: </label>
  <select
      :value="$i18n.locale"
      @change="setLocale($event.target.value)"
      id="locale"
  >
    <option v-for="locale in allLocales" :value="locale">{{ locale }}</option>
  </select>
</template>

<style scoped>
  label, select {
    width: 100%;
  }
  select {
    height: 25px;
  }
</style>

The <select> element is populated with options by looping through the list of all available locales, allLocales, as defined in the i18n.js file, whereby the locale name is set as the option’s value with :value="locale".

In line 8 we bind the value of the <select> element to the locale set in the global I18n instance with :value="$i18n.locale". When the locale is changed, the <select> element calls the setLocale() function by setting @change="setLocale($event.target.value)" with the selected locale being set as a parameter.

The final app has the following folder structure:

public/
  locales/
    es.json
  favicon.ico
src/
  assets/
    ...
  components/
    icons/
      ...
    HelloWorld.vue
    LocaleSeclect.vue
    TheWelcome.vue
    WelcomeItem.vue
  locales/
    de.json
    en.json
  App.vue
  i18n.js
  main.js
.gitignore
index.html
package.json
package-lock.json
README.md
vite.config.js

Running the App

Execute the production build and start the local preview server with:

Then open your browser and its developer tools. Navigate to the network tab and browse to http://localhost:4173. If you change the language from English to German (en to de), you won’t see any network activities, since both locales are included in the main chunk that Vite creates during build. However, when you select Spanish (es), you will see, that the locale es.json file is download from to local preview server. If you change the language back to English or German and then select Spanish again, the locale is switched to Spanish without fetching it from the server again.

Final app with lazy loading

Summary

In Vue projects with many locales, loading them asynchronously as they are needed can reduce the initial bundle size of the application. One way to implement this is by storing the locale message in JSON files and fetching them from the server when they are needed. The JSON files must then be compiled on the client side and added Vue I18n instance.

Report a problem (E-Mail)