@aoctech/ui 0.1.5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -105,7 +105,7 @@ so a badge renders what the server said rather than a client-side mapping of an
105
105
  | `Checkbox` | Base UI `Checkbox`. |
106
106
  | `Radio` + `RadioGroup` | Base UI `Radio`. |
107
107
  | `Switch` | Base UI `Switch`. |
108
- | `DatePicker` + `Calendar` | Built on `react-day-picker`. |
108
+ | `DatePicker` + `Calendar` | Built on `react-day-picker`. `locale` (BCP-47, default `"pt-BR"`; `"en"` built in) drives calendar names, first weekday, displayed date (Intl) and aria copy; `labels` overrides single strings. `Modal` and `ErrorState` take the same `locale` (Modal also `labels`). Since 0.2.0. |
109
109
 
110
110
  Deliberately small. Components arrive when a second app needs one, not in anticipation — the whole
111
111
  point of extracting this was to stop maintaining four copies, and a component with one consumer is
@@ -1,11 +1,17 @@
1
1
  import type { Matcher } from "react-day-picker";
2
+ import { type DatePickerLabels, type Locale } from "../lib/i18n";
2
3
  interface CalendarProps {
3
4
  selected?: Date;
4
5
  onSelect?: (date: Date | undefined) => void;
5
6
  disabled?: Matcher | Matcher[];
6
7
  className?: string;
8
+ /** BCP-47 tag; drives month/weekday names, first day of week and aria copy. Defaults to "pt-BR". */
9
+ locale?: Locale;
10
+ /** Overrides individual built-in strings (aria labels). */
11
+ labels?: Partial<DatePickerLabels>;
7
12
  }
8
- declare function Calendar({ selected, onSelect, disabled, className }: CalendarProps): import("react").JSX.Element;
13
+ /** An inline, keyboard-operable calendar. Use DatePicker for a field trigger. */
14
+ declare function Calendar({ selected, onSelect, disabled, className, locale, labels }: CalendarProps): import("react").JSX.Element;
9
15
  interface DatePickerProps {
10
16
  value?: Date;
11
17
  defaultValue?: Date;
@@ -18,11 +24,15 @@ interface DatePickerProps {
18
24
  min?: Date;
19
25
  max?: Date;
20
26
  defaultOpen?: boolean;
27
+ /** BCP-47 tag (e.g. "pt-BR", "en"). Drives calendar, date display and copy. Defaults to "pt-BR". */
28
+ locale?: Locale;
29
+ /** Overrides individual built-in strings (placeholder, today, selected, month navigation). */
30
+ labels?: Partial<DatePickerLabels>;
21
31
  }
22
32
  /**
23
33
  * A single-date field backed by an accessible Popover + Calendar. Dates remain
24
34
  * Date objects at the API boundary so products can apply their own timezone
25
35
  * and serialization policies explicitly.
26
36
  */
27
- declare function DatePicker({ value, defaultValue, onValueChange, placeholder, disabled, id, name, className, min, max, defaultOpen, }: DatePickerProps): import("react").JSX.Element;
37
+ declare function DatePicker({ value, defaultValue, onValueChange, placeholder, disabled, id, name, className, min, max, defaultOpen, locale, labels, }: DatePickerProps): import("react").JSX.Element;
28
38
  export { Calendar, DatePicker, type CalendarProps, type DatePickerProps };
@@ -1,26 +1,31 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Popover } from "@base-ui/react/popover";
4
- import { format } from "date-fns";
5
- import { ptBR } from "date-fns/locale/pt-BR";
6
4
  import { useState } from "react";
7
- import { DayPicker } from "react-day-picker";
5
+ import { DateLib, DayPicker } from "react-day-picker";
6
+ import { enUS, ptBR } from "react-day-picker/locale";
8
7
  import { cn } from "../lib/cn";
8
+ import { DEFAULT_LOCALE, getDatePickerLabels, resolveCatalog } from "../lib/i18n";
9
9
  import { useThemeScope } from "./theme-provider";
10
10
  /** An inline, keyboard-operable calendar. Use DatePicker for a field trigger. */
