feat: add i18n translated form errors (#509)

This commit is contained in:
Meier Lukas
2024-05-18 16:55:08 +02:00
committed by GitHub
parent b312032f02
commit dfed804f65
32 changed files with 501 additions and 156 deletions

View File

@@ -16,7 +16,7 @@ const boardNameSchema = z
.string()
.min(1)
.max(255)
.regex(/^[A-Za-z0-9-\\._]+$/);
.regex(/^[A-Za-z0-9-\\._]*$/);
const byNameSchema = z.object({
name: boardNameSchema,
@@ -53,12 +53,7 @@ const savePartialSettingsSchema = z
customCss: z.string().max(16384),
columnCount: z.number().min(1).max(24),
})
.partial()
.and(
z.object({
id: z.string(),
}),
);
.partial();
const saveSchema = z.object({
id: z.string(),

View File

@@ -0,0 +1,139 @@
import type { ErrorMapCtx, z, ZodTooBigIssue, ZodTooSmallIssue } from "zod";
import { ZodIssueCode } from "zod";
import type { TranslationObject } from "@homarr/translation";
export const zodErrorMap = <
// eslint-disable-next-line @typescript-eslint/no-explicit-any
TFunction extends (key: string, ...params: any[]) => string,
>(
t: TFunction,
) => {
return (issue: z.ZodIssueOptionalMessage, ctx: ErrorMapCtx) => {
const error = handleZodError(issue, ctx);
if ("message" in error && error.message)
return {
message: error.message ?? ctx.defaultError,
};
return {
message: t(
error.key ? `common.zod.${error.key}` : "common.zod.errors.default",
error.params ?? {},
),
};
};
};
const handleStringError = (issue: z.ZodInvalidStringIssue) => {
if (issue.validation === "email") {
return {
key: "errors.string.invalidEmail",
} as const;
}
if (typeof issue.validation === "object") {
if ("startsWith" in issue.validation) {
return {
key: "errors.string.startsWith",
params: {
startsWith: issue.validation.startsWith,
},
} as const;
} else if ("endsWith" in issue.validation) {
return {
key: "errors.string.endsWith",
params: {
endsWith: issue.validation.endsWith,
},
} as const;
}
return {
key: "errors.invalid_string.includes",
params: {
includes: issue.validation.includes,
},
} as const;
}
return {
message: issue.message,
};
};
const handleTooSmallError = (issue: ZodTooSmallIssue) => {
if (issue.type !== "string" && issue.type !== "number") {
return {
message: issue.message,
};
}
if (issue.type === "string" && issue.minimum === 1) {
return {
key: "errors.required",
} as const;
}
return {
key: `errors.tooSmall.${issue.type}`,
params: {
minimum: issue.minimum,
count: issue.minimum,
},
} as const;
};
const handleTooBigError = (issue: ZodTooBigIssue) => {
if (issue.type !== "string" && issue.type !== "number") {
return {
message: issue.message,
};
}
return {
key: `errors.tooBig.${issue.type}`,
params: {
maximum: issue.maximum,
count: issue.maximum,
},
} as const;
};
const handleZodError = (issue: z.ZodIssueOptionalMessage, ctx: ErrorMapCtx) => {
if (ctx.defaultError === "Required") {
return {
key: "errors.required",
params: {},
} as const;
}
if (issue.code === ZodIssueCode.invalid_string) {
return handleStringError(issue);
}
if (issue.code === ZodIssueCode.too_small) {
return handleTooSmallError(issue);
}
if (issue.code === ZodIssueCode.too_big) {
return handleTooBigError(issue);
}
if (issue.code === ZodIssueCode.custom && issue.params?.i18n) {
const { i18n } = issue.params as CustomErrorParams;
return {
key: `errors.custom.${i18n.key}`,
} as const;
}
return {
message: issue.message,
};
};
export interface CustomErrorParams {
i18n: {
key: keyof TranslationObject["common"]["zod"]["errors"]["custom"];
params?: Record<string, unknown>;
};
}
export const createCustomErrorParams = (
i18n: CustomErrorParams["i18n"] | CustomErrorParams["i18n"]["key"],
) => (typeof i18n === "string" ? { i18n: { key: i18n } } : { i18n });

View File

@@ -1,25 +1,34 @@
import { z } from "zod";
import { createCustomErrorParams } from "./form/i18n";
const usernameSchema = z.string().min(3).max(255);
const passwordSchema = z.string().min(8).max(255);
const confirmPasswordRefine = [
(data: { password: string; confirmPassword: string }) =>
data.password === data.confirmPassword,
{
path: ["confirmPassword"],
params: createCustomErrorParams("passwordsDoNotMatch"),
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
] satisfies [(args: any) => boolean, unknown];
const createUserSchema = z
.object({
username: usernameSchema,
password: passwordSchema,
confirmPassword: z.string(),
email: z.string().email().optional(),
email: z.string().email().or(z.string().length(0).optional()),
})
.refine((data) => data.password === data.confirmPassword, {
path: ["confirmPassword"],
message: "Passwords do not match",
});
.refine(confirmPasswordRefine[0], confirmPasswordRefine[1]);
const initUserSchema = createUserSchema;
const signInSchema = z.object({
name: z.string(),
password: z.string(),
name: z.string().min(1),
password: z.string().min(1),
});
const registrationSchema = z
@@ -28,10 +37,7 @@ const registrationSchema = z
password: passwordSchema,
confirmPassword: z.string(),
})
.refine((data) => data.password === data.confirmPassword, {
path: ["confirmPassword"],
message: "Passwords do not match",
});
.refine(confirmPasswordRefine[0], confirmPasswordRefine[1]);
const registrationSchemaApi = registrationSchema.and(
z.object({
@@ -54,14 +60,11 @@ const editProfileSchema = z.object({
const changePasswordSchema = z
.object({
previousPassword: z.string(),
previousPassword: z.string().min(1),
password: passwordSchema,
confirmPassword: z.string(),
})
.refine((data) => data.password === data.confirmPassword, {
path: ["confirmPassword"],
message: "Passwords do not match",
});
.refine(confirmPasswordRefine[0], confirmPasswordRefine[1]);
const changePasswordApiSchema = changePasswordSchema.and(
z.object({ userId: z.string() }),