@hy_ong/zod-kit 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prettierrc.json +9 -0
- package/README.md +183 -0
- package/eslint.config.mjs +10 -0
- package/package.json +41 -0
- package/src/common/boolean.ts +37 -0
- package/src/common/date.ts +44 -0
- package/src/common/email.ts +45 -0
- package/src/common/integer.ts +47 -0
- package/src/common/number.ts +38 -0
- package/src/common/password.ts +34 -0
- package/src/common/text.ts +35 -0
- package/src/common/url.ts +38 -0
- package/src/config.ts +9 -0
- package/src/i18n/index.ts +22 -0
- package/src/i18n/locales/en.json +68 -0
- package/src/i18n/locales/zh-TW.json +65 -0
- package/src/index.ts +8 -0
- package/tests/common/boolean.test.ts +118 -0
- package/tests/common/email.test.ts +77 -0
- package/tests/common/integer.test.ts +90 -0
- package/tests/common/number.test.ts +77 -0
- package/tests/common/password.test.ts +89 -0
- package/tests/common/text.test.ts +89 -0
- package/tests/common/url.test.ts +69 -0
- package/tsconfig.json +17 -0
- package/vitest.config.ts +5 -0
package/.prettierrc.json
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# Zod Kit
|
|
2
|
+
|
|
3
|
+
A TypeScript library that provides common validation schemas built on top of [Zod](https://github.com/colinhacks/zod) with internationalization support.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🔍 Pre-built validation schemas for common data types
|
|
8
|
+
- 🌐 Internationalization support (English and Traditional Chinese)
|
|
9
|
+
- 📝 TypeScript-first with full type safety
|
|
10
|
+
- ⚡ Built on top of Zod for robust validation
|
|
11
|
+
- 🎯 Configurable validation options
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install zod-kit
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { email, password, text, number } from 'zod-kit'
|
|
23
|
+
|
|
24
|
+
// Email validation
|
|
25
|
+
const emailSchema = email({ label: 'Email' })
|
|
26
|
+
emailSchema.parse('user@example.com') // ✅
|
|
27
|
+
|
|
28
|
+
// Password validation with requirements
|
|
29
|
+
const passwordSchema = password({
|
|
30
|
+
label: 'Password',
|
|
31
|
+
min: 8,
|
|
32
|
+
uppercase: true,
|
|
33
|
+
lowercase: true,
|
|
34
|
+
digits: true,
|
|
35
|
+
special: true
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
// Text validation
|
|
39
|
+
const nameSchema = text({
|
|
40
|
+
label: 'Name',
|
|
41
|
+
min: 2,
|
|
42
|
+
max: 50
|
|
43
|
+
})
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Available Schemas
|
|
47
|
+
|
|
48
|
+
### Email
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
email({
|
|
52
|
+
label: string, // Field label for error messages
|
|
53
|
+
required? : boolean, // Default: true
|
|
54
|
+
domain? : string, // Restrict to specific domain
|
|
55
|
+
min? : number, // Minimum length
|
|
56
|
+
max? : number, // Maximum length
|
|
57
|
+
includes? : string // Must include substring
|
|
58
|
+
})
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Password
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
password({
|
|
65
|
+
label: string, // Field label for error messages
|
|
66
|
+
required? : boolean, // Default: true
|
|
67
|
+
min? : number, // Minimum length
|
|
68
|
+
max? : number, // Maximum length
|
|
69
|
+
uppercase? : boolean, // Require uppercase letters
|
|
70
|
+
lowercase? : boolean, // Require lowercase letters
|
|
71
|
+
digits? : boolean, // Require digits
|
|
72
|
+
special? : boolean // Require special characters
|
|
73
|
+
})
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Text
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
text({
|
|
80
|
+
label: string, // Field label for error messages
|
|
81
|
+
required? : boolean, // Default: true
|
|
82
|
+
min? : number, // Minimum length
|
|
83
|
+
max? : number, // Maximum length
|
|
84
|
+
includes? : string, // Must include substring
|
|
85
|
+
regex? : RegExp // Custom regex pattern
|
|
86
|
+
})
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Number
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
number({
|
|
93
|
+
label: string, // Field label for error messages
|
|
94
|
+
required? : boolean, // Default: true
|
|
95
|
+
min? : number, // Minimum value
|
|
96
|
+
max? : number, // Maximum value
|
|
97
|
+
positive? : boolean, // Must be positive
|
|
98
|
+
negative? : boolean, // Must be negative
|
|
99
|
+
finite? : boolean // Must be finite
|
|
100
|
+
})
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Integer
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
integer({
|
|
107
|
+
label: string, // Field label for error messages
|
|
108
|
+
required? : boolean, // Default: true
|
|
109
|
+
min? : number, // Minimum value
|
|
110
|
+
max? : number, // Maximum value
|
|
111
|
+
positive? : boolean, // Must be positive
|
|
112
|
+
negative? : boolean // Must be negative
|
|
113
|
+
})
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### URL
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
url({
|
|
120
|
+
label: string, // Field label for error messages
|
|
121
|
+
required? : boolean, // Default: true
|
|
122
|
+
protocol? : string[] // Allowed protocols (e.g., ['https'])
|
|
123
|
+
})
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Boolean
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
boolean({
|
|
130
|
+
label: string, // Field label for error messages
|
|
131
|
+
required? : boolean // Default: true
|
|
132
|
+
})
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Internationalization
|
|
136
|
+
|
|
137
|
+
Set the locale for error messages:
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
import { setLocale } from 'zod-kit'
|
|
141
|
+
|
|
142
|
+
// Set to English (default is Traditional Chinese)
|
|
143
|
+
setLocale('en')
|
|
144
|
+
|
|
145
|
+
// Set to Traditional Chinese
|
|
146
|
+
setLocale('zh-TW')
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Optional Fields
|
|
150
|
+
|
|
151
|
+
All schemas support optional validation by setting `required: false`:
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
const optionalEmail = email({
|
|
155
|
+
label: 'Email',
|
|
156
|
+
required: false
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
optionalEmail.parse(null) // ✅ null
|
|
160
|
+
optionalEmail.parse('') // ✅ null
|
|
161
|
+
optionalEmail.parse('user@example.com') // ✅ 'user@example.com'
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Development
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
# Install dependencies
|
|
168
|
+
npm install
|
|
169
|
+
|
|
170
|
+
# Run tests
|
|
171
|
+
npm test
|
|
172
|
+
|
|
173
|
+
# Build the package
|
|
174
|
+
npm run build
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## License
|
|
178
|
+
|
|
179
|
+
ISC
|
|
180
|
+
|
|
181
|
+
## Author
|
|
182
|
+
|
|
183
|
+
Ong Hoe Yuan
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hy_ong/zod-kit",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Zod Kit",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"zod",
|
|
7
|
+
"kit",
|
|
8
|
+
"zod-kit"
|
|
9
|
+
],
|
|
10
|
+
"homepage": "https://github.com/hy-ong/zod-kit#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/hy-ong/zod-kit/issues"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/hy-ong/zod-kit.git"
|
|
17
|
+
},
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"license": "ISC",
|
|
22
|
+
"author": "Ong Hoe Yuan",
|
|
23
|
+
"type": "commonjs",
|
|
24
|
+
"main": "dist/index.js",
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsup src/index.ts --format esm,cjs --dts",
|
|
27
|
+
"test": "vitest"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"dayjs": "^1.11.13",
|
|
31
|
+
"zod": "^4.0.14"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@eslint/js": "^9.32.0",
|
|
35
|
+
"eslint": "^9.32.0",
|
|
36
|
+
"prettier": "^3.6.2",
|
|
37
|
+
"typescript": "^5.8.3",
|
|
38
|
+
"typescript-eslint": "^8.38.0",
|
|
39
|
+
"vitest": "^3.2.4"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { z, ZodBoolean, ZodNullable, ZodType } from "zod"
|
|
2
|
+
import { t } from "../i18n"
|
|
3
|
+
|
|
4
|
+
export type BooleanOptions<IsRequired extends boolean = true> = {
|
|
5
|
+
required?: IsRequired
|
|
6
|
+
label: string
|
|
7
|
+
defaultValue?: IsRequired extends true ? boolean : boolean | null
|
|
8
|
+
shouldBe?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type BooleanSchema<IsRequired extends boolean> = IsRequired extends true ? ZodBoolean : ZodNullable<ZodBoolean>
|
|
12
|
+
|
|
13
|
+
export function boolean<IsRequired extends boolean = true>(options?: BooleanOptions<IsRequired>): BooleanSchema<IsRequired> {
|
|
14
|
+
const { required = true, label, defaultValue = null, shouldBe } = options ?? {}
|
|
15
|
+
|
|
16
|
+
let result: ZodType = z.preprocess(
|
|
17
|
+
(val) => {
|
|
18
|
+
if (val === "" || val === undefined || val === null) return defaultValue
|
|
19
|
+
if (val === "true" || val === 1 || val === "1") return true
|
|
20
|
+
if (val === "false" || val === 0 || val === "0") return false
|
|
21
|
+
return val
|
|
22
|
+
},
|
|
23
|
+
z.union([z.literal(true), z.literal(false), z.literal(null)])
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
if (required && defaultValue === null) {
|
|
27
|
+
result = result.refine((val) => val !== null, { message: t("common.boolean.required", { label }) })
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (shouldBe === true) {
|
|
31
|
+
result = result.refine((val) => val === true, { message: t("common.boolean.shouldBe.true", { label }) })
|
|
32
|
+
} else if (shouldBe === false) {
|
|
33
|
+
result = result.refine((val) => val === false, { message: t("common.boolean.shouldBe.false", { label }) })
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return result as IsRequired extends true ? ZodBoolean : ZodNullable<ZodBoolean>
|
|
37
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { z, ZodNullable, ZodString } from "zod"
|
|
2
|
+
import { t } from "../i18n"
|
|
3
|
+
import dayjs from "dayjs"
|
|
4
|
+
import customParseFormat from "dayjs/plugin/customParseFormat"
|
|
5
|
+
import isSameOrAfter from "dayjs/plugin/isSameOrAfter"
|
|
6
|
+
import isSameOrBefore from "dayjs/plugin/isSameOrBefore"
|
|
7
|
+
|
|
8
|
+
dayjs.extend(isSameOrAfter)
|
|
9
|
+
dayjs.extend(isSameOrBefore)
|
|
10
|
+
dayjs.extend(customParseFormat)
|
|
11
|
+
|
|
12
|
+
export type DateOptions<IsRequired extends boolean = true> = {
|
|
13
|
+
required?: IsRequired
|
|
14
|
+
label: string
|
|
15
|
+
min?: number
|
|
16
|
+
max?: number
|
|
17
|
+
format?: string
|
|
18
|
+
includes?: string
|
|
19
|
+
defaultValue?: IsRequired extends true ? string : string | null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type DateSchema<IsRequired extends boolean> = IsRequired extends true ? ZodString : ZodNullable<ZodString>
|
|
23
|
+
|
|
24
|
+
export function date<IsRequired extends boolean = true>(options?: DateOptions<IsRequired>): DateSchema<IsRequired> {
|
|
25
|
+
const { required = true, label, min, max, format = "YYYY-MM-DD", includes, defaultValue = null } = options ?? {}
|
|
26
|
+
|
|
27
|
+
const baseSchema = required
|
|
28
|
+
? z.preprocess((val) => (val === "" || val === null || val === undefined ? defaultValue : val), z.coerce.string().trim())
|
|
29
|
+
: z.preprocess((val) => (val === "" || val === null || val === undefined ? defaultValue : val), z.coerce.string().trim().nullable())
|
|
30
|
+
|
|
31
|
+
const schema = baseSchema
|
|
32
|
+
.refine(
|
|
33
|
+
(val) => {
|
|
34
|
+
if (!val) return !required
|
|
35
|
+
return dayjs(val, format, true).isValid()
|
|
36
|
+
},
|
|
37
|
+
{ message: t("common.date.format", { label, format }) }
|
|
38
|
+
)
|
|
39
|
+
.refine((val) => val === null || min === undefined || dayjs(val, format).isSameOrAfter(dayjs(min, format)), { message: t("common.date.min", { label, min }) })
|
|
40
|
+
.refine((val) => val === null || max === undefined || dayjs(val, format).isSameOrBefore(dayjs(max, format)), { message: t("common.date.max", { label, max }) })
|
|
41
|
+
.refine((val) => val === null || includes === undefined || val.includes(includes), { message: t("common.date.includes", { label, includes }) })
|
|
42
|
+
|
|
43
|
+
return schema as unknown as DateSchema<IsRequired>
|
|
44
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { z, ZodNullable, ZodString } from "zod"
|
|
2
|
+
import { t } from "../i18n"
|
|
3
|
+
import { TextSchema } from "./text"
|
|
4
|
+
|
|
5
|
+
export type EmailOptions<IsRequired extends boolean = true> = {
|
|
6
|
+
required?: IsRequired
|
|
7
|
+
label: string
|
|
8
|
+
domain?: string
|
|
9
|
+
min?: number
|
|
10
|
+
max?: number
|
|
11
|
+
includes?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type EmailSchema<IsRequired extends boolean> = IsRequired extends true ? ZodString : ZodNullable<ZodString>
|
|
15
|
+
|
|
16
|
+
export function email<IsRequired extends boolean = true>(options?: EmailOptions<IsRequired>): EmailSchema<IsRequired> {
|
|
17
|
+
const { required = true, label, domain, min, max, includes } = options ?? {}
|
|
18
|
+
|
|
19
|
+
const baseSchema = required
|
|
20
|
+
? z.preprocess(
|
|
21
|
+
(val) => (val === "" || val === null || val === undefined ? null : val),
|
|
22
|
+
z.email({
|
|
23
|
+
error: (issue) => {
|
|
24
|
+
if (issue.code === "invalid_type") return t("common.email.required", { label })
|
|
25
|
+
else if (issue.code === "invalid_format") return t("common.email.invalid", { label })
|
|
26
|
+
return t("common.email.invalid", { label })
|
|
27
|
+
},
|
|
28
|
+
})
|
|
29
|
+
)
|
|
30
|
+
: z.preprocess((val) => (val === "" || val === null || val === undefined ? null : val), z.email({ message: t("common.email.invalid", { label }) }).nullable())
|
|
31
|
+
|
|
32
|
+
const schema = baseSchema
|
|
33
|
+
.refine((val) => (required ? val !== "" && val !== "null" && val !== "undefined" : true), { message: t("common.text.required", { label }) })
|
|
34
|
+
.refine((val) => val === null || min === undefined || val.length >= min, { message: t("common.text.min", { label, min }) })
|
|
35
|
+
.refine((val) => val === null || max === undefined || val.length <= max, { message: t("common.text.max", { label, max }) })
|
|
36
|
+
.refine((val) => val === null || includes === undefined || val.includes(includes), { message: t("common.text.includes", { label, includes }) })
|
|
37
|
+
.refine(
|
|
38
|
+
(val) => {
|
|
39
|
+
if (val === null || domain === undefined) return true
|
|
40
|
+
return val.split("@")[1]?.toLowerCase() === domain.toLowerCase()
|
|
41
|
+
},
|
|
42
|
+
{ message: t("common.email.domain", { label, domain }) }
|
|
43
|
+
)
|
|
44
|
+
return schema as unknown as TextSchema<IsRequired>
|
|
45
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { z, ZodNullable, ZodNumber } from "zod"
|
|
2
|
+
import { t } from "../i18n"
|
|
3
|
+
|
|
4
|
+
export type IntegerOptions<IsRequired extends boolean = true> = {
|
|
5
|
+
required?: IsRequired
|
|
6
|
+
label: string
|
|
7
|
+
min?: number
|
|
8
|
+
max?: number
|
|
9
|
+
defaultValue?: IsRequired extends true ? number : number | null
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type IntegerSchema<IsRequired extends boolean> = IsRequired extends true ? ZodNumber : ZodNullable<ZodNumber>
|
|
13
|
+
|
|
14
|
+
export function integer<IsRequired extends boolean = true>(options?: IntegerOptions<IsRequired>): IntegerSchema<IsRequired> {
|
|
15
|
+
const { required = true, label, min, max, defaultValue } = options ?? {}
|
|
16
|
+
|
|
17
|
+
const schema = z
|
|
18
|
+
.preprocess(
|
|
19
|
+
(val) => {
|
|
20
|
+
if (val === "" || val === undefined || val === null) return defaultValue ?? null
|
|
21
|
+
return typeof val === "string" ? Number(val) : val
|
|
22
|
+
},
|
|
23
|
+
z.union([
|
|
24
|
+
z.number({
|
|
25
|
+
error: (issue) => {
|
|
26
|
+
if (issue.code === "invalid_type") return t("common.integer.integer", { label })
|
|
27
|
+
return t("common.integer.required", { label })
|
|
28
|
+
},
|
|
29
|
+
}),
|
|
30
|
+
z.null(),
|
|
31
|
+
])
|
|
32
|
+
)
|
|
33
|
+
.refine((val) => !required || val !== null, {
|
|
34
|
+
message: t("common.integer.required", { label }),
|
|
35
|
+
})
|
|
36
|
+
.refine((val) => val === null || Number.isInteger(val), {
|
|
37
|
+
message: t("common.integer.integer", { label }),
|
|
38
|
+
})
|
|
39
|
+
.refine((val) => val === null || min === undefined || val >= min, {
|
|
40
|
+
message: t("common.integer.min", { label, min }),
|
|
41
|
+
})
|
|
42
|
+
.refine((val) => val === null || max === undefined || val <= max, {
|
|
43
|
+
message: t("common.integer.max", { label, max }),
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
return schema as unknown as IntegerSchema<IsRequired>
|
|
47
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z, ZodNullable, ZodNumber } from "zod"
|
|
2
|
+
import { t } from "../i18n"
|
|
3
|
+
|
|
4
|
+
export type NumberOptions<IsRequired extends boolean = true> = {
|
|
5
|
+
required?: IsRequired
|
|
6
|
+
label: string
|
|
7
|
+
min?: number
|
|
8
|
+
max?: number
|
|
9
|
+
defaultValue?: IsRequired extends true ? number : number | null
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type NumberSchema<IsRequired extends boolean> = IsRequired extends true ? ZodNumber : ZodNullable<ZodNumber>
|
|
13
|
+
|
|
14
|
+
export function number<IsRequired extends boolean = true>(options?: NumberOptions<IsRequired>): NumberSchema<IsRequired> {
|
|
15
|
+
const { required = true, label, min, max, defaultValue } = options ?? {}
|
|
16
|
+
|
|
17
|
+
const schema = z
|
|
18
|
+
.preprocess(
|
|
19
|
+
(val) => {
|
|
20
|
+
if (val === "" || val === undefined || val === null) return defaultValue ?? null
|
|
21
|
+
return typeof val === "string" ? Number(val) : val
|
|
22
|
+
},
|
|
23
|
+
z.union([
|
|
24
|
+
z.number({
|
|
25
|
+
error: (issue) => {
|
|
26
|
+
if (issue.code === "invalid_type") return t("common.number.integer", { label })
|
|
27
|
+
return t("common.number.required", { label })
|
|
28
|
+
},
|
|
29
|
+
}),
|
|
30
|
+
z.null(),
|
|
31
|
+
])
|
|
32
|
+
)
|
|
33
|
+
.refine((val) => !required || val !== null, { message: t("common.number.required", { label }) })
|
|
34
|
+
.refine((val) => val === null || min === undefined || val >= min, { message: t("common.number.min", { label, min }) })
|
|
35
|
+
.refine((val) => val === null || max === undefined || val <= max, { message: t("common.number.max", { label, max }) })
|
|
36
|
+
|
|
37
|
+
return schema as unknown as NumberSchema<IsRequired>
|
|
38
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { z, ZodNullable, ZodString } from "zod"
|
|
2
|
+
import { t } from "../i18n"
|
|
3
|
+
|
|
4
|
+
export type PasswordOptions<IsRequired extends boolean = true> = {
|
|
5
|
+
required?: IsRequired
|
|
6
|
+
label: string
|
|
7
|
+
min?: number
|
|
8
|
+
max?: number
|
|
9
|
+
uppercase?: boolean
|
|
10
|
+
lowercase?: boolean
|
|
11
|
+
digits?: boolean
|
|
12
|
+
special?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type PasswordSchema<IsRequired extends boolean> = IsRequired extends true ? ZodString : ZodNullable<ZodString>
|
|
16
|
+
|
|
17
|
+
export function password<IsRequired extends boolean = true>(options?: PasswordOptions<IsRequired>): PasswordSchema<IsRequired> {
|
|
18
|
+
const { required = true, label, min, max, uppercase, lowercase, digits, special } = options ?? {}
|
|
19
|
+
|
|
20
|
+
const baseSchema = required
|
|
21
|
+
? z.preprocess((val) => (val === "" || val === null || val === undefined ? null : val), z.coerce.string().trim())
|
|
22
|
+
: z.preprocess((val) => (val === "" || val === null || val === undefined ? null : val), z.coerce.string().trim().nullable())
|
|
23
|
+
|
|
24
|
+
const schema = baseSchema
|
|
25
|
+
.refine((val) => (required ? val !== "" && val !== "null" && val !== "undefined" : true), { message: t("common.password.required", { label }) })
|
|
26
|
+
.refine((val) => val === null || min === undefined || val.length >= min, { message: t("common.password.min", { label, min }) })
|
|
27
|
+
.refine((val) => val === null || max === undefined || val.length <= max, { message: t("common.password.max", { label, max }) })
|
|
28
|
+
.refine((val) => val === null || !uppercase || /[A-Z]/.test(val), { message: t("common.password.uppercase", { label }) })
|
|
29
|
+
.refine((val) => val === null || !lowercase || /[a-z]/.test(val), { message: t("common.password.lowercase", { label }) })
|
|
30
|
+
.refine((val) => val === null || !special || /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]+/.test(val), { message: t("common.password.special", { label }) })
|
|
31
|
+
.refine((val) => val === null || !digits || /[0-9]/.test(val), { message: t("common.password.digits", { label }) })
|
|
32
|
+
|
|
33
|
+
return schema as unknown as PasswordSchema<IsRequired>
|
|
34
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z, ZodNullable, ZodString } from "zod"
|
|
2
|
+
import { t } from "../i18n"
|
|
3
|
+
|
|
4
|
+
export type TextOptions<IsRequired extends boolean = true> = {
|
|
5
|
+
required?: IsRequired
|
|
6
|
+
label: string
|
|
7
|
+
min?: number
|
|
8
|
+
max?: number
|
|
9
|
+
startsWith?: string
|
|
10
|
+
endsWith?: string
|
|
11
|
+
includes?: string
|
|
12
|
+
regex?: RegExp
|
|
13
|
+
defaultValue?: IsRequired extends true ? string : string | null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type TextSchema<IsRequired extends boolean> = IsRequired extends true ? ZodString : ZodNullable<ZodString>
|
|
17
|
+
|
|
18
|
+
export function text<IsRequired extends boolean = true>(options?: TextOptions<IsRequired>): TextSchema<IsRequired> {
|
|
19
|
+
const { required = true, label, min, max, startsWith, endsWith, includes, regex, defaultValue = null } = options ?? {}
|
|
20
|
+
|
|
21
|
+
const baseSchema = required
|
|
22
|
+
? z.preprocess((val) => (val === "" || val === null || val === undefined ? defaultValue : val), z.coerce.string().trim())
|
|
23
|
+
: z.preprocess((val) => (val === "" || val === null || val === undefined ? defaultValue : val), z.coerce.string().trim().nullable())
|
|
24
|
+
|
|
25
|
+
const schema = baseSchema
|
|
26
|
+
.refine((val) => (required ? val !== "" && val !== "null" && val !== "undefined" : true), { message: t("common.text.required", { label }) })
|
|
27
|
+
.refine((val) => val === null || min === undefined || val.length >= min, { message: t("common.text.min", { label, min }) })
|
|
28
|
+
.refine((val) => val === null || max === undefined || val.length <= max, { message: t("common.text.max", { label, max }) })
|
|
29
|
+
.refine((val) => val === null || startsWith === undefined || val.startsWith(startsWith), { message: t("common.text.startsWith", { label, startsWith }) })
|
|
30
|
+
.refine((val) => val === null || endsWith === undefined || val.endsWith(endsWith), { message: t("common.text.endsWith", { label, endsWith }) })
|
|
31
|
+
.refine((val) => val === null || includes === undefined || val.includes(includes), { message: t("common.text.includes", { label, includes }) })
|
|
32
|
+
.refine((val) => val === null || regex === undefined || regex.test(val), { message: t("common.text.invalid", { label, regex }) })
|
|
33
|
+
|
|
34
|
+
return schema as unknown as TextSchema<IsRequired>
|
|
35
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z, ZodNullable, ZodString } from "zod"
|
|
2
|
+
import { t } from "../i18n"
|
|
3
|
+
import { TextSchema } from "./text"
|
|
4
|
+
|
|
5
|
+
export type UrlOptions<IsRequired extends boolean = true> = {
|
|
6
|
+
required?: IsRequired
|
|
7
|
+
label: string
|
|
8
|
+
min?: number
|
|
9
|
+
max?: number
|
|
10
|
+
includes?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type UrlSchema<IsRequired extends boolean> = IsRequired extends true ? ZodString : ZodNullable<ZodString>
|
|
14
|
+
|
|
15
|
+
export function url<IsRequired extends boolean = true>(options?: UrlOptions<IsRequired>): UrlSchema<IsRequired> {
|
|
16
|
+
const { required = true, label, min, max, includes } = options ?? {}
|
|
17
|
+
|
|
18
|
+
const baseSchema = required
|
|
19
|
+
? z.preprocess(
|
|
20
|
+
(val) => (val === "" || val === null || val === undefined ? null : val),
|
|
21
|
+
z.url({
|
|
22
|
+
error: (issue) => {
|
|
23
|
+
if (issue.code === "invalid_type") return t("common.url.required", { label })
|
|
24
|
+
else if (issue.code === "invalid_format") return t("common.url.invalid", { label })
|
|
25
|
+
return t("common.url.invalid", { label })
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
)
|
|
29
|
+
: z.preprocess((val) => (val === "" || val === null || val === undefined ? null : val), z.url({ message: t("common.url.invalid", { label }) }).nullable())
|
|
30
|
+
|
|
31
|
+
const schema = baseSchema
|
|
32
|
+
.refine((val) => (required ? val !== "" && val !== "null" && val !== "undefined" : true), { message: t("common.text.required", { label }) })
|
|
33
|
+
.refine((val) => val === null || min === undefined || val.length >= min, { message: t("common.text.min", { label, min }) })
|
|
34
|
+
.refine((val) => val === null || max === undefined || val.length <= max, { message: t("common.text.max", { label, max }) })
|
|
35
|
+
.refine((val) => val === null || includes === undefined || val.includes(includes), { message: t("common.text.includes", { label, includes }) })
|
|
36
|
+
|
|
37
|
+
return schema as unknown as TextSchema<IsRequired>
|
|
38
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import zhTW from "./locales/zh-TW.json"
|
|
2
|
+
import en from "./locales/en.json"
|
|
3
|
+
import { getLocale } from "../config"
|
|
4
|
+
|
|
5
|
+
const dicts = {
|
|
6
|
+
"zh-TW": zhTW,
|
|
7
|
+
en,
|
|
8
|
+
} satisfies Record<string, unknown>
|
|
9
|
+
|
|
10
|
+
export const t = (key: string, params: Record<string, any> = {}): string => {
|
|
11
|
+
const locale = getLocale()
|
|
12
|
+
const dict = dicts[locale] || {}
|
|
13
|
+
|
|
14
|
+
const template = getNestedValue(dict, key) ?? key
|
|
15
|
+
|
|
16
|
+
return template.replace(/\$\{(\w+)}/g, (_, k) => params[k] ?? "")
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getNestedValue(obj: Record<string, any>, path: string): string | undefined {
|
|
20
|
+
const result = path.split(".").reduce((acc: any, part: string) => (acc && typeof acc === "object" ? acc[part] : undefined), obj)
|
|
21
|
+
return typeof result === "string" ? result : undefined
|
|
22
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"common": {
|
|
3
|
+
"boolean": {
|
|
4
|
+
"required": "${label} is required",
|
|
5
|
+
"shouldBe": {
|
|
6
|
+
"true": "${label} must be True",
|
|
7
|
+
"false": "${label} must be False"
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
"email": {
|
|
11
|
+
"required": "${label} is required",
|
|
12
|
+
"min": "${label} must be at least ${min} characters",
|
|
13
|
+
"max": "${label} must be at most ${max} characters",
|
|
14
|
+
"includes": "${label} must include ${includes}",
|
|
15
|
+
"invalid": "${label} is invalid format",
|
|
16
|
+
"domain": "${label} must be under the domain @${domain}"
|
|
17
|
+
},
|
|
18
|
+
"url": {
|
|
19
|
+
"required": "${label} is required",
|
|
20
|
+
"min": "${label} must be at least ${min} characters",
|
|
21
|
+
"max": "${label} must be at most ${max} characters",
|
|
22
|
+
"includes": "${label} must include ${includes}",
|
|
23
|
+
"invalid": "${label} is invalid format"
|
|
24
|
+
},
|
|
25
|
+
"password": {
|
|
26
|
+
"required": "${label} is required",
|
|
27
|
+
"min": "${label} must be at least ${min} characters",
|
|
28
|
+
"max": "${label} must be at most ${max} characters",
|
|
29
|
+
"uppercase": "${label} must include at least one uppercase letter",
|
|
30
|
+
"lowercase": "${label} must include at least one lowercase letter",
|
|
31
|
+
"digits": "${label} must include at least one digit",
|
|
32
|
+
"special": "${label} must include at least one special character"
|
|
33
|
+
},
|
|
34
|
+
"number": {
|
|
35
|
+
"required": "${label} is required",
|
|
36
|
+
"min": "${label} must be at least ${min}",
|
|
37
|
+
"max": "${label} must be at most ${max}"
|
|
38
|
+
},
|
|
39
|
+
"integer": {
|
|
40
|
+
"required": "${label} is required",
|
|
41
|
+
"min": "${label} must be at least ${min}",
|
|
42
|
+
"max": "${label} must be at most ${max}",
|
|
43
|
+
"integer": "${label} must be an integer"
|
|
44
|
+
},
|
|
45
|
+
"float": {
|
|
46
|
+
"required": "${label} is required",
|
|
47
|
+
"min": "${label} must be at least ${min}",
|
|
48
|
+
"max": "${label} must be at most ${max}",
|
|
49
|
+
"float": "${label} must be an float"
|
|
50
|
+
},
|
|
51
|
+
"text": {
|
|
52
|
+
"required": "${label} is required",
|
|
53
|
+
"min": "${label} must be at least ${min} characters",
|
|
54
|
+
"max": "${label} must be at most ${max} characters",
|
|
55
|
+
"startsWith": "${label} must start with ${startsWith}",
|
|
56
|
+
"endsWith": "${label} must end with ${endsWith}",
|
|
57
|
+
"includes": "${label} must include ${includes}",
|
|
58
|
+
"invalid": "${label} is invalid format"
|
|
59
|
+
},
|
|
60
|
+
"date": {
|
|
61
|
+
"required": "${label} is required",
|
|
62
|
+
"min": "${label} must be at least ${min} characters",
|
|
63
|
+
"max": "${label} must be at most ${max} characters",
|
|
64
|
+
"includes": "${label} must include ${includes}",
|
|
65
|
+
"format": "${label} must be in ${format} format"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|