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