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.

Module options versus vue-i18n options The locale list, routing strategy, lazy loading and message directory are module options and belong in the Nuxt config. Fallback locale, date and number formats, missing-key warnings and custom plural rules are vue-i18n options: placed in the Nuxt config they are silently ignored, and they take effect only from the runtime i18n config file. Which option belongs in which file nuxt.config.ts i18n.config.ts locales, strategy yes no lazy, langDir yes no fallbackLocale ignored yes datetimeFormats ignored yes missingWarn ignored yes pluralRules ignored yes
Nothing warns about the wrong half of this table — the option simply has no effect.

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.

Four symptoms of a misplaced runtime option A fallback locale placed in the wrong file leaves missing keys rendering as raw paths. Misplaced date and number formats revert to engine defaults. A misplaced missingWarn setting lets development warnings reach production logs. Misplaced custom plural rules leave one language selecting the wrong form. What an ignored option looks like The option is set and nothing changes no error, no warning fallbackLocale ignored missing keys render raw datetimeFormats ignored dates fall back to defaults missingWarn ignored console floods in production pluralRules ignored wrong form for one language
Each symptom is usually debugged as its own bug; all four have one cause.

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.

Four steps to a runtime config that is actually loaded A configuration file at the project root exports the vue-i18n options through defineI18nConfig. Every runtime option moves into it. Where auto-detection does not apply, the module option points at the file path. A single runtime assertion then proves the file was loaded rather than assumed. Moving the options to where they take effect 1 Create i18n.config.ts at the project root exported via defineI18nConfig 2 Move every vue-i18n option into it fallback, formats, warnings, rules 3 Point the module at it if not auto-detected i18n.vueI18n: "./i18n.config.ts" 4 Assert one option at runtime a test proves the file is loaded
The last step is the only one that distinguishes "configured" from "in effect".

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.

Part of Nuxt i18n Module Setup.