Strings and locales
How Nizivo's built-in strings work. Every one is in a labels prop with an English default; @nizivo/react/locales/en-US lists them all; NizivoConfig passes an app's own strings to the client parts, and a file that wraps each server part once passes them to the rest; formatLabel fills placeholders and picks CLDR plural forms.
Nizivo writes a few words of its own: accessible names ("Pagination", "Close"), status text ("Loading Revenue") and short visible text ("+4", "Show password"). It never reads the browser's language to choose them, so the server and the client always render the same words.
Labels
Each component that writes a string takes a labels prop: a plain object of strings, each one
optional, with an English default. labels is serialisable, so a Server Component can pass it to
any component, client components included. A placeholder in braces is filled by the component:
{page} with the page number, {count} with the count, {label} with the component's own label.
<Pagination page={2} totalPages={9} labels={{ page: 'Seite {page}', next: 'Nächste Seite' }} />Numbers in a string are formatted for the component's locale ('en-US' by default), so pass the
same locale you write the strings in.
The catalogue
@nizivo/react/locales/en-US exports enUS: every built-in English string, keyed by component
(enUS.pagination, enUS.avatarGroup, enUS.fileUpload, …), each typed by that component's
Labels type. The module is the strings and nothing else, and the root entry does not re-export it.
Read it to see every string you need to translate, or copy its shape for your own locale: a
catalogue typed typeof enUS must give every string. A test keeps it equal to the components'
defaults. See the entry page for its type.
Strings that React Aria renders from its own translations are not in labels and not in the
catalogue: the visually hidden "Dismiss" buttons of the overlays, Select's "Select an item",
NumberField's "Number field" role description and step buttons' names ("Increase Price"), and
Combobox's Trigger name ("Show suggestions"), list name ("Suggestions", for a field without a
label) and its announcements of the number of options. They
follow the locale of the nearest NizivoConfig, and the component's Labels type names them as
engine-owned (for example DialogLabels.dismiss). NumberField's labels prop can still replace its
step buttons' names for one field.
Your own locale
Write your locale's strings once, in a catalogue of the enUS shape, and pass them in two ways: the
client parts read them from NizivoConfig, and the server parts get them from a file that wraps
each one once. A Server Component cannot read a React context, so no provider can reach the server
parts; Nizivo has no module-level setting either, which would give every request on a server the
same language.
// app/ui/de-DE.ts
import type { NizivoLabels } from '@nizivo/react/config';
export const deDE = {
button: { pending: 'Wird geladen…' },
dialog: { close: 'Schließen' },
pagination: {
nav: 'Seitennavigation',
previous: 'Vorherige Seite',
next: 'Nächste Seite',
page: 'Seite {page}',
},
searchField: { label: 'Suchen', clear: 'Suche leeren' },
toaster: { region: 'Benachrichtigungen (F8)', dismiss: 'Schließen' },
} satisfies NizivoLabels;Once it holds every key, type it typeof enUS instead (import type { enUS } from '@nizivo/react/locales/en-US'), so a missing string is a type error. An app with an i18n library
(next-intl, react-i18next, Lingui) builds the same object from its messages.
Client parts: NizivoConfig
NizivoConfig takes labels, the same shape (NizivoLabels from @nizivo/react/config: every key
and every string optional, so a whole catalogue fits). Put it around the app once, with the Toaster
inside it:
// app/layout.tsx
import type { ReactNode } from 'react';
import { NizivoConfig } from '@nizivo/react/config';
import { Toaster } from '@nizivo/react/toast';
import { deDE } from './ui/de-DE';
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="de-DE">
<body>
<NizivoConfig locale="de-DE" labels={deDE}>
{children}
<Toaster />
</NizivoConfig>
</body>
</html>
);
}NizivoConfig is a client component, and the catalogue is plain strings, so a Server Component
layout passes it as it is; the pages under it stay Server Components. Each string resolves in this
order: the part's own labels prop, key by key; then the nearest NizivoConfig's labels; then
the English default. A part outside every NizivoConfig shows English. Keep the object stable (a
module constant, as here): a new object on every render re-renders every part that reads it.
The provider reaches these parts:
| Part | Catalogue key | Strings |
|---|---|---|
| AlertDismiss | alertDismiss | dismiss |
| BannerDismiss | bannerDismiss | dismiss |
| ChipGroup | chipGroup | unavailable |
| Dialog.Close | dialog | close |
| Drawer.Close | drawer | close |
| FileUpload | fileUpload | all 14 (prompt, browse, summary, …) |
| Form | form | summaryTitle |
| MobileTabBarMore | mobileTabBarMore | more, and close for its sheet's Close button |
| PasswordInput | passwordInput | toggle |
| Popover.Close | popover | close |
| QuantityStepper | quantityStepper | decrement, increment |
| SearchField | searchField | label, clear |
| SubmitButton | button | pending |
| Toaster | toaster | region, dismiss |
MobileTabBarMore names its sheet's Close button with its own close, so mobileTabBarMore.close
applies there, not drawer.close.
It does not reach the server parts, whose keys it ignores: AppShell (appShell), AvatarGroup
(avatarGroup), Breadcrumb (breadcrumb), Button (button), KpiTile (kpiTile), Metric
(metric), MobileTabBar's landmark (mobileTabBar), PageIndicator (pageIndicator), Pagination (pagination), RankedList
(rankedList), Sidebar (sidebar), BulkActionBar (bulkActionBar), TableSelectAll (tableSelection) and TrendCard
(trendCard). SubmitButton, Button's client twin, reads button.pending; a plain Button does
not.
Server parts: wrap once
For the server parts, one app file wraps each one with the catalogue's strings, and the rest of
the app imports them from there. The file has no 'use client', so the wrappers stay Server
Components and ship no JavaScript; a labels prop passed to a wrapper still wins key by key.
// app/ui/nizivo.tsx
import { Button as NizivoButton, type ButtonProps } from '@nizivo/react/button';
import { Pagination as NizivoPagination, type PaginationProps } from '@nizivo/react/pagination';
import { deDE } from './de-DE';
export function Button(props: ButtonProps) {
return <NizivoButton {...props} labels={{ ...deDE.button, ...props.labels }} />;
}
export function Pagination(props: PaginationProps) {
return (
<NizivoPagination locale="de-DE" {...props} labels={{ ...deDE.pagination, ...props.labels }} />
);
}// a page: import { Button, Pagination } from '../ui/nizivo', not from @nizivo/react
<Pagination page={2} totalPages={9} getHref={(page) => `?page=${page}`} />Wrap AvatarGroup the same way when its count strings need plural objects in your locale (see the next section).
Plurals and formatLabel
A string whose default has a {count} placeholder can also be a plural object: one template per
CLDR plural category of its locale, other always included. The count picks the category through
Intl.PluralRules, so English needs one and other, Polish one, few, many and other,
and Arabic all six. Today these are AvatarGroup's labels.more and labels.overflow. List exactly
the categories Intl.PluralRules has for your locale, which its resolvedOptions() returns as
pluralCategories: Nizivo's own catalogues are tested that way.
formatLabel from @nizivo/react/i18n is the function the components use, for your own strings
too (about 0.2 KB, server-safe):
import { formatLabel, type PluralLabel } from '@nizivo/react/i18n';
const files: PluralLabel = {
one: '{count} plik',
few: '{count} pliki',
many: '{count} plików',
other: '{count} pliku',
};
formatLabel(files, { count: 3 }, 'pl'); // '3 pliki'
formatLabel(files, { count: 1250 }, 'pl'); // '1250 plików'
formatLabel('Remove {name}', { name: 'q3.pdf' }); // 'Remove q3.pdf'values.count chooses the plural form; each {name} is replaced by values.name, a number
formatted with Intl.NumberFormat(locale) and a string as written; a placeholder with no value
stays as written. There are no exact-number keys ('=0'): write the zero case in your own code.
Strings are never joined from pieces, so a translation can put the count anywhere.
Static or interactive: choosing a component
Which Nizivo components ship client JavaScript, what makes them ship it, and when to use a static variant (IconButtonStatic, FieldStatic) instead of the normal component. A checklist, a table of all 78 components, the measured costs and worked examples.
The registry
Copy a pattern, or one component's stylesheet, into your app with the shadcn CLI. Each item has an immutable URL per release, depends only on the @nizivo packages it imports, and is yours to edit under MIT; it gets no updates.