@alepha/react 0.14.1 → 0.14.3
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/dist/auth/index.browser.js +1488 -4
- package/dist/auth/index.browser.js.map +1 -1
- package/dist/auth/index.d.ts +2 -2
- package/dist/auth/index.js +1827 -4
- package/dist/auth/index.js.map +1 -1
- package/dist/core/index.d.ts +58 -937
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +139 -2014
- package/dist/core/index.js.map +1 -1
- package/dist/form/index.d.ts.map +1 -1
- package/dist/form/index.js +6 -1
- package/dist/form/index.js.map +1 -1
- package/dist/head/index.browser.js +3 -1
- package/dist/head/index.browser.js.map +1 -1
- package/dist/head/index.d.ts +552 -8
- package/dist/head/index.d.ts.map +1 -1
- package/dist/head/index.js +17 -2
- package/dist/head/index.js.map +1 -1
- package/dist/{core → router}/index.browser.js +126 -516
- package/dist/router/index.browser.js.map +1 -0
- package/dist/router/index.d.ts +1334 -0
- package/dist/router/index.d.ts.map +1 -0
- package/dist/router/index.js +1939 -0
- package/dist/router/index.js.map +1 -0
- package/package.json +12 -6
- package/src/auth/__tests__/$auth.spec.ts +188 -0
- package/src/auth/index.ts +1 -1
- package/src/auth/services/ReactAuth.ts +1 -1
- package/src/core/__tests__/Router.spec.tsx +169 -0
- package/src/core/components/ClientOnly.tsx +14 -0
- package/src/core/components/ErrorBoundary.tsx +3 -2
- package/src/core/contexts/AlephaContext.ts +3 -0
- package/src/core/contexts/AlephaProvider.tsx +2 -1
- package/src/core/hooks/useAction.browser.spec.tsx +569 -0
- package/src/core/hooks/useAction.ts +11 -0
- package/src/core/index.ts +13 -102
- package/src/form/hooks/useForm.browser.spec.tsx +366 -0
- package/src/form/services/FormModel.ts +5 -0
- package/src/head/__tests__/expandSeo.spec.ts +203 -0
- package/src/head/__tests__/page-head.spec.ts +39 -0
- package/src/head/__tests__/seo-head.spec.ts +121 -0
- package/src/head/hooks/useHead.spec.tsx +288 -0
- package/src/head/index.ts +18 -8
- package/src/head/interfaces/Head.ts +3 -0
- package/src/head/providers/BrowserHeadProvider.browser.spec.ts +271 -0
- package/src/head/providers/HeadProvider.ts +6 -1
- package/src/head/providers/ServerHeadProvider.spec.ts +163 -0
- package/src/head/providers/ServerHeadProvider.ts +20 -0
- package/src/i18n/__tests__/integration.spec.tsx +239 -0
- package/src/i18n/components/Localize.spec.tsx +357 -0
- package/src/i18n/hooks/useI18n.browser.spec.tsx +438 -0
- package/src/i18n/providers/I18nProvider.spec.ts +389 -0
- package/src/{core → router}/components/ErrorViewer.tsx +2 -0
- package/src/router/components/Link.tsx +21 -0
- package/src/{core → router}/components/NestedView.tsx +3 -5
- package/src/router/components/NotFound.tsx +30 -0
- package/src/router/errors/Redirection.ts +28 -0
- package/src/{core → router}/hooks/useActive.ts +6 -2
- package/src/{core → router}/hooks/useQueryParams.ts +2 -2
- package/src/{core → router}/hooks/useRouter.ts +1 -1
- package/src/{core → router}/hooks/useRouterState.ts +1 -1
- package/src/{core → router}/index.browser.ts +14 -12
- package/src/{core/index.shared-router.ts → router/index.shared.ts} +6 -3
- package/src/router/index.ts +125 -0
- package/src/router/primitives/$page.browser.spec.tsx +702 -0
- package/src/router/primitives/$page.spec.tsx +702 -0
- package/src/{core → router}/primitives/$page.ts +1 -1
- package/src/{core → router}/providers/ReactBrowserProvider.ts +3 -13
- package/src/{core → router}/providers/ReactBrowserRendererProvider.ts +3 -0
- package/src/{core → router}/providers/ReactBrowserRouterProvider.ts +3 -0
- package/src/{core → router}/providers/ReactPageProvider.ts +5 -3
- package/src/router/providers/ReactServerProvider.spec.tsx +316 -0
- package/src/{core → router}/providers/ReactServerProvider.ts +12 -30
- package/src/{core → router}/services/ReactPageServerService.ts +3 -0
- package/src/{core → router}/services/ReactPageService.ts +5 -5
- package/src/{core → router}/services/ReactRouter.ts +26 -5
- package/dist/core/index.browser.js.map +0 -1
- package/dist/core/index.native.js +0 -403
- package/dist/core/index.native.js.map +0 -1
- package/src/core/components/Link.tsx +0 -18
- package/src/core/components/NotFound.tsx +0 -27
- package/src/core/errors/Redirection.ts +0 -13
- package/src/core/hooks/useSchema.ts +0 -88
- package/src/core/index.native.ts +0 -21
- package/src/core/index.shared.ts +0 -9
- /package/src/{core → router}/contexts/RouterLayerContext.ts +0 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { Alepha } from "alepha";
|
|
2
|
+
import { describe, test } from "vitest";
|
|
3
|
+
import { $dictionary } from "../primitives/$dictionary.ts";
|
|
4
|
+
import { AlephaReactI18n } from "../index.ts";
|
|
5
|
+
import { I18nProvider } from "../providers/I18nProvider.ts";
|
|
6
|
+
|
|
7
|
+
describe("I18n Integration Tests", () => {
|
|
8
|
+
test("should lazy load dictionaries on server start", async ({ expect }) => {
|
|
9
|
+
let enLoaded = false;
|
|
10
|
+
let frLoaded = false;
|
|
11
|
+
|
|
12
|
+
class App {
|
|
13
|
+
en = $dictionary({
|
|
14
|
+
lazy: async () => {
|
|
15
|
+
enLoaded = true;
|
|
16
|
+
return { default: { hello: "Hello" } };
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
fr = $dictionary({
|
|
21
|
+
lazy: async () => {
|
|
22
|
+
frLoaded = true;
|
|
23
|
+
return { default: { hello: "Bonjour" } };
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const alepha = Alepha.create().with(AlephaReactI18n);
|
|
29
|
+
const app = alepha.inject(App);
|
|
30
|
+
|
|
31
|
+
expect(enLoaded).toBe(false);
|
|
32
|
+
expect(frLoaded).toBe(false);
|
|
33
|
+
|
|
34
|
+
await alepha.start();
|
|
35
|
+
|
|
36
|
+
expect(enLoaded).toBe(true);
|
|
37
|
+
expect(frLoaded).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("should share I18nProvider instance across application", async ({
|
|
41
|
+
expect,
|
|
42
|
+
}) => {
|
|
43
|
+
class App {
|
|
44
|
+
en = $dictionary({
|
|
45
|
+
lazy: async () => ({ default: { key: "value" } }),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const alepha = Alepha.create().with(AlephaReactI18n);
|
|
50
|
+
const app = alepha.inject(App);
|
|
51
|
+
|
|
52
|
+
const i18n1 = alepha.inject(I18nProvider);
|
|
53
|
+
const i18n2 = alepha.inject(I18nProvider);
|
|
54
|
+
|
|
55
|
+
expect(i18n1).toBe(i18n2);
|
|
56
|
+
expect(i18n1.registry).toBe(i18n2.registry);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("should support multiple dictionary registrations", async ({
|
|
60
|
+
expect,
|
|
61
|
+
}) => {
|
|
62
|
+
class App {
|
|
63
|
+
en = $dictionary({
|
|
64
|
+
lazy: async () => ({
|
|
65
|
+
default: {
|
|
66
|
+
"app.title": "My Application",
|
|
67
|
+
"app.subtitle": "Built with Alepha",
|
|
68
|
+
"auth.login": "Log In",
|
|
69
|
+
"auth.logout": "Log Out",
|
|
70
|
+
},
|
|
71
|
+
}),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
fr = $dictionary({
|
|
75
|
+
lazy: async () => ({
|
|
76
|
+
default: {
|
|
77
|
+
"app.title": "Mon Application",
|
|
78
|
+
"app.subtitle": "Construit avec Alepha",
|
|
79
|
+
"auth.login": "Se connecter",
|
|
80
|
+
"auth.logout": "Se déconnecter",
|
|
81
|
+
},
|
|
82
|
+
}),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const alepha = Alepha.create().with(AlephaReactI18n);
|
|
87
|
+
const app = alepha.inject(App);
|
|
88
|
+
const i18n = alepha.inject(I18nProvider);
|
|
89
|
+
|
|
90
|
+
await alepha.start();
|
|
91
|
+
|
|
92
|
+
// Test English
|
|
93
|
+
expect(i18n.tr("app.title")).toBe("My Application");
|
|
94
|
+
expect(i18n.tr("app.subtitle")).toBe("Built with Alepha");
|
|
95
|
+
expect(i18n.tr("auth.login")).toBe("Log In");
|
|
96
|
+
expect(i18n.tr("auth.logout")).toBe("Log Out");
|
|
97
|
+
|
|
98
|
+
// Switch to French
|
|
99
|
+
await i18n.setLang("fr");
|
|
100
|
+
|
|
101
|
+
expect(i18n.tr("app.title")).toBe("Mon Application");
|
|
102
|
+
expect(i18n.tr("app.subtitle")).toBe("Construit avec Alepha");
|
|
103
|
+
expect(i18n.tr("auth.login")).toBe("Se connecter");
|
|
104
|
+
expect(i18n.tr("auth.logout")).toBe("Se déconnecter");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("should handle translation with complex interpolation across languages", async ({
|
|
108
|
+
expect,
|
|
109
|
+
}) => {
|
|
110
|
+
class App {
|
|
111
|
+
en = $dictionary({
|
|
112
|
+
lazy: async () => ({
|
|
113
|
+
default: {
|
|
114
|
+
notification: "$1 sent you $2 message(s) about $3",
|
|
115
|
+
},
|
|
116
|
+
}),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
fr = $dictionary({
|
|
120
|
+
lazy: async () => ({
|
|
121
|
+
default: {
|
|
122
|
+
notification: "$1 vous a envoyé $2 message(s) à propos de $3",
|
|
123
|
+
},
|
|
124
|
+
}),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
es = $dictionary({
|
|
128
|
+
lazy: async () => ({
|
|
129
|
+
default: {
|
|
130
|
+
notification: "$1 te envió $2 mensaje(s) sobre $3",
|
|
131
|
+
},
|
|
132
|
+
}),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const alepha = Alepha.create().with(AlephaReactI18n);
|
|
137
|
+
const app = alepha.inject(App);
|
|
138
|
+
const i18n = alepha.inject(I18nProvider);
|
|
139
|
+
|
|
140
|
+
await alepha.start();
|
|
141
|
+
|
|
142
|
+
const args = ["John", "3", "your project"];
|
|
143
|
+
|
|
144
|
+
expect(i18n.tr("notification", { args })).toBe(
|
|
145
|
+
"John sent you 3 message(s) about your project",
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
await i18n.setLang("fr");
|
|
149
|
+
expect(i18n.tr("notification", { args })).toBe(
|
|
150
|
+
"John vous a envoyé 3 message(s) à propos de your project",
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
await i18n.setLang("es");
|
|
154
|
+
expect(i18n.tr("notification", { args })).toBe(
|
|
155
|
+
"John te envió 3 mensaje(s) sobre your project",
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("should properly cleanup on stop", async ({ expect }) => {
|
|
160
|
+
class App {
|
|
161
|
+
en = $dictionary({
|
|
162
|
+
lazy: async () => ({ default: { test: "Test" } }),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const alepha = Alepha.create().with(AlephaReactI18n);
|
|
167
|
+
const app = alepha.inject(App);
|
|
168
|
+
const i18n = alepha.inject(I18nProvider);
|
|
169
|
+
|
|
170
|
+
await alepha.start();
|
|
171
|
+
expect(i18n.registry[0].translations).toEqual({ test: "Test" });
|
|
172
|
+
|
|
173
|
+
await alepha.stop();
|
|
174
|
+
// Registry should still exist after stop
|
|
175
|
+
expect(i18n.registry).toBeDefined();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("should work with empty language fallback chain", async ({ expect }) => {
|
|
179
|
+
class App {
|
|
180
|
+
en = $dictionary({
|
|
181
|
+
lazy: async () => ({ default: { only_en: "Only in English" } }),
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
fr = $dictionary({
|
|
185
|
+
lazy: async () => ({ default: { only_fr: "Seulement en français" } }),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const alepha = Alepha.create().with(AlephaReactI18n);
|
|
190
|
+
const app = alepha.inject(App);
|
|
191
|
+
const i18n = alepha.inject(I18nProvider);
|
|
192
|
+
|
|
193
|
+
await alepha.start();
|
|
194
|
+
|
|
195
|
+
// In English, can't find French-only key
|
|
196
|
+
expect(i18n.tr("only_fr")).toBe("only_fr");
|
|
197
|
+
expect(i18n.tr("only_en")).toBe("Only in English");
|
|
198
|
+
|
|
199
|
+
// In French, falls back to English for English-only key
|
|
200
|
+
await i18n.setLang("fr");
|
|
201
|
+
expect(i18n.tr("only_fr")).toBe("Seulement en français");
|
|
202
|
+
expect(i18n.tr("only_en")).toBe("Only in English");
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("should handle rapid language switches", async ({ expect }) => {
|
|
206
|
+
class App {
|
|
207
|
+
en = $dictionary({
|
|
208
|
+
lazy: async () => ({ default: { status: "Active" } }),
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
fr = $dictionary({
|
|
212
|
+
lazy: async () => ({ default: { status: "Actif" } }),
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
de = $dictionary({
|
|
216
|
+
lazy: async () => ({ default: { status: "Aktiv" } }),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const alepha = Alepha.create().with(AlephaReactI18n);
|
|
221
|
+
const app = alepha.inject(App);
|
|
222
|
+
const i18n = alepha.inject(I18nProvider);
|
|
223
|
+
|
|
224
|
+
await alepha.start();
|
|
225
|
+
|
|
226
|
+
// Rapidly switch languages
|
|
227
|
+
await i18n.setLang("fr");
|
|
228
|
+
expect(i18n.tr("status")).toBe("Actif");
|
|
229
|
+
|
|
230
|
+
await i18n.setLang("de");
|
|
231
|
+
expect(i18n.tr("status")).toBe("Aktiv");
|
|
232
|
+
|
|
233
|
+
await i18n.setLang("en");
|
|
234
|
+
expect(i18n.tr("status")).toBe("Active");
|
|
235
|
+
|
|
236
|
+
await i18n.setLang("fr");
|
|
237
|
+
expect(i18n.tr("status")).toBe("Actif");
|
|
238
|
+
});
|
|
239
|
+
});
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { AlephaContext } from "@alepha/react";
|
|
2
|
+
import { I18nProvider, Localize } from "../index.ts";
|
|
3
|
+
import { Alepha, t } from "alepha";
|
|
4
|
+
import { DateTimeProvider } from "alepha/datetime";
|
|
5
|
+
import type { ReactNode } from "react";
|
|
6
|
+
import { renderToString } from "react-dom/server";
|
|
7
|
+
import { describe, expect, it } from "vitest";
|
|
8
|
+
|
|
9
|
+
describe("<Localize/>", () => {
|
|
10
|
+
const setup = () => {
|
|
11
|
+
const alepha = Alepha.create();
|
|
12
|
+
return {
|
|
13
|
+
i18n: alepha.inject(I18nProvider),
|
|
14
|
+
dateTime: alepha.inject(DateTimeProvider),
|
|
15
|
+
alepha,
|
|
16
|
+
render: (children: ReactNode) =>
|
|
17
|
+
renderToString(
|
|
18
|
+
<AlephaContext value={alepha}>{children}</AlephaContext>,
|
|
19
|
+
),
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
it("should format date", () => {
|
|
24
|
+
const { render, i18n } = setup();
|
|
25
|
+
const date = new Date("2024-09-23T23:45:00Z");
|
|
26
|
+
expect(render(<Localize value={date} />)).toBe("9/24/2024");
|
|
27
|
+
i18n.setLang("fr");
|
|
28
|
+
expect(render(<Localize value={date} />)).toBe("24/09/2024");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should format number", () => {
|
|
32
|
+
const { render, i18n } = setup();
|
|
33
|
+
const number = 1234567.89;
|
|
34
|
+
expect(render(<Localize value={number} />)).toBe("1,234,567.89");
|
|
35
|
+
i18n.setLang("de");
|
|
36
|
+
expect(render(<Localize value={number} />)).toBe("1.234.567,89");
|
|
37
|
+
i18n.setLang("fr");
|
|
38
|
+
expect(render(<Localize value={number} />)).toBe("1\u202f234\u202f567,89");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// it("should format typebox error", async () => {
|
|
42
|
+
// const { render, i18n, alepha } = setup();
|
|
43
|
+
// const boom = async () => alepha.codec.decode(t.number(), "..");
|
|
44
|
+
// const error = await boom().catch((err) => err);
|
|
45
|
+
// expect(render(<Localize value={error} />)).toBe("must be number");
|
|
46
|
+
// await i18n.setLang("fr");
|
|
47
|
+
// expect(render(<Localize value={error} />)).toBe("doit être number");
|
|
48
|
+
// });
|
|
49
|
+
|
|
50
|
+
describe("number formatting options", () => {
|
|
51
|
+
it("should format currency", () => {
|
|
52
|
+
const { render } = setup();
|
|
53
|
+
const number = 1234.56;
|
|
54
|
+
expect(
|
|
55
|
+
render(
|
|
56
|
+
<Localize
|
|
57
|
+
value={number}
|
|
58
|
+
number={{ style: "currency", currency: "USD" }}
|
|
59
|
+
/>,
|
|
60
|
+
),
|
|
61
|
+
).toBe("$1,234.56");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("should format currency with locale", () => {
|
|
65
|
+
const { render, i18n } = setup();
|
|
66
|
+
const number = 1234.56;
|
|
67
|
+
i18n.setLang("fr");
|
|
68
|
+
expect(
|
|
69
|
+
render(
|
|
70
|
+
<Localize
|
|
71
|
+
value={number}
|
|
72
|
+
number={{ style: "currency", currency: "EUR" }}
|
|
73
|
+
/>,
|
|
74
|
+
),
|
|
75
|
+
).toBe("1\u202f234,56\u00a0€");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should format percentage", () => {
|
|
79
|
+
const { render } = setup();
|
|
80
|
+
const number = 0.1234;
|
|
81
|
+
expect(
|
|
82
|
+
render(<Localize value={number} number={{ style: "percent" }} />),
|
|
83
|
+
).toBe("12%");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("should format with minimum fraction digits", () => {
|
|
87
|
+
const { render } = setup();
|
|
88
|
+
const number = 10;
|
|
89
|
+
expect(
|
|
90
|
+
render(
|
|
91
|
+
<Localize value={number} number={{ minimumFractionDigits: 2 }} />,
|
|
92
|
+
),
|
|
93
|
+
).toBe("10.00");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("should format with maximum fraction digits", () => {
|
|
97
|
+
const { render } = setup();
|
|
98
|
+
const number = 10.123456;
|
|
99
|
+
expect(
|
|
100
|
+
render(
|
|
101
|
+
<Localize value={number} number={{ maximumFractionDigits: 2 }} />,
|
|
102
|
+
),
|
|
103
|
+
).toBe("10.12");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("should format with notation", () => {
|
|
107
|
+
const { render } = setup();
|
|
108
|
+
const number = 1000000;
|
|
109
|
+
expect(
|
|
110
|
+
render(<Localize value={number} number={{ notation: "compact" }} />),
|
|
111
|
+
).toBe("1M");
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("date formatting options", () => {
|
|
116
|
+
it("should format with dayjs format string - LLL", () => {
|
|
117
|
+
const { render, i18n, dateTime } = setup();
|
|
118
|
+
const date = dateTime.utc("2024-09-24T00:45:00Z").toDate();
|
|
119
|
+
const formatted = render(<Localize value={date} date="LLL" />);
|
|
120
|
+
// Format depends on local timezone, so just check it contains the key parts
|
|
121
|
+
expect(formatted).toContain("September");
|
|
122
|
+
expect(formatted).toContain("2024");
|
|
123
|
+
i18n.setLang("fr");
|
|
124
|
+
const frFormatted = render(<Localize value={date} date="LLL" />);
|
|
125
|
+
expect(frFormatted).toContain("septembre");
|
|
126
|
+
expect(frFormatted).toContain("2024");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("should format with dayjs format string - YYYY-MM-DD", () => {
|
|
130
|
+
const { render } = setup();
|
|
131
|
+
const date = new Date("2024-09-23T23:45:00Z");
|
|
132
|
+
expect(render(<Localize value={date} date="YYYY-MM-DD" />)).toBe(
|
|
133
|
+
"2024-09-24",
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("should format with dayjs format string - dddd, MMMM D YYYY", () => {
|
|
138
|
+
const { render } = setup();
|
|
139
|
+
const date = new Date("2024-09-23T23:45:00Z");
|
|
140
|
+
expect(render(<Localize value={date} date="dddd, MMMM D YYYY" />)).toBe(
|
|
141
|
+
"Tuesday, September 24 2024",
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("should format with fromNow for Date", () => {
|
|
146
|
+
const { render, dateTime } = setup();
|
|
147
|
+
dateTime.pause();
|
|
148
|
+
const date = dateTime.now().subtract(2, "hour").toDate();
|
|
149
|
+
const result = render(<Localize value={date} date="fromNow" />);
|
|
150
|
+
expect(result).toBe("2 hours ago");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("should format with fromNow for DateTime", () => {
|
|
154
|
+
const { render, dateTime } = setup();
|
|
155
|
+
dateTime.pause();
|
|
156
|
+
const date = dateTime.now().subtract(3, "day");
|
|
157
|
+
const result = render(<Localize value={date} date="fromNow" />);
|
|
158
|
+
expect(result).toBe("3 days ago");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("should format DateTime with dayjs format string", () => {
|
|
162
|
+
const { render, dateTime } = setup();
|
|
163
|
+
const date = dateTime.of("2024-09-23T23:45:00Z");
|
|
164
|
+
expect(render(<Localize value={date} date="YYYY-MM-DD" />)).toBe(
|
|
165
|
+
"2024-09-24",
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("should format with Intl.DateTimeFormatOptions", () => {
|
|
170
|
+
const { render } = setup();
|
|
171
|
+
const date = new Date("2024-09-23T23:45:00Z");
|
|
172
|
+
expect(
|
|
173
|
+
render(
|
|
174
|
+
<Localize
|
|
175
|
+
value={date}
|
|
176
|
+
date={{
|
|
177
|
+
weekday: "long",
|
|
178
|
+
year: "numeric",
|
|
179
|
+
month: "long",
|
|
180
|
+
day: "numeric",
|
|
181
|
+
}}
|
|
182
|
+
/>,
|
|
183
|
+
),
|
|
184
|
+
).toBe("Tuesday, September 24, 2024");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("should format with Intl.DateTimeFormatOptions and locale", () => {
|
|
188
|
+
const { render, i18n } = setup();
|
|
189
|
+
const date = new Date("2024-09-23T23:45:00Z");
|
|
190
|
+
i18n.setLang("fr");
|
|
191
|
+
expect(
|
|
192
|
+
render(
|
|
193
|
+
<Localize
|
|
194
|
+
value={date}
|
|
195
|
+
date={{
|
|
196
|
+
weekday: "long",
|
|
197
|
+
year: "numeric",
|
|
198
|
+
month: "long",
|
|
199
|
+
day: "numeric",
|
|
200
|
+
}}
|
|
201
|
+
/>,
|
|
202
|
+
),
|
|
203
|
+
).toBe("mardi 24 septembre 2024");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("should format DateTime with Intl.DateTimeFormatOptions", () => {
|
|
207
|
+
const { render, dateTime } = setup();
|
|
208
|
+
const date = dateTime.utc("2024-09-24T00:45:00Z");
|
|
209
|
+
const formatted = render(
|
|
210
|
+
<Localize
|
|
211
|
+
value={date}
|
|
212
|
+
date={{
|
|
213
|
+
hour: "2-digit",
|
|
214
|
+
minute: "2-digit",
|
|
215
|
+
hour12: true,
|
|
216
|
+
}}
|
|
217
|
+
/>,
|
|
218
|
+
);
|
|
219
|
+
// Format depends on local timezone, so just verify it contains time parts
|
|
220
|
+
expect(formatted).toMatch(/\d{1,2}:\d{2}\s(AM|PM)/);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe("string date parsing", () => {
|
|
225
|
+
it("should parse string date when dateOptions is provided", () => {
|
|
226
|
+
const { render } = setup();
|
|
227
|
+
const dateString = "2024-09-24T12:00:00Z";
|
|
228
|
+
expect(render(<Localize value={dateString} date="YYYY-MM-DD" />)).toBe(
|
|
229
|
+
"2024-09-24",
|
|
230
|
+
);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("should parse string date with format options", () => {
|
|
234
|
+
const { render } = setup();
|
|
235
|
+
const dateString = "2024-09-24T12:00:00Z";
|
|
236
|
+
expect(render(<Localize value={dateString} date="LLL" />)).toContain(
|
|
237
|
+
"September",
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("should parse string date with timezone", () => {
|
|
242
|
+
const { render } = setup();
|
|
243
|
+
const dateString = "2024-09-24T12:00:00Z";
|
|
244
|
+
expect(
|
|
245
|
+
render(
|
|
246
|
+
<Localize
|
|
247
|
+
value={dateString}
|
|
248
|
+
timezone="America/New_York"
|
|
249
|
+
date="YYYY-MM-DD HH:mm"
|
|
250
|
+
/>,
|
|
251
|
+
),
|
|
252
|
+
).toBe("2024-09-24 08:00");
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("should return string as-is when no dateOptions provided", () => {
|
|
256
|
+
const { render } = setup();
|
|
257
|
+
const dateString = "2024-09-24T12:00:00Z";
|
|
258
|
+
expect(render(<Localize value={dateString} />)).toBe(dateString);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe("timezone formatting", () => {
|
|
263
|
+
it("should format Date with timezone using Intl", () => {
|
|
264
|
+
const { render } = setup();
|
|
265
|
+
const date = new Date("2024-09-24T12:00:00Z");
|
|
266
|
+
expect(
|
|
267
|
+
render(<Localize value={date} timezone="America/New_York" />),
|
|
268
|
+
).toBe("9/24/2024");
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("should format Date with timezone and dateOptions", () => {
|
|
272
|
+
const { render } = setup();
|
|
273
|
+
const date = new Date("2024-09-24T12:00:00Z");
|
|
274
|
+
expect(
|
|
275
|
+
render(
|
|
276
|
+
<Localize
|
|
277
|
+
value={date}
|
|
278
|
+
timezone="America/New_York"
|
|
279
|
+
date={{
|
|
280
|
+
hour: "2-digit",
|
|
281
|
+
minute: "2-digit",
|
|
282
|
+
hour12: true,
|
|
283
|
+
timeZoneName: "short",
|
|
284
|
+
}}
|
|
285
|
+
/>,
|
|
286
|
+
),
|
|
287
|
+
).toContain("EDT");
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("should format Date with timezone using dayjs format", () => {
|
|
291
|
+
const { render } = setup();
|
|
292
|
+
const date = new Date("2024-09-24T12:00:00Z");
|
|
293
|
+
const formatted = render(
|
|
294
|
+
<Localize
|
|
295
|
+
value={date}
|
|
296
|
+
timezone="America/New_York"
|
|
297
|
+
date="YYYY-MM-DD HH:mm"
|
|
298
|
+
/>,
|
|
299
|
+
);
|
|
300
|
+
expect(formatted).toBe("2024-09-24 08:00");
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("should format DateTime with timezone using dayjs format", () => {
|
|
304
|
+
const { render, dateTime } = setup();
|
|
305
|
+
const date = dateTime.utc("2024-09-24T12:00:00Z");
|
|
306
|
+
const formatted = render(
|
|
307
|
+
<Localize
|
|
308
|
+
value={date}
|
|
309
|
+
timezone="Europe/Paris"
|
|
310
|
+
date="YYYY-MM-DD HH:mm"
|
|
311
|
+
/>,
|
|
312
|
+
);
|
|
313
|
+
expect(formatted).toBe("2024-09-24 14:00");
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("should format DateTime with timezone and Intl options", () => {
|
|
317
|
+
const { render, dateTime } = setup();
|
|
318
|
+
const date = dateTime.utc("2024-09-24T12:00:00Z");
|
|
319
|
+
const formatted = render(
|
|
320
|
+
<Localize
|
|
321
|
+
value={date}
|
|
322
|
+
timezone="Asia/Tokyo"
|
|
323
|
+
date={{
|
|
324
|
+
hour: "2-digit",
|
|
325
|
+
minute: "2-digit",
|
|
326
|
+
hour12: false,
|
|
327
|
+
}}
|
|
328
|
+
/>,
|
|
329
|
+
);
|
|
330
|
+
expect(formatted).toBe("21:00");
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("should format DateTime with timezone using LLL format", () => {
|
|
334
|
+
const { render, dateTime } = setup();
|
|
335
|
+
const date = dateTime.utc("2024-09-24T12:00:00Z");
|
|
336
|
+
const formatted = render(
|
|
337
|
+
<Localize value={date} timezone="America/Los_Angeles" date="LLL" />,
|
|
338
|
+
);
|
|
339
|
+
expect(formatted).toContain("September");
|
|
340
|
+
expect(formatted).toContain("2024");
|
|
341
|
+
expect(formatted).toContain("5:00 AM");
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it("should respect locale and timezone together", () => {
|
|
345
|
+
const { render, i18n, dateTime } = setup();
|
|
346
|
+
const date = dateTime.utc("2024-09-24T12:00:00Z");
|
|
347
|
+
i18n.setLang("fr");
|
|
348
|
+
const formatted = render(
|
|
349
|
+
<Localize value={date} timezone="Europe/Paris" date="LLL" />,
|
|
350
|
+
);
|
|
351
|
+
console.log(formatted);
|
|
352
|
+
expect(formatted).toContain("septembre");
|
|
353
|
+
expect(formatted).toContain("2024");
|
|
354
|
+
expect(formatted).toContain("14:00");
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
});
|