Nuxt i18n vue-i18n options ignored
You set fallbackLocale and missing German keys still render as raw paths. You add datetimeFormats and dates keep rendering in the engine default. You set missingWarn: false and production logs still fill with warnings. Nothing errors, nothing warns, and the configuration reads exactly like the documentation.
The options are in the wrong file. @nuxtjs/i18n splits its configuration across two surfaces, and options on the wrong side of that split are not merged, deprecated or reported — they are simply not read.
Root cause: two configurations with one name
The module owns route generation, locale detection and message loading, all of which happen at build time or in middleware. Those are module options, and they live in nuxt.config.ts under the i18n key.
vue-i18n owns message resolution, fallback behaviour, formatting and warnings. Those are runtime options belonging to the vue-i18n instance the module creates, and they live in a separate i18n.config.ts exported through defineI18nConfig.
Both are “i18n configuration” in every conversation about them, and in v7 of the module several runtime options could indeed be passed inline. The v8 shape moved them out, which means a configuration copied from an older project, an older article or an answer written against v7 is not wrong so much as inert.
The absence of a warning is the part that costs the most time. A misplaced option is indistinguishable from an option that does not work, so the search goes toward the feature — “why is my fallback locale not falling back” — rather than toward the file it is written in.
Minimal reproducible example
// nuxt.config.ts — every runtime option here is ignored
export default defineNuxtConfig({
modules: ['@nuxtjs/i18n'],
i18n: {
defaultLocale: 'en',
locales: [
{ code: 'en', language: 'en-GB', file: 'en.json' },
{ code: 'de', language: 'de-DE', file: 'de.json' },
],
fallbackLocale: 'en', // ← ignored
datetimeFormats: { 'de-DE': {} }, // ← ignored
missingWarn: false, // ← ignored
},
});
Every line above is valid TypeScript and valid Nuxt configuration. Three of them do nothing.
The fix: a runtime config file
// i18n.config.ts — the runtime half, at the project root
export default defineI18nConfig(() => ({
legacy: false,
fallbackLocale: { 'de-AT': ['de'], default: ['en'] },
missingWarn: process.env.NODE_ENV !== 'production',
fallbackWarn: false,
datetimeFormats: {
'de-DE': { short: { year: 'numeric', month: '2-digit', day: '2-digit' } },
'en-GB': { short: { year: 'numeric', month: 'short', day: 'numeric' } },
},
numberFormats: {
'de-DE': { currency: { style: 'currency', currency: 'EUR' } },
},
}));
The module picks this file up by convention when it sits at the project root. Where a project keeps configuration elsewhere, the module option vueI18n names the path — and that option is a module option, because it tells the build where to look rather than telling vue-i18n how to behave.
Proving the file is loaded
Configuration that fails silently deserves an assertion, and one runtime check is enough to cover the whole file: if any option from it is in effect, the file was loaded.
// tests/i18n-config.spec.ts
import { setup, $fetch } from '@nuxt/test-utils/e2e';
await setup({ server: true });
test('runtime i18n config is in effect', async () => {
// A key that exists only in en, requested in de, must fall back rather than render raw.
const html = await $fetch('/de/about');
expect(html).not.toContain('about.legal.notice'); // the raw key
expect(html).toContain('Legal notice'); // the English fallback
});
Asserting on the effect rather than on the configuration object is deliberate. A test that reads the config proves it was written; a test that observes fallback behaviour proves it was loaded, which is the thing that was in doubt.
Why the split exists at all
It is tempting to read the split as an accident, and understanding why it is not makes the boundary easier to remember.
Module options change what is built. The locale list determines which routes exist; the strategy determines their shape; langDir and lazy determine which chunks are emitted. All of that has to be known while the application is being compiled, and none of it can change afterwards without a rebuild.
Runtime options change how the instance behaves once it exists. A fallback chain, a date format or a warning flag can differ per environment, can be computed from an environment variable, and can in principle change while the application is running. Keeping them in a function — defineI18nConfig(() => ({ … })) — is what makes that possible.
Once the boundary is framed as build-time versus run-time rather than as two arbitrary files, placing a new option is usually obvious: ask whether the answer has to be known before the bundle is written.
An audit worth running once
Because misplaced options are invisible, a project that has been through a module upgrade usually has more than one. A single audit finds them all in a few minutes and is worth doing once rather than discovering them one bug at a time.
Start from the list of runtime option names rather than from the configuration file. vue-i18n’s surface is small enough to enumerate: fallbackLocale, fallbackWarn, missingWarn, missing, datetimeFormats, numberFormats, pluralRules, messages, silentTranslationWarn, warnHtmlMessage, escapeParameterHtml and legacy. Grepping the Nuxt config for those names finds every misplacement in one pass, including the ones nobody has noticed yet because the feature has not been exercised.
The messages key deserves particular attention. Passing messages inline in the module config appears to work — strings resolve — while quietly bypassing the lazy loading machinery, so every locale ends up in the main bundle. It is the one misplacement whose symptom is a bundle-size regression rather than a behavioural one, which means it can survive indefinitely without anyone filing a bug.
The audit is also a good moment to write down which file each option lives in, next to the configuration itself. A three-line comment at the top of nuxt.config.ts pointing at i18n.config.ts for runtime options costs nothing and prevents the next person from repeating the exercise. Configuration that fails silently is exactly the kind that benefits most from a comment, because the code cannot tell you it is wrong.
When to escalate
If the runtime file is loaded and an option still has no effect, check whether a second vue-i18n instance exists. A leftover manual createI18n in a plugin creates an instance the module does not configure, and the application may render with either one depending on import order — the same root cause described in Nuxt i18n lazy messages missing after a locale switch.
If fallbackLocale is loaded and correct but keys still render raw, the fallback locale may itself be missing the key. A chain terminates at a bundle that is assumed complete, and enforcing that assumption is the subject of fallback chain configuration.
If formats apply in development and not in production, compare the language field of each locale against the format keys. Formats are keyed by the exact tag, so a format registered under de-DE does nothing for a locale whose language is de.
FAQ
Why does the documentation show vueI18n as a module option?
Because it is one — but its value is a path, not an options object. The module needs to know where the runtime configuration lives, which is a build-time question. Passing an object where a path is expected is the exact mistake this page describes.
Can I compute runtime options from environment variables?
Yes, and that is why the runtime config is a function. It is evaluated when the instance is created, so anything available at that moment — an environment variable, a runtime config value from Nuxt — can shape the returned object.
Do I need legacy: false?
For a Nuxt 3 application, yes. It puts vue-i18n into Composition API mode, which is what useI18n expects. Leaving it on the legacy default produces a different set of confusing symptoms, covered in Vue i18n composition API guide.
Is there a way to make misplaced options fail loudly?
Not from the module, but a project can add its own guard: a small build-time check that fails when the i18n key in nuxt.config.ts contains any of the known runtime option names. It is a dozen lines and it removes an entire category of afternoon.
Which file do custom plural rules go in?
The runtime one. Plural rules are resolution behaviour, and they are also the option whose absence is hardest to notice — a language quietly selecting the wrong form reads as a translation problem rather than a configuration one, which is why the parity checks in pluralization rules across languages are worth having regardless.
Related
- Nuxt i18n Module Setup — the full configuration split across both files.
- Vue i18n Composition API Guide — what the runtime options actually control.
- Fallback Chain Configuration — the behaviour a misplaced
fallbackLocalefails to deliver. - Date & Number Formatting Standards — what the ignored format tables were meant to configure.
Part of Nuxt i18n Module Setup.