Translate the components' words, write numbers and dates in any locale, and lay everything out right to left for Arabic.
The components say a few words of their own — “Search…”, “Rows per page”, “Page 2 of 7”, “Pick a date” — and write numbers and dates. Without any setup they do it in English, left to right. One provider changes all of it: the words, the locale numbers and dates are written in, and the reading direction.
LocaleProvider
Wrap your app in LocaleProvider with a BCP 47 locale and the words you translate. Words you leave out stay English, so you can translate a few at a time. dateLocale gives Calendar and the date pickers their month and day names and the first day of the week (a day-picker locale).
import { LocaleProvider } from "@/components/dashral/lib/locale"
import { id } from "@daypicker/react/locale"
const messages = {
search: "Cari…",
rowsPerPage: "Baris per halaman",
pageOf: "Halaman {page} dari {total}",
rows: { other: "{count} baris" },
}
export function Providers({ children }: { children: React.ReactNode }) {
return (
<LocaleProvider locale="id-ID" messages={messages} dateLocale={id}>
{children}
</LocaleProvider>
)
}Plurals and placeholders
A message is a string with {placeholders}, or an object of plural forms picked by count with the locale's own rules. Numbers in placeholders are written in the locale (1,175 · 1.175).
// Arabic has six plural forms; give the ones your language uses, and always "other".
rows: {
zero: "لا صفوف",
one: "صف واحد",
two: "صفان",
few: "{count} صفوف",
many: "{count} صفًا",
other: "{count} صف",
}With next-intl
Keep the kit's words with the rest of your app's, under one key such as kit, and pass them on. The dashboard template does exactly this, in English, Indonesian and Arabic: the language comes from a cookie (a menu in the header) or the browser, with no locale in the URL.
// app/layout.tsx — the language from next-intl (a cookie or the browser's), the kit's words from messages.kit
import { getLocale, getMessages } from "next-intl/server"
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const locale = await getLocale()
const messages = await getMessages()
const dir = locale === "ar" ? "rtl" : "ltr"
return (
<html lang={locale} dir={dir}>
<body>
<NextIntlClientProvider>
<LocaleProvider locale={locale} dir={dir} messages={messages.kit}>
{children}
</LocaleProvider>
</NextIntlClientProvider>
</body>
</html>
)
}Adding a language
The dashboard template comes in English, Indonesian and Arabic. A language is two things: its entry in lib/i18n.ts and its folder in messages/. To add Japanese:
- Copy
messages/entomessages/jaand translate the JSON files. Keep the keys and the{placeholders}; plurals use the language's own forms (Japanese has onlyother, Arabic six). Inmessages/ja/index.ts, import English's type and end the export withsatisfies typeof en, like the other languages. - Add its entry to
localeInfo: its name written in itself and its flag (for the language menu, fromcountry-flag-icons), the reading direction, the Intl tag numbers and dates use, and the calendars' locale from@daypicker/react/locale.moneyis optional: the Intl tag dollar amounts use when the language's own writes them oddly (Arabic's only has “US$”). - If the theme's fonts don't have its letters (Arabic, Thai, Hebrew…), load one with
next/fontinapp/layout.tsxand add its variable to the font stack, the way the Arabic font is. - Run the typecheck. A key English has and the new language lacks fails it, so nothing ships half translated. The language then shows in the header's menu and in Settings → Account, and the browser's preferred language picks it automatically.
For a right-to-left language, dir: "rtl" is all it takes: the layout, the menus, the charts and the arrows mirror on their own.
// lib/i18n.ts — one entry per language
import { ar, enUS, id, ja } from "@daypicker/react/locale"
import GB from "country-flag-icons/react/3x2/GB"
import JP from "country-flag-icons/react/3x2/JP"
// …
export const localeInfo = {
en: { label: "English", flag: GB, dir: "ltr", intl: "en-US", dateLocale: enUS },
id: { label: "Indonesia", flag: ID, dir: "ltr", intl: "id-ID", dateLocale: id },
ar: { label: "العربية", flag: SA, dir: "rtl", intl: "ar-u-nu-latn", money: "en-US", dateLocale: ar },
ja: { label: "日本語", flag: JP, dir: "ltr", intl: "ja-JP", dateLocale: ja },
}messages/
en/ the source: every key, and their types
index.ts
kit.json the UI kit's words
nav.json
settings.json
…
ja/ a copy of en/, translated
index.ts export default { kit, nav, … } satisfies typeof enServer components
Alert, Breadcrumb, Bar List, Card, Badge, Empty State, Steps, Timeline, Table, Button and Skeleton render on the server, where there's no provider. The few words they say are props, and Bar List takes a locale for its numbers.
// Server components can't read the provider: give them the words and the locale.
<Alert title="Tersimpan" onClose={hide} closeLabel="Tutup" />
<Breadcrumb items={trail} moreLabel="Tampilkan {count} halaman lagi" />
<BarList items={sources} locale="id-ID" />Right to left
Set dir="rtl" on <html> and on the provider. The components are written with logical sides — start and end instead of left and right — so the whole layout mirrors: sidebars, sheets, menus, sliders, switches, progress bars, the charts' growth, pagination and the arrow keys in Tree View, Tabs and Slider. Sheet's side="start" and "end" follow the direction too.
Write your own classes the same way:
<div className="ms-2 ps-4 text-start border-s" /> // follows the direction
<div className="ml-2 pl-4 text-left border-l" /> // always the left, even in Arabic
<ArrowRight className="rtl:-scale-x-100" /> // arrows point the reading wayMessage keys
Every word the components say, with its English default.
| Key | English |
|---|---|
| close | Close |
| cancel | Cancel |
| continue | Continue |
| navigation | Navigation |
| collapseSidebar | Collapse sidebar |
| expandSidebar | Expand sidebar |
| done | Done |
| dismiss | Dismiss |
| loading | Loading |
| remove | Remove {name} |
| selected | Selected |
| noResults | No results found. |
| noMatches | No matches |
| noData | No data |
| search | Search… |
| selectOption | Select an option |
| selectOptions | Select options |
| increase | Increase |
| decrease | Decrease |
| atMax | Max {value} |
| atMin | Min {value} |
| pressAndHold | (press and hold) |
| resize | Resize |
| copy | Copy |
| copied | Copied |
| copyFailed | Couldn't copy |
| showPassword | Show password |
| hidePassword | Hide password |
| pickDate | Pick a date |
| pickDateRange | Pick a date range |
| pickTime | Pick a time |
| typeTime | Type a time |
| shortcuts | Shortcuts |
| today | Today |
| yesterday | Yesterday |
| last7Days | Last 7 days |
| last30Days | Last 30 days |
| thisMonth | This month |
| lastMonth | Last month |
| addTag | Add a tag… |
| tags | Tags |
| tagLimit | {"one":"Up to {count} tag.","other":"Up to {count} tags."} |
| chosenFiles | Chosen files |
| dropFiles | Drop files here or {browse} |
| dropFile | Drop a file here or {browse} |
| browse | browse |
| fileType | {name}: this type of file isn't accepted. |
| fileSize | {name} is larger than {size}. |
| fileLimit | {"one":"{name}: up to {count} file.","other":"{name}: up to {count} files."} |
| upToSize | up to {size} |
| filesMax | {"one":"{count} file max","other":"{count} files max"} |
| images | Images |
| videos | Videos |
| audio | Audio |
| textFiles | Text files |
| pagination | Pagination |
| previous | Previous |
| next | Next |
| previousPage | Go to previous page |
| nextPage | Go to next page |
| firstPage | First page |
| lastPage | Last page |
| page | Page {page} |
| pageOf | Page {page} of {total} |
| morePages | More pages |
| carousel | Carousel |
| slideOf | {index} of {total} |
| previousSlide | Previous slide |
| nextSlide | Next slide |
| goToSlide | Go to slide {index} |
| pauseSlides | Pause |
| playSlides | Play |
| searchTable | Search... |
| noRows | No results. |
| tryAnother | Try another search or clear the filters. |
| clearFilters | Clear filters |
| clearFilter | Clear filter |
| reset | Reset |
| export | Export |
| exportAll | Export all matching rows |
| exportSelected | Export {count} selected |
| columns | Columns |
| toggleColumns | Toggle columns |
| rowsPerPage | Rows per page |
| rows | {"one":"{count} row","other":"{count} rows"} |
| rowsSelected | {selected} of {total} selected |
| selectAllRows | Select all rows on this page |
| selectRow | Select row |
| commandTitle | Command palette |
| commandDescription | Search for a command to run |
| commandPlaceholder | Type a command or search… |
| less | Less |
| more | More |
| trend | Trend from {from} to {to} |
| heatmapSummary | {count} over {weeks} weeks to {date} |
| heatmapBusiest | ; the busiest day was {date} with {value}. |
| donutChart | Donut chart |
| treemap | Treemap |
| treemapLevels | Treemap levels |
| all | All |
| zoomIn | {name}. Zoom in |
| candlestickChart | Candlestick chart |
| areaChart | Area chart |
| lineChart | Line chart |
| barChart | Bar chart |
| candle | {date}: open {open}, high {high}, low {low}, close {close} |
| candleOpen | O |
| candleHigh | H |
| candleLow | L |
| candleClose | C |
| volume | Vol |
| message | Message |
| askAnything | Ask anything… |
| sendMessage | Send message |
| stopGenerating | Stop generating |
| attachFiles | Attach files |
| attachments | Attachments |
| dropToAttach | Drop files to attach them |
| charactersLeft | {"one":"{count} character left","other":"{count} characters left"} |
| charactersOver | {"one":"{count} character over the limit","other":"{count} characters over the limit"} |
| you | You |
| assistant | Assistant |
| regenerate | Regenerate |
| goodResponse | Good response |
| badResponse | Bad response |
| messageFailed | Couldn't finish this answer. |
| tryAgain | Try again |
| conversation | Conversation |
| jumpToLatest | Jump to latest |
| thinking | Thinking… |
| thoughtFor | {"one":"Thought for {count} second","other":"Thought for {count} seconds"} |
| thoughtBriefly | Thought for a moment |
| toolActivity | Tool activity |
| toolPending | Waiting |
| toolRunning | Running |
| toolDone | Done |
| toolFailed | Failed |
| toolInput | Input |
| toolOutput | Output |
| toolStatus | {name}: {status} |
| needsApproval | Needs your approval |
| approve | Approve |
| allowOnce | Allow once |
| allowAlways | Always allow |
| deny | Deny |
| approved | Approved |
| denied | Denied |
| alwaysAllowed | Always allowed |
| suggestions | Suggestions |