@alepha/react 0.14.2 → 0.14.4

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.
Files changed (57) hide show
  1. package/dist/auth/index.browser.js +29 -14
  2. package/dist/auth/index.browser.js.map +1 -1
  3. package/dist/auth/index.js +960 -195
  4. package/dist/auth/index.js.map +1 -1
  5. package/dist/core/index.d.ts +4 -0
  6. package/dist/core/index.d.ts.map +1 -1
  7. package/dist/core/index.js +7 -4
  8. package/dist/core/index.js.map +1 -1
  9. package/dist/head/index.browser.js +59 -19
  10. package/dist/head/index.browser.js.map +1 -1
  11. package/dist/head/index.d.ts +99 -560
  12. package/dist/head/index.d.ts.map +1 -1
  13. package/dist/head/index.js +92 -87
  14. package/dist/head/index.js.map +1 -1
  15. package/dist/router/index.browser.js +30 -15
  16. package/dist/router/index.browser.js.map +1 -1
  17. package/dist/router/index.d.ts +616 -192
  18. package/dist/router/index.d.ts.map +1 -1
  19. package/dist/router/index.js +961 -196
  20. package/dist/router/index.js.map +1 -1
  21. package/package.json +4 -4
  22. package/src/auth/__tests__/$auth.spec.ts +188 -0
  23. package/src/core/__tests__/Router.spec.tsx +169 -0
  24. package/src/core/hooks/useAction.browser.spec.tsx +569 -0
  25. package/src/core/hooks/useAction.ts +11 -0
  26. package/src/form/hooks/useForm.browser.spec.tsx +366 -0
  27. package/src/head/helpers/SeoExpander.spec.ts +203 -0
  28. package/src/head/hooks/useHead.spec.tsx +288 -0
  29. package/src/head/index.ts +11 -28
  30. package/src/head/providers/BrowserHeadProvider.browser.spec.ts +196 -0
  31. package/src/head/providers/BrowserHeadProvider.ts +25 -19
  32. package/src/head/providers/HeadProvider.ts +76 -10
  33. package/src/head/providers/ServerHeadProvider.ts +22 -138
  34. package/src/i18n/__tests__/integration.spec.tsx +239 -0
  35. package/src/i18n/components/Localize.spec.tsx +357 -0
  36. package/src/i18n/hooks/useI18n.browser.spec.tsx +438 -0
  37. package/src/i18n/providers/I18nProvider.spec.ts +389 -0
  38. package/src/router/__tests__/page-head-browser.browser.spec.ts +91 -0
  39. package/src/router/__tests__/page-head.spec.ts +44 -0
  40. package/src/router/__tests__/seo-head.spec.ts +121 -0
  41. package/src/router/atoms/ssrManifestAtom.ts +60 -0
  42. package/src/router/constants/PAGE_PRELOAD_KEY.ts +6 -0
  43. package/src/router/errors/Redirection.ts +1 -1
  44. package/src/router/index.shared.ts +1 -0
  45. package/src/router/index.ts +16 -2
  46. package/src/router/primitives/$page.browser.spec.tsx +702 -0
  47. package/src/router/primitives/$page.spec.tsx +702 -0
  48. package/src/router/primitives/$page.ts +46 -10
  49. package/src/router/providers/ReactBrowserProvider.ts +14 -29
  50. package/src/router/providers/ReactBrowserRouterProvider.ts +5 -0
  51. package/src/router/providers/ReactPageProvider.ts +11 -4
  52. package/src/router/providers/ReactServerProvider.spec.tsx +316 -0
  53. package/src/router/providers/ReactServerProvider.ts +331 -315
  54. package/src/router/providers/ReactServerTemplateProvider.ts +775 -0
  55. package/src/router/providers/SSRManifestProvider.ts +365 -0
  56. package/src/router/services/ReactPageServerService.ts +5 -3
  57. package/src/router/services/ReactRouter.ts +3 -3
