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>
</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>
<LanguageSwitcher />
<NuxtPage />
</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>
</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 the dir
(direction) attribute accordingly. For example, when the Arabic version is requested, the HTML tag will have the dir
attribute set to rtl
. As long as our styles specify direction: rtl
for this case, the text should be displayed properly.
Using onMounted
hook: We use the onMounted
hook to set the preferred locale if it’s found in local storage.
<nav class="global-menu">
<NuxtLink :to="localePath('/')">{{ $t('home') }}</NuxtLink>
<NuxtLink :to="localePath('/about')">{{ $t('about') }}</NuxtLink>
</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>
</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>
</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>
</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>
</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!
Ilya is a lead of content/documentation/onboarding at Lokalise, an IT tutor and author, web developer, and ex-Microsoft/Cisco specialist. His primary programming languages are Ruby, JavaScript, Python, and Elixir. He enjoys coding, teaching people and learning new things. In his free time he writes educational posts, participates in OpenSource projects, goes in for sports and plays music.
Talk to one of our localization specialists
Book a call with one of our localization specialists and get a tailored consultation that can guide you on your localization path.
Get a demo
Behind the scenes of localization with one of Europe’s leading digital health providers
Rеad more