Skip to content

Commit

Permalink
Merge pull request #46 from aiji42/object-schema-path-template
Browse files Browse the repository at this point in the history
v2.0.0
  • Loading branch information
aiji42 authored Dec 20, 2022
2 parents bbf1307 + 5590438 commit e6a3410
Show file tree
Hide file tree
Showing 26 changed files with 842 additions and 213 deletions.
137 changes: 133 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,150 @@ import i18next from 'i18next'
import { z } from 'zod'
import { zodI18nMap } from 'zod-i18n-map'
// Import your language translation files
import translation from 'zod-i18n-map/locales/ja/zod.json'
import translation from 'zod-i18n-map/locales/es/zod.json'

// lng and resources key depend on your locale.
i18next.init({
lng: 'ja',
lng: 'es',
resources: {
ja: { zod: translation },
es: { zod: translation },
},
});
z.setErrorMap(zodI18nMap)

const schema = z.string().email()
schema.parse('foo') // メールアドレスの形式で入力してください。
// Translated into Spanish (es)
schema.parse('foo') // => correo inválido
```

## `makeZodI18nMap`

Detailed customization is possible by using `makeZodI18nMap` and option values.

```ts
export type MakeZodI18nMap = (option?: ZodI18nMapOption) => ZodErrorMap;

export type ZodI18nMapOption = {
t?: i18n["t"];
ns?: string | readonly string[]; // See: `Namespace`
handlePath?: { // See: `Handling object schema keys`
context?: string;
ns?: string | readonly string[];
keyPrefix?: string;
};
};
```

### Namespace (`ns`)

You can switch between translation files by specifying a namespace.
This is useful in cases where the application handles validation messages for different purposes, e.g., validation messages for forms are for end users, while input value checks for API schemas are for developers.

The default namespace is `zod`.

```ts
import i18next from 'i18next'
import { z } from 'zod'
import { makeZodI18nMap } from "zod-i18n-map";

i18next.init({
lng: 'en',
resources: {
en: {
zod: { // default namespace
invalid_type: "Error: expected {{expected}}, received {{received}}"
},
formValidation: { // custom namespace
invalid_type: "it is expected to provide {{expected}} but you provided {{received}}"
},
},
},
});

// use default namespace
z.setErrorMap(makeZodI18nMap())
z.string().parse(1) // => Error: expected string, received number

// select custom namespace
z.setErrorMap(makeZodI18nMap({ ns: 'formValidation' }))
z.string().parse(1) // => it is expected to provide string but you provided number
```

### Handling object schema keys (`handlePath`)

When dealing with structured data, such as when using Zod as a validator for form input values, it is common to generate a schema with `z.object`.
You can handle the object's key in the message by preparing messages with the key in the `with_path` context.

```ts
import i18next from 'i18next'
import { z } from 'zod'
import { zodI18nMap } from "zod-i18n-map";

i18next.init({
lng: "en",
resources: {
en: {
zod: {
errors: {
invalid_type: "Expected {{expected}}, received {{received}}",
invalid_type_with_path:
"{{path}} is expected {{expected}}, received {{received}}",
},
userName: "User's name",
},
},
},
});

z.setErrorMap(zodI18nMap);

z.string().parse(1) // => Expected string, received number

const schema = z.object({
userName: z.string(),
});
schema.parse({ userName: 1 }) // => User's name is expected string, received number
```

If `_with_path` is suffixed to the key of the message, that message will be adopted in the case of an object type schema.
If there is no message key with `_with_path`, fall back to the normal error message.
The suffix can be changed by specifying `handlePath.context`.

Object schema keys can be handled in the message with `{{path}}`.
By preparing the translated data for the same key as the key in the object schema, the translated value will be output in `{{path}}`, otherwise the key will be output as is.
You can also separate namespaces for translation data for `{{path}}` by specifying `handlePath.ns`. Furthermore, it is possible to access nested translation data by specifying `handlePath.keyPrefix`.

```ts
i18next.init({
lng: "en",
resources: {
en: {
zod: {
errors: {
invalid_type: "Expected {{expected}}, received {{received}}",
invalid_type_with_path:
"{{- path}} is expected {{expected}}, received {{received}}",
},
},
form: {
group: {
userName: "User's name",
}
}
},
},
});

