Skip to content

Localization

Switch the few words prfct components say on their own — close buttons, spinners, pagination, calendars — to any language with one provider.

prfct components carry almost no text: everything people read is your content, and it stays in your hands. A handful of components do say something on their own — mostly to screen readers: the name of a dialog's close button, a spinner's status, the landmarks of pagination and breadcrumbs. LocaleProvider switches all of them at once.

wrzesień 2026

Installation

$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/locale.json

Components that speak on their own already depend on it, so the CLI adds it with the first of them.

Usage

Wrap your app once, as high as possible — usually in the root layout:

app/layout.tsx
import { LocaleProvider } from "@/components/ui/locale"

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="pl">
      <body>
        <LocaleProvider locale="pl">{children}</LocaleProvider>
      </body>
    </html>
  )
}

Without a provider, components speak English. locale is a BCP 47 tag: pl, pl-PL and pl-pl all pick the Polish messages, and the full tag formats numbers — chart tooltips print 12 345,5 in Polish and 12,345.5 in English.

Set the language twice
LocaleProvider decides what components say; <html lang> decides how screen readers pronounce it. Set both, from the same value.

Built-in languages

prfct ships English and Polish. Each message is written for its purpose — an accessible name, a landmark, a role description — not translated word by word.

MessageEnglishPolski
closeCloseZamknij
loadingLoadingŁadowanie
breadcrumbBreadcrumbŚcieżka nawigacji
moreMoreWięcej
paginationPaginationPaginacja
previousPreviousPoprzednia
nextNextNastępna
previousPageGo to previous pagePrzejdź do poprzedniej strony
nextPageGo to next pagePrzejdź do następnej strony
morePagesMore pagesWięcej stron
carouselcarouselkaruzela
slideslideslajd
previousSlidePrevious slidePoprzedni slajd
nextSlideNext slideNastępny slajd
sidebarSidebarPasek boczny
sidebarDescriptionDisplays the mobile sidebar.Wyświetla pasek boczny na urządzeniu mobilnym.
toggleSidebarToggle sidebarPokaż lub ukryj pasek boczny
commandPaletteCommand palettePaleta poleceń
commandDescriptionSearch for a command to run.Wyszukaj polecenie do wykonania.
navigateHintto navigatenawigacja
selectHintto selectwybór
closeHintto closezamknięcie
clearClearWyczyść
toggleOptionsToggle optionsPokaż lub ukryj opcje
removeRemoveUsuń
removeItemRemove {item}Usuń {item}
notificationsNotificationsPowiadomienia
closeNotificationClose notificationZamknij powiadomienie

Role descriptions (carousel, slide) are lowercase because screen readers announce them mid-sentence: “Featured work, carousel”.

Other languages

Pass messages for any other language. Keys you leave out fall back to English, so you can translate incrementally — or change a single phrase in a built-in language:

<LocaleProvider
  locale="de"
  messages={{
    close: "Schließen",
    loading: "Wird geladen",
    previous: "Zurück",
    next: "Weiter",
    previousPage: "Zur vorherigen Seite",
    nextPage: "Zur nächsten Seite",
  }}
>
  {children}
</LocaleProvider>

Messages are plain strings. removeItem receives the chip's text as {item}: "Entferne {item}".

Calendar

Month names, weekdays, the first day of the week and the calendar's own labels come from its date locale. Import it from react-day-picker/locale and pass it as calendar:

"use client"

import { pl } from "react-day-picker/locale"

import { LocaleProvider } from "@/components/ui/locale"

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <LocaleProvider locale="pl" calendar={pl}>
      {children}
    </LocaleProvider>
  )
}

Date locales contain functions, which can't cross from a Server Component to a Client Component — that's why this provider lives in a small Client Component of its own. A locale passed directly to a Calendar still wins.

Load date locales where calendars are
A date locale adds a few kilobytes of JavaScript. If only a few pages show a calendar, wrap those pages — not the whole app — in the provider that sets calendar.

Your content

prfct doesn't ship a translation framework for your own text: use next-intl, Lingui or plain typed dictionaries, whatever your app already uses. Keep the component language and the content language in one place, so they never disagree.

This site is available in English and Polish, built with no library at all:

  • Interface text lives in typed dictionaries. Polish plurals use Intl.PluralRules, which knows that 2 is komponenty and 5 is komponentów.
  • Every page lives under a [locale] segment. English keeps its URLs without a prefix; Polish lives under /pl.
  • A documentation page is an MDX file per language. Translations keep the English heading structure, so #accessibility works in both.
  • Examples are written once. A catalog of translated strings produces the Polish copy at build time — the code stays identical.

Accessibility

  • Set <html lang> to the page's language, and mark passages in another language with their own lang, so screen readers switch pronunciation.
  • Keep accessible names short and specific: “Close” names a button; “Click here to close this dialog” doesn't.
  • Test with a screen reader in each language you ship — a name that reads well in English may be ambiguous once translated.

API reference

LocaleProvider

PropTypeDefault
locale

BCP 47 language tag. Picks the built-in messages and formats numbers.

string"en"
messages

Overrides built-in messages. Missing keys fall back to the locale's built-in messages, then to English.

Partial<LocaleMessages>No default
calendar

Date locale for Calendar, from react-day-picker/locale.

LocaleNo default

useLocale

Returns { locale, messages, calendar } for components of your own that speak on their own. Outside a provider it returns English.

const { messages } = useLocale()

return <span className="sr-only">{messages.close}</span>