@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/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>") // "<b>hello</b>"
|
|
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>') // "<script>"
|
|
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)|
|