z.setErrorMap(zodI18nMap({
handlePath: {
ns: "form",
keyPrefix: "group"
}
}));
```



## Translation Files
`zod-i18n-map` contains translation files for several locales.

Expand Down
4 changes: 2 additions & 2 deletions examples/with-next-i18next/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ module.exports = {
By giving the `t` method from `useTranslation` as an argument to `makeZodI18nMap` and giving it as an argument to `z.setErrorMap`, the zod error messages are automatically translated.
```ts
const { t } = useTranslation();
z.setErrorMap(makeZodI18nMap(t));
z.setErrorMap(makeZodI18nMap({ t }));
```

Finally, the page file will be as follows.
Expand All @@ -89,7 +89,7 @@ const schema = z.object({

export default function Page() {
const { t } = useTranslation();
z.setErrorMap(makeZodI18nMap(t));
z.setErrorMap(makeZodI18nMap({ t }));
const {
register,
handleSubmit,
Expand Down
12 changes: 6 additions & 6 deletions examples/with-next-i18next/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const schema = z.object({

export default function HookForm() {
const { t } = useTranslation();
z.setErrorMap(makeZodI18nMap(t));
z.setErrorMap(makeZodI18nMap({ t, handlePath: { ns: ["common", "zod"] } }));
const router = useRouter();

const {
Expand Down Expand Up @@ -90,11 +90,11 @@ export default function HookForm() {
<form onSubmit={handleSubmit(console.log)}>
<FormControl isInvalid={!!errors.username} mb={4}>
<FormLabel htmlFor="username">
<Trans>User name</Trans>
<Trans>username</Trans>
</FormLabel>
<Input
id="username"
placeholder={t("John Doe") ?? undefined}
placeholder={t("username_placeholder") ?? undefined}
{...register("username")}
/>
<FormErrorMessage>
Expand All @@ -103,7 +103,7 @@ export default function HookForm() {
</FormControl>
<FormControl isInvalid={!!errors.email} mb={4}>
<FormLabel htmlFor="email">
<Trans>Email</Trans>
<Trans>email</Trans>
</FormLabel>
<Input
id="email"
Expand All @@ -116,7 +116,7 @@ export default function HookForm() {
</FormControl>
<FormControl isInvalid={!!errors.favoriteNumber} mb={4}>
<FormLabel htmlFor="favoriteNumber">
<Trans>Favorite number</Trans>
<Trans>favoriteNumber</Trans>
</FormLabel>
<Input
id="favoriteNumber"
Expand All @@ -132,7 +132,7 @@ export default function HookForm() {
isLoading={isSubmitting}
type="submit"
>
<Trans>Submit</Trans>
<Trans>submit</Trans>
</Button>
</form>
</Container>
Expand Down
10 changes: 5 additions & 5 deletions examples/with-next-i18next/public/locales/ar/common.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"User name": "اسم المستخدم",
"John Doe": "محمد ماهر",
"Email": "البريد الالكتروني",
"Favorite number": "الرقم المفضل",
"Submit": "ارسال"
"username": "اسم المستخدم",
"username_placeholder": "محمد ماهر",
"email": "البريد الالكتروني",
"favoriteNumber": "الرقم المفضل",
"submit": "ارسال"
}
8 changes: 7 additions & 1 deletion examples/with-next-i18next/public/locales/en/common.json
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
{}
{
"username": "User name",
"username_placeholder": "John Doe",
"email": "Email",
"favoriteNumber": "Favorite number",
"submit": "Submit"
}
17 changes: 13 additions & 4 deletions examples/with-next-i18next/public/locales/en/zod.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"errors": {
"invalid_type": "Expected {{expected}}, received {{received}}",
"invalid_type_with_path": "{{path}} is expected {{expected}}, but received {{received}}",
"invalid_type_received_undefined": "Required",
"invalid_literal": "Invalid literal value, expected {{expected}}",
"unrecognized_keys": "Unrecognized key(s) in object: {{- keys}}",
Expand Down Expand Up @@ -31,11 +32,15 @@
},
"string": {
"inclusive": "String must contain at least {{minimum}} character(s)",
"not_inclusive": "String must contain over {{minimum}} character(s)"
"inclusive_with_path": "{{path}} must contain at least {{minimum}} character(s)",
"not_inclusive": "String must contain over {{minimum}} character(s)",
"not_inclusive_with_path": "{{path}} must contain over {{minimum}} character(s)"
},
"number": {
"inclusive": "Number must be greater than or equal to {{minimum}}",
"not_inclusive": "Number must be greater than {{minimum}}"
"inclusive_with_path": "{{path}} must be greater than or equal to {{minimum}}",
"not_inclusive": "Number must be greater than {{minimum}}",
"not_inclusive_with_path": "{{path}} must be greater than {{minimum}}"
},
"set": {
"inclusive": "Invalid input",
Expand All @@ -53,11 +58,15 @@
},
"string": {
"inclusive": "String must contain at most {{maximum}} character(s)",
"not_inclusive": "String must contain under {{maximum}} character(s)"
"inclusive_with_path": "{{path}} must contain at most {{maximum}} character(s)",
"not_inclusive": "String must contain under {{maximum}} character(s)",
"not_inclusive_with_path": "{{path}} must contain under {{maximum}} character(s)"
},
"number": {
"inclusive": "Number must be less than or equal to {{maximum}}",
"not_inclusive": "Number must be less than {{maximum}}"
"inclusive_with_path": "{{path}} must be less than or equal to {{maximum}}",
"not_inclusive": "Number must be less than {{maximum}}",
"not_inclusive_with_path": "{{path}} must be less than {{maximum}}"
},
"set": {
"inclusive": "Invalid input",
Expand Down
10 changes: 5 additions & 5 deletions examples/with-next-i18next/public/locales/es/common.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"User name": "Nombre de usuario",
"John Doe": "John Doe",
"Email": "Correo",
"Favorite number": "Número favorito",
"Submit": "Enviar"
"username": "Nombre de usuario",
"username_placeholder": "John Doe",
"email": "Correo",
"favoriteNumber": "Número favorito",
"submit": "Enviar"
}
10 changes: 5 additions & 5 deletions examples/with-next-i18next/public/locales/fr/common.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"User name": "Nom d'utilisateur",
"John Doe": "John Doe",
"Email": "E-mail",
"Favorite number": "Nombre favori",
"Submit": "Soumettre"
"username": "Nom d'utilisateur",
"username_placeholder": "John Doe",
"email": "E-mail",
"favoriteNumber": "Nombre favori",
"submit": "Soumettre"
}
10 changes: 5 additions & 5 deletions examples/with-next-i18next/public/locales/is/common.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"User name": "Notendanafn",
"John Doe": "Jón Jónsson",
"Email": "Netfang",
"Favorite number": "Uppáhalds tala",
"Submit": "Senda"
"username": "Notendanafn",
"username_placeholder": "Jón Jónsson",
"email": "Netfang",
"favoriteNumber": "Uppáhalds tala",
"submit": "Senda"
}
10 changes: 5 additions & 5 deletions examples/with-next-i18next/public/locales/ja/common.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"User name": "ユーザー名",
"John Doe": "山田太郎",
"Email": "メールアドレス",
"Favorite number": "好きな数字",
"Submit": "送信"
"username": "ユーザー名",
"username_placeholder": "山田太郎",
"email": "メールアドレス",
"favoriteNumber": "好きな数字",
"submit": "送信"
}
17 changes: 13 additions & 4 deletions examples/with-next-i18next/public/locales/ja/zod.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"errors": {
"invalid_type":
"{{expected}}での入力を期待していますが、{{received}}が入力されました。",
"invalid_type_with_path": "{{path}}は{{expected}}で入力してください。",
"invalid_type_received_undefined": "必須",
"invalid_literal": "無効なリテラル値です。{{expected}}を入力してください。",
"unrecognized_keys": "オブジェクトのキー{{- keys}}が識別できません。",
Expand Down Expand Up @@ -34,11 +35,15 @@
},
"string": {
"inclusive": "{{minimum}}文字以上の文字列である必要があります。",
"not_inclusive": "{{minimum}}文字より長い文字列である必要があります。"
"inclusive_with_path": "{{path}}は{{minimum}}文字以上の文字列である必要があります。",
"not_inclusive": "{{minimum}}文字より長い文字列である必要があります。",
"not_inclusive_with_path": "{{path}}は{{minimum}}文字より長い文字列である必要があります。"
},
"number": {
"inclusive": "{{minimum}}以上の数値である必要があります。",
"not_inclusive": "{{minimum}}より大きな数値である必要があります。"
"inclusive_with_path": "{{path}}は{{minimum}}以上の数値である必要があります。",
"not_inclusive": "{{minimum}}より大きな数値である必要があります。",
"not_inclusive_with_path": "{{path}}は{{minimum}}より大きな数値である必要があります。"
},
"set": {
"inclusive": "入力形式が間違っています。",
Expand All @@ -57,11 +62,15 @@
},
"string": {
"inclusive": "{{maximum}}文字以下の文字列である必要があります。",
"not_inclusive": "{{maximum}}文字より短い文字列である必要があります。"
"inclusive_with_path": "{{path}}は{{maximum}}文字以下の文字列である必要があります。",
"not_inclusive": "{{maximum}}文字より短い文字列である必要があります。",
"not_inclusive_with_path": "{{path}}は{{maximum}}文字より短い文字列である必要があります。"
},
"number": {
"inclusive": "{{maximum}}以下の数値である必要があります。",
"not_inclusive": "{{maximum}}より小さな数値である必要があります。"
"inclusive_with_path": "{{path}}は{{maximum}}以下の数値である必要があります。",
"not_inclusive": "{{maximum}}より小さな数値である必要があります。",
"not_inclusive_with_path": "{{path}}は{{maximum}}より小さな数値である必要があります。"
},
"set": {
"inclusive": "入力形式が間違っています。",
Expand Down
10 changes: 5 additions & 5 deletions examples/with-next-i18next/public/locales/pt/common.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"User name": "Nome do usuário",
"John Doe": "João Maria",
"Email": "E-mail",
"Favorite number": "Número favorito",
"Submit": "Enviar"
"username": "Nome do usuário",
"username_placeholder": "João Maria",
"email": "E-mail",
"favoriteNumber": "Número favorito",
"submit": "Enviar"
}
Loading

1 comment on commit e6a3410

@vercel
Copy link

@vercel vercel bot commented on e6a3410 Dec 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

zod-i18n – ./

zod-i18n-git-main-aiji42.vercel.app
zod-i18n-aiji42.vercel.app
zod-i18n.vercel.app

Please sign in to comment.