@lokascript/i18n 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.md +286 -0
- package/dist/browser.cjs +7669 -0
- package/dist/browser.cjs.map +1 -0
- package/dist/browser.d.cts +50 -0
- package/dist/browser.d.ts +50 -0
- package/dist/browser.js +7592 -0
- package/dist/browser.js.map +1 -0
- package/dist/hyperfixi-i18n.min.js +2 -0
- package/dist/hyperfixi-i18n.min.js.map +1 -0
- package/dist/hyperfixi-i18n.mjs +8558 -0
- package/dist/hyperfixi-i18n.mjs.map +1 -0
- package/dist/index.cjs +14205 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +947 -0
- package/dist/index.d.ts +947 -0
- package/dist/index.js +14095 -0
- package/dist/index.js.map +1 -0
- package/dist/transformer-Ckask-yw.d.cts +1041 -0
- package/dist/transformer-Ckask-yw.d.ts +1041 -0
- package/package.json +84 -0
- package/src/browser.ts +122 -0
- package/src/compatibility/browser-tests/grammar-demo.spec.ts +169 -0
- package/src/constants.ts +366 -0
- package/src/dictionaries/ar.ts +233 -0
- package/src/dictionaries/bn.ts +156 -0
- package/src/dictionaries/de.ts +233 -0
- package/src/dictionaries/derive.ts +515 -0
- package/src/dictionaries/en.ts +237 -0
- package/src/dictionaries/es.ts +233 -0
- package/src/dictionaries/fr.ts +233 -0
- package/src/dictionaries/hi.ts +270 -0
- package/src/dictionaries/id.ts +233 -0
- package/src/dictionaries/index.ts +238 -0
- package/src/dictionaries/it.ts +233 -0
- package/src/dictionaries/ja.ts +233 -0
- package/src/dictionaries/ko.ts +233 -0
- package/src/dictionaries/ms.ts +276 -0
- package/src/dictionaries/pl.ts +239 -0
- package/src/dictionaries/pt.ts +237 -0
- package/src/dictionaries/qu.ts +233 -0
- package/src/dictionaries/ru.ts +270 -0
- package/src/dictionaries/sw.ts +233 -0
- package/src/dictionaries/th.ts +156 -0
- package/src/dictionaries/tl.ts +276 -0
- package/src/dictionaries/tr.ts +233 -0
- package/src/dictionaries/uk.ts +270 -0
- package/src/dictionaries/vi.ts +210 -0
- package/src/dictionaries/zh.ts +233 -0
- package/src/enhanced-i18n.test.ts +454 -0
- package/src/enhanced-i18n.ts +713 -0
- package/src/examples/new-languages.ts +326 -0
- package/src/formatting.test.ts +213 -0
- package/src/formatting.ts +416 -0
- package/src/grammar/direct-mappings.ts +353 -0
- package/src/grammar/grammar.test.ts +1053 -0
- package/src/grammar/index.ts +59 -0
- package/src/grammar/profiles/index.ts +860 -0
- package/src/grammar/transformer.ts +1318 -0
- package/src/grammar/types.ts +630 -0
- package/src/index.ts +202 -0
- package/src/new-languages.test.ts +389 -0
- package/src/parser/analyze-conflicts.test.ts +229 -0
- package/src/parser/ar.ts +40 -0
- package/src/parser/create-provider.ts +309 -0
- package/src/parser/de.ts +36 -0
- package/src/parser/es.ts +31 -0
- package/src/parser/fr.ts +31 -0
- package/src/parser/id.ts +34 -0
- package/src/parser/index.ts +50 -0
- package/src/parser/ja.ts +36 -0
- package/src/parser/ko.ts +37 -0
- package/src/parser/locale-manager.test.ts +198 -0
- package/src/parser/locale-manager.ts +197 -0
- package/src/parser/parser-integration.test.ts +439 -0
- package/src/parser/pt.ts +37 -0
- package/src/parser/qu.ts +37 -0
- package/src/parser/sw.ts +37 -0
- package/src/parser/tr.ts +38 -0
- package/src/parser/types.ts +113 -0
- package/src/parser/zh.ts +38 -0
- package/src/plugins/vite.ts +224 -0
- package/src/plugins/webpack.ts +124 -0
- package/src/pluralization.test.ts +197 -0
- package/src/pluralization.ts +393 -0
- package/src/runtime.ts +441 -0
- package/src/ssr-integration.ts +225 -0
- package/src/test-setup.ts +195 -0
- package/src/translation-validation.test.ts +171 -0
- package/src/translator.test.ts +252 -0
- package/src/translator.ts +297 -0
- package/src/types.ts +209 -0
- package/src/utils/locale.ts +190 -0
- package/src/utils/tokenizer-adapter.ts +469 -0
- package/src/utils/tokenizer.ts +19 -0
- package/src/validators/index.ts +174 -0
- package/src/validators/schema.ts +129 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
// packages/i18n/src/formatting.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Locale-aware formatting utilities for i18n
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface NumberFormatOptions {
|
|
8
|
+
style?: 'decimal' | 'currency' | 'percent';
|
|
9
|
+
currency?: string;
|
|
10
|
+
minimumFractionDigits?: number;
|
|
11
|
+
maximumFractionDigits?: number;
|
|
12
|
+
useGrouping?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface DateFormatOptions {
|
|
16
|
+
dateStyle?: 'full' | 'long' | 'medium' | 'short';
|
|
17
|
+
timeStyle?: 'full' | 'long' | 'medium' | 'short';
|
|
18
|
+
year?: 'numeric' | '2-digit';
|
|
19
|
+
month?: 'numeric' | '2-digit' | 'long' | 'short' | 'narrow';
|
|
20
|
+
day?: 'numeric' | '2-digit';
|
|
21
|
+
hour?: 'numeric' | '2-digit';
|
|
22
|
+
minute?: 'numeric' | '2-digit';
|
|
23
|
+
second?: 'numeric' | '2-digit';
|
|
24
|
+
timeZone?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface RelativeTimeFormatOptions {
|
|
28
|
+
style?: 'long' | 'short' | 'narrow';
|
|
29
|
+
numeric?: 'always' | 'auto';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Format numbers according to locale
|
|
34
|
+
*/
|
|
35
|
+
export class NumberFormatter {
|
|
36
|
+
private locale: string;
|
|
37
|
+
private defaultOptions: NumberFormatOptions;
|
|
38
|
+
|
|
39
|
+
constructor(locale: string, defaultOptions: NumberFormatOptions = {}) {
|
|
40
|
+
this.locale = locale;
|
|
41
|
+
this.defaultOptions = defaultOptions;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
format(value: number, options: NumberFormatOptions = {}): string {
|
|
45
|
+
const mergedOptions = { ...this.defaultOptions, ...options };
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const result = new Intl.NumberFormat(this.locale, mergedOptions).format(value);
|
|
49
|
+
// Validate result - if style is currency but no currency symbol, use fallback
|
|
50
|
+
if (mergedOptions.style === 'currency' && !/[$€£¥₹₩₽฿₪₴₱₫₵₦]/.test(result)) {
|
|
51
|
+
return this.fallbackFormat(value, mergedOptions);
|
|
52
|
+
}
|
|
53
|
+
// Validate result - if style is percent but no % symbol, use fallback
|
|
54
|
+
if (mergedOptions.style === 'percent' && !result.includes('%')) {
|
|
55
|
+
return this.fallbackFormat(value, mergedOptions);
|
|
56
|
+
}
|
|
57
|
+
// Validate result - if input has decimals but output lost them (broken Intl like jsdom)
|
|
58
|
+
// Check by seeing if the result represents a rounded integer when it shouldn't
|
|
59
|
+
const hasFractionalPart = value % 1 !== 0;
|
|
60
|
+
if (hasFractionalPart && mergedOptions.style !== 'percent') {
|
|
61
|
+
// Extract numeric value from result (remove thousand separators, keep decimal)
|
|
62
|
+
// If the result only contains digits and thousand separators (no decimal), it's broken
|
|
63
|
+
const digitsOnly = result.replace(/[^\d.-]/g, '');
|
|
64
|
+
const parsedValue = parseFloat(digitsOnly);
|
|
65
|
+
// If parsed value is an integer but input had decimals, Intl rounded incorrectly
|
|
66
|
+
if (!isNaN(parsedValue) && parsedValue % 1 === 0) {
|
|
67
|
+
return this.fallbackFormat(value, mergedOptions);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
// Fallback for unsupported locales
|
|
73
|
+
return this.fallbackFormat(value, mergedOptions);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
formatCurrency(
|
|
78
|
+
value: number,
|
|
79
|
+
currency: string,
|
|
80
|
+
options: Omit<NumberFormatOptions, 'style' | 'currency'> = {}
|
|
81
|
+
): string {
|
|
82
|
+
return this.format(value, {
|
|
83
|
+
...options,
|
|
84
|
+
style: 'currency',
|
|
85
|
+
currency,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
formatPercent(value: number, options: Omit<NumberFormatOptions, 'style'> = {}): string {
|
|
90
|
+
// Note: Intl.NumberFormat with style: 'percent' multiplies by 100
|
|
91
|
+
// So 0.25 becomes "25%"
|
|
92
|
+
return this.format(value, {
|
|
93
|
+
...options,
|
|
94
|
+
style: 'percent',
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private fallbackFormat(value: number, options: NumberFormatOptions): string {
|
|
99
|
+
const { style, currency, useGrouping = true } = options;
|
|
100
|
+
// Default fraction digits based on style
|
|
101
|
+
const defaultMin = style === 'currency' ? 2 : 0;
|
|
102
|
+
const defaultMax = style === 'currency' ? 2 : 3;
|
|
103
|
+
const minimumFractionDigits = options.minimumFractionDigits ?? defaultMin;
|
|
104
|
+
const maximumFractionDigits = options.maximumFractionDigits ?? defaultMax;
|
|
105
|
+
|
|
106
|
+
let formatted = value.toFixed(
|
|
107
|
+
Math.min(maximumFractionDigits, Math.max(minimumFractionDigits, 0))
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// Remove trailing zeros after decimal point if not required
|
|
111
|
+
if (minimumFractionDigits === 0 && formatted.includes('.')) {
|
|
112
|
+
formatted = formatted.replace(/\.?0+$/, '');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Add thousand separators if grouping is enabled
|
|
116
|
+
if (useGrouping !== false) {
|
|
117
|
+
const parts = formatted.split('.');
|
|
118
|
+
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
119
|
+
formatted = parts.join('.');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (style === 'currency' && currency) {
|
|
123
|
+
// Map currency codes to symbols
|
|
124
|
+
const currencySymbols: Record<string, string> = {
|
|
125
|
+
USD: '$',
|
|
126
|
+
EUR: '€',
|
|
127
|
+
GBP: '£',
|
|
128
|
+
JPY: '¥',
|
|
129
|
+
CNY: '¥',
|
|
130
|
+
KRW: '₩',
|
|
131
|
+
RUB: '₽',
|
|
132
|
+
INR: '₹',
|
|
133
|
+
THB: '฿',
|
|
134
|
+
ILS: '₪',
|
|
135
|
+
UAH: '₴',
|
|
136
|
+
PHP: '₱',
|
|
137
|
+
VND: '₫',
|
|
138
|
+
GHS: '₵',
|
|
139
|
+
NGN: '₦',
|
|
140
|
+
};
|
|
141
|
+
const symbol = currencySymbols[currency] || currency;
|
|
142
|
+
return `${symbol}${formatted}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (style === 'percent') {
|
|
146
|
+
// Multiply by 100 for percent display (matching Intl behavior)
|
|
147
|
+
const percentValue = (value * 100).toFixed(
|
|
148
|
+
Math.min(maximumFractionDigits, Math.max(minimumFractionDigits, 0))
|
|
149
|
+
);
|
|
150
|
+
const cleanPercent =
|
|
151
|
+
minimumFractionDigits === 0 && percentValue.includes('.')
|
|
152
|
+
? percentValue.replace(/\.?0+$/, '')
|
|
153
|
+
: percentValue;
|
|
154
|
+
return `${cleanPercent}%`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return formatted;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Format dates according to locale
|
|
163
|
+
*/
|
|
164
|
+
export class DateFormatter {
|
|
165
|
+
private locale: string;
|
|
166
|
+
private defaultOptions: DateFormatOptions;
|
|
167
|
+
|
|
168
|
+
constructor(locale: string, defaultOptions: DateFormatOptions = {}) {
|
|
169
|
+
this.locale = locale;
|
|
170
|
+
this.defaultOptions = defaultOptions;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
format(date: Date | string | number, options: DateFormatOptions = {}): string {
|
|
174
|
+
const dateObj = new Date(date);
|
|
175
|
+
const mergedOptions = { ...this.defaultOptions, ...options };
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
const result = new Intl.DateTimeFormat(this.locale, mergedOptions).format(dateObj);
|
|
179
|
+
// Validate: if timeStyle was requested but no time in result, use fallback
|
|
180
|
+
if (mergedOptions.timeStyle && !/\d{1,2}:\d{2}/.test(result)) {
|
|
181
|
+
return this.fallbackFormat(dateObj, mergedOptions);
|
|
182
|
+
}
|
|
183
|
+
return result;
|
|
184
|
+
} catch (error) {
|
|
185
|
+
return this.fallbackFormat(dateObj, mergedOptions);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
formatRelative(date: Date | string | number, options: RelativeTimeFormatOptions = {}): string {
|
|
190
|
+
const dateObj = new Date(date);
|
|
191
|
+
const now = new Date();
|
|
192
|
+
const diffMs = dateObj.getTime() - now.getTime();
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const rtf = new Intl.RelativeTimeFormat(this.locale, options);
|
|
196
|
+
|
|
197
|
+
const diffSeconds = Math.round(diffMs / 1000);
|
|
198
|
+
const diffMinutes = Math.round(diffMs / (1000 * 60));
|
|
199
|
+
const diffHours = Math.round(diffMs / (1000 * 60 * 60));
|
|
200
|
+
const diffDays = Math.round(diffMs / (1000 * 60 * 60 * 24));
|
|
201
|
+
|
|
202
|
+
if (Math.abs(diffSeconds) < 60) {
|
|
203
|
+
return rtf.format(diffSeconds, 'second');
|
|
204
|
+
} else if (Math.abs(diffMinutes) < 60) {
|
|
205
|
+
return rtf.format(diffMinutes, 'minute');
|
|
206
|
+
} else if (Math.abs(diffHours) < 24) {
|
|
207
|
+
return rtf.format(diffHours, 'hour');
|
|
208
|
+
} else if (Math.abs(diffDays) < 30) {
|
|
209
|
+
return rtf.format(diffDays, 'day');
|
|
210
|
+
} else {
|
|
211
|
+
return this.format(dateObj);
|
|
212
|
+
}
|
|
213
|
+
} catch (error) {
|
|
214
|
+
return this.fallbackRelativeFormat(dateObj, now);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private fallbackFormat(date: Date, options: DateFormatOptions): string {
|
|
219
|
+
// Basic fallback formatting
|
|
220
|
+
const year = date.getFullYear();
|
|
221
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
222
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
223
|
+
const hour = String(date.getHours()).padStart(2, '0');
|
|
224
|
+
const minute = String(date.getMinutes()).padStart(2, '0');
|
|
225
|
+
|
|
226
|
+
const dateStr = `${month}/${day}/${year}`;
|
|
227
|
+
const timeStr = `${hour}:${minute}`;
|
|
228
|
+
|
|
229
|
+
// Include time if timeStyle is set
|
|
230
|
+
if (options.timeStyle) {
|
|
231
|
+
return options.dateStyle ? `${dateStr} ${timeStr}` : timeStr;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (options.dateStyle || (!options.dateStyle && !options.timeStyle)) {
|
|
235
|
+
return dateStr;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return date.toString();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private fallbackRelativeFormat(date: Date, now: Date): string {
|
|
242
|
+
const diffMs = date.getTime() - now.getTime();
|
|
243
|
+
const diffDays = Math.round(diffMs / (1000 * 60 * 60 * 24));
|
|
244
|
+
|
|
245
|
+
if (diffDays === 0) return 'today';
|
|
246
|
+
if (diffDays === 1) return 'tomorrow';
|
|
247
|
+
if (diffDays === -1) return 'yesterday';
|
|
248
|
+
if (diffDays > 0) return `in ${diffDays} days`;
|
|
249
|
+
return `${Math.abs(diffDays)} days ago`;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Locale-aware formatting manager
|
|
255
|
+
*/
|
|
256
|
+
export class LocaleFormatter {
|
|
257
|
+
private locale: string;
|
|
258
|
+
private numberFormatter: NumberFormatter;
|
|
259
|
+
private dateFormatter: DateFormatter;
|
|
260
|
+
|
|
261
|
+
constructor(locale: string) {
|
|
262
|
+
this.locale = locale;
|
|
263
|
+
this.numberFormatter = new NumberFormatter(locale);
|
|
264
|
+
this.dateFormatter = new DateFormatter(locale);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Number formatting
|
|
268
|
+
formatNumber(value: number, options?: NumberFormatOptions): string {
|
|
269
|
+
return this.numberFormatter.format(value, options);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
formatCurrency(
|
|
273
|
+
value: number,
|
|
274
|
+
currency: string,
|
|
275
|
+
options?: Omit<NumberFormatOptions, 'style' | 'currency'>
|
|
276
|
+
): string {
|
|
277
|
+
return this.numberFormatter.formatCurrency(value, currency, options);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
formatPercent(value: number, options?: Omit<NumberFormatOptions, 'style'>): string {
|
|
281
|
+
return this.numberFormatter.formatPercent(value, options);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Date formatting
|
|
285
|
+
formatDate(date: Date | string | number, options?: DateFormatOptions): string {
|
|
286
|
+
return this.dateFormatter.format(date, options);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
formatRelativeTime(date: Date | string | number, options?: RelativeTimeFormatOptions): string {
|
|
290
|
+
return this.dateFormatter.formatRelative(date, options);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Specialized hyperscript formatting
|
|
294
|
+
formatHyperscriptValue(value: any, type?: string): string {
|
|
295
|
+
if (typeof value === 'number') {
|
|
296
|
+
if (type === 'currency') {
|
|
297
|
+
return this.formatCurrency(value, 'USD');
|
|
298
|
+
}
|
|
299
|
+
if (type === 'percent') {
|
|
300
|
+
return this.formatPercent(value);
|
|
301
|
+
}
|
|
302
|
+
return this.formatNumber(value);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (value instanceof Date || (typeof value === 'string' && !isNaN(Date.parse(value)))) {
|
|
306
|
+
if (type === 'relative') {
|
|
307
|
+
return this.formatRelativeTime(value);
|
|
308
|
+
}
|
|
309
|
+
return this.formatDate(value);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return String(value);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// List formatting
|
|
316
|
+
formatList(
|
|
317
|
+
items: string[],
|
|
318
|
+
options: {
|
|
319
|
+
style?: 'long' | 'short' | 'narrow';
|
|
320
|
+
type?: 'conjunction' | 'disjunction' | 'unit';
|
|
321
|
+
} = {}
|
|
322
|
+
): string {
|
|
323
|
+
if (items.length === 0) return '';
|
|
324
|
+
if (items.length === 1) return items[0];
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
// ListFormat may not be available in all environments
|
|
328
|
+
const ListFormatCtor = (Intl as any).ListFormat;
|
|
329
|
+
if (ListFormatCtor) {
|
|
330
|
+
const listFormat = new ListFormatCtor(this.locale, options);
|
|
331
|
+
return listFormat.format(items);
|
|
332
|
+
}
|
|
333
|
+
throw new Error('ListFormat not available');
|
|
334
|
+
} catch (_error) {
|
|
335
|
+
// Fallback for unsupported locales
|
|
336
|
+
const { type = 'conjunction' } = options;
|
|
337
|
+
const connector = type === 'disjunction' ? 'or' : 'and';
|
|
338
|
+
|
|
339
|
+
if (items.length === 2) {
|
|
340
|
+
return `${items[0]} ${connector} ${items[1]}`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return `${items.slice(0, -1).join(', ')}, ${connector} ${items[items.length - 1]}`;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Unit formatting
|
|
348
|
+
formatUnit(
|
|
349
|
+
value: number,
|
|
350
|
+
unit: string,
|
|
351
|
+
options: { style?: 'long' | 'short' | 'narrow' } = {}
|
|
352
|
+
): string {
|
|
353
|
+
try {
|
|
354
|
+
// Map common hyperscript units to Intl units
|
|
355
|
+
const unitMap: Record<string, string> = {
|
|
356
|
+
second: 'second',
|
|
357
|
+
seconds: 'second',
|
|
358
|
+
minute: 'minute',
|
|
359
|
+
minutes: 'minute',
|
|
360
|
+
hour: 'hour',
|
|
361
|
+
hours: 'hour',
|
|
362
|
+
day: 'day',
|
|
363
|
+
days: 'day',
|
|
364
|
+
pixel: 'pixel',
|
|
365
|
+
pixels: 'pixel',
|
|
366
|
+
px: 'pixel',
|
|
367
|
+
percent: 'percent',
|
|
368
|
+
'%': 'percent',
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const intlUnit = unitMap[unit.toLowerCase()] || unit;
|
|
372
|
+
|
|
373
|
+
if (Intl.NumberFormat.prototype.constructor.name === 'NumberFormat') {
|
|
374
|
+
// Check if environment supports unit formatting
|
|
375
|
+
const testFormatter = new Intl.NumberFormat(this.locale, {
|
|
376
|
+
style: 'unit',
|
|
377
|
+
unit: intlUnit,
|
|
378
|
+
unitDisplay: options.style || 'long',
|
|
379
|
+
} as any);
|
|
380
|
+
|
|
381
|
+
return testFormatter.format(value);
|
|
382
|
+
}
|
|
383
|
+
} catch (error) {
|
|
384
|
+
// Fallback formatting
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return `${value} ${unit}`;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Change locale
|
|
391
|
+
setLocale(locale: string): void {
|
|
392
|
+
this.locale = locale;
|
|
393
|
+
this.numberFormatter = new NumberFormatter(locale);
|
|
394
|
+
this.dateFormatter = new DateFormatter(locale);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
getLocale(): string {
|
|
398
|
+
return this.locale;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Global formatting utilities
|
|
404
|
+
*/
|
|
405
|
+
export const formatters = new Map<string, LocaleFormatter>();
|
|
406
|
+
|
|
407
|
+
export function getFormatter(locale: string): LocaleFormatter {
|
|
408
|
+
if (!formatters.has(locale)) {
|
|
409
|
+
formatters.set(locale, new LocaleFormatter(locale));
|
|
410
|
+
}
|
|
411
|
+
return formatters.get(locale)!;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export function formatForLocale(locale: string, value: any, type?: string): string {
|
|
415
|
+
return getFormatter(locale).formatHyperscriptValue(value, type);
|
|
416
|
+
}
|