feat: add i18n translated form errors (#509)
This commit is contained in:
@@ -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(),
|
||||
|
||||
139
packages/validation/src/form/i18n.ts
Normal file
139
packages/validation/src/form/i18n.ts
Normal 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 });
|
||||
@@ -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() }),
|
||||
|
||||
Reference in New Issue
Block a user