Skip to content
duxt

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

  1. Narrow the list with duxt.locales.
    locales: ['en-GB', 'de-DE']
    

    Not i18n's own locales array: that one merges across layers instead of replacing, so declaring two there leaves the other five routed and announced in hreflang.
  2. Pick the unprefixed one in nuxt.config.ts with i18n.defaultLocale. It is URL-relevant: changing it later moves every page in both directions at once.
  3. 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 for duxt.defaults.* — those are the layer's internal keys, and a rename there would break your site silently.
  4. Set the origin. hreflang is not valid relative, so it needs i18n.baseUrl or NUXT_PUBLIC_I18N_BASE_URL.
  5. 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'] }]
    
docs/
  • 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.

  1. 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.

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.locales lists exactly the locales you serve
  • /sitemap.xml contains one entry per page per locale, and no others
  • hreflang links 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.defaultLocale matches i18n.defaultLocale
Was this page helpful?