Locale

Free · MIT

"rtl" for Arabic, Hebrew, Persian, Urdu and the like; "ltr" otherwise.

Markdown

Preview

Install

With the Candor CLI

First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/locale

Usage

TSX · The example above
"use client"

import * as React from "react"
import { LocaleProvider } from "@/components/locale"
import { RelativeTime } from "@/components/relative-time"
import { Composer } from "@/components/composer"
import { Message } from "@/components/message"
import { Thread } from "@/components/thread"
import type { Message as MessageData } from "@/lib/chat"

const agent = { id: "a1", name: "وكيل الدعم", role: "assistant" }
const at = new Date(Date.now() - 4 * 60_000).toISOString()

const messages: MessageData[] = [
  { id: "u1", author: { role: "user" }, createdAt: at, status: "complete", blocks: [{ kind: "plain", text: "أين طلبي رقم 1042؟" }] },
  {
    id: "a1",
    author: { role: "agent", agent },
    createdAt: at,
    status: "complete",
    blocks: [{ kind: "paragraph", nodes: [{ kind: "text", text: "شُحن طلبك أمس، ويصل خلال يومين." }] }],
  },
]

/** Arabic, right to left: the provider sets the words, the locale and the direction; `dir` on the wrapper flips the layout. */
const arabic = {
  thread: { messages: "الرسائل", jumpToLatest: "الانتقال إلى الأحدث" },
  message: { you: "أنت" },
  composer: { label: "رسالة", placeholder: "اكتب سؤالك", send: "إرسال", hintSend: "إرسال", hintNewLine: "سطر جديد", hintMention: "إشارة" },
  relativeTime: { justNow: "الآن" },
}

export default function Example() {
  const [value, setValue] = React.useState("")
  return (
    <div className="flex flex-col gap-4">
      <LocaleProvider locale="ar" strings={arabic}>
        <div dir="rtl" lang="ar" className="h-80 overflow-hidden rounded-lg border">
          <Thread className="h-full" footer={<Composer value={value} onChange={setValue} onSubmit={() => setValue("")} />}>
            {messages.map((m) => (
              <Message key={m.id} message={m} />
            ))}
          </Thread>
        </div>
      </LocaleProvider>
      <LocaleProvider locale="fr-FR" timeZone="Europe/Paris">
        <p lang="fr" className="text-sm text-muted-foreground">
          Envoyé <RelativeTime date={at} /> · <RelativeTime date={at} format="datetime" />
        </p>
      </LocaleProvider>
    </div>
  )
}

<LocaleProvider>

PropTypeDefaultDescription
localestring | undefined—A BCP 47 tag such as "en-GB", "fr" or "ar-EG", for dates, numbers and plural forms. Left out, the browser's own is used once the page has hydrated.
dirTextDirection | undefined—Text direction. Left out, it follows the locale: "rtl" for Arabic, Hebrew, Persian, Urdu and the like. Set `dir` on `<html>` too, so the page's own layout flips with Candor's.
timeZonestring | undefined—An IANA time zone such as "Europe/Paris". With `locale`, server and browser format dates the same way, so absolute times render on the server too.
stringsStringOverrides | undefined—Overrides by component name, e.g. `{ thread: { jumpToLatest: "…" } }`; any left out stay English. Nested providers merge.
childrenrequiredReact.ReactNode—What goes inside.

Functions

directionOf(locale: string | undefined)

"rtl" for Arabic, Hebrew, Persian, Urdu and the like; "ltr" otherwise.

fill(template: string, vars: Record<string, string | number> | undefined, number: (n: number) => string = String)

Fill {name} placeholders. Numbers go through the locale's number format.

pick(message: Phrase, count: number | undefined, rules: Intl.PluralRules)

The plural form for count, falling back to other.

place(text: string, nodes: Record<string, React.ReactNode>)

A translated sentence with elements in it: place(t("signedInAs"), { email: <strong>{email}</strong> }). Each {name} becomes that node, so translators can move it within the sentence and it keeps its styling.

useLocale()

The locale, time zone and text direction in effect.

useFormat()

Locale-aware formatting that's safe to render on the server. With no locale set, numbers use "en-US" until the page hydrates and then the browser's locale; dates show their ISO form until then. Set locale (and timeZone for dates) on the provider to render the final text on the server.

useHydrated()

False while rendering on the server and during React's hydration pass, true after. Anything that depends on the browser (its locale, its time zone) waits for true, so server and browser render the same markup.

useStrings(name: string, defaults: T)

A component's strings: t("jumpToLatest"), t("remove", { name }), t("files", { count: 3 }). name is the component's registry name in camelCase, the key apps override it under; defaults is its English table.

useStartSide()

Physical side for the reading direction: "left" in LTR, "right" in RTL. For the few places CSS can't say start.

Types

Phrase
One string, or its plural forms chosen by `count`; `{name}` placeholders are filled in.
string | ({ other: string } & Partial<Record<Intl.LDMLPluralRule, string>>)
StringTable
A component's strings: its English defaults, or your overrides.
Record<string, Phrase>
StringOverrides
Overrides by component name, e.g. `{ thread: { jumpToLatest: "…" }, composer: { send: "…" } }`.
Record<string, Partial<StringTable> | undefined>
TextDirection
"ltr" | "rtl"
DateStyle
How an absolute time reads: "5 Nov 2026", "5 Nov 2026, 14:03" or "14:03".
"date" | "datetime" | "time"
Formatter
{ /** The locale actually used right now; "en-US" before hydration when no locale is set. */ tag: string | undefined /** A number in the locale: "1,234.5", "1 234,5". */ number: (n: number, options?: Intl.NumberFormatOptions) => string /** An absolute time. Until the page hydrates it reads as an ISO date in UTC, unless both `locale` and `timeZone` are set. */ date: (date: Date | string | number, s
Translate
Looks up one string by key and fills its placeholders; `count` also picks the plural form.
(key: keyof T & string, vars?: Record<string, string | number>) => string