feat(ui): add framework-neutral localization support

- add a lightweight UI i18n provider with translation and locale contexts\n- localize accessibility labels across breadcrumb, carousel, dialog, pagination, sheet, sidebar, spinner, and toast components\n- integrate calendar locale data without coupling UI components to Lingui\n- publish tree-shakeable catalogs for eight common locales
This commit is contained in:
Maofeng
2026-07-30 14:47:51 +08:00
parent 0768e8fa29
commit ae1798f6f4
20 changed files with 551 additions and 87 deletions
+25 -9
View File
@@ -5,12 +5,16 @@ import {
DayPicker,
getDefaultClassNames,
type DayButton,
type Locale,
} from "react-day-picker"
import { cn } from "@workspace/ui/lib/utils"
import { Button, buttonVariants } from "@workspace/ui/components/button"
import { ChevronLeftIcon, ChevronRightIcon, ChevronDownIcon } from "lucide-react"
import { useCalendarLocale, useLocale } from "@workspace/ui/components/i18n"
import {
ChevronLeftIcon,
ChevronRightIcon,
ChevronDownIcon,
} from "lucide-react"
function Calendar({
className,
@@ -18,7 +22,8 @@ function Calendar({
showOutsideDays = true,
captionLayout = "label",
buttonVariant = "ghost",
locale,
locale: providedLocale,
lang,
formatters,
components,
...props
@@ -26,6 +31,10 @@ function Calendar({
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
}) {
const defaultClassNames = getDefaultClassNames()
const contextLocale = useLocale()
const contextCalendarLocale = useCalendarLocale()
const locale = providedLocale ?? contextCalendarLocale
const localeCode = locale?.code ?? contextLocale
return (
<DayPicker
@@ -38,9 +47,10 @@ function Calendar({
)}
captionLayout={captionLayout}
locale={locale}
lang={lang ?? localeCode}
formatters={{
formatMonthDropdown: (date) =>
date.toLocaleString(locale?.code, { month: "short" }),
date.toLocaleString(localeCode, { month: "short" }),
...formatters,
}}
classNames={{
@@ -147,13 +157,19 @@ function Calendar({
Chevron: ({ className, orientation, ...props }) => {
if (orientation === "left") {
return (
<ChevronLeftIcon className={cn("rtl:rotate-180 size-4", className)} {...props} />
<ChevronLeftIcon
className={cn("size-4 rtl:rotate-180", className)}
{...props}
/>
)
}
if (orientation === "right") {
return (
<ChevronRightIcon className={cn("rtl:rotate-180 size-4", className)} {...props} />
<ChevronRightIcon
className={cn("size-4 rtl:rotate-180", className)}
{...props}
/>
)
}
@@ -162,7 +178,7 @@ function Calendar({
)
},
DayButton: ({ ...props }) => (
<CalendarDayButton locale={locale} {...props} />
<CalendarDayButton locale={localeCode} {...props} />
),
WeekNumber: ({ children, ...props }) => {
return (
@@ -186,7 +202,7 @@ function CalendarDayButton({
modifiers,
locale,
...props
}: React.ComponentProps<typeof DayButton> & { locale?: Partial<Locale> }) {
}: React.ComponentProps<typeof DayButton> & { locale?: string }) {
const defaultClassNames = getDefaultClassNames()
const ref = React.useRef<HTMLButtonElement>(null)
@@ -198,7 +214,7 @@ function CalendarDayButton({
<Button
variant="ghost"
size="icon"
data-day={day.date.toLocaleDateString(locale?.code)}
data-day={day.date.toLocaleDateString(locale)}
data-selected-single={
modifiers.selected &&
!modifiers.range_start &&