@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.
Files changed (59) hide show
  1. package/.claude/settings.local.json +28 -0
  2. package/LICENSE +21 -0
  3. package/README.md +465 -97
  4. package/debug.js +21 -0
  5. package/debug.ts +16 -0
  6. package/dist/index.cjs +3127 -146
  7. package/dist/index.d.cts +3021 -25
  8. package/dist/index.d.ts +3021 -25
  9. package/dist/index.js +3081 -144
  10. package/eslint.config.mts +8 -0
  11. package/package.json +10 -9
  12. package/src/config.ts +1 -1
  13. package/src/i18n/locales/en.json +161 -25
  14. package/src/i18n/locales/zh-TW.json +165 -26
  15. package/src/index.ts +17 -7
  16. package/src/validators/common/boolean.ts +191 -0
  17. package/src/validators/common/date.ts +299 -0
  18. package/src/validators/common/datetime.ts +673 -0
  19. package/src/validators/common/email.ts +313 -0
  20. package/src/validators/common/file.ts +384 -0
  21. package/src/validators/common/id.ts +471 -0
  22. package/src/validators/common/number.ts +319 -0
  23. package/src/validators/common/password.ts +386 -0
  24. package/src/validators/common/text.ts +271 -0
  25. package/src/validators/common/time.ts +600 -0
  26. package/src/validators/common/url.ts +347 -0
  27. package/src/validators/taiwan/business-id.ts +262 -0
  28. package/src/validators/taiwan/fax.ts +327 -0
  29. package/src/validators/taiwan/mobile.ts +242 -0
  30. package/src/validators/taiwan/national-id.ts +425 -0
  31. package/src/validators/taiwan/postal-code.ts +1049 -0
  32. package/src/validators/taiwan/tel.ts +330 -0
  33. package/tests/common/boolean.test.ts +340 -92
  34. package/tests/common/date.test.ts +458 -0
  35. package/tests/common/datetime.test.ts +693 -0
  36. package/tests/common/email.test.ts +232 -60
  37. package/tests/common/file.test.ts +479 -0
  38. package/tests/common/id.test.ts +535 -0
  39. package/tests/common/number.test.ts +230 -60
  40. package/tests/common/password.test.ts +271 -44
  41. package/tests/common/text.test.ts +210 -13
  42. package/tests/common/time.test.ts +528 -0
  43. package/tests/common/url.test.ts +492 -67
  44. package/tests/taiwan/business-id.test.ts +240 -0
  45. package/tests/taiwan/fax.test.ts +463 -0
  46. package/tests/taiwan/mobile.test.ts +373 -0
  47. package/tests/taiwan/national-id.test.ts +435 -0
  48. package/tests/taiwan/postal-code.test.ts +705 -0
  49. package/tests/taiwan/tel.test.ts +467 -0
  50. package/eslint.config.mjs +0 -10
  51. package/src/common/boolean.ts +0 -36
  52. package/src/common/date.ts +0 -43
  53. package/src/common/email.ts +0 -44
  54. package/src/common/integer.ts +0 -46
  55. package/src/common/number.ts +0 -37
  56. package/src/common/password.ts +0 -33
  57. package/src/common/text.ts +0 -34
  58. package/src/common/url.ts +0 -37
  59. 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 }