11
- // react-day-picker's default labels are English ("Go to the Previous Month",
12
- // "Today, …"): a screen reader in a pt-BR product read half its calendar in
13
- // another language.
14
- const LABELS = {
15
- labelPrevious: () => "Mês anterior",
16
- labelNext: () => "Próximo mês",
17
- labelDayButton: (date, modifiers) => `${modifiers.today ? "Hoje, " : ""}${format(date, "PPPP", { locale: ptBR })}${modifiers.selected ? ", selecionado" : ""}`,
18
- };
19
- function Calendar({ selected, onSelect, disabled, className }) {
11
+ function Calendar({ selected, onSelect, disabled, className, locale = DEFAULT_LOCALE, labels }) {
12
+ const text = getDatePickerLabels(locale, labels);
13
+ const dayLocale = resolveCatalog(locale) === "pt-BR" ? ptBR : enUS;
20
14
  return (_jsx(DayPicker, { mode: "single",
21
15
  // Open on the selected date's month, not today's: editing a date months
22
16
  // away otherwise starts by paging back to it.
23
- defaultMonth: selected, labels: LABELS, selected: selected, onSelect: onSelect, disabled: disabled, locale: ptBR, classNames: {
17
+ defaultMonth: selected, selected: selected, onSelect: onSelect, disabled: disabled, locale: dayLocale, labels: {
18
+ labelPrevious: () => text.previousMonth,
19
+ labelNext: () => text.nextMonth,
20
+ labelDayButton: (date, modifiers, options, dateLib) => {
21
+ let label = (dateLib ?? new DateLib(options)).format(date, "PPPP");
22
+ if (modifiers.today)
23
+ label = `${text.today}, ${label}`;
24
+ if (modifiers.selected)
25
+ label = `${label}, ${text.selected}`;
26
+ return label;
27
+ },
28
+ }, classNames: {
24
29
  month_caption: "calendar-month-caption",
25
30
  caption_label: "calendar-caption-label",
26
31
  nav: "calendar-nav",
@@ -38,7 +43,7 @@ function Calendar({ selected, onSelect, disabled, className }) {
38
43
  * Date objects at the API boundary so products can apply their own timezone
39
44
  * and serialization policies explicitly.
40
45
  */
41
- function DatePicker({ value, defaultValue, onValueChange, placeholder = "Selecionar data", disabled = false, id, name, className, min, max, defaultOpen = false, }) {
46
+ function DatePicker({ value, defaultValue, onValueChange, placeholder, disabled = false, id, name, className, min, max, defaultOpen = false, locale = DEFAULT_LOCALE, labels, }) {
42
47
  const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
43
48
  const [open, setOpen] = useState(defaultOpen);
44
49
  const { theme, density } = useThemeScope();
@@ -47,9 +52,8 @@ function DatePicker({ value, defaultValue, onValueChange, placeholder = "Selecio
47
52
  ...(min ? [{ before: min }] : []),
48
53
  ...(max ? [{ after: max }] : []),
49
54
  ];
50
- const label = selected
51
- ? new Intl.DateTimeFormat("pt-BR", { day: "2-digit", month: "long", year: "numeric" }).format(selected)
52
- : placeholder;
55
+ const text = getDatePickerLabels(locale, labels);
56
+ const label = selected ? formatDateLabel(selected, locale) : placeholder ?? text.placeholder;
53
57
  function selectDate(date) {
54
58
  if (value === undefined)
55
59
  setUncontrolledValue(date);
@@ -57,7 +61,15 @@ function DatePicker({ value, defaultValue, onValueChange, placeholder = "Selecio
57
61
  if (date)
58
62
  setOpen(false);
59
63
  }
60
- return (_jsxs(Popover.Root, { open: open, onOpenChange: setOpen, children: [_jsxs(Popover.Trigger, { id: id, disabled: disabled, className: cn("flex h-11 w-full items-center justify-between rounded-lg border border-border bg-background px-3 text-left text-sm outline-none", "text-foreground transition-[border-color,box-shadow] duration-150 hover:bg-surface focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/35", "disabled:cursor-not-allowed disabled:opacity-50 in-data-[density=compact]:h-8 in-data-[density=compact]:px-2.5", !selected && "text-muted-foreground", className), children: [_jsx("span", { className: "truncate", children: label }), _jsx(CalendarGlyph, {})] }), name && _jsx("input", { type: "hidden", name: name, value: selected ? formatDateValue(selected) : "" }), _jsx(Popover.Portal, { "data-ctech-theme": theme, "data-density": density, children: _jsx(Popover.Positioner, { side: "bottom", align: "start", sideOffset: 8, className: "z-50", children: _jsx(Popover.Popup, { className: "rounded-xl border border-border bg-background shadow-modal outline-none transition-[opacity,scale] duration-150 data-[ending-style]:scale-[0.98] data-[ending-style]:opacity-0 data-[starting-style]:scale-[0.98] data-[starting-style]:opacity-0 motion-reduce:transition-none", children: _jsx(Calendar, { selected: selected, onSelect: selectDate, disabled: disabled || disabledDays.length > 0 ? (disabled ? true : disabledDays) : undefined }) }) }) })] }));
64
+ return (_jsxs(Popover.Root, { open: open, onOpenChange: setOpen, children: [_jsxs(Popover.Trigger, { id: id, disabled: disabled, className: cn("flex h-11 w-full items-center justify-between rounded-lg border border-border bg-background px-3 text-left text-sm outline-none", "text-foreground transition-[border-color,box-shadow] duration-150 hover:bg-surface focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/35", "disabled:cursor-not-allowed disabled:opacity-50 in-data-[density=compact]:h-8 in-data-[density=compact]:px-2.5", !selected && "text-muted-foreground", className), children: [_jsx("span", { className: "truncate", children: label }), _jsx(CalendarGlyph, {})] }), name && _jsx("input", { type: "hidden", name: name, value: selected ? formatDateValue(selected) : "" }), _jsx(Popover.Portal, { "data-ctech-theme": theme, "data-density": density, children: _jsx(Popover.Positioner, { side: "bottom", align: "start", sideOffset: 8, className: "z-50", children: _jsx(Popover.Popup, { className: "rounded-xl border border-border bg-background shadow-modal outline-none transition-[opacity,scale] duration-150 data-[ending-style]:scale-[0.98] data-[ending-style]:opacity-0 data-[starting-style]:scale-[0.98] data-[starting-style]:opacity-0 motion-reduce:transition-none", children: _jsx(Calendar, { locale: locale, labels: labels, selected: selected, onSelect: selectDate, disabled: disabled || disabledDays.length > 0 ? (disabled ? true : disabledDays) : undefined }) }) }) })] }));
65
+ }
66
+ function formatDateLabel(value, locale) {
67
+ try {
68
+ return new Intl.DateTimeFormat(locale, { day: "2-digit", month: "long", year: "numeric" }).format(value);
69
+ }
70
+ catch {
71
+ return new Intl.DateTimeFormat(DEFAULT_LOCALE, { day: "2-digit", month: "long", year: "numeric" }).format(value);
72
+ }
61
73
  }
62
74
  function formatDateValue(value) {
63
75
  return `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, "0")}-${String(value.getDate()).padStart(2, "0")}`;
@@ -1,32 +1,17 @@
1
1
  import type { ReactNode } from "react";
2
- declare const errorMessages: {
3
- readonly 400: {
4
- readonly title: "Não foi possível abrir este endereço";
5
- readonly description: "Confira os parâmetros do link ou volte ao início para continuar.";
6
- };
7
- readonly 404: {
8
- readonly title: "Não encontramos esta página";
9
- readonly description: "O endereço pode ter mudado ou o conteúdo não está disponível.";
10
- };
11
- readonly 500: {
12
- readonly title: "Não foi possível carregar a página";
13
- readonly description: "Ocorreu um erro inesperado. Tente novamente em instantes.";
14
- };
15
- readonly 503: {
16
- readonly title: "Serviço temporariamente indisponível";
17
- readonly description: "Estamos sem conexão com o serviço. Verifique sua conexão e tente novamente.";
18
- };
19
- };
20
- type ErrorStatus = keyof typeof errorMessages;
2
+ import { type Locale } from "../lib/i18n";
3
+ type ErrorStatus = 400 | 404 | 500 | 503;
21
4
  interface ErrorStateProps {
22
5
  status: ErrorStatus;
23
6
  title?: string;
24
7
  description?: string;
25
8
  action?: ReactNode;
26
9
  className?: string;
10
+ /** BCP-47 tag for the built-in copy. Defaults to "pt-BR". */
11
+ locale?: Locale;
27
12
  }
28
13
  /** Shared error presentation. Apps own routing, retries and availability checks.
29
14
  * Never pass exception messages, stack traces, tokens or private data as copy.
30
15
  */
31
- declare function ErrorState({ status, title, description, action, className }: ErrorStateProps): import("react").JSX.Element;
16
+ declare function ErrorState({ status, title, description, action, className, locale }: ErrorStateProps): import("react").JSX.Element;
32
17
  export { ErrorState, type ErrorStateProps, type ErrorStatus };
@@ -1,16 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cn } from "../lib/cn";
3
- const errorMessages = {
4
- 400: { title: "Não foi possível abrir este endereço", description: "Confira os parâmetros do link ou volte ao início para continuar." },
5
- 404: { title: "Não encontramos esta página", description: "O endereço pode ter mudado ou o conteúdo não está disponível." },
6
- 500: { title: "Não foi possível carregar a página", description: "Ocorreu um erro inesperado. Tente novamente em instantes." },
7
- 503: { title: "Serviço temporariamente indisponível", description: "Estamos sem conexão com o serviço. Verifique sua conexão e tente novamente." },
8
- };
3
+ import { DEFAULT_LOCALE, getErrorMessages } from "../lib/i18n";
9
4
  /** Shared error presentation. Apps own routing, retries and availability checks.
10
5
  * Never pass exception messages, stack traces, tokens or private data as copy.
11
6
  */
12
- function ErrorState({ status, title, description, action, className }) {
13
- const message = errorMessages[status];
7
+ function ErrorState({ status, title, description, action, className, locale = DEFAULT_LOCALE }) {
8
+ const message = getErrorMessages(locale)[status];
14
9
  return (_jsxs("section", { "data-slot": "error-state", role: "alert", className: cn("mx-auto flex max-w-2xl flex-col items-center gap-5 px-5 py-16 text-center", className), children: [_jsx("p", { className: "font-mono text-sm tabular-nums text-muted-foreground", children: status }), _jsx("h1", { className: "text-2xl font-semibold text-foreground sm:text-3xl", children: title || message.title }), _jsx("p", { className: "max-w-[48ch] text-pretty text-base text-muted-foreground", children: description || message.description }), action && _jsx("div", { className: "mt-2 flex flex-wrap justify-center gap-3", children: action })] }));
15
10
  }
16
11
  export { ErrorState };
@@ -1,4 +1,5 @@
1
1
  import type { ReactNode } from "react";
2
+ import { type Locale, type ModalLabels } from "../lib/i18n";
2
3
  /**
3
4
  * Built on Base UI's Dialog rather than a hand-rolled portal. The focus trap,
4
5
  * the scroll lock, the Escape handling, the restore-focus-on-close and the
@@ -26,6 +27,10 @@ interface ModalProps {
26
27
  danger?: boolean;
27
28
  submitDisabled?: boolean;
28
29
  size?: keyof typeof SIZE;
30
+ /** BCP-47 tag for built-in copy. Defaults to "pt-BR". */
31
+ locale?: Locale;
32
+ /** Overrides built-in strings; submitLabel/cancelLabel take precedence. */
33
+ labels?: Partial<ModalLabels>;
29
34
  }
30
- declare function Modal({ open, onClose, title, description, children, onSubmit, submitLabel, cancelLabel, loading, danger, submitDisabled, size, }: ModalProps): import("react").JSX.Element;
35
+ declare function Modal({ open, onClose, title, description, children, onSubmit, submitLabel, cancelLabel, loading, danger, submitDisabled, size, locale, labels, }: ModalProps): import("react").JSX.Element;
31
36
  export { Modal, type ModalProps };
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Dialog } from "@base-ui/react/dialog";
4
4
  import { cn } from "../lib/cn";
5
+ import { DEFAULT_LOCALE, getModalLabels } from "../lib/i18n";
5
6
  import { Button } from "./button";
6
7
  import { useThemeScope } from "./theme-provider";
7
8
  /**
@@ -17,9 +18,10 @@ const SIZE = {
17
18
  lg: "max-w-2xl",
18
19
  xl: "max-w-4xl",
19
20
  };
20
- function Modal({ open, onClose, title, description, children, onSubmit, submitLabel = "Salvar", cancelLabel = "Cancelar", loading = false, danger = false, submitDisabled = false, size = "md", }) {
21
+ function Modal({ open, onClose, title, description, children, onSubmit, submitLabel, cancelLabel, loading = false, danger = false, submitDisabled = false, size = "md", locale = DEFAULT_LOCALE, labels, }) {
22
+ const text = getModalLabels(locale, labels);
21
23
  const { theme, density } = useThemeScope();
22
- return (_jsx(Dialog.Root, { open: open, onOpenChange: next => !next && onClose(), children: _jsxs(Dialog.Portal, { "data-ctech-theme": theme, "data-density": density, children: [_jsx(Dialog.Backdrop, { className: cn("fixed inset-0 z-40 bg-black/40 backdrop-blur-[1px]", "transition-opacity duration-200 ease-out", "data-[ending-style]:opacity-0 data-[starting-style]:opacity-0", "motion-reduce:transition-none") }), _jsxs(Dialog.Popup, { className: cn("fixed top-1/2 left-1/2 z-50 -translate-x-1/2 -translate-y-1/2", "w-[calc(100vw-2rem)] max-h-[85vh] overflow-y-auto", "rounded-xl border border-border bg-background text-foreground shadow-modal outline-none", "transition-[opacity,translate,scale] duration-200 ease-out", "data-[ending-style]:scale-[0.98] data-[ending-style]:opacity-0", "data-[starting-style]:scale-[0.98] data-[starting-style]:opacity-0", "motion-reduce:transition-opacity motion-reduce:data-[ending-style]:scale-100 motion-reduce:data-[starting-style]:scale-100", SIZE[size]), children: [_jsxs("header", { className: "sticky top-0 z-10 flex items-start justify-between gap-4 border-b border-border bg-background px-6 py-4", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Dialog.Title, { className: "text-base font-semibold text-foreground", children: title }), description && (_jsx(Dialog.Description, { className: "text-sm text-muted-foreground", children: description }))] }), _jsx(Dialog.Close, { render: _jsx(Button, { variant: "ghost", size: "icon", "aria-label": "Fechar", className: "-mr-2 shrink-0 text-muted-foreground" }), children: _jsx(CloseGlyph, {}) })] }), children && _jsx("div", { className: "px-6 py-5", children: children }), _jsxs("footer", { className: "sticky bottom-0 flex justify-end gap-3 border-t border-border bg-surface px-6 py-4", children: [_jsx(Button, { type: "button", variant: "outline", onClick: onClose, disabled: loading, children: cancelLabel }), onSubmit && (_jsx(Button, { type: "button", variant: danger ? "danger" : "brand", onClick: onSubmit, disabled: loading || submitDisabled, children: loading ? "Aguarde…" : submitLabel }))] })] })] }) }));
24
+ return (_jsx(Dialog.Root, { open: open, onOpenChange: next => !next && onClose(), children: _jsxs(Dialog.Portal, { "data-ctech-theme": theme, "data-density": density, children: [_jsx(Dialog.Backdrop, { className: cn("fixed inset-0 z-40 bg-black/40 backdrop-blur-[1px]", "transition-opacity duration-200 ease-out", "data-[ending-style]:opacity-0 data-[starting-style]:opacity-0", "motion-reduce:transition-none") }), _jsxs(Dialog.Popup, { className: cn("fixed top-1/2 left-1/2 z-50 -translate-x-1/2 -translate-y-1/2", "w-[calc(100vw-2rem)] max-h-[85vh] overflow-y-auto", "rounded-xl border border-border bg-background text-foreground shadow-modal outline-none", "transition-[opacity,translate,scale] duration-200 ease-out", "data-[ending-style]:scale-[0.98] data-[ending-style]:opacity-0", "data-[starting-style]:scale-[0.98] data-[starting-style]:opacity-0", "motion-reduce:transition-opacity motion-reduce:data-[ending-style]:scale-100 motion-reduce:data-[starting-style]:scale-100", SIZE[size]), children: [_jsxs("header", { className: "sticky top-0 z-10 flex items-start justify-between gap-4 border-b border-border bg-background px-6 py-4", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Dialog.Title, { className: "text-base font-semibold text-foreground", children: title }), description && (_jsx(Dialog.Description, { className: "text-sm text-muted-foreground", children: description }))] }), _jsx(Dialog.Close, { render: _jsx(Button, { variant: "ghost", size: "icon", "aria-label": text.close, className: "-mr-2 shrink-0 text-muted-foreground" }), children: _jsx(CloseGlyph, {}) })] }), children && _jsx("div", { className: "px-6 py-5", children: children }), _jsxs("footer", { className: "sticky bottom-0 flex justify-end gap-3 border-t border-border bg-surface px-6 py-4", children: [_jsx(Button, { type: "button", variant: "outline", onClick: onClose, disabled: loading, children: cancelLabel ?? text.cancel }), onSubmit && (_jsx(Button, { type: "button", variant: danger ? "danger" : "brand", onClick: onSubmit, disabled: loading || submitDisabled, children: loading ? text.loading : submitLabel ?? text.submit }))] })] })] }) }));
23
25
  }
24
26
  function CloseGlyph() {
25
27
  return (_jsx("svg", { viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.5, "aria-hidden": true, children: _jsx("path", { d: "m4 4 8 8M12 4l-8 8", strokeLinecap: "round" }) }));
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { cn } from "./lib/cn";
2
+ export { type DatePickerLabels, type Locale, type ModalLabels } from "./lib/i18n";
2
3
  export { Badge, badgeVariants, type BadgeTone } from "./components/badge";
3
4
  export { Button, buttonVariants } from "./components/button";
4
5
  export { Alert, alertVariants, type AlertProps } from "./components/alert";
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Tiny built-in i18n for the strings shared components own. Two catalogs
3
+ * (pt-BR, en); any BCP-47 tag resolves to one of them by language, while Intl
4
+ * formatting still honours the exact tag. Products override single strings with
5
+ * each component's `labels` prop instead of forking the catalog.
6
+ */
7
+ type CatalogLocale = "pt-BR" | "en";
8
+ /** A BCP-47 tag such as "pt-BR" or "en". Unknown languages fall back to English copy. */
9
+ type Locale = string;
10
+ declare const DEFAULT_LOCALE: Locale;
11
+ declare function resolveCatalog(locale: Locale | undefined): CatalogLocale;
12
+ declare const datePickerMessages: {
13
+ readonly "pt-BR": {
14
+ readonly placeholder: "Selecionar data";
15
+ readonly today: "Hoje";
16
+ readonly selected: "selecionado";
17
+ readonly previousMonth: "Ir para o mês anterior";
18
+ readonly nextMonth: "Ir para o próximo mês";
19
+ };
20
+ readonly en: {
21
+ readonly placeholder: "Select date";
22
+ readonly today: "Today";
23
+ readonly selected: "selected";
24
+ readonly previousMonth: "Go to the previous month";
25
+ readonly nextMonth: "Go to the next month";
26
+ };
27
+ };
28
+ declare const modalMessages: {
29
+ readonly "pt-BR": {
30
+ readonly submit: "Salvar";
31
+ readonly cancel: "Cancelar";
32
+ readonly close: "Fechar";
33
+ readonly loading: "Aguarde…";
34
+ };
35
+ readonly en: {
36
+ readonly submit: "Save";
37
+ readonly cancel: "Cancel";
38
+ readonly close: "Close";
39
+ readonly loading: "Please wait…";
40
+ };
41
+ };
42
+ type DatePickerLabels = {
43
+ -readonly [K in keyof (typeof datePickerMessages)["en"]]: string;
44
+ };
45
+ type ModalLabels = {
46
+ -readonly [K in keyof (typeof modalMessages)["en"]]: string;
47
+ };
48
+ declare function getDatePickerLabels(locale: Locale | undefined, overrides?: Partial<DatePickerLabels>): DatePickerLabels;
49
+ declare function getModalLabels(locale: Locale | undefined, overrides?: Partial<ModalLabels>): ModalLabels;
50
+ declare function getErrorMessages(locale: Locale | undefined): {
51
+ readonly 400: {
52
+ readonly title: "Não foi possível abrir este endereço";
53
+ readonly description: "Confira os parâmetros do link ou volte ao início para continuar.";
54
+ };
55
+ readonly 404: {
56
+ readonly title: "Não encontramos esta página";
57
+ readonly description: "O endereço pode ter mudado ou o conteúdo não está disponível.";
58
+ };
59
+ readonly 500: {
60
+ readonly title: "Não foi possível carregar a página";
61
+ readonly description: "Ocorreu um erro inesperado. Tente novamente em instantes.";
62
+ };
63
+ readonly 503: {
64
+ readonly title: "Serviço temporariamente indisponível";
65
+ readonly description: "Estamos sem conexão com o serviço. Verifique sua conexão e tente novamente.";
66
+ };
67
+ } | {
68
+ readonly 400: {
69
+ readonly title: "We couldn't open this address";
70
+ readonly description: "Check the link's parameters or go back to the start to continue.";
71
+ };
72
+ readonly 404: {
73
+ readonly title: "We couldn't find this page";
74
+ readonly description: "The address may have changed or the content is unavailable.";
75
+ };
76
+ readonly 500: {
77
+ readonly title: "We couldn't load the page";
78
+ readonly description: "An unexpected error occurred. Please try again shortly.";
79
+ };
80
+ readonly 503: {
81
+ readonly title: "Service temporarily unavailable";
82
+ readonly description: "We can't reach the service. Check your connection and try again.";
83
+ };
84
+ };
85
+ export { DEFAULT_LOCALE, getDatePickerLabels, getErrorMessages, getModalLabels, resolveCatalog, type CatalogLocale, type DatePickerLabels, type Locale, type ModalLabels, };
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Tiny built-in i18n for the strings shared components own. Two catalogs
3
+ * (pt-BR, en); any BCP-47 tag resolves to one of them by language, while Intl
4
+ * formatting still honours the exact tag. Products override single strings with
5
+ * each component's `labels` prop instead of forking the catalog.
6
+ */
7
+ const DEFAULT_LOCALE = "pt-BR";
8
+ function resolveCatalog(locale) {
9
+ const language = (locale ?? DEFAULT_LOCALE).toLowerCase().split(/[-_]/)[0];
10
+ return language === "pt" ? "pt-BR" : "en";
11
+ }
12
+ const datePickerMessages = {
13
+ "pt-BR": {
14
+ placeholder: "Selecionar data",
15
+ today: "Hoje",
16
+ selected: "selecionado",
17
+ previousMonth: "Ir para o mês anterior",
18
+ nextMonth: "Ir para o próximo mês",
19
+ },
20
+ en: {
21
+ placeholder: "Select date",
22
+ today: "Today",
23
+ selected: "selected",
24
+ previousMonth: "Go to the previous month",
25
+ nextMonth: "Go to the next month",
26
+ },
27
+ };
28
+ const modalMessages = {
29
+ "pt-BR": { submit: "Salvar", cancel: "Cancelar", close: "Fechar", loading: "Aguarde…" },
30
+ en: { submit: "Save", cancel: "Cancel", close: "Close", loading: "Please wait…" },
31
+ };
32
+ const errorMessages = {
33
+ "pt-BR": {
34
+ 400: { title: "Não foi possível abrir este endereço", description: "Confira os parâmetros do link ou volte ao início para continuar." },
35
+ 404: { title: "Não encontramos esta página", description: "O endereço pode ter mudado ou o conteúdo não está disponível." },
36
+ 500: { title: "Não foi possível carregar a página", description: "Ocorreu um erro inesperado. Tente novamente em instantes." },
37
+ 503: { title: "Serviço temporariamente indisponível", description: "Estamos sem conexão com o serviço. Verifique sua conexão e tente novamente." },
38
+ },
39
+ en: {
40
+ 400: { title: "We couldn't open this address", description: "Check the link's parameters or go back to the start to continue." },
41
+ 404: { title: "We couldn't find this page", description: "The address may have changed or the content is unavailable." },
42
+ 500: { title: "We couldn't load the page", description: "An unexpected error occurred. Please try again shortly." },
43
+ 503: { title: "Service temporarily unavailable", description: "We can't reach the service. Check your connection and try again." },
44
+ },
45
+ };
46
+ function getDatePickerLabels(locale, overrides) {
47
+ return { ...datePickerMessages[resolveCatalog(locale)], ...overrides };
48
+ }
49
+ function getModalLabels(locale, overrides) {
50
+ return { ...modalMessages[resolveCatalog(locale)], ...overrides };
51
+ }
52
+ function getErrorMessages(locale) {
53
+ return errorMessages[resolveCatalog(locale)];
54
+ }
55
+ export { DEFAULT_LOCALE, getDatePickerLabels, getErrorMessages, getModalLabels, resolveCatalog, };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aoctech/ui",
3
- "version": "0.1.5",
3
+ "version": "0.2.0",
4
4
  "description": "Shared CTech UI primitives: Base UI behaviour, Tailwind classes, no colours of its own. Themed entirely by the consuming app's CSS variables.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "scripts": {
38
38
  "build": "tsc -p tsconfig.json && node scripts/copy-styles.mjs",
39
- "test": "npm run build",
39
+ "test": "node --test src/lib/i18n.test.ts && npm run build",
40
40
  "storybook": "storybook dev -p 6006",
41
41
  "build-storybook": "storybook build"
42
42
  },