Skip to Content
Documentation

i18n

Localized URLs

Give each language of a VitNode app its own URLs, with a /pl prefix by default and translated paths such as /pl/odkrywaj through i18n.routePaths.

Every language you add gets its own URLs. With English as the default, Polish pages live under /pl/... without any extra setup. You can go one step further and translate the paths themselves, so /discover becomes /pl/odkrywaj for Polish readers. It's the same route, the same component and the same <Link to="/discover">, with a second spelling.

Before you begin

  • A second language in i18n.locales. Languages adds one with vitnode i18n create.

Check the prefix

Restart dev after adding the language. Every public page now answers in Polish under a /pl prefix: /discover is English, /pl/discover is Polish. The language switcher in the header (LanguageSwitcher from @vitnode/core/tanstack/layout, rendered once the app has more than one language) moves between them.

Find the English path

Every plugin documents the paths its pages answer to. Core's are /discover, /search, /users/:nameCode, /login, /settings and friends. Those English paths are the keys you translate. Your app's own routes work the same way.

Translate it in vitnode.config.ts

Add routePaths, keyed by locale:

apps/web/src/vitnode.config.ts
export const vitNodeConfig = buildConfig({
  i18n: {
    defaultLocale: 'en',
    locales: [
      { code: 'en', name: 'English' },
      { code: 'pl', name: 'Polski' },
    ],
    routePaths: {
      pl: {
        '/blog/article/:slug': '/blog/artykul/:slug',
        '/discover': '/odkrywaj',
        '/search': '/szukaj',
        '/users/:nameCode': '/uzytkownicy/:nameCode',
      },
    },
    timeZone: 'UTC',
  },
})

This is the configuration this site runs on.

Restart dev

Every link, redirect, canonical URL and sitemap line now uses the new spelling. You don't touch any route file.

PageEnglishPolish beforePolish after
A post/blog/article/hello/pl/blog/article/hello/pl/blog/artykul/hello
Discover/discover/pl/discover/pl/odkrywaj
Search/search/pl/search/pl/szukaj
A profile/users/jane-1/pl/users/jane-1/pl/uzytkownicy/jane-1
Login/login/pl/login/pl/login (untranslated)

The old spellings keep working. /pl/discover now redirects to /pl/odkrywaj, so links people already shared don't break.

One route, many spellings

A translation never creates a route. The router's URL rewrite does the work in both directions:

  • Coming in, /pl/odkrywaj is read as the Polish version of /discover, so the route tree matches /discover.
  • Going out, <Link to="/discover"> renders /pl/odkrywaj for a Polish reader.

Route files, route IDs, lazy-loaded chunks and every to="..." in your code stay exactly as they were. See Navigation for how the rewrite fits around Link.

Translation rules

  • Keys are English source paths in VitNode syntax: :param for a parameter and a trailing * for a catch-all. They must be real routes in your app, or the build tells you which one you probably meant.
  • Only static segments get translated. A parameter keeps its value: jane-1 stays jane-1 in /pl/uzytkownicy/jane-1.
  • Parameters must match exactly. You can't drop one, add one or rename one (:nameCode stays :nameCode). A * catch-all has to stay the last segment.
  • Lowercase only. Non-ASCII letters are fine and get percent-encoded: '/files': '/załączniki' goes out as /pl/za%C5%82%C4%85czniki, and the browser shows załączniki.
  • Don't start with a locale code. /pl/... or /en/... as the first segment would be read as a language prefix.
  • / can't be translated. The home page is spelled by localePrefix.
  • /admin and /api are never localized. They carry no prefix and no translation.
  • A missing translation falls back to English. /login without an entry is /pl/login, so a half-translated app still works.

A translated layout and a translated parameter route have two extra rules; Advanced localized URLs covers both.

localePrefix modes

ModeEnglishPolishNotes
as-needed (default)/discover/pl/odkrywajThe default locale has no prefix. /en/... redirects away.
always/en/discover/pl/odkrywajEvery locale is prefixed. / redirects to /en.
never/discover/odkrywajNo prefix. Needs one locale per host, see below.

never needs a domain per language; see A domain per language.

Switching languages

The language switcher keeps you on the same page, with the same parameters, query and hash. /pl/uzytkownicy/jane-1 switches to /users/jane-1.

Pages whose record has a different slug per language (see Translated segments vs localized slugs) declare their alternates, and the switcher follows them to the other language's own slug. If the language you pick isn't among the declared alternates, because that translation isn't published, you land on that language's home page instead of a 404. Switching to a language on another domain is a full page load.