@harpy-js/core 0.4.7 → 0.4.9
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/cli.js +0 -0
- package/dist/client/use-i18n.d.ts +2 -3
- package/dist/client/use-i18n.js +34 -51
- package/dist/core/jsx.engine.d.ts +2 -7
- package/dist/index.d.ts +1 -1
- package/dist/types/jsx.types.d.ts +10 -0
- package/dist/types/jsx.types.js +2 -0
- package/package.json +1 -1
- package/src/client/use-i18n.ts +56 -77
- package/src/core/jsx.engine.ts +7 -6
- package/src/index.ts +1 -1
- package/src/types/jsx.types.ts +30 -0
package/dist/cli.js
CHANGED
|
File without changes
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
interface UseI18nReturn {
|
|
2
|
-
switchLocale: (locale: string) => void
|
|
3
|
-
|
|
4
|
-
buildUrl: (path: string, locale?: string) => string;
|
|
2
|
+
switchLocale: (locale: string) => Promise<void>;
|
|
3
|
+
isLoading: boolean;
|
|
5
4
|
}
|
|
6
5
|
export declare function useI18n(): UseI18nReturn;
|
|
7
6
|
export {};
|
package/dist/client/use-i18n.js
CHANGED
|
@@ -1,64 +1,47 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
|
|
2
|
+
'use client';
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
exports.useI18n = useI18n;
|
|
5
5
|
const react_1 = require("react");
|
|
6
6
|
function useI18n() {
|
|
7
|
-
(0, react_1.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
7
|
+
const [isLoading, setIsLoading] = (0, react_1.useState)(false);
|
|
8
|
+
const switchLocale = async (locale) => {
|
|
9
|
+
if (typeof window === 'undefined')
|
|
10
|
+
return;
|
|
11
|
+
setIsLoading(true);
|
|
12
|
+
try {
|
|
13
|
+
console.log('[useI18n] Switching locale to:', locale);
|
|
14
|
+
const formData = new URLSearchParams();
|
|
15
|
+
formData.append('locale', locale);
|
|
16
|
+
formData.append('redirect', window.location.pathname + window.location.search);
|
|
17
|
+
const response = await fetch('/api/i18n/switch-locale', {
|
|
18
|
+
method: 'POST',
|
|
19
|
+
headers: {
|
|
20
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
21
|
+
},
|
|
22
|
+
body: formData.toString(),
|
|
23
|
+
redirect: 'manual',
|
|
24
|
+
});
|
|
25
|
+
console.log('[useI18n] Response status:', response.status);
|
|
26
|
+
if (response.type === 'opaqueredirect' || response.status === 302 || response.status === 0) {
|
|
27
|
+
window.location.reload();
|
|
28
|
+
}
|
|
29
|
+
else if (response.ok || response.redirected) {
|
|
30
|
+
window.location.reload();
|
|
21
31
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (currentLang && url.origin === window.location.origin && !url.searchParams.has('lang')) {
|
|
26
|
-
url.searchParams.set('lang', currentLang);
|
|
27
|
-
target.href = url.toString();
|
|
28
|
-
}
|
|
32
|
+
else {
|
|
33
|
+
console.error('[useI18n] Unexpected response:', response.status);
|
|
34
|
+
window.location.reload();
|
|
29
35
|
}
|
|
30
|
-
});
|
|
31
|
-
})();
|
|
32
|
-
`;
|
|
33
|
-
document.head.appendChild(script);
|
|
34
36
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
url.searchParams.set("lang", locale);
|
|
41
|
-
window.location.href = url.toString();
|
|
42
|
-
};
|
|
43
|
-
const getCurrentLocale = () => {
|
|
44
|
-
if (typeof window === "undefined")
|
|
45
|
-
return null;
|
|
46
|
-
const url = new URL(window.location.href);
|
|
47
|
-
return url.searchParams.get("lang");
|
|
48
|
-
};
|
|
49
|
-
const buildUrl = (path, locale) => {
|
|
50
|
-
if (typeof window === "undefined")
|
|
51
|
-
return path;
|
|
52
|
-
const currentLocale = locale || getCurrentLocale();
|
|
53
|
-
if (!currentLocale)
|
|
54
|
-
return path;
|
|
55
|
-
const url = new URL(path, window.location.origin);
|
|
56
|
-
url.searchParams.set("lang", currentLocale);
|
|
57
|
-
return url.pathname + url.search;
|
|
37
|
+
catch (err) {
|
|
38
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
39
|
+
console.error('[useI18n] Error:', errorMsg);
|
|
40
|
+
window.location.reload();
|
|
41
|
+
}
|
|
58
42
|
};
|
|
59
43
|
return {
|
|
60
44
|
switchLocale,
|
|
61
|
-
|
|
62
|
-
buildUrl,
|
|
45
|
+
isLoading,
|
|
63
46
|
};
|
|
64
47
|
}
|
|
@@ -1,9 +1,4 @@
|
|
|
1
1
|
import { NestFastifyApplication } from "@nestjs/platform-fastify";
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
export interface JsxLayoutProps {
|
|
5
|
-
children: React.ReactNode;
|
|
6
|
-
meta?: MetaOptions;
|
|
7
|
-
}
|
|
8
|
-
export type JsxLayout = (props: JsxLayoutProps) => React.ReactElement;
|
|
2
|
+
import { JsxLayout, JsxLayoutProps, PageProps } from "../types/jsx.types";
|
|
3
|
+
export type { JsxLayout, JsxLayoutProps, PageProps };
|
|
9
4
|
export declare function withJsxEngine(app: NestFastifyApplication, defaultLayout: JsxLayout): void;
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export { StaticAssetsController } from "./core/static-assets.controller";
|
|
|
7
7
|
export { JsxRender } from "./decorators/jsx.decorator";
|
|
8
8
|
export { WithLayout } from "./decorators/layout.decorator";
|
|
9
9
|
export type { MetaOptions, RenderOptions } from "./decorators/jsx.decorator";
|
|
10
|
-
export type { JsxLayout, JsxLayoutProps } from "./
|
|
10
|
+
export type { JsxLayout, JsxLayoutProps, PageProps } from "./types/jsx.types";
|
|
11
11
|
export { RouterModule } from "./core/router.module";
|
|
12
12
|
export { NavigationService } from "./core/navigation.service";
|
|
13
13
|
export { AutoRegisterModule } from "./core/auto-register.module";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { MetaOptions } from "../decorators/jsx.decorator";
|
|
3
|
+
export interface PageProps {
|
|
4
|
+
[key: string]: any;
|
|
5
|
+
}
|
|
6
|
+
export interface JsxLayoutProps {
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
meta?: MetaOptions;
|
|
9
|
+
}
|
|
10
|
+
export type JsxLayout = (props: JsxLayoutProps) => React.ReactElement;
|
package/package.json
CHANGED
package/src/client/use-i18n.ts
CHANGED
|
@@ -1,111 +1,90 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* React hook for i18n locale switching (Client-side only)
|
|
3
3
|
*
|
|
4
|
-
* This hook provides a
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* The hook updates the URL with the selected locale and reloads the page.
|
|
8
|
-
* The server-side interceptor will automatically set the cookie based on the URL.
|
|
9
|
-
*
|
|
10
|
-
* When used in a component, it automatically injects the language-aware navigation script.
|
|
4
|
+
* This hook provides a Next.js-style server action pattern for locale switching.
|
|
5
|
+
* It posts form data to the server, which sets the locale cookie and returns
|
|
6
|
+
* a redirect URL. Then we navigate to that URL to reload with the new locale.
|
|
11
7
|
*
|
|
12
8
|
* @example
|
|
13
9
|
* ```tsx
|
|
14
10
|
* import { useI18n } from '@harpy-js/core/client';
|
|
15
11
|
*
|
|
16
12
|
* function MyComponent() {
|
|
17
|
-
* const { switchLocale } = useI18n();
|
|
18
|
-
* return
|
|
13
|
+
* const { switchLocale, isLoading } = useI18n();
|
|
14
|
+
* return (
|
|
15
|
+
* <>
|
|
16
|
+
* <button onClick={() => switchLocale('fr')} disabled={isLoading}>
|
|
17
|
+
* {isLoading ? 'Loading...' : 'French'}
|
|
18
|
+
* </button>
|
|
19
|
+
* </>
|
|
20
|
+
* );
|
|
19
21
|
* }
|
|
20
22
|
* ```
|
|
21
23
|
*/
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
'use client';
|
|
24
26
|
|
|
25
|
-
import {
|
|
27
|
+
import { useState } from 'react';
|
|
26
28
|
|
|
27
29
|
interface UseI18nReturn {
|
|
28
|
-
switchLocale: (locale: string) => void
|
|
29
|
-
|
|
30
|
-
buildUrl: (path: string, locale?: string) => string;
|
|
30
|
+
switchLocale: (locale: string) => Promise<void>;
|
|
31
|
+
isLoading: boolean;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
export function useI18n(): UseI18nReturn {
|
|
34
|
-
|
|
35
|
-
useEffect(() => {
|
|
36
|
-
// Mark that i18n is being used in this render
|
|
37
|
-
if (
|
|
38
|
-
typeof window !== "undefined" &&
|
|
39
|
-
!(window as any).__HARPY_I18N_INITIALIZED__
|
|
40
|
-
) {
|
|
41
|
-
(window as any).__HARPY_I18N_INITIALIZED__ = true;
|
|
42
|
-
|
|
43
|
-
// Inject language-aware navigation script
|
|
44
|
-
const script = document.createElement("script");
|
|
45
|
-
script.textContent = `
|
|
46
|
-
(function() {
|
|
47
|
-
if (window.__HARPY_I18N_NAV_INSTALLED__) return;
|
|
48
|
-
window.__HARPY_I18N_NAV_INSTALLED__ = true;
|
|
49
|
-
|
|
50
|
-
document.addEventListener('click', function(e) {
|
|
51
|
-
var target = e.target;
|
|
52
|
-
while (target && target.tagName !== 'A') {
|
|
53
|
-
target = target.parentElement;
|
|
54
|
-
}
|
|
55
|
-
if (target && target.tagName === 'A' && target.href) {
|
|
56
|
-
var url = new URL(target.href, window.location.origin);
|
|
57
|
-
var currentLang = new URLSearchParams(window.location.search).get('lang');
|
|
58
|
-
if (currentLang && url.origin === window.location.origin && !url.searchParams.has('lang')) {
|
|
59
|
-
url.searchParams.set('lang', currentLang);
|
|
60
|
-
target.href = url.toString();
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
})();
|
|
65
|
-
`;
|
|
66
|
-
document.head.appendChild(script);
|
|
67
|
-
}
|
|
68
|
-
}, []);
|
|
35
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
69
36
|
|
|
70
37
|
/**
|
|
71
|
-
* Switch to a new locale by
|
|
38
|
+
* Switch to a new locale by posting to the server endpoint
|
|
39
|
+
* The server will set the locale cookie and return a redirect URL
|
|
72
40
|
*/
|
|
73
|
-
const switchLocale = (locale: string): void => {
|
|
74
|
-
if (typeof window ===
|
|
41
|
+
const switchLocale = async (locale: string): Promise<void> => {
|
|
42
|
+
if (typeof window === 'undefined') return;
|
|
75
43
|
|
|
76
|
-
|
|
77
|
-
url.searchParams.set("lang", locale);
|
|
78
|
-
window.location.href = url.toString();
|
|
79
|
-
};
|
|
44
|
+
setIsLoading(true);
|
|
80
45
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
*/
|
|
84
|
-
const getCurrentLocale = (): string | null => {
|
|
85
|
-
if (typeof window === "undefined") return null;
|
|
46
|
+
try {
|
|
47
|
+
console.log('[useI18n] Switching locale to:', locale);
|
|
86
48
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
49
|
+
// Create FormData to send as application/x-www-form-urlencoded
|
|
50
|
+
const formData = new URLSearchParams();
|
|
51
|
+
formData.append('locale', locale);
|
|
52
|
+
formData.append('redirect', window.location.pathname + window.location.search);
|
|
90
53
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
54
|
+
const response = await fetch('/api/i18n/switch-locale', {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
headers: {
|
|
57
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
58
|
+
},
|
|
59
|
+
body: formData.toString(),
|
|
60
|
+
redirect: 'manual', // Don't follow redirects automatically
|
|
61
|
+
});
|
|
97
62
|
|
|
98
|
-
|
|
99
|
-
if (!currentLocale) return path;
|
|
63
|
+
console.log('[useI18n] Response status:', response.status);
|
|
100
64
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
65
|
+
// Server will return 302 redirect, which fetch sees as status 0 with 'opaqueredirect' type
|
|
66
|
+
// Or it might return the redirect URL in the response body
|
|
67
|
+
if (response.type === 'opaqueredirect' || response.status === 302 || response.status === 0) {
|
|
68
|
+
// Cookie is set, just reload the current page
|
|
69
|
+
window.location.reload();
|
|
70
|
+
} else if (response.ok || response.redirected) {
|
|
71
|
+
// If we got a response, reload
|
|
72
|
+
window.location.reload();
|
|
73
|
+
} else {
|
|
74
|
+
console.error('[useI18n] Unexpected response:', response.status);
|
|
75
|
+
// Try to reload anyway since cookie might be set
|
|
76
|
+
window.location.reload();
|
|
77
|
+
}
|
|
78
|
+
} catch (err) {
|
|
79
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
80
|
+
console.error('[useI18n] Error:', errorMsg);
|
|
81
|
+
// Even on error, try to reload in case cookie was set
|
|
82
|
+
window.location.reload();
|
|
83
|
+
}
|
|
104
84
|
};
|
|
105
85
|
|
|
106
86
|
return {
|
|
107
87
|
switchLocale,
|
|
108
|
-
|
|
109
|
-
buildUrl,
|
|
88
|
+
isLoading,
|
|
110
89
|
};
|
|
111
90
|
}
|
package/src/core/jsx.engine.ts
CHANGED
|
@@ -3,17 +3,18 @@ import { FastifyReply } from "fastify";
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { renderToPipeableStream, renderToString } from "react-dom/server";
|
|
5
5
|
import { MetaOptions, RenderOptions } from "../decorators/jsx.decorator";
|
|
6
|
+
import {
|
|
7
|
+
JsxLayout,
|
|
8
|
+
JsxLayoutProps,
|
|
9
|
+
PageProps,
|
|
10
|
+
} from "../types/jsx.types";
|
|
6
11
|
import { hydrationContext, initializeHydrationContext } from "./hydration";
|
|
7
12
|
import { getChunkPath, getHydrationManifest } from "./hydration-manifest";
|
|
8
13
|
import { LiveReloadController } from "./live-reload.controller";
|
|
9
14
|
import { StaticAssetsController } from "./static-assets.controller";
|
|
10
15
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
meta?: MetaOptions;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export type JsxLayout = (props: JsxLayoutProps) => React.ReactElement;
|
|
16
|
+
// Export types for use in applications
|
|
17
|
+
export type { JsxLayout, JsxLayoutProps, PageProps };
|
|
17
18
|
|
|
18
19
|
// Cache for component-to-chunk path mappings (loaded once at startup)
|
|
19
20
|
const chunkPathCache = new Map<string, string>();
|
package/src/index.ts
CHANGED
|
@@ -15,7 +15,7 @@ export type { MetaOptions, RenderOptions } from "./decorators/jsx.decorator";
|
|
|
15
15
|
// Consumers should import i18n types and modules from that package.
|
|
16
16
|
|
|
17
17
|
// Types
|
|
18
|
-
export type { JsxLayout, JsxLayoutProps } from "./
|
|
18
|
+
export type { JsxLayout, JsxLayoutProps, PageProps } from "./types/jsx.types";
|
|
19
19
|
export { RouterModule } from "./core/router.module";
|
|
20
20
|
export { NavigationService } from "./core/navigation.service";
|
|
21
21
|
export { AutoRegisterModule } from "./core/auto-register.module";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { MetaOptions } from "../decorators/jsx.decorator";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Base props interface for all page components.
|
|
6
|
+
* Extend this interface to add custom props to your pages.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* interface HomePage extends PageProps {
|
|
10
|
+
* items: string[];
|
|
11
|
+
* user?: User;
|
|
12
|
+
* }
|
|
13
|
+
*/
|
|
14
|
+
export interface PageProps {
|
|
15
|
+
[key: string]: any;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Props interface for layout components.
|
|
20
|
+
* Layouts receive the page content as children and optional metadata.
|
|
21
|
+
*/
|
|
22
|
+
export interface JsxLayoutProps {
|
|
23
|
+
children: React.ReactNode;
|
|
24
|
+
meta?: MetaOptions;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Type for layout component functions.
|
|
29
|
+
*/
|
|
30
|
+
export type JsxLayout = (props: JsxLayoutProps) => React.ReactElement;
|