This repo is a simpler version of using zod and useTranslations from next-intl. It is generic. That means you don't have to manually create a custom message for each of your validations. All you have to do is copy this code structure. If you want a different language version, simply duplicate the en.json file into your language of choice and then follow the guidelines from next-intl to extend your website. I created this because the other solutions I found online were overly complicated. I have added notes below to credit those websites I have used for inspiration and copied ideas from. Enjoy!
- This zod en object was taken from here: https://github.com/gcascio/next-intl-zod
- react-hook-form with zod validation https://www.freecodecamp.org/news/react-form-validation-zod-react-hook-form/
- git clone thisrepo
- cd thisrepo
- pnpm i
- pnpm run dev
- Test the validation
-
Install dependencies
npm i zod next-intl
-
messages/en.json
{
"HomePage": {
"title": "Hello world!"
},
"zod": {
/* Paste latest zod messages here. Example... */
"invalid_type": "Expected {expected}, received {received}",
"invalid_type_with_path": "{path} is expected {expected}, but received {received}",
"invalid_type_received_undefined": "Required",
}
}
- pages/_app.tsx
- Wrap with NextIntlClientProvider
- inherit local 'en' from router
- pass pageProps into next page
import type { ReactElement, ReactNode } from "react";
import type { NextPage } from "next";
import { useRouter } from "next/router";
import { NextIntlClientProvider } from "next-intl";
import type { AppProps } from "next/app";
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
getLayout?: (page: ReactElement) => ReactNode;
};
type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout;
};
function App({ Component, pageProps }: AppPropsWithLayout) {
const router = useRouter();
return (
<NextIntlClientProvider
locale={router.locale}
timeZone="Europe/London"
messages={pageProps.messages}
>
<Component {...pageProps} />
</NextIntlClientProvider>
);
}
export default App;
- Example - Using the validation in react-hook-form - FormRegister.tsx
import React, { useRef } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useValidationSchema } from "./useValidationSchema";
import { Button } from "@/component/Form/Button/Button";
import { FormFieldEmail } from "./FormFieldEmail";
import { FormFieldPassword } from "./FormFieldPassword";
import { FormFieldConfirmPassword } from "./FormFieldConfirmPassword";
import { FormFieldUsername } from "./FormFieldUsername";
import styles from "@/component/Form/Form.module.css";
import useMutateRegister from "./useMutationRegister";
import { PropsFormRegister } from "@/@types";
import { useTranslations } from "next-intl";
export const FormRegister = () => {
const tZod = useTranslations('zod');
const t = useTranslations('Register');
const T_legend = t('legend');
const T_submit = t('submit');
const zodValidationSchema = useValidationSchema(tZod);
const methods = useForm<PropsFormRegister>({
resolver: zodResolver(zodValidationSchema),
});
const { handleSubmit, reset, formState } = methods;
const onSubmit = (values: PropsFormRegister) => {
console.log("submit...");
mutate(values);
};
return (
<FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmit)} className={styles.form}>
<fieldset className={styles.fieldset}>
<legend className={styles.legend}>{T_legend}</legend>
<FormFieldUsername />
<FormFieldEmail />
<FormFieldPassword />
<FormFieldConfirmPassword />
</fieldset>
<div className={styles.actions}>
<Button type="submit" variant="primary">
{T_submit}
</Button>
</div>
</form>
</FormProvider>
);
};
- getStaticProps in your Page.tsx
import { useTranslations } from "next-intl";
export default function Home() {
const t = useTranslations('HomePage');
return (
<main>
<h1>{t('title')}</h1>
</main>
);
}
/* eslint-enable @typescript-eslint/no-explicit-any */
export async function getStaticProps(context: any) {
return {
props: {
messages: (await import(`@/messages/${context.locale}.json`)).default
}
};
}
Troubleshoot - Pages router = Error: MISSING_MESSAGE: No messages were configured on the provider.
- Ensure you aren't putting a Component inside the _app.tsx, because that will not enable you to use getStaticProps via the page context
- DON'T DO THIS app.tsx
<NextIntlClientProvider
locale={router.locale}
timeZone="Europe/London"
messages={pageProps.messages}
>
<SpecificComponentConsumesNextIntlUseTranslations/>
</NextIntlClientProvider>
Instead wrap your pages component with a higher order component... hoc/withMyPageComponent.tsx
const withMyPageComponent = <P extends object>(
SomePageComponent: React.ComponentType<P>
) => {
const WithWrapper = (props: P) => {
return (
<>
<SpecificComponentConsumesNextIntlUseTranslations/>
<SomePageComponent {...props} />
</>
);
};
return WithWrapper;
};
withMyPageComponent.displayType = "hoc";
export default withMyPageComponent;
// use
// pages/Home.tsx
// const Home = (props: Props) => {}
// getStaticProps
// export default withMyPageComponent<Props>(Home);
- OPTIONAL - if using app router:
- next.config.ts
import createNextIntlPlugin from 'next-intl/plugin';
const withNextIntl = createNextIntlPlugin();
const nextConfig = {
i18n: {
locales: ['en'],
defaultLocale: 'en',
},
};
export default withNextIntl(nextConfig);
- i18n/request.ts
import {getRequestConfig} from 'next-intl/server';
export default getRequestConfig(async () => {
const locale = 'en';
return {
locale,
messages: (await import(`@/messages/${locale}.json`)).default
};
});
- app/ Need an app directory in order for _app.tsx to work Otherwise you will get:
Error: MISSING_MESSAGE: No messages were configured on the provider.
For either Pages or App router - see: https://next-intl.dev/docs/getting-started/pages-router
done. Enjoy!