@messagevisor/sdk 0.0.1 → 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,294 @@
1
+ import type { Context, DatafileContent, Segment } from "@messagevisor/types";
2
+
3
+ import { evaluateCondition, evaluateGroupSegment } from "./conditions";
4
+ import { createMessagevisor } from "./index";
5
+
6
+ const edgeContext: Context = {
7
+ zero: 0,
8
+ falseValue: false,
9
+ emptyString: "",
10
+ nested: {
11
+ zero: {
12
+ value: 0,
13
+ },
14
+ falseValue: {
15
+ value: false,
16
+ },
17
+ emptyString: {
18
+ value: "",
19
+ },
20
+ },
21
+ numericString: "42",
22
+ nonNumericString: "forty-two",
23
+ list: ["alpha", "beta"],
24
+ };
25
+
26
+ const datafile: DatafileContent = {
27
+ schemaVersion: "1",
28
+ messagevisorVersion: "0.0.1",
29
+ revision: "1",
30
+ target: "web",
31
+ locale: "en-US",
32
+ formats: {},
33
+ segments: {
34
+ "zero-segment": {
35
+ conditions: { attribute: "zero", operator: "equals", value: 0 },
36
+ },
37
+ "false-segment": {
38
+ conditions: { attribute: "falseValue", operator: "equals", value: false },
39
+ },
40
+ "empty-string-segment": {
41
+ conditions: { attribute: "emptyString", operator: "equals", value: "" },
42
+ },
43
+ },
44
+ messages: {
45
+ "empty.translation": {},
46
+ "zero.override": {
47
+ overrides: [
48
+ {
49
+ key: "zero",
50
+ segments: "zero-segment",
51
+ translation: "Zero matched",
52
+ },
53
+ ],
54
+ },
55
+ "false.override": {
56
+ overrides: [
57
+ {
58
+ key: "false",
59
+ segments: "false-segment",
60
+ translation: "False matched",
61
+ },
62
+ ],
63
+ },
64
+ "empty.override": {
65
+ overrides: [
66
+ {
67
+ key: "empty",
68
+ segments: "empty-string-segment",
69
+ translation: "Empty matched",
70
+ },
71
+ ],
72
+ },
73
+ "empty.override.withFallback": {
74
+ overrides: [
75
+ {
76
+ key: "empty",
77
+ segments: "empty-string-segment",
78
+ translation: "",
79
+ },
80
+ ],
81
+ },
82
+ "deprecated.override": {
83
+ deprecated: true,
84
+ deprecationWarning: "Use replacement.override.",
85
+ overrides: [
86
+ {
87
+ key: "zero",
88
+ segments: "zero-segment",
89
+ translation: "Deprecated override",
90
+ },
91
+ ],
92
+ },
93
+ },
94
+ translations: {
95
+ "empty.translation": "",
96
+ "zero.override": "Zero default",
97
+ "false.override": "False default",
98
+ "empty.override": "Empty default",
99
+ "empty.override.withFallback": "Empty override default",
100
+ "deprecated.override": "Deprecated default",
101
+ },
102
+ };
103
+
104
+ describe("condition and evaluation edge cases", function () {
105
+ let consoleInfoSpy: jest.SpyInstance;
106
+ let consoleErrorSpy: jest.SpyInstance;
107
+
108
+ beforeEach(function () {
109
+ consoleInfoSpy = jest.spyOn(console, "info").mockImplementation(function () {});
110
+ consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(function () {});
111
+ });
112
+
113
+ afterEach(function () {
114
+ consoleInfoSpy.mockRestore();
115
+ consoleErrorSpy.mockRestore();
116
+ });
117
+
118
+ it("treats top-level falsy values as valid condition values", function () {
119
+ expect(
120
+ evaluateCondition(
121
+ { attribute: "zero", operator: "equals", value: 0 },
122
+ { context: edgeContext },
123
+ ),
124
+ ).toEqual(true);
125
+ expect(
126
+ evaluateCondition(
127
+ { attribute: "falseValue", operator: "equals", value: false },
128
+ { context: edgeContext },
129
+ ),
130
+ ).toEqual(true);
131
+ expect(
132
+ evaluateCondition(
133
+ { attribute: "emptyString", operator: "equals", value: "" },
134
+ { context: edgeContext },
135
+ ),
136
+ ).toEqual(true);
137
+ expect(
138
+ evaluateCondition({ attribute: "zero", operator: "exists" }, { context: edgeContext }),
139
+ ).toEqual(true);
140
+ expect(
141
+ evaluateCondition({ attribute: "falseValue", operator: "exists" }, { context: edgeContext }),
142
+ ).toEqual(true);
143
+ expect(
144
+ evaluateCondition({ attribute: "emptyString", operator: "exists" }, { context: edgeContext }),
145
+ ).toEqual(true);
146
+ });
147
+
148
+ it("documents nested falsy traversal behavior for future SDK ports", function () {
149
+ expect(
150
+ evaluateCondition(
151
+ { attribute: "nested.zero.value", operator: "equals", value: 0 },
152
+ { context: edgeContext },
153
+ ),
154
+ ).toEqual(true);
155
+ expect(
156
+ evaluateCondition(
157
+ { attribute: "nested.falseValue.value", operator: "equals", value: false },
158
+ { context: edgeContext },
159
+ ),
160
+ ).toEqual(true);
161
+ expect(
162
+ evaluateCondition(
163
+ { attribute: "nested.emptyString.value", operator: "equals", value: "" },
164
+ { context: edgeContext },
165
+ ),
166
+ ).toEqual(true);
167
+ });
168
+
169
+ it("uses JavaScript number coercion for numeric comparisons", function () {
170
+ expect(
171
+ evaluateCondition(
172
+ { attribute: "numericString", operator: "greaterThan", value: 41 },
173
+ { context: edgeContext },
174
+ ),
175
+ ).toEqual(true);
176
+ expect(
177
+ evaluateCondition(
178
+ { attribute: "numericString", operator: "lessThanOrEquals", value: 42 },
179
+ { context: edgeContext },
180
+ ),
181
+ ).toEqual(true);
182
+ expect(
183
+ evaluateCondition(
184
+ { attribute: "nonNumericString", operator: "greaterThan", value: 1 },
185
+ { context: edgeContext },
186
+ ),
187
+ ).toEqual(false);
188
+ });
189
+
190
+ it("evaluates array conditions as all-of and group segment arrays as all-of", function () {
191
+ const segments: Record<string, Segment> = {
192
+ a: { conditions: { attribute: "zero", operator: "equals", value: 0 } },
193
+ b: { conditions: { attribute: "falseValue", operator: "equals", value: false } },
194
+ c: { conditions: { attribute: "emptyString", operator: "equals", value: "not-empty" } },
195
+ };
196
+
197
+ expect(
198
+ evaluateCondition(
199
+ [
200
+ { attribute: "zero", operator: "equals", value: 0 },
201
+ { attribute: "falseValue", operator: "equals", value: false },
202
+ ],
203
+ { context: edgeContext },
204
+ ),
205
+ ).toEqual(true);
206
+ expect(evaluateGroupSegment(["a", "b"], { context: edgeContext, segments })).toEqual(true);
207
+ expect(evaluateGroupSegment(["a", "c"], { context: edgeContext, segments })).toEqual(false);
208
+ });
209
+
210
+ it("returns empty datafile translations as explicit values", function () {
211
+ const m = createMessagevisor({
212
+ datafile,
213
+ logLevel: "fatal",
214
+ });
215
+
216
+ expect(m.translate("empty.translation")).toEqual("");
217
+ expect(
218
+ m.translate("empty.translation", undefined, {
219
+ defaultTranslation: "Default",
220
+ }),
221
+ ).toEqual("");
222
+ });
223
+
224
+ it("uses falsy context values when evaluating message overrides", function () {
225
+ const m = createMessagevisor({
226
+ datafile,
227
+ context: {
228
+ zero: 0,
229
+ falseValue: false,
230
+ emptyString: "",
231
+ },
232
+ logLevel: "fatal",
233
+ });
234
+
235
+ expect(m.translate("zero.override")).toEqual("Zero matched");
236
+ expect(m.translate("false.override")).toEqual("False matched");
237
+ expect(m.translate("empty.override")).toEqual("Empty matched");
238
+ });
239
+
240
+ it("returns empty matching overrides as explicit values", function () {
241
+ const m = createMessagevisor({
242
+ datafile,
243
+ context: { emptyString: "" },
244
+ defaultTranslations: {
245
+ "en-US": {
246
+ "empty.override.withFallback": "Fallback override",
247
+ },
248
+ },
249
+ logLevel: "fatal",
250
+ });
251
+
252
+ expect(m.translate("empty.override.withFallback")).toEqual("");
253
+ });
254
+
255
+ it("emits deprecated diagnostics when a deprecated message resolves through an override", function () {
256
+ const diagnostics: any[] = [];
257
+ const m = createMessagevisor({
258
+ datafile,
259
+ context: { zero: 0 },
260
+ logLevel: "warn",
261
+ onDiagnostic: (diagnostic) => diagnostics.push(diagnostic),
262
+ });
263
+
264
+ expect(m.translate("deprecated.override")).toEqual("Deprecated override");
265
+ expect(diagnostics).toEqual([
266
+ expect.objectContaining({
267
+ code: "deprecated_message",
268
+ messageKey: "deprecated.override",
269
+ deprecationWarning: "Use replacement.override.",
270
+ }),
271
+ ]);
272
+ });
273
+
274
+ it("documents nested traversal through primitive falsy parents", function () {
275
+ expect(
276
+ evaluateCondition(
277
+ { attribute: "zero.value", operator: "notExists" },
278
+ { context: edgeContext },
279
+ ),
280
+ ).toEqual(true);
281
+ expect(
282
+ evaluateCondition(
283
+ { attribute: "falseValue.value", operator: "notExists" },
284
+ { context: edgeContext },
285
+ ),
286
+ ).toEqual(true);
287
+ expect(
288
+ evaluateCondition(
289
+ { attribute: "emptyString.value", operator: "notExists" },
290
+ { context: edgeContext },
291
+ ),
292
+ ).toEqual(true);
293
+ });
294
+ });
@@ -0,0 +1,295 @@
1
+ import type { DatafileContent } from "@messagevisor/types";
2
+
3
+ import { createMessagevisor, createMessagevisorCache } from "./index";
4
+
5
+ const datafile: DatafileContent = {
6
+ schemaVersion: "1",
7
+ messagevisorVersion: "0.0.1",
8
+ revision: "1",
9
+ target: "web",
10
+ locale: "en-US",
11
+ direction: "ltr",
12
+ formats: {
13
+ number: {
14
+ currencyCode: { style: "currency", currency: "USD", currencyDisplay: "code" },
15
+ runtimeCurrency: { style: "currency", currencyDisplay: "code" },
16
+ precise: { minimumFractionDigits: 2, maximumFractionDigits: 2 },
17
+ significant3: { minimumSignificantDigits: 3, maximumSignificantDigits: 3 },
18
+ signAlways: { signDisplay: "always", maximumFractionDigits: 0 },
19
+ percent: { style: "percent", maximumFractionDigits: 1 },
20
+ compactShort: { notation: "compact", compactDisplay: "short" },
21
+ compactLong: { notation: "compact", compactDisplay: "long" },
22
+ unitKilometerLong: { style: "unit", unit: "kilometer", unitDisplay: "long" },
23
+ moneyGbp: { style: "currency", currency: "GBP" },
24
+ moneyJpy: { style: "currency", currency: "JPY" },
25
+ },
26
+ date: {
27
+ utcDate: { year: "numeric", month: "2-digit", day: "2-digit", timeZone: "UTC" },
28
+ runtimeDate: { year: "numeric", month: "2-digit", day: "2-digit" },
29
+ long: { year: "numeric", month: "long", day: "numeric", timeZone: "UTC" },
30
+ },
31
+ time: {
32
+ utcTime: { hour: "2-digit", minute: "2-digit", hour12: false, timeZone: "UTC" },
33
+ runtimeTime: { hour: "2-digit", minute: "2-digit", hour12: false },
34
+ utc: { hour: "numeric", minute: "2-digit", timeZone: "UTC" },
35
+ zoneShort: { hour: "numeric", minute: "2-digit", timeZone: "UTC", timeZoneName: "short" },
36
+ zoneShortGeneric: {
37
+ hour: "numeric",
38
+ minute: "2-digit",
39
+ timeZone: "America/New_York",
40
+ timeZoneName: "shortGeneric",
41
+ },
42
+ },
43
+ dateTimeRange: {
44
+ utcRange: {
45
+ year: "numeric",
46
+ month: "short",
47
+ day: "numeric",
48
+ hour: "numeric",
49
+ minute: "2-digit",
50
+ timeZone: "UTC",
51
+ },
52
+ },
53
+ relative: {
54
+ longAuto: { numeric: "auto", style: "long" },
55
+ narrowAlways: { numeric: "always", style: "narrow" },
56
+ },
57
+ },
58
+ segments: {},
59
+ messages: {},
60
+ translations: {},
61
+ };
62
+
63
+ describe("Intl formatter helpers", function () {
64
+ let consoleInfoSpy: jest.SpyInstance;
65
+ let consoleWarnSpy: jest.SpyInstance;
66
+
67
+ beforeEach(function () {
68
+ consoleInfoSpy = jest.spyOn(console, "info").mockImplementation(function () {});
69
+ consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(function () {});
70
+ });
71
+
72
+ afterEach(function () {
73
+ consoleInfoSpy.mockRestore();
74
+ consoleWarnSpy.mockRestore();
75
+ });
76
+
77
+ it("reuses caller-provided cache buckets across helper methods", function () {
78
+ const cache = createMessagevisorCache();
79
+ const m = createMessagevisor({ datafile, cache, logLevel: "fatal" });
80
+ const date = new Date("2026-05-12T08:30:00Z");
81
+
82
+ expect(Object.keys(cache.numberFormat)).toHaveLength(0);
83
+ expect(Object.keys(cache.dateTimeFormat)).toHaveLength(0);
84
+ expect(Object.keys(cache.relativeTimeFormat)).toHaveLength(0);
85
+ expect(Object.keys(cache.pluralRules)).toHaveLength(0);
86
+
87
+ expect(m.formatNumber(12, "precise")).toEqual("12.00");
88
+ expect(m.formatNumberToParts(12, "precise").map((part) => part.type)).toContain("integer");
89
+ expect(Object.keys(cache.numberFormat)).toHaveLength(1);
90
+
91
+ expect(m.formatDate(date, "utcDate")).toEqual("05/12/2026");
92
+ expect(m.formatTime(date, "utcTime")).toEqual("08:30");
93
+ expect(m.formatDateToParts(date, "utcDate").map((part) => part.type)).toContain("year");
94
+ expect(m.formatTimeToParts(date, "utcTime").map((part) => part.type)).toContain("hour");
95
+ expect(Object.keys(cache.dateTimeFormat)).toHaveLength(2);
96
+
97
+ expect(m.formatRelativeTime(-1, "day", "longAuto")).toEqual("yesterday");
98
+ expect(m.formatRelativeTime(3, "day", "narrowAlways")).toContain("3");
99
+ expect(Object.keys(cache.relativeTimeFormat)).toHaveLength(2);
100
+
101
+ expect(m.formatPlural(1)).toEqual("one");
102
+ expect(m.formatPlural(2)).toEqual("other");
103
+ expect(Object.keys(cache.pluralRules)).toHaveLength(1);
104
+ });
105
+
106
+ it("partitions formatter caches by locale and resolved options", function () {
107
+ const cache = createMessagevisorCache();
108
+ const m = createMessagevisor({ datafile, cache, logLevel: "fatal" });
109
+
110
+ m.formatNumber(12, "precise");
111
+ m.formatNumber(12, "percent");
112
+ expect(Object.keys(cache.numberFormat)).toHaveLength(2);
113
+
114
+ m.setDatafile({ ...datafile, locale: "nl-NL", revision: "nl-1" });
115
+ m.setLocale("nl-NL");
116
+ m.formatNumber(12, "precise");
117
+ expect(Object.keys(cache.numberFormat)).toHaveLength(3);
118
+
119
+ m.formatDate("2026-05-12T00:00:00Z", "runtimeDate", { timeZone: "UTC" });
120
+ m.formatDate("2026-05-12T00:00:00Z", "runtimeDate", { timeZone: "Asia/Tokyo" });
121
+ expect(Object.keys(cache.dateTimeFormat)).toHaveLength(2);
122
+ });
123
+
124
+ it("falls back to default Intl options for unknown presets", function () {
125
+ const m = createMessagevisor({ datafile, logLevel: "fatal" });
126
+
127
+ expect(m.formatNumber(1200, "missing")).toEqual("1,200");
128
+ expect(m.formatNumberToParts(1200, "missing").map((part) => part.type)).toContain("integer");
129
+ expect(m.formatDate("2026-05-12T00:00:00Z", "missing", { timeZone: "UTC" })).toEqual(
130
+ "5/12/2026",
131
+ );
132
+ expect(m.formatTime("2026-05-12T08:30:00Z", "missing", { timeZone: "UTC" })).toEqual(
133
+ "5/12/2026",
134
+ );
135
+ expect(m.formatRelativeTime(-1, "day", "missing")).toEqual("1 day ago");
136
+ });
137
+
138
+ it("supports locale-keyed default formats before any datafile is loaded", function () {
139
+ const m = createMessagevisor({
140
+ locale: "nl-NL",
141
+ currency: "EUR",
142
+ timeZone: "UTC",
143
+ defaultFormats: {
144
+ "nl-NL": {
145
+ number: {
146
+ money: { style: "currency", currencyDisplay: "symbol" },
147
+ },
148
+ date: {
149
+ short: { year: "numeric", month: "2-digit", day: "2-digit" },
150
+ },
151
+ time: {
152
+ short: { hour: "2-digit", minute: "2-digit", hour12: false },
153
+ },
154
+ },
155
+ },
156
+ logLevel: "fatal",
157
+ });
158
+
159
+ expect(m.formatNumber(12, "money")).toEqual("\u20ac\u00a012,00");
160
+ expect(m.formatDate("2026-05-12T00:00:00Z", "short")).toEqual("12-05-2026");
161
+ expect(m.formatTime("2026-05-12T08:30:00Z", "short")).toEqual("08:30");
162
+ });
163
+
164
+ it("applies currency precedence consistently across number helpers", function () {
165
+ const m = createMessagevisor({ datafile, currency: "GBP", logLevel: "fatal" });
166
+
167
+ expect(m.formatNumber(12, "currencyCode")).toContain("USD");
168
+ expect(m.formatNumberToParts(12, "currencyCode").some((part) => part.value === "USD")).toEqual(
169
+ true,
170
+ );
171
+ expect(m.formatNumber(12, "runtimeCurrency")).toContain("GBP");
172
+ expect(m.formatNumber(12, "runtimeCurrency", { currency: "EUR" })).toContain("EUR");
173
+ expect(m.formatNumber(12, "runtimeCurrency")).toContain("GBP");
174
+ expect(m.formatNumber(12, "runtimeCurrency", { currency: "JPY" })).toContain("JPY");
175
+ });
176
+
177
+ it("applies time zone precedence consistently across date and time helpers", function () {
178
+ const m = createMessagevisor({ datafile, timeZone: "Asia/Tokyo", logLevel: "fatal" });
179
+ const value = "2026-05-12T23:30:00Z";
180
+
181
+ expect(m.formatDate(value, "utcDate")).toEqual("05/12/2026");
182
+ expect(m.formatDate(value, "runtimeDate")).toEqual("05/13/2026");
183
+ expect(m.formatDate(value, "runtimeDate", { timeZone: "America/New_York" })).toEqual(
184
+ "05/12/2026",
185
+ );
186
+ expect(m.formatTime(value, "utcTime")).toEqual("23:30");
187
+ expect(m.formatTime(value, "runtimeTime")).toEqual("08:30");
188
+ expect(m.formatTime(value, "runtimeTime", { timeZone: "America/New_York" })).toEqual("19:30");
189
+ });
190
+
191
+ it("formats range, list, list parts, display names, and plural categories", function () {
192
+ const m = createMessagevisor({ datafile, logLevel: "fatal" });
193
+ const start = "2026-05-12T08:00:00Z";
194
+ const end = "2026-05-12T09:30:00Z";
195
+
196
+ expect(m.formatDateTimeRange(start, end, "utcRange")).toContain("May 12, 2026");
197
+ expect(m.formatList(["A", "B", "C"], { type: "conjunction" })).toEqual("A, B, and C");
198
+ expect(
199
+ m.formatListToParts(["A", "B"], { type: "conjunction" }).map((part: any) => part.type),
200
+ ).toContain("element");
201
+ expect(m.formatDisplayName("NL", { type: "region" })).toEqual("Netherlands");
202
+ expect(m.formatPlural(1, { type: "ordinal" })).toEqual("one");
203
+ expect(m.formatPlural(2, { type: "ordinal" })).toEqual("two");
204
+ });
205
+
206
+ it("keeps the portable formatter contract stable across direct helpers", function () {
207
+ const m = createMessagevisor({ datafile, logLevel: "fatal" });
208
+ const when = "2026-05-12T08:30:45.678Z";
209
+
210
+ expect(m.formatNumber(1234567.891)).toEqual("1,234,567.891");
211
+ expect(m.formatNumber(12, "precise")).toEqual("12.00");
212
+ expect(m.formatNumber(12345.678, "significant3")).toEqual("12,300");
213
+ expect(m.formatNumber(5, "signAlways")).toEqual("+5");
214
+ expect(m.formatNumber(12500, "compactShort")).toEqual("13K");
215
+ expect(m.formatNumber(12500, "compactLong")).toEqual("13 thousand");
216
+ expect(m.formatNumber(42, "unitKilometerLong")).toEqual("42 kilometers");
217
+ expect(m.formatNumber(99.5, "moneyGbp")).toEqual("\u00a399.50");
218
+ expect(m.formatNumber(1999.99, "moneyJpy")).toEqual("\u00a52,000");
219
+ expect(m.formatNumber(0.0575, "percent")).toEqual("5.8%");
220
+ expect(m.formatDate(when, "utcDate")).toEqual("05/12/2026");
221
+ expect(m.formatDate(when, "long")).toEqual("May 12, 2026");
222
+ expect(m.formatTime(when, "utc")).toEqual("8:30 AM");
223
+ expect(m.formatTime(when, "zoneShort")).toEqual("8:30 AM UTC");
224
+ expect(m.formatTime(when, "zoneShortGeneric")).toEqual("4:30 AM ET");
225
+ expect(m.formatList(["A", "B", "C"], { type: "conjunction" })).toEqual("A, B, and C");
226
+ expect(m.formatDisplayName("NL", { type: "region" })).toEqual("Netherlands");
227
+ expect(m.formatRelativeTime(-2, "day", "longAuto")).toEqual("2 days ago");
228
+ });
229
+
230
+ it("uses Intl for non-Latin locale formatter output", function () {
231
+ const m = createMessagevisor({
232
+ datafile: { ...datafile, locale: "bn-BD" },
233
+ timeZone: "UTC",
234
+ logLevel: "fatal",
235
+ });
236
+ const when = "2026-05-12T08:30:45.678Z";
237
+
238
+ expect(m.formatNumber(1234567.891)).toEqual("১২,৩৪,৫৬৭.৮৯১");
239
+ expect(m.formatNumber(12500, "compactShort")).toEqual("১৩\u00a0হা");
240
+ expect(m.formatNumber(42, "unitKilometerLong")).toEqual("৪২ কিলোমিটার");
241
+ expect(m.formatDate(when, "utcDate")).toEqual("১২/০৫/২০২৬");
242
+ expect(m.formatTime(when, "zoneShort")).toEqual("৮:৩০ AM UTC");
243
+ });
244
+
245
+ it("falls back and reports diagnostics when optional Intl helpers are unavailable", function () {
246
+ const originalListFormat = (Intl as any).ListFormat;
247
+ const originalDisplayNames = (Intl as any).DisplayNames;
248
+ const diagnostics: any[] = [];
249
+ const m = createMessagevisor({
250
+ datafile,
251
+ logLevel: "warn",
252
+ onDiagnostic: (diagnostic) => diagnostics.push(diagnostic),
253
+ });
254
+
255
+ try {
256
+ (Intl as any).ListFormat = undefined;
257
+ (Intl as any).DisplayNames = undefined;
258
+
259
+ expect(m.formatList(["A", "B"])).toEqual("A, B");
260
+ expect(m.formatListToParts(["A", "B"])).toEqual(["A", "B"]);
261
+ expect(m.formatDisplayName("NL", { type: "region" })).toEqual("NL");
262
+ expect(m.formatDisplayName("NL", { type: "region", fallback: "none" })).toEqual(undefined);
263
+ } finally {
264
+ (Intl as any).ListFormat = originalListFormat;
265
+ (Intl as any).DisplayNames = originalDisplayNames;
266
+ }
267
+
268
+ expect(diagnostics).toEqual([
269
+ expect.objectContaining({ code: "unsupported_formatter", level: "warn" }),
270
+ expect.objectContaining({ code: "unsupported_formatter", level: "warn" }),
271
+ expect.objectContaining({ code: "unsupported_formatter", level: "warn" }),
272
+ expect.objectContaining({ code: "unsupported_formatter", level: "warn" }),
273
+ ]);
274
+ expect(consoleWarnSpy).not.toHaveBeenCalled();
275
+ });
276
+
277
+ it("uses console warnings for unsupported optional Intl helpers when no diagnostic handler exists", function () {
278
+ const originalDisplayNames = (Intl as any).DisplayNames;
279
+ const m = createMessagevisor({ datafile, logLevel: "warn" });
280
+
281
+ try {
282
+ (Intl as any).DisplayNames = undefined;
283
+ consoleWarnSpy.mockClear();
284
+
285
+ expect(m.formatDisplayName("NL", { type: "region" })).toEqual("NL");
286
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
287
+ "[Messagevisor]",
288
+ "Intl.DisplayNames is not available in this environment.",
289
+ expect.objectContaining({ code: "unsupported_formatter", level: "warn", locale: "en-US" }),
290
+ );
291
+ } finally {
292
+ (Intl as any).DisplayNames = originalDisplayNames;
293
+ }
294
+ });
295
+ });
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./conditions";
2
+ export * from "./instance";