Several languages
Serve the interface in several languages, and translate the pages themselves.
The theme's own strings are translated in seven locales. A site serves the subset it wants, translates the strings it configured itself, and — if it wants to — the pages too.
Steps
- Narrow the list with
duxt.locales.locales: ['en-GB', 'de-DE']
Not i18n's ownlocalesarray: that one merges across layers instead of replacing, so declaring two there leaves the other five routed and announced inhreflang. - Pick the unprefixed one in
nuxt.config.tswithi18n.defaultLocale. It is URL-relevant: changing it later moves every page in both directions at once. - Translate your own strings. For a handful, the record form costs less than
a locale file per language:
sections: [ { label: { 'en-GB': 'Guides', 'de-DE': 'Anleitungen' }, to: '/guides', icon: 'lucide:book-open' } ]
For more than a handful, register your own i18n keys and write the key. Do not reach forduxt.defaults.*— those are the layer's internal keys, and a rename there would break your site silently. - Set the origin.
hreflangis not valid relative, so it needsi18n.baseUrlorNUXT_PUBLIC_I18N_BASE_URL. - Translate the pages, if you want to. A folder per language inside the
source; the default locale stays where it is and moves no URL:
sources: [{ path: 'docs', locales: ['en-GB', 'de-DE'] }]
- index.md
- 1.getting-started
- index.md
- de-DE
- index.md
Translate as few pages as you like — everything else falls back, with a banner telling the reader why.
- Open a page under each locale and check that the interface changed language, the path still resolves, and no configured label prints as a raw key.
Only some versions, or another repository
locales on a ref overrides the source's, which is the usual shape: the current
version is translated and the two behind it are not.
{
path: 'docs',
locales: ['en-GB', 'de-DE'],
refs: [
{ tag: 'v2.0.0' }, // both languages
{ tag: 'v1.0.0', locales: ['en-GB'] } // original only
]
}
The object form moves a language somewhere else entirely — its own repository, with its own maintainers and its own pace:
locales: ['en-GB', { locale: 'de-DE', repo: 'acme/docs-de', path: 'docs' }]
That is what React and Vue do outside their tooling, with de.react.dev and the
vuejs-translations org, because translators work to their own schedule and
review. Here it is a source entry rather than a second website — one search
index, one language switcher.
Roughly two seconds of build and 0.6 MB of database per language × version × repository, measured linear to at least 200 collections. Translating every version of every repository multiplies all three; translating only the current version is what most projects actually do.
When a page has no translation
The reader gets the nearest language that has it, with a banner, and the page
carries noindex plus a canonical pointing at the language it is really in. The
chain — locale, base language, sibling region, fallbackLocale, original — is in
Localisation.
Checklist
-
duxt.localeslists exactly the locales you serve -
/sitemap.xmlcontains one entry per page per locale, and no others -
hreflanglinks are absolute - No configured label prints as a raw key
- Switching locale keeps the reader on the same page
- A page you did not translate shows the fallback banner, not a 404
-
duxt.sourceOptions.defaultLocalematchesi18n.defaultLocale