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.
- 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-i18npackage) - 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
@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).
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.
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.
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.
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.
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.
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.