Combining routing strategies, lazy loading, and hreflang correctly
@nuxtjs/i18n is the de facto standard module for shipping a Nuxt application in multiple languages, and it covers far more than plain text translation: it decides the URL structure per language, lazy-loads translation files on demand, and automatically generates the hreflang tags needed for international SEO. Getting the routing strategy wrong early on often means migrating the entire URL structure of the site later, which is why a deliberate decision at the start of a project pays off.
Table of Contents
- 1. Why @nuxtjs/i18n is the standard choice for multilingual Nuxt apps
- 2. Installing the module and setting up locales
- 3. Comparing routing strategies: prefix, subdomain, and domain per locale
- 4. prefix vs. prefix_except_default: which variant fits when
- 5. Lazy-loaded translation files for better load times
- 6. SEO implications: letting hreflang tags be generated automatically
- 7. Using pluralized translations correctly
- 8. Interpolated translations with dynamic values
- 9. Keeping fallback locales and missing translations under control
- 10. Summary
- 11. FAQ
1. Why @nuxtjs/i18n is the standard choice for multilingual Nuxt apps
A multilingual application needs more than a translation table of key-value pairs. It needs a consistent URL structure per language so search engines and users can clearly tell which language version of a page they are looking at, a strategy for switching languages without losing the current context, and a way to serve metadata such as title and description per language as well. @nuxtjs/i18n handles exactly these responsibilities in a single solution tightly integrated with the Nuxt router, instead of spreading them across several separate libraries.
The advantage over a hand-rolled solution shows up mostly in the details that are easy to overlook: automatically generated canonical and alternate links, clean handling of dynamic routes that share a name across different languages, and a composable API that fits naturally into Vue components and Nuxt server routes. Building all of that yourself is possible, but it takes considerably more time than configuring an established, well-maintained module.
2. Installing the module and setting up locales
After installing it through the Nuxt module mechanism, @nuxtjs/i18n is configured in nuxt.config.ts with a list of locales, each specifying a language code, a display name, and optionally a dedicated filename for its translation file. The defaultLocale option additionally defines which language acts as the starting point when no other signal is available, for example on a first visit with no detectable browser language preference.
This base configuration is the foundation for every other feature the module offers, from the routing strategy to automatic language detection. A common beginner mistake is extending the locale list later without creating the matching translation files, which then leads to missing text at runtime that ideally gets caught by a configured fallback.
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxtjs/i18n'],
i18n: {
locales: [
{ code: 'de', iso: 'de-DE', name: 'Deutsch', file: 'de.json' },
{ code: 'en', iso: 'en-US', name: 'English', file: 'en.json' },
{ code: 'fr', iso: 'fr-FR', name: 'Francais', file: 'fr.json' },
],
defaultLocale: 'de',
strategy: 'prefix_except_default',
lazy: true,
langDir: 'locales/',
},
})
3. Comparing routing strategies: prefix, subdomain, and domain per locale
The most important decision when setting up @nuxtjs/i18n is the routing strategy, since it determines what the URL looks like for each language. With prefix-based routing, every language (or every language except the default) gets its own path segment, for example /en/products instead of /products, which is technically the simplest option because a single domain covers every language.
Subdomain routing instead spreads languages across dedicated subdomains such as en.example.com, while a fully separate domain per language, for example example.de next to example.com, offers the strongest separation but also the highest operational overhead, since every domain needs its own SSL certificate, its own DNS setup, and often its own hosting configuration. For most projects, prefix-based routing is the most pragmatic starting point, while domain-per-locale pays off mainly for very distinct markets with separate branding strategies.
4. prefix vs. prefix_except_default: which variant fits when
Within prefix-based routing, @nuxtjs/i18n offers several finer-grained options. The prefix strategy adds a prefix to every single language without exception, including the default one, so even the German homepage becomes reachable at /de/. That gives maximum consistency across all language versions, but can feel unnecessary for the main audience if most visitors only ever use the default language anyway.
prefix_except_default instead leaves the default language unprefixed at the root URL and only adds a segment for the remaining languages, which in practice is usually the more intuitive choice, because the existing URL structure of the default language stays unchanged. This strategy is therefore the most common starting point for projects that add multiple languages later without wanting to put the main language's existing SEO rankings at risk.
5. Lazy-loaded translation files for better load times
Without lazy loading, every translation file for every configured language would end up in the shipped JavaScript bundle, even if a given visitor only ever sees a single language. On projects with many languages and sizeable translation texts, this quickly adds up to a noticeable amount of unused code that unnecessarily extends the initial load time.
With the lazy: true option, @nuxtjs/i18n instead loads only the translation file for the currently active language, and dynamically fetches the new file at runtime whenever the language changes, without requiring a full page reload. This setting should be enabled in practically every project with more than two languages, since the configuration effort is minimal while the effect on load time can be quite noticeable.
6. SEO implications: letting hreflang tags be generated automatically
Search engines such as Google use hreflang tags to recognize that several URLs represent the same content in different languages, and to show searching users the language version that matches their preference. If these tags are missing or incorrectly linked, search engines may end up showing the wrong language version in results or mistakenly treat several language versions as duplicate content.
@nuxtjs/i18n automatically generates the necessary hreflang alternate links from the configured locale list and the current route, once the module's SEO option is enabled, and keeps them consistent across every language or route change. That relieves developers of the error-prone manual upkeep of these tags, but it does not remove the need to actually provide fully translated content per language, since hreflang should only ever point to translations that genuinely exist.
7. Using pluralized translations correctly
Many languages have plural rules far more complex than the simple singular-plural pattern of German or English, with several distinct forms for different number ranges. @nuxtjs/i18n builds on vue-i18n and inherits its pluralization system, where a translation key can contain several forms separated by pipe characters, from which the correct form is automatically chosen based on a supplied count.
For languages with more than two plural forms, such as Polish or Russian, a custom pluralization rule can be supplied as a function that overrides the default logic. Handling pluralization consistently through this mechanism from the start, instead of writing texts with hardcoded number words, saves considerable rework later when additional languages get added.
8. Interpolated translations with dynamic values
Beyond static text, most applications contain messages with dynamic values, such as a username or a product count, that need to be inserted into a translation string at runtime. vue-i18n supports named placeholders in curly braces inside the translation file, which are filled in at call time through an object carrying the matching values.
This interpolation works regardless of word order in a given language, which matters because different languages place dynamic values at different points in a sentence. A hardcoded string concatenation in application code would lose that flexibility and would need separate handling for every language, while the placeholder syntax leaves the entire sentence structure to the translation file.
9. Keeping fallback locales and missing translations under control
In practice, translations are rarely complete from day one, especially when new features are shipped faster than they can be translated. The fallbackLocale option determines which language steps in when a key is missing in the currently active language, so users see at least a readable fallback translation instead of an empty string or a raw key name.
For quality assurance during development, a missing-handler that logs every missing key to the console is also worth setting up, so gaps become visible before they reach production. A systematic comparison of translation files per language, for example through a small script that diffs every key of the default language against the remaining languages, prevents gaps from silently persisting over a long period.
| Strategy | URL example | Advantage | Drawback |
|---|---|---|---|
| prefix | /de/start, /en/start | Consistent across every language | Default language gets an unnecessary prefix |
| prefix_except_default | /start, /en/start | Default URL structure stays unchanged | Slightly inconsistent structure across languages |
| prefix_and_default | /start and /de/start, /en/start | Both URL forms reachable for the default language | Risk of duplicate content without a clean canonical |
| Subdomain (differentDomains) | de.example.com, en.example.com | Clear separation, own branding possible | More DNS and SSL management needed |
| Domain per locale | example.de, example.com | Strongest market and brand separation | Highest operational overhead, separate hosting |
Mironsoft
Vue architecture, Composition API, and Nuxt performance
Vue applications that don't get more complicated with every feature?
We review existing Vue and Nuxt projects for unstructured composables, unnecessary reactivity, and bloated bundles, then build an architecture that absorbs new features without making the codebase harder to follow.
Architecture Review
Checking composables, state management, and component structure for maintainability.
Performance Audit
Systematically optimizing reactivity overhead, bundle size, and Nuxt rendering strategy.
Nuxt Integration
Building robust, type-safe SSR/SSG setup and API integration.
10. Summary
Multilingual Nuxt apps with @nuxtjs/i18n: the essentials at a glance
Default strategy
prefix_except_default is usually the most pragmatic starting point for existing projects.
Load time
lazy: true loads only the active language file instead of bundling every language.
SEO
hreflang alternate links are generated automatically from the locale list.
Foundation
@nuxtjs/i18n builds on vue-i18n, including pluralization and interpolation.