@djangocfg/ui-core 2.1.110 → 2.1.112

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/ui-core",
3
- "version": "2.1.110",
3
+ "version": "2.1.112",
4
4
  "description": "Pure React UI component library without Next.js dependencies - for Electron, Vite, CRA apps",
5
5
  "keywords": [
6
6
  "ui-components",
@@ -65,6 +65,7 @@
65
65
  "check": "tsc --noEmit"
66
66
  },
67
67
  "peerDependencies": {
68
+ "@djangocfg/i18n": "^2.1.112",
68
69
  "lucide-react": "^0.545.0",
69
70
  "moment": "^2.30.1",
70
71
  "react": "^19.0.0",
@@ -122,7 +123,8 @@
122
123
  "vaul": "1.1.2"
123
124
  },
124
125
  "devDependencies": {
125
- "@djangocfg/typescript-config": "^2.1.110",
126
+ "@djangocfg/i18n": "^2.1.112",
127
+ "@djangocfg/typescript-config": "^2.1.112",
126
128
  "@types/node": "^24.7.2",
127
129
  "@types/react": "^19.1.0",
128
130
  "@types/react-dom": "^19.1.0",
@@ -4,6 +4,7 @@ import { format } from 'date-fns';
4
4
  import { CalendarIcon } from 'lucide-react';
5
5
  import * as React from 'react';
6
6
 
7
+ import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
7
8
  import { cn } from '../../lib/utils';
8
9
  import { Button } from '../button';
9
10
  import { Popover, PopoverContent, PopoverTrigger } from '../popover';
@@ -43,7 +44,7 @@ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
43
44
  {
44
45
  value,
45
46
  onChange,
46
- placeholder = 'Pick a date',
47
+ placeholder,
47
48
  dateFormat = 'PPP',
48
49
  disabled = false,
49
50
  fromDate,
@@ -54,8 +55,11 @@ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
54
55
  },
55
56
  ref
56
57
  ) => {
58
+ const t = useTypedT<I18nTranslations>()
57
59
  const [open, setOpen] = React.useState(false)
58
60
 
61
+ const resolvedPlaceholder = placeholder ?? t('ui.datetime.pickDate')
62
+
59
63
  const handleSelect: SelectSingleEventHandler = (date) => {
60
64
  onChange?.(date)
61
65
  setOpen(false)
@@ -75,7 +79,7 @@ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
75
79
  )}
76
80
  >
77
81
  <CalendarIcon className="mr-2 h-4 w-4" />
78
- {value ? format(value, dateFormat) : placeholder}
82
+ {value ? format(value, dateFormat) : resolvedPlaceholder}
79
83
  </Button>
80
84
  </PopoverTrigger>
81
85
  <PopoverContent className="w-auto p-0" align={align}>
@@ -132,7 +136,7 @@ const DateRangePicker = React.forwardRef<HTMLButtonElement, DateRangePickerProps
132
136
  {
133
137
  value,
134
138
  onChange,
135
- placeholder = 'Pick a date range',
139
+ placeholder,
136
140
  dateFormat = 'LLL dd, y',
137
141
  disabled = false,
138
142
  fromDate,
@@ -144,10 +148,13 @@ const DateRangePicker = React.forwardRef<HTMLButtonElement, DateRangePickerProps
144
148
  },
145
149
  ref
