79 lines
3.3 KiB
TypeScript
79 lines
3.3 KiB
TypeScript
|
|
/**
|
||
|
|
* Zod schemas wrapping the i18n primitives. Used by route handlers
|
||
|
|
* and form-level validation so the same rules run client + server.
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { z } from 'zod';
|
||
|
|
|
||
|
|
import { ISO_COUNTRIES } from '@/lib/i18n/countries';
|
||
|
|
import { isValidE164 } from '@/lib/i18n/phone';
|
||
|
|
import { isValidSubdivisionCode } from '@/lib/i18n/subdivisions';
|
||
|
|
|
||
|
|
// ─── Country ──────────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
/** ISO-3166-1 alpha-2, uppercase. */
|
||
|
|
export const countryIsoSchema = z
|
||
|
|
.string()
|
||
|
|
.length(2)
|
||
|
|
.toUpperCase()
|
||
|
|
.refine((c) => ISO_COUNTRIES.has(c), 'Unknown country code');
|
||
|
|
|
||
|
|
// ─── Phone ────────────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
/** E.164 form, e.g. '+442079460958'. */
|
||
|
|
export const phoneE164Schema = z
|
||
|
|
.string()
|
||
|
|
.min(1)
|
||
|
|
.refine((v) => isValidE164(v), 'Invalid phone number');
|
||
|
|
|
||
|
|
// ─── Timezone ─────────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
/**
|
||
|
|
* IANA timezone (e.g. 'Europe/Warsaw'). Validates against
|
||
|
|
* `Intl.supportedValuesOf('timeZone')` when available. Older Node
|
||
|
|
* runtimes that lack the API fall back to a permissive shape check
|
||
|
|
* (`Region/City`) so the validator never blocks the path.
|
||
|
|
*/
|
||
|
|
export const ianaTimezoneSchema = z
|
||
|
|
.string()
|
||
|
|
.min(1)
|
||
|
|
.refine((tz) => {
|
||
|
|
if (typeof Intl !== 'undefined' && 'supportedValuesOf' in Intl) {
|
||
|
|
try {
|
||
|
|
const supported = Intl.supportedValuesOf('timeZone') as string[];
|
||
|
|
if (supported.length > 0) return supported.includes(tz);
|
||
|
|
} catch {
|
||
|
|
// fall through
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return /^[A-Z][A-Za-z_+-]+\/[A-Za-z_+-]+/.test(tz);
|
||
|
|
}, 'Unknown timezone');
|
||
|
|
|
||
|
|
// ─── Subdivision ──────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
/** ISO 3166-2 code, e.g. 'PL-MZ'. */
|
||
|
|
export const subdivisionIsoSchema = z
|
||
|
|
.string()
|
||
|
|
.min(2)
|
||
|
|
.refine((code) => isValidSubdivisionCode(code), 'Unknown subdivision code');
|
||
|
|
|
||
|
|
// ─── Optional variants ────────────────────────────────────────────────────────
|
||
|
|
// Inline forms most callers will use — empty strings normalize to null
|
||
|
|
// so the user clearing a field doesn't fail validation.
|
||
|
|
|
||
|
|
export const optionalCountryIsoSchema = z
|
||
|
|
.union([z.literal(''), z.null(), countryIsoSchema])
|
||
|
|
.transform((v) => (v === '' || v === null ? null : v));
|
||
|
|
|
||
|
|
export const optionalPhoneE164Schema = z
|
||
|
|
.union([z.literal(''), z.null(), phoneE164Schema])
|
||
|
|
.transform((v) => (v === '' || v === null ? null : v));
|
||
|
|
|
||
|
|
export const optionalIanaTimezoneSchema = z
|
||
|
|
.union([z.literal(''), z.null(), ianaTimezoneSchema])
|
||
|
|
.transform((v) => (v === '' || v === null ? null : v));
|
||
|
|
|
||
|
|
export const optionalSubdivisionIsoSchema = z
|
||
|
|
.union([z.literal(''), z.null(), subdivisionIsoSchema])
|
||
|
|
.transform((v) => (v === '' || v === null ? null : v));
|