@bleedingdev/modern-js-plugin-i18n 3.4.0-ultramodern.1 → 3.4.0-ultramodern.11

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 (35) hide show
  1. package/dist/cjs/cli/index.js +2 -1
  2. package/dist/cjs/runtime/context.js +8 -0
  3. package/dist/cjs/runtime/core.js +217 -0
  4. package/dist/cjs/runtime/i18n/backend/middleware.node.js +22 -4
  5. package/dist/cjs/runtime/index.js +44 -173
  6. package/dist/cjs/runtime/no-react-i18next.js +76 -0
  7. package/dist/esm/cli/index.mjs +2 -1
  8. package/dist/esm/runtime/context.mjs +8 -0
  9. package/dist/esm/runtime/core.mjs +152 -0
  10. package/dist/esm/runtime/i18n/backend/middleware.node.mjs +19 -4
  11. package/dist/esm/runtime/index.mjs +5 -140
  12. package/dist/esm/runtime/no-react-i18next.mjs +6 -0
  13. package/dist/esm-node/cli/index.mjs +2 -1
  14. package/dist/esm-node/runtime/context.mjs +8 -0
  15. package/dist/esm-node/runtime/core.mjs +153 -0
  16. package/dist/esm-node/runtime/i18n/backend/middleware.node.mjs +19 -4
  17. package/dist/esm-node/runtime/index.mjs +5 -140
  18. package/dist/esm-node/runtime/no-react-i18next.mjs +7 -0
  19. package/dist/types/runtime/context.d.ts +1 -0
  20. package/dist/types/runtime/core.d.ts +30 -0
  21. package/dist/types/runtime/i18n/backend/middleware.node.d.ts +4 -1
  22. package/dist/types/runtime/index.d.ts +2 -24
  23. package/dist/types/runtime/no-react-i18next.d.ts +3 -0
  24. package/package.json +21 -11
  25. package/rstest.config.mts +1 -0
  26. package/src/cli/index.ts +6 -1
  27. package/src/runtime/context.tsx +13 -0
  28. package/src/runtime/core.tsx +360 -0
  29. package/src/runtime/i18n/backend/middleware.node.ts +31 -1
  30. package/src/runtime/index.tsx +4 -316
  31. package/src/runtime/no-react-i18next.tsx +7 -0
  32. package/tests/i18nUtils.test.ts +34 -0
  33. package/tests/localisedUrls.test.ts +35 -0
  34. package/tests/reactI18nextRuntimeBoundary.test.ts +36 -0
  35. package/tests/routerAdapter.test.tsx +58 -1
@@ -1,143 +1,8 @@
1
1
  import "node:module";
