@eouia/intl-msg 0.1.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 +615 -0
- package/dist/browser/intl-msg.js +591 -0
- package/dist/cjs/compose.cjs +65 -0
- package/dist/cjs/loaders.cjs +73 -0
- package/dist/cjs/main.cjs +588 -0
- package/dist/esm/compose.js +60 -0
- package/dist/esm/loaders.js +66 -0
- package/dist/esm/main.js +586 -0
- package/dist/esm/package.json +3 -0
- package/package.json +67 -0
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
var IntlMsg = (function () {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var _intl = null;
|
|
5
|
+
if (typeof Intl !== 'undefined') {
|
|
6
|
+
_intl = Intl;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
function sanitizeLocaleInput(locale) {
|
|
11
|
+
if (typeof locale !== 'string') return null
|
|
12
|
+
return locale.replace(/_/g, '-')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function createLocaleInfo(locale) {
|
|
16
|
+
const sanitizedLocale = sanitizeLocaleInput(locale);
|
|
17
|
+
if (!sanitizedLocale) return null
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const canonicalLocales = _intl.getCanonicalLocales(sanitizedLocale);
|
|
21
|
+
const canonicalLocale = Array.isArray(canonicalLocales) ? canonicalLocales[0] : canonicalLocales;
|
|
22
|
+
const localeInfo = {
|
|
23
|
+
original: locale,
|
|
24
|
+
sanitized: sanitizedLocale,
|
|
25
|
+
canonical: canonicalLocale,
|
|
26
|
+
baseName: canonicalLocale,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
if (typeof _intl.Locale === 'function') {
|
|
30
|
+
const intlLocale = new _intl.Locale(canonicalLocale);
|
|
31
|
+
localeInfo.baseName = intlLocale.baseName || canonicalLocale;
|
|
32
|
+
localeInfo.language = intlLocale.language;
|
|
33
|
+
localeInfo.script = intlLocale.script;
|
|
34
|
+
localeInfo.region = intlLocale.region;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return localeInfo
|
|
38
|
+
} catch (e) {
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeToBcp47 (locale, func = ()=>{}) {
|
|
44
|
+
const localeInfo = createLocaleInfo(locale);
|
|
45
|
+
if (!localeInfo) return null
|
|
46
|
+
const canonicalLocales = [localeInfo.canonical];
|
|
47
|
+
if (typeof func === 'function') func(canonicalLocales);
|
|
48
|
+
return localeInfo.canonical
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isPlainObject(value) {
|
|
52
|
+
if (Object.prototype.toString.call(value) !== '[object Object]') {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const prototype = Object.getPrototypeOf(value);
|
|
57
|
+
return prototype === null || prototype === Object.prototype;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function toArray (item) {
|
|
61
|
+
if(!item) return []
|
|
62
|
+
return [...((Array.isArray(item)) ? [...item] : [item])]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function toShortStr (something, len = 20) {
|
|
66
|
+
var c = (something?.toString() || String(something));
|
|
67
|
+
return (c.length > len) ? c.slice(0, len) + '...' : c
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function toDate (dateLike) {
|
|
71
|
+
if (dateLike instanceof Date) return new Date(dateLike.getTime())
|
|
72
|
+
var date = new Date(dateLike);
|
|
73
|
+
if (date.toString() === 'Invalid Date') return dateLike
|
|
74
|
+
return date
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isRangeObject(value) {
|
|
78
|
+
return isPlainObject(value) && Object.hasOwn(value, 'start') && Object.hasOwn(value, 'end')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getSupportedIntlValues(key) {
|
|
82
|
+
if (typeof _intl.supportedValuesOf !== 'function') return null
|
|
83
|
+
try {
|
|
84
|
+
return _intl.supportedValuesOf(key)
|
|
85
|
+
} catch (e) {
|
|
86
|
+
return null
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function normalizeIntlOptionValue(key, value) {
|
|
91
|
+
if (typeof value !== 'string') return value
|
|
92
|
+
if (key === 'currency') return value.toUpperCase()
|
|
93
|
+
if (key === 'calendar' || key === 'numberingSystem' || key === 'unit') return value.toLowerCase()
|
|
94
|
+
return value
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function normalizeRelativeTimeUnit(unit) {
|
|
98
|
+
const normalizedUnit = normalizeIntlOptionValue('unit', unit);
|
|
99
|
+
const supportedUnits = getSupportedIntlValues('unit');
|
|
100
|
+
if (!supportedUnits) return normalizedUnit
|
|
101
|
+
if (supportedUnits.includes(normalizedUnit)) return normalizedUnit
|
|
102
|
+
if (normalizedUnit.endsWith('s')) {
|
|
103
|
+
const singularUnit = normalizedUnit.slice(0, -1);
|
|
104
|
+
if (supportedUnits.includes(singularUnit)) return singularUnit
|
|
105
|
+
}
|
|
106
|
+
return normalizedUnit
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function validateIntlOptions(options = {}, log = DEFAULT_LOGGER) {
|
|
110
|
+
if (!isPlainObject(options)) return { valid: false, options }
|
|
111
|
+
|
|
112
|
+
const supportedOptionKeys = ['currency', 'unit', 'calendar', 'numberingSystem'];
|
|
113
|
+
for (const key of supportedOptionKeys) {
|
|
114
|
+
if (!Object.hasOwn(options, key)) continue
|
|
115
|
+
|
|
116
|
+
const normalizedValue = normalizeIntlOptionValue(key, options[key]);
|
|
117
|
+
const supportedValues = getSupportedIntlValues(key);
|
|
118
|
+
if (!supportedValues) {
|
|
119
|
+
options[key] = normalizedValue;
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!supportedValues.includes(normalizedValue)) {
|
|
124
|
+
log.warn(`Invalid Intl option '${key}':`, normalizedValue);
|
|
125
|
+
return { valid: false, options }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
options[key] = normalizedValue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { valid: true, options }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function setPostFormatContext(config, context) {
|
|
135
|
+
if (!isPlainObject(config)) return
|
|
136
|
+
config.__postFormatContext = context;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function applyPostFormat(postFormat, context, fallbackValue, log, formatName, formatters = {}) {
|
|
140
|
+
if (typeof postFormat !== 'string') return fallbackValue
|
|
141
|
+
const transform = formatters?.[postFormat];
|
|
142
|
+
if (typeof transform !== 'function') return fallbackValue
|
|
143
|
+
try {
|
|
144
|
+
return transform(context) ?? fallbackValue
|
|
145
|
+
} catch (e) {
|
|
146
|
+
log.error(`Formatter '${formatName}' postFormat error.`);
|
|
147
|
+
log.error({
|
|
148
|
+
formatterConfig: context,
|
|
149
|
+
postFormat,
|
|
150
|
+
});
|
|
151
|
+
return fallbackValue
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function toPossibleLocales(locales = []) {
|
|
156
|
+
if (!(Array.isArray(locales) && locales.length > 0)) return []
|
|
157
|
+
return locales.reduce((result, locale) => {
|
|
158
|
+
const localeInfo = createLocaleInfo(locale);
|
|
159
|
+
if (!localeInfo) return result
|
|
160
|
+
|
|
161
|
+
if (!result.includes(localeInfo.canonical)) result.push(localeInfo.canonical);
|
|
162
|
+
|
|
163
|
+
var lcParts = localeInfo.baseName.split('-');
|
|
164
|
+
while(lcParts.length > 0) {
|
|
165
|
+
var search = lcParts.join('-');
|
|
166
|
+
if (!result.includes(search)) result.push(search);
|
|
167
|
+
lcParts.pop();
|
|
168
|
+
}
|
|
169
|
+
return result
|
|
170
|
+
}, [])
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function applyIntl(obj) {
|
|
174
|
+
const required = [
|
|
175
|
+
'getCanonicalLocales', 'PluralRules', 'DateTimeFormat', 'RelativeTimeFormat',
|
|
176
|
+
'ListFormat', 'NumberFormat', 'Locale'
|
|
177
|
+
];
|
|
178
|
+
if (
|
|
179
|
+
obj !== null && typeof obj === 'object'
|
|
180
|
+
&& required.every((p) => {
|
|
181
|
+
return obj.hasOwnProperty(p)
|
|
182
|
+
})
|
|
183
|
+
) _intl = obj;
|
|
184
|
+
if (!_intl || !_intl.hasOwnProperty(required[0])) throw new Error(
|
|
185
|
+
"This module requires native 'Intl' feature or representative polyfill injection."
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const DEFAULT_LOGGER = {
|
|
190
|
+
log: () => {},
|
|
191
|
+
info: () => {},
|
|
192
|
+
warn: () => {},
|
|
193
|
+
error: () => {}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
class Dictionary {
|
|
197
|
+
#terms = new Map()
|
|
198
|
+
#formatters = new Map()
|
|
199
|
+
#name = ''
|
|
200
|
+
constructor (locale) {
|
|
201
|
+
normalizeToBcp47(locale, (lc) => {
|
|
202
|
+
this.#name = (Array.isArray(lc)) ? lc[0] || locale : locale;
|
|
203
|
+
});
|
|
204
|
+
if (!this.#name) throw new Error(`Invalid locale name '${locale}' as dictionary`)
|
|
205
|
+
this.setTerm('TEST', 'This is a test phrase by default.');
|
|
206
|
+
}
|
|
207
|
+
setTerm (key, message) {
|
|
208
|
+
return this.#terms.set(key, message)
|
|
209
|
+
}
|
|
210
|
+
getTerm (key) {
|
|
211
|
+
return this.#terms.get(key)
|
|
212
|
+
}
|
|
213
|
+
getName () {
|
|
214
|
+
return this.#name
|
|
215
|
+
}
|
|
216
|
+
setFormatter (key, formatter) {
|
|
217
|
+
return this.#formatters.set(key, formatter)
|
|
218
|
+
}
|
|
219
|
+
getFormatter (key) {
|
|
220
|
+
return this.#formatters.get(key)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
class IntlMsg {
|
|
225
|
+
#dictionaries = new Map()
|
|
226
|
+
#locales = []
|
|
227
|
+
#formatters = {}
|
|
228
|
+
#log = DEFAULT_LOGGER
|
|
229
|
+
|
|
230
|
+
constructor ({ log = null, intlPolyfill = null, verbose = false } = {}) {
|
|
231
|
+
applyIntl(intlPolyfill);
|
|
232
|
+
if (!_intl.hasOwnProperty('getCanonicalLocales')) throw new Error("This module required native 'Intl' module or ")
|
|
233
|
+
this.#initFormatters();
|
|
234
|
+
this.setLogger(log, verbose);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
static factory ({ log = null, intlPolyfill = null, verbose = false, locales = null, dictionaries = null } = {}) {
|
|
238
|
+
const instance = new IntlMsg({ log, intlPolyfill, verbose });
|
|
239
|
+
if (locales) instance.addLocale(locales);
|
|
240
|
+
if (dictionaries) instance.addDictionary(dictionaries);
|
|
241
|
+
return instance
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
setLogger (log = null, verbose = false) {
|
|
245
|
+
if (!log) return
|
|
246
|
+
const required = ['log', 'info', 'warn', 'error'];
|
|
247
|
+
if (required.every((m) => { return log.hasOwnProperty(m)}) && verbose) this.#log = log;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
getLocale () {
|
|
251
|
+
return [...this.#locales]
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
setLocale (locales = []) {
|
|
256
|
+
this.#locales = [];
|
|
257
|
+
this.addLocale(locales);
|
|
258
|
+
return this
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
addLocale (locales = []) {
|
|
263
|
+
var newLocales = toArray(locales);
|
|
264
|
+
if (newLocales.length < 1) return this
|
|
265
|
+
newLocales.forEach((lc) => {
|
|
266
|
+
normalizeToBcp47(lc, (filtered) => {
|
|
267
|
+
if (!(Array.isArray(filtered) && filtered.length >= 1)) return
|
|
268
|
+
filtered.forEach((f) => {
|
|
269
|
+
if (this.#locales.includes(f)) return
|
|
270
|
+
this.#locales.push(f);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
return this
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
addDictionary(json = {}) {
|
|
278
|
+
if (!isPlainObject(json)) {
|
|
279
|
+
this.#log.warn('Invalid dictionary data:', toShortStr(json));
|
|
280
|
+
return this
|
|
281
|
+
}
|
|
282
|
+
Object.keys(json).forEach((locale) => {
|
|
283
|
+
var dict = json[locale];
|
|
284
|
+
this.#mergeDictionary(locale, dict);
|
|
285
|
+
});
|
|
286
|
+
return this
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
#mergeDictionary(locale, dictData = {}) {
|
|
290
|
+
if (!isPlainObject(dictData)) {
|
|
291
|
+
this.#log.warn(`Invalid dictionary entry for locale '${locale}':`, toShortStr(dictData));
|
|
292
|
+
return this
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
var {translations = {}, formatters = {}} = dictData;
|
|
296
|
+
var lc = normalizeToBcp47(locale);
|
|
297
|
+
if (!lc) return this
|
|
298
|
+
var dictionary = this.getDictionary(lc);
|
|
299
|
+
if (!(dictionary instanceof Dictionary)) {
|
|
300
|
+
dictionary = new Dictionary(lc);
|
|
301
|
+
this.#dictionaries.set(lc, dictionary);
|
|
302
|
+
}
|
|
303
|
+
if (translations && typeof translations === 'object') {
|
|
304
|
+
for(let [key, value] of Object.entries(translations)) {
|
|
305
|
+
if (typeof key === 'string')
|
|
306
|
+
dictionary.setTerm(key, value);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (formatters && typeof formatters === 'object') {
|
|
310
|
+
for(let [key, value] of Object.entries(formatters)) {
|
|
311
|
+
if (typeof key === 'string')
|
|
312
|
+
dictionary.setFormatter(key, value);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return this
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
getDictionary(locale) {
|
|
319
|
+
var lc = normalizeToBcp47(locale);
|
|
320
|
+
if (this.#dictionaries.has(lc)) return this.#dictionaries.get(lc)
|
|
321
|
+
return null
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
addTermToDictionary(locale, key, value) {
|
|
325
|
+
var dict = this.getDictionary(locale);
|
|
326
|
+
if (dict instanceof Dictionary) dict.setTerm(key, value);
|
|
327
|
+
return this
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
getTermFromDictionary(locale, key) {
|
|
331
|
+
var dict = this.getDictionary(locale);
|
|
332
|
+
if (!(dict instanceof Dictionary)) return undefined
|
|
333
|
+
return dict.getTerm(key)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
getDictionaryNames () {
|
|
337
|
+
return [...this.#dictionaries.keys()]
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
#findTerms (key, locales = null) {
|
|
341
|
+
var rootLocales = locales ? toArray(locales) : [...this.#locales];
|
|
342
|
+
|
|
343
|
+
const dictionaryList = [...this.#dictionaries.keys()];
|
|
344
|
+
var found = null;
|
|
345
|
+
var originalLocale = null;
|
|
346
|
+
for (let rootLc of rootLocales) {
|
|
347
|
+
if (found) break
|
|
348
|
+
for (let lc of toPossibleLocales([rootLc])) {
|
|
349
|
+
if (dictionaryList.includes(lc) && this.#dictionaries.get(lc).getTerm(key) !== undefined) {
|
|
350
|
+
found = lc;
|
|
351
|
+
originalLocale = rootLc;
|
|
352
|
+
break
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return {
|
|
357
|
+
message: found ? this.#dictionaries.get(found).getTerm(key) : key,
|
|
358
|
+
dictionaryName: found,
|
|
359
|
+
originalLocale: originalLocale,
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
getRawMessage(key, locales = null) {
|
|
364
|
+
var { message } = this.#findTerms(key, locales);
|
|
365
|
+
return message
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
message (key, options = {}) {
|
|
369
|
+
var { message, dictionaryName, originalLocale } = this.#findTerms(key);
|
|
370
|
+
|
|
371
|
+
for (const prop of Object.keys(options)) {
|
|
372
|
+
var pattern = `{{((?<t>${prop})((?:\\:)(?<f>\\w+))?)}}`;
|
|
373
|
+
var rx = new RegExp(pattern, 'gm');
|
|
374
|
+
[...message.matchAll(rx)].map((i) => {
|
|
375
|
+
var val = options[prop];
|
|
376
|
+
var placeholder = i[0];
|
|
377
|
+
var groups = i?.groups;
|
|
378
|
+
if (groups.f) {
|
|
379
|
+
var formatterDefinition = this.#dictionaries.get(dictionaryName)?.getFormatter(groups.f);
|
|
380
|
+
var formatterConfig = isPlainObject(formatterDefinition) ? { ...formatterDefinition } : formatterDefinition;
|
|
381
|
+
var format = formatterConfig?.format;
|
|
382
|
+
if (typeof this.#formatters[format] === 'function') {
|
|
383
|
+
try {
|
|
384
|
+
var rawValue = val;
|
|
385
|
+
formatterConfig.value = val;
|
|
386
|
+
if (formatterConfig.locales == null) formatterConfig.locales = originalLocale;
|
|
387
|
+
val = this.#formatters[format](formatterConfig) ?? {};
|
|
388
|
+
val = applyPostFormat(
|
|
389
|
+
formatterConfig?.postFormat,
|
|
390
|
+
formatterConfig?.__postFormatContext ?? {
|
|
391
|
+
value: val,
|
|
392
|
+
parts: undefined,
|
|
393
|
+
rawValue,
|
|
394
|
+
locales: formatterConfig.locales,
|
|
395
|
+
options: formatterConfig.options,
|
|
396
|
+
format,
|
|
397
|
+
},
|
|
398
|
+
val,
|
|
399
|
+
this.#log,
|
|
400
|
+
format,
|
|
401
|
+
this.#formatters
|
|
402
|
+
);
|
|
403
|
+
} catch (e) {
|
|
404
|
+
this.#log.error (`Formatter '${format}' call error.`);
|
|
405
|
+
this.#log.error({
|
|
406
|
+
key: key,
|
|
407
|
+
formatterConfig: formatterConfig,
|
|
408
|
+
});
|
|
409
|
+
// val은 원본 값으로 유지 → 아래 message.replace(ph, val)에서 원본 값으로 치환
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
message = message.replace(placeholder, val);
|
|
414
|
+
return i?.groups
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
return message
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
#initFormatters () {
|
|
423
|
+
this.registerFormatter('pluralRules', function({locales, value, options, rules}) {
|
|
424
|
+
if (isNaN(value)) return ''
|
|
425
|
+
var plural = new _intl.PluralRules(locales, options).select(value);
|
|
426
|
+
return rules?.[plural] ?? rules?.other ?? ''
|
|
427
|
+
});
|
|
428
|
+
this.registerFormatter('pluralRange', ({locales, value, options, rules}) => {
|
|
429
|
+
if (!isRangeObject(value)) return rules?.other ?? `${value?.toString() || value}`
|
|
430
|
+
if (isNaN(value.start) || isNaN(value.end)) return rules?.other ?? `${value.start} - ${value.end}`
|
|
431
|
+
|
|
432
|
+
var pluralRules = new _intl.PluralRules(locales, options);
|
|
433
|
+
if (typeof pluralRules.selectRange !== 'function') {
|
|
434
|
+
this.#log.warn("Formatter 'pluralRange' requires Intl.PluralRules.prototype.selectRange support.");
|
|
435
|
+
return rules?.other ?? `${value.start}-${value.end}`
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
var plural = pluralRules.selectRange(value.start, value.end);
|
|
439
|
+
return rules?.[plural] ?? rules?.other ?? ''
|
|
440
|
+
});
|
|
441
|
+
this.registerFormatter('list', (formatterConfig = {}) => {
|
|
442
|
+
var {locales, value, options = {}} = formatterConfig;
|
|
443
|
+
if (!Array.isArray(value)) return value?.toString() || value
|
|
444
|
+
var formatter = new _intl.ListFormat(locales, options);
|
|
445
|
+
var ret = formatter.format(value);
|
|
446
|
+
var parts = typeof formatter.formatToParts === 'function' ? formatter.formatToParts(value) : undefined;
|
|
447
|
+
setPostFormatContext(formatterConfig, { value: ret, parts, rawValue: value, locales, options, format: 'list' });
|
|
448
|
+
return ret
|
|
449
|
+
});
|
|
450
|
+
this.registerFormatter('number', (formatterConfig = {}) => {
|
|
451
|
+
var {locales, value, options = {}} = formatterConfig;
|
|
452
|
+
if (isNaN(value)) return value?.toString() || value
|
|
453
|
+
var { valid, options: validatedOptions } = validateIntlOptions(options, this.#log);
|
|
454
|
+
if (!valid) return value?.toString() || value
|
|
455
|
+
var formatter = new _intl.NumberFormat(locales, validatedOptions);
|
|
456
|
+
var ret = formatter.format(value);
|
|
457
|
+
var parts = typeof formatter.formatToParts === 'function' ? formatter.formatToParts(value) : undefined;
|
|
458
|
+
setPostFormatContext(formatterConfig, { value: ret, parts, rawValue: value, locales, options: validatedOptions, format: 'number' });
|
|
459
|
+
return ret
|
|
460
|
+
});
|
|
461
|
+
this.registerFormatter('numberRange', (formatterConfig = {}) => {
|
|
462
|
+
var {locales, value, options = {}} = formatterConfig;
|
|
463
|
+
if (!isRangeObject(value)) return value?.toString() || value
|
|
464
|
+
if (isNaN(value.start) || isNaN(value.end)) return `${value.start} - ${value.end}`
|
|
465
|
+
|
|
466
|
+
var { valid, options: validatedOptions } = validateIntlOptions(options, this.#log);
|
|
467
|
+
if (!valid) return `${value.start} - ${value.end}`
|
|
468
|
+
|
|
469
|
+
var formatter = new _intl.NumberFormat(locales, validatedOptions);
|
|
470
|
+
if (typeof formatter.formatRange !== 'function') {
|
|
471
|
+
this.#log.warn("Formatter 'numberRange' requires Intl.NumberFormat.prototype.formatRange support.");
|
|
472
|
+
return `${formatter.format(value.start)} - ${formatter.format(value.end)}`
|
|
473
|
+
}
|
|
474
|
+
var ret = formatter.formatRange(value.start, value.end);
|
|
475
|
+
var parts = typeof formatter.formatRangeToParts === 'function' ? formatter.formatRangeToParts(value.start, value.end) : undefined;
|
|
476
|
+
setPostFormatContext(formatterConfig, { value: ret, parts, rawValue: value, locales, options: validatedOptions, format: 'numberRange' });
|
|
477
|
+
return ret
|
|
478
|
+
});
|
|
479
|
+
this.registerFormatter('select', function({locales, value, options}) {
|
|
480
|
+
if (options?.[value]) return options?.[value]
|
|
481
|
+
return options?.["other"] ?? value?.toString() ?? value
|
|
482
|
+
});
|
|
483
|
+
this.registerFormatter('dateTime', (formatterConfig = {}) => {
|
|
484
|
+
var {locales, value, options = {}} = formatterConfig;
|
|
485
|
+
var date = toDate(value);
|
|
486
|
+
if (!(date instanceof Date)) return value
|
|
487
|
+
var { valid, options: validatedOptions } = validateIntlOptions(options, this.#log);
|
|
488
|
+
if (!valid) return value
|
|
489
|
+
var formatter = new _intl.DateTimeFormat(locales, validatedOptions);
|
|
490
|
+
var ret = formatter.format(date);
|
|
491
|
+
var parts = typeof formatter.formatToParts === 'function' ? formatter.formatToParts(date) : undefined;
|
|
492
|
+
setPostFormatContext(formatterConfig, { value: ret, parts, rawValue: value, locales, options: validatedOptions, format: 'dateTime' });
|
|
493
|
+
return ret
|
|
494
|
+
});
|
|
495
|
+
this.registerFormatter('dateTimeRange', (formatterConfig = {}) => {
|
|
496
|
+
var {locales, value, options = {}} = formatterConfig;
|
|
497
|
+
if (!isRangeObject(value)) return value?.toString() || value
|
|
498
|
+
|
|
499
|
+
var start = toDate(value.start);
|
|
500
|
+
var end = toDate(value.end);
|
|
501
|
+
var { valid, options: validatedOptions } = validateIntlOptions(options, this.#log);
|
|
502
|
+
|
|
503
|
+
if (!valid) return `${value.start} - ${value.end}`
|
|
504
|
+
|
|
505
|
+
var formatter = new _intl.DateTimeFormat(locales, validatedOptions);
|
|
506
|
+
|
|
507
|
+
if (!(start instanceof Date) || !(end instanceof Date)) return `${value.start} - ${value.end}`
|
|
508
|
+
if (typeof formatter.formatRange !== 'function') {
|
|
509
|
+
this.#log.warn("Formatter 'dateTimeRange' requires Intl.DateTimeFormat.prototype.formatRange support.");
|
|
510
|
+
return `${formatter.format(start)} - ${formatter.format(end)}`
|
|
511
|
+
}
|
|
512
|
+
var ret = formatter.formatRange(start, end);
|
|
513
|
+
var parts = typeof formatter.formatRangeToParts === 'function' ? formatter.formatRangeToParts(start, end) : undefined;
|
|
514
|
+
setPostFormatContext(formatterConfig, { value: ret, parts, rawValue: value, locales, options: validatedOptions, format: 'dateTimeRange' });
|
|
515
|
+
return ret
|
|
516
|
+
});
|
|
517
|
+
this.registerFormatter('relativeTime', (formatterConfig = {}) => {
|
|
518
|
+
var {locales, value, options = {}, unit='seconds'} = formatterConfig;
|
|
519
|
+
if (isNaN(value)) return value?.toString() || value
|
|
520
|
+
var normalizedUnit = normalizeRelativeTimeUnit(unit);
|
|
521
|
+
var { valid, options: validatedOptions } = validateIntlOptions(options, this.#log);
|
|
522
|
+
var supportedUnits = getSupportedIntlValues('unit');
|
|
523
|
+
|
|
524
|
+
if (!valid) return value?.toString() || value
|
|
525
|
+
if (supportedUnits && !supportedUnits.includes(normalizedUnit)) {
|
|
526
|
+
this.#log.warn("Invalid Intl option 'unit':", normalizedUnit);
|
|
527
|
+
return value?.toString() || value
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
var formatter = new _intl.RelativeTimeFormat(locales, validatedOptions);
|
|
531
|
+
var ret = formatter.format(value, normalizedUnit);
|
|
532
|
+
var parts = typeof formatter.formatToParts === 'function' ? formatter.formatToParts(value, normalizedUnit) : undefined;
|
|
533
|
+
setPostFormatContext(formatterConfig, { value: ret, parts, rawValue: value, locales, options: validatedOptions, format: 'relativeTime', unit: normalizedUnit });
|
|
534
|
+
return ret
|
|
535
|
+
});
|
|
536
|
+
this.registerFormatter('duration', ({locales, value, options = {}}) => {
|
|
537
|
+
if (typeof _intl.DurationFormat !== 'function') {
|
|
538
|
+
this.#log.warn("Formatter 'duration' requires Intl.DurationFormat support.");
|
|
539
|
+
return isPlainObject(value) ? JSON.stringify(value) : value?.toString() || value
|
|
540
|
+
}
|
|
541
|
+
if (!isPlainObject(value)) return value?.toString() || value
|
|
542
|
+
var { valid, options: validatedOptions } = validateIntlOptions(options, this.#log);
|
|
543
|
+
if (!valid) return isPlainObject(value) ? JSON.stringify(value) : value?.toString() || value
|
|
544
|
+
return new _intl.DurationFormat(locales, validatedOptions).format(value)
|
|
545
|
+
});
|
|
546
|
+
this.registerFormatter('humanizedRelativeTime', ({locales, value, options = {}}) => {
|
|
547
|
+
var date = toDate(value);
|
|
548
|
+
if (!(date instanceof Date)) return value
|
|
549
|
+
var unit = 'seconds';
|
|
550
|
+
var now = Date.now();
|
|
551
|
+
var diff = Math.round((date - now) / 1000);
|
|
552
|
+
var aDiff = Math.abs(diff);
|
|
553
|
+
var gap = diff;
|
|
554
|
+
const rules = [
|
|
555
|
+
['minutes', 60] ,
|
|
556
|
+
['hours', 60 * 60],
|
|
557
|
+
['days', 60 * 60 * 24],
|
|
558
|
+
['weeks', 60 * 60 * 24 * 7],
|
|
559
|
+
['months', 60 * 60 * 24 * 30],
|
|
560
|
+
['quarters', 60 * 60 * 24 * 90],
|
|
561
|
+
['years', 60 * 60 * 24 * 365]
|
|
562
|
+
];
|
|
563
|
+
for (let [u, f] of rules) {
|
|
564
|
+
if (Math.floor(aDiff / f) < 1) continue
|
|
565
|
+
unit = u;
|
|
566
|
+
gap = Math.floor(diff / f);
|
|
567
|
+
}
|
|
568
|
+
var normalizedUnit = normalizeRelativeTimeUnit(unit);
|
|
569
|
+
var { valid, options: validatedOptions } = validateIntlOptions(options, this.#log);
|
|
570
|
+
var supportedUnits = getSupportedIntlValues('unit');
|
|
571
|
+
|
|
572
|
+
if (!valid) return value
|
|
573
|
+
if (supportedUnits && !supportedUnits.includes(normalizedUnit)) {
|
|
574
|
+
this.#log.warn("Invalid Intl option 'unit':", normalizedUnit);
|
|
575
|
+
return value
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return new _intl.RelativeTimeFormat(locales, validatedOptions).format(gap, normalizedUnit)
|
|
579
|
+
});
|
|
580
|
+
return this
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
registerFormatter (format, func) {
|
|
584
|
+
if (typeof func === 'function') { this.#formatters[format] = func; }
|
|
585
|
+
return this
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
return IntlMsg;
|
|
590
|
+
|
|
591
|
+
})();
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
function isPlainObject(value) {
|
|
6
|
+
if (Object.prototype.toString.call(value) !== '[object Object]') {
|
|
7
|
+
return false
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const prototype = Object.getPrototypeOf(value);
|
|
11
|
+
return prototype === null || prototype === Object.prototype
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function mergeDictionaryEntry(target = {}, source = {}) {
|
|
15
|
+
const mergedTranslations = {
|
|
16
|
+
...(isPlainObject(target.translations) ? target.translations : {}),
|
|
17
|
+
...(isPlainObject(source.translations) ? source.translations : {}),
|
|
18
|
+
};
|
|
19
|
+
const mergedFormatters = {
|
|
20
|
+
...(isPlainObject(target.formatters) ? target.formatters : {}),
|
|
21
|
+
...(isPlainObject(source.formatters) ? source.formatters : {}),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const result = {};
|
|
25
|
+
if (Object.keys(mergedTranslations).length > 0) result.translations = mergedTranslations;
|
|
26
|
+
if (Object.keys(mergedFormatters).length > 0) result.formatters = mergedFormatters;
|
|
27
|
+
return result
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function mergeDictionaries(target = {}, source = {}) {
|
|
31
|
+
if (!isPlainObject(source)) return target
|
|
32
|
+
|
|
33
|
+
const result = { ...target };
|
|
34
|
+
for (const locale of Object.keys(source)) {
|
|
35
|
+
const existingEntry = isPlainObject(result[locale]) ? result[locale] : {};
|
|
36
|
+
const nextEntry = isPlainObject(source[locale]) ? source[locale] : null;
|
|
37
|
+
if (!nextEntry) continue
|
|
38
|
+
result[locale] = mergeDictionaryEntry(existingEntry, nextEntry);
|
|
39
|
+
}
|
|
40
|
+
return result
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function composeDictionaries(plan = [], loader, options = {}) {
|
|
44
|
+
if (!Array.isArray(plan)) throw new TypeError('plan must be an array')
|
|
45
|
+
if (typeof loader !== 'function') throw new TypeError('loader must be a function')
|
|
46
|
+
|
|
47
|
+
const onError = options.onError ?? 'throw';
|
|
48
|
+
let merged = {};
|
|
49
|
+
|
|
50
|
+
for (const entry of plan) {
|
|
51
|
+
try {
|
|
52
|
+
const loaded = await loader(entry);
|
|
53
|
+
if (!loaded) continue
|
|
54
|
+
merged = mergeDictionaries(merged, loaded);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (onError === 'skip') continue
|
|
57
|
+
throw error
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return merged
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
exports.composeDictionaries = composeDictionaries;
|
|
65
|
+
exports.default = composeDictionaries;
|