
Nuxt Route-Based Localization with @nuxtjs/i18n
As the web continues to reach a global audience, making your website accessible to speakers of multiple languages is no longer just a “nice to have”. It’s a strategic move for inclusivity, SEO, and user engagement. This article will guide you through setting up internationalization (i18n) or localization in a Nuxt project using the official @nuxtjs/i18n
module.
Hopefully, this guide will save you the future stress of having to manually migrate your entire app’s content into an i18n-friendly setup.
Why Should I Bother Localizing?
With billions of users coming online globally, many of whom do not speak English, building localized web experiences increases both your reach and impact. Offering your site in multiple languages is essential if your target audience includes non-English speakers or if you’re operating in multilingual regions.
Search engines like Google reward localized content. By having translated routes (e.g., /ja/about
or /de/about
), you’re more likely to rank for queries in those respective languages, broadening your discoverability.
If you wait too long to implement i18n, you’ll find yourself chasing down hardcoded strings across multiple pages, components, and internal code. By setting up i18n from the beginning, you create a scalable architecture that saves time and headaches down the road.
Initialize the Project with @nuxtjs/i18n
To get started with localization in Nuxt:
pnpm create nuxt nuxt-appcd nuxt-apppnpx nuxi@latest module add @nuxtjs/i18n
This installs and configures the @nuxtjs/i18n
module automatically.
The i18n Configuration
Now let’s define our supported locales by creating a new file:
import type { NuxtConfig } from "nuxt/schema";
/** * We have to add `as const` or else TypeScript will complain */export const i18n = { locales: [ { code: "en" as const, name: "English", file: "en.json" }, { code: "ja" as const, name: "Japanese", file: "ja.json" }, { code: "de" as const, name: "Deutsh", file: "de.json" }, ], defaultLocale: "en" as const, bundle: { optimizeTranslationDirective: false, }, lazy: true, // Lazy load translations instead of bundling them all at once strategy: "prefix_except_default" as const, // Adds locale prefix to all paths except default};
/** * Used to pre-render paths for each locale */export function prerenderi18nPaths( paths: string[], ctx: { routes: Set<string>; },) { const prerenderRoutes: string[] = [ ...paths, ...i18n.locales .filter((l) => l.code !== i18n.defaultLocale) .flatMap((locale) => paths.map((path) => `/${locale.code}${path}`)), ];
for (const route of prerenderRoutes) { ctx.routes.add(route); }}
/** * Creates route rules for all localized paths */export function createi18nRouteRules( paths: string[], rules: Exclude<NuxtConfig["routeRules"], undefined>[string],): NuxtConfig["routeRules"] { return Object.fromEntries( paths.flatMap((path) => [ [path, rules], ...i18n.locales .filter((l) => l.code !== i18n.defaultLocale) .map((l) => [`/${l.code}${path}`, rules]), ]), );}
This setup allows you to define all your i18n config in a single source of truth. Furthermore, I’ve added some helper utilities you can use in order to have fine-grained control over route generation and pre-rendering since the module doesn’t handle pre-rendering prefixed routes out of the box.
Register i18n in nuxt.config.ts
import { i18n } from "./config/i18n.config";
export default defineNuxtConfig({ compatibilityDate: "2025-05-15", devtools: { enabled: true }, modules: ["@nuxtjs/i18n"], i18n,});
Gotcha: Pre-rendering and Route Rules Not Working
CAUTION
By default, Nuxt’s pre-renderer does not automatically crawl locale-prefixed routes. While nitro.prerender.crawlLinks
attempts to infer routes by parsing <a>
tags, it may over-eagerly include dynamic pages that shouldn’t be pre-rendered.
To avoid this, use the prerenderi18nPaths
and createi18nRouteRules
utilities to explicitly define localized routes.
For example:
import { i18n, createi18nRouteRules, prerenderi18nPaths,} from "./config/i18n.config";
export default defineNuxtConfig({ compatibilityDate: "2025-05-15", devtools: { enabled: true }, modules: ["@nuxtjs/i18n"], i18n, routeRules: { ...createi18nRouteRules(["/render/spa"], { ssr: false }) }, hooks: { async "prerender:routes"(ctx) { const rawRoutes = ["/render/ssg"]; prerenderi18nPaths(rawRoutes, ctx); }, },});
Add Translation Files
By default, you have to place your translations inside the default folder i18n/locales
. Simply copy & paste the content below:
English (i18n/locales/en.json
)
{ "welcome": "Welcome to My Multilingual Site!", "choose_language": "Select your preferred language:"}
Japanese (i18n/locales/ja.json
)
{ "welcome": "多言語サイトへようこそ!", "choose_language": "言語を選択してください:"}
German (i18n/locales/de.json
)
{ "welcome": "Willkommen auf meiner mehrsprachigen Website!", "choose_language": "Wähle deine bevorzugte Sprache:"}
Language Switcher Component
We opt for <NuxtLink />
instead of handling it on the client via <button>
so that web crawlers (and Nitro if you’re using it) can crawl the links and index your alternative pages.
<script setup lang="ts">const { locale, locales } = useI18n();const switchLocalePath = useSwitchLocalePath();
const availableLocales = computed(() => { return locales.value.filter((i) => i.code !== locale.value);});</script>
<template> <div> <NuxtLink v-for="locale of availableLocales" :key="locale.code" :to="switchLocalePath(locale.code)" style="color: blue; cursor: pointer; margin-right: 1rem" > {{ locale.name }} </NuxtLink> </div></template>
Live Demo
Now, create a pages/index.vue
file and paste this code. It’s a simple page with the locale links.
<template> <div class="i18n-demo"> <h1 class="fade-in">{{ $t("welcome") }}</h1> <p>{{ $t("choose_language") }}</p> <div class="switcher"> <LanguageSwitcher /> </div> </div></template>
<style>.i18n-demo { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; background: linear-gradient(to bottom right, #f0f4ff, #ffffff); text-align: center; font-family: "Segoe UI", sans-serif;}
h1 { font-size: 3rem; color: #2b6cb0; text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.1); animation: fadeIn 0.8s ease-out forwards;}
@keyframes fadeIn { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: none; }}</style>
TIP
$t()
is the auto-imported translation function provided by Vue I18n. For advanced formatting like pluralization, rich text, and more, see the Composition API docs.
pnpm dev
Visit http://localhost:3000 and try switching between English, Japanese, and German using the links rendered by your custom <LanguageSwitcher />
component.
Scaling Translations
Currently, your translations live inside your repository; which isn’t always ideal, especially if you want to allow other non-technical people collaborate on your translations. Also, you may not want to disclose your source code to just about anyone, so giving them access to the codebase just to edit the translation files is not secure & most especially not user-friendly.
Thankfully, there are existing platforms that solve just that: Translation Management Systems (TMS) like Tolgee (self-hostable) and Crowdin (free up to a limit). They basically serve your translations on the cloud so that you can download them or even directly fetch from them. They also have other features like AI translations, live translation editing, and GitHub CI/CD.
You can also consider using a Content Management Systems (CMS) like Directus, which have support for managing files & even having a dedicated user interface for translators.
These platforms allow you to let other people edit the translations dynamically without much friction with your code base. However, they aren’t the focus on this article, so do check the Further Reading section for more information on how to set them up.
Conclusion
The Nuxt ecosystem makes internationalization a first-class citizen with tools like @nuxtjs/i18n
. Compared to frameworks like next-intl
for Next.js, Nuxt’s approach doesn’t require route middleware to intercept and handle localization logic. Instead, it integrates directly with the file system and Vue’s reactivity system, resulting in:
- Better performance during client-side navigation
- Lower server load due to static generation
- Simpler route handling without custom logic
@nuxtjs/i18n
is powered by vue-i18n
, a battle-tested library that supports advanced features like pluralization, rich text, and dynamic modifiers.
This guide only scratches the surface of this enourmous library, so there’s a lot more for you to explore. Happy translating!