@cogita/plugin-i18n 0.2.0 → 0.3.0
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 +7 -1
- package/dist/index.d.ts +1 -0
- package/dist/plugin.js +36 -0
- package/dist/switcher.js +51 -0
- package/dist/utils.js +40 -2
- package/package.json +11 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @cogita/plugin-i18n
|
|
2
2
|
|
|
3
|
-
Lightweight locale-aware UI copy for Cogita themes and third-party components. The plugin injects a message dictionary at build time
|
|
3
|
+
Lightweight locale-aware UI copy for Cogita themes and third-party components. The plugin injects a message dictionary at build time and renders a language switcher when more than one locale is configured. Markdown content remains a site-level choice and should be authored or built as localized content separately.
|
|
4
4
|
|
|
5
5
|
## Usage
|
|
6
6
|
|
|
@@ -33,4 +33,10 @@ import { t } from 'virtual-cogita-i18n-text';
|
|
|
33
33
|
const label = t('knowledge.home.search', 'Search knowledge');
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
When two or more message dictionaries are configured, the plugin adds a small global language switcher. The selected locale is stored in the browser and mirrored in the `lang` query parameter so a preview can be shared.
|
|
37
|
+
|
|
38
|
+
Set `showSwitcher: false` when the host theme already renders a native locale menu, such as an Rspress documentation site. For a site that publishes localized content incrementally, set `contentFallback: true` together with the Rspress `locales` config. Missing localized Markdown routes then reuse the default-language source until a file such as `content/zh-CN/guides/intro.md` is added. Existing localized files always win over the fallback route.
|
|
39
|
+
|
|
36
40
|
Locale resolution checks the full identifier first (for example, `en-US`), then the language prefix (for example, `en`), and finally `fallbackLocale`. When no message is found, the caller-provided fallback is used, so older sites remain safe when the plugin is not enabled.
|
|
41
|
+
|
|
42
|
+
The content fallback only keeps locale routes reachable; it does not claim that the fallback page is translated. Article titles, Markdown, and user-provided metadata remain site-level content.
|
package/dist/index.d.ts
CHANGED
package/dist/plugin.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import node_path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
1
4
|
import { COGITA_CAPABILITIES, COGITA_VIRTUAL_MODULE_IDS, createCogitaVirtualModule, getCogitaLogger } from "@cogita/shared";
|
|
5
|
+
import { glob } from "glob";
|
|
2
6
|
import { createI18nRuntimeModule, resolveI18nConfig } from "./utils.js";
|
|
3
7
|
function pluginI18n(config) {
|
|
4
8
|
const logger = getCogitaLogger(config);
|
|
@@ -7,6 +11,7 @@ function pluginI18n(config) {
|
|
|
7
11
|
return null;
|
|
8
12
|
}
|
|
9
13
|
const finalConfig = resolveI18nConfig(config.i18n, config.site?.lang);
|
|
14
|
+
const pluginDirectory = node_path.dirname(fileURLToPath(import.meta.url));
|
|
10
15
|
logger.info(`[I18n Plugin] \u{4F7F}\u{7528} ${finalConfig.locale} \u{754C}\u{9762}\u{6587}\u{6848}\u{FF0C}\u{56DE}\u{9000}\u{8BED}\u{8A00}\u{4E3A} ${finalConfig.fallbackLocale}`);
|
|
11
16
|
return {
|
|
12
17
|
name: '@cogita/plugin-i18n',
|
|
@@ -15,6 +20,37 @@ function pluginI18n(config) {
|
|
|
15
20
|
COGITA_CAPABILITIES.UI_I18N
|
|
16
21
|
]
|
|
17
22
|
},
|
|
23
|
+
globalUIComponents: finalConfig.showSwitcher ? [
|
|
24
|
+
node_path.resolve(pluginDirectory, './switcher.js')
|
|
25
|
+
] : void 0,
|
|
26
|
+
async addPages (_rspressConfig, _isProd) {
|
|
27
|
+
if (!config.i18n?.contentFallback || !config.contentDir || !config.locales?.length) return [];
|
|
28
|
+
const defaultLocale = config.site?.lang || config.locales[0]?.lang;
|
|
29
|
+
const localizedLocales = config.locales.map((locale)=>locale.lang).filter((locale)=>locale && locale !== defaultLocale);
|
|
30
|
+
if (!defaultLocale || 0 === localizedLocales.length) return [];
|
|
31
|
+
const contentRoot = node_path.resolve(config.root, config.contentDir);
|
|
32
|
+
const files = await glob('**/*.{md,mdx}', {
|
|
33
|
+
absolute: true,
|
|
34
|
+
cwd: contentRoot,
|
|
35
|
+
ignore: config.locales.map((locale)=>`${locale.lang}/**`),
|
|
36
|
+
nodir: true
|
|
37
|
+
});
|
|
38
|
+
const fallbackPages = [];
|
|
39
|
+
for (const filePath of files){
|
|
40
|
+
const relativePath = node_path.relative(contentRoot, filePath);
|
|
41
|
+
const routePath = relativePath.replace(/\.(md|mdx)$/i, '').replace(/(^|[/\\])index$/i, '$1').split(node_path.sep).join('/');
|
|
42
|
+
const normalizedRoute = routePath ? `/${routePath}` : '/';
|
|
43
|
+
for (const locale of localizedLocales){
|
|
44
|
+
const localizedFile = node_path.join(contentRoot, locale, relativePath);
|
|
45
|
+
if (!existsSync(localizedFile)) fallbackPages.push({
|
|
46
|
+
routePath: `/${locale}${'/' === normalizedRoute ? '/' : normalizedRoute}`,
|
|
47
|
+
filepath: filePath
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (fallbackPages.length > 0) logger.info(`[I18n Plugin] \u{4E3A} ${fallbackPages.length} \u{4E2A}\u{672A}\u{7FFB}\u{8BD1}\u{5185}\u{5BB9}\u{9875}\u{9762}\u{751F}\u{6210}\u{9ED8}\u{8BA4}\u{8BED}\u{8A00}\u{56DE}\u{9000}\u{8DEF}\u{7531}`);
|
|
52
|
+
return fallbackPages;
|
|
53
|
+
},
|
|
18
54
|
addRuntimeModules () {
|
|
19
55
|
return {
|
|
20
56
|
[COGITA_VIRTUAL_MODULE_IDS.I18N_TEXT]: createCogitaVirtualModule(createI18nRuntimeModule(finalConfig))
|
package/dist/switcher.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import react, { useState } from "react";
|
|
2
|
+
import { getLocale, setLocale, supportedLocales, t } from "virtual-cogita-i18n-text";
|
|
3
|
+
const localeLabels = {
|
|
4
|
+
'en-US': 'English',
|
|
5
|
+
'zh-CN': "\u4E2D\u6587"
|
|
6
|
+
};
|
|
7
|
+
function LanguageSwitcher() {
|
|
8
|
+
const [currentLocale, setCurrentLocale] = useState(getLocale);
|
|
9
|
+
if (supportedLocales.length < 2) return null;
|
|
10
|
+
return /*#__PURE__*/ react.createElement("div", {
|
|
11
|
+
"aria-label": t('i18n.switcher.label', 'Language'),
|
|
12
|
+
style: {
|
|
13
|
+
position: 'fixed',
|
|
14
|
+
bottom: 16,
|
|
15
|
+
right: 16,
|
|
16
|
+
zIndex: 1000,
|
|
17
|
+
display: 'inline-flex',
|
|
18
|
+
gap: 2,
|
|
19
|
+
padding: 3,
|
|
20
|
+
border: '1px solid var(--cogita-i18n-border, #e4e8f0)',
|
|
21
|
+
borderRadius: 999,
|
|
22
|
+
background: 'var(--cogita-i18n-surface, rgba(255, 255, 255, 0.92))',
|
|
23
|
+
boxShadow: '0 8px 24px rgba(29, 48, 91, 0.08)',
|
|
24
|
+
backdropFilter: 'blur(12px)'
|
|
25
|
+
}
|
|
26
|
+
}, supportedLocales.map((supportedLocale)=>{
|
|
27
|
+
const active = supportedLocale === currentLocale;
|
|
28
|
+
return /*#__PURE__*/ react.createElement("button", {
|
|
29
|
+
key: supportedLocale,
|
|
30
|
+
type: "button",
|
|
31
|
+
"aria-pressed": active,
|
|
32
|
+
onClick: ()=>{
|
|
33
|
+
setCurrentLocale(supportedLocale);
|
|
34
|
+
setLocale(supportedLocale);
|
|
35
|
+
},
|
|
36
|
+
style: {
|
|
37
|
+
minHeight: 28,
|
|
38
|
+
padding: '0 9px',
|
|
39
|
+
border: 0,
|
|
40
|
+
borderRadius: 999,
|
|
41
|
+
background: active ? 'var(--cogita-i18n-accent, #315efb)' : 'transparent',
|
|
42
|
+
color: active ? '#fff' : 'var(--cogita-i18n-text, #172033)',
|
|
43
|
+
cursor: 'pointer',
|
|
44
|
+
font: 'inherit',
|
|
45
|
+
fontSize: 12,
|
|
46
|
+
fontWeight: 700
|
|
47
|
+
}
|
|
48
|
+
}, localeLabels[supportedLocale] || supportedLocale);
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
export { LanguageSwitcher as default };
|
package/dist/utils.js
CHANGED
|
@@ -17,6 +17,7 @@ function resolveI18nConfig(config, siteLocale) {
|
|
|
17
17
|
const fallbackLocale = normalizeLocale(config?.fallbackLocale, 'en-US');
|
|
18
18
|
return {
|
|
19
19
|
enabled: config?.enabled !== false,
|
|
20
|
+
showSwitcher: config?.showSwitcher !== false,
|
|
20
21
|
locale: normalizeLocale(config?.locale, normalizeLocale(siteLocale, 'en-US')),
|
|
21
22
|
fallbackLocale,
|
|
22
23
|
messages: normalizeMessages(config?.messages)
|
|
@@ -25,9 +26,33 @@ function resolveI18nConfig(config, siteLocale) {
|
|
|
25
26
|
function createI18nRuntimeModule(config) {
|
|
26
27
|
const serializedConfig = JSON.stringify(config);
|
|
27
28
|
return `export const i18nConfig = ${serializedConfig};
|
|
28
|
-
export const locale = i18nConfig.locale;
|
|
29
29
|
export const fallbackLocale = i18nConfig.fallbackLocale;
|
|
30
30
|
export const messages = i18nConfig.messages;
|
|
31
|
+
export const supportedLocales = Object.keys(messages);
|
|
32
|
+
const localeStorageKey = 'cogita-locale';
|
|
33
|
+
|
|
34
|
+
function normalizeRuntimeLocale(value) {
|
|
35
|
+
return typeof value === 'string' ? value.trim().replace(/_/g, '-') : '';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isSupportedLocale(value) {
|
|
39
|
+
return supportedLocales.includes(value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function getLocale() {
|
|
43
|
+
if (typeof window === 'undefined') return i18nConfig.locale;
|
|
44
|
+
const queryLocale = normalizeRuntimeLocale(new URLSearchParams(window.location.search).get('lang'));
|
|
45
|
+
if (isSupportedLocale(queryLocale)) return queryLocale;
|
|
46
|
+
try {
|
|
47
|
+
const storedLocale = normalizeRuntimeLocale(window.localStorage.getItem(localeStorageKey));
|
|
48
|
+
if (isSupportedLocale(storedLocale)) return storedLocale;
|
|
49
|
+
} catch {
|
|
50
|
+
// \u{6D4F}\u{89C8}\u{5668}\u{7981}\u{7528}\u{5B58}\u{50A8}\u{65F6}\u{7EE7}\u{7EED}\u{4F7F}\u{7528}\u{6784}\u{5EFA}\u{671F}\u{8BED}\u{8A00}\u{3002}
|
|
51
|
+
}
|
|
52
|
+
return i18nConfig.locale;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const locale = getLocale();
|
|
31
56
|
|
|
32
57
|
function getLocaleCandidates(value) {
|
|
33
58
|
return value ? [value, value.split('-')[0]] : [];
|
|
@@ -42,7 +67,7 @@ function getMessage(key, candidates) {
|
|
|
42
67
|
}
|
|
43
68
|
|
|
44
69
|
export function t(key, fallback, values) {
|
|
45
|
-
const candidates = [...getLocaleCandidates(
|
|
70
|
+
const candidates = [...getLocaleCandidates(getLocale()), ...getLocaleCandidates(fallbackLocale)];
|
|
46
71
|
let text = getMessage(key, candidates) || fallback || key;
|
|
47
72
|
for (const [name, value] of Object.entries(values || {})) {
|
|
48
73
|
text = text.split('{{' + name + '}}').join(String(value));
|
|
@@ -50,6 +75,19 @@ export function t(key, fallback, values) {
|
|
|
50
75
|
return text;
|
|
51
76
|
}
|
|
52
77
|
|
|
78
|
+
export function setLocale(nextLocale) {
|
|
79
|
+
const normalizedLocale = normalizeRuntimeLocale(nextLocale);
|
|
80
|
+
if (!isSupportedLocale(normalizedLocale) || typeof window === 'undefined') return;
|
|
81
|
+
try {
|
|
82
|
+
window.localStorage.setItem(localeStorageKey, normalizedLocale);
|
|
83
|
+
} catch {
|
|
84
|
+
// \u{6D4F}\u{89C8}\u{5668}\u{7981}\u{7528}\u{5B58}\u{50A8}\u{65F6}\u{4ECD}\u{7136}\u{901A}\u{8FC7} URL \u{53C2}\u{6570}\u{5B8C}\u{6210}\u{672C}\u{6B21}\u{5207}\u{6362}\u{3002}
|
|
85
|
+
}
|
|
86
|
+
const url = new URL(window.location.href);
|
|
87
|
+
url.searchParams.set('lang', normalizedLocale);
|
|
88
|
+
window.location.assign(url.href);
|
|
89
|
+
}
|
|
90
|
+
|
|
53
91
|
export const translate = t;
|
|
54
92
|
`;
|
|
55
93
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cogita/plugin-i18n",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "为 Cogita 主题提供英文优先的界面文案国际化能力。",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cogita",
|
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
"types": "./dist/index.d.ts",
|
|
28
28
|
"import": "./dist/index.js",
|
|
29
29
|
"default": "./dist/index.js"
|
|
30
|
+
},
|
|
31
|
+
"./switcher": {
|
|
32
|
+
"import": "./dist/switcher.js",
|
|
33
|
+
"default": "./dist/switcher.js"
|
|
30
34
|
}
|
|
31
35
|
},
|
|
32
36
|
"main": "./dist/index.js",
|
|
@@ -41,11 +45,17 @@
|
|
|
41
45
|
"node": ">=18.0.0"
|
|
42
46
|
},
|
|
43
47
|
"dependencies": {
|
|
48
|
+
"glob": "^11.0.3",
|
|
44
49
|
"@cogita/shared": "0.13.0"
|
|
45
50
|
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
53
|
+
},
|
|
46
54
|
"devDependencies": {
|
|
47
55
|
"@rslib/core": "0.11.2",
|
|
48
56
|
"@rspress/core": "^1.45.1",
|
|
57
|
+
"@types/react": "^19.1.9",
|
|
58
|
+
"react": "^19.1.1",
|
|
49
59
|
"rsbuild-plugin-publint": "^0.3.3",
|
|
50
60
|
"typescript": "^5.0.4"
|
|
51
61
|
},
|