Hello and welcome to this Nuxt i18n guide that will cover translating your Nuxt.js app into multiple languages. We’ll cover everything you need to know to make your Nuxt.js app multilingual. We’ll start by setting up the i18n package and managing translation files. You’ll learn how to switch languages, localize routes, and save language preferences.
We’ll also dive into supporting right-to-left languages, handling pluralization, and localizing dates, times, and currencies. Let’s get started on making your app truly global!
The source code for this article can be found on GitHub.
Introduction
What is internationalization (i18n) and localization (l10n)?
Internationalization (often abbreviated as i18n) and localization (l10n) are essential practices for making software applications usable in multiple languages and regions.
Internationalization (i18n) is the process of designing your application so it can be easily adapted to various languages and regions without engineering changes. This involves preparing your codebase and resources to handle different languages, character sets, date formats, and more.
Localization (l10n) is the process of adapting your internationalized application to a specific language and region. This includes translating text, adjusting date and time formats, adapting layouts to fit text length, and ensuring cultural relevance.
To learn more about the differences between i18n and l10n, please refer to my other blog post.
Why you should care about i18n/l10n in your Nuxt.js app
- Reach a wider audience: By supporting multiple languages, you can reach users from different parts of the world, expanding your app’s user base.
- Improve user experience: Users are more comfortable and find applications more intuitive when they can use them in their native language. This can lead to higher user satisfaction and retention.
- Compliance and accessibility: In some regions, providing content in the local language is a legal requirement. Ensuring your app is accessible to non-English speakers can help you comply with local regulations.
- Competitive advantage: Offering a localized experience can give you an edge over competitors who only support one language, making your app more attractive to a global audience.
In the next sections, we’ll cover how to set up and implement i18n and l10n in your Nuxt.js app, ensuring it’s ready for a global audience.
Prerequisites for Nuxt i18n
Before diving into internationalization and localization in your Nuxt.js app, make sure you have the following:
- Basic JavaScript and/or TypeScript knowledge: Understanding JavaScript fundamentals is essential since Nuxt.js is built on top of Vue.js and uses JS or TS (depending on your setup) for development.
- Familiarity with Vue.js: Nuxt.js is a framework for Vue.js, so knowing the basics of Vue.js will help you follow along more easily.
- Code editor: Use any code editor you are comfortable with. Popular choices include Visual Studio Code, Sublime Text, and Atom.
- Node.js and npm/yarn: Make sure you have Node.js 18+ and either npm or yarn installed on your machine. These are required to set up and manage your Nuxt.js project.
With these prerequisites covered, you’re ready to start internationalizing and localizing your Nuxt.js app. In the next section, we’ll get started by setting up a new Nuxt.js project.
Installation
Setting up Nuxt.js
To get started with internationalization and localization in your Nuxt.js app, you first need to set up Nuxt.js on your local machine and create a new project. Follow these steps.
First, initialize a new project with the following command:
npx nuxi@latest init lokalise-nuxt-i18n
Replace lokalise-nuxt-i18n
with your desired project name.
Then follow the wizard’s instructions:
- Choose the package manager (e.g., npm)
- Enable or disable telemetry
- Decide whether to initialize a Git repository
To make sure everything is running smoothly, navigate into the project folder and start a local server:
cd lokalise-nuxt-i18n npm run dev
Open your browser and navigate to http://localhost:3000
. You should see a welcome page, indicating that the app is ready to go!
A bit of styling
Create assets/style.scss
file with the following content:
html, body { margin: 0; padding: 0; box-sizing: border-box; height: 100%; } .container { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: calc(100vh - 40px); text-align: center; // padding-top: 20px; /* Adjusted to add spacing from the top */ } h1 { font-size: 3em; margin-bottom: 0.5em; } p { font-size: 1.5em; } html[dir="rtl"] { .container { direction: rtl; } } .language-switcher, .global-menu { margin-bottom: 20px; a { margin: 0 10px; padding: 5px 10px; text-decoration: none; color: #000; &.router-link-active { font-weight: 800; } } } button { margin-top: 20px; padding: 10px 20px; font-size: 1em; cursor: pointer; }
You’ll need Sass installed, so make sure to add these lines to your package.json
and install the dependencies:
"devDependencies": { "sass": "^1.77.6", "sass-loader": "^14.2.1" }
Then, simply add your styles to the nuxt.config.ts
file:
export default defineNuxtConfig({ css: ['~/assets/style.scss'], })
Preparing the home page for Nuxt i18n
Now let’s replace the demo home page with some custom data.
- Create the
pages
directory: In the root of your project, create a directory calledpages
. - Create the
index.vue
file: Inside thepages
directory, add a file namedindex.vue
. - Add custom content to
index.vue
: Open theindex.vue
file and add the following content to display the welcome message and paragraph:
<template> <div class="container"> <h1>Welcome!</h1> <p>This tutorial is brought to you by Lokalise</p> </div> </template>
- Update
app.vue
to use the new page: Find theapp.vue
file in the project root, open it, and replace its content with:
<template> <NuxtPage /> </template>
This tells Nuxt.js to render the content from the pages/index.vue
file at the root URL.
- Save the changes and check your browser: Open your browser and navigate to
http://localhost:3000
. You should now see the new content being displayed.
Setting up Nuxt i18n
Installation and configuration
To start internationalizing your Nuxt.js app, you need to install the @nuxtjs/i18n
module and configure it. Here’s how to do it:
Install the @nuxtjs/i18n
module: Open your terminal and run the following command to install the module:
npx nuxi@latest module add i18n
Configure the module: After installation, you need to configure Nuxt i18n module. Create i18n/i18n.config.ts
file with the following content:
export default defineI18nConfig(() => ({ legacy: false, locale: 'en', fallbackLocale: 'en', }))
locale
sets the initial language for Vue I18n when the plugin is loaded.fallbackLocale
specifies the language to fall back to if a translation is missing in the current locale.
Open nuxt.config.ts
and add the following configuration:
import { defineNuxtConfig } from 'nuxt/config' export default defineNuxtConfig({ modules: ['@nuxtjs/i18n'], css: ['~/assets/style.scss'], i18n: { locales: [ { code: 'en', iso: 'en-US', name: 'English', file: 'en.json' }, { code: 'fr', iso: 'fr-FR', name: 'Français', file: 'fr.ts' }, { code: 'ar', iso: 'ar-AR', name: 'العربية', file: 'ar.json', dir: 'rtl' } ], defaultLocale: "en", strategy: "prefix_except_default", langDir: 'locales/', lazy: true, vueI18n: './i18n/i18n.config.ts' }, })
Let’s cover main settings:
locales
specifies the languages and their translation file names. The Arabic locale has thertl
option set for right-to-left text direction.defaultLocale
determines the default language of the application for initial visits and language detection.strategy
specifies how routes will contain language codes as prefixes. For example, the home page will be accessible aslocalhost:3000/
for English andlocalhost:3000/fr
for French.langDir
is the directory from which to load translation files.lazy
enables lazy loading of translation files, meaning Nuxt won’t load all translation files for all languages initially.vueI18n
specifies the location of your i18n config file.
Note that you can provide other configuration options. For example, it’s possible to adjust language detection and control how the preferred locale will be saved.
Prepare TypeScript types: You might need to run the following command to prepare TypeScript types:
npx nuxt prepare
With this setup, your Nuxt.js app is ready to handle multiple languages. In the next section, we’ll cover how to create translation files and use them in your components.
Performing simple translations
Now that we have the i18n module installed and configured, let’s translate the texts inside pages/index.vue
.
First of all, replace your markup inside the index.vue
file with:
<template> <div class="container"> <h1>{{ $t('welcome') }}</h1> <p>{{ $t('message') }}</p> </div> </template>
The $t()
function tries to fetch a translation value under the given key.
To provide translations, create a locales
folder in the project root and add an en.json
file inside:
{ "welcome": "Welcome!", "message": "This tutorial is brought to you by Lokalise" }
Then add fr.json
file:
{ "welcome": "Bienvenue!", "message": "Ce tutoriel vous est proposé par Lokalise" }
And then ar.json
:
{ "welcome": "أهلاً وسهلاً!", "message": "هذا الدليل مقدم لكم من قبل Lokalise", }
Note that it’s not mandatory to use JSON files for translations. You can use TS files:
export default defineI18nLocale(async locale => { // Simulate fetching translations from an API return { "welcome": "Bienvenue!", "message": "Ce tutoriel vous est proposé par {name}", } })
Also it’s even possible to perform fetch requests to return translations from the API.
Start your development server if it’s not already running, open your browser and navigate to http://localhost:3000
. You should see the “Welcome!” message and the paragraph translated based on the default locale (English).
Adding language switcher
Currently we don’t provide an option to set the locale, so let’s take care of it now.
Simple switcher
Create a components
folder in the project root and add a LanguageSwitcher.vue
inside:
<template> <div class="language-switcher"> <NuxtLink v-for="locale in availableLocales" :key="locale.code" :to="switchLocalePath(locale.code)" :class="{ active: currentLocale === locale.code }" > {{ locale.name }} </NuxtLink> </div> </template> <script setup> import { useI18n } from 'vue-i18n' import { computed } from 'vue' import { useSwitchLocalePath } from '#imports' const { locale, locales } = useI18n() const switchLocalePath = useSwitchLocalePath() const availableLocales = computed(() => { return locales.value.filter(i => i.code !== locale.value) }) const currentLocale = computed(() => locale.value) </script>
Then use this new component inside app.vue
file:
<template> <div> <LanguageSwitcher /> <NuxtPage /> </div> </template> <script setup> import LanguageSwitcher from '~/components/LanguageSwitcher.vue' </script>
Nice!
Saving language preferences and supporting RTL languages
As mentioned, i18n plugin can detect the preferred locale and store it in cookies. However, you can also code this functionality manually. Plus, we will need to properly handle right-to-left languages.
Therefore, let’s adjust our language switcher in the following way:
<template> <div class="language-switcher"> <NuxtLink v-for="locale in availableLocales" :key="locale.code" :to="switchLocalePath(locale.code)" @click="setLanguagePreference(locale.code)" :class="{ active: currentLocale === locale.code }" > {{ locale.name }} </NuxtLink> </div> </template> <script setup> import { useI18n } from 'vue-i18n' import { computed, watch, onMounted } from 'vue' import { useSwitchLocalePath } from '#imports' const { locale, locales } = useI18n() const switchLocalePath = useSwitchLocalePath() const availableLocales = computed(() => { return locales.value.filter(i => i.code !== locale.value) }) const setLanguagePreference = (code) => { localStorage.setItem('preferredLanguage', code) } const currentLocale = computed(() => locale.value) const updateDirAttribute = (newLocale) => { const currentLocale = locales.value.find(l => l.code === newLocale) document.documentElement.setAttribute('dir', currentLocale?.dir || 'ltr') } watch(locale, (newLocale) => { updateDirAttribute(newLocale) }) onMounted(() => { const savedLanguage = localStorage.getItem('preferredLanguage') if (savedLanguage && locales.value.some(locale => locale.code === savedLanguage)) { locale.value = savedLanguage } updateDirAttribute(locale.value) }) </script>
Key points here:
- Setting language preference: Upon clicking on the language name, we set the language preference in local storage.
- Updating
dir
attribute: Whenever the locale changes, we update thedir
(direction) attribute accordingly. For example, when the Arabic version is requested, the HTML tag will have thedir
attribute set tortl
. As long as our styles specifydirection: rtl
for this case, the text should be displayed properly. - Using
onMounted
hook: We use theonMounted
hook to set the preferred locale if it’s found in local storage.
Localized routes in Nuxt i18n
Next, let’s see how to work with localized routes, which is crucial if your app has multiple pages.
First of all, create a new about.vue
page:
<template> <div class="container"> <GlobalMenu /> <h1>{{ $t('aboutTitle') }}</h1> <p>{{ $t('aboutDescription') }}</p> </div> </template>
Then let’s create a new GlobalMenu.vue
component:
<template> <nav class="global-menu"> <NuxtLink :to="localePath('/')">{{ $t('home') }}</NuxtLink> <NuxtLink :to="localePath('/about')">{{ $t('about') }}</NuxtLink> </nav> </template> <script setup> import { useLocalePath } from '#imports' const localePath = useLocalePath() </script>
The localePath
will properly handle localized routes for you.
Now you can use this component in your pages, for example, in about.vue
:
<template> <div class="container"> <GlobalMenu /> <h1>{{ $t('aboutTitle') }}</h1> <p>{{ $t('aboutDescription') }}</p> </div> </template> <script setup> import GlobalMenu from '~/components/GlobalMenu.vue' </script>
Do the same for the index.vue
file.
Finally, provide translations. Here are the en.json
translations:
{ "home": "Home", "about": "About", "aboutTitle": "About Us", "aboutDescription": "This is the about page of our application." }
fr.json
:
{ "home": "Accueil", "about": "À propos", "aboutTitle": "À propos de nous", "aboutDescription": "Ceci est la page à propos de notre application." }
ar.json
:
{ "home": "الرئيسية", "about": "حول", "aboutTitle": "معلومات عنا", "aboutDescription": "هذه صفحة المعلومات عن تطبيقنا." }
Great job! You’ve now set up localized routes and translations for your Nuxt.js app.
Using placeholders in Nuxt translations
Next, I’ll show you how to interpolate custom values in your translations.
Let’s adjust the welcoming message in index.vue
like this:
<p>{{ $t('message', { name: 'Lokalise' }) }}</p>
The { name: 'Lokalise' }
is interpolation: we basically say that the name
variable should contain the Lokalise
string.
To use this variable in your translation, simply provide its name in the curly brackets:
{ "message": "This tutorial is brought to you by {name}" }
You can do the same for all other languages.
Handling pluralization
Now let’s see how to work with pluralization. First, let’s add a button on the main page that shows how many times it has been pressed:
<template> <div class="container"> <GlobalMenu /> <h1>{{ $t('welcome') }}</h1> <p>{{ $t('message', { name: 'Lokalise' }) }}</p> <button @click="incrementCount">{{ $t('buttonPressed', { count: count }) }}</button> </div> </template>
You can see that here we use interpolation once again. Depending on the count
we will display one of the messages.
Now update the script for the main page:
import { ref } from 'vue' import GlobalMenu from '~/components/GlobalMenu.vue' const count = ref(0) const incrementCount = () => { count.value++ }
Next let’s provide English translation:
{ "buttonPressed": "You've pressed the button {count} time | You've pressed the button {count} times" }
The pipe |
character serves as a separator and provides two plural forms: when the button has been pressed once and when it has been pressed multiple times or zero times.
Now French translations:
{ "buttonPressed": "Vous avez appuyé sur le bouton {count} fois | Vous avez appuyé sur le bouton {count} fois" }
With Arabic translations, things are somewhat more complex because it has more than two plural forms. To overcome this problem, let’s write a custom pluralization rule. Create a new i18n/plurals.ts
file:
export const arabicPlurals = (choice: number): number => { if (choice === 0) { return 0 // Zero times } if (choice === 1) { return 1 // One Time } if (choice === 2) { return 2 // Two Times } if (choice >= 3 && choice <= 10) { return 3 // Many Times (3-10) } return 4 // 11 and above }
Alternatively you can use intl plural rules.
Now import these rules and use it inside the i18n/i18n.config.ts
:
import { arabicPlurals } from "./plurals" export default defineI18nConfig(() => ({ legacy: false, locale: 'en', fallbackLocale: 'en', pluralRules: { "ar": arabicPlurals, }, }))
Finally, provide translations inside ar.json
:
{ "buttonPressed": "لم يتم الضغط على الزر | مرة واحدة | مرتين | {count} مرات | {count} مرة" }
With these steps, you’ve learned how to use placeholders for dynamic values and handle pluralization in multiple languages, including languages with complex pluralization rules like Arabic.
Localizing date and time
Datetime localization in Nuxt can be performed easily. First, let’s add two new paragraphs to index.vue
:
<template> <div class="container"> <GlobalMenu /> <h1>{{ $t('welcome') }}</h1> <p>{{ $t('message', { name: 'Lokalise' }) }}</p> <button @click="incrementCount">{{ $t('buttonPressed', { count: count }) }}</button> <p>{{ $t('currentDate', { date: $d(currentDate, 'short') }) }}</p> <p>{{ $t('deadline', { dateTime: $d(deadline, 'long') }) }}</p> </div> </template>
We will display the current date and a random deadline in two different formats: long and short. Note that we use the $d
function to localize dates.
Now update the script in the same file:
import { ref } from 'vue' import GlobalMenu from '~/components/GlobalMenu.vue' const count = ref(0) const currentDate = new Date() const deadline = new Date(currentDate) deadline.setDate(currentDate.getDate() + 7) const incrementCount = () => { count.value++ }
We will need to create custom datetime formats, so add an i18n/datetime.ts
file:
export const datetimeFormats = { en: { short: { year: 'numeric', month: 'short', day: 'numeric' }, long: { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric' } }, fr: { short: { year: 'numeric', month: 'short', day: 'numeric' }, long: { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric' } }, ar: { short: { year: 'numeric', month: 'long', day: 'numeric', }, long: { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric', }, } } as const;
Feel free to further adjust these per-locale rules as needed.
Use the rules in the i18n.config.ts
file:
import { arabicPlurals } from "./plurals" import { datetimeFormats } from "./datetime" export default defineI18nConfig(() => ({ legacy: false, locale: 'en', fallbackLocale: 'en', pluralRules: { "ar": arabicPlurals, }, datetimeFormats, }))
Now add English translations:
{ "currentDate": "Current date: {date}", "deadline": "Deadline: {dateTime}" }
fr.json
:
{ "currentDate": "Date actuelle: {date}", "deadline": "Date limite: {dateTime}" }
And finally ar.json
:
{ "currentDate": "التاريخ الحالي: {date}", "deadline": "الموعد النهائي: {dateTime}" }
That’s it!
Localizing numbers and currencies
Let’s also demonstrate how to localize numbers in Nuxt.js. Add a new paragraph to the main page:
Add a new paragraph to the main page:
<template> <div class="container"> <GlobalMenu /> <h1>{{ $t('welcome') }}</h1> <p>{{ $t('message', { name: 'Lokalise' }) }}</p> <button @click="incrementCount">{{ $t('buttonPressed', { count: count }) }}</button> <p>{{ $t('currentDate', { date: $d(currentDate, 'short') }) }}</p> <p>{{ $t('deadline', { dateTime: $d(deadline, 'long') }) }}</p> <p>{{ $t('unitsPrice', { units: $n(2.56, 'decimal'), price: $n(1245, 'currency') }) }}</p> </div> </template>
We will display a text saying that this number of some random units costs this much. Make sure to use the $n
function for numbers localization.
Now let’s create i18n/numbers.ts
file with custom number formats:
export const numberFormats = { en: { currency: { style: 'currency', currency: 'USD' }, decimal: { style: 'decimal', minimumFractionDigits: 2, maximumFractionDigits: 2 } }, fr: { currency: { style: 'currency', currency: 'EUR' }, decimal: { style: 'decimal', minimumFractionDigits: 2, maximumFractionDigits: 2 } }, ar: { currency: { style: 'currency', currency: 'AED' }, decimal: { style: 'decimal', minimumFractionDigits: 2, maximumFractionDigits: 2 } } } as const;
As you can see, we can provide formats for regular numbers and for currencies (you can also provide separate rules for percentages and other cases).
Now use these formats inside i18n.config.ts
:
import { arabicPlurals } from "./plurals" import { datetimeFormats } from "./datetime" import { numberFormats } from "./numbers" export default defineI18nConfig(() => ({ legacy: false, locale: 'en', fallbackLocale: 'en', pluralRules: { "ar": arabicPlurals, }, datetimeFormats, numberFormats, }))
Provide English translations:
{ "unitsPrice": "{units} units cost {price}" }
fr.json
:
{ "unitsPrice": "{units} unités coûtent {price}" }
And finally ar.json
:
{ "unitsPrice": "{units} وحدات تكلف {price}" }
SEO and meta tags localization
Another important step is to properly localize the contents in the head tag, specifically your meta tags. Let’s see how to achieve that.
First, let’s adjust the script
in the index.vue
file:
import { ref, computed } from 'vue' import { useI18n } from 'vue-i18n' import GlobalMenu from '~/components/GlobalMenu.vue' const { t, locale } = useI18n() const count = ref(0) const currentDate = new Date() const deadline = new Date(currentDate) deadline.setDate(currentDate.getDate() + 7) const incrementCount = () => { count.value++ } // Setting up localized meta tags const metaTitle = computed(() => t('meta.title')) const metaDescription = computed(() => t('meta.description')) useHead({ title: metaTitle.value, meta: [ { name: 'description', content: metaDescription.value }, { property: 'og:title', content: metaTitle.value }, { property: 'og:description', content: metaDescription.value }, { property: 'og:locale', content: locale.value } ] })
We utilize useHead()
function to translate title, description and og meta
tags.
Now let’s provide English translations:
{ "meta": { "title": "Welcome to Our Site", "description": "This is the best site ever made for tutorials." } }
fr.json
:
{ "meta": { "title": "Bienvenue sur notre site", "description": "C'est le meilleur site jamais créé pour les tutoriels." } }
And ar.json
:
{ "meta": { "title": "مرحبًا بكم في موقعنا", "description": "هذا هو أفضل موقع تم إنشاؤه على الإطلاق للحصول على الدروس." } }
You can use similar approach on other pages of your site. Great!
Use Lokalise for Nuxt i18n
As your app grows, managing your translations becomes increasingly complex. If you need support for more locales, hiring a professional or using AI for translation becomes essential. That’s why we’ve created the Lokalise translation management system. It does all the heavy lifting for you and provides many features, including collaborative access, the ability to hire translators, use AI, and integrate with third-party tools and services like GitHub, Figma, Asana, and many others.
To get started, grab your free trial. Follow the wizard’s instructions to create your first project. Then proceed to the Upload page and upload your translation files. Once done, you can modify your texts, add more languages, utilize AI to translate more data, and so on. When you’re ready, simply proceed to the Download page and export your texts back to the project.
That’s it! To learn more about the platform you can refer to our docs or watch the free onboarding course covering all platform features.
Conclusion
That’s it for today! We have seen how to perform Nuxt i18n easily with the help of the i18n module. We’ve covered all the main features and approaches, and hopefully by now you’re ready to put your knowledge into practice.
If you want more, feel free to browse our collection of i18n tutorials aimed at developers. Specifically, you might be interested in our Vue i18n tutorial.
Thank you for staying with me, and until next time!