@wikytam/helpers 1.0.0
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/LICENSE +21 -0
- package/README.md +574 -0
- package/README_vi.md +519 -0
- package/dist/index.cjs +1147 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +373 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +373 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1137 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["ones","digitWords","convert"],"sources":["../src/locales/en.ts","../src/locales/vi.ts","../src/locales/index.ts","../src/utils.ts","../src/formatter.ts","../src/global.ts"],"sourcesContent":["import type { LocaleSpellout, NumberShortConfig } from \"./types.js\"\n\nconst ones = [\n\t\"\",\n\t\"one\",\n\t\"two\",\n\t\"three\",\n\t\"four\",\n\t\"five\",\n\t\"six\",\n\t\"seven\",\n\t\"eight\",\n\t\"nine\",\n\t\"ten\",\n\t\"eleven\",\n\t\"twelve\",\n\t\"thirteen\",\n\t\"fourteen\",\n\t\"fifteen\",\n\t\"sixteen\",\n\t\"seventeen\",\n\t\"eighteen\",\n\t\"nineteen\",\n]\n\nconst tens = [\n\t\"\",\n\t\"\",\n\t\"twenty\",\n\t\"thirty\",\n\t\"forty\",\n\t\"fifty\",\n\t\"sixty\",\n\t\"seventy\",\n\t\"eighty\",\n\t\"ninety\",\n]\n\nconst digitWords: Record<string, string> = {\n\t\"0\": \"zero\",\n\t\"1\": \"one\",\n\t\"2\": \"two\",\n\t\"3\": \"three\",\n\t\"4\": \"four\",\n\t\"5\": \"five\",\n\t\"6\": \"six\",\n\t\"7\": \"seven\",\n\t\"8\": \"eight\",\n\t\"9\": \"nine\",\n}\n\nfunction convert(num: number): string {\n\tif (num === 0) return \"\"\n\tif (num < 20) return ones[num] ?? \"\"\n\tif (num < 100) {\n\t\tconst t = tens[Math.floor(num / 10)] ?? \"\"\n\t\tconst o = ones[num % 10]\n\t\treturn o ? `${t}-${o}` : t\n\t}\n\tif (num < 1000) {\n\t\tconst h = ones[Math.floor(num / 100)] ?? \"\"\n\t\tconst remainder = num % 100\n\t\treturn remainder ? `${h} hundred ${convert(remainder)}` : `${h} hundred`\n\t}\n\tif (num < 1_000_000) {\n\t\tconst th = convert(Math.floor(num / 1000))\n\t\tconst remainder = num % 1000\n\t\treturn remainder ? `${th} thousand ${convert(remainder)}` : `${th} thousand`\n\t}\n\tif (num < 1_000_000_000) {\n\t\tconst m = convert(Math.floor(num / 1_000_000))\n\t\tconst remainder = num % 1_000_000\n\t\treturn remainder ? `${m} million ${convert(remainder)}` : `${m} million`\n\t}\n\tconst b = convert(Math.floor(num / 1_000_000_000))\n\tconst remainder = num % 1_000_000_000\n\treturn remainder ? `${b} billion ${convert(remainder)}` : `${b} billion`\n}\n\nexport const enSpellout: LocaleSpellout = {\n\tzeroWord: \"zero\",\n\tpointWord: \"point\",\n\tnegativePrefix: \"minus\",\n\n\tintegerToWords(n: number): string {\n\t\tif (n === 0) return \"zero\"\n\t\treturn convert(n).trim()\n\t},\n\n\tdigitToWord(digit: string): string {\n\t\treturn digitWords[digit] ?? digit\n\t},\n}\n\nexport const enNumberShort: NumberShortConfig = {\n\tthresholds: [\n\t\t{ value: 1_000_000_000_000, suffix: \" Trillion\" },\n\t\t{ value: 1_000_000_000, suffix: \" Billion\" },\n\t\t{ value: 1_000_000, suffix: \" Million\" },\n\t\t{ value: 1_000, suffix: \"K\" },\n\t],\n}\n","import type { LocaleSpellout, NumberShortConfig } from \"./types.js\"\n\nconst ones = [\n\t\"\",\n\t\"m\\u1ed9t\",\n\t\"hai\",\n\t\"ba\",\n\t\"b\\u1ed1n\",\n\t\"n\\u0103m\",\n\t\"s\\u00e1u\",\n\t\"b\\u1ea3y\",\n\t\"t\\u00e1m\",\n\t\"ch\\u00edn\",\n]\n\nconst onesInTens = [\n\t\"\",\n\t\"m\\u1ed1t\",\n\t\"hai\",\n\t\"ba\",\n\t\"b\\u1ed1n\",\n\t\"l\\u0103m\", // 5 in tens position uses \"lam\" not \"nam\"\n\t\"s\\u00e1u\",\n\t\"b\\u1ea3y\",\n\t\"t\\u00e1m\",\n\t\"ch\\u00edn\",\n]\n\nconst digitWords: Record<string, string> = {\n\t\"0\": \"kh\\u00f4ng\",\n\t\"1\": \"m\\u1ed9t\",\n\t\"2\": \"hai\",\n\t\"3\": \"ba\",\n\t\"4\": \"b\\u1ed1n\",\n\t\"5\": \"n\\u0103m\",\n\t\"6\": \"s\\u00e1u\",\n\t\"7\": \"b\\u1ea3y\",\n\t\"8\": \"t\\u00e1m\",\n\t\"9\": \"ch\\u00edn\",\n}\n\n/**\n * Vietnamese number spellout following standard rules:\n * - 5 in ones position of tens => \"lam\" (not \"nam\")\n * - 1 in ones position of tens (>=20) => \"mot\" with special handling\n * - 0 in ones position of tens => \"muoi\" only (no trailing)\n * - Tens starting with 1 => \"muoi\", otherwise => \"muoi\" with prefix\n */\nfunction readTens(t: number, u: number): string {\n\tlet result = \"\"\n\n\tif (t === 1) {\n\t\tresult = \"m\\u01b0\\u1eddi\"\n\t} else {\n\t\tresult = `${ones[t]} m\\u01b0\\u01a1i`\n\t}\n\n\tif (u === 0) return result\n\tif (u === 1 && t > 1) return `${result} m\\u1ed1t`\n\tif (u === 5 && t > 0) return `${result} l\\u0103m`\n\treturn `${result} ${onesInTens[u] ?? \"\"}`\n}\n\nfunction readHundreds(h: number, t: number, u: number): string {\n\tconst result = `${ones[h]} tr\\u0103m`\n\tif (t === 0 && u === 0) return result\n\tif (t === 0) return `${result} linh ${ones[u]}`\n\treturn `${result} ${readTens(t, u)}`\n}\n\nfunction readBlock(num: number): string {\n\tif (num === 0) return \"\"\n\n\tconst h = Math.floor(num / 100)\n\tconst t = Math.floor((num % 100) / 10)\n\tconst u = num % 10\n\n\tif (h > 0) return readHundreds(h, t, u)\n\tif (t > 0) return readTens(t, u)\n\treturn ones[u] ?? \"\"\n}\n\nfunction convert(num: number): string {\n\tif (num === 0) return \"kh\\u00f4ng\"\n\n\tconst units = [\n\t\t{ value: 1_000_000_000, label: \"t\\u1ef7\" },\n\t\t{ value: 1_000_000, label: \"tri\\u1ec7u\" },\n\t\t{ value: 1_000, label: \"ngh\\u00ecn\" },\n\t\t{ value: 1, label: \"\" },\n\t]\n\n\tconst parts: string[] = []\n\tlet remaining = num\n\n\tfor (const unit of units) {\n\t\tif (remaining >= unit.value) {\n\t\t\tconst block = Math.floor(remaining / unit.value)\n\t\t\tremaining %= unit.value\n\n\t\t\tconst blockStr = readBlock(block)\n\t\t\tif (blockStr) {\n\t\t\t\tparts.push(unit.label ? `${blockStr} ${unit.label}` : blockStr)\n\t\t\t}\n\n\t\t\t// Handle leading zeros in next block (e.g. 1001 -> \"mot nghin khong tram linh mot\")\n\t\t\tif (remaining > 0 && remaining < unit.value / 10) {\n\t\t\t\t// Needs \"khong tram\" prefix if next block < 100\n\t\t\t\tif (remaining < 100 && unit.value >= 1000) {\n\t\t\t\t\tparts.push(\"kh\\u00f4ng tr\\u0103m\")\n\t\t\t\t\tif (remaining < 10) {\n\t\t\t\t\t\tparts.push(`linh ${ones[remaining]}`)\n\t\t\t\t\t\tremaining = 0\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn parts.join(\" \").trim()\n}\n\nexport const viSpellout: LocaleSpellout = {\n\tzeroWord: \"kh\\u00f4ng\",\n\tpointWord: \"ph\\u1ea9y\",\n\tnegativePrefix: \"\\u00e2m\",\n\n\tintegerToWords(n: number): string {\n\t\tif (n === 0) return \"kh\\u00f4ng\"\n\t\treturn convert(n)\n\t},\n\n\tdigitToWord(digit: string): string {\n\t\treturn digitWords[digit] ?? digit\n\t},\n}\n\nexport const viNumberShort: NumberShortConfig = {\n\tthresholds: [\n\t\t{ value: 1_000_000_000_000, suffix: \" Ngh\\u00ecn T\\u1ef7\" },\n\t\t{ value: 1_000_000_000, suffix: \" T\\u1ef7\" },\n\t\t{ value: 1_000_000, suffix: \" Tri\\u1ec7u\" },\n\t\t{ value: 1_000, suffix: \" Ng\\u00e0n\" },\n\t],\n}\n","import { enNumberShort, enSpellout } from \"./en.js\"\nimport type {\n\tLocaleRegistry,\n\tLocaleSpellout,\n\tNumberShortConfig,\n\tNumberShortRegistry,\n} from \"./types.js\"\nimport { viNumberShort, viSpellout } from \"./vi.js\"\n\nexport type { LocaleSpellout, NumberShortConfig } from \"./types.js\"\n\n/** Built-in spellout locale registry. */\nconst spelloutRegistry: LocaleRegistry = {\n\ten: enSpellout,\n\tvi: viSpellout,\n}\n\n/** Built-in number-short locale registry. */\nconst numberShortRegistry: NumberShortRegistry = {\n\ten: enNumberShort,\n\tvi: viNumberShort,\n}\n\n/** Get the spellout provider for a locale, falling back to English. */\nexport function getSpellout(locale: string): LocaleSpellout {\n\tconst lang = locale.split(\"-\")[0]\n\treturn spelloutRegistry[lang] ?? enSpellout\n}\n\n/** Get the number-short config for a locale, falling back to English. */\nexport function getNumberShortConfig(locale: string): NumberShortConfig {\n\tconst lang = locale.split(\"-\")[0]\n\treturn numberShortRegistry[lang] ?? enNumberShort\n}\n\n/** Register a custom spellout locale at runtime. */\nexport function registerSpellout(lang: string, impl: LocaleSpellout): void {\n\tspelloutRegistry[lang] = impl\n}\n\n/** Register a custom number-short config at runtime. */\nexport function registerNumberShort(\n\tlang: string,\n\tconfig: NumberShortConfig,\n): void {\n\tnumberShortRegistry[lang] = config\n}\n","import type { DateFormatPreset } from \"./types.js\"\n\n/**\n * Escape the 5 HTML-special characters, equivalent to PHP's htmlspecialchars().\n * No external dependency - pure string replacement.\n */\nexport function escapeHtml(value: string): string {\n\treturn value\n\t\t.replace(/&/g, \"&\")\n\t\t.replace(/</g, \"<\")\n\t\t.replace(/>/g, \">\")\n\t\t.replace(/\"/g, \""\")\n\t\t.replace(/'/g, \"'\")\n}\n\n/**\n * Normalize an input value into a Date object.\n * Accepts: Date, number (UNIX seconds or milliseconds), string (ISO 8601).\n */\nexport function normalizeDate(value: unknown): Date {\n\tif (value instanceof Date) return value\n\n\tif (typeof value === \"number\") {\n\t\t// Values below 1e12 are treated as seconds, otherwise milliseconds\n\t\treturn new Date(value < 1e12 ? value * 1000 : value)\n\t}\n\n\tif (typeof value === \"string\") {\n\t\tconst parsed = new Date(value)\n\t\tif (Number.isNaN(parsed.getTime())) {\n\t\t\tthrow new Error(`Cannot parse date value: \"${value}\"`)\n\t\t}\n\t\treturn parsed\n\t}\n\n\tthrow new Error(`Invalid data type for date: ${typeof value}`)\n}\n\n/**\n * Normalize an input value into a number.\n * Accepts: number, numeric string (with optional comma grouping), boolean.\n */\nexport function normalizeNumber(value: unknown): number {\n\tif (typeof value === \"number\") return value\n\n\tif (typeof value === \"string\") {\n\t\tconst trimmed = value.trim()\n\t\t// Strip common thousand separators before parsing\n\t\tconst cleaned = trimmed.replace(/,/g, \"\")\n\t\tconst num = Number(cleaned)\n\t\tif (Number.isNaN(num)) {\n\t\t\tthrow new Error(`Cannot parse numeric value: \"${value}\"`)\n\t\t}\n\t\treturn num\n\t}\n\n\tif (typeof value === \"boolean\") return value ? 1 : 0\n\n\tthrow new Error(`Invalid data type for number: ${typeof value}`)\n}\n\n/**\n * Convert a preset name (short/medium/long/full) to Intl.DateTimeFormatOptions.\n */\nexport function presetToDateOptions(\n\tpreset: DateFormatPreset,\n\ttype: \"date\" | \"time\" | \"datetime\",\n): Intl.DateTimeFormatOptions {\n\tconst dateOptions: Record<DateFormatPreset, Intl.DateTimeFormatOptions> = {\n\t\tshort: { year: \"2-digit\", month: \"numeric\", day: \"numeric\" },\n\t\tmedium: { year: \"numeric\", month: \"short\", day: \"numeric\" },\n\t\tlong: { year: \"numeric\", month: \"long\", day: \"numeric\" },\n\t\tfull: { year: \"numeric\", month: \"long\", day: \"numeric\", weekday: \"long\" },\n\t}\n\n\tconst timeOptions: Record<DateFormatPreset, Intl.DateTimeFormatOptions> = {\n\t\tshort: { hour: \"numeric\", minute: \"numeric\" },\n\t\tmedium: { hour: \"numeric\", minute: \"numeric\", second: \"numeric\" },\n\t\tlong: {\n\t\t\thour: \"numeric\",\n\t\t\tminute: \"numeric\",\n\t\t\tsecond: \"numeric\",\n\t\t\ttimeZoneName: \"short\",\n\t\t},\n\t\tfull: {\n\t\t\thour: \"numeric\",\n\t\t\tminute: \"numeric\",\n\t\t\tsecond: \"numeric\",\n\t\t\ttimeZoneName: \"long\",\n\t\t},\n\t}\n\n\tswitch (type) {\n\t\tcase \"date\":\n\t\t\treturn dateOptions[preset] ?? dateOptions.medium\n\t\tcase \"time\":\n\t\t\treturn timeOptions[preset] ?? timeOptions.medium\n\t\tcase \"datetime\":\n\t\t\treturn {\n\t\t\t\t...(dateOptions[preset] ?? dateOptions.medium),\n\t\t\t\t...(timeOptions[preset] ?? timeOptions.medium),\n\t\t\t}\n\t}\n}\n\n/**\n * Resolve a format value: string preset -> Intl options, object -> use directly.\n */\nexport function resolveDateFormat(\n\tformat: string | Intl.DateTimeFormatOptions | undefined,\n\tdefaultPreset: DateFormatPreset,\n\ttype: \"date\" | \"time\" | \"datetime\",\n): Intl.DateTimeFormatOptions {\n\tif (!format) return presetToDateOptions(defaultPreset, type)\n\tif (typeof format === \"object\") return format\n\treturn presetToDateOptions(format as DateFormatPreset, type)\n}\n\n/**\n * Replace locale-default separators with custom ones in a formatted string.\n * Uses temporary placeholders to avoid replacement collisions.\n */\nexport function applyCustomSeparators(\n\tformatted: string,\n\tlocale: string,\n\tcustomDecimal?: string | null,\n\tcustomThousand?: string | null,\n): string {\n\tif (customDecimal == null && customThousand == null) return formatted\n\n\t// Detect locale-default separators\n\tconst parts = new Intl.NumberFormat(locale).formatToParts(1234567.89)\n\tconst localeDecimal = parts.find((p) => p.type === \"decimal\")?.value ?? \".\"\n\tconst localeGroup = parts.find((p) => p.type === \"group\")?.value ?? \",\"\n\n\tlet result = formatted\n\n\t// Temporary placeholders to prevent collision during replacement\n\tconst PLACEHOLDER_DEC = \"\\x01\"\n\tconst PLACEHOLDER_GRP = \"\\x02\"\n\n\tif (customDecimal != null) {\n\t\tresult = result.replaceAll(localeDecimal, PLACEHOLDER_DEC)\n\t}\n\tif (customThousand != null) {\n\t\tresult = result.replaceAll(localeGroup, PLACEHOLDER_GRP)\n\t}\n\tif (customDecimal != null) {\n\t\tresult = result.replaceAll(PLACEHOLDER_DEC, customDecimal)\n\t}\n\tif (customThousand != null) {\n\t\tresult = result.replaceAll(PLACEHOLDER_GRP, customThousand)\n\t}\n\n\treturn result\n}\n","import { getNumberShortConfig, getSpellout } from \"./locales/index.js\"\nimport type {\n\tEmailOptions,\n\tFormatterOptions,\n\tFormatWidth,\n\tGpsDistanceOptions,\n\tHtmlSanitizeConfig,\n\tImageOptions,\n\tMaskOptions,\n\tMeasureUnitConfig,\n\tNumberShortOptions,\n\tOrdinalSuffixMap,\n\tParagraphOptions,\n\tUnitSystem,\n\tUrlOptions,\n} from \"./types.js\"\nimport {\n\tapplyCustomSeparators,\n\tescapeHtml,\n\tnormalizeDate,\n\tnormalizeNumber,\n\tresolveDateFormat,\n} from \"./utils.js\"\n\n/**\n * TypeScript port of yii\\i18n\\Formatter.\n *\n * Uses only built-in Intl APIs - zero external dependencies.\n * Supports: strings, HTML, numbers, currency, dates, times,\n * file sizes, measurement units, and more.\n */\nexport class Formatter {\n\tpublic locale: string\n\tpublic timeZone: string\n\tpublic defaultTimeZone: string\n\tpublic dateFormat: string | Intl.DateTimeFormatOptions\n\tpublic timeFormat: string | Intl.DateTimeFormatOptions\n\tpublic datetimeFormat: string | Intl.DateTimeFormatOptions\n\tpublic booleanFormat: [string, string]\n\tpublic nullDisplay: string\n\tpublic currencyCode: string\n\tpublic decimalSeparator: string | null\n\tpublic thousandSeparator: string | null\n\tpublic currencyDecimalSeparator: string | null\n\tpublic sizeFormatBase: 1024 | 1000\n\tpublic systemOfUnits: UnitSystem\n\tpublic defaultDecimalDigits: number | null\n\n\tconstructor(options: FormatterOptions = {}) {\n\t\tthis.locale = options.locale ?? \"en-US\"\n\t\tthis.timeZone = options.timeZone ?? \"UTC\"\n\t\tthis.defaultTimeZone = options.defaultTimeZone ?? \"UTC\"\n\t\tthis.dateFormat = options.dateFormat ?? \"medium\"\n\t\tthis.timeFormat = options.timeFormat ?? \"medium\"\n\t\tthis.datetimeFormat = options.datetimeFormat ?? \"medium\"\n\t\tthis.booleanFormat = options.booleanFormat ?? [\"No\", \"Yes\"]\n\t\tthis.nullDisplay = options.nullDisplay ?? \"(not set)\"\n\t\tthis.currencyCode = options.currencyCode ?? \"USD\"\n\t\tthis.decimalSeparator = options.decimalSeparator ?? null\n\t\tthis.thousandSeparator = options.thousandSeparator ?? null\n\t\tthis.currencyDecimalSeparator = options.currencyDecimalSeparator ?? null\n\t\tthis.sizeFormatBase = options.sizeFormatBase ?? 1024\n\t\tthis.systemOfUnits = options.systemOfUnits ?? \"metric\"\n\t\tthis.defaultDecimalDigits = options.defaultDecimalDigits ?? null\n\t}\n\n\t// ─── Generic dispatch ─────────────────────────────────────────────\n\n\t/**\n\t * Format a value by type name, like Yii2's `$formatter->format($value, 'date')`.\n\t * Supports both string and tuple `[formatName, ...params]` signatures.\n\t */\n\tpublic format(value: unknown, type: string | [string, ...unknown[]]): string {\n\t\tif (value === null || value === undefined) return this.nullDisplay\n\n\t\tconst formatName = Array.isArray(type) ? type[0] : type\n\t\tconst params = Array.isArray(type) ? type.slice(1) : []\n\t\tconst methodName = `as${formatName.charAt(0).toUpperCase()}${formatName.slice(1)}`\n\n\t\tconst method = (this as Record<string, unknown>)[methodName]\n\t\tif (typeof method === \"function\") {\n\t\t\treturn (method as (...args: unknown[]) => string).call(\n\t\t\t\tthis,\n\t\t\t\tvalue,\n\t\t\t\t...params,\n\t\t\t)\n\t\t}\n\n\t\tthrow new Error(`Unknown format type: ${formatName}`)\n\t}\n\n\t// ─── String & HTML ────────────────────────────────────────────────\n\n\t/** Returns the value as-is without any formatting. */\n\tpublic asRaw(value: unknown): string {\n\t\tif (value === null || value === undefined) return this.nullDisplay\n\t\treturn String(value)\n\t}\n\n\t/** Formats the value as HTML-encoded plain text. */\n\tpublic asText(value: unknown): string {\n\t\tif (value === null || value === undefined) return this.nullDisplay\n\t\treturn escapeHtml(String(value))\n\t}\n\n\t/**\n\t * Formats the value as HTML-encoded text with newlines converted to `<br />`.\n\t * Handles all line-ending variants: `\\r\\n` (Windows), `\\r` (old Mac), `\\n` (Unix).\n\t * Consecutive newlines produce multiple `<br />` tags.\n\t */\n\tpublic asNtext(value: unknown): string {\n\t\tif (value === null || value === undefined) return this.nullDisplay\n\t\tconst escaped = escapeHtml(String(value))\n\t\treturn escaped.replace(/\\r\\n/g, \"<br />\").replace(/[\\r\\n]/g, \"<br />\")\n\t}\n\n\t/**\n\t * Formats the value as HTML-encoded text paragraphs (split by double newlines).\n\t * Supports configurable wrapper tag and inline line-break conversion.\n\t */\n\tpublic asParagraphs(value: unknown, options?: ParagraphOptions): string {\n\t\tif (value === null || value === undefined) return this.nullDisplay\n\t\tconst tag = options?.tag ?? \"p\"\n\t\tconst lineBreaks = options?.lineBreaks ?? false\n\t\tconst text = String(value)\n\t\t// Normalize line endings before splitting\n\t\tconst normalized = text.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\")\n\t\tconst paragraphs = normalized.split(/\\n\\s*\\n/)\n\t\treturn paragraphs\n\t\t\t.map((p) => {\n\t\t\t\tlet content = escapeHtml(p.trim())\n\t\t\t\tif (lineBreaks) {\n\t\t\t\t\tcontent = content.replace(/\\n/g, \"<br />\")\n\t\t\t\t}\n\t\t\t\treturn `<${tag}>${content}</${tag}>`\n\t\t\t})\n\t\t\t.filter((p) => p !== `<${tag}></${tag}>`)\n\t\t\t.join(\"\\n\")\n\t}\n\n\t/**\n\t * Returns the value as HTML text.\n\t * When a sanitize config is provided, only allowed tags and attributes are kept.\n\t * Without config, the value is returned as-is (caller is responsible for safety).\n\t */\n\tpublic asHtml(value: unknown, sanitize?: HtmlSanitizeConfig): string {\n\t\tif (value === null || value === undefined) return this.nullDisplay\n\t\tconst html = String(value)\n\t\tif (!sanitize) return html\n\t\treturn Formatter.sanitizeHtml(html, sanitize)\n\t}\n\n\t/**\n\t * Allowlist-based HTML sanitizer. Strips tags and attributes not in the config.\n\t * Handles self-closing tags, nested tags, and attribute filtering.\n\t */\n\tprivate static sanitizeHtml(\n\t\thtml: string,\n\t\tconfig: HtmlSanitizeConfig,\n\t): string {\n\t\tconst allowedTags = new Set(\n\t\t\t(config.allowedTags ?? []).map((t) => t.toLowerCase()),\n\t\t)\n\t\tconst allowedAttrs = config.allowedAttributes ?? {}\n\n\t\t// Match opening tags, closing tags, and self-closing tags\n\t\treturn html.replace(\n\t\t\t/<\\/?([a-zA-Z][a-zA-Z0-9]*)\\b([^>]*?)\\s*\\/?>/g,\n\t\t\t(match, tagName: string, attrsStr: string) => {\n\t\t\t\tconst tag = tagName.toLowerCase()\n\t\t\t\tif (!allowedTags.has(tag)) return \"\"\n\n\t\t\t\tconst isClosing = match.startsWith(\"</\")\n\t\t\t\tif (isClosing) return `</${tag}>`\n\n\t\t\t\tconst isSelfClosing = match.endsWith(\"/>\")\n\t\t\t\tconst tagAllowedAttrs = new Set(\n\t\t\t\t\t(allowedAttrs[tag] ?? []).map((a) => a.toLowerCase()),\n\t\t\t\t)\n\n\t\t\t\t// Parse and filter attributes\n\t\t\t\tconst filteredAttrs: string[] = []\n\t\t\t\tconst attrRegex =\n\t\t\t\t\t/([a-zA-Z_:][\\w:.-]*)(?:\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|(\\S+)))?/g\n\t\t\t\tlet attrMatch: RegExpExecArray | null = null\n\t\t\t\twhile (true) {\n\t\t\t\t\tattrMatch = attrRegex.exec(attrsStr)\n\t\t\t\t\tif (!attrMatch) break\n\t\t\t\t\tconst attrName = attrMatch[1].toLowerCase()\n\t\t\t\t\tif (tagAllowedAttrs.has(attrName)) {\n\t\t\t\t\t\tconst attrValue = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4]\n\t\t\t\t\t\tif (attrValue !== undefined) {\n\t\t\t\t\t\t\tfilteredAttrs.push(`${attrName}=\"${escapeHtml(attrValue)}\"`)\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tfilteredAttrs.push(attrName)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst attrsOut =\n\t\t\t\t\tfilteredAttrs.length > 0 ? ` ${filteredAttrs.join(\" \")}` : \"\"\n\t\t\t\treturn isSelfClosing ? `<${tag}${attrsOut} />` : `<${tag}${attrsOut}>`\n\t\t\t},\n\t\t)\n\t}\n\n\t/**\n\t * Formats the value as a mailto link.\n\t * Supports custom display text, subject, and body parameters.\n\t * Validates email format - returns escaped plain text for invalid emails.\n\t */\n\tpublic asEmail(value: unknown, options?: EmailOptions): string {\n\t\tif (value === null || value === undefined) return this.nullDisplay\n\t\tconst email = String(value)\n\n\t\tif (!Formatter.isValidEmail(email)) {\n\t\t\treturn escapeHtml(email)\n\t\t}\n\n\t\tconst params: string[] = []\n\t\tif (options?.subject)\n\t\t\tparams.push(`subject=${encodeURIComponent(options.subject)}`)\n\t\tif (options?.body) params.push(`body=${encodeURIComponent(options.body)}`)\n\t\tconst query = params.length > 0 ? `?${params.join(\"&\")}` : \"\"\n\t\tconst displayText = escapeHtml(options?.text ?? email)\n\n\t\treturn `<a href=\"mailto:${escapeHtml(email)}${query}\">${displayText}</a>`\n\t}\n\n\t/** Basic email format validation (covers most common patterns). */\n\tprivate static isValidEmail(email: string): boolean {\n\t\treturn /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email)\n\t}\n\n\t/**\n\t * Formats the value as a hyperlink.\n\t * Detects http, https, ftp, ftps, and mailto schemes.\n\t * Prepends `http://` when no recognized scheme is present.\n\t */\n\tpublic asUrl(value: unknown, options?: UrlOptions): string {\n\t\tif (value === null || value === undefined) return this.nullDisplay\n\t\tconst url = String(value)\n\t\tconst href = /^(https?|ftps?|mailto):/i.test(url) ? url : `http://${url}`\n\t\tconst target = options?.target ?? \"_blank\"\n\t\tconst displayText = escapeHtml(options?.text ?? url)\n\n\t\tconst attrs: string[] = [\n\t\t\t`href=\"${escapeHtml(href)}\"`,\n\t\t\t`target=\"${escapeHtml(target)}\"`,\n\t\t]\n\t\tif (options?.rel) attrs.push(`rel=\"${escapeHtml(options.rel)}\"`)\n\t\tif (options?.class) attrs.push(`class=\"${escapeHtml(options.class)}\"`)\n\n\t\treturn `<a ${attrs.join(\" \")}>${displayText}</a>`\n\t}\n\n\t/**\n\t * Formats the value as an image tag.\n\t * Supports width, height, CSS class, and loading strategy attributes.\n\t */\n\tpublic asImage(value: unknown, options?: ImageOptions): string {\n\t\tif (value === null || value === undefined) return this.nullDisplay\n\t\tconst src = String(value)\n\t\tconst alt = options?.alt ?? \"\"\n\n\t\tconst attrs: string[] = [\n\t\t\t`src=\"${escapeHtml(src)}\"`,\n\t\t\t`alt=\"${escapeHtml(alt)}\"`,\n\t\t]\n\t\tif (options?.width != null)\n\t\t\tattrs.push(`width=\"${escapeHtml(String(options.width))}\"`)\n\t\tif (options?.height != null)\n\t\t\tattrs.push(`height=\"${escapeHtml(String(options.height))}\"`)\n\t\tif (options?.class) attrs.push(`class=\"${escapeHtml(options.class)}\"`)\n\t\tif (options?.loading) attrs.push(`loading=\"${escapeHtml(options.loading)}\"`)\n\n\t\treturn `<img ${attrs.join(\" \")} />`\n\t}\n\n\t/** Formats the value as a boolean using the configured booleanFormat labels. */\n\tpublic asBoolean(value: unknown): string {\n\t\tif (value === null || value === undefined) return this.nullDisplay\n\t\treturn value ? this.booleanFormat[1] : this.booleanFormat[0]\n\t}\n\n\t// ─── Number & Currency ────────────────────────────────────────────\n\n\t/** Formats the value as an integer by removing decimal digits without rounding. */\n\tpublic asInteger(value: unknown): string {\n\t\tif (value === null || value === undefined) return this.nullDisplay\n\t\tconst num = normalizeNumber(value)\n\t\tconst intVal = Math.trunc(num)\n\n\t\tconst formatted = new Intl.NumberFormat(this.locale, {\n\t\t\tmaximumFractionDigits: 0,\n\t\t\tminimumFractionDigits: 0,\n\t\t}).format(intVal)\n\n\t\treturn applyCustomSeparators(\n\t\t\tformatted,\n\t\t\tthis.locale,\n\t\t\tthis.decimalSeparator,\n\t\t\tthis.thousandSeparator,\n\t\t)\n\t}\n\n\t/** Formats the value as a decimal number. */\n\tpublic asDecimal(value: unknown, decimals?: number): string {\n\t\tif (value === null || value === undefined) return this.nullDisplay\n\t\tconst num = normalizeNumber(value)\n\t\tconst digits = decimals ?? this.defaultDecimalDigits ?? 2\n\n\t\tconst formatted = new Intl.NumberFormat(this.locale, {\n\t\t\tminimumFractionDigits: digits,\n\t\t\tmaximumFractionDigits: digits,\n\t\t}).format(num)\n\n\t\treturn applyCustomSeparators(\n\t\t\tformatted,\n\t\t\tthis.locale,\n\t\t\tthis.decimalSeparator,\n\t\t\tthis.thousandSeparator,\n\t\t)\n\t}\n\n\t/** Formats the value as a percent number with \"%\" sign. */\n\tpublic asPercent(value: unknown, decimals?: number): string {\n\t\tif (value === null || value === undefined) return this.nullDisplay\n\t\tconst num = normalizeNumber(value)\n\t\tconst digits = decimals ?? this.defaultDecimalDigits ?? 0\n\n\t\tconst formatted = new Intl.NumberFormat(this.locale, {\n\t\t\tstyle: \"percent\",\n\t\t\tminimumFractionDigits: digits,\n\t\t\tmaximumFractionDigits: digits,\n\t\t}).format(num)\n\n\t\treturn applyCustomSeparators(\n\t\t\tformatted,\n\t\t\tthis.locale,\n\t\t\tthis.decimalSeparator,\n\t\t\tthis.thousandSeparator,\n\t\t)\n\t}\n\n\t/** Formats the value as a currency number using ISO 4217 codes. */\n\tpublic asCurrency(value: unknown, currency?: string): string {\n\t\tif (value === null || value === undefined) return this.nullDisplay\n\t\tconst num = normalizeNumber(value)\n\t\tconst code = currency ?? this.currencyCode\n\n\t\tconst formatted = new Intl.NumberFormat(this.locale, {\n\t\t\tstyle: \"currency\",\n\t\t\tcurrency: code,\n\t\t}).format(num)\n\n\t\treturn applyCustomSeparators(\n\t\t\tformatted,\n\t\t\tthis.locale,\n\t\t\tthis.currencyDecimalSeparator ?? this.decimalSeparator,\n\t\t\tthis.thousandSeparator,\n\t\t)\n\t}\n\n\t/** Formats the value as a scientific number (e-notation). */\n\tpublic asScientific(value: unknown, decimals?: number): string {\n\t\tif (value === null || value === undefined) return this.nullDisplay\n\t\tconst num = normalizeNumber(value)\n\t\tconst digits = decimals ?? this.defaultDecimalDigits ?? 2\n\n\t\treturn new Intl.NumberFormat(this.locale, {\n\t\t\tnotation: \"scientific\",\n\t\t\tminimumFractionDigits: digits,\n\t\t\tmaximumFractionDigits: digits,\n\t\t}).format(num)\n\t}\n\n\t/**\n\t * Formats the value as a number spellout (e.g. 42 -> \"forty-two\").\n\t * Supports multiple locales via the locales/ registry.\n\t */\n\tpublic asSpellout(value: unknown): string {\n\t\tif (value === null || value === undefined) return this.nullDisplay\n\t\tconst num = normalizeNumber(value)\n\n\t\tconst spellout = getSpellout(this.locale)\n\n\t\tif (num === 0) return spellout.zeroWord\n\n\t\tconst isNegative = num < 0\n\t\tconst absNum = Math.abs(num)\n\t\tconst intPart = Math.trunc(absNum)\n\t\tconst decPart = absNum - intPart\n\n\t\tlet result = spellout.integerToWords(intPart)\n\n\t\tif (decPart > 0) {\n\t\t\tconst decStr = String(absNum).split(\".\")[1] ?? \"\"\n\t\t\tconst decDigits = decStr.split(\"\").map((d) => spellout.digitToWord(d))\n\t\t\tresult += ` ${spellout.pointWord} ${decDigits.join(\" \")}`\n\t\t}\n\n\t\treturn isNegative ? `${spellout.negativePrefix} ${result}` : result\n\t}\n\n\t/**\n\t * Formats the value as an ordinal number (e.g. 1 -> \"1st\", 2 -> \"2nd\").\n\t * Supports multiple locales via built-in suffix maps and custom overrides\n\t * through `Formatter.registerOrdinalSuffixes()`.\n\t */\n\tpublic asOrdinal(value: unknown): string {\n\t\tif (value === null || value === undefined) return this.nullDisplay\n\t\tconst num = Math.trunc(normalizeNumber(value))\n\n\t\ttry {\n\t\t\tconst pr = new Intl.PluralRules(this.locale, { type: \"ordinal\" })\n\t\t\tconst rule = pr.select(num)\n\n\t\t\tconst lang = this.locale.split(\"-\")[0]\n\t\t\tconst enSuffixes = Formatter.ordinalSuffixes.en ?? { other: \"th\" }\n\t\t\tconst langSuffixes = Formatter.ordinalSuffixes[lang] ?? enSuffixes\n\t\t\tconst suffix = langSuffixes[rule] ?? langSuffixes.other ?? \"\"\n\n\t\t\treturn `${new Intl.NumberFormat(this.locale).format(num)}${suffix}`\n\t\t} catch {\n\t\t\treturn `${num}${this.getOrdinalSuffixEn(num)}`\n\t\t}\n\t}\n\n\t/** Built-in ordinal suffix registry. Extensible at runtime. */\n\tprivate static ordinalSuffixes: Record<string, OrdinalSuffixMap> = {\n\t\ten: { one: \"st\", two: \"nd\", few: \"rd\", other: \"th\" },\n\t\tvi: { other: \"\" },\n\t\tfr: { one: \"er\", other: \"e\" },\n\t\tde: { other: \".\" },\n\t\tes: { other: \".\" },\n\t\tpt: { other: \".\" },\n\t\tit: { other: \".\" },\n\t\tja: { other: \"\" },\n\t\tko: { other: \"\" },\n\t\tzh: { other: \"\" },\n\t}\n\n\t/**\n\t * Register ordinal suffixes for a language at runtime.\n\t * Keys are Intl.PluralRules ordinal categories: \"one\", \"two\", \"few\", \"other\".\n\t */\n\tpublic static registerOrdinalSuffixes(\n\t\tlang: string,\n\t\tsuffixes: OrdinalSuffixMap,\n\t): void {\n\t\tFormatter.ordinalSuffixes[lang] = suffixes\n\t}\n\n\t// ─── Date & Time ──────────────────────────────────────────────────\n\n\t/** Formats the value as a date. */\n\tpublic asDate(\n\t\tvalue: unknown,\n\t\tformat?: string | Intl.DateTimeFormatOptions,\n\t): string {\n\t\tif (value === null || value === undefined) return this.nullDisplay\n\t\tconst date = normalizeDate(value)\n\t\tconst resolved = resolveDateFormat(\n\t\t\tformat ?? this.dateFormat,\n\t\t\t\"medium\",\n\t\t\t\"date\",\n\t\t)\n\n\t\treturn new Intl.DateTimeFormat(this.locale, {\n\t\t\t...resolved,\n\t\t\ttimeZone: this.timeZone,\n\t\t}).format(date)\n\t}\n\n\t/** Formats the value as a time. */\n\tpublic asTime(\n\t\tvalue: unknown,\n\t\tformat?: string | Intl.DateTimeFormatOptions,\n\t): string {\n\t\tif (value === null || value === undefined) return this.nullDisplay\n\t\tconst date = normalizeDate(value)\n\t\tconst resolved = resolveDateFormat(\n\t\t\tformat ?? this.timeFormat,\n\t\t\t\"medium\",\n\t\t\t\"time\",\n\t\t)\n\n\t\treturn new Intl.DateTimeFormat(this.locale, {\n\t\t\t...resolved,\n\t\t\ttimeZone: this.timeZone,\n\t\t}).format(date)\n\t}\n\n\t/** Formats the value as a datetime. */\n\tpublic asDatetime(\n\t\tvalue: unknown,\n\t\tformat?: string | Intl.DateTimeFormatOptions,\n\t): string {\n\t\tif (value === null || value === undefined) return this.nullDisplay\n\t\tconst date = normalizeDate(value)\n\t\tconst resolved = resolveDateFormat(\n\t\t\tformat ?? this.datetimeFormat,\n\t\t\t\"medium\",\n\t\t\t\"datetime\",\n\t\t)\n\n\t\treturn new Intl.DateTimeFormat(this.locale, {\n\t\t\t...resolved,\n\t\t\ttimeZone: this.timeZone,\n\t\t}).format(date)\n\t}\n\n\t/** Returns the value as a UNIX timestamp (seconds since epoch). */\n\tpublic asTimestamp(value: unknown): string {\n\t\tif (value === null || value === undefined) return this.nullDisplay\n\t\tconst date = normalizeDate(value)\n\t\treturn String(Math.floor(date.getTime() / 1000))\n\t}\n\n\t/**\n\t * Formats the value as the time interval between a date and now in human readable form.\n\t * Uses Intl.RelativeTimeFormat (built-in in Node.js / browsers).\n\t */\n\tpublic asRelativeTime(value: unknown, referenceTime?: unknown): string {\n\t\tif (value === null || value === undefined) return this.nullDisplay\n\n\t\tconst date = normalizeDate(value)\n\t\tconst ref = referenceTime ? normalizeDate(referenceTime) : new Date()\n\t\tconst diffMs = date.getTime() - ref.getTime()\n\t\tconst diffSec = Math.round(diffMs / 1000)\n\n\t\tconst rtf = new Intl.RelativeTimeFormat(this.locale, { numeric: \"auto\" })\n\n\t\tconst absSec = Math.abs(diffSec)\n\t\tif (absSec < 60) return rtf.format(diffSec, \"second\")\n\t\tif (absSec < 3600) return rtf.format(Math.round(diffSec / 60), \"minute\")\n\t\tif (absSec < 86400) return rtf.format(Math.round(diffSec / 3600), \"hour\")\n\t\tif (absSec < 2592000) return rtf.format(Math.round(diffSec / 86400), \"day\")\n\t\tif (absSec < 31536000)\n\t\t\treturn rtf.format(Math.round(diffSec / 2592000), \"month\")\n\t\treturn rtf.format(Math.round(diffSec / 31536000), \"year\")\n\t}\n\n\t/**\n\t * Represents the value as duration in human readable format.\n\t * Example: 5400 -> \"1 hour, 30 minutes\"\n\t */\n\tpublic asDuration(value: unknown, implode?: string): string {\n\t\tif (value === null || value === undefined) return this.nullDisplay\n\t\tlet seconds = Math.abs(normalizeNumber(value))\n\t\tconst separator = implode ?? \", \"\n\n\t\tif (seconds === 0) return this.getDurationLabel(\"second\", 0)\n\n\t\tconst units: Array<{ unit: string; divisor: number }> = [\n\t\t\t{ unit: \"year\", divisor: 31536000 },\n\t\t\t{ unit: \"month\", divisor: 2592000 },\n\t\t\t{ unit: \"day\", divisor: 86400 },\n\t\t\t{ unit: \"hour\", divisor: 3600 },\n\t\t\t{ unit: \"minute\", divisor: 60 },\n\t\t\t{ unit: \"second\", divisor: 1 },\n\t\t]\n\n\t\tconst parts: string[] = []\n\t\tfor (const { unit, divisor } of units) {\n\t\t\tif (seconds >= divisor) {\n\t\t\t\tconst count = Math.floor(seconds / divisor)\n\t\t\t\tseconds %= divisor\n\t\t\t\tparts.push(this.getDurationLabel(unit, count))\n\t\t\t}\n\t\t}\n\n\t\treturn parts.join(separator)\n\t}\n\n\t// ─── Size & Measurement ──────────────────────────────────────────\n\n\t/** Formats the value in bytes as a size in human readable form (e.g. \"12 kilobytes\"). */\n\tpublic asSize(value: unknown, decimals?: number): string {\n\t\treturn this.formatBytes(value, decimals, \"long\")\n\t}\n\n\t/** Formats the value in bytes as a size in human readable form (e.g. \"12 kB\"). */\n\tpublic asShortSize(value: unknown, decimals?: number): string {\n\t\treturn this.formatBytes(value, decimals, \"short\")\n\t}\n\n\t/** Formats the value as a length in human readable form (e.g. \"12 meters\"). */\n\tpublic asLength(value: unknown, decimals?: number): string {\n\t\treturn this.formatMeasure(value, \"length\", \"long\", decimals)\n\t}\n\n\t/** Formats the value as a length in human readable form (e.g. \"12 m\"). */\n\tpublic asShortLength(value: unknown, decimals?: number): string {\n\t\treturn this.formatMeasure(value, \"length\", \"short\", decimals)\n\t}\n\n\t/** Formats the value as a weight in human readable form (e.g. \"12 kilograms\"). */\n\tpublic asWeight(value: unknown, decimals?: number): string {\n\t\treturn this.formatMeasure(value, \"mass\", \"long\", decimals)\n\t}\n\n\t/** Formats the value as a weight in human readable form (e.g. \"12 kg\"). */\n\tpublic asShortWeight(value: unknown, decimals?: number): string {\n\t\treturn this.formatMeasure(value, \"mass\", \"short\", decimals)\n\t}\n\n\t// ─── Utility Methods ─────────────────────────────────────────────\n\n\t/**\n\t * Abbreviate a large number with locale-aware suffixes.\n\t * e.g. 1500000 -> \"1.5 Million\" (en) or \"1,5 Trieu\" (vi).\n\t */\n\tpublic asNumberShort(value: unknown, options?: NumberShortOptions): string {\n\t\tif (value === null || value === undefined) return this.nullDisplay\n\t\tconst num = normalizeNumber(value)\n\t\tconst absNum = Math.abs(num)\n\t\tconst decimals = options?.decimals ?? 1\n\t\tconst fallback = options?.fallback ?? \"currency\"\n\t\tconst addSpace = options?.spaceBefore ?? false\n\t\tconst config = getNumberShortConfig(this.locale)\n\n\t\tfor (const { value: threshold, suffix } of config.thresholds) {\n\t\t\tif (absNum >= threshold) {\n\t\t\t\tconst short =\n\t\t\t\t\tMath.round((num / threshold) * 10 ** decimals) / 10 ** decimals\n\t\t\t\tconst sep = addSpace && !suffix.startsWith(\" \") ? \" \" : \"\"\n\t\t\t\treturn `${this.asDecimal(short, decimals)}${sep}${suffix}`\n\t\t\t}\n\t\t}\n\n\t\tswitch (fallback) {\n\t\t\tcase \"decimal\":\n\t\t\t\treturn this.asDecimal(num, decimals)\n\t\t\tcase \"integer\":\n\t\t\t\treturn this.asInteger(num)\n\t\t\tdefault:\n\t\t\t\treturn this.asCurrency(num)\n\t\t}\n\t}\n\n\t/**\n\t * Format the GPS (great-circle) distance between two coordinates.\n\t * Returns a human-readable string with unit suffix.\n\t */\n\tpublic asGpsDistance(\n\t\tlatFrom: number,\n\t\tlonFrom: number,\n\t\tlatTo: number,\n\t\tlonTo: number,\n\t\toptions?: GpsDistanceOptions,\n\t): string {\n\t\tconst earthRadius = options?.earthRadius ?? 6_371_000\n\t\tconst decimals = options?.decimals ?? 1\n\t\tconst rawUnit = options?.unit ?? \"auto\"\n\t\tconst meters = Formatter.gpsDistance(\n\t\t\tlatFrom,\n\t\t\tlonFrom,\n\t\t\tlatTo,\n\t\t\tlonTo,\n\t\t\tearthRadius,\n\t\t)\n\n\t\tlet value: number\n\t\tlet unit: string\n\t\tif (rawUnit === \"mi\") {\n\t\t\tvalue = meters / 1609.344\n\t\t\tunit = \"mi\"\n\t\t} else if (rawUnit === \"km\") {\n\t\t\tvalue = meters / 1000\n\t\t\tunit = \"km\"\n\t\t} else if (rawUnit === \"m\") {\n\t\t\tvalue = meters\n\t\t\tunit = \"m\"\n\t\t} else {\n\t\t\t// auto: use km if >= 1000m, otherwise m\n\t\t\tif (meters >= 1000) {\n\t\t\t\tvalue = meters / 1000\n\t\t\t\tunit = \"km\"\n\t\t\t} else {\n\t\t\t\tvalue = meters\n\t\t\t\tunit = \"m\"\n\t\t\t}\n\t\t}\n\n\t\treturn `${this.asDecimal(value, decimals)} ${unit}`\n\t}\n\n\t/** Haversine formula: returns distance in meters between two GPS coordinates. */\n\tprivate static gpsDistance(\n\t\tlatitudeFrom: number,\n\t\tlongitudeFrom: number,\n\t\tlatitudeTo: number,\n\t\tlongitudeTo: number,\n\t\tearthRadius = 6_371_000,\n\t): number {\n\t\tconst toRad = (deg: number) => (deg * Math.PI) / 180\n\n\t\tconst latFrom = toRad(latitudeFrom)\n\t\tconst latTo = toRad(latitudeTo)\n\t\tconst deltaLat = toRad(latitudeTo - latitudeFrom)\n\t\tconst deltaLon = toRad(longitudeTo - longitudeFrom)\n\n\t\tconst a =\n\t\t\tMath.sin(deltaLat / 2) ** 2 +\n\t\t\tMath.cos(latFrom) * Math.cos(latTo) * Math.sin(deltaLon / 2) ** 2\n\t\tconst c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))\n\n\t\treturn earthRadius * c\n\t}\n\n\t/**\n\t * Mask a string value, showing only the first and last N characters.\n\t * Instance method with options support.\n\t */\n\tpublic asMaskedValue(value: unknown, options?: MaskOptions): string {\n\t\tif (value === null || value === undefined) return this.nullDisplay\n\t\tconst str = String(value)\n\t\treturn Formatter.getMaskedValue(\n\t\t\tstr,\n\t\t\toptions?.startVisible ?? 4,\n\t\t\toptions?.endVisible ?? 3,\n\t\t\toptions?.maskChar ?? \"X\",\n\t\t)\n\t}\n\n\t/** Core masking logic used by asMaskedValue(). */\n\tprivate static getMaskedValue(\n\t\tvalue: string,\n\t\tstartVisible = 4,\n\t\tendVisible = 3,\n\t\tmaskChar = \"X\",\n\t): string {\n\t\tif (!value) return \"\"\n\t\tconst len = value.length\n\n\t\tif (len <= startVisible + endVisible) return value\n\n\t\tconst start = value.slice(0, startVisible)\n\t\tconst end = value.slice(len - endVisible)\n\t\tconst masked = maskChar.repeat(len - startVisible - endVisible)\n\n\t\treturn `${start}${masked}${end}`\n\t}\n\n\t// ─── Private helpers ──────────────────────────────────────────────\n\n\t/**\n\t * Format bytes into the most appropriate size unit.\n\t * Supports both base-1024 (binary) and base-1000 (decimal).\n\t */\n\tprivate formatBytes(\n\t\tvalue: unknown,\n\t\tdecimals: number | undefined,\n\t\twidth: FormatWidth,\n\t): string {\n\t\tif (value === null || value === undefined) return this.nullDisplay\n\t\tlet bytes = normalizeNumber(value)\n\t\tconst digits = decimals ?? this.defaultDecimalDigits ?? 2\n\t\tconst base = this.sizeFormatBase\n\n\t\tconst isNegative = bytes < 0\n\t\tbytes = Math.abs(bytes)\n\n\t\ttype SizeUnit = {\n\t\t\tthreshold: number\n\t\t\tlong: string\n\t\t\tshort: string\n\t\t}\n\n\t\tconst units1024: SizeUnit[] = [\n\t\t\t{ threshold: 1099511627776, long: \"terabytes\", short: \"TB\" },\n\t\t\t{ threshold: 1073741824, long: \"gigabytes\", short: \"GB\" },\n\t\t\t{ threshold: 1048576, long: \"megabytes\", short: \"MB\" },\n\t\t\t{ threshold: 1024, long: \"kilobytes\", short: \"KB\" },\n\t\t\t{ threshold: 0, long: \"bytes\", short: \"B\" },\n\t\t]\n\n\t\tconst units1000: SizeUnit[] = [\n\t\t\t{ threshold: 1000000000000, long: \"terabytes\", short: \"TB\" },\n\t\t\t{ threshold: 1000000000, long: \"gigabytes\", short: \"GB\" },\n\t\t\t{ threshold: 1000000, long: \"megabytes\", short: \"MB\" },\n\t\t\t{ threshold: 1000, long: \"kilobytes\", short: \"KB\" },\n\t\t\t{ threshold: 0, long: \"bytes\", short: \"B\" },\n\t\t]\n\n\t\tconst units = base === 1024 ? units1024 : units1000\n\n\t\tfor (const unit of units) {\n\t\t\tif (bytes >= unit.threshold && unit.threshold > 0) {\n\t\t\t\tconst val = bytes / unit.threshold\n\t\t\t\tconst sign = isNegative ? \"-\" : \"\"\n\t\t\t\tconst formatted = this.formatNumberPart(val, digits)\n\t\t\t\tconst label = width === \"long\" ? unit.long : unit.short\n\t\t\t\treturn `${sign}${formatted} ${label}`\n\t\t\t}\n\t\t}\n\n\t\tconst sign = isNegative ? \"-\" : \"\"\n\t\tconst label = width === \"long\" ? \"bytes\" : \"B\"\n\t\treturn `${sign}${Math.round(bytes)} ${label}`\n\t}\n\n\t/**\n\t * Format a measurement value (length or mass).\n\t * Automatically selects the most appropriate unit based on value magnitude.\n\t */\n\tprivate formatMeasure(\n\t\tvalue: unknown,\n\t\ttype: \"length\" | \"mass\",\n\t\twidth: FormatWidth,\n\t\tdecimals?: number,\n\t): string {\n\t\tif (value === null || value === undefined) return this.nullDisplay\n\t\tconst num = normalizeNumber(value)\n\t\tconst digits = decimals ?? this.defaultDecimalDigits ?? 2\n\n\t\tconst configs = this.getMeasureUnits(type)\n\t\tconst isNegative = num < 0\n\t\tconst absNum = Math.abs(num)\n\n\t\t// Find the best-fitting unit (largest unit where value >= 1)\n\t\tfor (let i = configs.length - 1; i >= 0; i--) {\n\t\t\tconst config = configs[i]\n\t\t\tif (!config) continue\n\t\t\tif (absNum >= config.factor || i === 0) {\n\t\t\t\tconst val = absNum / config.factor\n\t\t\t\tconst sign = isNegative ? \"-\" : \"\"\n\t\t\t\tconst formatted = this.formatNumberPart(val, digits)\n\t\t\t\tconst label = width === \"long\" ? config.longLabel : config.shortLabel\n\t\t\t\treturn `${sign}${formatted} ${label}`\n\t\t\t}\n\t\t}\n\n\t\treturn String(num)\n\t}\n\n\t/** Get measurement unit configs for the configured system (metric/imperial). */\n\tprivate getMeasureUnits(type: \"length\" | \"mass\"): MeasureUnitConfig[] {\n\t\tif (type === \"length\") {\n\t\t\tif (this.systemOfUnits === \"imperial\") {\n\t\t\t\treturn [\n\t\t\t\t\t{ factor: 1, longLabel: \"inches\", shortLabel: \"in\" },\n\t\t\t\t\t{ factor: 12, longLabel: \"feet\", shortLabel: \"ft\" },\n\t\t\t\t\t{ factor: 36, longLabel: \"yards\", shortLabel: \"yd\" },\n\t\t\t\t\t{ factor: 63360, longLabel: \"miles\", shortLabel: \"mi\" },\n\t\t\t\t]\n\t\t\t}\n\t\t\treturn [\n\t\t\t\t{ factor: 1, longLabel: \"millimeters\", shortLabel: \"mm\" },\n\t\t\t\t{ factor: 1000, longLabel: \"meters\", shortLabel: \"m\" },\n\t\t\t\t{ factor: 1000000, longLabel: \"kilometers\", shortLabel: \"km\" },\n\t\t\t]\n\t\t}\n\n\t\tif (this.systemOfUnits === \"imperial\") {\n\t\t\treturn [\n\t\t\t\t{ factor: 1, longLabel: \"grains\", shortLabel: \"gr\" },\n\t\t\t\t{ factor: 437.5, longLabel: \"ounces\", shortLabel: \"oz\" },\n\t\t\t\t{ factor: 7000, longLabel: \"pounds\", shortLabel: \"lb\" },\n\t\t\t]\n\t\t}\n\t\treturn [\n\t\t\t{ factor: 1, longLabel: \"grams\", shortLabel: \"g\" },\n\t\t\t{ factor: 1000, longLabel: \"kilograms\", shortLabel: \"kg\" },\n\t\t\t{ factor: 1000000, longLabel: \"tons\", shortLabel: \"t\" },\n\t\t]\n\t}\n\n\t/** Format the numeric part of a result using locale-aware Intl. */\n\tprivate formatNumberPart(num: number, digits: number): string {\n\t\tconst formatted = new Intl.NumberFormat(this.locale, {\n\t\t\tminimumFractionDigits: 0,\n\t\t\tmaximumFractionDigits: digits,\n\t\t}).format(num)\n\n\t\treturn applyCustomSeparators(\n\t\t\tformatted,\n\t\t\tthis.locale,\n\t\t\tthis.decimalSeparator,\n\t\t\tthis.thousandSeparator,\n\t\t)\n\t}\n\n\t/** Create a locale-aware duration label using Intl unit formatting. */\n\tprivate getDurationLabel(unit: string, count: number): string {\n\t\ttry {\n\t\t\tconst intlUnit = unit === \"month\" ? \"month\" : unit\n\t\t\treturn new Intl.NumberFormat(this.locale, {\n\t\t\t\tstyle: \"unit\",\n\t\t\t\tunit: intlUnit,\n\t\t\t\tunitDisplay: \"long\",\n\t\t\t}).format(count)\n\t\t} catch {\n\t\t\tconst plural = count !== 1 ? \"s\" : \"\"\n\t\t\treturn `${count} ${unit}${plural}`\n\t\t}\n\t}\n\n\t/** English ordinal suffix fallback. */\n\tprivate getOrdinalSuffixEn(n: number): string {\n\t\tconst abs = Math.abs(n)\n\t\tconst mod100 = abs % 100\n\t\tif (mod100 >= 11 && mod100 <= 13) return \"th\"\n\t\tswitch (abs % 10) {\n\t\t\tcase 1:\n\t\t\t\treturn \"st\"\n\t\t\tcase 2:\n\t\t\t\treturn \"nd\"\n\t\t\tcase 3:\n\t\t\t\treturn \"rd\"\n\t\t\tdefault:\n\t\t\t\treturn \"th\"\n\t\t}\n\t}\n}\n","import { Formatter } from \"./formatter.js\"\nimport type { FormatterOptions } from \"./types.js\"\n\n/**\n * Global singleton Formatter instance.\n *\n * Usage: import once at app entry, configure once, then use `formatter` everywhere.\n *\n * ```ts\n * // main.ts (once)\n * import { configureFormatter } from \"@template/helpers\"\n * configureFormatter({ locale: \"vi-VN\", currencyCode: \"VND\" })\n *\n * // any-page.tsx (no setup needed)\n * import { formatter } from \"@template/helpers\"\n * formatter.asCurrency(1234567)\n * ```\n */\nlet instance = new Formatter()\n\n/** The global Formatter singleton. Ready to use after `configureFormatter()`. */\nexport const formatter = new Proxy({} as Formatter, {\n\tget(_target, prop, receiver) {\n\t\treturn Reflect.get(instance, prop, receiver)\n\t},\n})\n\n/**\n * Configure the global formatter once (typically at app bootstrap).\n * Replaces the internal instance - all existing `formatter` references\n * automatically pick up the new config via the proxy.\n */\nexport function configureFormatter(options: FormatterOptions): void {\n\tinstance = new Formatter(options)\n}\n"],"mappings":"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"}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Configuration for the Formatter class, mirroring yii\i18n\Formatter properties.
|
|
4
|
+
* All properties are optional - sensible defaults are applied when not provided.
|
|
5
|
+
*/
|
|
6
|
+
interface FormatterOptions {
|
|
7
|
+
/** Locale used by the Intl API (default: "en-US") */
|
|
8
|
+
locale?: string;
|
|
9
|
+
/** Output time zone for date/time formatting (default: "UTC") */
|
|
10
|
+
timeZone?: string;
|
|
11
|
+
/** Time zone assumed for input values without explicit timezone (default: "UTC") */
|
|
12
|
+
defaultTimeZone?: string;
|
|
13
|
+
/** Default date format - ICU preset name or Intl options (default: "medium") */
|
|
14
|
+
dateFormat?: string | Intl.DateTimeFormatOptions;
|
|
15
|
+
/** Default time format (default: "medium") */
|
|
16
|
+
timeFormat?: string | Intl.DateTimeFormatOptions;
|
|
17
|
+
/** Default datetime format (default: "medium") */
|
|
18
|
+
datetimeFormat?: string | Intl.DateTimeFormatOptions;
|
|
19
|
+
/** [falsyLabel, truthyLabel] (default: ["No", "Yes"]) */
|
|
20
|
+
booleanFormat?: [string, string];
|
|
21
|
+
/** String displayed when value is null/undefined (default: "(not set)") */
|
|
22
|
+
nullDisplay?: string;
|
|
23
|
+
/** Default ISO 4217 currency code (default: "USD") */
|
|
24
|
+
currencyCode?: string;
|
|
25
|
+
/** Custom decimal separator (null = use locale default) */
|
|
26
|
+
decimalSeparator?: string | null;
|
|
27
|
+
/** Custom thousands separator (null = use locale default) */
|
|
28
|
+
thousandSeparator?: string | null;
|
|
29
|
+
/** Custom decimal separator for currency (null = use locale default) */
|
|
30
|
+
currencyDecimalSeparator?: string | null;
|
|
31
|
+
/** Base for file size calculation: 1024 (binary) or 1000 (decimal) */
|
|
32
|
+
sizeFormatBase?: 1024 | 1000;
|
|
33
|
+
/** System of measurement units: "metric" or "imperial" */
|
|
34
|
+
systemOfUnits?: "metric" | "imperial";
|
|
35
|
+
/** Default number of decimal digits (null = auto) */
|
|
36
|
+
defaultDecimalDigits?: number | null;
|
|
37
|
+
}
|
|
38
|
+
/** ICU-style date/time format width presets */
|
|
39
|
+
type DateFormatPreset = "short" | "medium" | "long" | "full";
|
|
40
|
+
/** Time units for Intl.RelativeTimeFormat */
|
|
41
|
+
type RelativeTimeUnit = "second" | "minute" | "hour" | "day" | "week" | "month" | "year";
|
|
42
|
+
/** System of measurement units */
|
|
43
|
+
type UnitSystem = "metric" | "imperial";
|
|
44
|
+
/** Measurement unit category */
|
|
45
|
+
type UnitType = "length" | "mass";
|
|
46
|
+
/** Format width for size/measurement output */
|
|
47
|
+
type FormatWidth = "long" | "short";
|
|
48
|
+
/** Configuration for a single measurement unit */
|
|
49
|
+
interface MeasureUnitConfig {
|
|
50
|
+
factor: number;
|
|
51
|
+
longLabel: string;
|
|
52
|
+
shortLabel: string;
|
|
53
|
+
}
|
|
54
|
+
/** Options for asEmail() */
|
|
55
|
+
interface EmailOptions {
|
|
56
|
+
/** Custom display text instead of the email address */
|
|
57
|
+
text?: string;
|
|
58
|
+
/** Email subject line */
|
|
59
|
+
subject?: string;
|
|
60
|
+
/** Email body content */
|
|
61
|
+
body?: string;
|
|
62
|
+
}
|
|
63
|
+
/** Options for asUrl() */
|
|
64
|
+
interface UrlOptions {
|
|
65
|
+
/** Target attribute for the link (default: "_blank") */
|
|
66
|
+
target?: string;
|
|
67
|
+
/** Custom display text instead of the URL */
|
|
68
|
+
text?: string;
|
|
69
|
+
/** Rel attribute for the link (e.g. "noopener noreferrer") */
|
|
70
|
+
rel?: string;
|
|
71
|
+
/** CSS class(es) for the link element */
|
|
72
|
+
class?: string;
|
|
73
|
+
}
|
|
74
|
+
/** Options for asImage() */
|
|
75
|
+
interface ImageOptions {
|
|
76
|
+
/** Alt text for the image */
|
|
77
|
+
alt?: string;
|
|
78
|
+
/** Image width (number for pixels, string for CSS value) */
|
|
79
|
+
width?: number | string;
|
|
80
|
+
/** Image height (number for pixels, string for CSS value) */
|
|
81
|
+
height?: number | string;
|
|
82
|
+
/** CSS class(es) for the image element */
|
|
83
|
+
class?: string;
|
|
84
|
+
/** Loading strategy: "lazy" defers offscreen images, "eager" loads immediately */
|
|
85
|
+
loading?: "lazy" | "eager";
|
|
86
|
+
}
|
|
87
|
+
/** Options for asParagraphs() */
|
|
88
|
+
interface ParagraphOptions {
|
|
89
|
+
/** HTML tag to wrap paragraphs (default: "p") */
|
|
90
|
+
tag?: string;
|
|
91
|
+
/** Whether to convert single newlines within paragraphs to `<br />` (default: false) */
|
|
92
|
+
lineBreaks?: boolean;
|
|
93
|
+
}
|
|
94
|
+
/** Allowlist-based HTML sanitizer config */
|
|
95
|
+
interface HtmlSanitizeConfig {
|
|
96
|
+
/** Allowed HTML tags (e.g. ["b", "i", "a", "p", "br"]) */
|
|
97
|
+
allowedTags?: string[];
|
|
98
|
+
/** Allowed attributes per tag (e.g. { a: ["href", "target"] }) */
|
|
99
|
+
allowedAttributes?: Record<string, string[]>;
|
|
100
|
+
}
|
|
101
|
+
/** Ordinal suffix map for a language: PluralRule category -> suffix */
|
|
102
|
+
type OrdinalSuffixMap = Record<string, string>;
|
|
103
|
+
/** Options for asNumberShort() */
|
|
104
|
+
interface NumberShortOptions {
|
|
105
|
+
/** Number of decimal places (default: 1) */
|
|
106
|
+
decimals?: number;
|
|
107
|
+
/** Fallback format when below smallest threshold: "currency" | "decimal" | "integer" (default: "currency") */
|
|
108
|
+
fallback?: "currency" | "decimal" | "integer";
|
|
109
|
+
/** Whether to add a space between number and suffix (default: false for backward compat) */
|
|
110
|
+
spaceBefore?: boolean;
|
|
111
|
+
}
|
|
112
|
+
/** Options for asGpsDistance() */
|
|
113
|
+
interface GpsDistanceOptions {
|
|
114
|
+
/** Output unit: "m" (meters), "km" (kilometers), "mi" (miles), "auto" (default: "auto") */
|
|
115
|
+
unit?: "m" | "km" | "mi" | "auto";
|
|
116
|
+
/** Number of decimal places (default: 1) */
|
|
117
|
+
decimals?: number;
|
|
118
|
+
/** Earth radius in meters (default: 6371000) */
|
|
119
|
+
earthRadius?: number;
|
|
120
|
+
}
|
|
121
|
+
/** Options for asMaskedValue() */
|
|
122
|
+
interface MaskOptions {
|
|
123
|
+
/** Number of visible characters at the start (default: 4) */
|
|
124
|
+
startVisible?: number;
|
|
125
|
+
/** Number of visible characters at the end (default: 3) */
|
|
126
|
+
endVisible?: number;
|
|
127
|
+
/** Character used for masking (default: "X") */
|
|
128
|
+
maskChar?: string;
|
|
129
|
+
/** Keep original character type (letters->letter mask, digits->digit mask). Currently uses maskChar for all. */
|
|
130
|
+
preserveFormat?: boolean;
|
|
131
|
+
}
|
|
132
|
+
//#endregion
|
|
133
|
+
//#region src/formatter.d.ts
|
|
134
|
+
/**
|
|
135
|
+
* TypeScript port of yii\i18n\Formatter.
|
|
136
|
+
*
|
|
137
|
+
* Uses only built-in Intl APIs - zero external dependencies.
|
|
138
|
+
* Supports: strings, HTML, numbers, currency, dates, times,
|
|
139
|
+
* file sizes, measurement units, and more.
|
|
140
|
+
*/
|
|
141
|
+
export declare class Formatter {
|
|
142
|
+
locale: string;
|
|
143
|
+
timeZone: string;
|
|
144
|
+
defaultTimeZone: string;
|
|
145
|
+
dateFormat: string | Intl.DateTimeFormatOptions;
|
|
146
|
+
timeFormat: string | Intl.DateTimeFormatOptions;
|
|
147
|
+
datetimeFormat: string | Intl.DateTimeFormatOptions;
|
|
148
|
+
booleanFormat: [string, string];
|
|
149
|
+
nullDisplay: string;
|
|
150
|
+
currencyCode: string;
|
|
151
|
+
decimalSeparator: string | null;
|
|
152
|
+
thousandSeparator: string | null;
|
|
153
|
+
currencyDecimalSeparator: string | null;
|
|
154
|
+
sizeFormatBase: 1024 | 1000;
|
|
155
|
+
systemOfUnits: UnitSystem;
|
|
156
|
+
defaultDecimalDigits: number | null;
|
|
157
|
+
constructor(options?: FormatterOptions);
|
|
158
|
+
/**
|
|
159
|
+
* Format a value by type name, like Yii2's `$formatter->format($value, 'date')`.
|
|
160
|
+
* Supports both string and tuple `[formatName, ...params]` signatures.
|
|
161
|
+
*/
|
|
162
|
+
format(value: unknown, type: string | [string, ...unknown[]]): string;
|
|
163
|
+
/** Returns the value as-is without any formatting. */
|
|
164
|
+
asRaw(value: unknown): string;
|
|
165
|
+
/** Formats the value as HTML-encoded plain text. */
|
|
166
|
+
asText(value: unknown): string;
|
|
167
|
+
/**
|
|
168
|
+
* Formats the value as HTML-encoded text with newlines converted to `<br />`.
|
|
169
|
+
* Handles all line-ending variants: `\r\n` (Windows), `\r` (old Mac), `\n` (Unix).
|
|
170
|
+
* Consecutive newlines produce multiple `<br />` tags.
|
|
171
|
+
*/
|
|
172
|
+
asNtext(value: unknown): string;
|
|
173
|
+
/**
|
|
174
|
+
* Formats the value as HTML-encoded text paragraphs (split by double newlines).
|
|
175
|
+
* Supports configurable wrapper tag and inline line-break conversion.
|
|
176
|
+
*/
|
|
177
|
+
asParagraphs(value: unknown, options?: ParagraphOptions): string;
|
|
178
|
+
/**
|
|
179
|
+
* Returns the value as HTML text.
|
|
180
|
+
* When a sanitize config is provided, only allowed tags and attributes are kept.
|
|
181
|
+
* Without config, the value is returned as-is (caller is responsible for safety).
|
|
182
|
+
*/
|
|
183
|
+
asHtml(value: unknown, sanitize?: HtmlSanitizeConfig): string;
|
|
184
|
+
/**
|
|
185
|
+
* Allowlist-based HTML sanitizer. Strips tags and attributes not in the config.
|
|
186
|
+
* Handles self-closing tags, nested tags, and attribute filtering.
|
|
187
|
+
*/
|
|
188
|
+
private static sanitizeHtml;
|
|
189
|
+
/**
|
|
190
|
+
* Formats the value as a mailto link.
|
|
191
|
+
* Supports custom display text, subject, and body parameters.
|
|
192
|
+
* Validates email format - returns escaped plain text for invalid emails.
|
|
193
|
+
*/
|
|
194
|
+
asEmail(value: unknown, options?: EmailOptions): string;
|
|
195
|
+
/** Basic email format validation (covers most common patterns). */
|
|
196
|
+
private static isValidEmail;
|
|
197
|
+
/**
|
|
198
|
+
* Formats the value as a hyperlink.
|
|
199
|
+
* Detects http, https, ftp, ftps, and mailto schemes.
|
|
200
|
+
* Prepends `http://` when no recognized scheme is present.
|
|
201
|
+
*/
|
|
202
|
+
asUrl(value: unknown, options?: UrlOptions): string;
|
|
203
|
+
/**
|
|
204
|
+
* Formats the value as an image tag.
|
|
205
|
+
* Supports width, height, CSS class, and loading strategy attributes.
|
|
206
|
+
*/
|
|
207
|
+
asImage(value: unknown, options?: ImageOptions): string;
|
|
208
|
+
/** Formats the value as a boolean using the configured booleanFormat labels. */
|
|
209
|
+
asBoolean(value: unknown): string;
|
|
210
|
+
/** Formats the value as an integer by removing decimal digits without rounding. */
|
|
211
|
+
asInteger(value: unknown): string;
|
|
212
|
+
/** Formats the value as a decimal number. */
|
|
213
|
+
asDecimal(value: unknown, decimals?: number): string;
|
|
214
|
+
/** Formats the value as a percent number with "%" sign. */
|
|
215
|
+
asPercent(value: unknown, decimals?: number): string;
|
|
216
|
+
/** Formats the value as a currency number using ISO 4217 codes. */
|
|
217
|
+
asCurrency(value: unknown, currency?: string): string;
|
|
218
|
+
/** Formats the value as a scientific number (e-notation). */
|
|
219
|
+
asScientific(value: unknown, decimals?: number): string;
|
|
220
|
+
/**
|
|
221
|
+
* Formats the value as a number spellout (e.g. 42 -> "forty-two").
|
|
222
|
+
* Supports multiple locales via the locales/ registry.
|
|
223
|
+
*/
|
|
224
|
+
asSpellout(value: unknown): string;
|
|
225
|
+
/**
|
|
226
|
+
* Formats the value as an ordinal number (e.g. 1 -> "1st", 2 -> "2nd").
|
|
227
|
+
* Supports multiple locales via built-in suffix maps and custom overrides
|
|
228
|
+
* through `Formatter.registerOrdinalSuffixes()`.
|
|
229
|
+
*/
|
|
230
|
+
asOrdinal(value: unknown): string;
|
|
231
|
+
/** Built-in ordinal suffix registry. Extensible at runtime. */
|
|
232
|
+
private static ordinalSuffixes;
|
|
233
|
+
/**
|
|
234
|
+
* Register ordinal suffixes for a language at runtime.
|
|
235
|
+
* Keys are Intl.PluralRules ordinal categories: "one", "two", "few", "other".
|
|
236
|
+
*/
|
|
237
|
+
static registerOrdinalSuffixes(lang: string, suffixes: OrdinalSuffixMap): void;
|
|
238
|
+
/** Formats the value as a date. */
|
|
239
|
+
asDate(value: unknown, format?: string | Intl.DateTimeFormatOptions): string;
|
|
240
|
+
/** Formats the value as a time. */
|
|
241
|
+
asTime(value: unknown, format?: string | Intl.DateTimeFormatOptions): string;
|
|
242
|
+
/** Formats the value as a datetime. */
|
|
243
|
+
asDatetime(value: unknown, format?: string | Intl.DateTimeFormatOptions): string;
|
|
244
|
+
/** Returns the value as a UNIX timestamp (seconds since epoch). */
|
|
245
|
+
asTimestamp(value: unknown): string;
|
|
246
|
+
/**
|
|
247
|
+
* Formats the value as the time interval between a date and now in human readable form.
|
|
248
|
+
* Uses Intl.RelativeTimeFormat (built-in in Node.js / browsers).
|
|
249
|
+
*/
|
|
250
|
+
asRelativeTime(value: unknown, referenceTime?: unknown): string;
|
|
251
|
+
/**
|
|
252
|
+
* Represents the value as duration in human readable format.
|
|
253
|
+
* Example: 5400 -> "1 hour, 30 minutes"
|
|
254
|
+
*/
|
|
255
|
+
asDuration(value: unknown, implode?: string): string;
|
|
256
|
+
/** Formats the value in bytes as a size in human readable form (e.g. "12 kilobytes"). */
|
|
257
|
+
asSize(value: unknown, decimals?: number): string;
|
|
258
|
+
/** Formats the value in bytes as a size in human readable form (e.g. "12 kB"). */
|
|
259
|
+
asShortSize(value: unknown, decimals?: number): string;
|
|
260
|
+
/** Formats the value as a length in human readable form (e.g. "12 meters"). */
|
|
261
|
+
asLength(value: unknown, decimals?: number): string;
|
|
262
|
+
/** Formats the value as a length in human readable form (e.g. "12 m"). */
|
|
263
|
+
asShortLength(value: unknown, decimals?: number): string;
|
|
264
|
+
/** Formats the value as a weight in human readable form (e.g. "12 kilograms"). */
|
|
265
|
+
asWeight(value: unknown, decimals?: number): string;
|
|
266
|
+
/** Formats the value as a weight in human readable form (e.g. "12 kg"). */
|
|
267
|
+
asShortWeight(value: unknown, decimals?: number): string;
|
|
268
|
+
/**
|
|
269
|
+
* Abbreviate a large number with locale-aware suffixes.
|
|
270
|
+
* e.g. 1500000 -> "1.5 Million" (en) or "1,5 Trieu" (vi).
|
|
271
|
+
*/
|
|
272
|
+
asNumberShort(value: unknown, options?: NumberShortOptions): string;
|
|
273
|
+
/**
|
|
274
|
+
* Format the GPS (great-circle) distance between two coordinates.
|
|
275
|
+
* Returns a human-readable string with unit suffix.
|
|
276
|
+
*/
|
|
277
|
+
asGpsDistance(latFrom: number, lonFrom: number, latTo: number, lonTo: number, options?: GpsDistanceOptions): string;
|
|
278
|
+
/** Haversine formula: returns distance in meters between two GPS coordinates. */
|
|
279
|
+
private static gpsDistance;
|
|
280
|
+
/**
|
|
281
|
+
* Mask a string value, showing only the first and last N characters.
|
|
282
|
+
* Instance method with options support.
|
|
283
|
+
*/
|
|
284
|
+
asMaskedValue(value: unknown, options?: MaskOptions): string;
|
|
285
|
+
/** Core masking logic used by asMaskedValue(). */
|
|
286
|
+
private static getMaskedValue;
|
|
287
|
+
/**
|
|
288
|
+
* Format bytes into the most appropriate size unit.
|
|
289
|
+
* Supports both base-1024 (binary) and base-1000 (decimal).
|
|
290
|
+
*/
|
|
291
|
+
private formatBytes;
|
|
292
|
+
/**
|
|
293
|
+
* Format a measurement value (length or mass).
|
|
294
|
+
* Automatically selects the most appropriate unit based on value magnitude.
|
|
295
|
+
*/
|
|
296
|
+
private formatMeasure;
|
|
297
|
+
/** Get measurement unit configs for the configured system (metric/imperial). */
|
|
298
|
+
private getMeasureUnits;
|
|
299
|
+
/** Format the numeric part of a result using locale-aware Intl. */
|
|
300
|
+
private formatNumberPart;
|
|
301
|
+
/** Create a locale-aware duration label using Intl unit formatting. */
|
|
302
|
+
private getDurationLabel;
|
|
303
|
+
/** English ordinal suffix fallback. */
|
|
304
|
+
private getOrdinalSuffixEn;
|
|
305
|
+
}
|
|
306
|
+
//#endregion
|
|
307
|
+
//#region src/global.d.ts
|
|
308
|
+
/** The global Formatter singleton. Ready to use after `configureFormatter()`. */
|
|
309
|
+
export declare const formatter: Formatter;
|
|
310
|
+
/**
|
|
311
|
+
* Configure the global formatter once (typically at app bootstrap).
|
|
312
|
+
* Replaces the internal instance - all existing `formatter` references
|
|
313
|
+
* automatically pick up the new config via the proxy.
|
|
314
|
+
*/
|
|
315
|
+
export declare function configureFormatter(options: FormatterOptions): void;
|
|
316
|
+
//#endregion
|
|
317
|
+
//#region src/locales/types.d.ts
|
|
318
|
+
/**
|
|
319
|
+
* Interface for locale-specific spellout and number-short providers.
|
|
320
|
+
* Implement this to add a new language for asSpellout() and formatNumberShort().
|
|
321
|
+
*/
|
|
322
|
+
interface LocaleSpellout {
|
|
323
|
+
/** Convert an integer to words (e.g. 42 -> "forty-two") */
|
|
324
|
+
integerToWords(n: number): string;
|
|
325
|
+
/** Convert a single digit to its word form (e.g. 5 -> "five") */
|
|
326
|
+
digitToWord(digit: string): string;
|
|
327
|
+
/** The word for "point" used between integer and decimal parts */
|
|
328
|
+
pointWord: string;
|
|
329
|
+
/** The word for "zero" */
|
|
330
|
+
zeroWord: string;
|
|
331
|
+
/** The prefix for negative numbers (e.g. "minus", "am") */
|
|
332
|
+
negativePrefix: string;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Short-number suffix configuration for a locale.
|
|
336
|
+
* Used by formatNumberShort() to abbreviate large numbers.
|
|
337
|
+
*/
|
|
338
|
+
interface NumberShortConfig {
|
|
339
|
+
thresholds: Array<{
|
|
340
|
+
value: number;
|
|
341
|
+
suffix: string;
|
|
342
|
+
}>;
|
|
343
|
+
}
|
|
344
|
+
//#endregion
|
|
345
|
+
//#region src/locales/index.d.ts
|
|
346
|
+
/** Get the spellout provider for a locale, falling back to English. */
|
|
347
|
+
export declare function getSpellout(locale: string): LocaleSpellout;
|
|
348
|
+
/** Get the number-short config for a locale, falling back to English. */
|
|
349
|
+
export declare function getNumberShortConfig(locale: string): NumberShortConfig;
|
|
350
|
+
/** Register a custom spellout locale at runtime. */
|
|
351
|
+
export declare function registerSpellout(lang: string, impl: LocaleSpellout): void;
|
|
352
|
+
/** Register a custom number-short config at runtime. */
|
|
353
|
+
export declare function registerNumberShort(lang: string, config: NumberShortConfig): void;
|
|
354
|
+
//#endregion
|
|
355
|
+
//#region src/utils.d.ts
|
|
356
|
+
/**
|
|
357
|
+
* Escape the 5 HTML-special characters, equivalent to PHP's htmlspecialchars().
|
|
358
|
+
* No external dependency - pure string replacement.
|
|
359
|
+
*/
|
|
360
|
+
export declare function escapeHtml(value: string): string;
|
|
361
|
+
/**
|
|
362
|
+
* Normalize an input value into a Date object.
|
|
363
|
+
* Accepts: Date, number (UNIX seconds or milliseconds), string (ISO 8601).
|
|
364
|
+
*/
|
|
365
|
+
export declare function normalizeDate(value: unknown): Date;
|
|
366
|
+
/**
|
|
367
|
+
* Normalize an input value into a number.
|
|
368
|
+
* Accepts: number, numeric string (with optional comma grouping), boolean.
|
|
369
|
+
*/
|
|
370
|
+
export declare function normalizeNumber(value: unknown): number;
|
|
371
|
+
//#endregion
|
|
372
|
+
export type { DateFormatPreset, EmailOptions, FormatWidth, FormatterOptions, GpsDistanceOptions, HtmlSanitizeConfig, ImageOptions, LocaleSpellout, MaskOptions, MeasureUnitConfig, NumberShortConfig, NumberShortOptions, OrdinalSuffixMap, ParagraphOptions, RelativeTimeUnit, UnitSystem, UnitType, UrlOptions };
|
|
373
|
+
//# sourceMappingURL=index.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/types.ts","../src/formatter.ts","../src/global.ts","../src/locales/types.ts","../src/locales/index.ts","../src/utils.ts"],"mappings":";;;;;UAIiB;;EAEhB;;EAGA;;EAGA;;EAGA,sBAAsB,KAAK;;EAG3B,sBAAsB,KAAK;;EAG3B,0BAA0B,KAAK;;EAG/B;;EAGA;;EAGA;;EAGA;;EAGA;;EAGA;;EAGA;;EAGA;;EAGA;;;KAIW;;KAGA;;KAUA;;KAGA;;KAGA;;UAGK;EAChB;EACA;EACA;;;UAIgB;;EAEhB;;EAEA;;EAEA;;;UAIgB;;EAEhB;;EAEA;;EAEA;;EAEA;;;UAIgB;;EAEhB;;EAEA;;EAEA;;EAEA;;EAEA;;;UAIgB;;EAEhB;;EAEA;;;UAIgB;;EAEhB;;EAEA,oBAAoB;;;KAIT,mBAAmB;;UAGd;;EAEhB;;EAEA;;EAEA;;;UAIgB;;EAEhB;;EAEA;;EAEA;;;UAIgB;;EAEhB;;EAEA;;EAEA;;EAEA;;;;;;;;;;;qBCrIY;EACL;EACA;EACA;EACA,qBAAqB,KAAK;EAC1B,qBAAqB,KAAK;EAC1B,yBAAyB,KAAK;EAC9B;EACA;EACA;EACA;EACA;EACA;EACA;EACA,eAAe;EACf;EAEP,YAAY,UAAS;;;;;EAwBd,OAAO,gBAAgB;;EAsBvB,MAAM;;EAMN,OAAO;;;;;;EAUP,QAAQ;;;;;EAUR,aAAa,gBAAgB,UAAU;;;;;;EAyBvC,OAAO,gBAAgB,WAAW;;;;;iBAW1B;;;;;;EAuDR,QAAQ,gBAAgB,UAAU;;iBAmB1B;;;;;;EASR,MAAM,gBAAgB,UAAU;;;;;EAqBhC,QAAQ,gBAAgB,UAAU;;EAoBlC,UAAU;;EAQV,UAAU;;EAmBV,UAAU,gBAAgB;;EAmB1B,UAAU,gBAAgB;;EAoB1B,WAAW,gBAAgB;;EAmB3B,aAAa,gBAAgB;;;;;EAgB7B,WAAW;;;;;;EA6BX,UAAU;;iBAoBF;;;;;SAiBD,wBACb,cACA,UAAU;;EAQJ,OACN,gBACA,kBAAkB,KAAK;;EAiBjB,OACN,gBACA,kBAAkB,KAAK;;EAiBjB,WACN,gBACA,kBAAkB,KAAK;;EAiBjB,YAAY;;;;;EAUZ,eAAe,gBAAgB;;;;;EAwB/B,WAAW,gBAAgB;;EA+B3B,OAAO,gBAAgB;;EAKvB,YAAY,gBAAgB;;EAK5B,SAAS,gBAAgB;;EAKzB,cAAc,gBAAgB;;EAK9B,SAAS,gBAAgB;;EAKzB,cAAc,gBAAgB;;;;;EAU9B,cAAc,gBAAgB,UAAU;;;;;EAgCxC,cACN,iBACA,iBACA,eACA,eACA,UAAU;;iBAuCI;;;;;EA0BR,cAAc,gBAAgB,UAAU;;iBAYhC;;;;;UAwBP;;;;;UAwDA;;UA+BA;;UAgCA;;UAeA;;UAeA;;;;;qBCh3BI,WAAS;;;;;;wBAWN,mBAAmB,SAAS;;;;;;;UC5B3B;;EAEhB,eAAe;;EAGf,YAAY;;EAGZ;;EAGA;;EAGA;;;;;;UAOgB;EAChB,YAAY;IACX;IACA;;;;;;wBCJc,YAAY,iBAAiB;;wBAM7B,qBAAqB,iBAAiB;;wBAMtC,iBAAiB,cAAc,MAAM;;wBAKrC,oBACf,cACA,QAAQ;;;;;;;wBCrCO,WAAW;;;;;wBAaX,cAAc,iBAAiB;;;;;wBAuB/B,gBAAgB"}
|