Skip to content
Nuxt i18n

Nuxt i18n Integration

Full Nuxt 3 support with auto-imports, SSR, and Nitro compatibility.

Setup

Wire Better i18n into Nuxt in three steps

No Nuxt module to install: a plugin, the zero-dependency core client, and the vue-i18n you already have.

Install the client

core delivers and caches the messages. Add @nuxtjs/i18n only if you also want it to own locale routing.

terminalbash
npm install @better-i18n/core vue-i18n

# Optional — only if you want @nuxtjs/i18n to own
# locale routing, prefixes and hreflang:
npm install @nuxtjs/i18n

Create the client at module scope

One instance per server process, so the 60-second cache is shared across requests instead of rebuilt per render.

lib/better-i18n.tsts
import { createI18nCore } from '@better-i18n/core'

// Module scope, not inside a composable: the TTL cache lives on the
// instance, so a per-request client would refetch on every render.
export const betterI18n = createI18nCore({
  projectId: 'your-org/your-project', // Settings → General → Project ID
  defaultLocale: 'en',
})

Register vue-i18n from a Nuxt plugin

The plugin awaits the messages before the app mounts, so SSR output and client hydration read the same strings.

plugins/better-i18n.tsts
import { createI18n } from 'vue-i18n'
import { betterI18n } from '~/lib/better-i18n'

export default defineNuxtPlugin(async (nuxtApp) => {
  const locale = useRoute().params.locale?.toString() ?? 'en'

  // Runs on the server during SSR, and in the browser on a cold client
  // navigation. The CDN shape is the shape vue-i18n expects.
  const messages = await betterI18n.getMessages(locale)

  nuxtApp.vueApp.use(
    createI18n({
      legacy: false,
      locale,
      fallbackLocale: 'en',
      messages: { [locale]: messages },
    })
  )
})

How it works

Where a Nuxt string comes from

No build step and no bundled locale files: the plugin resolves a locale at runtime, on the server first.

Read path — every locale load

Your Nuxt app

useI18n() reads the messages the plugin registered before mount.

0 network calls per render

@better-i18n/core

getMessages(locale) from the plugin — server-side during SSR.

60s TTL · 0 deps

CDN edge

Cloudflare worker answers from the nearest edge cache.

max-age=60 · always 200

R2 object store

The published translation files the sync worker wrote.

source of truth

Fallback chain — tried in order when a hop fails

1In-memory TTL cache
2CDN fetch, with timeout and one retry
3Persistent storage, if configured
4staticData bundled with the app
5Throw — after everything above missed

Write path — dashboard to app

AI or translator

Proposal reviewed in the dashboard, glossary enforced.

MCP · dashboard · CLI

Publish

Sync worker writes the locale files to R2.

better-i18n publish

CDN purge

Fire-and-forget purge of the affected keys and the manifest.

non-critical by design

Live in the app

The next getMessages() past the TTL returns the new copy.

~60s worst case

0dependencies in core
60scache TTL, client and edge
200CDN status, even on failure
5fallback layers before an error
In a page

Quick Start

Add i18n to your Nuxt app with the module.

pages/[locale]/index.vuevue
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { betterI18n } from '~/lib/better-i18n'

const { t, locale, setLocaleMessage } = useI18n()

// Locales come from the project manifest, not a hardcoded array
const { data: languages } = await useAsyncData('locales', () =>
  betterI18n.getLanguages()
)

async function switchTo(next: string) {
  setLocaleMessage(next, await betterI18n.getMessages(next))
  locale.value = next
}
</script>

<template>
  <h1>{{ t('home.title') }}</h1>
  <p>{{ t('home.greeting', { name: 'World' }) }}</p>
</template>
On the server

Translated Nitro server routes

The client has no browser-only dependency, so an API route can answer in the caller's language too.

server/api/greeting.get.tsts
// server/api/greeting.get.ts — Nitro route, same client
import { betterI18n } from '~/lib/better-i18n'

export default defineEventHandler(async (event) => {
  const locale = getQuery(event).locale?.toString() ?? 'en'

  // Shares the in-memory cache with the rest of the server process.
  const messages = await betterI18n.getMessages(locale)

  return { text: messages.api?.greeting ?? '', locale }
})
Capabilities

Features

Nuxt 3 native module
Auto-import composables
SEO utilities (useHead, useSeoMeta)
Locale-based routing
Lazy loading locales
Full SSR support
Nuxt DevTools integration
Nitro server support
Hybrid rendering support
Works with

Better I18N and the Nuxt ecosystem

We handle translation management and delivery; the Nuxt pieces you already use keep their jobs.

Better I18N + @nuxtjs/i18n

The official Nuxt module for locale routing: path prefixes, redirects, hreflang and SEO head tags.Keeps routing, hands translation delivery to Better I18N

Better I18N + vue-i18n

The formatter underneath: t(), ICU plurals, date and number formatting, reactive locale state.Receives the CDN messages as-is — no adapter in between

Better I18N + Nitro

Nuxt's server engine, where API routes and server-side rendering actually execute.Runs the same zero-dependency client, sharing one cache

Explore Other Framework Guides

Get started

Start Building with Nuxt i18n

Free tier available. No credit card required.