Internationalization (I18n)

Translating Pages

Render localized strings with use-intl, declare route namespaces, and load translation bundles.

VitNode uses use-intl for frontend translations. To keep initial payloads small, pages only download the translation namespaces they explicitly declare.

Quick start

Plugin routes declare their namespaces as messages in routes.ts. VitNode loads the strings in parallel with the route chunk automatically:

plugins/blog/src/routes.ts
import { definePluginRoutes, lazy, page } from '@vitnode/core/routing'

export const routes = definePluginRoutes([
  page('/blog/about', {
    component: lazy(() => import('./pages/about-page')),
    messages: ['@vitnode/blog.about'], 
  }),
])

Read strings directly with useTranslations:

plugins/blog/src/pages/about-page.tsx
import { useTranslations } from 'use-intl'

const AboutPage = () => {
  const t = useTranslations('@vitnode/blog.about')

  return <h1>{t('title')}</h1>
}

export default AboutPage

And define the messages in plugins/blog/src/locales/en.json:

plugins/blog/src/locales/en.json
{
  "@vitnode/blog": {
    "about": {
      "title": "About Our Blog"
    }
  }
}

Keep feature copy in the plugin

Host messages are for the site shell. A product page should declare its plugin namespace as the route's messages and keep its locale JSON beside the route.

Placeholders and Pluralization

VitNode supports ICU message syntax out of the box:

plugins/blog/src/locales/en.json
{
  "cart": {
    "greeting": "Hello, {name}!",
    "items": "{count, plural, =0 {No items} one {1 item} other {# items}}"
  }
}

In your React component:

const t = useTranslations('cart')

return (
  <div>
    <p>{t('greeting', { name: 'Alex' })}</p>
    <p>{t('items', { count: 3 })}</p>
  </div>
)

Global namespaces

core.global is provided by the root shell to every route, supplying shared strings for dialogs, toasts, and buttons.

Learn More