Guide

Lang Switcher

How to change your website's current language.

When Nuxt i18n module is loaded in your app, it adds your locales configuration to nuxtApp.$i18n (or this.$i18n), which makes it really easy to display a lang switcher anywhere in your app.

Here's an example of a lang switcher where a name key has been added to each locale object in order to display friendlier titles for each link:

<script setup>
const { locale, locales } = useI18n()
const switchLocalePath = useSwitchLocalePath()

const availableLocales = computed(() => {
  return locales.value.filter(i => i.code !== locale.value)
})
</script>

<template>
  <NuxtLink v-for="locale in availableLocales" :key="locale.code" :to="switchLocalePath(locale.code)">
    {{ locale.name }}
  </NuxtLink>
</template>
nuxt.config.ts
export default defineNuxtConfig({
  i18n: {
    locales: [
      {
        code: 'en',
        name: 'English'
      },
      {
        code: 'es',
        name: 'Español'
      },
      {
        code: 'fr',
        name: 'Français'
      }
    ]
  }
})
To persist the locale on a route change when using detectBrowserLanguage, you must explicitly update the stored locale cookie. This is done with setLocaleCookie(locale) or setLocale(locale), which sets the cookie and switches to the route of the specified locale. Not doing so can cause redirects based on the locale set on the locale cookie during navigation.

The template code might look like this, for example:

<script setup>
const { locale, locales, setLocale } = useI18n()

const availableLocales = computed(() => {
  return locales.value.filter(i => i.code !== locale.value)
})
</script>

<template>
  ...
  <a href="#" v-for="locale in availableLocales" :key="locale.code" @click.prevent.stop="setLocale(locale.code)">
    {{ locale.name }}
  </a>
  ...
</template>

Dynamic route parameters

Dealing with dynamic route parameters requires a bit more work because you need to provide parameters translations to Nuxt i18n module. The composable useSetI18nParams can be used to set translations for route parameters, this is used to set SEO tags as well as changing the routes returned by switchLocalePath.

During SSR it matters when and where you set i18n parameters, since there is no reactivity during SSR.

Components which have already been rendered do not update by changes introduced by pages and components further down the tree. Instead, these links are updated on the client side during hydration, in most cases this should not cause issues.

This is not the case for SEO tags, these are updated correctly during SSR regardless of when and where i18n parameters are set.

Check out the experimental switchLocalePathLinkSSR feature, which combined with the <SwitchLocalePathLink> component, correctly renders links during SSR regardless of where and when it is used.

An example (replace slug with the applicable route parameter):

<script setup>
// fetch product from API... (red mug)

const setI18nParams = useSetI18nParams()
setI18nParams({
  en: { slug: data.slugs.en }, // slug: 'red-mug'
  nl: { slug: data.slugs.nl } // slug: 'rode-mok'
})

const switchLocalePath = useSwitchLocalePath()
switchLocalePath('en') // /products/red-mug
switchLocalePath('nl') // /nl/products/rode-mok
</script>

<template>
  <!-- pages/products/[slug].vue -->
</template>

Note that for the special case of a catch-all route named like [...pathMatch.vue], the key of the object needs to say pathMatch. For example:

<script>
const setI18nParams = useSetI18nParams()
setI18nParams({
  en: { pathMatch: ['not-found-my-post'] },
  fr: { pathMatch: ['not-found-mon-article'] }
})
</script>

<template>
  <!-- pages/[...pathMatch].vue -->
</template>

Note that a catch all route is defined as an array. In this case, there is only one element, but if you want to use a sub-path, for example /not-found/post, define multiple elements as in ['not-found', 'post']. You will need to define more than one, e.g. ['not-found', 'post'].

Nuxt i18n module won't reset parameters translations for you, this means that if you use identical parameters for different routes, navigating between those routes might result in conflicting parameters. Make sure you always set params translations in such cases.

Wait for page transition

By default, the locale will be changed right away when navigating to a route with a different locale which means that if you have a page transition, it will fade out the page with the text already switched to the new language and fade back in with the same content.

To work around the issue, you can set the option skipSettingLocaleOnNavigate to true and handle setting the locale yourself from a onBeforeEnter transition hook defined in a plugin.

Global transition

If you would like to transition the entire Nuxt app, you can use the transition of NuxtPage to control it as follows:

nuxt.config.ts
export default defineNuxtConfig({
  i18n: {
    // ... your other options
    skipSettingLocaleOnNavigate: true
  }
}
pages/app.vue
<script setup lang="ts">
const { finalizePendingLocaleChange } = useI18n()

const onBeforeEnter = async () => {
  await finalizePendingLocaleChange()
}
</script>

<template>
  <NuxtLayout>
    <NuxtPage
      :transition="{
        name: 'my',
        mode: 'out-in',
        onBeforeEnter
      }"
    />
  </NuxtLayout>
</template>

<style>
.my-enter-active,
.my-leave-active {
  transition: opacity 0.3s;
}
.my-enter,
.my-leave-active {
  opacity: 0;
}
</style>

Optional, wait for locale before scrolling for a smoother transition with Router Options:

app/router.options.ts
import type { RouterConfig } from '@nuxt/schema'

export default <RouterConfig>{
  async scrollBehavior(to, from, savedPosition) {
    const nuxtApp = useNuxtApp()

    // make sure the route has changed.
    if (nuxtApp.$i18n && to.name !== from.name) {
      // `$i18n` is injected in the `setup` of the nuxtjs/i18n module.
      // `scrollBehavior` is guarded against being called even when it is not completed
      await nuxtApp.$i18n.waitForPendingLocaleChange()
    }

    return savedPosition || { top: 0 }
  }
}

Per page component transition

If you have a specific transition defined in a page component with definePageMeta and need to add finalizePendingLocaleChange at onBeforeEnter hook for pageTransition.

Example:

pages/about.vue
<script setup lang="ts">
const route = useRoute()
const { finalizePendingLocaleChange } = useI18n()

definePageMeta({
  pageTransition: {
    name: 'page',
    mode: 'out-in'
  }
})

route.meta.pageTransition.onBeforeEnter = async () => {
  await finalizePendingLocaleChange()
}
</script>

<style scoped>
.page-enter-active,
.page-leave-active {
  transition: opacity 1s;
}
.page-enter,
.page-leave-active {
  opacity: 0;
}
</style>

Dynamic route parameters using definePageMeta (Deprecated)

Dynamic route params using nuxtI18n on definePageMeta has been deprecated and will be removed in v9, use the composable useSetI18nParams instead.

Dynamic params can be configured using definePageMeta. These will be merged with route params when generating lang switch routes with switchLocalePath().

You have to set the dynamicRouteParams option to true in Nuxt i18n module's options to enable dynamic route parameters.

An example (replace id with the applicable route parameter):

<script setup>
definePageMeta({
  // ...
  nuxtI18n: {
    en: { id: 'my-post' },
    fr: { id: 'mon-article' }
  }
  // ...
})
</script>
<template>
  <!-- pages/post/[id].vue -->
</template>

Note that for the special case of a catch-all route named like [...pathMatch.vue], the key of the object needs to say pathMatch. For example:

<script>
definePageMeta({
  // ...
  nuxtI18n: {
    en: { pathMatch: ['not-found-my-post'] },
    fr: { pathMatch: ['not-found-mon-article'] }
  }
  // ...
})
</script>
<template>
  <!-- pages/[...pathMatch].vue -->
</template>

Vue i18n caveat

In contrast to Vue i18n you should not directly set locale, switch language by using setLocale or navigating to a route returned by switchLocalePath. This loads translations, triggers hooks and updates the locale cookie if used.


Copyright © 2024