@messagevisor/react-intl-compat 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,23 @@
1
+ import * as React from "react";
2
+
3
+ import { MessagevisorContext, useMessagevisorSnapshot } from "@messagevisor/react";
4
+
5
+ import type { IntlShape } from "./intl";
6
+ import { createIntlFromMessagevisor } from "./intl";
7
+
8
+ export function useIntl() {
9
+ const context = React.useContext(MessagevisorContext);
10
+
11
+ if (!context) {
12
+ throw new Error("useIntl must be used within MessagevisorProvider.");
13
+ }
14
+
15
+ const snapshot = useMessagevisorSnapshot();
16
+
17
+ return React.useMemo(
18
+ function getIntlShape(): IntlShape {
19
+ return createIntlFromMessagevisor(context.instance);
20
+ },
21
+ [context.instance, snapshot.version],
22
+ );
23
+ }
@@ -0,0 +1,469 @@
1
+ import * as React from "react";
2
+ import { fireEvent, render, screen } from "@testing-library/react";
3
+ import "@testing-library/jest-dom";
4
+
5
+ import type { DatafileContent } from "@messagevisor/types";
6
+ import { createMessagevisor } from "@messagevisor/sdk";
7
+ import { createICUModule } from "@messagevisor/module-icu";
8
+ import { MessagevisorProvider } from "@messagevisor/react";
9
+
10
+ import {
11
+ defineMessage,
12
+ defineMessages,
13
+ FormattedDate,
14
+ FormattedList,
15
+ FormattedMessage,
16
+ FormattedNumber,
17
+ FormattedNumberParts,
18
+ FormattedPlural,
19
+ injectIntl,
20
+ useIntl,
21
+ } from "./index";
22
+
23
+ const datafile: DatafileContent = {
24
+ schemaVersion: "1",
25
+ messagevisorVersion: "0.0.1",
26
+ revision: "1",
27
+ target: "web",
28
+ locale: "en-US",
29
+ formats: {
30
+ number: {
31
+ money: { style: "currency", currency: "USD", currencyDisplay: "symbol" },
32
+ },
33
+ date: {
34
+ short: { year: "numeric", month: "short", day: "numeric", timeZone: "UTC" },
35
+ },
36
+ },
37
+ segments: {
38
+ web: {
39
+ conditions: [{ attribute: "platform", operator: "equals", value: "web" }],
40
+ },
41
+ },
42
+ messages: {
43
+ greeting: {
44
+ overrides: [
45
+ {
46
+ key: "web",
47
+ segments: "web",
48
+ translation: "Hello web {name}",
49
+ },
50
+ ],
51
+ },
52
+ rich: {},
53
+ },
54
+ translations: {
55
+ greeting: "Hello {name}",
56
+ rich: "Read <link>terms</link>.",
57
+ },
58
+ };
59
+
60
+ const nlDatafile: DatafileContent = {
61
+ ...datafile,
62
+ revision: "2",
63
+ locale: "nl-NL",
64
+ formats: {
65
+ number: {
66
+ money: { style: "currency", currency: "EUR", currencyDisplay: "symbol" },
67
+ },
68
+ date: {
69
+ short: { year: "numeric", month: "short", day: "numeric", timeZone: "UTC" },
70
+ },
71
+ },
72
+ translations: {
73
+ greeting: "Hallo {name}",
74
+ rich: "Lees <link>voorwaarden</link>.",
75
+ },
76
+ };
77
+
78
+ describe("@messagevisor/react-intl-compat", function () {
79
+ it("supports bridge mode with locale-keyed catalogs and useIntl", function () {
80
+ function Example() {
81
+ const intl = useIntl();
82
+
83
+ return (
84
+ <div>
85
+ <span>
86
+ {intl.formatMessage({ id: "greeting", defaultMessage: "Hi {name}" }, { name: "Ada" })}
87
+ </span>
88
+ <span>{intl.formatNumber(12)}</span>
89
+ </div>
90
+ );
91
+ }
92
+
93
+ const instance = createMessagevisor({
94
+ locale: "en-US",
95
+ defaultTranslations: {
96
+ "en-US": {
97
+ greeting: "Hello {name}",
98
+ },
99
+ },
100
+ modules: [createICUModule()],
101
+ });
102
+
103
+ render(
104
+ <MessagevisorProvider instance={instance}>
105
+ <Example />
106
+ </MessagevisorProvider>,
107
+ );
108
+
109
+ expect(screen.getByText("Hello Ada")).toBeInTheDocument();
110
+ expect(screen.getByText("12")).toBeInTheDocument();
111
+ });
112
+
113
+ it("maps descriptor defaultMessage to SDK defaultTranslation", function () {
114
+ function Example() {
115
+ const intl = useIntl();
116
+
117
+ return (
118
+ <span>
119
+ {intl.formatMessage(
120
+ { id: "missing.greeting", defaultMessage: "Hi {name}" },
121
+ { name: "Ada" },
122
+ )}
123
+ </span>
124
+ );
125
+ }
126
+
127
+ const instance = createMessagevisor({
128
+ locale: "en-US",
129
+ modules: [createICUModule()],
130
+ });
131
+
132
+ render(
133
+ <MessagevisorProvider instance={instance}>
134
+ <Example />
135
+ </MessagevisorProvider>,
136
+ );
137
+
138
+ expect(screen.getByText("Hi Ada")).toBeInTheDocument();
139
+ });
140
+
141
+ it("maps empty defaultMessage through defaultTranslation without falling back to the message key", function () {
142
+ function Example() {
143
+ const intl = useIntl();
144
+
145
+ return (
146
+ <span data-testid="empty-default">
147
+ {intl.formatMessage({ id: "missing.empty", defaultMessage: "" })}
148
+ </span>
149
+ );
150
+ }
151
+
152
+ const instance = createMessagevisor({
153
+ locale: "en-US",
154
+ modules: [createICUModule()],
155
+ });
156
+
157
+ render(
158
+ <MessagevisorProvider instance={instance}>
159
+ <Example />
160
+ </MessagevisorProvider>,
161
+ );
162
+
163
+ expect(screen.getByTestId("empty-default")).toBeEmptyDOMElement();
164
+ });
165
+
166
+ it("uses defaultMessage for FormattedMessage when the id is missing, including rich text", function () {
167
+ render(
168
+ <MessagevisorProvider
169
+ instance={createMessagevisor({
170
+ datafile,
171
+ modules: [createICUModule({ ignoreTags: false })],
172
+ })}
173
+ textComponent="span"
174
+ defaultRichTextElements={{
175
+ link: function link(chunks) {
176
+ return <a href="/terms">{chunks}</a>;
177
+ },
178
+ }}
179
+ >
180
+ <FormattedMessage id="missing.rich" defaultMessage="Read <link>terms</link>." />
181
+ </MessagevisorProvider>,
182
+ );
183
+
184
+ expect(screen.getByRole("link", { name: "terms" })).toHaveAttribute("href", "/terms");
185
+ });
186
+
187
+ it("supports datafile mode and preserves Messagevisor overrides", function () {
188
+ render(
189
+ <MessagevisorProvider
190
+ instance={createMessagevisor({ datafile, modules: [createICUModule()] })}
191
+ >
192
+ <FormattedMessage id="greeting" values={{ name: "Ada" }} />
193
+ </MessagevisorProvider>,
194
+ );
195
+
196
+ expect(screen.getByText("Hello Ada")).toBeInTheDocument();
197
+
198
+ const instance = createMessagevisor({
199
+ datafile,
200
+ context: { platform: "web" },
201
+ modules: [createICUModule()],
202
+ });
203
+
204
+ render(
205
+ <MessagevisorProvider instance={instance}>
206
+ <FormattedMessage id="greeting" values={{ name: "Lin" }} />
207
+ </MessagevisorProvider>,
208
+ );
209
+
210
+ expect(screen.getByText("Hello web Lin")).toBeInTheDocument();
211
+ });
212
+
213
+ it("supports rich text defaults and textComponent wrapping", function () {
214
+ const instance = createMessagevisor({
215
+ datafile,
216
+ modules: [createICUModule({ ignoreTags: false })],
217
+ });
218
+
219
+ render(
220
+ <MessagevisorProvider
221
+ instance={instance}
222
+ textComponent="span"
223
+ defaultRichTextElements={{
224
+ link: function link(chunks) {
225
+ return <a href="/terms">{chunks}</a>;
226
+ },
227
+ }}
228
+ >
229
+ <FormattedMessage id="rich" />
230
+ </MessagevisorProvider>,
231
+ );
232
+
233
+ expect(screen.getByRole("link", { name: "terms" })).toHaveAttribute("href", "/terms");
234
+ });
235
+
236
+ it("exports defineMessage helpers and imperative components", function () {
237
+ const message = defineMessage({ id: "hello", defaultMessage: "Hello" });
238
+ const messages = defineMessages({
239
+ count: { id: "count", defaultMessage: "{count, number}" },
240
+ });
241
+
242
+ expect(message.id).toEqual("hello");
243
+ expect(messages.count.id).toEqual("count");
244
+
245
+ render(
246
+ <MessagevisorProvider
247
+ instance={createMessagevisor({
248
+ locale: "en-US",
249
+ defaultTranslations: {
250
+ "en-US": {
251
+ hello: "Hello",
252
+ count: "{count, number}",
253
+ },
254
+ },
255
+ modules: [createICUModule()],
256
+ })}
257
+ >
258
+ <FormattedNumber value={12} />
259
+ <FormattedDate
260
+ value={new Date("2025-01-01T12:00:00Z")}
261
+ format={{ year: "numeric", timeZone: "UTC" }}
262
+ />
263
+ <FormattedList value={["A", "B"]} />
264
+ <FormattedPlural value={1} one="item" other="items" />
265
+ <FormattedNumberParts value={12}>
266
+ {(parts) => <span>{parts[0].value}</span>}
267
+ </FormattedNumberParts>
268
+ </MessagevisorProvider>,
269
+ );
270
+
271
+ expect(screen.getByText("12")).toBeInTheDocument();
272
+ expect(document.body.textContent).toContain("A and B");
273
+ expect(document.body.textContent).toContain("item");
274
+ });
275
+
276
+ it("supports injectIntl", function () {
277
+ const Base = injectIntl(function Base(props: { intl: ReturnType<typeof useIntl> }) {
278
+ return <span>{props.intl.formatMessage({ id: "greeting" }, { name: "Ada" })}</span>;
279
+ });
280
+
281
+ render(
282
+ <MessagevisorProvider
283
+ instance={createMessagevisor({
284
+ locale: "en-US",
285
+ defaultTranslations: {
286
+ "en-US": {
287
+ greeting: "Hello {name}",
288
+ },
289
+ },
290
+ modules: [createICUModule()],
291
+ })}
292
+ >
293
+ <Base />
294
+ </MessagevisorProvider>,
295
+ );
296
+
297
+ expect(screen.getByText("Hello Ada")).toBeInTheDocument();
298
+ });
299
+
300
+ it("updates useIntl locale and formatted messages after SDK locale changes", function () {
301
+ function Example() {
302
+ const intl = useIntl();
303
+
304
+ return (
305
+ <section>
306
+ <p>{intl.locale}</p>
307
+ <p>{intl.formatMessage({ id: "greeting" }, { name: "Ada" })}</p>
308
+ <button
309
+ onClick={() => {
310
+ intl.messagevisor.setDatafile(nlDatafile);
311
+ intl.messagevisor.setLocale("nl-NL");
312
+ }}
313
+ >
314
+ switch
315
+ </button>
316
+ </section>
317
+ );
318
+ }
319
+
320
+ render(
321
+ <MessagevisorProvider
322
+ instance={createMessagevisor({ datafile, modules: [createICUModule()] })}
323
+ >
324
+ <Example />
325
+ </MessagevisorProvider>,
326
+ );
327
+
328
+ expect(screen.getByText("en-US")).toBeInTheDocument();
329
+ expect(screen.getByText("Hello Ada")).toBeInTheDocument();
330
+
331
+ fireEvent.click(screen.getByRole("button", { name: "switch" }));
332
+
333
+ expect(screen.getByText("nl-NL")).toBeInTheDocument();
334
+ expect(screen.getByText("Hallo Ada")).toBeInTheDocument();
335
+ });
336
+
337
+ it("updates FormattedMessage after SDK locale changes", function () {
338
+ function Example() {
339
+ const intl = useIntl();
340
+
341
+ return (
342
+ <section>
343
+ <FormattedMessage id="greeting" values={{ name: "Ada" }} />
344
+ <button
345
+ onClick={() => {
346
+ intl.messagevisor.setDatafile(nlDatafile);
347
+ intl.messagevisor.setLocale("nl-NL");
348
+ }}
349
+ >
350
+ switch
351
+ </button>
352
+ </section>
353
+ );
354
+ }
355
+
356
+ render(
357
+ <MessagevisorProvider
358
+ instance={createMessagevisor({ datafile, modules: [createICUModule()] })}
359
+ >
360
+ <Example />
361
+ </MessagevisorProvider>,
362
+ );
363
+
364
+ expect(screen.getByText("Hello Ada")).toBeInTheDocument();
365
+
366
+ fireEvent.click(screen.getByRole("button", { name: "switch" }));
367
+
368
+ expect(screen.getByText("Hallo Ada")).toBeInTheDocument();
369
+ });
370
+
371
+ it("updates formatter components after SDK locale changes", function () {
372
+ function Example() {
373
+ const intl = useIntl();
374
+
375
+ return (
376
+ <section>
377
+ <span data-testid="number">
378
+ <FormattedNumber value={1234.5} />
379
+ </span>
380
+ <span data-testid="currency">{intl.formatNumber(12, "money")}</span>
381
+ <button
382
+ onClick={() => {
383
+ intl.messagevisor.setDatafile(nlDatafile);
384
+ intl.messagevisor.setLocale("nl-NL");
385
+ }}
386
+ >
387
+ switch
388
+ </button>
389
+ </section>
390
+ );
391
+ }
392
+
393
+ render(
394
+ <MessagevisorProvider
395
+ instance={createMessagevisor({ datafile, modules: [createICUModule()] })}
396
+ >
397
+ <Example />
398
+ </MessagevisorProvider>,
399
+ );
400
+
401
+ const initialNumber = screen.getByTestId("number").textContent;
402
+ expect(screen.getByTestId("currency")).toHaveTextContent("$12.00");
403
+
404
+ fireEvent.click(screen.getByRole("button", { name: "switch" }));
405
+
406
+ expect(screen.getByTestId("number").textContent).not.toEqual(initialNumber);
407
+ expect(screen.getByTestId("currency")).toHaveTextContent("€");
408
+ expect(screen.getByTestId("currency")).not.toHaveTextContent("$12.00");
409
+ });
410
+
411
+ it("updates injected intl props after SDK locale changes", function () {
412
+ const Base = injectIntl(function Base(props: { intl: ReturnType<typeof useIntl> }) {
413
+ return <span>{props.intl.formatMessage({ id: "greeting" }, { name: "Ada" })}</span>;
414
+ });
415
+
416
+ function Example() {
417
+ const intl = useIntl();
418
+
419
+ return (
420
+ <section>
421
+ <Base />
422
+ <button
423
+ onClick={() => {
424
+ intl.messagevisor.setDatafile(nlDatafile);
425
+ intl.messagevisor.setLocale("nl-NL");
426
+ }}
427
+ >
428
+ switch
429
+ </button>
430
+ </section>
431
+ );
432
+ }
433
+
434
+ render(
435
+ <MessagevisorProvider
436
+ instance={createMessagevisor({ datafile, modules: [createICUModule()] })}
437
+ >
438
+ <Example />
439
+ </MessagevisorProvider>,
440
+ );
441
+
442
+ expect(screen.getByText("Hello Ada")).toBeInTheDocument();
443
+
444
+ fireEvent.click(screen.getByRole("button", { name: "switch" }));
445
+
446
+ expect(screen.getByText("Hallo Ada")).toBeInTheDocument();
447
+ });
448
+
449
+ it("fails clearly when ICU formatting is requested without the ICU module", function () {
450
+ expect(() =>
451
+ render(
452
+ <MessagevisorProvider
453
+ instance={createMessagevisor({
454
+ locale: "en-US",
455
+ defaultTranslations: {
456
+ "en-US": {
457
+ greeting: "Hello {name}",
458
+ },
459
+ },
460
+ })}
461
+ >
462
+ <FormattedMessage id="greeting" values={{ name: "Ada" }} />
463
+ </MessagevisorProvider>,
464
+ ),
465
+ ).toThrow(
466
+ "Message formatting requires a Messagevisor instance configured with createICUModule().",
467
+ );
468
+ });
469
+ });