2
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
- import { RuntimeContext, isBrowser } from "@modern-js/runtime";
4
- import { Helmet } from "@modern-js/runtime/head";
5
- import { merge } from "@modern-js/runtime-utils/merge";
6
- import { useContext, useEffect, useMemo, useRef, useState } from "react";
7
- import { ModernI18nProvider, useModernI18n } from "./context.mjs";
8
- import { createContextValue, useClientSideRedirect, useLanguageSync, useSdkResourcesLoader } from "./hooks.mjs";
9
- import { getI18nInstance } from "./i18n/index.mjs";
10
- import { mergeBackendOptions } from "./i18n/backend/index.mjs";
11
- import { useI18nextBackend } from "./i18n/backend/middleware.mjs";
12
- import { detectLanguageWithPriority, exportServerLngToWindow, mergeDetectionOptions } from "./i18n/detection/index.mjs";
13
- import { useI18nextLanguageDetector } from "./i18n/detection/middleware.mjs";
14
- import { getI18nextInstanceForProvider } from "./i18n/instance.mjs";
15
- import { changeI18nLanguage, ensureLanguageMatch, initializeI18nInstance, setupClonedInstance } from "./i18n/utils.mjs";
16
- import { buildLocalizedUrl, getPathname, splitUrlTarget } from "./utils.mjs";
17
- import "./types.mjs";
18
- const i18nPlugin = (options)=>({
19
- name: '@modern-js/plugin-i18n',
20
- setup: (api)=>{
21
- const { entryName, i18nInstance: userI18nInstance, initOptions, localeDetection, backend, htmlLangAttr = false, reactI18next = true } = options;
22
- const { localePathRedirect = false, i18nextDetector = true, languages = [], fallbackLanguage = 'en', detection, ignoreRedirectRoutes, localisedUrls } = localeDetection || {};
23
- const { enabled: backendEnabled = false } = backend || {};
24
- let latestI18nInstance;
25
- let I18nextProvider;
26
- const loadReactI18nextIntegration = async ()=>{
27
- if (!reactI18next) return null;
28
- const { getReactI18nextIntegration } = await import("./i18n/react-i18next.mjs");
29
- return getReactI18nextIntegration();
30
- };
31
- api.onBeforeRender(async (context)=>{
32
- let i18nInstance = await getI18nInstance(userI18nInstance);
33
- const { i18n: otherConfig } = api.getRuntimeConfig();
34
- const { initOptions: otherInitOptions } = otherConfig || {};
35
- const userInitOptions = merge(otherInitOptions || {}, initOptions || {});
36
- const reactI18nextIntegration = await loadReactI18nextIntegration();
37
- I18nextProvider = reactI18nextIntegration?.I18nextProvider ?? null;
38
- if (reactI18nextIntegration?.initReactI18next) i18nInstance.use(reactI18nextIntegration.initReactI18next);
39
- const pathname = getPathname(context);
40
- if (i18nextDetector) useI18nextLanguageDetector(i18nInstance);
41
- const mergedDetection = mergeDetectionOptions(i18nextDetector, detection, localePathRedirect, userInitOptions);
42
- const mergedBackend = mergeBackendOptions(backend, userInitOptions);
43
- const hasSdkConfig = 'function' == typeof userInitOptions?.backend?.sdk || mergedBackend?.sdk && 'function' == typeof mergedBackend.sdk;
44
- if (mergedBackend && (backendEnabled || hasSdkConfig)) useI18nextBackend(i18nInstance, mergedBackend);
45
- const { finalLanguage } = await detectLanguageWithPriority(i18nInstance, {
46
- languages,
47
- fallbackLanguage,
48
- localePathRedirect,
49
- i18nextDetector,
50
- detection,
51
- userInitOptions,
52
- mergedBackend,
53
- pathname,
54
- ssrContext: context.ssrContext
55
- });
56
- await initializeI18nInstance(i18nInstance, finalLanguage, fallbackLanguage, languages, mergedDetection, mergedBackend, userInitOptions);
57
- if (!isBrowser() && i18nInstance.cloneInstance) {
58
- i18nInstance = i18nInstance.cloneInstance();
59
- await setupClonedInstance(i18nInstance, finalLanguage, fallbackLanguage, languages, backendEnabled, backend, i18nextDetector, detection, localePathRedirect, userInitOptions);
60
- }
61
- if (localePathRedirect) await ensureLanguageMatch(i18nInstance, finalLanguage);
62
- if (!isBrowser()) exportServerLngToWindow(context, finalLanguage);
63
- context.i18nInstance = i18nInstance;
64
- latestI18nInstance = i18nInstance;
65
- context.changeLanguage = async (newLang)=>{
66
- await changeI18nLanguage(i18nInstance, newLang, {
67
- detectionOptions: mergedDetection
68
- });
69
- };
70
- });
71
- api.wrapRoot((App)=>(props)=>{
72
- const runtimeContext = useContext(RuntimeContext);
73
- const i18nInstance = runtimeContext.i18nInstance || latestI18nInstance;
74
- const initialLang = useMemo(()=>i18nInstance?.language || (localeDetection?.fallbackLanguage ?? 'en'), [
75
- i18nInstance?.language,
76
- localeDetection?.fallbackLanguage
77
- ]);
78
- const [lang, setLang] = useState(initialLang);
79
- const [forceUpdate, setForceUpdate] = useState(0);
80
- const prevLangRef = useRef(lang);
81
- const runtimeContextRef = useRef(runtimeContext);
82
- runtimeContextRef.current = runtimeContext;
83
- useEffect(()=>{
84
- if (i18nInstance?.language) {
85
- const translator = i18nInstance.translator;
86
- if (translator) translator.language = i18nInstance.language;
87
- }
88
- }, [
89
- i18nInstance?.language
90
- ]);
91
- useEffect(()=>{
92
- prevLangRef.current = lang;
93
- }, [
94
- lang
95
- ]);
96
- useSdkResourcesLoader(i18nInstance, setForceUpdate);
97
- useLanguageSync(i18nInstance, localePathRedirect, languages, runtimeContextRef, prevLangRef, setLang);
98
- useClientSideRedirect(i18nInstance, localePathRedirect, languages, fallbackLanguage, ignoreRedirectRoutes, localisedUrls);
99
- const contextValue = useMemo(()=>createContextValue(lang, i18nInstance, entryName, languages, localePathRedirect, ignoreRedirectRoutes, localisedUrls, setLang), [
100
- lang,
101
- i18nInstance,
102
- entryName,
103
- languages,
104
- localePathRedirect,
105
- ignoreRedirectRoutes,
106
- localisedUrls,
107
- forceUpdate
108
- ]);
109
- const children = props.children;
110
- const appContent = /*#__PURE__*/ jsxs(Fragment, {
111
- children: [
112
- Boolean(htmlLangAttr) && /*#__PURE__*/ jsx(Helmet, {
113
- htmlAttributes: {
114
- lang
115
- }
116
- }),
117
- /*#__PURE__*/ jsx(ModernI18nProvider, {
118
- value: contextValue,
119
- children: App ? /*#__PURE__*/ jsx(App, {
120
- ...props,
121
- children: children
122
- }) : children
123
- })
124
- ]
125
- });
126
- if (!i18nInstance) return appContent;
127
- if (I18nextProvider) {
128
- const i18nextInstanceForProvider = getI18nextInstanceForProvider(i18nInstance);
129
- return /*#__PURE__*/ jsx(I18nextProvider, {
130
- i18n: i18nextInstanceForProvider,
131
- children: appContent
132
- });
133
- }
134
- return appContent;
135
- });
136
- }
137
- });
2
+ import { createI18nPlugin } from "./core.mjs";
3
+ import { getReactI18nextIntegration } from "./i18n/react-i18next.mjs";
4
+ export * from "./core.mjs";
5
+ const i18nPlugin = createI18nPlugin(getReactI18nextIntegration);
138
6
  const runtime = i18nPlugin;
