Skip to content

Nuxt + @nuxtjs/i18n + Locize

The canonical Nuxt internationalization module, paired with Locize for cloud translation management, AI translation, and CDN-synced JSON.

@nuxtjs/i18n is the official Nuxt internationalization module, built on top of vue-i18n. It adds the Nuxt-specific layer that plain vue-i18n cannot provide on its own: locale-aware routing (prefix, prefix_except_default, no_prefix strategies), automatic <html lang> and <link rel="alternate"> SEO tags, browser-language detection, lazy-loaded locale files, cookie-based locale persistence, and a useI18n() composable that works on the server during Nitro SSR, on the client during hydration, and during nuxt generate for static sites.

The Locize integration is bundle-based, not live-fetch. Because Nuxt 4 ships Nitro SSR by default, translations are synced from Locize into your i18n/locales/ folder at build time via locize-cli — the same JSON is then available to Nitro on the server and shipped as static chunks to the client. Optional runtime saveMissing pushes newly-encountered keys back to Locize during development via vue-i18n's missing handler, and the in-context editor uses locize 4.1's getVueI18nImplementation to wire vue-i18n's composer into the locize editor overlay.

Key facts
  • Module: @nuxtjs/i18n (built on vue-i18n)
  • Current version: v10.x for Nuxt 4 (Nuxt 3 also supported)
  • Format: vue-i18n syntax (handled by Locize's format-vue-i18n package)
  • Loading strategy: bundle-at-build-time via locize-cli (Nitro-safe)
  • In-context editor: locize 4.1+ via getVueI18nImplementation
  • Example repo: locize-nuxt-example
  • Companion blog: How to internationalize a Nuxt 4 app with @nuxtjs/i18n and Locize
  • Best for: Nuxt 3 / 4 projects that need SSR / SSG, locale-aware routing, and SEO helpers

How @nuxtjs/i18n is configured

The module is registered in nuxt.config.ts. langDir + per-locale file entries point @nuxtjs/i18n at the locale loader files (i18n/locales/en.ts, i18n/locales/de.ts), which in turn assemble the per-namespace JSON synced from Locize.

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxtjs/i18n'],

  i18n: {
    strategy: 'no_prefix',
    defaultLocale: 'en',
    lazy: true,
    langDir: 'locales/',
    locales: [
      { code: 'en', name: 'English', file: 'en.ts' },
      { code: 'de', name: 'Deutsch', file: 'de.ts' }
    ]
  },

  runtimeConfig: {
    public: {
      locizeProjectId: process.env.LOCIZE_PROJECT_ID,
      locizeApiKey: process.env.LOCIZE_API_KEY, // dev/preview only
      locizeVersion: process.env.LOCIZE_VERSION || 'latest',
      locizeReferenceLng: 'en'
    }
  }
})

The runtimeConfig.public block exposes the Locize project id, version, and (dev-only) write API key to client code via useRuntimeConfig(). The public locizeApiKey is unset in production builds so it never ships to end users.

Syncing translations with locize-cli

Translations live in your Locize project; locize-cli pulls them into i18n/locales/{lang}/ before each build. Add scripts to package.json:

# package.json scripts
{
  "scripts": {
    "downloadLocales": "locize download --project-id=$LOCIZE_PROJECT_ID --ver=latest --path=./i18n/locales --clean=true",
    "syncLocales": "locize sync --project-id=$LOCIZE_PROJECT_ID --api-key=$LOCIZE_API_KEY --ver=latest --path=./i18n/locales"
  }
}

Run npm run downloadLocales in CI before nuxt build. Use syncLocales in development to push new keys back to Locize without a runtime saveMissing call.

Wiring the in-context editor and saveMissing

The locize package itself is only used in dev / preview. A client-only Nuxt plugin grabs vue-i18n's composer and starts the editor when ?incontext=true is in the URL:

1. The Nuxt client plugin

// app/plugins/locize.client.ts
import locize, { getVueI18nImplementation } from 'locize'
import { useI18n } from 'vue-i18n'
import { watch } from 'vue'
import { locizeRuntime } from '../utils/locize-runtime'

export default defineNuxtPlugin((nuxtApp) => {
  const config = useRuntimeConfig()
  const { composer } = useI18n({ useScope: 'global' })

  // Share the composer + config with i18n.config.ts so the missing handler
  // can call locizer.add(...) without re-importing locize.
  locizeRuntime.composer = composer
  locizeRuntime.projectId = config.public.locizeProjectId
  locizeRuntime.apiKey = config.public.locizeApiKey
  locizeRuntime.version = config.public.locizeVersion
  locizeRuntime.referenceLng = config.public.locizeReferenceLng

  // Only start the in-context editor when explicitly requested.
  if (window.location.search.includes('incontext=true')) {
    locize({
      projectId: config.public.locizeProjectId,
      version: config.public.locizeVersion,
      referenceLng: config.public.locizeReferenceLng,
      inContext: true
    }, getVueI18nImplementation(composer, watch))
  }
})

2. vue-i18n config — missing handler and postTranslation

// i18n/i18n.config.ts
import type { Composer } from 'vue-i18n'
import { locizeRuntime } from '../app/utils/locize-runtime'

