// Field Template - wraps each form field with label and description import type { FieldTemplateProps, StrictRJSFSchema } from "@rjsf/utils"; import { Label } from "@/components/ui/label"; import { cn } from "@/lib/utils"; import { useTranslation } from "react-i18next"; import { isNullableUnionSchema } from "../fields/nullableUtils"; /** * Build the i18n translation key path for nested fields using the field path * provided by RJSF. This avoids ambiguity with underscores in field names. */ function buildTranslationPath(path: Array): string { return path.filter((segment) => typeof segment === "string").join("."); } export function FieldTemplate(props: FieldTemplateProps) { const { id, label, children, errors, help, description, hidden, required, displayLabel, schema, uiSchema, registry, fieldPathId, } = props; // Get i18n namespace from form context (passed through registry) const formContext = registry?.formContext as | Record | undefined; const i18nNamespace = formContext?.i18nNamespace as string | undefined; const { t } = useTranslation([i18nNamespace || "common"]); if (hidden) { return
{children}
; } // Get UI options const uiOptions = uiSchema?.["ui:options"] || {}; const isAdvanced = uiOptions.advanced === true; // Boolean fields (switches) render label inline const isBoolean = schema.type === "boolean"; const isNullableUnion = isNullableUnionSchema(schema as StrictRJSFSchema); // Get translation path for this field const translationPath = buildTranslationPath(fieldPathId.path); // Use schema title/description as primary source (from JSON Schema) const schemaTitle = (schema as Record).title as | string | undefined; const schemaDescription = (schema as Record).description as | string | undefined; // Try to get translated label, falling back to schema title, then RJSF label let finalLabel = label; if (i18nNamespace && translationPath) { const translationKey = `${translationPath}.label`; const translatedLabel = t(translationKey, { ns: i18nNamespace, defaultValue: "", }); // Only use translation if it's not the key itself (which means translation exists) if (translatedLabel && translatedLabel !== translationKey) { finalLabel = translatedLabel; } else if (schemaTitle) { finalLabel = schemaTitle; } } else if (schemaTitle) { finalLabel = schemaTitle; } // Try to get translated description, falling back to schema description let finalDescription = description || ""; if (i18nNamespace && translationPath) { const translatedDesc = t(`${translationPath}.description`, { ns: i18nNamespace, defaultValue: "", }); if (translatedDesc && translatedDesc !== `${translationPath}.description`) { finalDescription = translatedDesc; } else if (schemaDescription) { finalDescription = schemaDescription; } } else if (schemaDescription) { finalDescription = schemaDescription; } return (
{displayLabel && finalLabel && !isBoolean && !isNullableUnion && ( )} {isBoolean ? (
{displayLabel && finalLabel && ( )} {finalDescription && !isNullableUnion && (

{String(finalDescription)}

)}
{children}
) : ( <> {finalDescription && !isNullableUnion && (

{finalDescription}

)} {children} )} {errors} {help}
); }