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:
@@ -0,0 +1,156 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import type { Locale as DayPickerLocale } from "react-day-picker"
|
||||
|
||||
export type MessageValues = Record<string, unknown>
|
||||
|
||||
export interface MessageDescriptor {
|
||||
id: string
|
||||
message: string
|
||||
comment?: string
|
||||
values?: MessageValues
|
||||
}
|
||||
|
||||
export type TranslateFunction = (
|
||||
descriptor: MessageDescriptor,
|
||||
values?: MessageValues
|
||||
) => string
|
||||
|
||||
export type CalendarLocale = Partial<DayPickerLocale>
|
||||
|
||||
type UiMessages = Record<string, MessageDescriptor>
|
||||
|
||||
export const uiMessages = {
|
||||
breadcrumb: /* i18n */ {
|
||||
id: "ui.breadcrumb.label",
|
||||
message: "Breadcrumb",
|
||||
},
|
||||
close: /* i18n */ {
|
||||
id: "ui.common.close",
|
||||
message: "Close",
|
||||
},
|
||||
closeToast: /* i18n */ {
|
||||
id: "ui.toast.close",
|
||||
message: "Close notification",
|
||||
},
|
||||
loading: /* i18n */ {
|
||||
id: "ui.common.loading",
|
||||
message: "Loading",
|
||||
},
|
||||
more: /* i18n */ {
|
||||
id: "ui.common.more",
|
||||
message: "More",
|
||||
},
|
||||
morePages: /* i18n */ {
|
||||
id: "ui.pagination.more",
|
||||
message: "More pages",
|
||||
},
|
||||
next: /* i18n */ {
|
||||
id: "ui.pagination.next",
|
||||
message: "Next",
|
||||
},
|
||||
nextPage: /* i18n */ {
|
||||
id: "ui.pagination.nextPage",
|
||||
message: "Go to next page",
|
||||
},
|
||||
nextSlide: /* i18n */ {
|
||||
id: "ui.carousel.nextSlide",
|
||||
message: "Next slide",
|
||||
},
|
||||
pagination: /* i18n */ {
|
||||
id: "ui.pagination.label",
|
||||
message: "Pagination",
|
||||
},
|
||||
previous: /* i18n */ {
|
||||
id: "ui.pagination.previous",
|
||||
message: "Previous",
|
||||
},
|
||||
previousPage: /* i18n */ {
|
||||
id: "ui.pagination.previousPage",
|
||||
message: "Go to previous page",
|
||||
},
|
||||
previousSlide: /* i18n */ {
|
||||
id: "ui.carousel.previousSlide",
|
||||
message: "Previous slide",
|
||||
},
|
||||
sidebar: /* i18n */ {
|
||||
id: "ui.sidebar.title",
|
||||
message: "Sidebar",
|
||||
},
|
||||
sidebarDescription: /* i18n */ {
|
||||
id: "ui.sidebar.description",
|
||||
message: "Displays the mobile sidebar.",
|
||||
},
|
||||
toggleSidebar: /* i18n */ {
|
||||
id: "ui.sidebar.toggle",
|
||||
message: "Toggle sidebar",
|
||||
},
|
||||
} satisfies UiMessages
|
||||
|
||||
const DEFAULT_LOCALE = "en-US"
|
||||
const defaultTranslate: TranslateFunction = (descriptor) => descriptor.message
|
||||
|
||||
const TranslateContext =
|
||||
React.createContext<TranslateFunction>(defaultTranslate)
|
||||
const LocaleContext = React.createContext(DEFAULT_LOCALE)
|
||||
const CalendarLocaleContext = React.createContext<CalendarLocale | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
export interface I18nProviderProps {
|
||||
calendarLocale?: CalendarLocale
|
||||
children: React.ReactNode
|
||||
locale?: string
|
||||
t?: TranslateFunction
|
||||
}
|
||||
|
||||
export function I18nProvider({
|
||||
calendarLocale,
|
||||
children,
|
||||
locale = DEFAULT_LOCALE,
|
||||
t = defaultTranslate,
|
||||
}: I18nProviderProps) {
|
||||
return (
|
||||
<TranslateContext.Provider value={t}>
|
||||
<LocaleContext.Provider value={locale}>
|
||||
<CalendarLocaleContext.Provider value={calendarLocale}>
|
||||
{children}
|
||||
</CalendarLocaleContext.Provider>
|
||||
</LocaleContext.Provider>
|
||||
</TranslateContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useTranslate(): TranslateFunction {
|
||||
return React.useContext(TranslateContext)
|
||||
}
|
||||
|
||||
export function useMessage(
|
||||
descriptor: MessageDescriptor,
|
||||
values?: MessageValues
|
||||
) {
|
||||
const translate = useTranslate()
|
||||
|
||||
return React.useMemo(
|
||||
() => translate(descriptor, values),
|
||||
[descriptor, translate, values]
|
||||
)
|
||||
}
|
||||
|
||||
export interface LocalizedTextProps {
|
||||
message: MessageDescriptor
|
||||
values?: MessageValues
|
||||
}
|
||||
|
||||
export function LocalizedText({ message, values }: LocalizedTextProps) {
|
||||
return useMessage(message, values)
|
||||
}
|
||||
|
||||
export function useLocale(): string {
|
||||
return React.useContext(LocaleContext)
|
||||
}
|
||||
|
||||
export function useCalendarLocale(): CalendarLocale | undefined {
|
||||
return React.useContext(CalendarLocaleContext)
|
||||
}
|
||||
Reference in New Issue
Block a user