@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/README_vi.md ADDED
@@ -0,0 +1,519 @@
1
+ # @wikytam/helpers
2
+
3
+ Thư viện tiện ích TypeScript được chuyển đổi từ [yii\i18n\Formatter](https://www.yiiframework.com/doc/api/2.0/yii-i18n-formatter) và các hàm helper phổ biến của Yii2, **không phụ thuộc thư viện bên ngoài** - chỉ sử dụng `Intl` API có sẵn.
4
+
5
+ | | Kích thước |
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
+ ## Cài đặt
12
+
13
+ ```bash
14
+ npm install @wikytam/helpers
15
+ # hoặc
16
+ pnpm add @wikytam/helpers
17
+ # hoặc
18
+ yarn add @wikytam/helpers
19
+ ```
20
+
21
+ ```typescript
22
+ import { Formatter } from "@wikytam/helpers"
23
+ ```
24
+
25
+ ## Bắt đầu nhanh
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
+ ## Singleton toàn cục (cấu hình một lần, dùng mọi nơi)
49
+
50
+ Package cung cấp sẵn một instance `formatter` toàn cục. Bạn chỉ cần gọi `configureFormatter()` **một lần duy nhất** khi khởi động ứng dụng, sau đó import `formatter` ở bất kỳ file nào - không cần thiết lập thêm gì.
51
+
52
+ ### Bước 1: Cấu hình một lần tại điểm khởi động ứng dụng
53
+
54
+ ```typescript
55
+ // 1. Gọi ở file khởi động ứng dụng (main.tsx hoặc main.ts)
56
+ // Chỉ cần gọi MỘT LẦN DUY NHẤT, formatter sẽ tự động áp dụng
57
+ // cho toàn bộ ứng dụng (cả frontend lẫn backend)
58
+
59
+ // apps/frontend/src/main.tsx (frontend)
60
+ // hoặc apps/backend/src/main.ts (backend)
61
+ import { configureFormatter } from "@wikytam/helpers"
62
+
63
+ configureFormatter({
64
+ locale: "vi-VN", // Ngôn ngữ hiển thị
65
+ timeZone: "Asia/Ho_Chi_Minh", // Múi giờ đầu ra
66
+ currencyCode: "VND", // Mã tiền tệ ISO 4217
67
+ booleanFormat: ["Không", "Có"], // Nhãn cho giá trị đúng/sai
68
+ nullDisplay: "(chưa đặt)", // Hiển thị khi giá trị null/undefined
69
+ decimalSeparator: ",", // Dấu phân cách thập phân
70
+ thousandSeparator: ".", // Dấu phân cách hàng nghìn
71
+ })
72
+ ```
73
+
74
+ ### Bước 2: Sử dụng ở bất kỳ đâu - chỉ cần import `formatter`
75
+
76
+ ```typescript
77
+ // 2. Bất kỳ trang/component/service nào - chỉ cần import và dùng
78
+ // Không cần truyền instance, không cần tạo file riêng
79
+ // formatter đã được cấu hình sẵn từ bước 1
80
+
81
+ // Ví dụ: Trang danh sách đối tác
82
+ // pages/partners/PartnersList.tsx
83
+ import { formatter } from "@wikytam/helpers"
84
+
85
+ formatter.asCurrency(1234567) // "1.234.567 ₫"
86
+ formatter.asDate("2024-03-15") // "15 thg 3, 2024"
87
+ formatter.asBoolean(true) // "Có"
88
+ formatter.asSpellout(42) // "bốn mươi hai"
89
+ formatter.asNumberShort(5000000) // "5,0 Triệu"
90
+ ```
91
+
92
+ ```typescript
93
+ // Ví dụ: Trang chi tiết hợp đồng
94
+ // pages/contracts/ContractDetail.tsx
95
+ import { formatter } from "@wikytam/helpers"
96
+
97
+ formatter.asDuration(5400) // "1 giờ, 30 phút"
98
+ formatter.asRelativeTime(createdAt) // "2 ngày trước"
99
+ ```
100
+
101
+ ```typescript
102
+ // Ví dụ: Service phía backend
103
+ // services/export.service.ts
104
+ import { formatter } from "@wikytam/helpers"
105
+
106
+ const label = formatter.asCurrency(amount) // "1.234.567 ₫"
107
+ ```
108
+
109
+ Không cần tạo file riêng, không cần truyền instance qua props hay context.
110
+ Chỉ cần gọi `configureFormatter()` một lần ở đầu ứng dụng, sau đó `formatter` hoạt động ở mọi nơi.
111
+
112
+ ### Ghi đè cấu hình cho một trang cụ thể
113
+
114
+ Khi một trang cần cấu hình khác (ví dụ: trang xuất báo cáo tiếng Anh), chỉ cần tạo instance riêng:
115
+
116
+ ```typescript
117
+ // Ví dụ: Trang xuất báo cáo cần hiển thị tiếng Anh + USD
118
+ // pages/reports/ExportPage.tsx
119
+ import { Formatter } from "@wikytam/helpers"
120
+
121
+ // Tạo instance riêng với cấu hình khác, không ảnh hưởng đến formatter toàn cục
122
+ const exportFormatter = new Formatter({
123
+ locale: "en-US",
124
+ currencyCode: "USD",
125
+ })
126
+
127
+ export function ExportPage() {
128
+ return <span>{exportFormatter.asCurrency(1234.56)}</span> // "$1,234.56"
129
+ }
130
+ ```
131
+
132
+
133
+
134
+ ## Cấu hình
135
+
136
+ Tất cả các tùy chọn đều không bắt buộc, có giá trị mặc định hợp lý:
137
+
138
+ ```typescript
139
+ const f = new Formatter({
140
+ locale: "vi-VN", // Ngôn ngữ Intl (mặc định: "en-US")
141
+ timeZone: "Asia/Ho_Chi_Minh", // Múi giờ đầu ra (mặc định: "UTC")
142
+ defaultTimeZone: "UTC", // Múi giờ mặc định cho đầu vào không có timezone
143
+ dateFormat: "medium", // Định dạng ngày mặc định (preset hoặc Intl options)
144
+ timeFormat: "medium", // Định dạng giờ mặc định (preset hoặc Intl options)
145
+ datetimeFormat: "medium", // Định dạng ngày giờ mặc định (preset hoặc Intl options)
146
+ booleanFormat: ["Không", "Có"], // [giá trị sai, giá trị đúng]
147
+ nullDisplay: "(chưa đặt)", // Hiển thị khi giá trị null/undefined
148
+ currencyCode: "VND", // Mã tiền tệ ISO 4217
149
+ decimalSeparator: ",", // Dấu phân cách thập phân (null = theo ngôn ngữ)
150
+ thousandSeparator: ".", // Dấu phân cách hàng nghìn (null = theo ngôn ngữ)
151
+ currencyDecimalSeparator: null, // Dấu thập phân cho tiền tệ (null = theo ngôn ngữ)
152
+ sizeFormatBase: 1024, // 1024 (nhị phân) hoặc 1000 (thập phân)
153
+ systemOfUnits: "metric", // "metric" (mét) hoặc "imperial" (Anh/Mỹ)
154
+ defaultDecimalDigits: null, // Số chữ số thập phân mặc định (null = tự động)
155
+ })
156
+ ```
157
+
158
+
159
+
160
+ ### Định dạng ngày theo preset
161
+
162
+ Các tùy chọn `dateFormat`, `timeFormat`, `datetimeFormat` chấp nhận chuỗi preset hoặc đối tượng `Intl.DateTimeFormatOptions`:
163
+
164
+ | Preset | Ví dụ ngày | Ví dụ giờ |
165
+ | ---------- | -------------------- | ---------------- |
166
+ | `"short"` | `3/15/24` | `2:30 PM` |
167
+ | `"medium"` | `Mar 15, 2024` | `2:30:45 PM` |
168
+ | `"long"` | `March 15, 2024` | `2:30:45 PM UTC` |
169
+ | `"full"` | `Friday, March 15..` | `2:30:45 PM ..` |
170
+
171
+
172
+
173
+ ## Phương thức `format()` tổng quát
174
+
175
+ Giống Yii2, bạn có thể gọi linh hoạt theo tên định dạng:
176
+
177
+ ```typescript
178
+ f.format(value, "date") // gọi asDate(value)
179
+ f.format(value, "integer") // gọi asInteger(value)
180
+ f.format(value, ["decimal", 3]) // gọi asDecimal(value, 3)
181
+ f.format(value, ["currency", "EUR"]) // gọi asCurrency(value, "EUR")
182
+ f.format(null, "text") // trả về nullDisplay
183
+ ```
184
+
185
+
186
+
187
+ ## Tham chiếu API
188
+
189
+
190
+
191
+ ### Chuỗi & HTML
192
+
193
+ | Phương thức | Mô tả | Chữ ký |
194
+ | ---------------- | ------------------------------------------------------------------------------------- | ------------------------------- |
195
+ | `asRaw()` | Trả về giá trị nguyên bản, không định dạng gì. | `asRaw(value)` |
196
+ | `asText()` | Mã hóa HTML rồi trả về dạng văn bản thuần. | `asText(value)` |
197
+ | `asNtext()` | Mã hóa HTML, chuyển `\n`, `\r\n`, `\r` thành `<br />`. | `asNtext(value)` |
198
+ | `asParagraphs()` | Tách đoạn văn bằng dòng trống kép. Hỗ trợ tùy chọn tag wrapper và `<br />`. | `asParagraphs(value, options?)` |
199
+ | `asHtml()` | Trả về HTML, tùy chọn lọc (sanitize) theo danh sách trắng. | `asHtml(value, sanitize?)` |
200
+ | `asEmail()` | Tạo liên kết mailto. Hỗ trợ subject, body, text tùy chỉnh. Validate email. | `asEmail(value, options?)` |
201
+ | `asUrl()` | Tạo siêu liên kết. Nhận diện ftp://, mailto:. Hỗ trợ rel, class, text. | `asUrl(value, options?)` |
202
+ | `asImage()` | Tạo thẻ `<img>`. Hỗ trợ width, height, class, loading. | `asImage(value, options?)` |
203
+ | `asBoolean()` | Định dạng thành nhãn đúng/sai theo cấu hình. | `asBoolean(value)` |
204
+
205
+ ```typescript
206
+ // Định dạng cơ bản
207
+ f.asRaw("<b>hello</b>") // "<b>hello</b>"
208
+ f.asText("<b>hello</b>") // "&lt;b&gt;hello&lt;/b&gt;"
209
+ f.asNtext("a\nb") // "a<br />b"
210
+ f.asNtext("a\r\nb") // "a<br />b" (hỗ trợ Windows \r\n)
211
+ f.asNtext("a\n\nb") // "a<br /><br />b" (giữ dòng trống liên tiếp)
212
+ f.asBoolean(true) // "Yes"
213
+
214
+ // asParagraphs tùy chọn
215
+ f.asParagraphs("p1\n\np2") // "<p>p1</p>\n<p>p2</p>"
216
+ f.asParagraphs("A\n\nB", { tag: "div" }) // "<div>A</div>\n<div>B</div>"
217
+ f.asParagraphs("a\nb\n\nc", { lineBreaks: true }) // "<p>a<br />b</p>\n<p>c</p>"
218
+
219
+ // asHtml với trình lọc HTML
220
+ f.asHtml("<p>safe</p>") // "<p>safe</p>" (không lọc nếu không truyền config)
221
+ f.asHtml('<p>ok</p><script>bad</script>', {
222
+ allowedTags: ["p", "b", "i"],
223
+ }) // '<p>ok</p>bad'
224
+
225
+ // asEmail tùy chọn
226
+ f.asEmail("a@b.com") // '<a href="mailto:a@b.com">a@b.com</a>'
227
+ f.asEmail("a@b.com", { text: "Liên hệ" }) // '<a href="mailto:a@b.com">Liên hệ</a>'
228
+ f.asEmail("a@b.com", { subject: "Xin chào" }) // '<a href="mailto:a@b.com?subject=Xin%20ch%C3%A0o">a@b.com</a>'
229
+ f.asEmail("email-sai") // "email-sai" (trả về văn bản thuần nếu không hợp lệ)
230
+
231
+ // asUrl tùy chọn
232
+ f.asUrl("example.com") // '<a href="http://example.com" target="_blank">example.com</a>'
233
+ f.asUrl("ftp://files.example.com") // nhận diện ftp://
234
+ f.asUrl("https://x.com", { text: "Truy cập", rel: "noopener", class: "link" })
235
+
236
+ // asImage tùy chọn
237
+ f.asImage("/pic.jpg") // '<img src="/pic.jpg" alt="" />'
238
+ f.asImage("/pic.jpg", { alt: "Ảnh", width: 200, height: 150, loading: "lazy" })
239
+ ```
240
+
241
+
242
+
243
+ ### Số & Tiền tệ
244
+
245
+ | Phương thức | Mô tả | Chữ ký |
246
+ | ---------------- | --------------------------------------------------------------------------------------- | -------------------------------- |
247
+ | `asInteger()` | Định dạng thành số nguyên, bỏ phần thập phân (không làm tròn). | `asInteger(value)` |
248
+ | `asDecimal()` | Định dạng thành số thập phân. | `asDecimal(value, decimals?)` |
249
+ | `asPercent()` | Định dạng thành phần trăm có ký hiệu "%". | `asPercent(value, decimals?)` |
250
+ | `asCurrency()` | Định dạng thành tiền tệ theo mã ISO 4217. | `asCurrency(value, currency?)` |
251
+ | `asScientific()` | Định dạng thành ký hiệu khoa học (e-notation). | `asScientific(value, decimals?)` |
252
+ | `asSpellout()` | Đọc số thành chữ (ví dụ: 42 → "forty-two"). | `asSpellout(value)` |
253
+ | `asOrdinal()` | Định dạng thành số thứ tự (ví dụ: 1 → "1st"). | `asOrdinal(value)` |
254
+
255
+ ```typescript
256
+ f.asInteger(1234.99) // "1,234"
257
+ f.asDecimal(1234.5) // "1,234.50"
258
+ f.asDecimal(1234.5, 3) // "1,234.500"
259
+ f.asPercent(0.156, 1) // "15.6%"
260
+ f.asCurrency(1234.56) // "$1,234.56"
261
+ f.asCurrency(1234, "EUR") // "EUR 1,234.00" (tùy theo ngôn ngữ)
262
+ f.asScientific(1234567) // "1.23E6"
263
+ f.asSpellout(42) // "forty-two"
264
+ f.asOrdinal(3) // "3rd"
265
+ ```
266
+
267
+ #### Hỗ trợ ordinal đa ngôn ngữ
268
+
269
+ Hậu tố thứ tự có sẵn cho: `en`, `vi`, `fr`, `de`, `es`, `pt`, `it`, `ja`, `ko`, `zh`.
270
+
271
+ Thêm hậu tố tùy chỉnh lúc chạy:
272
+
273
+ ```typescript
274
+ Formatter.registerOrdinalSuffixes("nl", { other: "e" })
275
+
276
+ const fNl = new Formatter({ locale: "nl-NL" })
277
+ fNl.asOrdinal(1) // "1e"
278
+ ```
279
+
280
+
281
+
282
+ ### Ngày & Giờ
283
+
284
+ | Phương thức | Mô tả | Chữ ký |
285
+ | ------------------ | ----------------------------------------------------------------------------------------- | --------------------------------------- |
286
+ | `asDate()` | Định dạng thành ngày. | `asDate(value, format?)` |
287
+ | `asTime()` | Định dạng thành giờ. | `asTime(value, format?)` |
288
+ | `asDatetime()` | Định dạng thành ngày giờ. | `asDatetime(value, format?)` |
289
+ | `asTimestamp()` | Chuyển đổi thành UNIX timestamp (số giây kể từ 01-01-1970). | `asTimestamp(value)` |
290
+ | `asRelativeTime()` | Hiển thị khoảng cách thời gian so với hiện tại dưới dạng dễ đọc. | `asRelativeTime(value, referenceTime?)` |
291
+ | `asDuration()` | Hiển thị thời lượng dưới dạng dễ đọc (giờ, phút, giây). | `asDuration(value, implode?)` |
292
+
293
+ ```typescript
294
+ f.asDate("2024-03-15") // "Mar 15, 2024"
295
+ f.asDate("2024-03-15", "long") // "March 15, 2024"
296
+ f.asDate("2024-03-15", { year: "numeric", month: "2-digit", day: "2-digit" })
297
+ // "03/15/2024"
298
+ f.asTime(date) // "2:30:45 PM"
299
+ f.asDatetime(date) // "Mar 15, 2024, 2:30:45 PM"
300
+ f.asTimestamp("2024-03-15T14:30:45.000Z") // "1710513045"
301
+ f.asRelativeTime(twoDaysAgo) // "2 days ago"
302
+ f.asRelativeTime(date, referenceDate) // "in 2 hours"
303
+ f.asDuration(5400) // "1 hour, 30 minutes"
304
+ f.asDuration(90061) // "1 day, 1 hour, 1 minute, 1 second"
305
+ ```
306
+
307
+ Đầu vào ngày chấp nhận: đối tượng `Date`, UNIX timestamp (giây hoặc mili giây), chuỗi ISO 8601.
308
+
309
+ ### Kích thước & Đo lường
310
+
311
+ | Phương thức | Mô tả | Chữ ký |
312
+ | ----------------- | ------------------------------------------------------------------------- | --------------------------------- |
313
+ | `asSize()` | Định dạng byte thành dạng dễ đọc, ví dụ `12 kilobytes`. | `asSize(value, decimals?)` |
314
+ | `asShortSize()` | Định dạng byte thành dạng rút gọn, ví dụ `12 kB`. | `asShortSize(value, decimals?)` |
315
+ | `asLength()` | Định dạng độ dài dạng đầy đủ, ví dụ `12 meters`. | `asLength(value, decimals?)` |
316
+ | `asShortLength()` | Định dạng độ dài dạng rút gọn, ví dụ `12 m`. | `asShortLength(value, decimals?)` |
317
+ | `asWeight()` | Định dạng trọng lượng dạng đầy đủ, ví dụ `12 kilograms`. | `asWeight(value, decimals?)` |
318
+ | `asShortWeight()` | Định dạng trọng lượng dạng rút gọn, ví dụ `12 kg`. | `asShortWeight(value, decimals?)` |
319
+
320
+ ```typescript
321
+ // Kích thước file (cơ số 1024 mặc định)
322
+ f.asSize(1536) // "1.5 kilobytes"
323
+ f.asShortSize(1048576) // "1 MB"
324
+ f.asShortSize(1073741824) // "1 GB"
325
+
326
+ // Độ dài hệ mét (mặc định)
327
+ f.asLength(1500) // "1.5 meters"
328
+ f.asShortLength(5000000) // "5 km"
329
+
330
+ // Trọng lượng hệ mét (mặc định)
331
+ f.asWeight(1500) // "1.5 kilograms"
332
+ f.asShortWeight(5000000) // "5 t"
333
+
334
+ // Hệ đo Anh/Mỹ (imperial)
335
+ const fImp = new Formatter({ systemOfUnits: "imperial" })
336
+ fImp.asLength(24) // "2 feet"
337
+ fImp.asWeight(14000) // "2 pounds"
338
+
339
+ // Cơ số thập phân cho kích thước file
340
+ const f1000 = new Formatter({ sizeFormatBase: 1000 })
341
+ f1000.asShortSize(1500) // "1.5 KB"
342
+ ```
343
+
344
+
345
+
346
+ ### Phương thức tiện ích
347
+
348
+ | Phương thức | Mô tả | Chữ ký |
349
+ | -------------------- | --------------------------------------------------------------------------------------- | ---------------------------------------------- |
350
+ | `asNumberShort()` | Viết tắt số lớn kèm hậu tố theo ngôn ngữ (ví dụ: "1,5 Triệu", "2,3 Tỷ"). | `asNumberShort(value, options?)` |
351
+ | `asGpsDistance()` | Khoảng cách GPS được định dạng kèm đơn vị (km, m, mi). | `asGpsDistance(lat1, lon1, lat2, lon2, options?)`|
352
+ | `asMaskedValue()` | Che giấu chuỗi, chỉ hiển thị N ký tự đầu và cuối. | `asMaskedValue(value, options?)` |
353
+
354
+ ```typescript
355
+ // Viết tắt số lớn
356
+ f.asNumberShort(1_500_000) // "1.5 Million"
357
+ f.asNumberShort(42_000, { spaceBefore: true }) // "42.0 K"
358
+ f.asNumberShort(500, { fallback: "decimal" }) // "500.0"
359
+ f.asNumberShort(500, { fallback: "integer" }) // "500"
360
+ f.asNumberShort(1_234_567, { decimals: 2 }) // "1.23 Million"
361
+
362
+ const fVi = new Formatter({ locale: "vi-VN", currencyCode: "VND" })
363
+ fVi.asNumberShort(5_000_000) // "5,0 Triệu"
364
+ fVi.asNumberShort(1_500_000_000_000) // "1,5 Nghìn Tỷ"
365
+
366
+ // Khoảng cách GPS (có đơn vị)
367
+ f.asGpsDistance(40.7128, -74.006, 34.0522, -118.2437) // "3,944.4 km" (tự động)
368
+ f.asGpsDistance(40.7128, -74.006, 34.0522, -118.2437, { unit: "mi" }) // "2,450.8 mi"
369
+ f.asGpsDistance(10, 20, 10.001, 20) // "111.2 m" (khoảng cách ngắn)
370
+
371
+ // Che giấu dữ liệu nhạy cảm
372
+ f.asMaskedValue("0901234567") // "0901XXX567"
373
+ f.asMaskedValue("4111111111111111", { startVisible: 4, endVisible: 4 }) // "4111XXXXXXXX1111"
374
+ f.asMaskedValue("secret", { startVisible: 2, endVisible: 2, maskChar: "*" }) // "se**et"
375
+ f.asMaskedValue(null) // "(not set)"
376
+ ```
377
+
378
+
379
+
380
+ ## Xử lý giá trị null
381
+
382
+ Tất cả phương thức trả về `nullDisplay` khi giá trị là `null` hoặc `undefined`:
383
+
384
+ ```typescript
385
+ const f = new Formatter({ nullDisplay: "Không có dữ liệu" })
386
+ f.asText(null) // "Không có dữ liệu"
387
+ f.asInteger(null) // "Không có dữ liệu"
388
+ f.asDate(undefined) // "Không có dữ liệu"
389
+ ```
390
+
391
+
392
+
393
+ ## Ví dụ đa ngôn ngữ
394
+
395
+ ```typescript
396
+ // Tiếng Việt
397
+ const fVi = new Formatter({
398
+ locale: "vi-VN",
399
+ timeZone: "Asia/Ho_Chi_Minh",
400
+ currencyCode: "VND",
401
+ booleanFormat: ["Không", "Có"],
402
+ })
403
+ fVi.asCurrency(1234567) // "1.234.567 ₫"
404
+
405
+ // Tiếng Nhật
406
+ const fJa = new Formatter({
407
+ locale: "ja-JP",
408
+ timeZone: "Asia/Tokyo",
409
+ currencyCode: "JPY",
410
+ })
411
+ fJa.asCurrency(1234) // "¥1,234"
412
+ ```
413
+
414
+
415
+
416
+ ## Đọc số thành chữ (Spellout) đa ngôn ngữ
417
+
418
+ Phương thức `asSpellout()` hỗ trợ nhiều ngôn ngữ thông qua hệ thống đăng ký locale:
419
+
420
+ ```typescript
421
+ // Tiếng Anh (có sẵn)
422
+ const fEn = new Formatter({ locale: "en-US" })
423
+ fEn.asSpellout(42) // "forty-two"
424
+ fEn.asSpellout(1234567) // "one million two hundred thirty-four thousand five hundred sixty-seven"
425
+
426
+ // Tiếng Việt (có sẵn)
427
+ const fVi = new Formatter({ locale: "vi-VN" })
428
+ fVi.asSpellout(42) // "bốn mươi hai"
429
+ fVi.asSpellout(1500) // "một nghìn năm trăm"
430
+ fVi.asSpellout(1000000) // "một triệu"
431
+ ```
432
+
433
+
434
+
435
+ ### Thêm locale tùy chỉnh
436
+
437
+ ```typescript
438
+ import { registerSpellout, registerNumberShort } from "@wikytam/helpers"
439
+ import type { LocaleSpellout, NumberShortConfig } from "@wikytam/helpers"
440
+
441
+ // Ví dụ: Thêm tiếng Nhật
442
+ const jaSpellout: LocaleSpellout = {
443
+ zeroWord: "zero",
444
+ pointWord: "ten",
445
+ negativePrefix: "mainasu",
446
+ integerToWords: (n) => { /* triển khai logic đọc số tiếng Nhật */ },
447
+ digitToWord: (d) => { /* chuyển chữ số thành chữ tiếng Nhật */ },
448
+ }
449
+
450
+ registerSpellout("ja", jaSpellout)
451
+
452
+ // Formatter với locale "ja-JP" sẽ sử dụng triển khai của bạn
453
+ ```
454
+
455
+
456
+
457
+ ### Cấu trúc thư mục locale
458
+
459
+ ```
460
+ src/locales/
461
+ types.ts - Giao diện LocaleSpellout / NumberShortConfig
462
+ en.ts - Đọc số tiếng Anh + viết tắt số
463
+ vi.ts - Đọc số tiếng Việt + viết tắt số
464
+ index.ts - Bộ đăng ký: getSpellout(), registerSpellout(), v.v.
465
+ ```
466
+
467
+
468
+
469
+ ## Build
470
+
471
+ ```bash
472
+ pnpm build # Build ESM + CJS bằng tsdown
473
+ pnpm test # Chạy unit test
474
+ pnpm type-check # Kiểm tra kiểu TypeScript
475
+ ```
476
+
477
+
478
+
479
+ ## Các hàm tiện ích được export
480
+
481
+ Ngoài lớp `Formatter`, package cũng export các hàm helper:
482
+
483
+ ```typescript
484
+ import { escapeHtml, normalizeDate, normalizeNumber } from "@wikytam/helpers"
485
+
486
+ escapeHtml('<script>') // "&lt;script&gt;"
487
+ normalizeDate(1710513045) // Đối tượng Date
488
+ normalizeNumber("1,234.56") // 1234.56
489
+ ```
490
+
491
+ ```typescript
492
+ import { registerSpellout, registerNumberShort, getSpellout } from "@wikytam/helpers"
493
+
494
+ // Đăng ký locale lúc chạy (runtime), lấy provider locale
495
+ registerSpellout("ja", myJaSpellout)
496
+ const sp = getSpellout("ja")
497
+ ```
498
+
499
+
500
+
501
+ ## Khác biệt so với Yii2
502
+
503
+ | Tính năng | Yii2 | Package này |
504
+ | --------------------- | ------------------- | ------------------------------------------------ |
505
+ | Phụ thuộc | PHP intl extension | Intl API có sẵn (không phụ thuộc gì) |
506
+ | `asSpellout()` | ICU spellout | Hệ thống locale (en + vi có sẵn, mở rộng được) |
507
+ | `asHtml()` | HTMLPurifier | Trình lọc HTML theo danh sách trắng tích hợp |
508
+ | `asNtext()` | Chỉ `\n` | Hỗ trợ `\r\n`, `\r`, `\n` + dòng trống liên tiếp|
509
+ | `asParagraphs()` | Cố định thẻ `<p>` | Tùy chọn tag wrapper + `<br />` trong đoạn |
510
+ | `asEmail()` | Mailto cơ bản | Subject, body, text, validate email |
511
+ | `asUrl()` | Href cơ bản | rel, class, text, nhận diện ftp/mailto |
512
+ | `asImage()` | Img cơ bản | width, height, class, loading |
513
+ | `asOrdinal()` | ICU ordinal | 10 ngôn ngữ có sẵn + đăng ký runtime |
514
+ | Định dạng ngày | ICU patterns | Intl presets hoặc `Intl.DateTimeFormatOptions` |
515
+ | Cấu hình | Mảng PHP | Interface TypeScript `FormatterOptions` |
516
+ | `asNumberShort()` | Helper Yii2 tùy chỉnh | Instance method + options (fallback, spaceBefore)|
517
+ | `asGpsDistance()` | Helper Yii2 tùy chỉnh | Instance method + đơn vị/decimals options |
518
+ | `asMaskedValue()` | Helper Yii2 tùy chỉnh | Instance method + null handling + options |
519
+ | Đầu vào ngày | Chỉ Date/string | Tự nhận diện UNIX timestamp (giây hoặc mili giây)|