@alepha/react 0.11.10 → 0.11.12
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/README.md +1 -183
- package/dist/auth/index.browser.js +1460 -0
- package/dist/auth/index.browser.js.map +1 -0
- package/dist/auth/index.cjs +3647 -0
- package/dist/auth/index.cjs.map +1 -0
- package/dist/auth/index.d.cts +564 -0
- package/dist/auth/index.d.cts.map +1 -0
- package/dist/auth/index.d.ts +564 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +3615 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/{index.browser.js → core/index.browser.js} +36 -35
- package/dist/core/index.browser.js.map +1 -0
- package/dist/{index.cjs → core/index.cjs} +141 -140
- package/dist/core/index.cjs.map +1 -0
- package/dist/{index.d.cts → core/index.d.cts} +68 -68
- package/dist/core/index.d.cts.map +1 -0
- package/dist/{index.d.ts → core/index.d.ts} +68 -68
- package/dist/core/index.d.ts.map +1 -0
- package/dist/{index.js → core/index.js} +39 -38
- package/dist/core/index.js.map +1 -0
- package/dist/form/index.cjs +2054 -0
- package/dist/form/index.cjs.map +1 -0
- package/dist/form/index.d.cts +211 -0
- package/dist/form/index.d.cts.map +1 -0
- package/dist/form/index.d.ts +211 -0
- package/dist/form/index.d.ts.map +1 -0
- package/dist/form/index.js +2026 -0
- package/dist/form/index.js.map +1 -0
- package/dist/head/index.browser.js +1503 -0
- package/dist/head/index.browser.js.map +1 -0
- package/dist/head/index.cjs +1908 -0
- package/dist/head/index.cjs.map +1 -0
- package/dist/head/index.d.cts +595 -0
- package/dist/head/index.d.cts.map +1 -0
- package/dist/head/index.d.ts +601 -0
- package/dist/head/index.d.ts.map +1 -0
- package/dist/head/index.js +1880 -0
- package/dist/head/index.js.map +1 -0
- package/dist/i18n/index.cjs +1886 -0
- package/dist/i18n/index.cjs.map +1 -0
- package/dist/i18n/index.d.cts +168 -0
- package/dist/i18n/index.d.cts.map +1 -0
- package/dist/i18n/index.d.ts +168 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/index.js +1857 -0
- package/dist/i18n/index.js.map +1 -0
- package/dist/websocket/index.cjs +1774 -0
- package/dist/websocket/index.cjs.map +1 -0
- package/dist/websocket/index.d.cts +118 -0
- package/dist/websocket/index.d.cts.map +1 -0
- package/dist/websocket/index.d.ts +118 -0
- package/dist/websocket/index.d.ts.map +1 -0
- package/dist/websocket/index.js +1750 -0
- package/dist/websocket/index.js.map +1 -0
- package/package.json +89 -67
- package/src/auth/descriptors/$auth.ts +436 -0
- package/src/auth/descriptors/$authApple.ts +8 -0
- package/src/auth/descriptors/$authGithub.ts +81 -0
- package/src/auth/descriptors/$authGoogle.ts +38 -0
- package/src/auth/errors/SessionExpiredError.ts +6 -0
- package/src/auth/hooks/useAuth.ts +31 -0
- package/src/auth/index.browser.ts +16 -0
- package/src/auth/index.shared.ts +3 -0
- package/src/auth/index.ts +47 -0
- package/src/auth/providers/ReactAuthProvider.ts +629 -0
- package/src/auth/schemas/tokenResponseSchema.ts +11 -0
- package/src/auth/schemas/tokensSchema.ts +21 -0
- package/src/auth/schemas/userinfoResponseSchema.ts +10 -0
- package/src/auth/services/ReactAuth.ts +124 -0
- package/src/{components → core/components}/ErrorViewer.tsx +3 -2
- package/src/{components → core/components}/NestedView.tsx +1 -1
- package/src/{contexts → core/contexts}/AlephaContext.ts +1 -1
- package/src/{descriptors → core/descriptors}/$page.ts +4 -4
- package/src/{hooks → core/hooks}/useAction.ts +1 -1
- package/src/{hooks → core/hooks}/useAlepha.ts +1 -1
- package/src/{hooks → core/hooks}/useClient.ts +1 -1
- package/src/{hooks → core/hooks}/useEvents.ts +1 -1
- package/src/{hooks → core/hooks}/useInject.ts +1 -1
- package/src/{hooks → core/hooks}/useQueryParams.ts +1 -1
- package/src/{hooks → core/hooks}/useRouterState.ts +1 -1
- package/src/{hooks → core/hooks}/useSchema.ts +3 -3
- package/src/{hooks → core/hooks}/useStore.ts +2 -2
- package/src/{index.browser.ts → core/index.browser.ts} +4 -4
- package/src/{index.ts → core/index.ts} +6 -6
- package/src/{providers → core/providers}/ReactBrowserProvider.ts +6 -6
- package/src/{providers → core/providers}/ReactBrowserRendererProvider.ts +2 -2
- package/src/{providers → core/providers}/ReactBrowserRouterProvider.ts +3 -3
- package/src/{providers → core/providers}/ReactPageProvider.ts +3 -3
- package/src/{providers → core/providers}/ReactServerProvider.ts +7 -7
- package/src/{services → core/services}/ReactPageServerService.ts +2 -2
- package/src/{services → core/services}/ReactPageService.ts +1 -1
- package/src/{services → core/services}/ReactRouter.ts +1 -1
- package/src/form/components/FormState.tsx +17 -0
- package/src/form/hooks/useForm.ts +47 -0
- package/src/form/hooks/useFormState.ts +130 -0
- package/src/form/index.ts +38 -0
- package/src/form/services/FormModel.ts +548 -0
- package/src/head/descriptors/$head.ts +25 -0
- package/src/head/hooks/useHead.ts +62 -0
- package/src/head/index.browser.ts +25 -0
- package/src/head/index.ts +47 -0
- package/src/head/interfaces/Head.ts +46 -0
- package/src/head/providers/BrowserHeadProvider.ts +105 -0
- package/src/head/providers/HeadProvider.ts +73 -0
- package/src/head/providers/ServerHeadProvider.ts +109 -0
- package/src/i18n/README.md +76 -0
- package/src/i18n/components/Localize.tsx +35 -0
- package/src/i18n/descriptors/$dictionary.ts +65 -0
- package/src/i18n/hooks/useI18n.ts +18 -0
- package/src/i18n/index.ts +34 -0
- package/src/i18n/providers/I18nProvider.ts +277 -0
- package/src/websocket/hooks/useRoom.tsx +223 -0
- package/src/websocket/index.ts +7 -0
- package/dist/index.browser.js.map +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- /package/src/{components → core/components}/ClientOnly.tsx +0 -0
- /package/src/{components → core/components}/ErrorBoundary.tsx +0 -0
- /package/src/{components → core/components}/Link.tsx +0 -0
- /package/src/{components → core/components}/NotFound.tsx +0 -0
- /package/src/{contexts → core/contexts}/RouterLayerContext.ts +0 -0
- /package/src/{errors → core/errors}/Redirection.ts +0 -0
- /package/src/{hooks → core/hooks}/useActive.ts +0 -0
- /package/src/{hooks → core/hooks}/useRouter.ts +0 -0
- /package/src/{index.shared.ts → core/index.shared.ts} +0 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AlephaReact,
|
|
3
|
+
type PageConfigSchema,
|
|
4
|
+
type TPropsDefault,
|
|
5
|
+
type TPropsParentDefault,
|
|
6
|
+
} from "@alepha/react";
|
|
7
|
+
import { $module } from "alepha";
|
|
8
|
+
import { $head } from "./descriptors/$head.ts";
|
|
9
|
+
import type { Head } from "./interfaces/Head.ts";
|
|
10
|
+
import { ServerHeadProvider } from "./providers/ServerHeadProvider.ts";
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
export * from "./descriptors/$head.ts";
|
|
15
|
+
export * from "./hooks/useHead.ts";
|
|
16
|
+
export * from "./interfaces/Head.ts";
|
|
17
|
+
export * from "./providers/ServerHeadProvider.ts";
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
declare module "@alepha/react" {
|
|
22
|
+
interface PageDescriptorOptions<
|
|
23
|
+
TConfig extends PageConfigSchema = PageConfigSchema,
|
|
24
|
+
TProps extends object = TPropsDefault,
|
|
25
|
+
TPropsParent extends object = TPropsParentDefault,
|
|
26
|
+
> {
|
|
27
|
+
head?: Head | ((props: TProps, previous?: Head) => Head);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface ReactRouterState {
|
|
31
|
+
head: Head;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Fill `<head>` server & client side.
|
|
39
|
+
*
|
|
40
|
+
* @see {@link ServerHeadProvider}
|
|
41
|
+
* @module alepha.react.head
|
|
42
|
+
*/
|
|
43
|
+
export const AlephaReactHead = $module({
|
|
44
|
+
name: "alepha.react.head",
|
|
45
|
+
descriptors: [$head],
|
|
46
|
+
services: [AlephaReact, ServerHeadProvider],
|
|
47
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export interface Head extends SimpleHead {
|
|
2
|
+
description?: string;
|
|
3
|
+
|
|
4
|
+
// TODO
|
|
5
|
+
keywords?: string[];
|
|
6
|
+
author?: string;
|
|
7
|
+
robots?: string;
|
|
8
|
+
themeColor?: string;
|
|
9
|
+
viewport?:
|
|
10
|
+
| string
|
|
11
|
+
| {
|
|
12
|
+
width?: string;
|
|
13
|
+
height?: string;
|
|
14
|
+
initialScale?: string;
|
|
15
|
+
maximumScale?: string;
|
|
16
|
+
userScalable?: "no" | "yes" | "0" | "1";
|
|
17
|
+
interactiveWidget?:
|
|
18
|
+
| "resizes-visual"
|
|
19
|
+
| "resizes-content"
|
|
20
|
+
| "overlays-content";
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
og?: {
|
|
24
|
+
title?: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
image?: string;
|
|
27
|
+
url?: string;
|
|
28
|
+
type?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
twitter?: {
|
|
32
|
+
card?: string;
|
|
33
|
+
title?: string;
|
|
34
|
+
description?: string;
|
|
35
|
+
image?: string;
|
|
36
|
+
site?: string;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface SimpleHead {
|
|
41
|
+
title?: string;
|
|
42
|
+
titleSeparator?: string;
|
|
43
|
+
htmlAttributes?: Record<string, string>;
|
|
44
|
+
bodyAttributes?: Record<string, string>;
|
|
45
|
+
meta?: Array<{ name: string; content: string }>;
|
|
46
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { $hook, $inject } from "alepha";
|
|
2
|
+
import type { Head } from "../interfaces/Head";
|
|
3
|
+
import { HeadProvider } from "./HeadProvider.ts";
|
|
4
|
+
|
|
5
|
+
export class BrowserHeadProvider {
|
|
6
|
+
protected readonly headProvider = $inject(HeadProvider);
|
|
7
|
+
|
|
8
|
+
protected get document(): Document {
|
|
9
|
+
return window.document;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
protected readonly onBrowserRender = $hook({
|
|
13
|
+
on: "react:browser:render",
|
|
14
|
+
handler: async ({ state }) => {
|
|
15
|
+
this.headProvider.fillHead(state);
|
|
16
|
+
if (state.head) {
|
|
17
|
+
this.renderHead(this.document, state.head);
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
protected readonly onTransitionEnd = $hook({
|
|
23
|
+
on: "react:transition:end",
|
|
24
|
+
handler: async ({ state }) => {
|
|
25
|
+
this.headProvider.fillHead(state);
|
|
26
|
+
if (state.head) {
|
|
27
|
+
this.renderHead(this.document, state.head);
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
public getHead(document: Document): Head {
|
|
33
|
+
return {
|
|
34
|
+
get title() {
|
|
35
|
+
return document.title;
|
|
36
|
+
},
|
|
37
|
+
get htmlAttributes() {
|
|
38
|
+
const attrs: Record<string, string> = {};
|
|
39
|
+
for (const attr of document.documentElement.attributes) {
|
|
40
|
+
attrs[attr.name] = attr.value;
|
|
41
|
+
}
|
|
42
|
+
return attrs;
|
|
43
|
+
},
|
|
44
|
+
get bodyAttributes() {
|
|
45
|
+
const attrs: Record<string, string> = {};
|
|
46
|
+
for (const attr of document.body.attributes) {
|
|
47
|
+
attrs[attr.name] = attr.value;
|
|
48
|
+
}
|
|
49
|
+
return attrs;
|
|
50
|
+
},
|
|
51
|
+
get meta() {
|
|
52
|
+
const metas: { name: string; content: string }[] = [];
|
|
53
|
+
for (const meta of document.head.querySelectorAll("meta[name]")) {
|
|
54
|
+
const name = meta.getAttribute("name");
|
|
55
|
+
const content = meta.getAttribute("content");
|
|
56
|
+
if (name && content) {
|
|
57
|
+
metas.push({ name, content });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return metas;
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public renderHead(document: Document, head: Head): void {
|
|
66
|
+
if (head.title) {
|
|
67
|
+
document.title = head.title;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (head.bodyAttributes) {
|
|
71
|
+
for (const [key, value] of Object.entries(head.bodyAttributes)) {
|
|
72
|
+
if (value) {
|
|
73
|
+
document.body.setAttribute(key, value);
|
|
74
|
+
} else {
|
|
75
|
+
document.body.removeAttribute(key);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (head.htmlAttributes) {
|
|
81
|
+
for (const [key, value] of Object.entries(head.htmlAttributes)) {
|
|
82
|
+
if (value) {
|
|
83
|
+
document.documentElement.setAttribute(key, value);
|
|
84
|
+
} else {
|
|
85
|
+
document.documentElement.removeAttribute(key);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (head.meta) {
|
|
91
|
+
for (const it of head.meta) {
|
|
92
|
+
const { name, content } = it;
|
|
93
|
+
const meta = document.querySelector(`meta[name="${name}"]`);
|
|
94
|
+
if (meta) {
|
|
95
|
+
meta.setAttribute("content", content);
|
|
96
|
+
} else {
|
|
97
|
+
const newMeta = document.createElement("meta");
|
|
98
|
+
newMeta.setAttribute("name", name);
|
|
99
|
+
newMeta.setAttribute("content", content);
|
|
100
|
+
document.head.appendChild(newMeta);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { PageRoute, ReactRouterState } from "@alepha/react";
|
|
2
|
+
import type { Head } from "../interfaces/Head.ts";
|
|
3
|
+
|
|
4
|
+
export class HeadProvider {
|
|
5
|
+
public global?: Head | (() => Head);
|
|
6
|
+
|
|
7
|
+
protected getGlobalHead(): Head | undefined {
|
|
8
|
+
if (typeof this.global === "function") {
|
|
9
|
+
return this.global();
|
|
10
|
+
}
|
|
11
|
+
return this.global;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public fillHead(state: ReactRouterState) {
|
|
15
|
+
state.head = {
|
|
16
|
+
...state.head,
|
|
17
|
+
...this.getGlobalHead(),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
for (const layer of state.layers) {
|
|
21
|
+
if (layer.route?.head && !layer.error) {
|
|
22
|
+
this.fillHeadByPage(layer.route, state, layer.props ?? {});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
protected fillHeadByPage(
|
|
28
|
+
page: PageRoute,
|
|
29
|
+
state: ReactRouterState,
|
|
30
|
+
props: Record<string, any>,
|
|
31
|
+
): void {
|
|
32
|
+
if (!page.head) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
state.head ??= {};
|
|
37
|
+
|
|
38
|
+
const head =
|
|
39
|
+
typeof page.head === "function"
|
|
40
|
+
? page.head(props, state.head)
|
|
41
|
+
: page.head;
|
|
42
|
+
|
|
43
|
+
if (head.title) {
|
|
44
|
+
state.head ??= {};
|
|
45
|
+
|
|
46
|
+
if (state.head.titleSeparator) {
|
|
47
|
+
state.head.title = `${head.title}${state.head.titleSeparator}${state.head.title}`;
|
|
48
|
+
} else {
|
|
49
|
+
state.head.title = head.title;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
state.head.titleSeparator = head.titleSeparator;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (head.htmlAttributes) {
|
|
56
|
+
state.head.htmlAttributes = {
|
|
57
|
+
...state.head.htmlAttributes,
|
|
58
|
+
...head.htmlAttributes,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (head.bodyAttributes) {
|
|
63
|
+
state.head.bodyAttributes = {
|
|
64
|
+
...state.head.bodyAttributes,
|
|
65
|
+
...head.bodyAttributes,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (head.meta) {
|
|
70
|
+
state.head.meta = [...(state.head.meta ?? []), ...(head.meta ?? [])];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { $hook, $inject } from "alepha";
|
|
2
|
+
import { ServerTimingProvider } from "alepha/server";
|
|
3
|
+
import type { SimpleHead } from "../interfaces/Head.ts";
|
|
4
|
+
import { HeadProvider } from "./HeadProvider.ts";
|
|
5
|
+
|
|
6
|
+
export class ServerHeadProvider {
|
|
7
|
+
protected readonly headProvider = $inject(HeadProvider);
|
|
8
|
+
protected readonly serverTimingProvider = $inject(ServerTimingProvider);
|
|
9
|
+
|
|
10
|
+
protected readonly onServerRenderEnd = $hook({
|
|
11
|
+
on: "react:server:render:end",
|
|
12
|
+
handler: async (ev) => {
|
|
13
|
+
this.serverTimingProvider.beginTiming("renderHead");
|
|
14
|
+
this.headProvider.fillHead(ev.state);
|
|
15
|
+
if (ev.state.head) {
|
|
16
|
+
ev.html = this.renderHead(ev.html, ev.state.head);
|
|
17
|
+
}
|
|
18
|
+
this.serverTimingProvider.endTiming("renderHead");
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
public renderHead(template: string, head: SimpleHead): string {
|
|
23
|
+
let result = template;
|
|
24
|
+
|
|
25
|
+
// Inject htmlAttributes
|
|
26
|
+
const htmlAttributes = head.htmlAttributes;
|
|
27
|
+
if (htmlAttributes) {
|
|
28
|
+
result = result.replace(
|
|
29
|
+
/<html([^>]*)>/i,
|
|
30
|
+
(_, existingAttrs) =>
|
|
31
|
+
`<html${this.mergeAttributes(existingAttrs, htmlAttributes)}>`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Inject bodyAttributes
|
|
36
|
+
const bodyAttributes = head.bodyAttributes;
|
|
37
|
+
if (bodyAttributes) {
|
|
38
|
+
result = result.replace(
|
|
39
|
+
/<body([^>]*)>/i,
|
|
40
|
+
(_, existingAttrs) =>
|
|
41
|
+
`<body${this.mergeAttributes(existingAttrs, bodyAttributes)}>`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Build head content
|
|
46
|
+
let headContent = "";
|
|
47
|
+
const title = head.title;
|
|
48
|
+
if (title) {
|
|
49
|
+
if (template.includes("<title>")) {
|
|
50
|
+
result = result.replace(
|
|
51
|
+
/<title>(.*?)<\/title>/i,
|
|
52
|
+
() => `<title>${this.escapeHtml(title)}</title>`,
|
|
53
|
+
);
|
|
54
|
+
} else {
|
|
55
|
+
headContent += `<title>${this.escapeHtml(title)}</title>\n`;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (head.meta) {
|
|
60
|
+
for (const meta of head.meta) {
|
|
61
|
+
headContent += `<meta name="${this.escapeHtml(meta.name)}" content="${this.escapeHtml(meta.content)}">\n`;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Inject into <head>...</head>
|
|
66
|
+
result = result.replace(
|
|
67
|
+
/<head([^>]*)>(.*?)<\/head>/is,
|
|
68
|
+
(_, existingAttrs, existingHead) =>
|
|
69
|
+
`<head${existingAttrs}>${existingHead}${headContent}</head>`,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
return result.trim();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
protected mergeAttributes(
|
|
76
|
+
existing: string,
|
|
77
|
+
attrs: Record<string, string>,
|
|
78
|
+
): string {
|
|
79
|
+
const existingAttrs = this.parseAttributes(existing);
|
|
80
|
+
const merged = { ...existingAttrs, ...attrs };
|
|
81
|
+
return Object.entries(merged)
|
|
82
|
+
.map(([k, v]) => ` ${k}="${this.escapeHtml(v)}"`)
|
|
83
|
+
.join("");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
protected parseAttributes(attrStr: string): Record<string, string> {
|
|
87
|
+
attrStr = attrStr.replaceAll("'", '"');
|
|
88
|
+
|
|
89
|
+
const attrs: Record<string, string> = {};
|
|
90
|
+
const attrRegex = /([^\s=]+)(?:="([^"]*)")?/g;
|
|
91
|
+
let match: RegExpExecArray | null = attrRegex.exec(attrStr);
|
|
92
|
+
|
|
93
|
+
while (match) {
|
|
94
|
+
attrs[match[1]] = match[2] ?? "";
|
|
95
|
+
match = attrRegex.exec(attrStr);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return attrs;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
protected escapeHtml(str: string): string {
|
|
102
|
+
return str
|
|
103
|
+
.replace(/&/g, "&")
|
|
104
|
+
.replace(/</g, "<")
|
|
105
|
+
.replace(/>/g, ">")
|
|
106
|
+
.replace(/"/g, """)
|
|
107
|
+
.replace(/'/g, "'");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Alepha React I18n
|
|
2
|
+
|
|
3
|
+
Internationalization (i18n) support for React applications using Alepha.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
This package is part of the Alepha framework and can be installed via the all-in-one package:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install alepha
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Module
|
|
14
|
+
|
|
15
|
+
Add i18n support to your Alepha React application. SSR and CSR compatible.
|
|
16
|
+
|
|
17
|
+
It supports lazy loading of translations and provides a context to access the current language.
|
|
18
|
+
|
|
19
|
+
This module can be imported and used as follows:
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { Alepha, run } from "alepha";
|
|
23
|
+
import { AlephaReactI18n } from "@alepha/react/i18n";
|
|
24
|
+
|
|
25
|
+
const alepha = Alepha.create()
|
|
26
|
+
.with(AlephaReactI18n);
|
|
27
|
+
|
|
28
|
+
run(alepha);
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## API Reference
|
|
32
|
+
|
|
33
|
+
### Descriptors
|
|
34
|
+
|
|
35
|
+
Descriptors are functions that define and configure various aspects of your application. They follow the convention of starting with ` $ ` and return configured descriptor instances.
|
|
36
|
+
|
|
37
|
+
For more details, see the [Descriptors documentation](/docs/descriptors).
|
|
38
|
+
|
|
39
|
+
#### $dictionary()
|
|
40
|
+
|
|
41
|
+
Register a dictionary entry for translations.
|
|
42
|
+
|
|
43
|
+
It allows you to define a set of translations for a specific language.
|
|
44
|
+
Entry can be lazy-loaded, which is useful for large dictionaries or when translations are not needed immediately.
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
import { $dictionary } from "@alepha/react/i18n";
|
|
48
|
+
|
|
49
|
+
const Example = () => {
|
|
50
|
+
const { tr } = useI18n<App, "en">();
|
|
51
|
+
return <div>{tr("hello")}</div>; //
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
class App {
|
|
55
|
+
|
|
56
|
+
en = $dictionary({
|
|
57
|
+
// { default: { hello: "Hey" } }
|
|
58
|
+
lazy: () => import("./translations/en.ts"),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
home = $page({
|
|
62
|
+
path: "/",
|
|
63
|
+
component: Example,
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
run(App);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Hooks
|
|
71
|
+
|
|
72
|
+
Hooks provide a way to tap into various lifecycle events and extend functionality. They follow the convention of starting with `use` and return configured hook instances.
|
|
73
|
+
|
|
74
|
+
#### useI18n()
|
|
75
|
+
|
|
76
|
+
Hook to access the i18n service.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { TypeBoxError } from "alepha";
|
|
2
|
+
import type { DateTime } from "alepha/datetime";
|
|
3
|
+
import { useI18n } from "../hooks/useI18n.ts";
|
|
4
|
+
|
|
5
|
+
export interface LocalizeProps {
|
|
6
|
+
value: string | number | Date | DateTime | TypeBoxError;
|
|
7
|
+
/**
|
|
8
|
+
* Options for number formatting (when value is a number)
|
|
9
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat
|
|
10
|
+
*/
|
|
11
|
+
number?: Intl.NumberFormatOptions;
|
|
12
|
+
/**
|
|
13
|
+
* Options for date formatting (when value is a Date or DateTime)
|
|
14
|
+
* Can be:
|
|
15
|
+
* - A dayjs format string (e.g., "LLL", "YYYY-MM-DD", "dddd, MMMM D YYYY")
|
|
16
|
+
* - "fromNow" for relative time (e.g., "2 hours ago")
|
|
17
|
+
* - Intl.DateTimeFormatOptions for native formatting
|
|
18
|
+
* @see https://day.js.org/docs/en/display/format
|
|
19
|
+
* @see https://day.js.org/docs/en/display/from-now
|
|
20
|
+
*/
|
|
21
|
+
date?: string | "fromNow" | Intl.DateTimeFormatOptions;
|
|
22
|
+
/**
|
|
23
|
+
* Timezone to display dates in (when value is a Date or DateTime)
|
|
24
|
+
* Uses IANA timezone names (e.g., "America/New_York", "Europe/Paris", "Asia/Tokyo")
|
|
25
|
+
* @see https://day.js.org/docs/en/timezone/timezone
|
|
26
|
+
*/
|
|
27
|
+
timezone?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const Localize = (props: LocalizeProps) => {
|
|
31
|
+
const i18n = useI18n();
|
|
32
|
+
return i18n.l(props.value, props);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default Localize;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { $inject, createDescriptor, Descriptor, KIND } from "alepha";
|
|
2
|
+
import { I18nProvider } from "../providers/I18nProvider.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Register a dictionary entry for translations.
|
|
6
|
+
*
|
|
7
|
+
* It allows you to define a set of translations for a specific language.
|
|
8
|
+
* Entry can be lazy-loaded, which is useful for large dictionaries or when translations are not needed immediately.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { $dictionary } from "@alepha/react/i18n";
|
|
13
|
+
*
|
|
14
|
+
* const Example = () => {
|
|
15
|
+
* const { tr } = useI18n<App, "en">();
|
|
16
|
+
* return <div>{tr("hello")}</div>; //
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* class App {
|
|
20
|
+
*
|
|
21
|
+
* en = $dictionary({
|
|
22
|
+
* // { default: { hello: "Hey" } }
|
|
23
|
+
* lazy: () => import("./translations/en.ts"),
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* home = $page({
|
|
27
|
+
* path: "/",
|
|
28
|
+
* component: Example,
|
|
29
|
+
* })
|
|
30
|
+
* }
|
|
31
|
+
*
|
|
32
|
+
* run(App);
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export const $dictionary = <T extends Record<string, string>>(
|
|
36
|
+
options: DictionaryDescriptorOptions<T>,
|
|
37
|
+
): DictionaryDescriptor<T> => {
|
|
38
|
+
return createDescriptor(DictionaryDescriptor<T>, options);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
export interface DictionaryDescriptorOptions<T extends Record<string, string>> {
|
|
44
|
+
lang?: string;
|
|
45
|
+
name?: string;
|
|
46
|
+
lazy: () => Promise<{ default: T }>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
export class DictionaryDescriptor<
|
|
52
|
+
T extends Record<string, string>,
|
|
53
|
+
> extends Descriptor<DictionaryDescriptorOptions<T>> {
|
|
54
|
+
protected provider = $inject(I18nProvider);
|
|
55
|
+
protected onInit() {
|
|
56
|
+
this.provider.registry.push({
|
|
57
|
+
name: this.options.name ?? this.config.propertyKey,
|
|
58
|
+
lang: this.options.lang ?? this.config.propertyKey,
|
|
59
|
+
loader: () => this.options.lazy().then((it) => it.default),
|
|
60
|
+
translations: {},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
$dictionary[KIND] = DictionaryDescriptor;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useInject, useStore } from "@alepha/react";
|
|
2
|
+
import type { DictionaryDescriptor } from "../descriptors/$dictionary.ts";
|
|
3
|
+
import { I18nProvider } from "../providers/I18nProvider.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Hook to access the i18n service.
|
|
7
|
+
*/
|
|
8
|
+
export const useI18n = <
|
|
9
|
+
S extends object,
|
|
10
|
+
K extends keyof ServiceDictionary<S>,
|
|
11
|
+
>(): I18nProvider<S, K> => {
|
|
12
|
+
useStore("alepha.react.i18n.lang");
|
|
13
|
+
return useInject(I18nProvider<S, K>);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type ServiceDictionary<T extends object> = {
|
|
17
|
+
[K in keyof T]: T[K] extends DictionaryDescriptor<infer U> ? U : never;
|
|
18
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { $module } from "alepha";
|
|
2
|
+
import { $dictionary } from "./descriptors/$dictionary.ts";
|
|
3
|
+
import { I18nProvider } from "./providers/I18nProvider.ts";
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
export type { LocalizeProps } from "./components/Localize.tsx";
|
|
8
|
+
export { default as Localize } from "./components/Localize.tsx";
|
|
9
|
+
export * from "./descriptors/$dictionary.ts";
|
|
10
|
+
export * from "./hooks/useI18n.ts";
|
|
11
|
+
export * from "./providers/I18nProvider.ts";
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
declare module "alepha" {
|
|
16
|
+
export interface State {
|
|
17
|
+
"alepha.react.i18n.lang"?: string;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Add i18n support to your Alepha React application. SSR and CSR compatible.
|
|
25
|
+
*
|
|
26
|
+
* It supports lazy loading of translations and provides a context to access the current language.
|
|
27
|
+
*
|
|
28
|
+
* @module alepha.react.i18n
|
|
29
|
+
*/
|
|
30
|
+
export const AlephaReactI18n = $module({
|
|
31
|
+
name: "alepha.react.i18n",
|
|
32
|
+
descriptors: [$dictionary],
|
|
33
|
+
services: [I18nProvider],
|
|
34
|
+
});
|