@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 wikytam
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
# @wikytam/helpers
|
|
2
|
+
|
|
3
|
+
A TypeScript utility library ported from [yii\i18n\Formatter](https://www.yiiframework.com/doc/api/2.0/yii-i18n-formatter) and common Yii2 helpers, with **zero external dependencies** - uses only built-in `Intl` APIs.
|
|
4
|
+
|
|
5
|
+
| | Size |
|
|
6
|
+
|---|---|
|
|
7
|
+
| ESM | 35.2 kB (9.5 kB gzip) |
|
|
8
|
+
| CJS | 35.5 kB (9.6 kB gzip) |
|
|
9
|
+
| Types | 16.0 kB |
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @wikytam/helpers
|
|
15
|
+
# or
|
|
16
|
+
pnpm add @wikytam/helpers
|
|
17
|
+
# or
|
|
18
|
+
yarn add @wikytam/helpers
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { Formatter } from "@wikytam/helpers"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { Formatter } from "@wikytam/helpers"
|
|
29
|
+
|
|
30
|
+
const f = new Formatter({
|
|
31
|
+
locale: "en-US",
|
|
32
|
+
timeZone: "UTC",
|
|
33
|
+
currencyCode: "USD",
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
f.asDate("2024-03-15") // "Mar 15, 2024"
|
|
37
|
+
f.asCurrency(1234.56) // "$1,234.56"
|
|
38
|
+
f.asPercent(0.156, 1) // "15.6%"
|
|
39
|
+
f.asShortSize(1048576) // "1 MB"
|
|
40
|
+
f.asRelativeTime(pastDate) // "2 days ago"
|
|
41
|
+
f.asBoolean(true) // "Yes"
|
|
42
|
+
f.asSpellout(42) // "forty-two"
|
|
43
|
+
f.asOrdinal(3) // "3rd"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
## Global Singleton (Configure Once, Use Everywhere)
|
|
49
|
+
|
|
50
|
+
The package ships a built-in global `formatter` singleton. Call `configureFormatter()` **once** at app startup, then import `formatter` in any file - no extra setup needed.
|
|
51
|
+
|
|
52
|
+
### Step 1: Configure once at app entry
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
// Call at your app entry point (main.tsx or main.ts)
|
|
56
|
+
// Only needs to be called ONCE - the formatter will be available globally
|
|
57
|
+
import { configureFormatter } from "@wikytam/helpers"
|
|
58
|
+
|
|
59
|
+
configureFormatter({
|
|
60
|
+
locale: "vi-VN",
|
|
61
|
+
timeZone: "Asia/Ho_Chi_Minh",
|
|
62
|
+
currencyCode: "VND",
|
|
63
|
+
booleanFormat: ["Không", "Có"], // Vietnamese: [false, true] labels
|
|
64
|
+
nullDisplay: "(chua dat)", // Vietnamese: shown for null/undefined
|
|
65
|
+
decimalSeparator: ",",
|
|
66
|
+
thousandSeparator: ".",
|
|
67
|
+
})
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Step 2: Use anywhere - just import `formatter`
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
// ANY page, component, or service - just import and use
|
|
74
|
+
// No need to pass instances, no need to create wrapper files
|
|
75
|
+
import { formatter } from "@wikytam/helpers"
|
|
76
|
+
|
|
77
|
+
formatter.asCurrency(1234567) // "1.234.567 d"
|
|
78
|
+
formatter.asDate("2024-03-15") // "15 thg 3, 2024"
|
|
79
|
+
formatter.asBoolean(true) // "Có"
|
|
80
|
+
formatter.asSpellout(42) // "bốn mươi hai"
|
|
81
|
+
formatter.asNumberShort(5000000) // "5,0 Trieu"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
// Works in backend services too
|
|
86
|
+
import { formatter } from "@wikytam/helpers"
|
|
87
|
+
|
|
88
|
+
formatter.asDuration(5400) // "1 giờ, 30 phút"
|
|
89
|
+
formatter.asRelativeTime(createdAt) // "2 ngày trước"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
One `configureFormatter()` call at startup, then `formatter` works everywhere.
|
|
93
|
+
|
|
94
|
+
### Override for a specific page
|
|
95
|
+
|
|
96
|
+
When one page needs different settings, create a local instance:
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
import { Formatter } from "@wikytam/helpers"
|
|
100
|
+
|
|
101
|
+
// Local instance with different settings - does not affect the global singleton
|
|
102
|
+
const exportFormatter = new Formatter({
|
|
103
|
+
locale: "en-US",
|
|
104
|
+
currencyCode: "USD",
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
export function ExportPage() {
|
|
108
|
+
return <span>{exportFormatter.asCurrency(1234.56)}</span> // "$1,234.56"
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
## Configuration
|
|
115
|
+
|
|
116
|
+
All options are optional with sensible defaults:
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
const f = new Formatter({
|
|
120
|
+
locale: "vi-VN", // Intl locale (default: "en-US")
|
|
121
|
+
timeZone: "Asia/Ho_Chi_Minh", // Output timezone (default: "UTC")
|
|
122
|
+
defaultTimeZone: "UTC", // Assumed TZ for inputs without timezone
|
|
123
|
+
dateFormat: "medium", // Default date format preset or Intl options
|
|
124
|
+
timeFormat: "medium", // Default time format preset or Intl options
|
|
125
|
+
datetimeFormat: "medium", // Default datetime format preset or Intl options
|
|
126
|
+
booleanFormat: ["No", "Yes"], // [falsy, truthy] labels
|
|
127
|
+
nullDisplay: "(not set)", // Shown for null/undefined values
|
|
128
|
+
currencyCode: "VND", // ISO 4217 currency code
|
|
129
|
+
decimalSeparator: ",", // Custom decimal separator (null = locale default)
|
|
130
|
+
thousandSeparator: ".", // Custom thousand separator (null = locale default)
|
|
131
|
+
currencyDecimalSeparator: null, // Custom decimal for currency (null = locale default)
|
|
132
|
+
sizeFormatBase: 1024, // 1024 (binary) or 1000 (decimal)
|
|
133
|
+
systemOfUnits: "metric", // "metric" or "imperial"
|
|
134
|
+
defaultDecimalDigits: null, // Default fraction digits (null = auto per method)
|
|
135
|
+
})
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
### Date Format Presets
|
|
141
|
+
|
|
142
|
+
The `dateFormat`, `timeFormat`, and `datetimeFormat` options accept either a preset string or an `Intl.DateTimeFormatOptions` object:
|
|
143
|
+
|
|
144
|
+
| Preset | Date Example | Time Example |
|
|
145
|
+
| ---------- | -------------------- | ---------------- |
|
|
146
|
+
| `"short"` | `3/15/24` | `2:30 PM` |
|
|
147
|
+
| `"medium"` | `Mar 15, 2024` | `2:30:45 PM` |
|
|
148
|
+
| `"long"` | `March 15, 2024` | `2:30:45 PM UTC` |
|
|
149
|
+
| `"full"` | `Friday, March 15..` | `2:30:45 PM ..` |
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
## Generic `format()` Method
|
|
154
|
+
|
|
155
|
+
Like Yii2, you can dispatch dynamically by format name:
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
f.format(value, "date") // calls asDate(value)
|
|
159
|
+
f.format(value, "integer") // calls asInteger(value)
|
|
160
|
+
f.format(value, ["decimal", 3]) // calls asDecimal(value, 3)
|
|
161
|
+
f.format(value, ["currency", "EUR"]) // calls asCurrency(value, "EUR")
|
|
162
|
+
f.format(null, "text") // returns nullDisplay
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
## API Reference
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
### String & HTML
|
|
172
|
+
|
|
173
|
+
| Method | Description | Signature |
|
|
174
|
+
| ---------------- | ------------------------------------------------------------ | ----------------------------------- |
|
|
175
|
+
| `asRaw()` | Returns the value as-is without any formatting. | `asRaw(value)` |
|
|
176
|
+
| `asText()` | HTML-encodes the value as plain text. | `asText(value)` |
|
|
177
|
+
| `asNtext()` | HTML-encodes with newlines (`\n`, `\r\n`, `\r`) as `<br />`.| `asNtext(value)` |
|
|
178
|
+
| `asParagraphs()` | Splits by double newlines into paragraphs. | `asParagraphs(value, options?)` |
|
|
179
|
+
| `asHtml()` | Returns HTML, optionally sanitized via allowlist. | `asHtml(value, sanitize?)` |
|
|
180
|
+
| `asEmail()` | Creates a mailto link with optional subject/body. | `asEmail(value, options?)` |
|
|
181
|
+
| `asUrl()` | Creates a hyperlink with rel, class, text options. | `asUrl(value, options?)` |
|
|
182
|
+
| `asImage()` | Creates an `<img>` tag with width/height/class/loading. | `asImage(value, options?)` |
|
|
183
|
+
| `asBoolean()` | Formats as boolean using configured labels. | `asBoolean(value)` |
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
// Basic formatting
|
|
187
|
+
f.asRaw("<b>hello</b>") // "<b>hello</b>"
|
|
188
|
+
f.asText("<b>hello</b>") // "<b>hello</b>"
|
|
189
|
+
f.asNtext("a\nb") // "a<br />b"
|
|
190
|
+
f.asNtext("a\r\nb") // "a<br />b" (Windows line endings)
|
|
191
|
+
f.asNtext("a\n\nb") // "a<br /><br />b" (consecutive newlines preserved)
|
|
192
|
+
f.asParagraphs("p1\n\np2") // "<p>p1</p>\n<p>p2</p>"
|
|
193
|
+
f.asBoolean(true) // "Yes"
|
|
194
|
+
|
|
195
|
+
// asParagraphs options
|
|
196
|
+
f.asParagraphs("A\n\nB", { tag: "div" }) // "<div>A</div>\n<div>B</div>"
|
|
197
|
+
f.asParagraphs("a\nb\n\nc", { lineBreaks: true }) // "<p>a<br />b</p>\n<p>c</p>"
|
|
198
|
+
|
|
199
|
+
// asHtml with sanitizer
|
|
200
|
+
f.asHtml("<p>safe</p>") // "<p>safe</p>" (no sanitize config = pass-through)
|
|
201
|
+
f.asHtml('<p>ok</p><script>bad</script>', {
|
|
202
|
+
allowedTags: ["p", "b", "i"],
|
|
203
|
+
}) // '<p>ok</p>bad'
|
|
204
|
+
f.asHtml('<a href="x" onclick="evil()">Link</a>', {
|
|
205
|
+
allowedTags: ["a"],
|
|
206
|
+
allowedAttributes: { a: ["href"] },
|
|
207
|
+
}) // '<a href="x">Link</a>'
|
|
208
|
+
|
|
209
|
+
// asEmail options
|
|
210
|
+
f.asEmail("a@b.com") // '<a href="mailto:a@b.com">a@b.com</a>'
|
|
211
|
+
f.asEmail("a@b.com", { text: "Contact us" }) // '<a href="mailto:a@b.com">Contact us</a>'
|
|
212
|
+
f.asEmail("a@b.com", { subject: "Hi", body: "Hello" })
|
|
213
|
+
// '<a href="mailto:a@b.com?subject=Hi&body=Hello">a@b.com</a>'
|
|
214
|
+
f.asEmail("invalid-email") // "invalid-email" (plain text for invalid)
|
|
215
|
+
|
|
216
|
+
// asUrl options
|
|
217
|
+
f.asUrl("example.com") // '<a href="http://example.com" target="_blank">example.com</a>'
|
|
218
|
+
f.asUrl("ftp://files.example.com") // detects ftp:// scheme
|
|
219
|
+
f.asUrl("https://x.com", { text: "Visit", rel: "noopener", class: "link" })
|
|
220
|
+
// '<a href="https://x.com" target="_blank" rel="noopener" class="link">Visit</a>'
|
|
221
|
+
|
|
222
|
+
// asImage options
|
|
223
|
+
f.asImage("/pic.jpg") // '<img src="/pic.jpg" alt="" />'
|
|
224
|
+
f.asImage("/pic.jpg", { alt: "Photo", width: 200, height: 150 })
|
|
225
|
+
// '<img src="/pic.jpg" alt="Photo" width="200" height="150" />'
|
|
226
|
+
f.asImage("/pic.jpg", { class: "rounded", loading: "lazy" })
|
|
227
|
+
// '<img src="/pic.jpg" alt="" class="rounded" loading="lazy" />'
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
#### Option Types
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
interface EmailOptions {
|
|
234
|
+
text?: string // Custom display text
|
|
235
|
+
subject?: string // Email subject
|
|
236
|
+
body?: string // Email body
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
interface UrlOptions {
|
|
240
|
+
target?: string // Link target (default: "_blank")
|
|
241
|
+
text?: string // Custom display text
|
|
242
|
+
rel?: string // Rel attribute
|
|
243
|
+
class?: string // CSS class(es)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
interface ImageOptions {
|
|
247
|
+
alt?: string // Alt text
|
|
248
|
+
width?: number | string // Width
|
|
249
|
+
height?: number | string // Height
|
|
250
|
+
class?: string // CSS class(es)
|
|
251
|
+
loading?: "lazy" | "eager" // Loading strategy
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
interface ParagraphOptions {
|
|
255
|
+
tag?: string // Wrapper tag (default: "p")
|
|
256
|
+
lineBreaks?: boolean // Convert \n to <br /> within paragraphs (default: false)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
interface HtmlSanitizeConfig {
|
|
260
|
+
allowedTags?: string[] // Allowed HTML tags
|
|
261
|
+
allowedAttributes?: Record<string, string[]> // Allowed attributes per tag
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
### Number & Currency
|
|
268
|
+
|
|
269
|
+
| Method | Description | Signature |
|
|
270
|
+
| ---------------- | -------------------------------------------------- | -------------------------------- |
|
|
271
|
+
| `asInteger()` | Formats as integer (truncates, no rounding). | `asInteger(value)` |
|
|
272
|
+
| `asDecimal()` | Formats as decimal number. | `asDecimal(value, decimals?)` |
|
|
273
|
+
| `asPercent()` | Formats as percent with "%" sign. | `asPercent(value, decimals?)` |
|
|
274
|
+
| `asCurrency()` | Formats as currency using ISO 4217 codes. | `asCurrency(value, currency?)` |
|
|
275
|
+
| `asScientific()` | Formats as scientific notation (e-notation). | `asScientific(value, decimals?)` |
|
|
276
|
+
| `asSpellout()` | Spells out a number in words. | `asSpellout(value)` |
|
|
277
|
+
| `asOrdinal()` | Formats as ordinal (e.g. "1st", "2nd"). | `asOrdinal(value)` |
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
f.asInteger(1234.99) // "1,234"
|
|
281
|
+
f.asDecimal(1234.5) // "1,234.50"
|
|
282
|
+
f.asDecimal(1234.5, 3) // "1,234.500"
|
|
283
|
+
f.asPercent(0.156, 1) // "15.6%"
|
|
284
|
+
f.asCurrency(1234.56) // "$1,234.56"
|
|
285
|
+
f.asCurrency(1234, "EUR") // "EUR 1,234.00" (varies by locale)
|
|
286
|
+
f.asScientific(1234567) // "1.23E6"
|
|
287
|
+
f.asSpellout(42) // "forty-two"
|
|
288
|
+
f.asOrdinal(3) // "3rd"
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
#### Ordinal Locale Support
|
|
292
|
+
|
|
293
|
+
Built-in ordinal suffixes for: `en`, `vi`, `fr`, `de`, `es`, `pt`, `it`, `ja`, `ko`, `zh`.
|
|
294
|
+
|
|
295
|
+
Add custom ordinal suffixes at runtime:
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
Formatter.registerOrdinalSuffixes("nl", { other: "e" })
|
|
299
|
+
|
|
300
|
+
const fNl = new Formatter({ locale: "nl-NL" })
|
|
301
|
+
fNl.asOrdinal(1) // "1e"
|
|
302
|
+
fNl.asOrdinal(5) // "5e"
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
### Date & Time
|
|
308
|
+
|
|
309
|
+
| Method | Description | Signature |
|
|
310
|
+
| ------------------ | --------------------------------------------- | --------------------------------------- |
|
|
311
|
+
| `asDate()` | Formats as date. | `asDate(value, format?)` |
|
|
312
|
+
| `asTime()` | Formats as time. | `asTime(value, format?)` |
|
|
313
|
+
| `asDatetime()` | Formats as datetime. | `asDatetime(value, format?)` |
|
|
314
|
+
| `asTimestamp()` | Converts to UNIX timestamp (seconds). | `asTimestamp(value)` |
|
|
315
|
+
| `asRelativeTime()` | Human-readable time interval from now. | `asRelativeTime(value, referenceTime?)` |
|
|
316
|
+
| `asDuration()` | Human-readable duration. | `asDuration(value, implode?)` |
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
f.asDate("2024-03-15") // "Mar 15, 2024"
|
|
320
|
+
f.asDate("2024-03-15", "long") // "March 15, 2024"
|
|
321
|
+
f.asDate("2024-03-15", { year: "numeric", month: "2-digit", day: "2-digit" })
|
|
322
|
+
// "03/15/2024"
|
|
323
|
+
f.asTime(date) // "2:30:45 PM"
|
|
324
|
+
f.asDatetime(date) // "Mar 15, 2024, 2:30:45 PM"
|
|
325
|
+
f.asTimestamp("2024-03-15T14:30:45.000Z") // "1710513045"
|
|
326
|
+
f.asRelativeTime(twoDaysAgo) // "2 days ago"
|
|
327
|
+
f.asRelativeTime(date, referenceDate) // "in 2 hours"
|
|
328
|
+
f.asDuration(5400) // "1 hour, 30 minutes"
|
|
329
|
+
f.asDuration(90061) // "1 day, 1 hour, 1 minute, 1 second"
|
|
330
|
+
|
|
331
|
+
// UNIX timestamps are auto-detected and converted
|
|
332
|
+
f.asDate(1710513045) // "Mar 15, 2024" (seconds -> auto-convert)
|
|
333
|
+
f.asDate(1710513045000) // "Mar 15, 2024" (milliseconds -> auto-convert)
|
|
334
|
+
f.asDatetime(1710513045) // "Mar 15, 2024, 2:30:45 PM"
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Date inputs accept: `Date` objects, UNIX timestamps (seconds or milliseconds - auto-detected), ISO 8601 strings.
|
|
338
|
+
Numbers below `1e12` are treated as seconds, above as milliseconds.
|
|
339
|
+
|
|
340
|
+
### Size & Measurement
|
|
341
|
+
|
|
342
|
+
| Method | Description | Signature |
|
|
343
|
+
| ----------------- | -------------------------------------------------------- | --------------------------------- |
|
|
344
|
+
| `asSize()` | Formats bytes as size (e.g. `12 kilobytes`). | `asSize(value, decimals?)` |
|
|
345
|
+
| `asShortSize()` | Formats bytes as short size (e.g. `12 kB`). | `asShortSize(value, decimals?)` |
|
|
346
|
+
| `asLength()` | Formats length (e.g. `12 meters`). | `asLength(value, decimals?)` |
|
|
347
|
+
| `asShortLength()` | Formats short length (e.g. `12 m`). | `asShortLength(value, decimals?)` |
|
|
348
|
+
| `asWeight()` | Formats weight (e.g. `12 kilograms`). | `asWeight(value, decimals?)` |
|
|
349
|
+
| `asShortWeight()` | Formats short weight (e.g. `12 kg`). | `asShortWeight(value, decimals?)` |
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
// File sizes (base 1024 by default)
|
|
353
|
+
f.asSize(1536) // "1.5 kilobytes"
|
|
354
|
+
f.asShortSize(1048576) // "1 MB"
|
|
355
|
+
f.asShortSize(1073741824) // "1 GB"
|
|
356
|
+
|
|
357
|
+
// Metric lengths (default)
|
|
358
|
+
f.asLength(1500) // "1.5 meters"
|
|
359
|
+
f.asShortLength(5000000) // "5 km"
|
|
360
|
+
|
|
361
|
+
// Metric weights (default)
|
|
362
|
+
f.asWeight(1500) // "1.5 kilograms"
|
|
363
|
+
f.asShortWeight(5000000) // "5 t"
|
|
364
|
+
|
|
365
|
+
// Imperial system
|
|
366
|
+
const fImp = new Formatter({ systemOfUnits: "imperial" })
|
|
367
|
+
fImp.asLength(24) // "2 feet"
|
|
368
|
+
fImp.asWeight(14000) // "2 pounds"
|
|
369
|
+
|
|
370
|
+
// Decimal base for file sizes
|
|
371
|
+
const f1000 = new Formatter({ sizeFormatBase: 1000 })
|
|
372
|
+
f1000.asShortSize(1500) // "1.5 KB"
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
### Utility Methods
|
|
378
|
+
|
|
379
|
+
| Method | Description | Signature |
|
|
380
|
+
| -------------------- | ------------------------------------------------- | -------------------------------------------------- |
|
|
381
|
+
| `asNumberShort()` | Abbreviates large numbers with locale suffixes. | `asNumberShort(value, options?)` |
|
|
382
|
+
| `asGpsDistance()` | Formatted distance between GPS coordinates. | `asGpsDistance(lat1, lon1, lat2, lon2, options?)` |
|
|
383
|
+
| `asMaskedValue()` | Masks a string, showing first/last N characters. | `asMaskedValue(value, options?)` |
|
|
384
|
+
|
|
385
|
+
```typescript
|
|
386
|
+
// Abbreviate large numbers
|
|
387
|
+
f.asNumberShort(1_500_000) // "1.5 Million"
|
|
388
|
+
f.asNumberShort(2_300_000_000) // "2.3 Billion"
|
|
389
|
+
f.asNumberShort(999) // "$999.00" (falls back to currency)
|
|
390
|
+
f.asNumberShort(42_000, { spaceBefore: true }) // "42.0 K"
|
|
391
|
+
f.asNumberShort(500, { fallback: "decimal" }) // "500.0"
|
|
392
|
+
f.asNumberShort(500, { fallback: "integer" }) // "500"
|
|
393
|
+
f.asNumberShort(1_234_567, { decimals: 2 }) // "1.23 Million"
|
|
394
|
+
|
|
395
|
+
const fVi = new Formatter({ locale: "vi-VN", currencyCode: "VND" })
|
|
396
|
+
fVi.asNumberShort(5_000_000) // "5,0 Trieu"
|
|
397
|
+
fVi.asNumberShort(1_500_000_000_000) // "1,5 Nghin Ty"
|
|
398
|
+
|
|
399
|
+
// GPS distance (formatted with units)
|
|
400
|
+
f.asGpsDistance(40.7128, -74.006, 34.0522, -118.2437) // "3,944.4 km" (auto)
|
|
401
|
+
f.asGpsDistance(40.7128, -74.006, 34.0522, -118.2437, { unit: "mi" }) // "2,450.8 mi"
|
|
402
|
+
f.asGpsDistance(10, 20, 10.001, 20) // "111.2 m" (auto: short distance)
|
|
403
|
+
f.asGpsDistance(10, 20, 11, 20, { unit: "km", decimals: 3 }) // "111.195 km"
|
|
404
|
+
|
|
405
|
+
// Mask sensitive data
|
|
406
|
+
f.asMaskedValue("0901234567") // "0901XXX567"
|
|
407
|
+
f.asMaskedValue("4111111111111111", { startVisible: 4, endVisible: 4 }) // "4111XXXXXXXX1111"
|
|
408
|
+
f.asMaskedValue("secret", { startVisible: 2, endVisible: 2, maskChar: "*" }) // "se**et"
|
|
409
|
+
f.asMaskedValue(null) // "(not set)"
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
#### Option Types
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
interface NumberShortOptions {
|
|
416
|
+
decimals?: number // Decimal places (default: 1)
|
|
417
|
+
fallback?: "currency" | "decimal" | "integer" // Below threshold (default: "currency")
|
|
418
|
+
spaceBefore?: boolean // Space between number and suffix (default: false)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
interface GpsDistanceOptions {
|
|
422
|
+
unit?: "m" | "km" | "mi" | "auto" // Output unit (default: "auto")
|
|
423
|
+
decimals?: number // Decimal places (default: 1)
|
|
424
|
+
earthRadius?: number // Earth radius in meters (default: 6371000)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
interface MaskOptions {
|
|
428
|
+
startVisible?: number // Visible chars at start (default: 4)
|
|
429
|
+
endVisible?: number // Visible chars at end (default: 3)
|
|
430
|
+
maskChar?: string // Mask character (default: "X")
|
|
431
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
## Null Handling
|
|
437
|
+
|
|
438
|
+
All methods return `nullDisplay` when the value is `null` or `undefined`:
|
|
439
|
+
|
|
440
|
+
```typescript
|
|
441
|
+
const f = new Formatter({ nullDisplay: "N/A" })
|
|
442
|
+
f.asText(null) // "N/A"
|
|
443
|
+
f.asInteger(null) // "N/A"
|
|
444
|
+
f.asDate(undefined) // "N/A"
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
## Multi-locale Examples
|
|
450
|
+
|
|
451
|
+
```typescript
|
|
452
|
+
// Vietnamese
|
|
453
|
+
const fVi = new Formatter({
|
|
454
|
+
locale: "vi-VN",
|
|
455
|
+
timeZone: "Asia/Ho_Chi_Minh",
|
|
456
|
+
currencyCode: "VND",
|
|
457
|
+
booleanFormat: ["Khong", "Co"],
|
|
458
|
+
})
|
|
459
|
+
fVi.asCurrency(1234567) // "1.234.567 d"
|
|
460
|
+
|
|
461
|
+
// Japanese
|
|
462
|
+
const fJa = new Formatter({
|
|
463
|
+
locale: "ja-JP",
|
|
464
|
+
timeZone: "Asia/Tokyo",
|
|
465
|
+
currencyCode: "JPY",
|
|
466
|
+
})
|
|
467
|
+
fJa.asCurrency(1234) // "Y1,234"
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
## Multi-locale Spellout
|
|
473
|
+
|
|
474
|
+
The `asSpellout()` method supports multiple languages via a pluggable locale registry:
|
|
475
|
+
|
|
476
|
+
```typescript
|
|
477
|
+
// English (built-in)
|
|
478
|
+
const fEn = new Formatter({ locale: "en-US" })
|
|
479
|
+
fEn.asSpellout(42) // "forty-two"
|
|
480
|
+
fEn.asSpellout(1234567) // "one million two hundred thirty-four thousand five hundred sixty-seven"
|
|
481
|
+
|
|
482
|
+
// Vietnamese (built-in)
|
|
483
|
+
const fVi = new Formatter({ locale: "vi-VN" })
|
|
484
|
+
fVi.asSpellout(42) // "bon muoi hai"
|
|
485
|
+
fVi.asSpellout(1500) // "mot nghin nam tram"
|
|
486
|
+
fVi.asSpellout(1000000) // "mot trieu"
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
### Adding a Custom Locale
|
|
492
|
+
|
|
493
|
+
```typescript
|
|
494
|
+
import { registerSpellout, registerNumberShort } from "@wikytam/helpers"
|
|
495
|
+
import type { LocaleSpellout, NumberShortConfig } from "@wikytam/helpers"
|
|
496
|
+
|
|
497
|
+
const jaSpellout: LocaleSpellout = {
|
|
498
|
+
zeroWord: "zero",
|
|
499
|
+
pointWord: "ten",
|
|
500
|
+
negativePrefix: "mainasu",
|
|
501
|
+
integerToWords: (n) => { /* ... */ },
|
|
502
|
+
digitToWord: (d) => { /* ... */ },
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
registerSpellout("ja", jaSpellout)
|
|
506
|
+
|
|
507
|
+
// Now Formatter with locale "ja-JP" will use your implementation
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
### Locale Directory Structure
|
|
513
|
+
|
|
514
|
+
```
|
|
515
|
+
src/locales/
|
|
516
|
+
types.ts - LocaleSpellout / NumberShortConfig interfaces
|
|
517
|
+
en.ts - English spellout + number-short
|
|
518
|
+
vi.ts - Vietnamese spellout + number-short
|
|
519
|
+
index.ts - Registry with getSpellout(), registerSpellout(), etc.
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
## Build
|
|
525
|
+
|
|
526
|
+
```bash
|
|
527
|
+
pnpm build # Build ESM + CJS via tsdown
|
|
528
|
+
pnpm test # Run unit tests
|
|
529
|
+
pnpm type-check # TypeScript type checking
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
## Exported Utilities
|
|
535
|
+
|
|
536
|
+
In addition to the `Formatter` class, the package exports helper functions:
|
|
537
|
+
|
|
538
|
+
```typescript
|
|
539
|
+
import { escapeHtml, normalizeDate, normalizeNumber } from "@wikytam/helpers"
|
|
540
|
+
|
|
541
|
+
escapeHtml('<script>') // "<script>"
|
|
542
|
+
normalizeDate(1710513045) // Date object
|
|
543
|
+
normalizeNumber("1,234.56") // 1234.56
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
```typescript
|
|
547
|
+
import { registerSpellout, registerNumberShort, getSpellout } from "@wikytam/helpers"
|
|
548
|
+
|
|
549
|
+
// Register runtime locale, get locale provider
|
|
550
|
+
registerSpellout("ja", myJaSpellout)
|
|
551
|
+
const sp = getSpellout("ja")
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
## Differences from Yii2
|
|
557
|
+
|
|
558
|
+
| Feature | Yii2 | This Package |
|
|
559
|
+
| --------------------- | ------------------- | ---------------------------------------------- |
|
|
560
|
+
| Dependencies | PHP intl extension | Built-in Intl API (zero deps) |
|
|
561
|
+
| `asSpellout()` | ICU spellout | Locale registry (en + vi built-in, extensible) |
|
|
562
|
+
| `asHtml()` | HTMLPurifier | Built-in allowlist sanitizer + pass-through |
|
|
563
|
+
| `asNtext()` | Basic newlines | Handles `\r\n`, `\r`, `\n` + consecutive |
|
|
564
|
+
| `asParagraphs()` | Fixed `<p>` tag | Configurable tag + inline `<br />` option |
|
|
565
|
+
| `asEmail()` | Basic mailto | Subject, body, text, email validation |
|
|
566
|
+
| `asUrl()` | Basic href | rel, class, text, ftp/mailto scheme detection |
|
|
567
|
+
| `asImage()` | Basic img | width, height, class, loading attributes |
|
|
568
|
+
| `asOrdinal()` | ICU ordinal | 10 built-in locales + runtime registration |
|
|
569
|
+
| Date formats | ICU patterns | Intl presets or `Intl.DateTimeFormatOptions` |
|
|
570
|
+
| Config | PHP array | TypeScript `FormatterOptions` interface |
|
|
571
|
+
| `asNumberShort()` | Custom Yii2 helper | Instance method + options (fallback, spaceBefore)|
|
|
572
|
+
| `asGpsDistance()` | Custom Yii2 helper | Instance method + unit/decimals options |
|
|
573
|
+
| `asMaskedValue()` | Custom Yii2 helper | Instance method + null handling + options |
|
|
574
|
+
| Date inputs | Only Date/string | Auto-detects UNIX timestamps (s or ms) |
|