@open-mercato/ui 0.6.6-develop.6519.1.bdc91fb755 → 0.6.6-develop.6520.1.55df6df792

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.
@@ -15,6 +15,7 @@ import {
15
15
  SelectValue,
16
16
  } from '../../primitives/select'
17
17
  import { CUSTOM_FIELD_KINDS } from '@open-mercato/shared/modules/entities/kinds'
18
+ import { useOptionalT } from '@open-mercato/shared/lib/i18n/context'
18
19
  import { FieldRegistry } from '../fields/registry'
19
20
  import { slugify } from '@open-mercato/shared/lib/slugify'
20
21
  import { useConfirmDialog } from '../confirm-dialog'
@@ -70,11 +71,6 @@ export type FieldDefinitionsEditorProps = {
70
71
  translate?: (key: string, fallback: string) => string
71
72
  }
72
73
 
73
- const DEFAULT_KIND_OPTIONS = CUSTOM_FIELD_KINDS.map((k) => ({
74
- value: k,
75
- label: k.charAt(0).toUpperCase() + k.slice(1),
76
- }))
77
-
78
74
  const DEFAULT_VALUE_NONE = '__open_mercato_no_default__'
79
75
 
80
76
  function getDefaultValueNoneOptionValue(options: CustomFieldOptionDto[]): string {
@@ -158,14 +154,10 @@ export function FieldDefinitionsEditor({
158
154
  definitions,
159
155
  errors,
160
156
  deletedKeys,
161
- kindOptions = DEFAULT_KIND_OPTIONS,
157
+ kindOptions,
162
158
  orderNotice,
163
- infoNote = (
164
- <div className="text-xs text-muted-foreground mt-2">
165
- Supported kinds: text, multiline, integer, float, boolean, select (with options/optionsUrl), currency (fixed currencies list), relation (with related entity and options URL).
166
- </div>
167
- ),
168
- addButtonLabel = 'Add Field',
159
+ infoNote,
160
+ addButtonLabel,
169
161
  fieldsets = [],
170
162
  activeFieldset = null,
171
163
  onActiveFieldsetChange,
@@ -187,7 +179,33 @@ export function FieldDefinitionsEditor({
187
179
  const { confirm, ConfirmDialogElement } = useConfirmDialog()
188
180
  const dragIndex = React.useRef<number | null>(null)
189
181
  const hasFieldsets = fieldsets.length > 0
190
- const t = React.useCallback((key: string, fallback: string) => (translate ? translate(key, fallback) : fallback), [translate])
182
+ const contextT = useOptionalT()
183
+ const t = React.useCallback(
184
+ (key: string, fallback: string) => {
185
+ if (translate) return translate(key, fallback)
186
+ if (contextT) return contextT(key, fallback)
187
+ return fallback
188
+ },
189
+ [translate, contextT],
190
+ )
191
+ const resolvedKindOptions = React.useMemo(
192
+ () =>
193
+ kindOptions ??
194
+ CUSTOM_FIELD_KINDS.map((kind) => ({
195
+ value: kind,
196
+ label: t(`entities.customFields.editor.kindOption.${kind}`, kind.charAt(0).toUpperCase() + kind.slice(1)),
197
+ })),
198
+ [kindOptions, t],
199
+ )
200
+ const resolvedInfoNote = infoNote !== undefined ? infoNote : (
201
+ <div className="text-xs text-muted-foreground mt-2">
202
+ {t(
203
+ 'entities.customFields.editor.supportedKindsNote',
204
+ 'Supported kinds: text, multiline, integer, float, boolean, select (with options/optionsUrl), currency (fixed currencies list), relation (with related entity and options URL).',
205
+ )}
206
+ </div>
207
+ )
208
+ const resolvedAddButtonLabel = addButtonLabel ?? t('entities.customFields.editor.addField', 'Add Field')
191
209
  const resolvedActiveFieldset = React.useMemo(() => {
192
210
  if (!hasFieldsets) return activeFieldset ?? null
193
211
  if (activeFieldset === null) return null
@@ -237,7 +255,7 @@ export function FieldDefinitionsEditor({
237
255
  const handleAddFieldset = () => {
238
256
  if (!onFieldsetsChange) return
239
257
  const code = ensureUniqueFieldsetCode(`fieldset_${fieldsets.length + 1}`, fieldsets)
240
- const nextFieldsets = [...fieldsets, { code, label: 'New fieldset', icon: 'layers' }]
258
+ const nextFieldsets = [...fieldsets, { code, label: t('entities.customFields.editor.newFieldsetLabel', 'New fieldset'), icon: 'layers' }]
241
259
  onFieldsetsChange(nextFieldsets)
242
260
  onActiveFieldsetChange?.(code)
243
261
  }
@@ -246,8 +264,8 @@ export function FieldDefinitionsEditor({
246
264
  if (!onFieldsetsChange) return
247
265
  if (!resolvedActiveFieldset) return
248
266
  const confirmed = await confirm({
249
- title: `Delete fieldset "${resolvedActiveFieldset}"?`,
250
- text: 'This will move its fields to Unassigned.',
267
+ title: t('entities.customFields.editor.deleteFieldsetTitle', 'Delete fieldset "{code}"?').replace('{code}', resolvedActiveFieldset),
268
+ text: t('entities.customFields.editor.deleteFieldsetText', 'This will move its fields to Unassigned.'),
251
269
  variant: 'destructive',
252
270
  })
253
271
  if (!confirmed) return
@@ -309,13 +327,13 @@ export function FieldDefinitionsEditor({
309
327
  {hasFieldsets ? (
310
328
  <div className="rounded border bg-card p-3 space-y-3">
311
329
  <div className="flex flex-wrap items-center gap-2">
312
- <label className="text-xs font-medium text-muted-foreground">Fieldset</label>
330
+ <label className="text-xs font-medium text-muted-foreground">{t('entities.customFields.fieldsetSelectorLabel', 'Fieldset')}</label>
313
331
  <Select
314
332
  value={resolvedActiveFieldset || undefined}
315
333
  onValueChange={(value) => handleActiveFieldsetChange(value ?? '')}
316
334
  >
317
335
  <SelectTrigger size="sm" className="w-auto min-w-[10rem]">
318
- <SelectValue placeholder="Unassigned fields" />
336
+ <SelectValue placeholder={t('entities.customFields.editor.unassignedFields', 'Unassigned fields')} />
319
337
  </SelectTrigger>
320
338
  <SelectContent>
321
339
  {fieldsets.map((fs) => (
@@ -331,7 +349,7 @@ export function FieldDefinitionsEditor({
331
349
  onClick={handleAddFieldset}
332
350
  className="text-xs"
333
351
  >
334
- <Plus className="h-3.5 w-3.5" /> Add
352
+ <Plus className="h-3.5 w-3.5" /> {t('entities.customFields.editor.add', 'Add')}
335
353
  </Button>
336
354
  <Button
337
355
  variant="outline"
@@ -340,12 +358,12 @@ export function FieldDefinitionsEditor({
340
358
  disabled={!resolvedActiveFieldset}
341
359
  className="text-xs"
342
360
  >
343
- <Trash2 className="h-3.5 w-3.5" /> Delete
361
+ <Trash2 className="h-3.5 w-3.5" /> {t('entities.customFields.editor.delete', 'Delete')}
344
362
  </Button>
345
363
  </div>
346
364
  {resolvedActiveFieldset && activeFieldsetConfig ? (
347
365
  <div className="grid grid-cols-1 md:grid-cols-4 gap-3">
348
- <FormField label="Code">
366
+ <FormField label={t('entities.customFields.editor.code', 'Code')}>
349
367
  <Input
350
368
  size="sm"
351
369
  inputClassName="font-mono"
@@ -353,14 +371,14 @@ export function FieldDefinitionsEditor({
353
371
  onChange={(event) => handleFieldsetCodeInput(activeFieldsetConfig.code, event.target.value)}
354
372
  />
355
373
  </FormField>
356
- <FormField label="Label">
374
+ <FormField label={t('entities.customFields.editor.label', 'Label')}>
357
375
  <Input
358
376
  size="sm"
359
377
  value={activeFieldsetConfig.label}
360
378
  onChange={(event) => handleFieldsetPatch(activeFieldsetConfig.code, { label: event.target.value })}
361
379
  />
362
380
  </FormField>
363
- <FormField label="Icon">
381
+ <FormField label={t('entities.customFields.editor.icon', 'Icon')}>
364
382
  <Select
365
383
  value={activeFieldsetConfig.icon || undefined}
366
384
  onValueChange={(value) =>
@@ -370,18 +388,18 @@ export function FieldDefinitionsEditor({
370
388
  }
371
389
  >
372
390
  <SelectTrigger size="sm">
373
- <SelectValue placeholder="Default" />
391
+ <SelectValue placeholder={t('entities.customFields.editor.defaultOption', 'Default')} />
374
392
  </SelectTrigger>
375
393
  <SelectContent>
376
394
  {FIELDSET_ICON_OPTIONS.map((option) => (
377
395
  <SelectItem key={option.value} value={option.value}>
378
- {option.label}
396
+ {t(`entities.customFields.editor.iconOption.${option.value}`, option.label)}
379
397
  </SelectItem>
380
398
  ))}
381
399
  </SelectContent>
382
400
  </Select>
383
401
  </FormField>
384
- <FormField label="Description">
402
+ <FormField label={t('entities.customFields.editor.description', 'Description')}>
385
403
  <Input
386
404
  size="sm"
387
405
  value={activeFieldsetConfig.description ?? ''}
@@ -396,18 +414,18 @@ export function FieldDefinitionsEditor({
396
414
  disabled={!canToggleSingleFieldset}
397
415
  checked={singleFieldsetChecked}
398
416
  onCheckedChange={(checked) => onSingleFieldsetPerRecordChange?.(checked === true)}
399
- label="Single fieldset per entity"
417
+ label={t('entities.customFields.editor.singleFieldsetPerEntity', 'Single fieldset per entity')}
400
418
  containerClassName="text-xs"
401
419
  contentClassName="gap-1 [&_label]:text-xs [&_label]:font-normal"
402
420
  />
403
421
  {!canToggleSingleFieldset ? (
404
- <span className="text-xs text-muted-foreground">(add at least two fieldsets to toggle)</span>
422
+ <span className="text-xs text-muted-foreground">{t('entities.customFields.editor.singleFieldsetToggleHint', '(add at least two fieldsets to toggle)')}</span>
405
423
  ) : null}
406
424
  </div>
407
425
  </div>
408
426
  ) : (
409
427
  <div className="rounded border border-dashed bg-muted/30 p-4 text-sm text-muted-foreground flex flex-col gap-3">
410
- <div>No fieldsets defined yet. Fieldsets let you group custom fields for different variants of the same entity (e.g., Fashion vs. Sport products).</div>
428
+ <div>{t('entities.customFields.editor.noFieldsets', 'No fieldsets defined yet. Fieldsets let you group custom fields for different variants of the same entity (e.g., Fashion vs. Sport products).')}</div>
411
429
  <div>
412
430
  <Button
413
431
  variant="outline"
@@ -415,7 +433,7 @@ export function FieldDefinitionsEditor({
415
433
  onClick={handleAddFieldset}
416
434
  >
417
435
  <Plus className="h-4 w-4" />
418
- Add first fieldset
436
+ {t('entities.customFields.editor.addFirstFieldset', 'Add first fieldset')}
419
437
  </Button>
420
438
  </div>
421
439
  </div>
@@ -423,7 +441,9 @@ export function FieldDefinitionsEditor({
423
441
  {orderNotice?.dirty && (
424
442
  <div className="sticky top-0 z-sticky -mt-1 -mb-1">
425
443
  <div className="inline-flex items-center gap-2 text-xs px-2 py-1 rounded border border-status-warning-border bg-status-warning-bg text-status-warning-text shadow-sm">
426
- {orderNotice?.saving ? 'Saving order…' : (orderNotice?.message ?? 'Reordered — saving soon')}
444
+ {orderNotice?.saving
445
+ ? t('entities.customFields.editor.savingOrder', 'Saving order…')
446
+ : (orderNotice?.message ?? t('entities.customFields.editor.reorderedSavingSoon', 'Reordered — saving soon'))}
427
447
  </div>
428
448
  </div>
429
449
  )}
@@ -462,7 +482,7 @@ export function FieldDefinitionsEditor({
462
482
  <FieldDefinitionCard
463
483
  definition={definition}
464
484
  error={errors?.[index]}
465
- kindOptions={kindOptions}
485
+ kindOptions={resolvedKindOptions}
466
486
  onChange={(next) => onDefinitionChange(index, next)}
467
487
  onRemove={() => onRemoveField(index)}
468
488
  allowFieldsetSelection={hasFieldsets}
@@ -482,12 +502,12 @@ export function FieldDefinitionsEditor({
482
502
  size="sm"
483
503
  onClick={onAddField}
484
504
  >
485
- <Plus className="h-4 w-4" /> {addButtonLabel}
505
+ <Plus className="h-4 w-4" /> {resolvedAddButtonLabel}
486
506
  </Button>
487
- {infoNote}
507
+ {resolvedInfoNote}
488
508
  {deletedKeys && deletedKeys.length > 0 && onRestoreField ? (
489
509
  <div className="text-xs text-muted-foreground mt-2">
490
- Restore deleted fields:{' '}
510
+ {t('entities.customFields.editor.restoreDeletedFields', 'Restore deleted fields:')}{' '}
491
511
  {deletedKeys.map((key, idx) => (
492
512
  <span key={key}>
493
513
  <Button
@@ -669,7 +689,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
669
689
  if (!currentFieldsetValue) return
670
690
  const code = slugifyFieldsetCode(groupDraft.code || '')
671
691
  if (!code) {
672
- setGroupError('Group code is required.')
692
+ setGroupError(t('entities.customFields.editor.groupCodeRequired', 'Group code is required.'))
673
693
  return
674
694
  }
675
695
  const group: FieldsetGroup = {
@@ -725,7 +745,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
725
745
  const value = optionValueDraft.trim()
726
746
  const label = optionLabelDraft.trim()
727
747
  if (!value) {
728
- setOptionFormError('Value is required')
748
+ setOptionFormError(t('entities.customFields.editor.valueRequired', 'Value is required'))
729
749
  return
730
750
  }
731
751
  setOptionFormError(null)
@@ -749,30 +769,30 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
749
769
  <span className="inline-flex items-center justify-center h-5 w-5 rounded hover:bg-muted cursor-grab active:cursor-grabbing">
750
770
  <GripVertical className="h-4 w-4 opacity-70" />
751
771
  </span>
752
- Drag to reorder
772
+ {t('entities.customFields.editor.dragToReorder', 'Drag to reorder')}
753
773
  </div>
754
774
  <div className="flex items-center gap-3">
755
775
  <CheckboxField
756
776
  size="sm"
757
777
  checked={local.isActive !== false}
758
778
  onCheckedChange={(checked) => { apply({ isActive: checked === true }, true) }}
759
- label="Active"
779
+ label={t('entities.customFields.editor.active', 'Active')}
760
780
  containerClassName="text-sm"
761
781
  contentClassName="gap-1 [&_label]:text-sm [&_label]:font-normal"
762
782
  />
763
783
  {onTranslate && (
764
- <IconButton variant="outline" size="sm" onClick={onTranslate} aria-label="Translate field">
784
+ <IconButton variant="outline" size="sm" onClick={onTranslate} aria-label={t('entities.customFields.editor.translateField', 'Translate field')}>
765
785
  <Languages className="h-4 w-4" />
766
786
  </IconButton>
767
787
  )}
768
- <IconButton variant="outline" size="sm" onClick={onRemove} aria-label="Remove field">
788
+ <IconButton variant="outline" size="sm" onClick={onRemove} aria-label={t('entities.customFields.editor.removeField', 'Remove field')}>
769
789
  <Trash2 className="h-4 w-4" />
770
790
  </IconButton>
771
791
  </div>
772
792
  </div>
773
793
 
774
794
  <div className="mt-3 grid grid-cols-1 md:grid-cols-12 gap-3 items-center">
775
- <FormField label="Key" error={error?.key} className="md:col-span-6">
795
+ <FormField label={t('entities.customFields.editor.key', 'Key')} error={error?.key} className="md:col-span-6">
776
796
  <Input
777
797
  inputClassName="font-mono"
778
798
  placeholder="snake_case"
@@ -781,7 +801,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
781
801
  onBlur={commit}
782
802
  />
783
803
  </FormField>
784
- <FormField label="Kind" error={error?.kind} className="md:col-span-6">
804
+ <FormField label={t('entities.customFields.editor.kind', 'Kind')} error={error?.kind} className="md:col-span-6">
785
805
  <Select
786
806
  value={local.kind || undefined}
787
807
  onValueChange={(value) => { apply({ kind: value ?? '' }, true) }}
@@ -803,13 +823,13 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
803
823
  {allowFieldsetSelection ? (
804
824
  <div className="mt-3 grid grid-cols-1 md:grid-cols-2 gap-3">
805
825
  <div>
806
- <label className="text-xs">Assign to fieldset</label>
826
+ <label className="text-xs">{t('entities.customFields.editor.assignToFieldset', 'Assign to fieldset')}</label>
807
827
  <Select
808
828
  value={currentFieldsetValue || undefined}
809
829
  onValueChange={(value) => handleFieldsetSelect(value ?? '')}
810
830
  >
811
831
  <SelectTrigger>
812
- <SelectValue placeholder="Unassigned" />
832
+ <SelectValue placeholder={t('entities.customFields.editor.unassigned', 'Unassigned')} />
813
833
  </SelectTrigger>
814
834
  <SelectContent>
815
835
  {(fieldsets || []).map((fs) => (
@@ -823,7 +843,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
823
843
  {currentFieldsetValue ? (
824
844
  <div>
825
845
  <div className="flex items-center justify-between">
826
- <label className="text-xs">Group</label>
846
+ <label className="text-xs">{t('entities.customFields.editor.group', 'Group')}</label>
827
847
  </div>
828
848
  <div className="flex items-center gap-2 mt-1">
829
849
  <Select
@@ -831,7 +851,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
831
851
  onValueChange={(value) => handleGroupSelect(value ?? '')}
832
852
  >
833
853
  <SelectTrigger className="flex-1">
834
- <SelectValue placeholder="No group" />
854
+ <SelectValue placeholder={t('entities.customFields.editor.noGroup', 'No group')} />
835
855
  </SelectTrigger>
836
856
  <SelectContent>
837
857
  {groupOptions.map((group) => (
@@ -845,7 +865,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
845
865
  variant="outline"
846
866
  className="text-muted-foreground"
847
867
  onClick={() => handleOpenGroupDialog()}
848
- aria-label="Create group"
868
+ aria-label={t('entities.customFields.editor.createGroup', 'Create group')}
849
869
  >
850
870
  <Plus className="h-4 w-4" />
851
871
  </IconButton>
@@ -853,7 +873,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
853
873
  variant="outline"
854
874
  className="text-muted-foreground"
855
875
  onClick={() => handleOpenGroupDialog()}
856
- aria-label="Edit groups"
876
+ aria-label={t('entities.customFields.editor.editGroups', 'Edit groups')}
857
877
  >
858
878
  <Cog className="h-4 w-4" />
859
879
  </IconButton>
@@ -864,14 +884,14 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
864
884
  ) : null}
865
885
 
866
886
  <div className="mt-3 grid grid-cols-1 md:grid-cols-2 gap-3">
867
- <FormField label="Label">
887
+ <FormField label={t('entities.customFields.editor.label', 'Label')}>
868
888
  <Input
869
889
  value={typeof local.configJson?.label === 'string' ? local.configJson.label : ''}
870
890
  onChange={(event) => apply({ configJson: { ...(local.configJson || {}), label: event.target.value } })}
871
891
  onBlur={commit}
872
892
  />
873
893
  </FormField>
874
- <FormField label="Description">
894
+ <FormField label={t('entities.customFields.editor.description', 'Description')}>
875
895
  <Input
876
896
  value={typeof local.configJson?.description === 'string' ? local.configJson.description : ''}
877
897
  onChange={(event) => apply({ configJson: { ...(local.configJson || {}), description: event.target.value } })}
@@ -882,18 +902,18 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
882
902
  {(local.kind === 'text' || local.kind === 'multiline') && (
883
903
  <>
884
904
  <div>
885
- <label className="text-xs">Editor</label>
905
+ <label className="text-xs">{t('entities.customFields.editor.editor', 'Editor')}</label>
886
906
  <Select
887
907
  value={typeof local.configJson?.editor === 'string' && local.configJson.editor ? local.configJson.editor : undefined}
888
908
  onValueChange={(value) => { apply({ configJson: { ...(local.configJson || {}), editor: value || undefined } }, true) }}
889
909
  >
890
910
  <SelectTrigger>
891
- <SelectValue placeholder="Default" />
911
+ <SelectValue placeholder={t('entities.customFields.editor.defaultOption', 'Default')} />
892
912
  </SelectTrigger>
893
913
  <SelectContent>
894
- <SelectItem value="markdown">Markdown (UIW)</SelectItem>
895
- <SelectItem value="simpleMarkdown">Simple Markdown</SelectItem>
896
- <SelectItem value="htmlRichText">HTML Rich Text</SelectItem>
914
+ <SelectItem value="markdown">{t('entities.customFields.editor.editorMarkdown', 'Markdown (UIW)')}</SelectItem>
915
+ <SelectItem value="simpleMarkdown">{t('entities.customFields.editor.editorSimpleMarkdown', 'Simple Markdown')}</SelectItem>
916
+ <SelectItem value="htmlRichText">{t('entities.customFields.editor.editorHtmlRichText', 'HTML Rich Text')}</SelectItem>
897
917
  </SelectContent>
898
918
  </Select>
899
919
  </div>
@@ -903,13 +923,13 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
903
923
  size="sm"
904
924
  checked={!!local.configJson?.multi}
905
925
  onCheckedChange={(checked) => { apply({ configJson: { ...(local.configJson || {}), multi: checked === true } }, true) }}
906
- label="Multiple"
926
+ label={t('entities.customFields.editor.multiple', 'Multiple')}
907
927
  containerClassName="md:col-span-2"
908
928
  contentClassName="gap-1 [&_label]:text-xs [&_label]:font-normal"
909
929
  />
910
930
  {!!local.configJson?.multi && (
911
931
  <div className="md:col-span-2">
912
- <label className="text-xs">Multi-select input style</label>
932
+ <label className="text-xs">{t('entities.customFields.editor.multiSelectInputStyle', 'Multi-select input style')}</label>
913
933
  <Select
914
934
  value={local.configJson?.input === 'listbox' ? 'listbox' : 'default'}
915
935
  onValueChange={(value) => {
@@ -923,8 +943,8 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
923
943
  <SelectValue />
924
944
  </SelectTrigger>
925
945
  <SelectContent>
926
- <SelectItem value="default">Default</SelectItem>
927
- <SelectItem value="listbox">Listbox (searchable)</SelectItem>
946
+ <SelectItem value="default">{t('entities.customFields.editor.defaultOption', 'Default')}</SelectItem>
947
+ <SelectItem value="listbox">{t('entities.customFields.editor.listboxSearchable', 'Listbox (searchable)')}</SelectItem>
928
948
  </SelectContent>
929
949
  </Select>
930
950
  </div>
@@ -936,7 +956,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
936
956
 
937
957
  {local.kind === 'select' && (
938
958
  <div className="md:col-span-6 space-y-3">
939
- <label className="text-xs">Options</label>
959
+ <label className="text-xs">{t('entities.customFields.editor.options', 'Options')}</label>
940
960
  <div className="space-y-2">
941
961
  {resolvedOptions.length > 0 ? (
942
962
  resolvedOptions.map((option, idx) => (
@@ -953,14 +973,14 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
953
973
  size="xs"
954
974
  className="text-destructive hover:text-destructive/80"
955
975
  onClick={() => handleRemoveOption(idx)}
956
- aria-label="Remove option"
976
+ aria-label={t('entities.customFields.editor.removeOption', 'Remove option')}
957
977
  >
958
978
  <Trash2 className="h-3.5 w-3.5" />
959
979
  </IconButton>
960
980
  </div>
961
981
  ))
962
982
  ) : (
963
- <span className="text-xs text-muted-foreground">No options defined.</span>
983
+ <span className="text-xs text-muted-foreground">{t('entities.customFields.editor.noOptionsDefined', 'No options defined.')}</span>
964
984
  )}
965
985
  </div>
966
986
  <div className="flex justify-end">
@@ -971,7 +991,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
971
991
  onClick={handleOpenOptionDialog}
972
992
  >
973
993
  <Plus className="h-3.5 w-3.5" />
974
- Add option
994
+ {t('entities.customFields.editor.addOption', 'Add option')}
975
995
  </Button>
976
996
  </div>
977
997
  </div>
@@ -979,7 +999,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
979
999
 
980
1000
  {(local.kind === 'select' || local.kind === 'relation') && (
981
1001
  <>
982
- <FormField label="Options URL">
1002
+ <FormField label={t('entities.customFields.editor.optionsUrl', 'Options URL')}>
983
1003
  <Input
984
1004
  placeholder="/api/..."
985
1005
  value={typeof local.configJson?.optionsUrl === 'string' ? local.configJson.optionsUrl : ''}
@@ -988,7 +1008,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
988
1008
  />
989
1009
  </FormField>
990
1010
  {local.kind === 'relation' && (
991
- <FormField label="Related Entity ID">
1011
+ <FormField label={t('entities.customFields.editor.relatedEntityId', 'Related Entity ID')}>
992
1012
  <Input
993
1013
  inputClassName="font-mono"
994
1014
  placeholder="module:entity"
@@ -1015,7 +1035,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
1015
1035
 
1016
1036
  {local.kind === 'currency' && (
1017
1037
  <div className="md:col-span-2">
1018
- <label className="text-xs">Options source</label>
1038
+ <label className="text-xs">{t('entities.customFields.editor.optionsSource', 'Options source')}</label>
1019
1039
  <div className="rounded border bg-muted px-2 py-1 text-xs text-muted-foreground">
1020
1040
  /api/currencies/options
1021
1041
  </div>
@@ -1023,9 +1043,9 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
1023
1043
  )}
1024
1044
 
1025
1045
  {(local.kind === 'integer' || local.kind === 'float') && (
1026
- <FormField label="Units (optional)" className="md:col-span-2">
1046
+ <FormField label={t('entities.customFields.editor.unitsOptional', 'Units (optional)')} className="md:col-span-2">
1027
1047
  <Input
1028
- placeholder="kg, cm, etc."
1048
+ placeholder={t('entities.customFields.editor.unitsPlaceholder', 'kg, cm, etc.')}
1029
1049
  value={typeof local.configJson?.unit === 'string' ? local.configJson.unit : ''}
1030
1050
  onChange={(event) => apply({ configJson: { ...(local.configJson || {}), unit: event.target.value } })}
1031
1051
  onBlur={commit}
@@ -1036,7 +1056,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
1036
1056
 
1037
1057
  <div className="mt-3 pt-3 border-t">
1038
1058
  <div className="flex items-center justify-between mb-2">
1039
- <label className="text-sm font-medium">Validation rules</label>
1059
+ <label className="text-sm font-medium">{t('entities.customFields.editor.validationRules', 'Validation rules')}</label>
1040
1060
  <Button
1041
1061
  variant="outline"
1042
1062
  size="sm"
@@ -1044,13 +1064,13 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
1044
1064
  onClick={() => {
1045
1065
  apply((current) => {
1046
1066
  const list = Array.isArray(current.configJson?.validation) ? [...current.configJson.validation] : []
1047
- list.push({ rule: 'required', message: 'This field is required' } as any)
1067
+ list.push({ rule: 'required', message: t('entities.customFields.editor.ruleRequiredMessage', 'This field is required') } as any)
1048
1068
  return { configJson: { ...(current.configJson || {}), validation: list } }
1049
1069
  }, true)
1050
1070
  }}
1051
1071
  >
1052
1072
  <Plus className="h-3.5 w-3.5" />
1053
- Add rule
1073
+ {t('entities.customFields.editor.addRule', 'Add rule')}
1054
1074
  </Button>
1055
1075
  </div>
1056
1076
  <div className="space-y-2">
@@ -1089,7 +1109,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
1089
1109
  </div>
1090
1110
  <div className="md:col-span-4">
1091
1111
  <Input
1092
- placeholder={rule?.rule === 'regex' ? 'Pattern (e.g. ^[a-z]+$)' : (['lt','lte','gt','gte'].includes(rule?.rule) ? 'Number' : '—')}
1112
+ placeholder={rule?.rule === 'regex' ? t('entities.customFields.editor.rulePatternPlaceholder', 'Pattern (e.g. ^[a-z]+$)') : (['lt','lte','gt','gte'].includes(rule?.rule) ? t('entities.customFields.editor.ruleNumberPlaceholder', 'Number') : '—')}
1093
1113
  value={rule?.param ?? ''}
1094
1114
  onChange={(event) => {
1095
1115
  const value = ['lt','lte','gt','gte'].includes(rule?.rule) ? Number(event.target.value) : event.target.value
@@ -1106,7 +1126,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
1106
1126
  </div>
1107
1127
  <div className="md:col-span-4">
1108
1128
  <Input
1109
- placeholder="Error message"
1129
+ placeholder={t('entities.customFields.editor.ruleErrorMessagePlaceholder', 'Error message')}
1110
1130
  value={rule?.message || ''}
1111
1131
  onChange={(event) => {
1112
1132
  const message = event.target.value
@@ -1124,7 +1144,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
1124
1144
  <IconButton
1125
1145
  variant="outline"
1126
1146
  size="sm"
1127
- aria-label="Remove rule"
1147
+ aria-label={t('entities.customFields.editor.removeRule', 'Remove rule')}
1128
1148
  onClick={() => {
1129
1149
  apply((current) => {
1130
1150
  const list = Array.isArray(current.configJson?.validation) ? [...current.configJson.validation] : []
@@ -1139,7 +1159,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
1139
1159
  </div>
1140
1160
  ))}
1141
1161
  {(!Array.isArray(local.configJson?.validation) || local.configJson!.validation.length === 0) && (
1142
- <div className="text-xs text-muted-foreground">No validation rules defined.</div>
1162
+ <div className="text-xs text-muted-foreground">{t('entities.customFields.editor.noValidationRules', 'No validation rules defined.')}</div>
1143
1163
  )}
1144
1164
  </div>
1145
1165
  </div>
@@ -1303,11 +1323,11 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
1303
1323
  >
1304
1324
  <DialogContent className="max-w-sm">
1305
1325
  <DialogHeader>
1306
- <DialogTitle>Add option</DialogTitle>
1307
- <DialogDescription>Provide the stored value and optional label shown to users.</DialogDescription>
1326
+ <DialogTitle>{t('entities.customFields.editor.addOption', 'Add option')}</DialogTitle>
1327
+ <DialogDescription>{t('entities.customFields.editor.addOptionDescription', 'Provide the stored value and optional label shown to users.')}</DialogDescription>
1308
1328
  </DialogHeader>
1309
1329
  <div className="space-y-4 py-2">
1310
- <FormField label="Value" error={optionFormError ?? undefined}>
1330
+ <FormField label={t('entities.customFields.editor.value', 'Value')} error={optionFormError ?? undefined}>
1311
1331
  <Input
1312
1332
  inputClassName="font-mono"
1313
1333
  placeholder="unique_value"
@@ -1318,9 +1338,9 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
1318
1338
  }}
1319
1339
  />
1320
1340
  </FormField>
1321
- <FormField label="Label">
1341
+ <FormField label={t('entities.customFields.editor.label', 'Label')}>
1322
1342
  <Input
1323
- placeholder="Label shown to users (optional)"
1343
+ placeholder={t('entities.customFields.editor.optionLabelPlaceholder', 'Label shown to users (optional)')}
1324
1344
  value={optionLabelDraft}
1325
1345
  onChange={(event) => setOptionLabelDraft(event.target.value)}
1326
1346
  />
@@ -1328,10 +1348,10 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
1328
1348
  </div>
1329
1349
  <DialogFooter>
1330
1350
  <Button variant="outline" size="sm" onClick={handleCloseOptionDialog}>
1331
- Cancel
1351
+ {t('entities.customFields.editor.cancel', 'Cancel')}
1332
1352
  </Button>
1333
1353
  <Button size="sm" onClick={handleAddOption}>
1334
- Add option
1354
+ {t('entities.customFields.editor.addOption', 'Add option')}
1335
1355
  </Button>
1336
1356
  </DialogFooter>
1337
1357
  </DialogContent>
@@ -1349,13 +1369,19 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
1349
1369
  >
1350
1370
  <DialogContent className="max-w-md">
1351
1371
  <DialogHeader>
1352
- <DialogTitle>{editingGroupCode ? 'Edit group' : 'New group'}</DialogTitle>
1372
+ <DialogTitle>
1373
+ {editingGroupCode
1374
+ ? t('entities.customFields.editor.editGroup', 'Edit group')
1375
+ : t('entities.customFields.editor.newGroup', 'New group')}
1376
+ </DialogTitle>
1353
1377
  <DialogDescription>
1354
- {editingGroupCode ? 'Update the selected group for this fieldset.' : 'Add a reusable group for this fieldset.'}
1378
+ {editingGroupCode
1379
+ ? t('entities.customFields.editor.editGroupDescription', 'Update the selected group for this fieldset.')
1380
+ : t('entities.customFields.editor.newGroupDescription', 'Add a reusable group for this fieldset.')}
1355
1381
  </DialogDescription>
1356
1382
  </DialogHeader>
1357
1383
  <div className="space-y-3">
1358
- <FormField label="Group code" error={groupError ?? undefined} disabled={!!editingGroupCode}>
1384
+ <FormField label={t('entities.customFields.editor.groupCode', 'Group code')} error={groupError ?? undefined} disabled={!!editingGroupCode}>
1359
1385
  <Input
1360
1386
  inputClassName="font-mono"
1361
1387
  value={groupDraft.code}
@@ -1364,26 +1390,26 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
1364
1390
  if (groupError) setGroupError(null)
1365
1391
  }}
1366
1392
  disabled={!!editingGroupCode}
1367
- placeholder="e.g. buying_committee"
1393
+ placeholder={t('entities.customFields.editor.groupCodePlaceholder', 'e.g. buying_committee')}
1368
1394
  />
1369
1395
  </FormField>
1370
- <FormField label="Label">
1396
+ <FormField label={t('entities.customFields.editor.label', 'Label')}>
1371
1397
  <Input
1372
1398
  value={groupDraft.title}
1373
1399
  onChange={(event) => setGroupDraft((prev) => ({ ...prev, title: event.target.value }))}
1374
- placeholder="Buying committee"
1400
+ placeholder={t('entities.customFields.editor.groupLabelPlaceholder', 'Buying committee')}
1375
1401
  />
1376
1402
  </FormField>
1377
- <FormField label="Hint">
1403
+ <FormField label={t('entities.customFields.editor.hint', 'Hint')}>
1378
1404
  <Input
1379
1405
  value={groupDraft.hint}
1380
1406
  onChange={(event) => setGroupDraft((prev) => ({ ...prev, hint: event.target.value }))}
1381
- placeholder="Visible to merchandisers"
1407
+ placeholder={t('entities.customFields.editor.groupHintPlaceholder', 'Visible to merchandisers')}
1382
1408
  />
1383
1409
  </FormField>
1384
1410
  {currentFieldsetValue && groupOptions.length > 0 ? (
1385
1411
  <div>
1386
- <div className="text-xs font-medium mb-1">Existing groups</div>
1412
+ <div className="text-xs font-medium mb-1">{t('entities.customFields.editor.existingGroups', 'Existing groups')}</div>
1387
1413
  <div className="space-y-2 max-h-40 overflow-y-auto">
1388
1414
  {groupOptions.map((group) => (
1389
1415
  <div key={group.code} className="flex items-center justify-between rounded border px-3 py-2 text-sm">
@@ -1400,7 +1426,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
1400
1426
  size="xs"
1401
1427
  className="text-muted-foreground hover:text-foreground"
1402
1428
  onClick={() => handleEditGroupEntry(group)}
1403
- aria-label={`Edit ${group.code}`}
1429
+ aria-label={t('entities.customFields.editor.editGroupAria', 'Edit {code}').replace('{code}', group.code)}
1404
1430
  >
1405
1431
  <Pencil className="h-4 w-4" />
1406
1432
  </IconButton>
@@ -1409,7 +1435,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
1409
1435
  size="xs"
1410
1436
  className="text-destructive hover:text-destructive/80"
1411
1437
  onClick={() => handleRemoveGroupEntry(group.code)}
1412
- aria-label={`Delete ${group.code}`}
1438
+ aria-label={t('entities.customFields.editor.deleteGroupAria', 'Delete {code}').replace('{code}', group.code)}
1413
1439
  >
1414
1440
  <Trash2 className="h-4 w-4" />
1415
1441
  </IconButton>
@@ -1422,10 +1448,10 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
1422
1448
  </div>
1423
1449
  <DialogFooter>
1424
1450
  <Button variant="outline" size="sm" onClick={() => setGroupDialogOpen(false)}>
1425
- Cancel
1451
+ {t('entities.customFields.editor.cancel', 'Cancel')}
1426
1452
  </Button>
1427
1453
  <Button size="sm" onClick={handleGroupDialogSubmit}>
1428
- Save group
1454
+ {t('entities.customFields.editor.saveGroup', 'Save group')}
1429
1455
  </Button>
1430
1456
  </DialogFooter>
1431
1457
  </DialogContent>