mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-10 10:33:11 +03:00
generic and custom per-field validation
This commit is contained in:
parent
f7cc87e8ce
commit
dd0c497fd3
20
web/public/locales/en/validation.json
Normal file
20
web/public/locales/en/validation.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"minimum": "Must be at least {{limit}}",
|
||||
"maximum": "Must be at most {{limit}}",
|
||||
"exclusiveMinimum": "Must be greater than {{limit}}",
|
||||
"exclusiveMaximum": "Must be less than {{limit}}",
|
||||
"minLength": "Must be at least {{limit}} character(s)",
|
||||
"maxLength": "Must be at most {{limit}} character(s)",
|
||||
"minItems": "Must have at least {{limit}} items",
|
||||
"maxItems": "Must have at most {{limit}} items",
|
||||
"pattern": "Invalid format",
|
||||
"required": "This field is required",
|
||||
"type": "Invalid value type",
|
||||
"enum": "Must be one of the allowed values",
|
||||
"const": "Value does not match expected constant",
|
||||
"uniqueItems": "All items must be unique",
|
||||
"format": "Invalid format",
|
||||
"additionalProperties": "Unknown property is not allowed",
|
||||
"oneOf": "Must match exactly one of the allowed schemas",
|
||||
"anyOf": "Must match at least one of the allowed schemas"
|
||||
}
|
||||
@ -68,7 +68,11 @@ export function ConfigForm({
|
||||
formContext,
|
||||
i18nNamespace,
|
||||
}: ConfigFormProps) {
|
||||
const { t } = useTranslation([i18nNamespace || "common", "views/settings"]);
|
||||
const { t } = useTranslation([
|
||||
i18nNamespace || "common",
|
||||
"views/settings",
|
||||
"validation",
|
||||
]);
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
|
||||
// Determine which fields to hide based on advanced toggle
|
||||
@ -113,7 +117,7 @@ export function ConfigForm({
|
||||
);
|
||||
|
||||
// Create error transformer for user-friendly error messages
|
||||
const errorTransformer = useMemo(() => createErrorTransformer(), []);
|
||||
const errorTransformer = useMemo(() => createErrorTransformer(t), [t]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(e: IChangeEvent) => {
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
// Maps JSON Schema validation keywords to user-friendly messages
|
||||
|
||||
import type { ErrorTransformer } from "@rjsf/utils";
|
||||
import type { TFunction } from "i18next";
|
||||
|
||||
export interface ErrorMessageMap {
|
||||
[keyword: string]: string | ((params: Record<string, unknown>) => string);
|
||||
@ -30,7 +31,13 @@ export const defaultErrorMessages: ErrorMessageMap = {
|
||||
};
|
||||
return formatLabels[format] || `Invalid ${format} format`;
|
||||
},
|
||||
enum: "Must be one of the allowed values",
|
||||
enum: (params) => {
|
||||
const allowedValues = params.allowedValues as unknown;
|
||||
if (Array.isArray(allowedValues)) {
|
||||
return `Must be one of: ${allowedValues.join(", ")}`;
|
||||
}
|
||||
return "Must be one of the allowed values";
|
||||
},
|
||||
const: "Value does not match expected constant",
|
||||
uniqueItems: "All items must be unique",
|
||||
minItems: (params) => `Must have at least ${params.limit} items`,
|
||||
@ -44,25 +51,107 @@ export const defaultErrorMessages: ErrorMessageMap = {
|
||||
* Creates an error transformer function for RJSF
|
||||
* Transforms technical JSON Schema errors into user-friendly messages
|
||||
*/
|
||||
export function createErrorTransformer(
|
||||
customMessages: ErrorMessageMap = {},
|
||||
): ErrorTransformer {
|
||||
const messages = { ...defaultErrorMessages, ...customMessages };
|
||||
export function createErrorTransformer(t: TFunction): ErrorTransformer {
|
||||
const getDefaultMessage = (
|
||||
errorType: string,
|
||||
params: Record<string, unknown>,
|
||||
): string | undefined => {
|
||||
const template = defaultErrorMessages[errorType];
|
||||
|
||||
return (errors) => {
|
||||
return errors.map((error) => {
|
||||
const keyword = error.name || "";
|
||||
const messageTemplate = messages[keyword];
|
||||
if (!template) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!messageTemplate) {
|
||||
if (typeof template === "function") {
|
||||
return template(params);
|
||||
}
|
||||
|
||||
return template;
|
||||
};
|
||||
|
||||
const normalizeParams = (
|
||||
params: Record<string, unknown> | undefined,
|
||||
): Record<string, unknown> => {
|
||||
if (!params) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const allowedValues = params.allowedValues as unknown;
|
||||
|
||||
return {
|
||||
...params,
|
||||
allowedValues: Array.isArray(allowedValues)
|
||||
? allowedValues.join(", ")
|
||||
: allowedValues,
|
||||
};
|
||||
};
|
||||
|
||||
const getFieldPathFromProperty = (
|
||||
property: string | undefined,
|
||||
params: Record<string, unknown>,
|
||||
errorType: string,
|
||||
): string => {
|
||||
const basePath = (property || "").replace(/^\./, "").trim();
|
||||
const missingProperty = params.missingProperty as string | undefined;
|
||||
|
||||
if (errorType === "required" && missingProperty) {
|
||||
return basePath ? `${basePath}.${missingProperty}` : missingProperty;
|
||||
}
|
||||
|
||||
return basePath;
|
||||
};
|
||||
|
||||
return (errors) =>
|
||||
errors.map((error) => {
|
||||
const errorType = error.name || "";
|
||||
if (!errorType) {
|
||||
return error;
|
||||
}
|
||||
|
||||
let message: string;
|
||||
if (typeof messageTemplate === "function") {
|
||||
message = messageTemplate(error.params || {});
|
||||
} else {
|
||||
message = messageTemplate;
|
||||
const normalizedParams = normalizeParams(error.params);
|
||||
const fieldPath = getFieldPathFromProperty(
|
||||
error.property,
|
||||
normalizedParams,
|
||||
errorType,
|
||||
);
|
||||
|
||||
let message: string | undefined;
|
||||
|
||||
const missingTranslation = "__missing_translation__";
|
||||
|
||||
// Try field-specific validation message first
|
||||
if (fieldPath) {
|
||||
const fieldKey = `${fieldPath}.validation.${errorType}`;
|
||||
const translated = t(fieldKey, {
|
||||
...normalizedParams,
|
||||
ns: ["config"],
|
||||
defaultValue: missingTranslation,
|
||||
});
|
||||
if (translated !== fieldKey && translated !== missingTranslation) {
|
||||
message = translated;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to generic validation message
|
||||
if (!message) {
|
||||
const genericKey = errorType;
|
||||
const translated = t(genericKey, {
|
||||
...normalizedParams,
|
||||
ns: ["validation"],
|
||||
defaultValue: missingTranslation,
|
||||
});
|
||||
if (translated !== genericKey && translated !== missingTranslation) {
|
||||
message = translated;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to English defaults
|
||||
if (!message) {
|
||||
message = getDefaultMessage(errorType, normalizedParams);
|
||||
}
|
||||
|
||||
if (!message) {
|
||||
return error;
|
||||
}
|
||||
|
||||
return {
|
||||
@ -70,7 +159,6 @@ export function createErrorTransformer(
|
||||
message,
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Loading…
Reference in New Issue
Block a user