How to Load i18n Locales Asynchronously in Vue 3 + Vite
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:
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.
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.