139
- export { I18nLink } from "./I18nLink.mjs";
140
- export { Link } from "./Link.mjs";
141
- export { canonicalPath, localizePath, useLocalizedLocation, useLocalizedPaths } from "./localizedPaths.mjs";
142
7
  export default runtime;
143
- export { buildLocalizedUrl, i18nPlugin, splitUrlTarget, useModernI18n };
8
+ export { i18nPlugin };
@@ -0,0 +1,7 @@
1
+ import "node:module";
2
+ import { createI18nPlugin } from "./core.mjs";
3
+ export * from "./core.mjs";
4
+ const i18nPlugin = createI18nPlugin();
5
+ const no_react_i18next = i18nPlugin;
6
+ export default no_react_i18next;
7
+ export { i18nPlugin };
@@ -19,6 +19,7 @@ export declare const ModernI18nProvider: FC<ModernI18nProviderProps>;
19
19
  export interface UseModernI18nReturn {
20
20
  language: string;
21
21
  changeLanguage: (newLang: string) => Promise<void>;
22
+ t: (key: string | string[], ...args: any[]) => string;
22
23
  i18nInstance: I18nInstance;
23
24
  supportedLanguages: string[];
24
25
  localisedUrls?: LocalisedUrlsOption;
@@ -0,0 +1,30 @@
1
+ import { type RuntimePlugin } from '@modern-js/runtime';
2
+ import type React from 'react';
3
+ import type { BaseBackendOptions, BaseLocaleDetectionOptions } from '../shared/type';
4
+ import type { I18nInitOptions, I18nInstance } from './i18n';
5
+ import './types';
6
+ export type { I18nSdkLoader, I18nSdkLoadOptions } from '../shared/type';
7
+ export type { Resources } from './i18n/instance';
8
+ export interface I18nPluginOptions {
9
+ entryName?: string;
10
+ localeDetection?: BaseLocaleDetectionOptions;
11
+ backend?: BaseBackendOptions;
12
+ i18nInstance?: I18nInstance;
13
+ changeLanguage?: (lang: string) => void;
14
+ initOptions?: I18nInitOptions;
15
+ htmlLangAttr?: boolean;
16
+ reactI18next?: boolean;
17
+ [key: string]: any;
18
+ }
19
+ export interface ReactI18nextIntegration {
20
+ I18nextProvider: React.ComponentType<any> | null;
21
+ initReactI18next: any | null;
22
+ }
23
+ export type LoadReactI18nextIntegration = () => Promise<ReactI18nextIntegration | null>;
24
+ export declare const createI18nPlugin: (loadReactI18nextIntegration?: LoadReactI18nextIntegration) => ((options: I18nPluginOptions) => RuntimePlugin);
25
+ export type { AllowedLinkTarget, CanonicalRoutePath, UltramodernCanonicalRoutes, } from './canonicalRoutes';
26
+ export { useModernI18n } from './context';
27
+ export { I18nLink, type I18nLinkProps } from './I18nLink';
28
+ export { Link, type LinkActiveOptions, type LinkBaseProps, type LinkParams, type LinkProps, } from './Link';
29
+ export { canonicalPath, type LocalizedPathsConfig, localizePath, type UseLocalizedLocationReturn, type UseLocalizedPathsReturn, useLocalizedLocation, useLocalizedPaths, } from './localizedPaths';
30
+ export { buildLocalizedUrl, splitUrlTarget } from './utils';
@@ -1,6 +1,8 @@
1
- import Backend from 'i18next-fs-backend/cjs';
2
1
  import type { ExtendedBackendOptions } from '../../../shared/type';
3
2
  import type { I18nInstance } from '../instance';
3
+ type BackendConstructor = new (...args: any[]) => any;
4
+ export declare const resolveFsBackendConstructor: (backendModule: unknown) => BackendConstructor;
5
+ declare const Backend: BackendConstructor;
4
6
  /**
5
7
  * Wrapper for FS backend to add a no-op save method
6
8
  * This is required for i18next-chained-backend to trigger refresh logic
@@ -11,3 +13,4 @@ export declare class FsBackendWithSave extends Backend {
11
13
  }
12
14
  export declare const HttpBackendWithSave: typeof FsBackendWithSave;
13
15
  export declare const useI18nextBackend: (i18nInstance: I18nInstance, backend?: ExtendedBackendOptions) => void;
16
+ export {};
@@ -1,25 +1,3 @@
1
- import { type RuntimePlugin } from '@modern-js/runtime';
2
- import type { BaseBackendOptions, BaseLocaleDetectionOptions } from '../shared/type';
3
- import type { I18nInitOptions, I18nInstance } from './i18n';
4
- import './types';
5
- export type { I18nSdkLoader, I18nSdkLoadOptions } from '../shared/type';
6
- export type { Resources } from './i18n/instance';
7
- export interface I18nPluginOptions {
8
- entryName?: string;
9
- localeDetection?: BaseLocaleDetectionOptions;
10
- backend?: BaseBackendOptions;
11
- i18nInstance?: I18nInstance;
12
- changeLanguage?: (lang: string) => void;
13
- initOptions?: I18nInitOptions;
14
- htmlLangAttr?: boolean;
15
- reactI18next?: boolean;
16
- [key: string]: any;
17
- }
18
- export declare const i18nPlugin: (options: I18nPluginOptions) => RuntimePlugin;
19
- export type { AllowedLinkTarget, CanonicalRoutePath, UltramodernCanonicalRoutes, } from './canonicalRoutes';
20
- export { useModernI18n } from './context';
21
- export { I18nLink, type I18nLinkProps } from './I18nLink';
22
- export { Link, type LinkActiveOptions, type LinkBaseProps, type LinkParams, type LinkProps, } from './Link';
23
- export { canonicalPath, type LocalizedPathsConfig, localizePath, type UseLocalizedLocationReturn, type UseLocalizedPathsReturn, useLocalizedLocation, useLocalizedPaths, } from './localizedPaths';
24
- export { buildLocalizedUrl, splitUrlTarget } from './utils';
1
+ export * from './core';
2
+ export declare const i18nPlugin: (options: import("./core").I18nPluginOptions) => import("@modern-js/runtime").RuntimePlugin;
25
3
  export default i18nPlugin;
@@ -0,0 +1,3 @@
1
+ export * from './core';
2
+ export declare const i18nPlugin: (options: import("./core").I18nPluginOptions) => import("@modern-js/runtime").RuntimePlugin;
3
+ export default i18nPlugin;
package/package.json CHANGED
@@ -17,7 +17,7 @@
17
17
  "modern",
18
18
  "modern.js"
19
19
  ],
20
- "version": "3.4.0-ultramodern.1",
20
+ "version": "3.4.0-ultramodern.11",
21
21
  "engines": {
22
22
  "node": ">=20"
23
23
  },
@@ -48,6 +48,13 @@
48
48
  },
49
49
  "default": "./dist/esm/runtime/index.mjs"
50
50
  },
51
+ "./runtime/no-react-i18next": {
52
+ "types": "./dist/types/runtime/no-react-i18next.d.ts",
53
+ "node": {
54
+ "module": "./dist/esm/runtime/no-react-i18next.mjs"
55
+ },
56
+ "default": "./dist/esm/runtime/no-react-i18next.mjs"
57
+ },
51
58
  "./server": {
52
59
  "types": "./dist/types/server/index.d.ts",
53
60
  "node": {
@@ -72,6 +79,9 @@
72
79
  "runtime": [
73
80
  "./dist/types/runtime/index.d.ts"
74
81
  ],
82
+ "runtime/no-react-i18next": [
83
+ "./dist/types/runtime/no-react-i18next.d.ts"
84
+ ],
75
85
  "server": [
76
86
  "./dist/types/server/index.d.ts"
77
87
  ],
@@ -87,15 +97,15 @@
87
97
  "i18next-fs-backend": "^2.6.6",
88
98
  "i18next-http-backend": "^4.0.0",
89
99
  "i18next-http-middleware": "^3.9.7",
90
- "@modern-js/server-runtime": "npm:@bleedingdev/modern-js-server-runtime@3.4.0-ultramodern.1",
91
- "@modern-js/types": "npm:@bleedingdev/modern-js-types@3.4.0-ultramodern.1",
92
- "@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.4.0-ultramodern.1",
93
- "@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.4.0-ultramodern.1",
94
- "@modern-js/runtime-utils": "npm:@bleedingdev/modern-js-runtime-utils@3.4.0-ultramodern.1",
95
- "@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.4.0-ultramodern.1"
100
+ "@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.4.0-ultramodern.11",
101
+ "@modern-js/runtime-utils": "npm:@bleedingdev/modern-js-runtime-utils@3.4.0-ultramodern.11",
102
+ "@modern-js/types": "npm:@bleedingdev/modern-js-types@3.4.0-ultramodern.11",
103
+ "@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.4.0-ultramodern.11",
104
+ "@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.4.0-ultramodern.11",
105
+ "@modern-js/server-runtime": "npm:@bleedingdev/modern-js-server-runtime@3.4.0-ultramodern.11"
96
106
  },
97
107
  "peerDependencies": {
98
- "@modern-js/runtime": "3.4.0-ultramodern.1",
108
+ "@modern-js/runtime": "3.4.0-ultramodern.11",
99
109
  "i18next": ">=26.3.1",
100
110
  "react": "^19.2.7",
101
111
  "react-dom": "^19.2.7",
@@ -111,7 +121,7 @@
111
121
  },
112
122
  "devDependencies": {
113
123
  "@rslib/core": "0.23.0",
114
- "@types/node": "^26.0.0",
124
+ "@types/node": "^26.0.1",
115
125
  "@typescript/native-preview": "7.0.0-dev.20260624.1",
116
126
  "i18next": "26.3.2",
117
127
  "react": "^19.2.7",
@@ -119,8 +129,8 @@
119
129
  "react-i18next": "17.0.8",
120
130
  "ts-node": "^10.9.2",
121
131
  "typescript": "^6.0.3",
122
- "@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.4.0-ultramodern.1",
123
- "@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.4.0-ultramodern.1"
132
+ "@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.4.0-ultramodern.11",
133
+ "@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.4.0-ultramodern.11"
124
134
  },
125
135
  "sideEffects": false,
126
136
  "publishConfig": {
package/rstest.config.mts CHANGED
@@ -31,6 +31,7 @@ export default {
31
31
  'tests/localisedUrls.test.ts',
32
32
  'tests/linkTypes.test.ts',
33
33
  'tests/backendDefaults.test.ts',
34
+ 'tests/reactI18nextRuntimeBoundary.test.ts',
34
35
  ],
35
36
  extends: commonConfig,
36
37
  }),
package/src/cli/index.ts CHANGED
@@ -126,10 +126,15 @@ export const i18nPlugin = (
126
126
  backend: backendOptions,
127
127
  ...extendedConfig,
128
128
  };
129
+ const runtimePluginPath =
130
+ customPlugin?.runtime?.path ||
131
+ (config.reactI18next === false
132
+ ? `@${metaName}/plugin-i18n/runtime/no-react-i18next`
133
+ : `@${metaName}/plugin-i18n/runtime`);
129
134
 
130
135
  plugins.push({
131
136
  name: customPlugin?.runtime?.name || 'i18n',
132
- path: customPlugin?.runtime?.path || `@${metaName}/plugin-i18n/runtime`,
137
+ path: runtimePluginPath,
133
138
  config,
134
139
  });
135
140
  return {
@@ -53,6 +53,7 @@ export const ModernI18nProvider: FC<ModernI18nProviderProps> = ({
53
53
  export interface UseModernI18nReturn {
54
54
  language: string;
55
55
  changeLanguage: (newLang: string) => Promise<void>;
56
+ t: (key: string | string[], ...args: any[]) => string;
56
57
  i18nInstance: I18nInstance;
57
58
  supportedLanguages: string[];
58
59
  localisedUrls?: LocalisedUrlsOption;
@@ -250,6 +251,17 @@ export const useModernI18n = (): UseModernI18nReturn => {
250
251
  ],
251
252
  );
252
253
 
254
+ const t = useCallback(
255
+ (key: string | string[], ...args: any[]) => {
256
+ if (typeof i18nInstance.t !== 'function') {
257
+ throw new Error('i18nInstance.t is required');
258
+ }
259
+
260
+ return i18nInstance.t(key, ...args) as string;
261
+ },
262
+ [currentLanguage, i18nInstance],
263
+ );
264
+
253
265
  // Helper function to check if a language is supported
254
266
  const isLanguageSupported = useCallback(
255
267
  (lang: string) => {
@@ -310,6 +322,7 @@ export const useModernI18n = (): UseModernI18nReturn => {
310
322
  return {
311
323
  language: currentLanguage,
312
324
  changeLanguage,
325
+ t,
313
326
  i18nInstance,
314
327
  supportedLanguages: languages || [],
315
328
  localisedUrls,