Skip to content

Commit ad93c32

Browse files
authored
chore: add @modern-js/plugin-i18n for locale router (#7760)
1 parent 8665754 commit ad93c32

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+2313
-1506
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<p align="center">
2+
<a href="https://modernjs.dev" target="blank"><img src="https://lf3-static.bytednsdoc.com/obj/eden-cn/ylaelkeh7nuhfnuhf/modernjs-cover.png" width="300" alt="Modern.js Logo" /></a>
3+
</p>
4+
5+
<h1 align="center">Modern.js</h1>
6+
7+
<p align="center">
8+
A Progressive React Framework for modern web development.
9+
</p>
10+
11+
## Getting Started
12+
13+
Please follow [Quick Start](https://modernjs.dev/en/guides/get-started/quick-start) to get started with Modern.js.
14+
15+
## Documentation
16+
17+
- [English Documentation](https://modernjs.dev/en/)
18+
- [中文文档](https://modernjs.dev)
19+
20+
## Contributing
21+
22+
Please read the [Contributing Guide](https://github.com/web-infra-dev/modern.js/blob/main/CONTRIBUTING.md).
23+
24+
## License
25+
26+
Modern.js is [MIT licensed](https://github.com/web-infra-dev/modern.js/blob/main/LICENSE).
27+
28+
## Credist
29+
30+
Thanks to:
31+
32+
- [@loadable/webpack-plugin](https://github.com/gregberge/loadable-components) to create a webpack plugin prepare for loadable usage in ssr.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const { universalBuildConfig } = require('@scripts/build');
2+
3+
module.exports = {
4+
buildConfig: universalBuildConfig,
5+
};
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
{
2+
"name": "@modern-js/plugin-i18n",
3+
"description": "A Progressive React Framework for modern web development.",
4+
"homepage": "https://modernjs.dev",
5+
"type": "module",
6+
"bugs": "https://github.com/web-infra-dev/modern.js/issues",
7+
"repository": {
8+
"type": "git",
9+
"url": "https://github.com/web-infra-dev/modern.js",
10+
"directory": "packages/runtime/plugin-i18n"
11+
},
12+
"license": "MIT",
13+
"keywords": [
14+
"react",
15+
"framework",
16+
"modern",
17+
"modern.js"
18+
],
19+
"version": "2.68.1",
20+
"engines": {
21+
"node": ">=20"
22+
},
23+
"jsnext:source": "./src/cli/index.ts",
24+
"types": "./src/cli/index.ts",
25+
"main": "./dist/cjs/cli/index.js",
26+
"module": "./dist/esm/cli/index.js",
27+
"exports": {
28+
".": {
29+
"types": "./dist/types/cli/index.d.ts",
30+
"jsnext:source": "./src/cli/index.ts",
31+
"default": "./dist/cjs/cli/index.js"
32+
},
33+
"./package.json": "./package.json",
34+
"./cli": {
35+
"types": "./dist/types/cli/index.d.ts",
36+
"jsnext:source": "./src/cli/index.ts",
37+
"default": "./dist/cjs/cli/index.js"
38+
},
39+
"./runtime": {
40+
"types": "./dist/types/runtime/index.d.ts",
41+
"jsnext:source": "./src/runtime/index.ts",
42+
"default": "./dist/esm/runtime/index.js"
43+
},
44+
"./server": {
45+
"types": "./dist/types/server/index.d.ts",
46+
"jsnext:source": "./src/server/index.ts",
47+
"default": "./dist/esm/server/index.js"
48+
},
49+
"./i18n": {
50+
"types": "./dist/types/runtime/i18n/index.d.ts",
51+
"jsnext:source": "./src/runtime/i18n/index.ts",
52+
"default": "./dist/esm/runtime/i18n/index.js"
53+
}
54+
},
55+
"typesVersions": {
56+
"*": {
57+
".": [
58+
"./dist/types/cli/index.d.ts"
59+
],
60+
"cli": [
61+
"./dist/types/cli/index.d.ts"
62+
],
63+
"runtime": [
64+
"./dist/types/runtime/index.d.ts"
65+
],
66+
"server": [
67+
"./dist/types/server/index.d.ts"
68+
],
69+
"i18n": [
70+
"./dist/types/runtime/i18n/index.d.ts"
71+
]
72+
}
73+
},
74+
"scripts": {
75+
"dev": "modern-lib build --watch",
76+
"prepublishOnly": "only-allow-pnpm",
77+
"new": "modern-lib new",
78+
"build": "modern-lib build",
79+
"test": "jest"
80+
},
81+
"dependencies": {
82+
"@modern-js/plugin": "workspace:*",
83+
"@modern-js/server-runtime": "workspace:*",
84+
"@modern-js/types": "workspace:*",
85+
"@modern-js/utils": "workspace:*",
86+
"@swc/helpers": "^0.5.17"
87+
},
88+
"peerDependencies": {
89+
"react": ">=17",
90+
"react-dom": ">=17.0.2",
91+
"i18next": ">=25.2.1",
92+
"react-i18next": ">=15.5.2",
93+
"@modern-js/runtime": "workspace:^2.68.1"
94+
},
95+
"peerDependenciesMeta": {
96+
"i18next": {
97+
"optional": true
98+
},
99+
"react-i18next": {
100+
"optional": true
101+
}
102+
},
103+
"devDependencies": {
104+
"i18next":"25.2.1",
105+
"react-i18next":"15.5.2",
106+
"@modern-js/runtime": "workspace:*",
107+
"@modern-js/app-tools": "workspace:*",
108+
"@scripts/build": "workspace:*",
109+
"@scripts/jest-config": "workspace:*",
110+
"@testing-library/react": "^13.4.0",
111+
"@types/jest": "^29.5.14",
112+
"@types/node": "^20",
113+
"jest": "^29.7.0",
114+
"react": "^19.2.0",
115+
"react-dom": "^19.2.0",
116+
"ts-jest": "^29.4.5",
117+
"ts-node": "^10.9.2",
118+
"typescript": "^5"
119+
},
120+
"sideEffects": false,
121+
"publishConfig": {
122+
"registry": "https://registry.npmjs.org/",
123+
"access": "public",
124+
"types": "./dist/types/cli/index.d.ts"
125+
}
126+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import type { AppTools, CliPlugin } from '@modern-js/app-tools';
2+
import {
3+
type LocaleDetectionOptions,
4+
getLocaleDetectionOptions,
5+
} from '../utils/config';
6+
7+
export interface I18nPluginOptions {
8+
localeDetection?: LocaleDetectionOptions;
9+
}
10+
11+
export const i18nPlugin = (
12+
options: I18nPluginOptions = {},
13+
): CliPlugin<AppTools> => ({
14+
name: '@modern-js/plugin-i18n',
15+
setup: api => {
16+
const { localeDetection } = options;
17+
api._internalRuntimePlugins(({ entrypoint, plugins }) => {
18+
const localeDetectionOptions = localeDetection
19+
? getLocaleDetectionOptions(entrypoint.entryName, localeDetection)
20+
: undefined;
21+
plugins.push({
22+
name: 'i18n',
23+
path: '@modern-js/plugin-i18n/runtime',
24+
config: {
25+
entryName: entrypoint.entryName,
26+
localeDetection: localeDetectionOptions,
27+
},
28+
});
29+
return {
30+
entrypoint,
31+
plugins,
32+
};
33+
});
34+
35+
api._internalServerPlugins(({ plugins }) => {
36+
plugins.push({
37+
name: '@modern-js/plugin-i18n/server',
38+
options: {
39+
localeDetection,
40+
},
41+
});
42+
return { plugins };
43+
});
44+
},
45+
});
46+
47+
export default i18nPlugin;
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { Link, useInRouterContext, useParams } from '@modern-js/runtime/router';
2+
import type React from 'react';
3+
import { useModernI18n } from './context';
4+
import { buildLocalizedUrl } from './utils';
5+
6+
export interface I18nLinkProps {
7+
to: string;
8+
children: React.ReactNode;
9+
[key: string]: any; // Allow other props to be passed through
10+
}
11+
12+
/**
13+
* I18nLink component that automatically adds language prefix to navigation links.
14+
* This component should be used within a :lang dynamic route context.
15+
*
16+
* @example
17+
* ```tsx
18+
* // When current language is 'en' and to="/about"
19+
* // The actual link will be "/en/about"
20+
* <I18nLink to="/about">About</I18nLink>
21+
*
22+
* // When current language is 'zh' and to="/"
23+
* // The actual link will be "/zh"
24+
* <I18nLink to="/">Home</I18nLink>
25+
* ```
26+
*/
27+
// Use static imports to avoid breaking router tree-shaking. Detect router context via useInRouterContext.
28+
const useRouterHooks = () => {
29+
const inRouter = useInRouterContext();
30+
return {
31+
Link: inRouter ? Link : null,
32+
params: inRouter ? useParams() : ({} as any),
33+
hasRouter: inRouter,
34+
};
35+
};
36+
37+
export const I18nLink: React.FC<I18nLinkProps> = ({
38+
to,
39+
children,
40+
...props
41+
}) => {
42+
const { Link, params, hasRouter } = useRouterHooks();
43+
const { language, supportedLanguages } = useModernI18n();
44+
45+
// Get the current language from context (which reflects the actual current language)
46+
// URL params might be stale after language changes, so we prioritize the context language
47+
const currentLang = language;
48+
49+
// Build the localized URL by adding language prefix
50+
const localizedTo = buildLocalizedUrl(to, currentLang, supportedLanguages);
51+
52+
// In development mode, warn if used outside of :lang route context
53+
if (process.env.NODE_ENV === 'development' && hasRouter && !params.lang) {
54+
console.warn(
55+
'I18nLink is being used outside of a :lang dynamic route context. ' +
56+
'This may cause unexpected behavior. Please ensure I18nLink is used within a route that has a :lang parameter.',
57+
);
58+
}
59+
60+
// If router is not available, render as a regular anchor tag
61+
if (!hasRouter || !Link) {
62+
return (
63+
<a href={localizedTo} {...props}>
64+
{children}
65+
</a>
66+
);
67+
}
68+
69+
return (
70+
<Link to={localizedTo} {...props}>
71+
{children}
72+
</Link>
73+
);
74+
};
75+
76+
export default I18nLink;

0 commit comments

Comments
 (0)