Dashral Pro is on the wayGet 30% off at launch

Dashral

Languages and RTL

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).

providers.tsx
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).

example.tsx
// 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
// 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:

  1. Copy messages/en to messages/ja and translate the JSON files. Keep the keys and the {placeholders}; plurals use the language's own forms (Japanese has only other, Arabic six). In messages/ja/index.ts, import English's type and end the export with satisfies typeof en, like the other languages.
  2. Add its entry to localeInfo: its name written in itself and its flag (for the language menu, from country-flag-icons), the reading direction, the Intl tag numbers and dates use, and the calendars' locale from @daypicker/react/locale. money is optional: the Intl tag dollar amounts use when the language's own writes them oddly (Arabic's only has “US$”).
  3. If the theme's fonts don't have its letters (Arabic, Thai, Hebrew…), load one with next/font in app/layout.tsx and add its variable to the font stack, the way the Arabic font is.
  4. 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
// 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/
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 en

Server 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.

example.tsx
// 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:

example.tsx
<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 way

Message keys

Every word the components say, with its English default.

KeyEnglish
closeClose
cancelCancel
continueContinue
navigationNavigation
collapseSidebarCollapse sidebar
expandSidebarExpand sidebar
doneDone
dismissDismiss
loadingLoading
removeRemove {name}
selectedSelected
noResultsNo results found.
noMatchesNo matches
noDataNo data
searchSearch…
selectOptionSelect an option
selectOptionsSelect options
increaseIncrease
decreaseDecrease
atMaxMax {value}
atMinMin {value}
pressAndHold(press and hold)
resizeResize
copyCopy
copiedCopied
copyFailedCouldn't copy
showPasswordShow password
hidePasswordHide password
pickDatePick a date
pickDateRangePick a date range
pickTimePick a time
typeTimeType a time
shortcutsShortcuts
todayToday
yesterdayYesterday
last7DaysLast 7 days
last30DaysLast 30 days
thisMonthThis month
lastMonthLast month
addTagAdd a tag…
tagsTags
tagLimit{"one":"Up to {count} tag.","other":"Up to {count} tags."}
chosenFilesChosen files
dropFilesDrop files here or {browse}
dropFileDrop a file here or {browse}
browsebrowse
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."}
upToSizeup to {size}
filesMax{"one":"{count} file max","other":"{count} files max"}
imagesImages
videosVideos
audioAudio
textFilesText files
paginationPagination
previousPrevious
nextNext
previousPageGo to previous page
nextPageGo to next page
firstPageFirst page
lastPageLast page
pagePage {page}
pageOfPage {page} of {total}
morePagesMore pages
carouselCarousel
slideOf{index} of {total}
previousSlidePrevious slide
nextSlideNext slide
goToSlideGo to slide {index}
pauseSlidesPause
playSlidesPlay
searchTableSearch...
noRowsNo results.
tryAnotherTry another search or clear the filters.
clearFiltersClear filters
clearFilterClear filter
resetReset
exportExport
exportAllExport all matching rows
exportSelectedExport {count} selected
columnsColumns
toggleColumnsToggle columns
rowsPerPageRows per page
rows{"one":"{count} row","other":"{count} rows"}
rowsSelected{selected} of {total} selected
selectAllRowsSelect all rows on this page
selectRowSelect row
commandTitleCommand palette
commandDescriptionSearch for a command to run
commandPlaceholderType a command or search…
lessLess
moreMore
trendTrend from {from} to {to}
heatmapSummary{count} over {weeks} weeks to {date}
heatmapBusiest; the busiest day was {date} with {value}.
donutChartDonut chart
treemapTreemap
treemapLevelsTreemap levels
allAll
zoomIn{name}. Zoom in
candlestickChartCandlestick chart
areaChartArea chart
lineChartLine chart
barChartBar chart
candle{date}: open {open}, high {high}, low {low}, close {close}
candleOpenO
candleHighH
candleLowL
candleCloseC
volumeVol
messageMessage
askAnythingAsk anything…
sendMessageSend message
stopGeneratingStop generating
attachFilesAttach files
attachmentsAttachments
dropToAttachDrop 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"}
youYou
assistantAssistant
regenerateRegenerate
goodResponseGood response
badResponseBad response
messageFailedCouldn't finish this answer.
tryAgainTry again
conversationConversation
jumpToLatestJump to latest
thinkingThinking…
thoughtFor{"one":"Thought for {count} second","other":"Thought for {count} seconds"}
thoughtBrieflyThought for a moment
toolActivityTool activity
toolPendingWaiting
toolRunningRunning
toolDoneDone
toolFailedFailed
toolInputInput
toolOutputOutput
toolStatus{name}: {status}
needsApprovalNeeds your approval
approveApprove
allowOnceAllow once
allowAlwaysAlways allow
denyDeny
approvedApproved
deniedDenied
alwaysAllowedAlways allowed
suggestionsSuggestions