@@ -0,0 +1,438 @@
1
+ import { AlephaContext } from "@alepha/react";
2
+ import { fireEvent, render } from "@testing-library/react";
3
+ import { Alepha } from "alepha";
4
+ import type { ReactNode } from "react";
5
+ import { act } from "react";
6
+ import { describe, test } from "vitest";
7
+ import { $dictionary } from "../primitives/$dictionary.ts";
8
+ import { useI18n } from "./useI18n.ts";
9
+ import { AlephaReactI18n } from "../index.ts";
10
+ import { I18nProvider } from "../providers/I18nProvider.ts";
11
+
12
+ describe("useI18n hook", () => {
13
+ const renderWithAlepha = (alepha: Alepha, element: ReactNode) => {
14
+ return render(
15
+ <AlephaContext.Provider value={alepha}>{element}</AlephaContext.Provider>,
16
+ );
17
+ };
18
+
19
+ test("should provide translation function", async ({ expect }) => {
20
+ const TranslationComponent = () => {
21
+ const { tr } = useI18n();
22
+ return <div data-testid="greeting">{tr("hello")}</div>;
23
+ };
24
+
25
+ class App {
26
+ en = $dictionary({
27
+ lazy: async () => ({ default: { hello: "Hello, World!" } }),
28
+ });
29
+ }
30
+
31
+ const alepha = Alepha.create().with(AlephaReactI18n);
32
+ const app = alepha.inject(App);
33
+ await alepha.start();
34
+
35
+ const ui = renderWithAlepha(alepha, <TranslationComponent />);
36
+
37
+ expect(ui.getByTestId("greeting").textContent).toBe("Hello, World!");
38
+ });
39
+
40
+ test("should support type-safe translation keys", async ({ expect }) => {
41
+ const TypeSafeComponent = () => {
42
+ const { tr } = useI18n<App, "en">();
43
+ return (
44
+ <div>
45
+ <div data-testid="hello">{tr("hello")}</div>
46
+ <div data-testid="goodbye">{tr("goodbye")}</div>
47
+ </div>
48
+ );
49
+ };
50
+
51
+ class App {
52
+ en = $dictionary({
53
+ lazy: async () => ({
54
+ default: {
55
+ hello: "Hello",
56
+ goodbye: "Goodbye",
57
+ },
58
+ }),
59
+ });
60
+ }
61
+
62
+ const alepha = Alepha.create().with(AlephaReactI18n);
63
+ const app = alepha.inject(App);
64
+ await alepha.start();
65
+
66
+ const ui = renderWithAlepha(alepha, <TypeSafeComponent />);
67
+
68
+ expect(ui.getByTestId("hello").textContent).toBe("Hello");
69
+ expect(ui.getByTestId("goodbye").textContent).toBe("Goodbye");
70
+ });
71
+
72
+ test("should react to language changes", async ({ expect }) => {
73
+ const LanguageSwitcher = () => {
74
+ const i18n = useI18n();
75
+
76
+ return (
77
+ <div>
78
+ <div data-testid="message">{i18n.tr("welcome")}</div>
79
+ <button
80
+ type="button"
81
+ data-testid="switch-fr"
82
+ onClick={() => i18n.setLang("fr")}
83
+ >
84
+ French
85
+ </button>
86
+ <button
87
+ type="button"
88
+ data-testid="switch-es"
89
+ onClick={() => i18n.setLang("es")}
90
+ >
91
+ Spanish
92
+ </button>
93
+ </div>
94
+ );
95
+ };
96
+
97
+ class App {
98
+ en = $dictionary({
99
+ lazy: async () => ({ default: { welcome: "Welcome" } }),
100
+ });
101
+
102
+ fr = $dictionary({
103
+ lazy: async () => ({ default: { welcome: "Bienvenue" } }),
104
+ });
105
+
106
+ es = $dictionary({
107
+ lazy: async () => ({ default: { welcome: "Bienvenido" } }),
108
+ });
109
+ }
110
+
111
+ const alepha = Alepha.create().with(AlephaReactI18n);
112
+ const app = alepha.inject(App);
113
+ await alepha.start();
114
+
115
+ const ui = renderWithAlepha(alepha, <LanguageSwitcher />);
116
+
117
+ // Initial state - English
118
+ expect(ui.getByTestId("message").textContent).toBe("Welcome");
119
+
120
+ // Switch to French
121
+ await act(async () => {
122
+ fireEvent.click(ui.getByTestId("switch-fr"));
123
+ await new Promise((resolve) => setTimeout(resolve, 50));
124
+ });
125
+ expect(ui.getByTestId("message").textContent).toBe("Bienvenue");
126
+
127
+ // Switch to Spanish
128
+ await act(async () => {
129
+ fireEvent.click(ui.getByTestId("switch-es"));
130
+ await new Promise((resolve) => setTimeout(resolve, 50));
131
+ });
132
+ expect(ui.getByTestId("message").textContent).toBe("Bienvenido");
133
+ });
134
+
135
+ test("should provide current language", async ({ expect }) => {
136
+ const LanguageDisplay = () => {
137
+ const i18n = useI18n();
138
+ return (
139
+ <div>
140
+ <div data-testid="current-lang">{i18n.lang}</div>
141
+ <button
142
+ type="button"
143
+ data-testid="change-lang"
144
+ onClick={() => i18n.setLang("de")}
145
+ >
146
+ Change
147
+ </button>
148
+ </div>
149
+ );
150
+ };
151
+
152
+ class App {
153
+ en = $dictionary({
154
+ lazy: async () => ({ default: {} }),
155
+ });
156
+
157
+ de = $dictionary({
158
+ lazy: async () => ({ default: {} }),
159
+ });
160
+ }
161
+
162
+ const alepha = Alepha.create().with(AlephaReactI18n);
163
+ const app = alepha.inject(App);
164
+ const i18n = alepha.inject(I18nProvider);
165
+ await alepha.start();
166
+
167
+ // Ensure we start with English
168
+ await i18n.setLang("en");
169
+
170
+ const ui = renderWithAlepha(alepha, <LanguageDisplay />);
171
+
172
+ expect(ui.getByTestId("current-lang").textContent).toBe("en");
173
+
174
+ await act(async () => {
175
+ fireEvent.click(ui.getByTestId("change-lang"));
176
+ await new Promise((resolve) => setTimeout(resolve, 50));
177
+ });
178
+ expect(ui.getByTestId("current-lang").textContent).toBe("de");
179
+ });
180
+
181
+ test("should provide available languages", async ({ expect }) => {
182
+ const LanguageList = () => {
183
+ const i18n = useI18n();
184
+ return (
185
+ <div>
186
+ <div data-testid="languages">{i18n.languages.join(", ")}</div>
187
+ </div>
188
+ );
189
+ };
190
+
191
+ class App {
192
+ en = $dictionary({
193
+ lazy: async () => ({ default: {} }),
194
+ });
195
+
196
+ fr = $dictionary({
197
+ lazy: async () => ({ default: {} }),
198
+ });
199
+
200
+ de = $dictionary({
201
+ lazy: async () => ({ default: {} }),
202
+ });
203
+ }
204
+
205
+ const alepha = Alepha.create().with(AlephaReactI18n);
206
+ const app = alepha.inject(App);
207
+ await alepha.start();
208
+
209
+ const ui = renderWithAlepha(alepha, <LanguageList />);
210
+
211
+ const text = ui.getByTestId("languages").textContent;
212
+ expect(text).toContain("en");
213
+ expect(text).toContain("fr");
214
+ expect(text).toContain("de");
215
+ });
216
+
217
+ test("should support interpolation with args", async ({ expect }) => {
218
+ const InterpolationComponent = () => {
219
+ const { tr } = useI18n();
220
+ return (
221
+ <div>
222
+ <div data-testid="message">
223
+ {tr("greeting", { args: ["Alice", "developer"] })}
224
+ </div>
225
+ </div>
226
+ );
227
+ };
228
+
229
+ class App {
230
+ en = $dictionary({
231
+ lazy: async () => ({
232
+ default: { greeting: "Hello $1, you are a $2!" },
233
+ }),
234
+ });
235
+ }
236
+
237
+ const alepha = Alepha.create().with(AlephaReactI18n);
238
+ const app = alepha.inject(App);
239
+ await alepha.start();
240
+
241
+ const ui = renderWithAlepha(alepha, <InterpolationComponent />);
242
+
243
+ expect(ui.getByTestId("message").textContent).toBe(
244
+ "Hello Alice, you are a developer!",
245
+ );
246
+ });
247
+
248
+ test("should support default option", async ({ expect }) => {
249
+ const DefaultComponent = () => {
250
+ const { tr } = useI18n();
251
+ return (
252
+ <div>
253
+ <div data-testid="existing">{tr("exists")}</div>
254
+ <div data-testid="missing">
255
+ {tr("missing", { default: "Default Message" })}
256
+ </div>
257
+ </div>
258
+ );
259
+ };
260
+
261
+ class App {
262
+ en = $dictionary({
263
+ lazy: async () => ({ default: { exists: "This exists" } }),
264
+ });
265
+ }
266
+
267
+ const alepha = Alepha.create().with(AlephaReactI18n);
268
+ const app = alepha.inject(App);
269
+ await alepha.start();
270
+
271
+ const ui = renderWithAlepha(alepha, <DefaultComponent />);
272
+
273
+ expect(ui.getByTestId("existing").textContent).toBe("This exists");
274
+ expect(ui.getByTestId("missing").textContent).toBe("Default Message");
275
+ });
276
+
277
+ test("should provide number formatter", async ({ expect }) => {
278
+ const NumberFormatComponent = () => {
279
+ const i18n = useI18n();
280
+ return (
281
+ <div>
282
+ <div data-testid="number">{i18n.numberFormat.format(1234567.89)}</div>
283
+ </div>
284
+ );
285
+ };
286
+
287
+ class App {
288
+ en = $dictionary({
289
+ lazy: async () => ({ default: {} }),
290
+ });
291
+ }
292
+
293
+ const alepha = Alepha.create().with(AlephaReactI18n);
294
+ const app = alepha.inject(App);
295
+ await alepha.start();
296
+
297
+ const ui = renderWithAlepha(alepha, <NumberFormatComponent />);
298
+
299
+ const formatted = ui.getByTestId("number").textContent;
300
+ expect(formatted).toBeDefined();
301
+ // Number format will vary by locale, just check it's formatted
302
+ expect(formatted).toMatch(/1[,.]234[,.]567/);
303
+ });
304
+
305
+ test("should handle multiple language switches", async ({ expect }) => {
306
+ const MultiSwitchComponent = () => {
307
+ const i18n = useI18n();
308
+ return (
309
+ <div>
310
+ <div data-testid="text">{i18n.tr("status")}</div>
311
+ <button
312
+ type="button"
313
+ data-testid="en"
314
+ onClick={() => i18n.setLang("en")}
315
+ >
316
+ EN
317
+ </button>
318
+ <button
319
+ type="button"
320
+ data-testid="fr"
321
+ onClick={() => i18n.setLang("fr")}
322
+ >
323
+ FR
324
+ </button>
325
+ <button
326
+ type="button"
327
+ data-testid="de"
328
+ onClick={() => i18n.setLang("de")}
329
+ >
330
+ DE
331
+ </button>
332
+ </div>
333
+ );
334
+ };
335
+
336
+ class App {
337
+ en = $dictionary({
338
+ lazy: async () => ({ default: { status: "Active" } }),
339
+ });
340
+
341
+ fr = $dictionary({
342
+ lazy: async () => ({ default: { status: "Actif" } }),
343
+ });
344
+
345
+ de = $dictionary({
346
+ lazy: async () => ({ default: { status: "Aktiv" } }),
347
+ });
348
+ }
349
+
350
+ const alepha = Alepha.create().with(AlephaReactI18n);
351
+ const app = alepha.inject(App);
352
+ const i18n = alepha.inject(I18nProvider);
353
+ await alepha.start();
354
+
355
+ // Ensure we start with English
356
+ await i18n.setLang("en");
357
+
358
+ const ui = renderWithAlepha(alepha, <MultiSwitchComponent />);
359
+
360
+ expect(ui.getByTestId("text").textContent).toBe("Active");
361
+
362
+ await act(async () => {
363
+ fireEvent.click(ui.getByTestId("fr"));
364
+ await new Promise((resolve) => setTimeout(resolve, 50));
365
+ });
366
+ expect(ui.getByTestId("text").textContent).toBe("Actif");
367
+
368
+ await act(async () => {
369
+ fireEvent.click(ui.getByTestId("de"));
370
+ await new Promise((resolve) => setTimeout(resolve, 50));
371
+ });
372
+ expect(ui.getByTestId("text").textContent).toBe("Aktiv");
373
+
374
+ await act(async () => {
375
+ fireEvent.click(ui.getByTestId("en"));
376
+ await new Promise((resolve) => setTimeout(resolve, 50));
377
+ });
378
+ expect(ui.getByTestId("text").textContent).toBe("Active");
379
+ });
380
+
381
+ test("should fallback to fallback language for missing keys", async ({
382
+ expect,
383
+ }) => {
384
+ const FallbackComponent = () => {
385
+ const i18n = useI18n();
386
+ return (
387
+ <div>
388
+ <div data-testid="common">{i18n.tr("common")}</div>
389
+ <div data-testid="specific">{i18n.tr("specific")}</div>
390
+ <button
391
+ type="button"
392
+ data-testid="switch"
393
+ onClick={() => i18n.setLang("fr")}
394
+ >
395
+ Switch
396
+ </button>
397
+ </div>
398
+ );
399
+ };
400
+
401
+ class App {
402
+ en = $dictionary({
403
+ lazy: async () => ({
404
+ default: {
405
+ common: "Common text",
406
+ specific: "English specific",
407
+ },
408
+ }),
409
+ });
410
+
411
+ fr = $dictionary({
412
+ lazy: async () => ({
413
+ default: {
414
+ common: "Texte commun",
415
+ // 'specific' is missing in French
416
+ },
417
+ }),
418
+ });
419
+ }
420
+
421
+ const alepha = Alepha.create().with(AlephaReactI18n);
422
+ const app = alepha.inject(App);
423
+ await alepha.start();
424
+
425
+ const ui = renderWithAlepha(alepha, <FallbackComponent />);
426
+
427
+ expect(ui.getByTestId("common").textContent).toBe("Common text");
428
+ expect(ui.getByTestId("specific").textContent).toBe("English specific");
429
+
430
+ await act(async () => {
431
+ fireEvent.click(ui.getByTestId("switch"));
432
+ await new Promise((resolve) => setTimeout(resolve, 50));
433
+ });
434
+ expect(ui.getByTestId("common").textContent).toBe("Texte commun");
435
+ // Should fallback to English for 'specific'
436
+ expect(ui.getByTestId("specific").textContent).toBe("English specific");
437
+ });
438
+ });