// IMPORTANT: do not use defineI18nConfig() — its source-transform regex
// can corrupt this file. A plain default export works identically.
export default function () {
  return {
    legacy: false,
    missing: handleMissing,
    postTranslation: handlePostTranslation
  }
}

function handleMissing (locale: string, key: string) {
  // Optional: push new keys back to Locize during development.
  // Skip in production — your write API key must never ship.
  if (!locizeRuntime.apiKey) return
  if (locale !== locizeRuntime.referenceLng) return
  fetch(`https://api.locize.app/missing/${locizeRuntime.projectId}/${locizeRuntime.version}/${locale}/messages`, {
    method: 'POST',
    headers: { Authorization: locizeRuntime.apiKey, 'Content-Type': 'application/json' },
    body: JSON.stringify({ [key]: key })
  })
}

// The in-context editor's wrap/unwrap markers — see locize 4.1.
function handlePostTranslation (str: any) { /* see example repo */ return str }

The full working example (with postTranslation handling for both string and Vue VNode shapes — needed because vue-i18n's <i18n-t> component passes VNode arrays through the pipeline) lives at locize-nuxt-example. Step-by-step walkthrough: How to internationalize a Nuxt 4 app with @nuxtjs/i18n and Locize.

Frequently asked questions

What is @nuxtjs/i18n?

@nuxtjs/i18n is the official Nuxt internationalization module. It is built on top of vue-i18n and adds Nuxt-specific features: locale-aware routing (prefix, prefix_except_default, no_prefix strategies), automatic <html lang> + <link rel="alternate"> SEO tags, browser-language detection, lazy-loaded locale files, cookie-based locale persistence, and a useI18n() composable that works across server, client, and during static generation. The current line is v10.x for Nuxt 4 (Nuxt 3 also supported).

Does Locize support @nuxtjs/i18n?

Yes. Since @nuxtjs/i18n is built on vue-i18n, you use Locize's native vue-i18n format support: pluralization handled within the message value (no separate _one / _other keys), named/list/index interpolation, and linked messages all render correctly in the Locize editor. The integration is bundle-based — translations are synced from Locize at build time via locize-cli (so Nitro SSR and the client both see the same JSON), with an optional runtime saveMissing handler that pushes newly-encountered keys back to Locize during development. The in-context editor is wired in via locize 4.1's getVueI18nImplementation.

How do I connect @nuxtjs/i18n to Locize?

Run locize-cli to download all locales into your i18n/locales/{lang}/{namespace}.json folders, register them through defineI18nLocale() wrappers in i18n/locales/en.ts and i18n/locales/de.ts, and configure @nuxtjs/i18n in nuxt.config.ts with langDir: 'locales/', strategy: 'no_prefix', and lazy: true. The locize package only ships in dev/preview via a client-side Nuxt plugin (app/plugins/locize.client.ts) that initializes locize with vue-i18n's composer and starts the in-context editor. Production builds skip locize entirely. A complete working example lives at github.com/locize/locize-nuxt-example.

Why bundle translations at build time instead of fetching live from the CDN?

Nuxt 4 ships Nitro SSR by default: the same code runs on the server during HTML rendering and on the client during hydration. Live-fetching from a CDN would mean an extra request during SSR (slower TTFB), a hydration mismatch risk if the client fetches before its first render, and brittle behaviour during static generation (nuxt generate). Bundling translations at build time via locize-cli sidesteps all three: every locale file is in your build artifact, Nitro serves them instantly during SSR, and the client receives them as static .js chunks. The trade-off is that translation updates require a rebuild — but that's how Nuxt itself wants you to ship content. For live updates without a rebuild, use the @nuxtjs/i18n + locize sync flow in your CI/CD pipeline.

Do I need saveMissing for @nuxtjs/i18n + Locize to work?

No. saveMissing is optional. Many teams prefer to manage keys explicitly via the Locize web app or via locize sync (which lets you push your dev-side JSON to Locize on demand). saveMissing is convenient during early development — every new t('some.key') call automatically appears in your Locize project — but it requires shipping a write API key to the browser, so it's strictly a dev-only feature. The locize-nuxt-example shows both flows: saveMissing via vue-i18n's missing handler when LOCIZE_API_KEY is set, and the locize-cli sync command for explicit key management.

Does the in-context editor work with @nuxtjs/i18n?

Yes, via locize 4.1+'s vue-i18n implementation. The example wires it up in app/plugins/locize.client.ts: it imports getVueI18nImplementation from locize, passes the vue-i18n composer and Vue's watch function, then calls locize({ inContext: true }) when ?incontext=true is in the URL. The editor uses i18next-subliminal markers (zero-width characters appended to each translation) to identify which key maps to which DOM node, so editing a paragraph in the live preview maps back to the exact Locize key.

How does @nuxtjs/i18n compare to plain vue-i18n for Locize integration?

Plain vue-i18n (without Nuxt) is simpler — you import createI18n directly and load translations via locizer.loadAll. With Nuxt, @nuxtjs/i18n adds the Nuxt-aware features (routing, SEO, SSR-safe locale state) but in return constrains how you load translations: messages must come from i18n/locales/{lang}.{ts,js} files at build time. That's why the Locize-Nuxt integration is bundle-based rather than live-fetch. Pick @nuxtjs/i18n for any production Nuxt app; pick plain vue-i18n only for Vue 3 apps that don't use Nuxt.