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