@messagevisor/sdk 0.0.1 → 0.2.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,1615 @@
1
+ import type {
2
+ Context,
3
+ DatafileContent,
4
+ DatafileMessage,
5
+ FormatDateTimePresetOptions,
6
+ FormatNumberPresetOptions,
7
+ FormatPresets,
8
+ FormatRelativeTimePresetOptions,
9
+ LocaleDirection,
10
+ LocaleKey,
11
+ MessageKey,
12
+ MessageMeta,
13
+ } from "@messagevisor/types";
14
+
15
+ import { evaluateCondition, evaluateGroupSegment } from "./conditions";
16
+
17
+ export interface MessagevisorOptions {
18
+ datafile?: DatafileContent | string;
19
+ defaultTranslations?: Record<LocaleKey, Record<MessageKey, string>>;
20
+ defaultFormats?: Record<LocaleKey, FormatPresets>;
21
+ currency?: string;
22
+ timeZone?: string;
23
+ context?: Context;
24
+ locale?: LocaleKey;
25
+ resolveFlag?: (featureKey: string, context?: Context) => boolean;
26
+ resolveVariation?: (experimentKey: string, context?: Context) => string | null;
27
+ onDiagnostic?: MessagevisorDiagnosticHandler;
28
+ logLevel?: MessagevisorLogLevel;
29
+ cache?: MessagevisorCache;
30
+ modules?: MessagevisorModule[];
31
+ }
32
+
33
+ export interface EvaluationOptions {
34
+ locale?: LocaleKey;
35
+ currency?: string;
36
+ timeZone?: string;
37
+ formats?: FormatPresets;
38
+ moduleOptions?: Record<string, unknown>;
39
+ }
40
+
41
+ export interface TranslateOptions extends EvaluationOptions {
42
+ context?: Context;
43
+ defaultTranslation?: string;
44
+ }
45
+
46
+ export type MessagePrimitiveValue =
47
+ | string
48
+ | number
49
+ | boolean
50
+ | Date
51
+ | null
52
+ | undefined
53
+ | unknown[]
54
+ | Record<string, unknown>;
55
+
56
+ export type MessageValue<T = never> = MessagePrimitiveValue | ((chunks: Array<string | T>) => T);
57
+
58
+ export type MessageValues<T = never> = Record<string, MessageValue<T>>;
59
+
60
+ /**
61
+ * JavaScript-enhanced return shape for modules that produce rich values.
62
+ *
63
+ * The portable Messagevisor SDK contract for other languages may return strings
64
+ * only from translate() and formatMessage(). Rich callback values, framework
65
+ * nodes, and arrays are JavaScript-specific capabilities.
66
+ */
67
+ export type MessageFormatResult<T = never> = [T] extends [never]
68
+ ? string
69
+ : string | T | Array<string | T>;
70
+
71
+ export type MessagevisorTranslationSource = "translation" | "formatMessage";
72
+
73
+ export interface MessagevisorTransformPayload {
74
+ translation: unknown;
75
+ locale: LocaleKey;
76
+ source: MessagevisorTranslationSource;
77
+ messageKey?: MessageKey;
78
+ meta?: MessageMeta;
79
+ }
80
+
81
+ export interface MessagevisorFormatPayload {
82
+ translation: unknown;
83
+ values?: MessageValues<any>;
84
+ locale: LocaleKey;
85
+ source: MessagevisorTranslationSource;
86
+ messageKey?: MessageKey;
87
+ meta?: MessageMeta;
88
+ formats: FormatPresets;
89
+ moduleOptions?: Record<string, unknown>;
90
+ }
91
+
92
+ export interface MessagevisorModuleApi {
93
+ setFlagResolver: (resolver?: (featureKey: string, context?: Context) => boolean) => void;
94
+ setVariationResolver: (
95
+ resolver?: (experimentKey: string, context?: Context) => string | null,
96
+ ) => void;
97
+ getRevision: (locale?: LocaleKey) => string;
98
+ onDiagnostic: (
99
+ handler: MessagevisorDiagnosticHandler,
100
+ options?: MessagevisorModuleDiagnosticOptions,
101
+ ) => MessagevisorUnsubscribe;
102
+ reportDiagnostic: (diagnostic: Omit<MessagevisorDiagnostic, "module">) => void;
103
+ }
104
+
105
+ export type MessagevisorModuleSetupApi = MessagevisorModuleApi;
106
+
107
+ export interface MessagevisorModule {
108
+ name?: string;
109
+ setup?: (api: MessagevisorModuleApi) => void;
110
+ format?: (payload: MessagevisorFormatPayload, api?: MessagevisorModuleApi) => unknown;
111
+ transform?: (payload: MessagevisorTransformPayload, api?: MessagevisorModuleApi) => unknown;
112
+ close?: () => void | Promise<void>;
113
+ }
114
+
115
+ export type MessagevisorDiagnosticCode =
116
+ | "sdk_initialized"
117
+ | "missing_translation"
118
+ | "missing_datafile"
119
+ | "missing_locale"
120
+ | "invalid_datafile"
121
+ | "invalid_message"
122
+ | "unsupported_formatter"
123
+ | "message_override_matched"
124
+ | "deprecated_message"
125
+ | "duplicate_module"
126
+ | (string & {});
127
+
128
+ export interface MessagevisorDiagnostic {
129
+ level: MessagevisorLogLevel;
130
+ code: MessagevisorDiagnosticCode;
131
+ message: string;
132
+ module?: string;
133
+ moduleName?: string;
134
+ locale?: LocaleKey | null;
135
+ messageKey?: MessageKey;
136
+ overrideKey?: string;
137
+ deprecationWarning?: string;
138
+ source?: MessagevisorTranslationSource;
139
+ originalError?: unknown;
140
+ }
141
+
142
+ export type MessagevisorDiagnosticHandler = (diagnostic: MessagevisorDiagnostic) => void;
143
+
144
+ export type MessagevisorLogLevel = "fatal" | "error" | "warn" | "info" | "debug";
145
+
146
+ export interface MessagevisorModuleDiagnosticOptions {
147
+ logLevel?: MessagevisorLogLevel;
148
+ }
149
+
150
+ interface MessagevisorModuleDiagnosticSubscription {
151
+ moduleKey: string;
152
+ handler: MessagevisorDiagnosticHandler;
153
+ logLevel: MessagevisorLogLevel;
154
+ }
155
+
156
+ export interface MessagevisorCache {
157
+ numberFormat: Record<string, Intl.NumberFormat>;
158
+ dateTimeFormat: Record<string, Intl.DateTimeFormat>;
159
+ relativeTimeFormat: Record<string, Intl.RelativeTimeFormat>;
160
+ pluralRules: Record<string, Intl.PluralRules>;
161
+ listFormat: Record<string, any>;
162
+ displayNames: Record<string, any>;
163
+ }
164
+
165
+ export type MessagevisorEventName =
166
+ | "change"
167
+ | "error"
168
+ | "datafile_set"
169
+ | "locale_set"
170
+ | "context_set"
171
+ | "currency_set"
172
+ | "timeZone_set";
173
+
174
+ export type MessagevisorUnsubscribe = () => void;
175
+
176
+ export interface MessagevisorSnapshot {
177
+ version: number;
178
+ locale: LocaleKey | null;
179
+ direction?: LocaleDirection;
180
+ context: Context;
181
+ currency?: string;
182
+ timeZone?: string;
183
+ datafileLocales: LocaleKey[];
184
+ datafileRevisionsByLocale: Record<LocaleKey, string>;
185
+ }
186
+
187
+ export interface MessagevisorEvent {
188
+ type: MessagevisorEventName;
189
+ version: number;
190
+ snapshot: MessagevisorSnapshot;
191
+ previousSnapshot: MessagevisorSnapshot;
192
+ locale?: LocaleKey | null;
193
+ previousLocale?: LocaleKey | null;
194
+ datafile?: DatafileContent;
195
+ context?: Context;
196
+ previousContext?: Context;
197
+ currency?: string;
198
+ previousCurrency?: string;
199
+ timeZone?: string;
200
+ previousTimeZone?: string;
201
+ diagnostic?: MessagevisorDiagnostic;
202
+ }
203
+
204
+ export type MessagevisorEventCallback = (event: MessagevisorEvent) => void;
205
+
206
+ const DEFAULT_CURRENCY = "USD";
207
+ const LOG_PREFIX = "[Messagevisor]";
208
+
209
+ class MessagevisorCloseError extends Error {
210
+ public readonly errors: unknown[];
211
+
212
+ constructor(message: string, errors: unknown[]) {
213
+ super(message);
214
+ this.name = "MessagevisorCloseError";
215
+ this.errors = errors;
216
+ }
217
+ }
218
+
219
+ function createEmptyRecord<T>() {
220
+ return {} as Record<string, T>;
221
+ }
222
+
223
+ export function createMessagevisorCache(): MessagevisorCache {
224
+ return {
225
+ numberFormat: createEmptyRecord<Intl.NumberFormat>(),
226
+ dateTimeFormat: createEmptyRecord<Intl.DateTimeFormat>(),
227
+ relativeTimeFormat: createEmptyRecord<Intl.RelativeTimeFormat>(),
228
+ pluralRules: createEmptyRecord<Intl.PluralRules>(),
229
+ listFormat: createEmptyRecord<any>(),
230
+ displayNames: createEmptyRecord<any>(),
231
+ };
232
+ }
233
+
234
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
235
+ return typeof value === "object" && value !== null && !Array.isArray(value);
236
+ }
237
+
238
+ function deepMerge<T>(parent?: T, child?: T): T | undefined {
239
+ if (typeof parent === "undefined") {
240
+ return child;
241
+ }
242
+
243
+ if (typeof child === "undefined") {
244
+ return parent;
245
+ }
246
+
247
+ if (!isPlainObject(parent) || !isPlainObject(child)) {
248
+ return child;
249
+ }
250
+
251
+ const result: Record<string, unknown> = { ...parent };
252
+
253
+ for (const key of Object.keys(child)) {
254
+ result[key] = deepMerge(result[key], child[key]);
255
+ }
256
+
257
+ return result as T;
258
+ }
259
+
260
+ function getDefaultTimeZone() {
261
+ return Intl.DateTimeFormat().resolvedOptions().timeZone;
262
+ }
263
+
264
+ function resolveCurrency(
265
+ optionCurrency: string | undefined,
266
+ presetCurrency: string | undefined,
267
+ instanceCurrency: string | undefined,
268
+ ) {
269
+ return optionCurrency || presetCurrency || instanceCurrency || DEFAULT_CURRENCY;
270
+ }
271
+
272
+ function resolveTimeZone(
273
+ optionTimeZone: string | undefined,
274
+ formatTimeZone: string | undefined,
275
+ instanceTimeZone: string | undefined,
276
+ ) {
277
+ return optionTimeZone || formatTimeZone || instanceTimeZone || getDefaultTimeZone();
278
+ }
279
+
280
+ function resolveDateTimeOptions(
281
+ formatOptions: FormatDateTimePresetOptions | undefined,
282
+ options: EvaluationOptions,
283
+ instanceTimeZone: string | undefined,
284
+ ) {
285
+ return {
286
+ ...(formatOptions || {}),
287
+ timeZone: resolveTimeZone(options.timeZone, formatOptions?.timeZone, instanceTimeZone),
288
+ } as Intl.DateTimeFormatOptions;
289
+ }
290
+
291
+ function shouldLog(currentLevel: MessagevisorLogLevel, level: MessagevisorLogLevel) {
292
+ const order: MessagevisorLogLevel[] = ["fatal", "error", "warn", "info", "debug"];
293
+
294
+ return order.indexOf(currentLevel) >= order.indexOf(level);
295
+ }
296
+
297
+ function mergeStoredDatafile(
298
+ existing: DatafileContent | undefined,
299
+ incoming: DatafileContent,
300
+ ): DatafileContent {
301
+ if (!existing) {
302
+ return incoming;
303
+ }
304
+
305
+ return {
306
+ schemaVersion: incoming.schemaVersion,
307
+ messagevisorVersion: incoming.messagevisorVersion,
308
+ revision: incoming.revision,
309
+ target: incoming.target,
310
+ locale: incoming.locale,
311
+ direction: incoming.direction ?? existing.direction,
312
+ formats: incoming.formats,
313
+ segments: {
314
+ ...(existing.segments || {}),
315
+ ...(incoming.segments || {}),
316
+ },
317
+ messages: {
318
+ ...(existing.messages || {}),
319
+ ...(incoming.messages || {}),
320
+ },
321
+ translations: {
322
+ ...(existing.translations || {}),
323
+ ...(incoming.translations || {}),
324
+ },
325
+ };
326
+ }
327
+
328
+ export class Messagevisor {
329
+ private datafiles: Record<LocaleKey, DatafileContent> = {};
330
+ private defaultTranslationsByLocale: Record<LocaleKey, Record<string, string>> = {};
331
+ private defaultFormatsByLocale: Record<LocaleKey, FormatPresets | undefined> = {};
332
+ private locale: LocaleKey | null = null;
333
+ private context: Context = {};
334
+ private currency?: string;
335
+ private timeZone?: string;
336
+ private resolveFlag?: (featureKey: string, context?: Context) => boolean;
337
+ private resolveVariation?: (experimentKey: string, context?: Context) => string | null;
338
+ private onDiagnostic?: MessagevisorDiagnosticHandler;
339
+ private logLevel: MessagevisorLogLevel = "info";
340
+ private cache: MessagevisorCache;
341
+ private modules: MessagevisorModule[] = [];
342
+ private moduleDiagnosticSubscriptions: MessagevisorModuleDiagnosticSubscription[] = [];
343
+ private moduleApis: Record<string, MessagevisorModuleApi> = {};
344
+ private moduleApiId = 0;
345
+ private closed = false;
346
+ private closePromise?: Promise<void>;
347
+ private version = 0;
348
+ private listeners: Record<MessagevisorEventName, MessagevisorEventCallback[]> = {
349
+ change: [],
350
+ error: [],
351
+ datafile_set: [],
352
+ locale_set: [],
353
+ context_set: [],
354
+ currency_set: [],
355
+ timeZone_set: [],
356
+ };
357
+
358
+ constructor(options: MessagevisorOptions = {}) {
359
+ this.currency = options.currency;
360
+ this.timeZone = options.timeZone;
361
+ this.context = options.context || {};
362
+ this.resolveFlag = options.resolveFlag;
363
+ this.resolveVariation = options.resolveVariation;
364
+ this.logLevel = options.logLevel || "info";
365
+ this.onDiagnostic = options.onDiagnostic;
366
+ this.cache = options.cache || createMessagevisorCache();
367
+ this.setFlagResolver(options.resolveFlag);
368
+ this.setVariationResolver(options.resolveVariation);
369
+
370
+ (options.modules || []).forEach((module) => {
371
+ this.addModule(module);
372
+ });
373
+
374
+ if (options.defaultTranslations) {
375
+ this.defaultTranslationsByLocale = { ...options.defaultTranslations };
376
+ }
377
+
378
+ if (options.defaultFormats) {
379
+ this.defaultFormatsByLocale = { ...options.defaultFormats };
380
+ }
381
+
382
+ if (options.datafile) {
383
+ this.setDatafile(options.datafile);
384
+ } else {
385
+ this.locale = options.locale || null;
386
+ }
387
+
388
+ this.reportDiagnostic({
389
+ level: "info",
390
+ code: "sdk_initialized",
391
+ message: "SDK initialized",
392
+ });
393
+ }
394
+
395
+ subscribe(callback: () => void): MessagevisorUnsubscribe {
396
+ if (this.closed) {
397
+ return () => {};
398
+ }
399
+
400
+ return this.on("change", callback);
401
+ }
402
+
403
+ on(
404
+ eventName: MessagevisorEventName,
405
+ callback: MessagevisorEventCallback,
406
+ ): MessagevisorUnsubscribe {
407
+ if (this.closed) {
408
+ return () => {};
409
+ }
410
+
411
+ if (this.listeners[eventName].indexOf(callback) === -1) {
412
+ this.listeners[eventName].push(callback);
413
+ }
414
+
415
+ return () => {
416
+ this.listeners[eventName] = this.listeners[eventName].filter(
417
+ (listener) => listener !== callback,
418
+ );
419
+ };
420
+ }
421
+
422
+ getSnapshot(): MessagevisorSnapshot {
423
+ const datafileRevisionsByLocale: Record<LocaleKey, string> = {};
424
+
425
+ Object.keys(this.datafiles).forEach((locale) => {
426
+ datafileRevisionsByLocale[locale] = this.datafiles[locale].revision;
427
+ });
428
+
429
+ return {
430
+ version: this.version,
431
+ locale: this.locale,
432
+ direction: this.locale ? this.datafiles[this.locale]?.direction : undefined,
433
+ context: { ...this.context },
434
+ currency: this.currency,
435
+ timeZone: this.timeZone,
436
+ datafileLocales: Object.keys(this.datafiles),
437
+ datafileRevisionsByLocale,
438
+ };
439
+ }
440
+
441
+ addModule(module: MessagevisorModule) {
442
+ if (this.closed) {
443
+ return;
444
+ }
445
+
446
+ if (
447
+ module.name &&
448
+ this.modules.some((registeredModule) => registeredModule.name === module.name)
449
+ ) {
450
+ this.reportDiagnostic({
451
+ level: "error",
452
+ code: "duplicate_module",
453
+ message: "Duplicate module name",
454
+ moduleName: module.name,
455
+ });
456
+ return;
457
+ }
458
+
459
+ this.runModuleSetup(module);
460
+ this.modules.push(module);
461
+ }
462
+
463
+ removeModule(name: string) {
464
+ if (this.closed) {
465
+ return;
466
+ }
467
+
468
+ const removedModules = this.modules.filter((module) => module.name === name);
469
+
470
+ this.modules = this.modules.filter((module) => module.name !== name);
471
+ removedModules.forEach((module) => this.clearModuleDiagnosticSubscriptions(module));
472
+ }
473
+
474
+ setFlagResolver(resolver?: (featureKey: string, context?: Context) => boolean) {
475
+ this.resolveFlag = resolver;
476
+ }
477
+
478
+ setVariationResolver(resolver?: (experimentKey: string, context?: Context) => string | null) {
479
+ this.resolveVariation = resolver;
480
+ }
481
+
482
+ setCurrency(currency: string) {
483
+ const previousSnapshot = this.getSnapshot();
484
+ const previousCurrency = this.currency;
485
+
486
+ this.currency = currency;
487
+
488
+ this.emit("currency_set", previousSnapshot, {
489
+ currency,
490
+ previousCurrency,
491
+ });
492
+ }
493
+
494
+ getCurrency() {
495
+ return this.currency;
496
+ }
497
+
498
+ setTimeZone(timeZone: string) {
499
+ const previousSnapshot = this.getSnapshot();
500
+ const previousTimeZone = this.timeZone;
501
+
502
+ this.timeZone = timeZone;
503
+
504
+ this.emit("timeZone_set", previousSnapshot, {
505
+ timeZone,
506
+ previousTimeZone,
507
+ });
508
+ }
509
+
510
+ getTimeZone() {
511
+ return this.timeZone;
512
+ }
513
+
514
+ setDatafile(datafile: DatafileContent | string, replace = false) {
515
+ const resolvedDatafile = this.resolveDatafileInput(datafile);
516
+
517
+ if (!resolvedDatafile) {
518
+ return;
519
+ }
520
+
521
+ const previousSnapshot = this.getSnapshot();
522
+ const previousLocale = this.locale;
523
+ const storedDatafile =
524
+ !replace && this.datafiles[resolvedDatafile.locale]
525
+ ? mergeStoredDatafile(this.datafiles[resolvedDatafile.locale], resolvedDatafile)
526
+ : resolvedDatafile;
527
+
528
+ this.datafiles[storedDatafile.locale] = storedDatafile;
529
+
530
+ if (!this.locale) {
531
+ this.locale = storedDatafile.locale;
532
+ }
533
+
534
+ this.emit("datafile_set", previousSnapshot, {
535
+ datafile: storedDatafile,
536
+ locale: this.locale,
537
+ previousLocale,
538
+ });
539
+ }
540
+
541
+ setContext(context: Context, replace = false) {
542
+ const previousSnapshot = this.getSnapshot();
543
+ const previousContext = this.context;
544
+
545
+ this.context = replace ? context : { ...this.context, ...context };
546
+
547
+ this.emit("context_set", previousSnapshot, {
548
+ context: { ...this.context },
549
+ previousContext: { ...previousContext },
550
+ });
551
+ }
552
+
553
+ getContext() {
554
+ return { ...this.context };
555
+ }
556
+
557
+ setLocale(locale: LocaleKey) {
558
+ if (!this.datafiles[locale]) {
559
+ throw new Error(`Datafile not found for locale: ${locale}`);
560
+ }
561
+
562
+ const previousSnapshot = this.getSnapshot();
563
+ const previousLocale = this.locale;
564
+
565
+ this.locale = locale;
566
+
567
+ this.emit("locale_set", previousSnapshot, {
568
+ locale,
569
+ previousLocale,
570
+ });
571
+ }
572
+
573
+ getLocale() {
574
+ return this.locale;
575
+ }
576
+
577
+ getDirection(locale: LocaleKey | null = this.locale) {
578
+ if (!locale) {
579
+ return undefined;
580
+ }
581
+
582
+ return this.getDatafile(locale).direction;
583
+ }
584
+
585
+ getDatafile(locale: LocaleKey | null = this.locale) {
586
+ if (!locale) {
587
+ this.reportDiagnostic({
588
+ level: "error",
589
+ code: "missing_locale",
590
+ message: "Datafile not found: no locale is set",
591
+ locale: this.locale,
592
+ });
593
+ throw new Error("Datafile not found: no locale is set");
594
+ }
595
+
596
+ const datafile = this.datafiles[locale];
597
+
598
+ if (!datafile) {
599
+ this.reportDiagnostic({
600
+ level: "error",
601
+ code: "missing_datafile",
602
+ message: "Datafile not found for locale",
603
+ locale,
604
+ });
605
+ throw new Error(`Datafile not found for locale: ${locale}`);
606
+ }
607
+
608
+ return datafile;
609
+ }
610
+
611
+ getRevision(locale?: LocaleKey) {
612
+ return this.getDatafile(locale || this.locale).revision;
613
+ }
614
+
615
+ private getCurrentLocale(options: Pick<EvaluationOptions, "locale"> = {}) {
616
+ const locale = options.locale || this.locale;
617
+
618
+ if (!locale) {
619
+ this.reportDiagnostic({
620
+ level: "error",
621
+ code: "missing_locale",
622
+ message: "Locale not set",
623
+ locale: this.locale,
624
+ });
625
+ throw new Error("Locale not set");
626
+ }
627
+
628
+ return locale;
629
+ }
630
+
631
+ private emitError(diagnostic: MessagevisorDiagnostic) {
632
+ if (this.closed) {
633
+ return;
634
+ }
635
+
636
+ const snapshot = this.getSnapshot();
637
+ const event: MessagevisorEvent = {
638
+ type: "error",
639
+ version: this.version,
640
+ snapshot,
641
+ previousSnapshot: snapshot,
642
+ diagnostic,
643
+ };
644
+
645
+ this.listeners.error.slice().forEach((callback) => callback(event));
646
+ }
647
+
648
+ private reportDiagnostic(diagnostic: MessagevisorDiagnostic, sourceModuleKey?: string) {
649
+ this.moduleDiagnosticSubscriptions.slice().forEach((subscription) => {
650
+ if (subscription.moduleKey === sourceModuleKey) {
651
+ return;
652
+ }
653
+
654
+ if (!shouldLog(subscription.logLevel, diagnostic.level)) {
655
+ return;
656
+ }
657
+
658
+ subscription.handler(diagnostic);
659
+ });
660
+
661
+ if (shouldLog(this.logLevel, diagnostic.level)) {
662
+ if (this.onDiagnostic) {
663
+ this.onDiagnostic(diagnostic);
664
+ } else {
665
+ const method =
666
+ diagnostic.level === "fatal" || diagnostic.level === "error"
667
+ ? "error"
668
+ : diagnostic.level === "warn"
669
+ ? "warn"
670
+ : diagnostic.level === "debug"
671
+ ? "debug"
672
+ : "info";
673
+ console[method](LOG_PREFIX, diagnostic.message, diagnostic);
674
+ }
675
+ }
676
+
677
+ if (diagnostic.level === "error") {
678
+ this.emitError(diagnostic);
679
+ }
680
+ }
681
+
682
+ private resolveDatafileInput(datafile: DatafileContent | string): DatafileContent | undefined {
683
+ try {
684
+ const parsedDatafile = typeof datafile === "string" ? JSON.parse(datafile) : datafile;
685
+
686
+ if (!isPlainObject(parsedDatafile) || typeof parsedDatafile.locale !== "string") {
687
+ throw new Error("Datafile must be an object with a string locale.");
688
+ }
689
+
690
+ return parsedDatafile as unknown as DatafileContent;
691
+ } catch (error) {
692
+ this.reportDiagnostic({
693
+ level: "error",
694
+ code: "invalid_datafile",
695
+ message: "could not parse datafile",
696
+ originalError: error,
697
+ });
698
+
699
+ return undefined;
700
+ }
701
+ }
702
+
703
+ getDefaultTranslations(locale: LocaleKey | null = this.locale) {
704
+ if (!locale) {
705
+ return undefined;
706
+ }
707
+
708
+ return this.defaultTranslationsByLocale[locale];
709
+ }
710
+
711
+ getDefaultFormats(locale: LocaleKey | null = this.locale) {
712
+ if (!locale) {
713
+ return undefined;
714
+ }
715
+
716
+ return this.defaultFormatsByLocale[locale];
717
+ }
718
+
719
+ private getMessageFromDatafile(
720
+ messageKey: MessageKey,
721
+ options: TranslateOptions = {},
722
+ locale = this.getCurrentLocale(options),
723
+ ): string | undefined {
724
+ const datafile = this.datafiles[locale];
725
+
726
+ if (!datafile) {
727
+ return undefined;
728
+ }
729
+
730
+ const evaluationContext = {
731
+ ...this.context,
732
+ ...(options.context || {}),
733
+ };
734
+ const message = datafile.messages[messageKey];
735
+ const overrides = message?.overrides || [];
736
+
737
+ for (let index = 0; index < overrides.length; index++) {
738
+ const override = overrides[index];
739
+ const matchesConditions = evaluateCondition(override.conditions, {
740
+ context: evaluationContext,
741
+ segments: datafile.segments,
742
+ resolveFlag: this.resolveFlag,
743
+ resolveVariation: this.resolveVariation,
744
+ });
745
+ const matchesSegments = evaluateGroupSegment(override.segments, {
746
+ context: evaluationContext,
747
+ segments: datafile.segments,
748
+ resolveFlag: this.resolveFlag,
749
+ resolveVariation: this.resolveVariation,
750
+ });
751
+ const matched = matchesConditions && matchesSegments;
752
+
753
+ if (matched) {
754
+ const diagnostic: MessagevisorDiagnostic = {
755
+ level: "debug",
756
+ code: "message_override_matched",
757
+ message: "Message override matched",
758
+ locale,
759
+ messageKey,
760
+ overrideKey: override.key,
761
+ };
762
+
763
+ this.reportDiagnostic(diagnostic);
764
+
765
+ return override.translation;
766
+ }
767
+ }
768
+
769
+ return datafile.translations[messageKey];
770
+ }
771
+
772
+ private getMessageMeta(
773
+ messageKey: MessageKey,
774
+ locale = this.getCurrentLocale(),
775
+ ): MessageMeta | undefined {
776
+ const datafile = this.datafiles[locale];
777
+
778
+ return datafile?.messages?.[messageKey]?.meta;
779
+ }
780
+
781
+ private getMessageDefinition(
782
+ messageKey: MessageKey,
783
+ locale = this.getCurrentLocale(),
784
+ ): DatafileMessage | undefined {
785
+ const datafile = this.datafiles[locale];
786
+
787
+ return datafile?.messages?.[messageKey];
788
+ }
789
+
790
+ private reportDeprecatedMessage(
791
+ messageKey: MessageKey,
792
+ message: DatafileMessage,
793
+ locale = this.getCurrentLocale(),
794
+ ) {
795
+ if (!message.deprecated) {
796
+ return;
797
+ }
798
+
799
+ this.reportDiagnostic({
800
+ level: "warn",
801
+ code: "deprecated_message",
802
+ message: "Deprecated message evaluated",
803
+ locale,
804
+ messageKey,
805
+ deprecationWarning: message.deprecationWarning,
806
+ source: "translation",
807
+ });
808
+ }
809
+
810
+ private resolveMessage(
811
+ messageKey: MessageKey | undefined,
812
+ defaultTranslation: string | undefined,
813
+ options: TranslateOptions | EvaluationOptions = {},
814
+ ) {
815
+ const locale = this.getCurrentLocale(options);
816
+ const isMissing = (value: string | undefined) => typeof value === "undefined";
817
+ let translated: string | undefined;
818
+
819
+ if (messageKey) {
820
+ translated = this.getMessageFromDatafile(messageKey, options as TranslateOptions, locale);
821
+
822
+ if (isMissing(translated)) {
823
+ const translations = this.getDefaultTranslations(locale);
824
+ translated = translations ? translations[messageKey] : undefined;
825
+ }
826
+ }
827
+
828
+ if (!messageKey && typeof defaultTranslation === "string") {
829
+ return {
830
+ locale,
831
+ source: defaultTranslation,
832
+ formatted: defaultTranslation,
833
+ messageKey: undefined,
834
+ };
835
+ }
836
+
837
+ if (!isMissing(translated)) {
838
+ const message = messageKey ? this.getMessageDefinition(messageKey, locale) : undefined;
839
+
840
+ if (messageKey && message) {
841
+ this.reportDeprecatedMessage(messageKey, message, locale);
842
+ }
843
+
844
+ return {
845
+ locale,
846
+ source: translated as string,
847
+ formatted: translated as string,
848
+ messageKey,
849
+ };
850
+ }
851
+
852
+ if (messageKey) {
853
+ this.reportDiagnostic({
854
+ level: "error",
855
+ code: "missing_translation",
856
+ message: "Missing translation",
857
+ locale,
858
+ messageKey,
859
+ source: "translation",
860
+ });
861
+ }
862
+
863
+ if (typeof defaultTranslation === "string") {
864
+ return {
865
+ locale,
866
+ source: defaultTranslation,
867
+ formatted: defaultTranslation,
868
+ messageKey,
869
+ };
870
+ }
871
+
872
+ return {
873
+ locale,
874
+ source: messageKey || "",
875
+ formatted: messageKey || "",
876
+ messageKey,
877
+ };
878
+ }
879
+
880
+ private emit(
881
+ type: Exclude<MessagevisorEventName, "change" | "error">,
882
+ previousSnapshot: MessagevisorSnapshot,
883
+ details: Partial<
884
+ Omit<MessagevisorEvent, "type" | "version" | "snapshot" | "previousSnapshot">
885
+ > = {},
886
+ ) {
887
+ if (this.closed) {
888
+ return;
889
+ }
890
+
891
+ this.version += 1;
892
+
893
+ const event: MessagevisorEvent = {
894
+ ...details,
895
+ type,
896
+ version: this.version,
897
+ snapshot: this.getSnapshot(),
898
+ previousSnapshot,
899
+ };
900
+
901
+ this.listeners[type].slice().forEach((callback) => callback(event));
902
+
903
+ const changeEvent: MessagevisorEvent = {
904
+ ...event,
905
+ type: "change",
906
+ };
907
+
908
+ this.listeners.change.slice().forEach((callback) => callback(changeEvent));
909
+ }
910
+
911
+ private getEvaluationFormats(
912
+ options: EvaluationOptions = {},
913
+ locale = this.getCurrentLocale(options),
914
+ ) {
915
+ const formats =
916
+ deepMerge(
917
+ deepMerge(
918
+ this.getDefaultFormats(locale),
919
+ this.datafiles[locale] ? this.datafiles[locale].formats : undefined,
920
+ ),
921
+ options.formats,
922
+ ) || {};
923
+ const numberFormats: NonNullable<FormatPresets["number"]> = {};
924
+ const dateFormats: NonNullable<FormatPresets["date"]> = {};
925
+ const timeFormats: NonNullable<FormatPresets["time"]> = {};
926
+ const dateTimeRangeFormats: NonNullable<FormatPresets["dateTimeRange"]> = {};
927
+
928
+ Object.keys(formats.number || {}).forEach((key) => {
929
+ const formatOptions = formats.number?.[key];
930
+
931
+ if (!formatOptions) {
932
+ return;
933
+ }
934
+
935
+ if (formatOptions.style !== "currency") {
936
+ numberFormats[key] = formatOptions;
937
+ return;
938
+ }
939
+
940
+ numberFormats[key] = {
941
+ ...formatOptions,
942
+ currency: resolveCurrency(options.currency, formatOptions.currency, this.currency),
943
+ };
944
+ });
945
+
946
+ Object.keys(formats.date || {}).forEach((key) => {
947
+ const formatOptions = formats.date?.[key];
948
+
949
+ if (!formatOptions) {
950
+ return;
951
+ }
952
+
953
+ dateFormats[key] = {
954
+ ...formatOptions,
955
+ timeZone: resolveTimeZone(options.timeZone, formatOptions.timeZone, this.timeZone),
956
+ };
957
+ });
958
+
959
+ Object.keys(formats.time || {}).forEach((key) => {
960
+ const formatOptions = formats.time?.[key];
961
+
962
+ if (!formatOptions) {
963
+ return;
964
+ }
965
+
966
+ timeFormats[key] = {
967
+ ...formatOptions,
968
+ timeZone: resolveTimeZone(options.timeZone, formatOptions.timeZone, this.timeZone),
969
+ };
970
+ });
971
+
972
+ Object.keys(formats.dateTimeRange || {}).forEach((key) => {
973
+ const formatOptions = formats.dateTimeRange?.[key];
974
+
975
+ if (!formatOptions) {
976
+ return;
977
+ }
978
+
979
+ dateTimeRangeFormats[key] = {
980
+ ...formatOptions,
981
+ timeZone: resolveTimeZone(options.timeZone, formatOptions.timeZone, this.timeZone),
982
+ };
983
+ });
984
+
985
+ return {
986
+ ...formats,
987
+ number: numberFormats,
988
+ date: dateFormats,
989
+ time: timeFormats,
990
+ dateTimeRange: dateTimeRangeFormats,
991
+ };
992
+ }
993
+
994
+ private getCachedNumberFormat(locale: LocaleKey, options: Intl.NumberFormatOptions) {
995
+ var cacheKey = JSON.stringify({
996
+ locale,
997
+ options,
998
+ });
999
+
1000
+ if (!this.cache.numberFormat[cacheKey]) {
1001
+ this.cache.numberFormat[cacheKey] = new Intl.NumberFormat(locale, options);
1002
+ }
1003
+
1004
+ return this.cache.numberFormat[cacheKey];
1005
+ }
1006
+
1007
+ private getCachedDateTimeFormat(locale: LocaleKey, options: Intl.DateTimeFormatOptions) {
1008
+ var cacheKey = JSON.stringify({
1009
+ locale,
1010
+ options,
1011
+ });
1012
+
1013
+ if (!this.cache.dateTimeFormat[cacheKey]) {
1014
+ this.cache.dateTimeFormat[cacheKey] = new Intl.DateTimeFormat(locale, options);
1015
+ }
1016
+
1017
+ return this.cache.dateTimeFormat[cacheKey];
1018
+ }
1019
+
1020
+ private getCachedRelativeTimeFormat(
1021
+ locale: LocaleKey,
1022
+ options: Intl.RelativeTimeFormatOptions = {},
1023
+ ) {
1024
+ var cacheKey = JSON.stringify({
1025
+ locale,
1026
+ options,
1027
+ });
1028
+
1029
+ if (!this.cache.relativeTimeFormat[cacheKey]) {
1030
+ this.cache.relativeTimeFormat[cacheKey] = new Intl.RelativeTimeFormat(locale, options);
1031
+ }
1032
+
1033
+ return this.cache.relativeTimeFormat[cacheKey];
1034
+ }
1035
+
1036
+ private createModuleApi(module: MessagevisorModule): MessagevisorModuleApi {
1037
+ const moduleKey = this.getModuleApiKey(module);
1038
+ const setFlagResolver = (resolver?: (featureKey: string, context?: Context) => boolean) => {
1039
+ this.setFlagResolver(resolver);
1040
+ };
1041
+ const setVariationResolver = (
1042
+ resolver?: (experimentKey: string, context?: Context) => string | null,
1043
+ ) => {
1044
+ this.setVariationResolver(resolver);
1045
+ };
1046
+ const getRevision = (locale?: LocaleKey) => {
1047
+ return this.getRevision(locale);
1048
+ };
1049
+ const onDiagnostic = (
1050
+ handler: MessagevisorDiagnosticHandler,
1051
+ options: MessagevisorModuleDiagnosticOptions = {},
1052
+ ) => {
1053
+ const subscription: MessagevisorModuleDiagnosticSubscription = {
1054
+ moduleKey,
1055
+ handler,
1056
+ logLevel: options.logLevel || "info",
1057
+ };
1058
+
1059
+ this.moduleDiagnosticSubscriptions.push(subscription);
1060
+
1061
+ return () => {
1062
+ this.moduleDiagnosticSubscriptions = this.moduleDiagnosticSubscriptions.filter(
1063
+ (currentSubscription) => currentSubscription !== subscription,
1064
+ );
1065
+ };
1066
+ };
1067
+ const reportDiagnostic = (diagnostic: Omit<MessagevisorDiagnostic, "module">) => {
1068
+ const moduleDiagnostic: MessagevisorDiagnostic = { ...diagnostic };
1069
+
1070
+ if (module.name) {
1071
+ moduleDiagnostic.module = module.name;
1072
+ }
1073
+
1074
+ this.reportDiagnostic(moduleDiagnostic, moduleKey);
1075
+ };
1076
+
1077
+ return {
1078
+ setFlagResolver,
1079
+ setVariationResolver,
1080
+ getRevision,
1081
+ onDiagnostic,
1082
+ reportDiagnostic,
1083
+ };
1084
+ }
1085
+
1086
+ private getModuleApiKey(module: MessagevisorModule) {
1087
+ if (module.name) {
1088
+ return `name:${module.name}`;
1089
+ }
1090
+
1091
+ const moduleWithApiKey = module as MessagevisorModule & { __messagevisorModuleApiKey?: string };
1092
+
1093
+ if (!moduleWithApiKey.__messagevisorModuleApiKey) {
1094
+ moduleWithApiKey.__messagevisorModuleApiKey = `anonymous:${++this.moduleApiId}`;
1095
+ }
1096
+
1097
+ return moduleWithApiKey.__messagevisorModuleApiKey;
1098
+ }
1099
+
1100
+ private getModuleApi(module: MessagevisorModule): MessagevisorModuleApi {
1101
+ const key = this.getModuleApiKey(module);
1102
+ const existingApi = this.moduleApis[key];
1103
+
1104
+ if (existingApi) {
1105
+ return existingApi;
1106
+ }
1107
+
1108
+ const api = this.createModuleApi(module);
1109
+
1110
+ this.moduleApis[key] = api;
1111
+
1112
+ return api;
1113
+ }
1114
+
1115
+ private runModuleSetup(module: MessagevisorModule) {
1116
+ if (!module.setup) {
1117
+ return;
1118
+ }
1119
+
1120
+ module.setup(this.getModuleApi(module));
1121
+ }
1122
+
1123
+ private clearModuleDiagnosticSubscriptions(module: MessagevisorModule) {
1124
+ const moduleKey = this.getModuleApiKey(module);
1125
+
1126
+ this.moduleDiagnosticSubscriptions = this.moduleDiagnosticSubscriptions.filter(
1127
+ (subscription) => subscription.moduleKey !== moduleKey,
1128
+ );
1129
+
1130
+ delete this.moduleApis[moduleKey];
1131
+ }
1132
+
1133
+ private runTransforms<T = never>(
1134
+ translation: MessageFormatResult<T>,
1135
+ payload: Omit<MessagevisorTransformPayload, "translation">,
1136
+ ): MessageFormatResult<T> {
1137
+ let currentTranslation = translation as unknown;
1138
+
1139
+ for (const module of this.modules) {
1140
+ const nextTranslation = module.transform?.(
1141
+ {
1142
+ ...payload,
1143
+ translation: currentTranslation,
1144
+ },
1145
+ this.getModuleApi(module),
1146
+ );
1147
+
1148
+ if (typeof nextTranslation !== "undefined") {
1149
+ currentTranslation = nextTranslation;
1150
+ }
1151
+ }
1152
+
1153
+ return currentTranslation as MessageFormatResult<T>;
1154
+ }
1155
+
1156
+ private runFormats<T = never>(
1157
+ translation: MessageFormatResult<T>,
1158
+ values: MessageValues<T> | undefined,
1159
+ payload: Omit<MessagevisorFormatPayload, "translation" | "values">,
1160
+ ): MessageFormatResult<T> {
1161
+ let currentTranslation = translation as unknown;
1162
+
1163
+ for (const module of this.modules) {
1164
+ if (!module.format) {
1165
+ continue;
1166
+ }
1167
+
1168
+ try {
1169
+ const nextTranslation = module.format(
1170
+ {
1171
+ ...payload,
1172
+ translation: currentTranslation,
1173
+ values,
1174
+ },
1175
+ this.getModuleApi(module),
1176
+ );
1177
+
1178
+ if (typeof nextTranslation !== "undefined") {
1179
+ currentTranslation = nextTranslation;
1180
+ }
1181
+ } catch (error) {
1182
+ this.reportDiagnostic({
1183
+ level: "error",
1184
+ code: "invalid_message",
1185
+ message: "Unable to format message",
1186
+ locale: payload.locale,
1187
+ messageKey: payload.messageKey,
1188
+ source: payload.source,
1189
+ originalError: error,
1190
+ });
1191
+
1192
+ throw error;
1193
+ }
1194
+ }
1195
+
1196
+ return currentTranslation as MessageFormatResult<T>;
1197
+ }
1198
+
1199
+ private formatMessageInternal<T = never>(
1200
+ message: string,
1201
+ values?: MessageValues<T>,
1202
+ options: EvaluationOptions = {},
1203
+ ): MessageFormatResult<T> {
1204
+ const locale = this.getCurrentLocale(options);
1205
+ const formats = this.getEvaluationFormats(options, locale);
1206
+
1207
+ const translation = this.runFormats(message as MessageFormatResult<T>, values, {
1208
+ locale,
1209
+ source: "formatMessage",
1210
+ meta: undefined,
1211
+ formats,
1212
+ moduleOptions: options.moduleOptions,
1213
+ });
1214
+
1215
+ return this.runTransforms(translation, {
1216
+ locale,
1217
+ source: "formatMessage",
1218
+ meta: undefined,
1219
+ });
1220
+ }
1221
+
1222
+ translate(
1223
+ messageKey: MessageKey,
1224
+ values?: Record<string, MessagePrimitiveValue>,
1225
+ options?: TranslateOptions,
1226
+ ): string;
1227
+ translate<T>(
1228
+ messageKey: MessageKey,
1229
+ values: MessageValues<T>,
1230
+ options?: TranslateOptions,
1231
+ ): MessageFormatResult<T>;
1232
+ translate<T = never>(
1233
+ messageKey: MessageKey,
1234
+ values?: MessageValues<T>,
1235
+ options: TranslateOptions = {},
1236
+ ): MessageFormatResult<T> {
1237
+ const rawMessage = this.getRawTranslation(messageKey, options);
1238
+ const locale = this.getCurrentLocale(options);
1239
+ const formats = this.getEvaluationFormats(options, locale);
1240
+ const meta = this.getMessageMeta(messageKey, locale);
1241
+ const translation = this.runFormats(rawMessage as MessageFormatResult<T>, values, {
1242
+ locale,
1243
+ source: "translation",
1244
+ messageKey,
1245
+ meta,
1246
+ formats,
1247
+ moduleOptions: options.moduleOptions,
1248
+ });
1249
+
1250
+ return this.runTransforms(translation, {
1251
+ locale,
1252
+ source: "translation",
1253
+ messageKey,
1254
+ meta,
1255
+ });
1256
+ }
1257
+
1258
+ t(
1259
+ messageKey: MessageKey,
1260
+ values?: Record<string, MessagePrimitiveValue>,
1261
+ options?: TranslateOptions,
1262
+ ): string;
1263
+ t<T>(
1264
+ messageKey: MessageKey,
1265
+ values: MessageValues<T>,
1266
+ options?: TranslateOptions,
1267
+ ): MessageFormatResult<T>;
1268
+ t<T = never>(
1269
+ messageKey: MessageKey,
1270
+ values?: MessageValues<T>,
1271
+ options: TranslateOptions = {},
1272
+ ): MessageFormatResult<T> {
1273
+ if (values === undefined) {
1274
+ return this.translate(messageKey, undefined, options);
1275
+ }
1276
+
1277
+ return this.translate(messageKey, values, options);
1278
+ }
1279
+
1280
+ getRawTranslation(messageKey: MessageKey, options: TranslateOptions = {}): string {
1281
+ return this.resolveMessage(messageKey, options.defaultTranslation, options).formatted;
1282
+ }
1283
+
1284
+ formatMessage(
1285
+ message: string,
1286
+ values?: Record<string, MessagePrimitiveValue>,
1287
+ options?: EvaluationOptions,
1288
+ ): string;
1289
+ formatMessage<T>(
1290
+ message: string,
1291
+ values: MessageValues<T>,
1292
+ options?: EvaluationOptions,
1293
+ ): MessageFormatResult<T>;
1294
+ formatMessage<T = never>(
1295
+ message: string,
1296
+ values?: MessageValues<T>,
1297
+ options: EvaluationOptions = {},
1298
+ ): MessageFormatResult<T> {
1299
+ return this.formatMessageInternal(message, values, options);
1300
+ }
1301
+
1302
+ async close(): Promise<void> {
1303
+ if (this.closePromise) {
1304
+ return this.closePromise;
1305
+ }
1306
+
1307
+ if (this.closed) {
1308
+ return;
1309
+ }
1310
+
1311
+ this.closed = true;
1312
+ Object.keys(this.listeners).forEach((eventName) => {
1313
+ this.listeners[eventName as MessagevisorEventName] = [];
1314
+ });
1315
+ this.moduleDiagnosticSubscriptions = [];
1316
+ this.moduleApis = {};
1317
+
1318
+ this.closePromise = this.closeModules();
1319
+ return this.closePromise;
1320
+ }
1321
+
1322
+ private async closeModules(): Promise<void> {
1323
+ const errors: unknown[] = [];
1324
+ const modulesToClose = [...this.modules].reverse();
1325
+
1326
+ for (const module of modulesToClose) {
1327
+ if (!module.close) {
1328
+ continue;
1329
+ }
1330
+
1331
+ try {
1332
+ await module.close();
1333
+ } catch (error) {
1334
+ errors.push(error);
1335
+ }
1336
+ }
1337
+
1338
+ this.modules = [];
1339
+
1340
+ if (errors.length > 0) {
1341
+ throw new MessagevisorCloseError("One or more Messagevisor modules failed to close.", errors);
1342
+ }
1343
+ }
1344
+
1345
+ formatNumber(
1346
+ value: number,
1347
+ presetOrOptions?: string | FormatNumberPresetOptions,
1348
+ options: EvaluationOptions = {},
1349
+ ) {
1350
+ const locale = this.getCurrentLocale(options);
1351
+ const evaluationFormats = this.getEvaluationFormats(options, locale);
1352
+ const formatOptions =
1353
+ typeof presetOrOptions === "string"
1354
+ ? evaluationFormats.number?.[presetOrOptions]
1355
+ : presetOrOptions;
1356
+ const finalOptions = { ...(formatOptions || {}) } as Intl.NumberFormatOptions;
1357
+
1358
+ if (finalOptions.style === "currency") {
1359
+ finalOptions.currency = resolveCurrency(
1360
+ options.currency,
1361
+ finalOptions.currency,
1362
+ this.currency,
1363
+ );
1364
+ }
1365
+
1366
+ return this.getCachedNumberFormat(locale, finalOptions).format(value);
1367
+ }
1368
+
1369
+ formatNumberToParts(
1370
+ value: number,
1371
+ presetOrOptions?: string | FormatNumberPresetOptions,
1372
+ options: EvaluationOptions = {},
1373
+ ) {
1374
+ const locale = this.getCurrentLocale(options);
1375
+ const evaluationFormats = this.getEvaluationFormats(options, locale);
1376
+ const formatOptions =
1377
+ typeof presetOrOptions === "string"
1378
+ ? evaluationFormats.number?.[presetOrOptions]
1379
+ : presetOrOptions;
1380
+ const finalOptions = { ...(formatOptions || {}) } as Intl.NumberFormatOptions;
1381
+
1382
+ if (finalOptions.style === "currency") {
1383
+ finalOptions.currency = resolveCurrency(
1384
+ options.currency,
1385
+ finalOptions.currency,
1386
+ this.currency,
1387
+ );
1388
+ }
1389
+
1390
+ return this.getCachedNumberFormat(locale, finalOptions).formatToParts(value);
1391
+ }
1392
+
1393
+ formatDate(
1394
+ value: Date | number | string,
1395
+ presetOrOptions?: string | FormatDateTimePresetOptions,
1396
+ options: EvaluationOptions = {},
1397
+ ) {
1398
+ const locale = this.getCurrentLocale(options);
1399
+ const evaluationFormats = this.getEvaluationFormats(options, locale);
1400
+ const formatOptions =
1401
+ typeof presetOrOptions === "string"
1402
+ ? evaluationFormats.date?.[presetOrOptions]
1403
+ : presetOrOptions;
1404
+
1405
+ return this.getCachedDateTimeFormat(
1406
+ locale,
1407
+ resolveDateTimeOptions(formatOptions, options, this.timeZone),
1408
+ ).format(new Date(value));
1409
+ }
1410
+
1411
+ formatDateToParts(
1412
+ value: Date | number | string,
1413
+ presetOrOptions?: string | FormatDateTimePresetOptions,
1414
+ options: EvaluationOptions = {},
1415
+ ) {
1416
+ const locale = this.getCurrentLocale(options);
1417
+ const evaluationFormats = this.getEvaluationFormats(options, locale);
1418
+ const formatOptions =
1419
+ typeof presetOrOptions === "string"
1420
+ ? evaluationFormats.date?.[presetOrOptions]
1421
+ : presetOrOptions;
1422
+
1423
+ return this.getCachedDateTimeFormat(
1424
+ locale,
1425
+ resolveDateTimeOptions(formatOptions, options, this.timeZone),
1426
+ ).formatToParts(new Date(value));
1427
+ }
1428
+
1429
+ formatTime(
1430
+ value: Date | number | string,
1431
+ presetOrOptions?: string | FormatDateTimePresetOptions,
1432
+ options: EvaluationOptions = {},
1433
+ ) {
1434
+ const locale = this.getCurrentLocale(options);
1435
+ const evaluationFormats = this.getEvaluationFormats(options, locale);
1436
+ const formatOptions =
1437
+ typeof presetOrOptions === "string"
1438
+ ? evaluationFormats.time?.[presetOrOptions]
1439
+ : presetOrOptions;
1440
+
1441
+ return this.getCachedDateTimeFormat(
1442
+ locale,
1443
+ resolveDateTimeOptions(formatOptions, options, this.timeZone),
1444
+ ).format(new Date(value));
1445
+ }
1446
+
1447
+ formatTimeToParts(
1448
+ value: Date | number | string,
1449
+ presetOrOptions?: string | FormatDateTimePresetOptions,
1450
+ options: EvaluationOptions = {},
1451
+ ) {
1452
+ const locale = this.getCurrentLocale(options);
1453
+ const evaluationFormats = this.getEvaluationFormats(options, locale);
1454
+ const formatOptions =
1455
+ typeof presetOrOptions === "string"
1456
+ ? evaluationFormats.time?.[presetOrOptions]
1457
+ : presetOrOptions;
1458
+
1459
+ return this.getCachedDateTimeFormat(
1460
+ locale,
1461
+ resolveDateTimeOptions(formatOptions, options, this.timeZone),
1462
+ ).formatToParts(new Date(value));
1463
+ }
1464
+
1465
+ formatDateTimeRange(
1466
+ start: Date | number | string,
1467
+ end: Date | number | string,
1468
+ presetOrOptions?: string | FormatDateTimePresetOptions,
1469
+ options: EvaluationOptions = {},
1470
+ ) {
1471
+ const locale = this.getCurrentLocale(options);
1472
+ const evaluationFormats = this.getEvaluationFormats(options, locale);
1473
+ const formatOptions =
1474
+ typeof presetOrOptions === "string"
1475
+ ? evaluationFormats.dateTimeRange?.[presetOrOptions]
1476
+ : presetOrOptions;
1477
+ const formatter = this.getCachedDateTimeFormat(
1478
+ locale,
1479
+ resolveDateTimeOptions(formatOptions, options, this.timeZone),
1480
+ );
1481
+
1482
+ const rangeFormatter = formatter as Intl.DateTimeFormat & {
1483
+ formatRange?: (startDate: Date, endDate: Date) => string;
1484
+ };
1485
+
1486
+ if (rangeFormatter.formatRange) {
1487
+ return rangeFormatter.formatRange(new Date(start), new Date(end));
1488
+ }
1489
+
1490
+ return `${formatter.format(new Date(start))} - ${formatter.format(new Date(end))}`;
1491
+ }
1492
+
1493
+ formatRelativeTime(
1494
+ value: number,
1495
+ unit: Intl.RelativeTimeFormatUnit,
1496
+ presetOrOptions?: string | FormatRelativeTimePresetOptions,
1497
+ options: EvaluationOptions = {},
1498
+ ) {
1499
+ const locale = this.getCurrentLocale(options);
1500
+ const evaluationFormats = this.getEvaluationFormats(options, locale);
1501
+ const formatOptions =
1502
+ typeof presetOrOptions === "string"
1503
+ ? evaluationFormats.relative?.[presetOrOptions]
1504
+ : presetOrOptions;
1505
+
1506
+ return this.getCachedRelativeTimeFormat(locale, formatOptions).format(value, unit);
1507
+ }
1508
+
1509
+ formatPlural(
1510
+ value: number,
1511
+ options: Intl.PluralRulesOptions & Pick<EvaluationOptions, "locale"> = {},
1512
+ ) {
1513
+ const { locale: optionLocale, ...pluralOptions } = options;
1514
+ const locale = this.getCurrentLocale({ locale: optionLocale });
1515
+ var cacheKey = JSON.stringify({
1516
+ locale,
1517
+ options: pluralOptions,
1518
+ });
1519
+
1520
+ if (!this.cache.pluralRules[cacheKey]) {
1521
+ this.cache.pluralRules[cacheKey] = new Intl.PluralRules(locale, pluralOptions);
1522
+ }
1523
+
1524
+ return this.cache.pluralRules[cacheKey].select(value);
1525
+ }
1526
+
1527
+ formatList(values: Array<string>, options: any = {}) {
1528
+ const { locale: optionLocale, ...listOptions } = options || {};
1529
+ const locale = this.getCurrentLocale({ locale: optionLocale });
1530
+ var cacheKey = JSON.stringify({
1531
+ locale,
1532
+ options: listOptions,
1533
+ });
1534
+ var ListFormat = (Intl as any).ListFormat;
1535
+
1536
+ if (!ListFormat) {
1537
+ this.reportDiagnostic({
1538
+ level: "warn",
1539
+ code: "unsupported_formatter",
1540
+ message: "Intl.ListFormat is not available in this environment.",
1541
+ locale,
1542
+ });
1543
+
1544
+ return values.join(", ");
1545
+ }
1546
+
1547
+ if (!this.cache.listFormat[cacheKey]) {
1548
+ this.cache.listFormat[cacheKey] = new ListFormat(locale, listOptions);
1549
+ }
1550
+
1551
+ return this.cache.listFormat[cacheKey].format(values);
1552
+ }
1553
+
1554
+ formatListToParts(values: Array<string>, options: any = {}) {
1555
+ const { locale: optionLocale, ...listOptions } = options || {};
1556
+ const locale = this.getCurrentLocale({ locale: optionLocale });
1557
+ var cacheKey = JSON.stringify({
1558
+ locale,
1559
+ options: listOptions,
1560
+ });
1561
+ var ListFormat = (Intl as any).ListFormat;
1562
+
1563
+ if (!ListFormat) {
1564
+ this.reportDiagnostic({
1565
+ level: "warn",
1566
+ code: "unsupported_formatter",
1567
+ message: "Intl.ListFormat is not available in this environment.",
1568
+ locale,
1569
+ });
1570
+
1571
+ return values;
1572
+ }
1573
+
1574
+ if (!this.cache.listFormat[cacheKey]) {
1575
+ this.cache.listFormat[cacheKey] = new ListFormat(locale, listOptions);
1576
+ }
1577
+
1578
+ if (typeof this.cache.listFormat[cacheKey].formatToParts !== "function") {
1579
+ return values;
1580
+ }
1581
+
1582
+ return this.cache.listFormat[cacheKey].formatToParts(values);
1583
+ }
1584
+
1585
+ formatDisplayName(value: string, options: any = {}) {
1586
+ const { locale: optionLocale, ...displayNameOptions } = options || {};
1587
+ const locale = this.getCurrentLocale({ locale: optionLocale });
1588
+ var cacheKey = JSON.stringify({
1589
+ locale,
1590
+ options: displayNameOptions,
1591
+ });
1592
+ var DisplayNames = (Intl as any).DisplayNames;
1593
+
1594
+ if (!DisplayNames) {
1595
+ this.reportDiagnostic({
1596
+ level: "warn",
1597
+ code: "unsupported_formatter",
1598
+ message: "Intl.DisplayNames is not available in this environment.",
1599
+ locale,
1600
+ });
1601
+
1602
+ return displayNameOptions && displayNameOptions.fallback === "none" ? undefined : value;
1603
+ }
1604
+
1605
+ if (!this.cache.displayNames[cacheKey]) {
1606
+ this.cache.displayNames[cacheKey] = new DisplayNames(locale, displayNameOptions);
1607
+ }
1608
+
1609
+ return this.cache.displayNames[cacheKey].of(value);
1610
+ }
1611
+ }
1612
+
1613
+ export function createMessagevisor(options: MessagevisorOptions = {}) {
1614
+ return new Messagevisor(options);
1615
+ }