import { z } from "zod";
import { DateTime } from "luxon";
import { type TranslationValues } from "next-intl";
export const createDebtRecurrentOptions = ["WEEKLY", "BIWEEKLY", "MONTHLY"] as const;
export const createDebtTypeOptions = ["SINGLE", "RECURRENT"] as const;
export const CURRENCIES = [
"COP",
"USD",
"MXN",
"EUR",
"UYU",
"ARS",
"CLP",
"BRL",
"PYG",
"PEN",
"GBP",
] as const;
export type Currency = (typeof CURRENCIES)[number];
export const DEBT_MAX_BORROWERS = 4;
export const DEBT_MAX_WEEKLY_DURATION = 8;
export const DEBT_MAX_BIWEEKLY_DURATION = 6;
export const DEBT_MAX_MONTHLY_DURATION = 12;
type Messages = keyof IntlMessages["general-info-input"];
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function getGeneralInfoInput(
t?: (key: Messages, object?: TranslationValues | undefined) => string
) {
return z
.object({
name: z
.string({
invalid_type_error: t?.("name-invalid-type"),
required_error: t?.("name-required"),
})
.trim()
.min(1, {
message: t?.("name-required"),
})
.max(50, {
message: t?.("name-max"),
}),
description: z
.string({
invalid_type_error: t?.("description-invalid-type"),
})
.trim()
.max(100, {
message: t?.("description-max"),
})
.nullable()
.transform((value) => (value === "" ? null : value)),
amount: z
.number({
invalid_type_error: t?.("amount-required"),
required_error: t?.("amount-required"),
})
.gte(1, {
message: t?.("amount-gte"),
})
.lte(1_000_000_000, {
message: t?.("amount-lte"),
})
.multipleOf(0.01, {
message: t?.("amount-multiple"),
}),
currency: z.enum(CURRENCIES),
dueDate: z
.string()
.refine((iso) => DateTime.fromISO(iso).isValid, t?.("due-date-invalid"))
.transform((iso) => DateTime.fromISO(iso).toUTC().toISO())
.refine((iso) => {
if (iso === null) return true;
const now = DateTime.now().toUTC();
const dueDate = DateTime.fromISO(iso).toUTC();
return dueDate > now;
}, t?.("due-date-future"))
.nullable(),
type: z.enum(createDebtTypeOptions),
recurrency: z
.object({
frequency: z.enum(createDebtRecurrentOptions, {
required_error: t?.("recurrency-frequency-required"),
}),
duration: z
.number({
coerce: true,
invalid_type_error: t?.("recurrency-duration-required"),
required_error: t?.("recurrency-duration-required"),
})
.gte(2, {
message: t?.("recurrency-duration-gte"),
}),
})
.superRefine((arg, ctx) => {
if (arg.frequency === "WEEKLY") {
if (arg.duration > DEBT_MAX_WEEKLY_DURATION) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message:
t?.("weekly-duration-max", { max: DEBT_MAX_WEEKLY_DURATION }) ??
`Weekly recurrence duration must be less than ${DEBT_MAX_WEEKLY_DURATION}`,
path: ["recurrency", "duration"],
});
}
}
if (arg.frequency === "BIWEEKLY") {
if (arg.duration > DEBT_MAX_BIWEEKLY_DURATION) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message:
t?.("biweekly-duration-max", { max: DEBT_MAX_BIWEEKLY_DURATION }) ??
`Biweekly recurrence duration must be less than ${DEBT_MAX_BIWEEKLY_DURATION}`,
path: ["recurrency", "duration"],
});
}
}
if (arg.frequency === "MONTHLY") {
if (arg.duration > DEBT_MAX_MONTHLY_DURATION) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message:
t?.("monthly-duration-max", { max: DEBT_MAX_MONTHLY_DURATION }) ??
`Monthly recurrence duration must be less than ${DEBT_MAX_MONTHLY_DURATION}`,
path: ["recurrency", "duration"],
});
}
}
})
.nullable(),
})
.superRefine((arg, ctx) => {
if (arg.type === "RECURRENT" && arg.recurrency === null) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t?.("recurrency-required"),
path: ["recurrency"],
});
}
});
}
export type GeneralInfoInput = z.infer<ReturnType<typeof getGeneralInfoInput>>;{
"general-info-input": {
"name-required": "Name is required",
"name-invalid-type": "Name must be a string",
"name-max": "Name must be less than 50 characters",
"description-max": "Description must be less than 100 characters",
"description-invalid-type": "Description must be a string",
"amount-gte": "Amount must be greater than 0",
"amount-lte": "Amount must be less than or equal to 1,000,000,000",
"amount-multiple": "Amount must be a multiple of 0.01",
"amount-required": "Amount is required",
"due-date-invalid": "Invalid date",
"due-date-future": "Due date must be in the future",
"recurrency-frequency-required": "You must select a recurrence frequency",
"recurrency-duration-gte": "Duration must be greater than 1",
"recurrency-duration-required": "You must add a recurrence duration",
"weekly-duration-max": "Duration must be less than {max}",
"biweekly-duration-max": "Duration must be less than {max}",
"monthly-duration-max": "Duration must be less than {max}",
"recurrency-required": "You must add a recurrence"
}
} const t = useTranslations("debt-info-form");
const formT = useTranslations("general-info-input");
const [openCyclesInfo, setOpenCyclesInfo] = React.useState(false);
const form = useForm<GeneralInfoInput>({
defaultValues: formData.generalInfo,
mode: "onSubmit",
reValidateMode: "onChange",
resolver: zodResolver(getGeneralInfoInput(formT)),
});The idea is to have two useTranslations. One for your actual content (in this case const t = useTranslations("debt-info-form")), and the other formT for the translations you will pass to the factory function.