@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.
- package/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/README.md +7 -0
- package/jest.config.js +8 -0
- package/lib/conditions.d.ts +10 -0
- package/lib/conditions.js +163 -0
- package/lib/conditions.js.map +1 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +19 -0
- package/lib/index.js.map +1 -0
- package/lib/instance.d.ts +218 -0
- package/lib/instance.js +1000 -0
- package/lib/instance.js.map +1 -0
- package/package.json +42 -13
- package/src/conditions.spec.ts +373 -0
- package/src/conditions.ts +207 -0
- package/src/conformance.spec.ts +553 -0
- package/src/evaluation-edges.spec.ts +294 -0
- package/src/formatters.spec.ts +295 -0
- package/src/index.ts +2 -0
- package/src/instance.spec.ts +2493 -0
- package/src/instance.ts +1615 -0
- package/src/lifecycle.spec.ts +268 -0
- package/tsconfig.cjs.json +14 -0
- package/tsconfig.typecheck.json +4 -0
|
@@ -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