@hy_ong/zod-kit 0.0.4 → 0.0.6
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/.claude/settings.local.json +28 -0
- package/LICENSE +21 -0
- package/README.md +465 -97
- package/debug.js +21 -0
- package/debug.ts +16 -0
- package/dist/index.cjs +3127 -146
- package/dist/index.d.cts +3021 -25
- package/dist/index.d.ts +3021 -25
- package/dist/index.js +3081 -144
- package/eslint.config.mts +8 -0
- package/package.json +10 -9
- package/src/config.ts +1 -1
- package/src/i18n/locales/en.json +161 -25
- package/src/i18n/locales/zh-TW.json +165 -26
- package/src/index.ts +17 -7
- package/src/validators/common/boolean.ts +191 -0
- package/src/validators/common/date.ts +299 -0
- package/src/validators/common/datetime.ts +673 -0
- package/src/validators/common/email.ts +313 -0
- package/src/validators/common/file.ts +384 -0
- package/src/validators/common/id.ts +471 -0
- package/src/validators/common/number.ts +319 -0
- package/src/validators/common/password.ts +386 -0
- package/src/validators/common/text.ts +271 -0
- package/src/validators/common/time.ts +600 -0
- package/src/validators/common/url.ts +347 -0
- package/src/validators/taiwan/business-id.ts +262 -0
- package/src/validators/taiwan/fax.ts +327 -0
- package/src/validators/taiwan/mobile.ts +242 -0
- package/src/validators/taiwan/national-id.ts +425 -0
- package/src/validators/taiwan/postal-code.ts +1049 -0
- package/src/validators/taiwan/tel.ts +330 -0
- package/tests/common/boolean.test.ts +340 -92
- package/tests/common/date.test.ts +458 -0
- package/tests/common/datetime.test.ts +693 -0
- package/tests/common/email.test.ts +232 -60
- package/tests/common/file.test.ts +479 -0
- package/tests/common/id.test.ts +535 -0
- package/tests/common/number.test.ts +230 -60
- package/tests/common/password.test.ts +271 -44
- package/tests/common/text.test.ts +210 -13
- package/tests/common/time.test.ts +528 -0
- package/tests/common/url.test.ts +492 -67
- package/tests/taiwan/business-id.test.ts +240 -0
- package/tests/taiwan/fax.test.ts +463 -0
- package/tests/taiwan/mobile.test.ts +373 -0
- package/tests/taiwan/national-id.test.ts +435 -0
- package/tests/taiwan/postal-code.test.ts +705 -0
- package/tests/taiwan/tel.test.ts +467 -0
- package/eslint.config.mjs +0 -10
- package/src/common/boolean.ts +0 -36
- package/src/common/date.ts +0 -43
- package/src/common/email.ts +0 -44
- package/src/common/integer.ts +0 -46
- package/src/common/number.ts +0 -37
- package/src/common/password.ts +0 -33
- package/src/common/text.ts +0 -34
- package/src/common/url.ts +0 -37
- package/tests/common/integer.test.ts +0 -90
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Taiwan Fax Number validator for Zod Kit
|
|
3
|
+
*
|
|
4
|
+
* Provides validation for Taiwan fax numbers according to the official 2024
|
|
5
|
+
* telecom numbering plan. Uses the same format as landline telephone numbers.
|
|
6
|
+
*
|
|
7
|
+
* @author Ong Hoe Yuan
|
|
8
|
+
* @version 0.0.5
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { z, ZodNullable, ZodString } from "zod"
|
|
12
|
+
import { t } from "../../i18n"
|
|
13
|
+
import { getLocale, type Locale } from "../../config"
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Type definition for fax number validation error messages
|
|
17
|
+
*
|
|
18
|
+
* @interface FaxMessages
|
|
19
|
+
* @property {string} [required] - Message when field is required but empty
|
|
20
|
+
* @property {string} [invalid] - Message when fax number format is invalid
|
|
21
|
+
* @property {string} [notInWhitelist] - Message when fax number is not in whitelist
|
|
22
|
+
*/
|
|
23
|
+
export type FaxMessages = {
|
|
24
|
+
required?: string
|
|
25
|
+
invalid?: string
|
|
26
|
+
notInWhitelist?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Configuration options for Taiwan fax number validation
|
|
31
|
+
*
|
|
32
|
+
* @template IsRequired - Whether the field is required (affects return type)
|
|
33
|
+
*
|
|
34
|
+
* @interface FaxOptions
|
|
35
|
+
* @property {IsRequired} [required=true] - Whether the field is required
|
|
36
|
+
* @property {string[]} [whitelist] - Array of specific fax numbers that are always allowed
|
|
37
|
+
* @property {Function} [transform] - Custom transformation function for fax number
|
|
38
|
+
* @property {string | null} [defaultValue] - Default value when input is empty
|
|
39
|
+
* @property {Record<Locale, FaxMessages>} [i18n] - Custom error messages for different locales
|
|
40
|
+
*/
|
|
41
|
+
export type FaxOptions<IsRequired extends boolean = true> = {
|
|
42
|
+
required?: IsRequired
|
|
43
|
+
whitelist?: string[]
|
|
44
|
+
transform?: (value: string) => string
|
|
45
|
+
defaultValue?: IsRequired extends true ? string : string | null
|
|
46
|
+
i18n?: Record<Locale, FaxMessages>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Type alias for fax number validation schema based on required flag
|
|
51
|
+
*
|
|
52
|
+
* @template IsRequired - Whether the field is required
|
|
53
|
+
* @typedef FaxSchema
|
|
54
|
+
* @description Returns ZodString if required, ZodNullable<ZodString> if optional
|
|
55
|
+
*/
|
|
56
|
+
export type FaxSchema<IsRequired extends boolean> = IsRequired extends true ? ZodString : ZodNullable<ZodString>
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Validates Taiwan fax number format (Official 2024 rules - same as landline)
|
|
60
|
+
*
|
|
61
|
+
* @param {string} value - The fax number to validate
|
|
62
|
+
* @returns {boolean} True if the fax number is valid
|
|
63
|
+
*
|
|
64
|
+
* @description
|
|
65
|
+
* Validates Taiwan fax numbers according to the official 2024 telecom numbering plan.
|
|
66
|
+
* Fax numbers follow the same format as landline telephone numbers in Taiwan.
|
|
67
|
+
*
|
|
68
|
+
* Supported area codes and formats (same as landline):
|
|
69
|
+
* - 02: Taipei, New Taipei, Keelung - 8 digits (2&3&5~8+7D)
|
|
70
|
+
* - 03: Taoyuan, Hsinchu, Yilan, Hualien - 7 digits
|
|
71
|
+
* - 037: Miaoli - 6 digits (2~9+5D)
|
|
72
|
+
* - 04: Taichung, Changhua - 7 digits
|
|
73
|
+
* - 049: Nantou - 7 digits (2~9+6D)
|
|
74
|
+
* - 05: Yunlin, Chiayi - 7 digits
|
|
75
|
+
* - 06: Tainan - 7 digits
|
|
76
|
+
* - 07: Kaohsiung - 7 digits (2~9+6D)
|
|
77
|
+
* - 08: Pingtung - 7 digits (4&7&8+6D)
|
|
78
|
+
* - 082: Kinmen - 6 digits (2~5&7~9+5D)
|
|
79
|
+
* - 0826: Wuqiu - 5 digits (6+4D)
|
|
80
|
+
* - 0836: Matsu - 5 digits (2~9+4D)
|
|
81
|
+
* - 089: Taitung - 6 digits (2~9+5D)
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```typescript
|
|
85
|
+
* validateTaiwanFax("0223456789") // true (Taipei area)
|
|
86
|
+
* validateTaiwanFax("0312345678") // true (Taoyuan area)
|
|
87
|
+
* validateTaiwanFax("037234567") // true (Miaoli area)
|
|
88
|
+
* validateTaiwanFax("02-2345-6789") // true (with separators)
|
|
89
|
+
* validateTaiwanFax("0812345678") // false (invalid for 08 area)
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
const validateTaiwanFax = (value: string): boolean => {
|
|
93
|
+
// Official Taiwan fax formats according to telecom numbering plan (same as landline):
|
|
94
|
+
// 02: Taipei, New Taipei, Keelung - 8 digits (2&3&5~8+7D)
|
|
95
|
+
// 03: Taoyuan, Hsinchu, Yilan, Hualien - 7 digits
|
|
96
|
+
// 037: Miaoli - 6 digits (2~9+5D)
|
|
97
|
+
// 04: Taichung, Changhua - 7 digits
|
|
98
|
+
// 049: Nantou - 7 digits (2~9+6D)
|
|
99
|
+
// 05: Yunlin, Chiayi - 7 digits
|
|
100
|
+
// 06: Tainan - 7 digits
|
|
101
|
+
// 07: Kaohsiung - 7 digits (2~9+6D)
|
|
102
|
+
// 08: Pingtung - 7 digits (4&7&8+6D)
|
|
103
|
+
// 082: Kinmen - 6 digits (2~5&7~9+5D)
|
|
104
|
+
// 0826: Wuqiu - 5 digits (6+4D)
|
|
105
|
+
// 0836: Matsu - 5 digits (2~9+4D)
|
|
106
|
+
// 089: Taitung - 6 digits (2~9+5D)
|
|
107
|
+
|
|
108
|
+
// Remove common separators for validation
|
|
109
|
+
const cleanValue = value.replace(/[-\s]/g, "")
|
|
110
|
+
|
|
111
|
+
// Basic format: starts with 0, then area code, then number
|
|
112
|
+
if (!/^0\d{7,10}$/.test(cleanValue)) {
|
|
113
|
+
return false
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check 4-digit area codes first
|
|
117
|
+
const areaCode4 = cleanValue.substring(0, 4)
|
|
118
|
+
if (areaCode4 === '0826') {
|
|
119
|
+
// Wuqiu: 0826 + 5 digits (6+4D), total 9 digits
|
|
120
|
+
return cleanValue.length === 9 && /^0826[6]\d{4}$/.test(cleanValue)
|
|
121
|
+
}
|
|
122
|
+
if (areaCode4 === '0836') {
|
|
123
|
+
// Matsu: 0836 + 5 digits (2~9+4D), total 9 digits
|
|
124
|
+
return cleanValue.length === 9 && /^0836[2-9]\d{4}$/.test(cleanValue)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Check 3-digit area codes
|
|
128
|
+
const areaCode3 = cleanValue.substring(0, 3)
|
|
129
|
+
if (areaCode3 === '037') {
|
|
130
|
+
// Miaoli: 037 + 6 digits (2~9+5D), total 9 digits
|
|
131
|
+
return cleanValue.length === 9 && /^037[2-9]\d{5}$/.test(cleanValue)
|
|
132
|
+
}
|
|
133
|
+
if (areaCode3 === '049') {
|
|
134
|
+
// Nantou: 049 + 7 digits (2~9+6D), total 10 digits
|
|
135
|
+
return cleanValue.length === 10 && /^049[2-9]\d{6}$/.test(cleanValue)
|
|
136
|
+
}
|
|
137
|
+
if (areaCode3 === '082') {
|
|
138
|
+
// Kinmen: 082 + 6 digits (2~5&7~9+5D), total 9 digits
|
|
139
|
+
return cleanValue.length === 9 && /^082[2-57-9]\d{5}$/.test(cleanValue)
|
|
140
|
+
}
|
|
141
|
+
if (areaCode3 === '089') {
|
|
142
|
+
// Taitung: 089 + 6 digits (2~9+5D), total 9 digits
|
|
143
|
+
return cleanValue.length === 9 && /^089[2-9]\d{5}$/.test(cleanValue)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Check 2-digit area codes
|
|
147
|
+
const areaCode2 = cleanValue.substring(0, 2)
|
|
148
|
+
|
|
149
|
+
if (areaCode2 === '02') {
|
|
150
|
+
// Taipei, New Taipei, Keelung: 02 + 8 digits (2&3&5~8+7D), total 10 digits
|
|
151
|
+
return cleanValue.length === 10 && /^02[235-8]\d{7}$/.test(cleanValue)
|
|
152
|
+
}
|
|
153
|
+
if (['03', '04', '05', '06'].includes(areaCode2)) {
|
|
154
|
+
// Taoyuan/Hsinchu/Yilan/Hualien (03), Taichung/Changhua (04),
|
|
155
|
+
// Yunlin/Chiayi (05), Tainan (06): 7 digits, total 9 digits
|
|
156
|
+
return cleanValue.length === 9
|
|
157
|
+
}
|
|
158
|
+
if (areaCode2 === '07') {
|
|
159
|
+
// Kaohsiung: 07 + 7 digits (2~9+6D), total 9 digits
|
|
160
|
+
return cleanValue.length === 9 && /^07[2-9]\d{6}$/.test(cleanValue)
|
|
161
|
+
}
|
|
162
|
+
if (areaCode2 === '08') {
|
|
163
|
+
// Pingtung: 08 + 7 digits (4&7&8+6D), total 9 digits
|
|
164
|
+
return cleanValue.length === 9 && /^08[478]\d{6}$/.test(cleanValue)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return false
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Creates a Zod schema for Taiwan fax number validation
|
|
172
|
+
*
|
|
173
|
+
* @template IsRequired - Whether the field is required (affects return type)
|
|
174
|
+
* @param {FaxOptions<IsRequired>} [options] - Configuration options for fax validation
|
|
175
|
+
* @returns {FaxSchema<IsRequired>} Zod schema for fax number validation
|
|
176
|
+
*
|
|
177
|
+
* @description
|
|
178
|
+
* Creates a comprehensive Taiwan fax number validator with support for all Taiwan
|
|
179
|
+
* area codes. Fax numbers follow the same format as landline telephone numbers.
|
|
180
|
+
*
|
|
181
|
+
* Features:
|
|
182
|
+
* - Complete Taiwan area code support (same as landline)
|
|
183
|
+
* - Automatic separator handling (hyphens and spaces)
|
|
184
|
+
* - Area-specific number length and pattern validation
|
|
185
|
+
* - Whitelist functionality for specific allowed numbers
|
|
186
|
+
* - Automatic trimming and preprocessing
|
|
187
|
+
* - Custom transformation functions
|
|
188
|
+
* - Comprehensive internationalization
|
|
189
|
+
* - Optional field support
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* ```typescript
|
|
193
|
+
* // Basic fax number validation
|
|
194
|
+
* const basicSchema = fax()
|
|
195
|
+
* basicSchema.parse("0223456789") // ✓ Valid (Taipei)
|
|
196
|
+
* basicSchema.parse("0312345678") // ✓ Valid (Taoyuan)
|
|
197
|
+
* basicSchema.parse("02-2345-6789") // ✓ Valid (with separators)
|
|
198
|
+
* basicSchema.parse("0812345678") // ✗ Invalid (wrong format for 08)
|
|
199
|
+
*
|
|
200
|
+
* // With whitelist (only specific numbers allowed)
|
|
201
|
+
* const whitelistSchema = fax({
|
|
202
|
+
* whitelist: ["0223456789", "0312345678"]
|
|
203
|
+
* })
|
|
204
|
+
* whitelistSchema.parse("0223456789") // ✓ Valid (in whitelist)
|
|
205
|
+
* whitelistSchema.parse("0287654321") // ✗ Invalid (not in whitelist)
|
|
206
|
+
*
|
|
207
|
+
* // Optional fax number
|
|
208
|
+
* const optionalSchema = fax({ required: false })
|
|
209
|
+
* optionalSchema.parse("") // ✓ Valid (returns null)
|
|
210
|
+
* optionalSchema.parse("0223456789") // ✓ Valid
|
|
211
|
+
*
|
|
212
|
+
* // With custom transformation
|
|
213
|
+
* const transformSchema = fax({
|
|
214
|
+
* transform: (value) => value.replace(/[^0-9]/g, '') // Keep only digits
|
|
215
|
+
* })
|
|
216
|
+
* transformSchema.parse("02-2345-6789") // ✓ Valid (separators removed)
|
|
217
|
+
*
|
|
218
|
+
* // With custom error messages
|
|
219
|
+
* const customSchema = fax({
|
|
220
|
+
* i18n: {
|
|
221
|
+
* en: { invalid: "Please enter a valid Taiwan fax number" },
|
|
222
|
+
* 'zh-TW': { invalid: "請輸入有效的台灣傳真號碼" }
|
|
223
|
+
* }
|
|
224
|
+
* })
|
|
225
|
+
* ```
|
|
226
|
+
*
|
|
227
|
+
* @throws {z.ZodError} When validation fails with specific error messages
|
|
228
|
+
* @see {@link FaxOptions} for all available configuration options
|
|
229
|
+
* @see {@link validateTaiwanFax} for validation logic details
|
|
230
|
+
*/
|
|
231
|
+
export function fax<IsRequired extends boolean = true>(options?: FaxOptions<IsRequired>): FaxSchema<IsRequired> {
|
|
232
|
+
const { required = true, whitelist, transform, defaultValue, i18n } = options ?? {}
|
|
233
|
+
|
|
234
|
+
// Set appropriate default value based on required flag
|
|
235
|
+
const actualDefaultValue = defaultValue ?? (required ? "" : null)
|
|
236
|
+
|
|
237
|
+
// Helper function to get custom message or fallback to default i18n
|
|
238
|
+
const getMessage = (key: keyof FaxMessages, params?: Record<string, any>) => {
|
|
239
|
+
if (i18n) {
|
|
240
|
+
const currentLocale = getLocale()
|
|
241
|
+
const customMessages = i18n[currentLocale]
|
|
242
|
+
if (customMessages && customMessages[key]) {
|
|
243
|
+
const template = customMessages[key]!
|
|
244
|
+
return template.replace(/\$\{(\w+)}/g, (_, k) => params?.[k] ?? "")
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return t(`taiwan.fax.${key}`, params)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Preprocessing function
|
|
251
|
+
const preprocessFn = (val: unknown) => {
|
|
252
|
+
if (val === null || val === undefined) {
|
|
253
|
+
return actualDefaultValue
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
let processed = String(val).trim()
|
|
257
|
+
|
|
258
|
+
// If after trimming we have an empty string
|
|
259
|
+
if (processed === "") {
|
|
260
|
+
// If empty string is in allowlist, return it as is
|
|
261
|
+
if (whitelist && whitelist.includes("")) {
|
|
262
|
+
return ""
|
|
263
|
+
}
|
|
264
|
+
// If the field is optional and empty string not in allowlist, return default value
|
|
265
|
+
if (!required) {
|
|
266
|
+
return actualDefaultValue
|
|
267
|
+
}
|
|
268
|
+
// If a field is required, return the default value (will be validated later)
|
|
269
|
+
return actualDefaultValue
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (transform) {
|
|
273
|
+
processed = transform(processed)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return processed
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const baseSchema = required ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
|
|
280
|
+
|
|
281
|
+
const schema = baseSchema.refine((val) => {
|
|
282
|
+
if (val === null) return true
|
|
283
|
+
|
|
284
|
+
// Required check
|
|
285
|
+
if (required && (val === "" || val === "null" || val === "undefined")) {
|
|
286
|
+
throw new z.ZodError([{ code: "custom", message: getMessage("required"), path: [] }])
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (val === null) return true
|
|
290
|
+
if (!required && val === "") return true
|
|
291
|
+
|
|
292
|
+
// Allowlist check (if an allowlist is provided, only allow values in the allowlist)
|
|
293
|
+
if (whitelist && whitelist.length > 0) {
|
|
294
|
+
if (whitelist.includes(val)) {
|
|
295
|
+
return true
|
|
296
|
+
}
|
|
297
|
+
// If not in the allowlist, reject regardless of format
|
|
298
|
+
throw new z.ZodError([{ code: "custom", message: getMessage("notInWhitelist"), path: [] }])
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Taiwan fax format validation (only if no allowlist or allowlist is empty)
|
|
302
|
+
if (!validateTaiwanFax(val)) {
|
|
303
|
+
throw new z.ZodError([{ code: "custom", message: getMessage("invalid"), path: [] }])
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return true
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
return schema as unknown as FaxSchema<IsRequired>
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Utility function exported for external use
|
|
314
|
+
*
|
|
315
|
+
* @description
|
|
316
|
+
* The validation function can be used independently for fax number validation
|
|
317
|
+
* without creating a full Zod schema.
|
|
318
|
+
*
|
|
319
|
+
* @example
|
|
320
|
+
* ```typescript
|
|
321
|
+
* import { validateTaiwanFax } from './fax'
|
|
322
|
+
*
|
|
323
|
+
* // Direct validation
|
|
324
|
+
* const isValid = validateTaiwanFax("0223456789") // boolean
|
|
325
|
+
* ```
|
|
326
|
+
*/
|
|
327
|
+
export { validateTaiwanFax }
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Taiwan Mobile Phone Number validator for Zod Kit
|
|
3
|
+
*
|
|
4
|
+
* Provides validation for Taiwan mobile phone numbers with support for
|
|
5
|
+
* all Taiwan mobile network operators and whitelist functionality.
|
|
6
|
+
*
|
|
7
|
+
* @author Ong Hoe Yuan
|
|
8
|
+
* @version 0.0.5
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { z, ZodNullable, ZodString } from "zod"
|
|
12
|
+
import { t } from "../../i18n"
|
|
13
|
+
import { getLocale, type Locale } from "../../config"
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Type definition for mobile phone validation error messages
|
|
17
|
+
*
|
|
18
|
+
* @interface MobileMessages
|
|
19
|
+
* @property {string} [required] - Message when field is required but empty
|
|
20
|
+
* @property {string} [invalid] - Message when mobile number format is invalid
|
|
21
|
+
* @property {string} [notInWhitelist] - Message when mobile number is not in whitelist
|
|
22
|
+
*/
|
|
23
|
+
export type MobileMessages = {
|
|
24
|
+
required?: string
|
|
25
|
+
invalid?: string
|
|
26
|
+
notInWhitelist?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Configuration options for Taiwan mobile phone validation
|
|
31
|
+
*
|
|
32
|
+
* @template IsRequired - Whether the field is required (affects return type)
|
|
33
|
+
*
|
|
34
|
+
* @interface MobileOptions
|
|
35
|
+
* @property {IsRequired} [required=true] - Whether the field is required
|
|
36
|
+
* @property {string[]} [whitelist] - Array of specific mobile numbers that are always allowed
|
|
37
|
+
* @property {Function} [transform] - Custom transformation function for mobile number
|
|
38
|
+
* @property {string | null} [defaultValue] - Default value when input is empty
|
|
39
|
+
* @property {Record<Locale, MobileMessages>} [i18n] - Custom error messages for different locales
|
|
40
|
+
*/
|
|
41
|
+
export type MobileOptions<IsRequired extends boolean = true> = {
|
|
42
|
+
required?: IsRequired
|
|
43
|
+
whitelist?: string[]
|
|
44
|
+
transform?: (value: string) => string
|
|
45
|
+
defaultValue?: IsRequired extends true ? string : string | null
|
|
46
|
+
i18n?: Record<Locale, MobileMessages>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Type alias for mobile phone validation schema based on required flag
|
|
51
|
+
*
|
|
52
|
+
* @template IsRequired - Whether the field is required
|
|
53
|
+
* @typedef MobileSchema
|
|
54
|
+
* @description Returns ZodString if required, ZodNullable<ZodString> if optional
|
|
55
|
+
*/
|
|
56
|
+
export type MobileSchema<IsRequired extends boolean> = IsRequired extends true ? ZodString : ZodNullable<ZodString>
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Validates Taiwan mobile phone number format
|
|
60
|
+
*
|
|
61
|
+
* @param {string} value - The mobile phone number to validate
|
|
62
|
+
* @returns {boolean} True if the mobile number is valid
|
|
63
|
+
*
|
|
64
|
+
* @description
|
|
65
|
+
* Validates Taiwan mobile phone numbers according to the official numbering plan.
|
|
66
|
+
* Taiwan mobile numbers use the format: 09X-XXXX-XXXX (10 digits total).
|
|
67
|
+
*
|
|
68
|
+
* Valid prefixes: 090, 091, 092, 093, 094, 095, 096, 097, 098, 099
|
|
69
|
+
* - All major Taiwan mobile operators are covered
|
|
70
|
+
* - Format: 09[0-9] followed by 7 additional digits
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```typescript
|
|
74
|
+
* validateTaiwanMobile("0912345678") // true
|
|
75
|
+
* validateTaiwanMobile("0987654321") // true
|
|
76
|
+
* validateTaiwanMobile("0812345678") // false (invalid prefix)
|
|
77
|
+
* validateTaiwanMobile("091234567") // false (too short)
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
const validateTaiwanMobile = (value: string): boolean => {
|
|
81
|
+
// Taiwan mobile phone format: 09 + 8 digits
|
|
82
|
+
// Valid prefixes: 090, 091, 092, 093, 094, 095, 096, 097, 098, 099
|
|
83
|
+
return /^09[0-9]\d{7}$/.test(value)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Creates a Zod schema for Taiwan mobile phone number validation
|
|
88
|
+
*
|
|
89
|
+
* @template IsRequired - Whether the field is required (affects return type)
|
|
90
|
+
* @param {MobileOptions<IsRequired>} [options] - Configuration options for mobile validation
|
|
91
|
+
* @returns {MobileSchema<IsRequired>} Zod schema for mobile phone validation
|
|
92
|
+
*
|
|
93
|
+
* @description
|
|
94
|
+
* Creates a comprehensive Taiwan mobile phone number validator with support for
|
|
95
|
+
* all Taiwan mobile network operators and optional whitelist functionality.
|
|
96
|
+
*
|
|
97
|
+
* Features:
|
|
98
|
+
* - Taiwan mobile number format validation (09X-XXXX-XXXX)
|
|
99
|
+
* - Support for all Taiwan mobile operators (090-099 prefixes)
|
|
100
|
+
* - Whitelist functionality for specific allowed numbers
|
|
101
|
+
* - Automatic trimming and preprocessing
|
|
102
|
+
* - Custom transformation functions
|
|
103
|
+
* - Comprehensive internationalization
|
|
104
|
+
* - Optional field support
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```typescript
|
|
108
|
+
* // Basic mobile number validation
|
|
109
|
+
* const basicSchema = mobile()
|
|
110
|
+
* basicSchema.parse("0912345678") // ✓ Valid
|
|
111
|
+
* basicSchema.parse("0987654321") // ✓ Valid
|
|
112
|
+
* basicSchema.parse("0812345678") // ✗ Invalid (wrong prefix)
|
|
113
|
+
*
|
|
114
|
+
* // With whitelist (only specific numbers allowed)
|
|
115
|
+
* const whitelistSchema = mobile({
|
|
116
|
+
* whitelist: ["0912345678", "0987654321"]
|
|
117
|
+
* })
|
|
118
|
+
* whitelistSchema.parse("0912345678") // ✓ Valid (in whitelist)
|
|
119
|
+
* whitelistSchema.parse("0911111111") // ✗ Invalid (not in whitelist)
|
|
120
|
+
*
|
|
121
|
+
* // Optional mobile number
|
|
122
|
+
* const optionalSchema = mobile({ required: false })
|
|
123
|
+
* optionalSchema.parse("") // ✓ Valid (returns null)
|
|
124
|
+
* optionalSchema.parse("0912345678") // ✓ Valid
|
|
125
|
+
*
|
|
126
|
+
* // With custom transformation
|
|
127
|
+
* const transformSchema = mobile({
|
|
128
|
+
* transform: (value) => value.replace(/[^0-9]/g, '') // Remove non-digits
|
|
129
|
+
* })
|
|
130
|
+
* transformSchema.parse("091-234-5678") // ✓ Valid (formatted input)
|
|
131
|
+
* transformSchema.parse("091 234 5678") // ✓ Valid (spaced input)
|
|
132
|
+
*
|
|
133
|
+
* // With custom error messages
|
|
134
|
+
* const customSchema = mobile({
|
|
135
|
+
* i18n: {
|
|
136
|
+
* en: { invalid: "Please enter a valid Taiwan mobile number" },
|
|
137
|
+
* 'zh-TW': { invalid: "請輸入有效的台灣手機號碼" }
|
|
138
|
+
* }
|
|
139
|
+
* })
|
|
140
|
+
* ```
|
|
141
|
+
*
|
|
142
|
+
* @throws {z.ZodError} When validation fails with specific error messages
|
|
143
|
+
* @see {@link MobileOptions} for all available configuration options
|
|
144
|
+
* @see {@link validateTaiwanMobile} for validation logic details
|
|
145
|
+
*/
|
|
146
|
+
export function mobile<IsRequired extends boolean = true>(options?: MobileOptions<IsRequired>): MobileSchema<IsRequired> {
|
|
147
|
+
const { required = true, whitelist, transform, defaultValue, i18n } = options ?? {}
|
|
148
|
+
|
|
149
|
+
// Set appropriate default value based on required flag
|
|
150
|
+
const actualDefaultValue = defaultValue ?? (required ? "" : null)
|
|
151
|
+
|
|
152
|
+
// Helper function to get custom message or fallback to default i18n
|
|
153
|
+
const getMessage = (key: keyof MobileMessages, params?: Record<string, any>) => {
|
|
154
|
+
if (i18n) {
|
|
155
|
+
const currentLocale = getLocale()
|
|
156
|
+
const customMessages = i18n[currentLocale]
|
|
157
|
+
if (customMessages && customMessages[key]) {
|
|
158
|
+
const template = customMessages[key]!
|
|
159
|
+
return template.replace(/\$\{(\w+)}/g, (_, k) => params?.[k] ?? "")
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return t(`taiwan.mobile.${key}`, params)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Preprocessing function
|
|
166
|
+
const preprocessFn = (val: unknown) => {
|
|
167
|
+
if (val === null || val === undefined) {
|
|
168
|
+
return actualDefaultValue
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let processed = String(val).trim()
|
|
172
|
+
|
|
173
|
+
// If after trimming we have an empty string
|
|
174
|
+
if (processed === "") {
|
|
175
|
+
// If empty string is in allowlist, return it as is
|
|
176
|
+
if (whitelist && whitelist.includes("")) {
|
|
177
|
+
return ""
|
|
178
|
+
}
|
|
179
|
+
// If the field is optional and empty string not in allowlist, return default value
|
|
180
|
+
if (!required) {
|
|
181
|
+
return actualDefaultValue
|
|
182
|
+
}
|
|
183
|
+
// If a field is required, return the default value (will be validated later)
|
|
184
|
+
return actualDefaultValue
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (transform) {
|
|
188
|
+
processed = transform(processed)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return processed
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const baseSchema = required ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
|
|
195
|
+
|
|
196
|
+
const schema = baseSchema.refine((val) => {
|
|
197
|
+
if (val === null) return true
|
|
198
|
+
|
|
199
|
+
// Required check
|
|
200
|
+
if (required && (val === "" || val === "null" || val === "undefined")) {
|
|
201
|
+
throw new z.ZodError([{ code: "custom", message: getMessage("required"), path: [] }])
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (val === null) return true
|
|
205
|
+
if (!required && val === "") return true
|
|
206
|
+
|
|
207
|
+
// Allowlist check (if an allowlist is provided, only allow values in the allowlist)
|
|
208
|
+
if (whitelist && whitelist.length > 0) {
|
|
209
|
+
if (whitelist.includes(val)) {
|
|
210
|
+
return true
|
|
211
|
+
}
|
|
212
|
+
// If not in the allowlist, reject regardless of format
|
|
213
|
+
throw new z.ZodError([{ code: "custom", message: getMessage("notInWhitelist"), path: [] }])
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Taiwan mobile phone format validation (only if no allowlist or allowlist is empty)
|
|
217
|
+
if (!validateTaiwanMobile(val)) {
|
|
218
|
+
throw new z.ZodError([{ code: "custom", message: getMessage("invalid"), path: [] }])
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return true
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
return schema as unknown as MobileSchema<IsRequired>
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Utility function exported for external use
|
|
229
|
+
*
|
|
230
|
+
* @description
|
|
231
|
+
* The validation function can be used independently for mobile number validation
|
|
232
|
+
* without creating a full Zod schema.
|
|
233
|
+
*
|
|
234
|
+
* @example
|
|
235
|
+
* ```typescript
|
|
236
|
+
* import { validateTaiwanMobile } from './mobile'
|
|
237
|
+
*
|
|
238
|
+
* // Direct validation
|
|
239
|
+
* const isValid = validateTaiwanMobile("0912345678") // boolean
|
|
240
|
+
* ```
|
|
241
|
+
*/
|
|
242
|
+
export { validateTaiwanMobile }
|