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.
Writing direction
The active locale supplies the document direction as well as its language. Navigation, sheets, arrows and page layout mirror when a consuming site adds an RTL locale because duxt components use logical start and end. Technical material does not mirror blindly: code, commands, paths, API names and other LTR syntax keep the direction in which they are readable.
duxt does not currently ship an RTL locale. Direction-aware means a site can add one without forking the theme; it does not mean one of the seven built-in locales is RTL.
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
- An added RTL locale mirrors the interface while technical syntax remains LTR
-
duxt.sourceOptions.defaultLocalematchesi18n.defaultLocale