@bleedingdev/modern-js-renderer-octane 0.0.0-stage → 3.9.3-ultramodern.10
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/LICENSE +21 -0
- package/bin/octane-tsc.mjs +4 -0
- package/dist/cjs/bootstrap.js +68 -0
- package/dist/cjs/client.js +421 -0
- package/dist/cjs/entry-application.js +88 -0
- package/dist/cjs/entry-client.js +95 -0
- package/dist/cjs/entry-server.js +135 -0
- package/dist/cjs/federation-context.js +72 -0
- package/dist/cjs/federation.js +205 -0
- package/dist/cjs/i18n/I18nProvider.js +59 -0
- package/dist/cjs/i18n/LocalizedLink.js +80 -0
- package/dist/cjs/i18n/context.js +41 -0
- package/dist/cjs/i18n/index.js +83 -0
- package/dist/cjs/i18n/languageSync.js +99 -0
- package/dist/cjs/i18n/types.js +18 -0
- package/dist/cjs/i18n/useI18n.js +110 -0
- package/dist/cjs/i18n-external-snapshot.js +645 -0
- package/dist/cjs/index.js +46 -0
- package/dist/cjs/manifest.js +167 -0
- package/dist/cjs/plugin/compiler.js +122 -0
- package/dist/cjs/plugin/create.js +266 -0
- package/dist/cjs/plugin/index.js +151 -0
- package/dist/cjs/plugin/profile.js +91 -0
- package/dist/cjs/plugin/source.js +56 -0
- package/dist/cjs/router-client.js +139 -0
- package/dist/cjs/router-external-snapshot.js +291 -0
- package/dist/cjs/router-handler.js +171 -0
- package/dist/cjs/router-server-cleanup.js +46 -0
- package/dist/cjs/router-server-snapshot.js +405 -0
- package/dist/cjs/router-server.js +100 -0
- package/dist/cjs/router-snapshot.js +270 -0
- package/dist/cjs/router.js +218 -0
- package/dist/cjs/routes.js +190 -0
- package/dist/cjs/server.js +245 -0
- package/dist/cjs/typecheck.js +615 -0
- package/dist/cjs/worker-federation-context.js +63 -0
- package/dist/cjs/worker-federation-contexts.js +97 -0
- package/dist/cjs/worker-federation.js +97 -0
- package/dist/esm/bootstrap.mjs +24 -0
- package/dist/esm/client.mjs +377 -0
- package/dist/esm/entry-application.mjs +44 -0
- package/dist/esm/entry-client.mjs +57 -0
- package/dist/esm/entry-server.mjs +91 -0
- package/dist/esm/federation-context.mjs +25 -0
- package/dist/esm/federation.mjs +167 -0
- package/dist/esm/i18n/I18nProvider.mjs +21 -0
- package/dist/esm/i18n/LocalizedLink.mjs +42 -0
- package/dist/esm/i18n/context.mjs +3 -0
- package/dist/esm/i18n/index.mjs +8 -0
- package/dist/esm/i18n/languageSync.mjs +61 -0
- package/dist/esm/i18n/types.mjs +0 -0
- package/dist/esm/i18n/useI18n.mjs +72 -0
- package/dist/esm/i18n-external-snapshot.mjs +576 -0
- package/dist/esm/index.mjs +8 -0
- package/dist/esm/manifest.mjs +117 -0
- package/dist/esm/router-client.mjs +98 -0
- package/dist/esm/router-external-snapshot.mjs +244 -0
- package/dist/esm/router-handler.mjs +133 -0
- package/dist/esm/router-server-cleanup.mjs +8 -0
- package/dist/esm/router-server-snapshot.mjs +367 -0
- package/dist/esm/router-server.mjs +11 -0
- package/dist/esm/router-snapshot.mjs +199 -0
- package/dist/esm/router.mjs +94 -0
- package/dist/esm/routes.mjs +134 -0
- package/dist/esm/server.mjs +204 -0
- package/dist/esm/worker-federation-context.mjs +19 -0
- package/dist/esm/worker-federation-contexts.mjs +47 -0
- package/dist/esm/worker-federation.mjs +59 -0
- package/dist/esm-node/bootstrap.mjs +24 -0
- package/dist/esm-node/manifest.mjs +117 -0
- package/dist/esm-node/plugin/compiler.mjs +67 -0
- package/dist/esm-node/plugin/create.mjs +228 -0
- package/dist/esm-node/plugin/index.mjs +103 -0
- package/dist/esm-node/plugin/profile.mjs +53 -0
- package/dist/esm-node/plugin/source.mjs +7 -0
- package/dist/esm-node/typecheck.mjs +560 -0
- package/dist/types/bootstrap.d.ts +10 -0
- package/dist/types/client.d.ts +40 -0
- package/dist/types/entry-application.d.ts +19 -0
- package/dist/types/entry-client.d.ts +10 -0
- package/dist/types/entry-server.d.ts +14 -0
- package/dist/types/federation-context.d.ts +28 -0
- package/dist/types/federation.d.ts +20 -0
- package/dist/types/i18n/I18nProvider.d.ts +8 -0
- package/dist/types/i18n/LocalizedLink.d.ts +14 -0
- package/dist/types/i18n/context.d.ts +9 -0
- package/dist/types/i18n/index.d.ts +9 -0
- package/dist/types/i18n/languageSync.d.ts +26 -0
- package/dist/types/i18n/types.d.ts +34 -0
- package/dist/types/i18n/useI18n.d.ts +21 -0
- package/dist/types/i18n-external-snapshot.d.ts +43 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/manifest.d.ts +34 -0
- package/dist/types/plugin/compiler-manifest.d.cts +10 -0
- package/dist/types/plugin/compiler.d.ts +5 -0
- package/dist/types/plugin/create.d.ts +23 -0
- package/dist/types/plugin/index.d.ts +3 -0
- package/dist/types/plugin/profile.d.ts +2 -0
- package/dist/types/plugin/source.d.ts +1 -0
- package/dist/types/plugin/svg-component-template.d.cts +6 -0
- package/dist/types/router-client.d.ts +12 -0
- package/dist/types/router-external-snapshot.d.ts +10 -0
- package/dist/types/router-handler.d.ts +13 -0
- package/dist/types/router-server-cleanup.d.ts +3 -0
- package/dist/types/router-server-snapshot.d.ts +15 -0
- package/dist/types/router-server.d.ts +11 -0
- package/dist/types/router-snapshot.d.ts +14 -0
- package/dist/types/router.d.ts +30 -0
- package/dist/types/routes.d.ts +20 -0
- package/dist/types/server.d.ts +47 -0
- package/dist/types/typecheck.d.ts +31 -0
- package/dist/types/worker-federation-context.d.ts +34 -0
- package/dist/types/worker-federation-contexts.d.ts +25 -0
- package/dist/types/worker-federation.d.ts +20 -0
- package/package.json +278 -4
- package/src/bootstrap.ts +60 -0
- package/src/client.ts +568 -0
- package/src/entry-application.ts +76 -0
- package/src/entry-client.ts +90 -0
- package/src/entry-server.ts +153 -0
- package/src/federation-context.ts +53 -0
- package/src/federation.ts +307 -0
- package/src/i18n/I18nProvider.ts +47 -0
- package/src/i18n/LocalizedLink.ts +86 -0
- package/src/i18n/context.ts +12 -0
- package/src/i18n/index.ts +31 -0
- package/src/i18n/languageSync.ts +132 -0
- package/src/i18n/types.ts +36 -0
- package/src/i18n/useI18n.ts +119 -0
- package/src/i18n-external-snapshot.ts +803 -0
- package/src/index.ts +10 -0
- package/src/manifest.ts +253 -0
- package/src/plugin/compiler-manifest.cjs +204 -0
- package/src/plugin/compiler.ts +104 -0
- package/src/plugin/create.ts +273 -0
- package/src/plugin/index.ts +107 -0
- package/src/plugin/profile.ts +50 -0
- package/src/plugin/source.ts +16 -0
- package/src/plugin/svg-component-template.cjs +17 -0
- package/src/router-client.ts +144 -0
- package/src/router-external-snapshot.ts +363 -0
- package/src/router-handler.ts +185 -0
- package/src/router-server-cleanup.ts +11 -0
- package/src/router-server-snapshot.ts +564 -0
- package/src/router-server.ts +21 -0
- package/src/router-snapshot.ts +251 -0
- package/src/router.ts +192 -0
- package/src/routes.ts +274 -0
- package/src/server.ts +340 -0
- package/src/typecheck.ts +723 -0
- package/src/worker-federation-context.ts +51 -0
- package/src/worker-federation-contexts.ts +91 -0
- package/src/worker-federation.ts +120 -0
- package/README.md +0 -3
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import {
|
|
2
|
+
configuredLanguage,
|
|
3
|
+
localizePath,
|
|
4
|
+
} from '@modern-js/i18n-runtime-extensions/paths';
|
|
5
|
+
import { createElement, hookSlots, type OctaneNode, useContext } from 'octane';
|
|
6
|
+
import {
|
|
7
|
+
type AnyRouter,
|
|
8
|
+
Link,
|
|
9
|
+
type LinkComponentProps,
|
|
10
|
+
useLocation,
|
|
11
|
+
useRouter,
|
|
12
|
+
} from '../router';
|
|
13
|
+
import { I18nContext } from './context';
|
|
14
|
+
|
|
15
|
+
// Plain TypeScript is not rewritten by the Octane compiler: router hooks take
|
|
16
|
+
// their stable slot explicitly.
|
|
17
|
+
const targetLocationSlot = Symbol(hookSlots(1));
|
|
18
|
+
|
|
19
|
+
export interface LocalizedLinkProps
|
|
20
|
+
extends Omit<
|
|
21
|
+
LinkComponentProps<'a', AnyRouter, string, string>,
|
|
22
|
+
'to' | 'mask' | 'href' | 'publicHref' | '_fromLocation'
|
|
23
|
+
> {
|
|
24
|
+
/** A canonical, language-agnostic target, e.g. `/products/red-shoe`. */
|
|
25
|
+
to: string;
|
|
26
|
+
/** Localize for a specific language instead of the current one. */
|
|
27
|
+
language?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The native router owns client navigation, preloading and active state. Its mask
|
|
32
|
+
* keeps the canonical route while publishing the selected language URL;
|
|
33
|
+
* the entry's i18n router subscription synchronizes the instance on navigation.
|
|
34
|
+
*/
|
|
35
|
+
export function LocalizedLink(props: LocalizedLinkProps): OctaneNode {
|
|
36
|
+
const context = useContext(I18nContext);
|
|
37
|
+
if (!context) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
'[renderer-octane] LocalizedLink was used outside of an I18nProvider.',
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
const router = useRouter();
|
|
43
|
+
const location = useLocation(undefined, targetLocationSlot);
|
|
44
|
+
const { language: requestedLanguage, to, children, ...options } = props;
|
|
45
|
+
const language =
|
|
46
|
+
requestedLanguage === undefined
|
|
47
|
+
? context.language
|
|
48
|
+
: configuredLanguage(requestedLanguage, context.languages);
|
|
49
|
+
const externalTarget = to.startsWith('//') || URL.canParse(to);
|
|
50
|
+
const pathname = externalTarget
|
|
51
|
+
? to
|
|
52
|
+
: localizePath(
|
|
53
|
+
router.buildLocation({
|
|
54
|
+
to,
|
|
55
|
+
from: options.from,
|
|
56
|
+
params: options.params,
|
|
57
|
+
unsafeRelative: options.unsafeRelative,
|
|
58
|
+
_fromLocation: location,
|
|
59
|
+
}).pathname,
|
|
60
|
+
language,
|
|
61
|
+
{
|
|
62
|
+
languages: [...context.languages],
|
|
63
|
+
localisedUrls: context.localisedUrls,
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
return createElement(
|
|
68
|
+
Link<AnyRouter, string, string, string, string>,
|
|
69
|
+
{
|
|
70
|
+
...options,
|
|
71
|
+
to: router.options.rewrite ? to : pathname,
|
|
72
|
+
...(router.options.rewrite && !externalTarget
|
|
73
|
+
? {
|
|
74
|
+
mask: {
|
|
75
|
+
to: pathname,
|
|
76
|
+
inheritSearch: true,
|
|
77
|
+
hash: options.hash,
|
|
78
|
+
state: options.state,
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
: {}),
|
|
82
|
+
...(language === context.language ? {} : { hreflang: language }),
|
|
83
|
+
},
|
|
84
|
+
children,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createContext } from 'octane';
|
|
2
|
+
import type { I18nContextValue } from './types';
|
|
3
|
+
|
|
4
|
+
export type { I18nContextValue, I18nInstanceLike } from './types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Octane's `createContext<T>(defaultValue: T)` requires a default (unlike
|
|
8
|
+
* Solid 2's default-less form), so a missing provider is detected by
|
|
9
|
+
* checking for `null` in `useI18n`/`LocalizedLink` instead — the same
|
|
10
|
+
* guard shape `@modern-js/plugin-i18n`'s React `ModernI18nContext` uses.
|
|
11
|
+
*/
|
|
12
|
+
export const I18nContext = createContext<I18nContextValue | null>(null);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export {
|
|
2
|
+
createRequestLanguageRedirect,
|
|
3
|
+
detectRequestLanguage,
|
|
4
|
+
languageFromPathname,
|
|
5
|
+
type RequestLanguageDetection,
|
|
6
|
+
type ResolvedRequestLanguage,
|
|
7
|
+
type ResolveRequestLanguageOptions,
|
|
8
|
+
resolveRequestLanguage,
|
|
9
|
+
} from '@modern-js/i18n-runtime-extensions/request-language';
|
|
10
|
+
export {
|
|
11
|
+
createI18nSsrHandoffInlineData,
|
|
12
|
+
I18N_SSR_HANDOFF_ELEMENT_ID,
|
|
13
|
+
type I18nSsrHandoffPayload,
|
|
14
|
+
type I18nSsrHandoffResources,
|
|
15
|
+
readI18nSsrHandoff,
|
|
16
|
+
} from '@modern-js/i18n-runtime-extensions/ssrLanguageHandoff';
|
|
17
|
+
// Renderer-neutral pieces an application entry composes with these bindings.
|
|
18
|
+
export {
|
|
19
|
+
type CreateI18nUrlRewriteOptions,
|
|
20
|
+
createI18nUrlRewrite,
|
|
21
|
+
type I18nLocationRewrite,
|
|
22
|
+
} from '@modern-js/i18n-runtime-extensions/urlRewrite';
|
|
23
|
+
export { I18nContext } from './context';
|
|
24
|
+
export { I18nProvider, type I18nProviderProps } from './I18nProvider';
|
|
25
|
+
export { LocalizedLink, type LocalizedLinkProps } from './LocalizedLink';
|
|
26
|
+
export {
|
|
27
|
+
type OctaneLatestLanguageSyncOptions,
|
|
28
|
+
useLatestLanguageSync,
|
|
29
|
+
} from './languageSync';
|
|
30
|
+
export type { I18nContextValue, I18nInstanceLike } from './types';
|
|
31
|
+
export { type UseI18nReturn, useI18n } from './useI18n';
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createLatestLanguageSyncBinding,
|
|
3
|
+
type LanguageSyncFailure,
|
|
4
|
+
type LanguageSyncPolicy,
|
|
5
|
+
} from '@modern-js/i18n-runtime-extensions/language-sync/controller';
|
|
6
|
+
import { hookSlots, useCallback, useEffect, useRef } from 'octane';
|
|
7
|
+
|
|
8
|
+
export interface OctaneLatestLanguageSyncOptions<TTarget extends object> {
|
|
9
|
+
changeLanguage: (
|
|
10
|
+
target: TTarget,
|
|
11
|
+
language: string,
|
|
12
|
+
) => PromiseLike<unknown> | unknown;
|
|
13
|
+
commitLanguage: (target: TTarget, language: string) => void;
|
|
14
|
+
desiredLanguage?: string;
|
|
15
|
+
enabled?: boolean;
|
|
16
|
+
policy?: LanguageSyncPolicy;
|
|
17
|
+
readLanguage?: (target: TTarget) => string | undefined;
|
|
18
|
+
reportFailure?: (failure: LanguageSyncFailure) => void;
|
|
19
|
+
target?: TTarget;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Plain TypeScript is not rewritten by the Octane compiler, so each hook call
|
|
23
|
+
// site carries its own stable slot.
|
|
24
|
+
const bindingSlot = Symbol(hookSlots(1));
|
|
25
|
+
const callbacksSlot = Symbol(hookSlots(1));
|
|
26
|
+
const activationSlot = Symbol(hookSlots(1));
|
|
27
|
+
const requestSlot = Symbol(hookSlots(1));
|
|
28
|
+
const retrySlot = Symbol(hookSlots(1));
|
|
29
|
+
const requestCallbackSlot = Symbol(hookSlots(1));
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Octane binding over the fork's renderer-neutral `Coordinator`/`Binding`
|
|
33
|
+
* pair (`@modern-js/i18n-runtime-extensions`'s `createLatestLanguageSyncBinding`):
|
|
34
|
+
* the same "latest intent wins, retry with backoff, report terminal failure"
|
|
35
|
+
* policy the React `useLatestLanguageSync` hook drives.
|
|
36
|
+
*
|
|
37
|
+
* Octane's own hook API (`useState`/`useEffect`/`useContext`/
|
|
38
|
+
* `useSyncExternalStore`, confirmed in `node_modules/octane/dist/runtime.d.ts`)
|
|
39
|
+
* is React-shaped, so this binding mirrors
|
|
40
|
+
* `i18n-extensions/src/language-sync/react.ts`'s `useLatestLanguageSync`
|
|
41
|
+
* directly — the Solid binding needed a `createEffect`/`onCleanup`
|
|
42
|
+
* reshaping for Solid 2's different (compute/effect-split, no
|
|
43
|
+
* `useEffect`-style dependency arrays) reactivity model; Octane does not.
|
|
44
|
+
*/
|
|
45
|
+
export const useLatestLanguageSync = <TTarget extends object>({
|
|
46
|
+
changeLanguage,
|
|
47
|
+
commitLanguage,
|
|
48
|
+
desiredLanguage,
|
|
49
|
+
enabled = true,
|
|
50
|
+
policy,
|
|
51
|
+
readLanguage,
|
|
52
|
+
reportFailure,
|
|
53
|
+
target,
|
|
54
|
+
}: OctaneLatestLanguageSyncOptions<TTarget>): ((language: string) => void) => {
|
|
55
|
+
const bindingRef = useRef<ReturnType<
|
|
56
|
+
typeof createLatestLanguageSyncBinding<TTarget>
|
|
57
|
+
> | null>(null, bindingSlot);
|
|
58
|
+
if (bindingRef.current === null) {
|
|
59
|
+
bindingRef.current = createLatestLanguageSyncBinding<TTarget>(policy);
|
|
60
|
+
}
|
|
61
|
+
const binding = bindingRef.current;
|
|
62
|
+
|
|
63
|
+
useEffect(
|
|
64
|
+
() => {
|
|
65
|
+
binding.updateCallbacks({
|
|
66
|
+
changeLanguage,
|
|
67
|
+
commitLanguage,
|
|
68
|
+
readLanguage,
|
|
69
|
+
reportFailure,
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
undefined,
|
|
73
|
+
callbacksSlot,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
useEffect(
|
|
77
|
+
() => {
|
|
78
|
+
if (!enabled || !target) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
binding.activate(target);
|
|
82
|
+
return () => binding.deactivate();
|
|
83
|
+
},
|
|
84
|
+
[binding, enabled, target],
|
|
85
|
+
activationSlot,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
useEffect(
|
|
89
|
+
() => {
|
|
90
|
+
if (!enabled || !target || !desiredLanguage) {
|
|
91
|
+
binding.clearRequest();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
binding.request(desiredLanguage);
|
|
95
|
+
},
|
|
96
|
+
[binding, desiredLanguage, enabled, target],
|
|
97
|
+
requestSlot,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
useEffect(
|
|
101
|
+
() => {
|
|
102
|
+
if (
|
|
103
|
+
!enabled ||
|
|
104
|
+
!target ||
|
|
105
|
+
!desiredLanguage ||
|
|
106
|
+
typeof window === 'undefined'
|
|
107
|
+
) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const retryCurrentIntent = () => binding.request(desiredLanguage);
|
|
111
|
+
const retryVisibleIntent = () => {
|
|
112
|
+
if (document.visibilityState === 'visible') {
|
|
113
|
+
retryCurrentIntent();
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
window.addEventListener('online', retryCurrentIntent);
|
|
117
|
+
document.addEventListener('visibilitychange', retryVisibleIntent);
|
|
118
|
+
return () => {
|
|
119
|
+
window.removeEventListener('online', retryCurrentIntent);
|
|
120
|
+
document.removeEventListener('visibilitychange', retryVisibleIntent);
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
[binding, desiredLanguage, enabled, target],
|
|
124
|
+
retrySlot,
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
return useCallback(
|
|
128
|
+
(language: string) => binding.request(language),
|
|
129
|
+
[binding],
|
|
130
|
+
requestCallbackSlot,
|
|
131
|
+
);
|
|
132
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { LocalisedUrlsOption } from '@modern-js/i18n-runtime-extensions/paths';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The slice of an i18next-shaped instance this binding needs. Deliberately
|
|
5
|
+
* not `@modern-js/plugin-i18n`'s `I18nInstance`: that type is intentionally
|
|
6
|
+
* narrowed (no index signature, non-overloaded methods — see plugin-i18n's
|
|
7
|
+
* fork notes) and, more importantly, the plugin package's peer dependencies
|
|
8
|
+
* require React/ReactDOM, which an Octane-only application must never need
|
|
9
|
+
* to install. `on`/`off` are real i18next `EventEmitter` methods this
|
|
10
|
+
* interface adds back for the reactive `languageChanged` subscription.
|
|
11
|
+
*/
|
|
12
|
+
export interface I18nInstanceLike {
|
|
13
|
+
language: string;
|
|
14
|
+
t(key: string | string[], options?: Record<string, unknown>): string;
|
|
15
|
+
changeLanguage?(lng?: string): Promise<unknown> | unknown;
|
|
16
|
+
on?(event: 'languageChanged', callback: (lng: string) => void): void;
|
|
17
|
+
off?(event: 'languageChanged', callback: (lng: string) => void): void;
|
|
18
|
+
/** SSR per-request isolation: the server clones a base instance per request. */
|
|
19
|
+
cloneInstance?(options?: Record<string, unknown>): I18nInstanceLike;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface I18nContextValue {
|
|
23
|
+
/** Per-request instance. Never a module-level singleton. */
|
|
24
|
+
instance: I18nInstanceLike;
|
|
25
|
+
languages: readonly string[];
|
|
26
|
+
localisedUrls?: LocalisedUrlsOption;
|
|
27
|
+
/**
|
|
28
|
+
* Reactive current language, owned by `I18nProvider`'s own `useState` +
|
|
29
|
+
* one `languageChanged` subscription. `instance.language` is a plain
|
|
30
|
+
* mutable field, not itself reactive — Octane's context only re-renders
|
|
31
|
+
* consumers when the Provider commits a *new* `value` object (its
|
|
32
|
+
* `$$version` bumps), so every consumer reads this field off the context
|
|
33
|
+
* value instead of reaching into `instance.language` directly.
|
|
34
|
+
*/
|
|
35
|
+
language: string;
|
|
36
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import {
|
|
2
|
+
configuredLanguage,
|
|
3
|
+
localizePath,
|
|
4
|
+
} from '@modern-js/i18n-runtime-extensions/paths';
|
|
5
|
+
import { languageFromPathname } from '@modern-js/i18n-runtime-extensions/request-language';
|
|
6
|
+
import { useContext } from 'octane';
|
|
7
|
+
import { useRouter } from '../router';
|
|
8
|
+
import { I18nContext } from './context';
|
|
9
|
+
import type { I18nInstanceLike } from './types';
|
|
10
|
+
|
|
11
|
+
export interface UseI18nReturn {
|
|
12
|
+
t: I18nInstanceLike['t'];
|
|
13
|
+
/** Current language. Re-renders the calling component on `languageChanged`. */
|
|
14
|
+
language: string;
|
|
15
|
+
languages: readonly string[];
|
|
16
|
+
/** The raw per-request instance, for advanced use (namespaces, `exists`, ...). */
|
|
17
|
+
instance: I18nInstanceLike;
|
|
18
|
+
/**
|
|
19
|
+
* Switches the i18next instance's language. With a router, also navigates to
|
|
20
|
+
* the localized path so the URL and active language stay aligned.
|
|
21
|
+
*/
|
|
22
|
+
changeLanguage: (language: string) => Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Language switches per instance run one at a time: a switch that a newer
|
|
27
|
+
* one has superseded before it starts is skipped, so no two switches ever
|
|
28
|
+
* interleave their language loads and navigations.
|
|
29
|
+
*/
|
|
30
|
+
const languageSwitches = new WeakMap<
|
|
31
|
+
object,
|
|
32
|
+
{ latest: number; queue: Promise<void> }
|
|
33
|
+
>();
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Must render under `I18nProvider`; throws rather than silently reading a
|
|
37
|
+
* fallback instance when no provider is mounted (Octane's `createContext`
|
|
38
|
+
* requires a default value, so `I18nContext`'s default is `null` and this
|
|
39
|
+
* hook turns that into a loud error instead of a shared singleton).
|
|
40
|
+
*/
|
|
41
|
+
export function useI18n(): UseI18nReturn {
|
|
42
|
+
const context = useContext(I18nContext);
|
|
43
|
+
if (!context) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
'[renderer-octane] useI18n() was called outside of an I18nProvider.',
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
const { instance, languages, localisedUrls, language } = context;
|
|
49
|
+
|
|
50
|
+
const t: I18nInstanceLike['t'] = (key, options) => instance.t(key, options);
|
|
51
|
+
|
|
52
|
+
const router = useRouter({ warn: false });
|
|
53
|
+
|
|
54
|
+
const changeLanguage = (requested: string): Promise<void> => {
|
|
55
|
+
// i18next would fall back silently and the URL would gain an unknown
|
|
56
|
+
// segment. An unsupported request is rejected before it supersedes any
|
|
57
|
+
// queued switch.
|
|
58
|
+
let nextLanguage: string;
|
|
59
|
+
try {
|
|
60
|
+
nextLanguage = configuredLanguage(requested, languages);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
return Promise.reject(error);
|
|
63
|
+
}
|
|
64
|
+
const state = languageSwitches.get(instance) ?? {
|
|
65
|
+
latest: 0,
|
|
66
|
+
queue: Promise.resolve(),
|
|
67
|
+
};
|
|
68
|
+
languageSwitches.set(instance, state);
|
|
69
|
+
const generation = ++state.latest;
|
|
70
|
+
const run = state.queue.then(async () => {
|
|
71
|
+
if (state.latest !== generation) return;
|
|
72
|
+
// Earlier switches have settled, so this is the language the current
|
|
73
|
+
// URL was committed with.
|
|
74
|
+
const committed = instance.language;
|
|
75
|
+
// The public URL carries the locale. Under the i18n URL rewrite the
|
|
76
|
+
// router's own location is canonical and has none.
|
|
77
|
+
const publicLanguage = () =>
|
|
78
|
+
router
|
|
79
|
+
? languageFromPathname(
|
|
80
|
+
new URL(router.state.location.publicHref, 'http://localhost')
|
|
81
|
+
.pathname,
|
|
82
|
+
languages,
|
|
83
|
+
router.options.basepath,
|
|
84
|
+
)
|
|
85
|
+
: undefined;
|
|
86
|
+
// A failed, blocked or redirected switch leaves the page on some URL;
|
|
87
|
+
// the language follows that URL's locale, or the committed language on
|
|
88
|
+
// a URL without one.
|
|
89
|
+
const reconcile = async () => {
|
|
90
|
+
const language = publicLanguage() ?? committed;
|
|
91
|
+
if (instance.language !== language)
|
|
92
|
+
await instance.changeLanguage?.(language);
|
|
93
|
+
};
|
|
94
|
+
try {
|
|
95
|
+
await instance.changeLanguage?.(nextLanguage);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
await reconcile();
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
if (!router) return;
|
|
101
|
+
// The full href keeps the query and fragment across the language switch.
|
|
102
|
+
const href = localizePath(router.state.location.href, nextLanguage, {
|
|
103
|
+
languages: [...languages],
|
|
104
|
+
localisedUrls,
|
|
105
|
+
});
|
|
106
|
+
try {
|
|
107
|
+
await router.navigate({ to: '.', href, replace: true });
|
|
108
|
+
} catch (error) {
|
|
109
|
+
await reconcile();
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
if (publicLanguage() !== nextLanguage) await reconcile();
|
|
113
|
+
});
|
|
114
|
+
state.queue = run.catch(() => {});
|
|
115
|
+
return run;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
return { t, language, languages, instance, changeLanguage };
|
|
119
|
+
}
|