146
150
  ) => {
151
+ const t = useTypedT<I18nTranslations>()
147
152
  const [open, setOpen] = React.useState(false)
148
153
 
154
+ const resolvedPlaceholder = placeholder ?? t('ui.datetime.pickDateRange')
155
+
149
156
  const formatRange = () => {
150
- if (!value?.from) return placeholder
157
+ if (!value?.from) return resolvedPlaceholder
151
158
  if (!value.to) return format(value.from, dateFormat)
152
159
  return `${format(value.from, dateFormat)} - ${format(value.to, dateFormat)}`
153
160
  }
@@ -5,6 +5,7 @@ import * as React from 'react';
5
5
 
6
6
  import { ArrowLeftIcon, ArrowRightIcon } from '@radix-ui/react-icons';
7
7
 
8
+ import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
8
9
  import { cn } from '../lib/utils';
9
10
  import { Button } from './button';
10
11
 
@@ -197,7 +198,9 @@ const CarouselPrevious = React.forwardRef<
197
198
  HTMLButtonElement,
198
199
  React.ComponentProps<typeof Button>
199
200
  >(({ className, variant = "outline", size = "icon", ...props }, ref) => {
201
+ const t = useTypedT<I18nTranslations>()
200
202
  const { orientation, scrollPrev, canScrollPrev } = useCarousel()
203
+ const previousLabel = t('ui.actions.previousSlide')
201
204
 
202
205
  return (
203
206
  <Button
@@ -216,7 +219,7 @@ const CarouselPrevious = React.forwardRef<
216
219
  {...props}
217
220
  >
218
221
  <ArrowLeftIcon className="h-4 w-4" />
219
- <span className="sr-only">Previous slide</span>
222
+ <span className="sr-only">{previousLabel}</span>
220
223
  </Button>
221
224
  )
222
225
  })
@@ -226,7 +229,9 @@ const CarouselNext = React.forwardRef<
226
229
  HTMLButtonElement,
227
230
  React.ComponentProps<typeof Button>
228
231
  >(({ className, variant = "outline", size = "icon", ...props }, ref) => {
232
+ const t = useTypedT<I18nTranslations>()
229
233
  const { orientation, scrollNext, canScrollNext } = useCarousel()
234
+ const nextLabel = t('ui.actions.nextSlide')
230
235
 
231
236
  return (
232
237
  <Button
@@ -245,7 +250,7 @@ const CarouselNext = React.forwardRef<
245
250
  {...props}
246
251
  >
247
252
  <ArrowRightIcon className="h-4 w-4" />
248
- <span className="sr-only">Next slide</span>
253
+ <span className="sr-only">{nextLabel}</span>
249
254
  </Button>
250
255
  )
251
256
  })
@@ -3,6 +3,7 @@
3
3
  import { Check, ChevronsUpDown } from 'lucide-react';
4
4
  import * as React from 'react';
5
5
 
6
+ import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
6
7
  import { cn } from '../lib/utils';
7
8
  import { Button } from './button';
8
9
  import {
@@ -34,18 +35,24 @@ export function Combobox({
34
35
  options,
35
36
  value,
36
37
  onValueChange,
37
- placeholder = "Select option...",
38
- searchPlaceholder = "Search...",
39
- emptyText = "No results found.",
38
+ placeholder,
39
+ searchPlaceholder,
40
+ emptyText,
40
41
  className,
41
42
  disabled = false,
42
43
  renderOption,
43
44
  renderValue,
44
45
  }: ComboboxProps) {
46
+ const t = useTypedT<I18nTranslations>()
45
47
  const [open, setOpen] = React.useState(false)
46
48
  const [search, setSearch] = React.useState("")
47
49
  const scrollRef = React.useRef<HTMLDivElement>(null)
48
50
 
51
+ // Resolve translations
52
+ const resolvedPlaceholder = placeholder ?? t('ui.select.placeholder')
53
+ const resolvedSearchPlaceholder = searchPlaceholder ?? t('ui.select.search')
54
+ const resolvedEmptyText = emptyText ?? t('ui.select.noResults')
55
+
49
56
  React.useEffect(() => {
50
57
  if (scrollRef.current && open) {
51
58
  // Force scrollable styles with !important
@@ -102,14 +109,14 @@ export function Combobox({
102
109
  ? renderValue(selectedOption)
103
110
  : selectedOption
104
111
  ? selectedOption.label
105
- : placeholder}
112
+ : resolvedPlaceholder}
106
113
  <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
107
114
  </Button>
108
115
  </PopoverTrigger>
109
116
  <PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0" align="start">
110
117
  <Command shouldFilter={false} className="flex flex-col">
111
118
  <CommandInput
112
- placeholder={searchPlaceholder}
119
+ placeholder={resolvedSearchPlaceholder}
113
120
  className="shrink-0"
114
121
  value={search}
115
122
  onValueChange={setSearch}
@@ -128,7 +135,7 @@ export function Combobox({
128
135
  >
129
136
  <CommandList className="!max-h-none !overflow-visible" style={{ pointerEvents: 'auto' }}>
130
137
  {filteredOptions.length === 0 ? (
131
- <CommandEmpty>{emptyText}</CommandEmpty>
138
+ <CommandEmpty>{resolvedEmptyText}</CommandEmpty>
132
139
  ) : (
133
140
  <CommandGroup className="!overflow-visible" style={{ pointerEvents: 'auto' }}>
134
141
  {filteredOptions.map((option) => (
@@ -3,6 +3,7 @@
3
3
  import { Check, Copy } from 'lucide-react';
4
4
  import * as React from 'react';
5
5
 
6
+ import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
6
7
  import { cn } from '../lib/utils';
7
8
  import { Button } from './button';
8
9
 
@@ -39,6 +40,7 @@ const CopyButton = React.forwardRef<HTMLButtonElement, CopyButtonProps>(
39
40
  iconClassName = 'h-4 w-4',
40
41
  ...props
41
42
  }, ref) => {
43
+ const t = useTypedT<I18nTranslations>()
42
44
  const [copied, setCopied] = React.useState(false)
43
45
 
44
46
  const handleCopy = async () => {
@@ -53,6 +55,8 @@ const CopyButton = React.forwardRef<HTMLButtonElement, CopyButtonProps>(
53
55
  }
54
56
 
55
57
  const hasLabel = !!children
58
+ const copiedText = t('ui.actions.copied')
59
+ const copyLabel = t('ui.actions.copyToClipboard')
56
60
 
57
61
  return (
58
62
  <Button
@@ -62,7 +66,7 @@ const CopyButton = React.forwardRef<HTMLButtonElement, CopyButtonProps>(
62
66
  variant={variant}
63
67
  onClick={handleCopy}
64
68
  className={cn('shrink-0', className)}
65
- aria-label={copied ? 'Copied' : 'Copy to clipboard'}
69
+ aria-label={copied ? copiedText : copyLabel}
66
70
  {...props}
67
71
  >
68
72
  {copied ? (
@@ -70,7 +74,7 @@ const CopyButton = React.forwardRef<HTMLButtonElement, CopyButtonProps>(
70
74
  ) : (
71
75
  <Copy className={iconClassName} />
72
76
  )}
73
- {hasLabel && <span className={copied ? 'text-green-500' : undefined}>{copied ? 'Copied' : children}</span>}
77
+ {hasLabel && <span className={copied ? 'text-green-500' : undefined}>{copied ? copiedText : children}</span>}
74
78
  </Button>
75
79
  )
76
80
  }
@@ -62,6 +62,7 @@ export { Calendar, CalendarDayButton } from './calendar';
62
62
  export { DatePicker, DateRangePicker } from './calendar';
63
63
  export type { DatePickerProps, DateRangePickerProps, DateRange } from './calendar';
64
64
  export { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from './carousel';
65
+ export type { CarouselApi } from './carousel';
65
66
  export { TokenIcon, getAllTokenSymbols, searchTokens, getTokensByCategory } from './token-icon';
66
67
  export type { TokenIconProps, TokenSymbol, TokenCategory } from './token-icon';
67
68
 
@@ -17,7 +17,7 @@ import { cva } from 'class-variance-authority';
17
17
  import { Check, ChevronsUpDown, Loader2, X, XCircle } from 'lucide-react';
18
18
  import * as React from 'react';
19
19
 
20
-
20
+ import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
21
21
  import { Badge } from '../badge';
22
22
  import { Button } from '../button';
23
23
  import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator } from '../command';
@@ -173,10 +173,10 @@ export const MultiSelectProAsync = React.forwardRef<MultiSelectProAsyncRef, Mult
173
173
  searchValue,
174
174
  onSearchChange,
175
175
  isLoading = false,
176
- placeholder = "Select options",
177
- searchPlaceholder = "Search...",
178
- emptyText = "No results found.",
179
- loadingText = "Loading...",
176
+ placeholder,
177
+ searchPlaceholder,
178
+ emptyText,
179
+ loadingText,
180
180
  variant = "default",
181
181
  animation = 0,
182
182
  animationConfig,
@@ -197,6 +197,7 @@ export const MultiSelectProAsync = React.forwardRef<MultiSelectProAsyncRef, Mult
197
197
  },
198
198
  ref
199
199
  ) => {
200
+ const t = useTypedT<I18nTranslations>()
200
201
  const [open, setOpen] = React.useState(false)
201
202
  const [selectedValues, setSelectedValues] = React.useState<string[]>(defaultValue)
202
203
  const buttonRef = React.useRef<HTMLButtonElement>(null)
@@ -204,6 +205,18 @@ export const MultiSelectProAsync = React.forwardRef<MultiSelectProAsyncRef, Mult
204
205
  // Cache selected options to persist them when they disappear from current options
205
206
  const [selectedOptionsCache, setSelectedOptionsCache] = React.useState<Map<string, MultiSelectProAsyncOption>>(new Map())
206
207
 
208
+ // Prepare translations
209
+ const translations = React.useMemo(() => ({
210
+ placeholder: placeholder ?? t('ui.select.placeholder'),
211
+ search: searchPlaceholder ?? t('ui.select.search'),
212
+ selectAll: t('ui.select.selectAll'),
213
+ clearAll: t('ui.select.clearAll'),
214
+ noResults: emptyText ?? t('ui.select.noResults'),
215
+ loading: loadingText ?? t('ui.select.loading'),
216
+ moreItems: (count: number) => t('ui.select.moreItems', { count }),
217
+ remove: (label: string) => t('ui.actions.remove', { item: label }),
218
+ }), [placeholder, searchPlaceholder, emptyText, loadingText, t])
219
+
207
220
  // Process options
208
221
  const flatOptions = React.useMemo(() => {
209
222
  const flat = flattenOptions(options)
@@ -373,7 +386,7 @@ export const MultiSelectProAsync = React.forwardRef<MultiSelectProAsyncRef, Mult
373
386
  e.stopPropagation()
374
387
  toggleOption(option.value)
375
388
  }}
376
- aria-label={`Remove ${option.label}`}
389
+ aria-label={translations.remove(option.label)}
377
390
  >
378
391
  <X className="h-3 w-3" />
379
392
  </button>
@@ -385,7 +398,7 @@ export const MultiSelectProAsync = React.forwardRef<MultiSelectProAsyncRef, Mult
385
398
  // Display value
386
399
  const displayValue = React.useMemo(() => {
387
400
  if (selectedOptions.length === 0) {
388
- return <span className="text-muted-foreground">{placeholder}</span>
401
+ return <span className="text-muted-foreground">{translations.placeholder}</span>
389
402
  }
390
403
 
391
404
  const displayed = selectedOptions.slice(0, maxCount)
@@ -396,12 +409,12 @@ export const MultiSelectProAsync = React.forwardRef<MultiSelectProAsyncRef, Mult
396
409
  {displayed.map((option, index) => renderBadge(option, index))}
397
410
  {remaining > 0 && (
398
411
  <Badge variant="outline" className="text-xs">
399
- +{remaining} more
412
+ {translations.moreItems(remaining)}
400
413
  </Badge>
401
414
  )}
402
415
  </div>
403
416
  )
404
- }, [selectedOptions, maxCount, placeholder, singleLine, variant, disabled, animConfig])
417
+ }, [selectedOptions, maxCount, translations, singleLine, variant, disabled, animConfig])
405
418
 
406
419
  // Render options
407
420
  const renderOptions = () => {
@@ -535,7 +548,7 @@ export const MultiSelectProAsync = React.forwardRef<MultiSelectProAsyncRef, Mult
535
548
  handleClearAll()
536
549
  }}
537
550
  className="rounded-full hover:bg-muted-foreground/20 focus:outline-none focus:ring-2 focus:ring-ring"
538
- aria-label="Clear all selections"
551
+ aria-label={translations.clearAll}
539
552
  >
540
553
  <XCircle className="h-4 w-4 shrink-0 opacity-50" />
541
554
  </button>
@@ -558,7 +571,7 @@ export const MultiSelectProAsync = React.forwardRef<MultiSelectProAsyncRef, Mult
558
571
  <Command shouldFilter={false}>
559
572
  {searchable && (
560
573
  <CommandInput
561
- placeholder={searchPlaceholder}
574
+ placeholder={translations.search}
562
575
  value={searchValue}
563
576
  onValueChange={onSearchChange}
564
577
  />
@@ -571,7 +584,7 @@ export const MultiSelectProAsync = React.forwardRef<MultiSelectProAsyncRef, Mult
571
584
  onSelect={handleSelectAll}
572
585
  className="cursor-pointer justify-center font-medium"
573
586
  >
574
- Select All
587
+ {translations.selectAll}
575
588
  </CommandItem>
576
589
  </CommandGroup>
577
590
  <Separator />
@@ -581,10 +594,10 @@ export const MultiSelectProAsync = React.forwardRef<MultiSelectProAsyncRef, Mult
581
594
  {isLoading ? (
582
595
  <div className="py-6 text-center text-sm">
583
596
  <Loader2 className="h-4 w-4 animate-spin mx-auto mb-2" />
584
- {loadingText}
597
+ {translations.loading}
585
598
  </div>
586
599
  ) : options.length === 0 ? (
587
- <CommandEmpty>{emptyText}</CommandEmpty>
600
+ <CommandEmpty>{translations.noResults}</CommandEmpty>
588
601
  ) : (
589
602
  renderOptions()
590
603
  )}
@@ -4,6 +4,7 @@ import { cva, type VariantProps } from 'class-variance-authority';
4
4
  import { Check, ChevronsUpDown, X, XCircle } from 'lucide-react';
5
5
  import * as React from 'react';
6
6
 
7
+ import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
7
8
  import { Badge } from '../badge';
8
9
  import { Button } from '../button';
9
10
  import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator } from '../command';
@@ -150,7 +151,7 @@ export const MultiSelectPro = React.forwardRef<MultiSelectProRef, MultiSelectPro
150
151
  options,
151
152
  onValueChange,
152
153
  defaultValue = [],
153
- placeholder = "Select options",
154
+ placeholder,
154
155
  variant = "default",
155
156
  animation = 0,
156
157
  animationConfig,
@@ -173,6 +174,7 @@ export const MultiSelectPro = React.forwardRef<MultiSelectProRef, MultiSelectPro
173
174
  },
174
175
  ref
175
176
  ) => {
177
+ const t = useTypedT<I18nTranslations>()
176
178
  const [open, setOpen] = React.useState(false)
177
179
  const [selectedValues, setSelectedValues] = React.useState<string[]>(defaultValue)
178
180
  const [search, setSearch] = React.useState("")
@@ -181,6 +183,17 @@ export const MultiSelectPro = React.forwardRef<MultiSelectProRef, MultiSelectPro
181
183
  // Cache selected options to persist them when they disappear from current options
182
184
  const [selectedOptionsCache, setSelectedOptionsCache] = React.useState<Map<string, MultiSelectProOption>>(new Map())
183
185
 
186
+ // Prepare translations
187
+ const translations = React.useMemo(() => ({
188
+ placeholder: placeholder ?? t('ui.select.placeholder'),
189
+ search: t('ui.select.search'),
190
+ selectAll: t('ui.select.selectAll'),
191
+ clearAll: t('ui.select.clearAll'),
192
+ noResults: t('ui.select.noResults'),
193
+ moreItems: (count: number) => t('ui.select.moreItems', { count }),
194
+ remove: (label: string) => t('ui.actions.remove', { item: label }),
195
+ }), [placeholder, t])
196
+
184
197
  // Process options
185
198
  const flatOptions = React.useMemo(() => {
186
199
  const flat = flattenOptions(options)
@@ -389,7 +402,7 @@ export const MultiSelectPro = React.forwardRef<MultiSelectProRef, MultiSelectPro
389
402
  e.stopPropagation()
390
403
  toggleOption(option.value)
391
404
  }}
392
- aria-label={`Remove ${option.label}`}
405
+ aria-label={translations.remove(option.label)}
393
406
  >
394
407
  <X className="h-3 w-3" />
395
408
  </button>
@@ -401,7 +414,7 @@ export const MultiSelectPro = React.forwardRef<MultiSelectProRef, MultiSelectPro
401
414
  // Display value
402
415
  const displayValue = React.useMemo(() => {
403
416
  if (selectedOptions.length === 0) {
404
- return <span className="text-muted-foreground">{placeholder}</span>
417
+ return <span className="text-muted-foreground">{translations.placeholder}</span>
405
418
  }
406
419
 
407
420
  const displayed = selectedOptions.slice(0, maxCount)
@@ -412,12 +425,12 @@ export const MultiSelectPro = React.forwardRef<MultiSelectProRef, MultiSelectPro
412
425
  {displayed.map((option, index) => renderBadge(option, index))}
413
426
  {remaining > 0 && (
414
427
  <Badge variant="outline" className="text-xs">
415
- +{remaining} more
428
+ {translations.moreItems(remaining)}
416
429
  </Badge>
417
430
  )}
418
431
  </div>
419
432
  )
420
- }, [selectedOptions, maxCount, placeholder, singleLine, variant, disabled, animConfig])
433
+ }, [selectedOptions, maxCount, translations, singleLine, variant, disabled, animConfig])
421
434
 
422
435
  // Render options
423
436
  const renderOptions = () => {
@@ -551,7 +564,7 @@ export const MultiSelectPro = React.forwardRef<MultiSelectProRef, MultiSelectPro
551
564
  handleClearAll()
552
565
  }}
553
566
  className="rounded-full hover:bg-muted-foreground/20 focus:outline-none focus:ring-2 focus:ring-ring"
554
- aria-label="Clear all selections"
567
+ aria-label={translations.clearAll}
555
568
  >
556
569
  <XCircle className="h-4 w-4 shrink-0 opacity-50" />
557
570
  </button>
@@ -574,7 +587,7 @@ export const MultiSelectPro = React.forwardRef<MultiSelectProRef, MultiSelectPro
574
587
  <Command shouldFilter={false}>
575
588
  {searchable && (
576
589
  <CommandInput
577
- placeholder="Search..."
590
+ placeholder={translations.search}
578
591
  value={search}
579
592
  onValueChange={setSearch}
580
593
  />
@@ -587,7 +600,7 @@ export const MultiSelectPro = React.forwardRef<MultiSelectProRef, MultiSelectPro
587
600
  onSelect={handleSelectAll}
588
601
  className="cursor-pointer justify-center font-medium"
589
602
  >
590
- Select All
603
+ {translations.selectAll}
591
604
  </CommandItem>
592
605
  </CommandGroup>
593
606
  <Separator />
@@ -596,7 +609,7 @@ export const MultiSelectPro = React.forwardRef<MultiSelectProRef, MultiSelectPro
596
609
 
597
610
  {filteredOptions.length === 0 ? (
598
611
  <CommandEmpty>
599
- {emptyIndicator || "No results found."}
612
+ {emptyIndicator || translations.noResults}
600
613
  </CommandEmpty>
601
614
  ) : (
602
615
  renderOptions()
@@ -3,6 +3,7 @@
3
3
  import { Check, ChevronsUpDown, X } from 'lucide-react';
4
4
  import * as React from 'react';
5
5
 
6
+ import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
6
7
  import { cn } from '../lib/utils';
7
8
  import { Badge } from './badge';
8
9
  import { Button } from './button';
@@ -34,17 +35,23 @@ export function MultiSelect({
34
35
  options,
35
36
  value = [],
36
37
  onChange,
37
- placeholder = "Select options...",
38
- searchPlaceholder = "Search...",
39
- emptyText = "No results found.",
38
+ placeholder,
39
+ searchPlaceholder,
40
+ emptyText,
40
41
  className,
41
42
  disabled = false,
42
43
  maxDisplay = 3,
43
44
  }: MultiSelectProps) {
45
+ const t = useTypedT<I18nTranslations>()
44
46
  const [open, setOpen] = React.useState(false)
45
47
  const [search, setSearch] = React.useState("")
46
48
  const scrollRef = React.useRef<HTMLDivElement>(null)
47
49
 
50
+ // Resolve translations
51
+ const resolvedPlaceholder = placeholder ?? t('ui.select.placeholder')
52
+ const resolvedSearchPlaceholder = searchPlaceholder ?? t('ui.select.search')
53
+ const resolvedEmptyText = emptyText ?? t('ui.select.noResults')
54
+
48
55
  React.useEffect(() => {
49
56
  if (scrollRef.current && open) {
50
57
  const el = scrollRef.current
@@ -88,7 +95,7 @@ export function MultiSelect({
88
95
 
89
96
  const displayValue = React.useMemo(() => {
90
97
  if (selectedOptions.length === 0) {
91
- return <span className="text-muted-foreground">{placeholder}</span>
98
+ return <span className="text-muted-foreground">{resolvedPlaceholder}</span>
92
99
  }
93
100
 
94
101
  const displayed = selectedOptions.slice(0, maxDisplay)
@@ -107,6 +114,7 @@ export function MultiSelect({
107
114
  className="ml-1 rounded-full hover:bg-muted-foreground/20"
108
115
  onClick={(e) => handleRemove(option.value, e)}
109
116
  disabled={disabled}
117
+ aria-label={t('ui.actions.remove', { item: option.label })}
110
118
  >
111
119
  <X className="h-3 w-3" />
112
120
  </button>
@@ -114,12 +122,12 @@ export function MultiSelect({
114
122
  ))}
115
123
  {remaining > 0 && (
116
124
  <Badge variant="outline" className="text-xs">
117
- +{remaining} more
125
+ {t('ui.select.moreItems', { count: remaining })}
118
126
  </Badge>
119
127
  )}
120
128
  </div>
121
129
  )
122
- }, [selectedOptions, maxDisplay, placeholder, disabled])
130
+ }, [selectedOptions, maxDisplay, resolvedPlaceholder, disabled, t])
123
131
 
124
132
  return (
125
133
  <Popover
@@ -151,7 +159,7 @@ export function MultiSelect({
151
159
  <PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0" align="start">
152
160
  <Command shouldFilter={false} className="flex flex-col">
153
161
  <CommandInput
154
- placeholder={searchPlaceholder}
162
+ placeholder={resolvedSearchPlaceholder}
155
163
  className="shrink-0"
156
164
  value={search}
157
165
  onValueChange={setSearch}
@@ -170,7 +178,7 @@ export function MultiSelect({
170
178
  >
171
179
  <CommandList className="!max-h-none !overflow-visible" style={{ pointerEvents: 'auto' }}>
172
180
  {filteredOptions.length === 0 ? (
173
- <CommandEmpty>{emptyText}</CommandEmpty>
181
+ <CommandEmpty>{resolvedEmptyText}</CommandEmpty>
174
182
  ) : (
175
183
  <CommandGroup className="!overflow-visible" style={{ pointerEvents: 'auto' }}>
176
184
  {filteredOptions.map((option) => {
@@ -6,6 +6,7 @@ import {
6
6
  import { ChevronDown, Search } from 'lucide-react';
7
7
  import * as React from 'react';
8
8
 
9
+ import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
9
10
  import { Button } from './button';
10
11
  import { Input } from './input';
11
12
  import { cn } from '../lib';
@@ -58,16 +59,23 @@ const PhoneInput = React.forwardRef<HTMLInputElement, PhoneInputProps>(
58
59
  value = '',
59
60
  onChange,
60
61
  defaultCountry = 'US',
61
- placeholder = "Enter phone number",
62
+ placeholder,
62
63
  disabled = false,
63
64
  ...props
64
65
  }, ref) => {
66
+ const t = useTypedT<I18nTranslations>()
65
67
  const [selectedCountry, setSelectedCountry] = React.useState<CountryCode>(defaultCountry)
66
68
  const [inputValue, setInputValue] = React.useState('')
67
69
  const [isDropdownOpen, setIsDropdownOpen] = React.useState(false)
68
70
  const [searchQuery, setSearchQuery] = React.useState('')
69
71
  const [highlightedIndex, setHighlightedIndex] = React.useState(-1)
70
72
 
73
+ // Prepare translations
74
+ const translations = React.useMemo(() => ({
75
+ searchCountries: t('ui.phone.searchCountries'),
76
+ noCountries: t('ui.phone.noCountries'),
77
+ }), [t])
78
+
71
79
  // Find country data
72
80
  const currentCountry = COUNTRIES.find(c => c.code === selectedCountry) || COUNTRIES[0]!
73
81
 
@@ -218,7 +226,7 @@ const PhoneInput = React.forwardRef<HTMLInputElement, PhoneInputProps>(
218
226
  <div className="relative">
219
227
  <Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
220
228
  <Input
221
- placeholder="Search countries..."
229
+ placeholder={translations.searchCountries}
222
230
  value={searchQuery}
223
231
  onChange={(e) => setSearchQuery(e.target.value)}
224
232
  className="pl-8 h-8"
@@ -231,7 +239,7 @@ const PhoneInput = React.forwardRef<HTMLInputElement, PhoneInputProps>(
231
239
  <div className="max-h-60 overflow-y-auto">
232
240
  {filteredCountries.length === 0 ? (
233
241
  <div className="p-4 text-sm text-muted-foreground text-center">
234
- No countries found
242
+ {translations.noCountries}
235
243
  </div>
236
244
  ) : (
237
245
  filteredCountries.map((country, index) => (
@@ -10,6 +10,7 @@
10
10
  import { Loader2 } from 'lucide-react';
11
11
  import React from 'react';
12
12
 
13
+ import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
13
14
  import { cn } from '../lib/utils';
14
15
  import { Spinner } from './spinner';
15
16
 
@@ -102,6 +103,8 @@ export function Preloader({
102
103
  className,
103
104
  spinnerClassName,
104
105
  }: PreloaderProps) {
106
+ const t = useTypedT<I18nTranslations>()
107
+ const loadingLabel = text || t('ui.states.loading')
105
108
  const spinnerSize = sizeMap[size];
106
109
 
107
110
  // Fullscreen variant
@@ -124,7 +127,7 @@ export function Preloader({
124
127
  className
125
128
  )}
126
129
  role="status"
127
- aria-label={text || 'Loading'}
130
+ aria-label={loadingLabel}
128
131
  aria-live="polite"
129
132
  >
130
133
  <div className="flex flex-col items-center gap-4 animate-in fade-in duration-300">
@@ -176,7 +179,7 @@ export function Preloader({
176
179
  className
177
180
  )}
178
181
  role="status"
179
- aria-label={text || 'Loading'}
182
+ aria-label={loadingLabel}
180
183
  aria-live="polite"
181
184
  >
182
185
  <Loader2
@@ -1,12 +1,16 @@
1
1
  import { Loader2Icon } from 'lucide-react';
2
2
 
3
+ import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
3
4
  import { cn } from '../lib/utils';
4
5
 
5
6
  function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
7
+ const t = useTypedT<I18nTranslations>()
8
+ const loadingLabel = t('ui.states.loading')
9
+
6
10
  return (
7
11
  <Loader2Icon
8
12
  role="status"
9
- aria-label="Loading"
13
+ aria-label={loadingLabel}
10
14
  className={cn("size-4 animate-spin", className)}
11
15
  {...props}
12
16
  />
@@ -6,6 +6,7 @@ import * as React from 'react';
6
6
  import * as TabsPrimitive from '@radix-ui/react-tabs';
7
7
 
8
8
  import { useIsMobile } from '../hooks';
9
+ import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
9
10
  import { cn } from '../lib/utils';
10
11
  import { Button } from './button';
11
12
  import { ScrollArea, ScrollBar } from './scroll-area';
@@ -40,7 +41,9 @@ interface TabsProps extends React.ComponentPropsWithoutRef<typeof TabsPrimitive.
40
41
  const Tabs = React.forwardRef<
41
42
  React.ElementRef<typeof TabsPrimitive.Root>,
42
43
  TabsProps
43
- >(({ mobileSheet = false, mobileSheetTitle = "Navigation", mobileTitleText, sticky = false, children, ...props }, ref) => {
44
+ >(({ mobileSheet = false, mobileSheetTitle, mobileTitleText, sticky = false, children, ...props }, ref) => {
45
+ const t = useTypedT<I18nTranslations>()
46
+ const resolvedMobileSheetTitle = mobileSheetTitle ?? t('ui.navigation.title')
44
47
  const isMobile = useIsMobile()
45
48
  const [open, setOpen] = React.useState(false)
46
49
 
@@ -109,7 +112,7 @@ const Tabs = React.forwardRef<
109
112
  </SheetTrigger>
110
113
  <SheetContent side="right" className="w-[280px] sm:w-[350px]">
111
114
  <SheetHeader>
112
- <SheetTitle>{mobileSheetTitle}</SheetTitle>
115
+ <SheetTitle>{resolvedMobileSheetTitle}</SheetTitle>
113
116
  </SheetHeader>
114
117
  <nav className="flex flex-col gap-2 mt-6">
115
118
  <TabsPrimitive.List className="flex flex-col gap-2">