@lobehub/chat 1.0.0 → 1.2.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.
Files changed (35) hide show
  1. package/.husky/commit-msg +0 -0
  2. package/CHANGELOG.md +50 -0
  3. package/locales/en_US/common.json +1 -0
  4. package/locales/en_US/setting.json +66 -54
  5. package/locales/zh_CN/common.json +2 -1
  6. package/locales/zh_CN/setting.json +64 -53
  7. package/package.json +5 -3
  8. package/scripts/genDefaultLocale.ts +18 -0
  9. package/scripts/genResources.ts +51 -0
  10. package/scripts/toc.ts +7 -0
  11. package/src/features/AvatarWithUpload/index.tsx +27 -0
  12. package/src/features/FolderPanel/index.tsx +3 -3
  13. package/src/{pages/Sidebar.tsx → features/SideBar/index.tsx} +4 -5
  14. package/src/layout/index.tsx +18 -3
  15. package/src/locales/create.ts +13 -12
  16. package/src/locales/default/setting.ts +15 -3
  17. package/src/locales/options.ts +19 -0
  18. package/src/locales/resources/en_US.ts +0 -2
  19. package/src/locales/resources/index.ts +8 -2
  20. package/src/pages/chat/layout.tsx +2 -2
  21. package/src/pages/setting/SettingForm.tsx +60 -6
  22. package/src/pages/setting/ThemeSwatches/ThemeSwatchesNeutral.tsx +33 -0
  23. package/src/pages/setting/ThemeSwatches/ThemeSwatchesPrimary.tsx +33 -0
  24. package/src/pages/setting/ThemeSwatches/index.ts +2 -0
  25. package/src/pages/setting/index.page.tsx +2 -2
  26. package/src/store/settings/action.ts +10 -5
  27. package/src/store/settings/initialState.ts +7 -5
  28. package/src/store/settings/selectors.ts +9 -1
  29. package/src/store/settings/store.ts +2 -2
  30. package/src/styles/antdOverride.ts +15 -11
  31. package/src/types/exportConfig.ts +10 -3
  32. package/src/types/locale.ts +1 -7
  33. package/tsconfig.json +6 -1
  34. package/scripts/genDefaultLocale.mjs +0 -12
  35. package/scripts/toc.mjs +0 -40
package/.husky/commit-msg CHANGED
File without changes
package/CHANGELOG.md CHANGED
@@ -2,6 +2,56 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 1.2.0](https://github.com/lobehub/lobe-chat/compare/v1.1.0...v1.2.0)
6
+
7
+ <sup>Released on **2023-07-18**</sup>
8
+
9
+ #### ✨ Features
10
+
11
+ - **misc**: Add new components, modify import statements, and update CSS styles.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's improved
19
+
20
+ - **misc**: Add new components, modify import statements, and update CSS styles ([9b261db](https://github.com/lobehub/lobe-chat/commit/9b261db))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
30
+ ## [Version 1.1.0](https://github.com/lobehub/lobe-chat/compare/v1.0.0...v1.1.0)
31
+
32
+ <sup>Released on **2023-07-18**</sup>
33
+
34
+ #### ✨ Features
35
+
36
+ - **misc**: Add i18n auto flow `pnpm run i18n`.
37
+
38
+ <br/>
39
+
40
+ <details>
41
+ <summary><kbd>Improvements and Fixes</kbd></summary>
42
+
43
+ #### What's improved
44
+
45
+ - **misc**: Add i18n auto flow `pnpm run i18n` ([e18fc57](https://github.com/lobehub/lobe-chat/commit/e18fc57))
46
+
47
+ </details>
48
+
49
+ <div align="right">
50
+
51
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
52
+
53
+ </div>
54
+
5
55
  ## Version 1.0.0
6
56
 
7
57
  <sup>Released on **2023-07-18**</sup>
@@ -13,6 +13,7 @@
13
13
  "agentTagPlaceholder": "Please enter a tag",
14
14
  "archive": "Archive",
15
15
  "autoGenerate": "Auto Generate",
16
+ "autoGenerateTooltip": "Description of the autocomplete assistant based on prompts",
16
17
  "cancel": "Cancel",
17
18
  "close": "Close",
18
19
  "confirmRemoveSessionItemAlert": "You are about to remove this agent. Once removed, it cannot be recovered. Please confirm your action.",
@@ -1,97 +1,109 @@
1
1
  {
2
2
  "danger": {
3
- "reset": {
4
- "title": "Reset All Settings",
5
- "desc": "Reset all settings to default values",
6
- "action": "Reset Now",
7
- "confirm": "Confirm reset all settings?",
8
- "currentVersion": "Current Version"
9
- },
10
3
  "clear": {
11
- "title": "Clear All Data",
12
- "desc": "Clear all chat and settings data",
13
4
  "action": "Clear Now",
14
- "confirm": "Confirm clear all chat and settings data?"
5
+ "confirm": "Are you sure you want to clear all chat and settings data?",
6
+ "desc": "Clear all chat and settings data",
7
+ "title": "Clear All Data"
8
+ },
9
+ "reset": {
10
+ "action": "Reset Now",
11
+ "confirm": "Are you sure you want to reset all settings?",
12
+ "currentVersion": "Current Version",
13
+ "desc": "Reset all settings to default values",
14
+ "title": "Reset All Settings"
15
15
  }
16
16
  },
17
17
  "header": "Settings",
18
18
  "settingChat": {
19
- "title": "Chat Settings",
20
- "inputTemplate": {
21
- "title": "User Input Template",
22
- "desc": "The latest user message will be filled into this template"
23
- },
24
19
  "compressThreshold": {
25
- "title": "History Message Length Compression Threshold",
26
- "desc": "When the uncompressed history message exceeds this value, it will be compressed"
20
+ "desc": "When the uncompressed chat history exceeds this value, it will be compressed",
21
+ "title": "Chat History Compression Threshold"
27
22
  },
28
23
  "historyCount": {
29
- "title": "Number of History Messages",
30
- "desc": "Number of history messages carried in each request"
24
+ "desc": "Number of chat history messages carried in each request",
25
+ "title": "History Message Count"
26
+ },
27
+ "inputTemplate": {
28
+ "desc": "The latest user message will be filled into this template",
29
+ "title": "User Input Preprocessing"
31
30
  },
32
31
  "maxTokens": {
33
- "title": "Max Tokens per Response",
34
- "desc": "The maximum number of tokens used for each interaction"
32
+ "desc": "Maximum number of tokens used for each interaction",
33
+ "title": "Reply Limit (max_tokens)"
35
34
  },
36
35
  "sendKey": {
37
36
  "title": "Send Key"
38
- }
37
+ },
38
+ "title": "Chat Settings"
39
39
  },
40
40
  "settingModel": {
41
- "title": "Model Settings",
41
+ "frequencyPenalty": {
42
+ "desc": "The higher the value, the more likely it is to reduce repeated words",
43
+ "title": "Frequency Penalty (frequency_penalty)"
44
+ },
42
45
  "model": {
43
46
  "title": "Model"
44
47
  },
48
+ "presencePenalty": {
49
+ "desc": "The higher the value, the more likely it is to expand to new topics",
50
+ "title": "Topic Freshness (presence_penalty)"
51
+ },
45
52
  "temperature": {
46
- "title": "Randomness (temperature)",
47
- "desc": "The higher the value, the more random the response"
53
+ "desc": "The higher the value, the more random the reply",
54
+ "title": "Randomness (temperature)"
48
55
  },
56
+ "title": "Model Settings",
49
57
  "topP": {
50
- "title": "Nucleus Sampling (top_p)",
51
- "desc": "Similar to randomness, but do not change it together with randomness"
52
- },
53
- "presencePenalty": {
54
- "title": "Topic Freshness (presence_penalty)",
55
- "desc": "The higher the value, the more likely it is to expand to new topics"
56
- },
57
- "frequencyPenalty": {
58
- "title": "Frequency Penalty (frequency_penalty)",
59
- "desc": "The higher the value, the more likely it is to reduce repeated words"
58
+ "desc": "Similar to randomness, but do not change together with randomness",
59
+ "title": "Nucleus Sampling (top_p)"
60
60
  }
61
61
  },
62
62
  "settingOpenAI": {
63
+ "endpoint": {
64
+ "desc": "Must include http(s)://, in addition to the default address",
65
+ "title": "API Endpoint"
66
+ },
63
67
  "title": "OpenAI Settings",
64
68
  "token": {
65
- "title": "API Key",
66
- "desc": "Use your own key to bypass password access restrictions",
67
- "placeholder": "OpenAI API Key"
68
- },
69
- "endpoint": {
70
- "title": "API Endpoint",
71
- "desc": "In addition to the default address, it must include http(s)://"
69
+ "desc": "Use your own Key to bypass password access restrictions",
70
+ "placeholder": "OpenAI API Key",
71
+ "title": "API Key"
72
72
  }
73
73
  },
74
74
  "settingSystem": {
75
- "title": "System Settings",
76
75
  "accessCode": {
77
- "title": "Access Code",
78
76
  "desc": "Encryption access has been enabled by the administrator",
79
- "placeholder": "Please enter the access code"
80
- }
77
+ "placeholder": "Please enter the access password",
78
+ "title": "Access Password"
79
+ },
80
+ "title": "System Settings"
81
81
  },
82
82
  "settingTheme": {
83
- "title": "Theme Settings",
84
83
  "avatar": {
85
- "title": "Avatar",
86
- "desc": "Supports URL / Base64 / Emoji"
84
+ "title": "Avatar"
87
85
  },
88
86
  "fontSize": {
89
- "title": "Font Size",
90
- "desc": "Font size of chat content"
87
+ "desc": "Font size of chat content",
88
+ "title": "Font Size"
91
89
  },
92
90
  "lang": {
93
- "name": "Language Settings",
94
- "all": "All Languages"
95
- }
91
+ "title": "Language Settings"
92
+ },
93
+ "neutralColor": {
94
+ "desc": "Custom grayscale for different color tendencies",
95
+ "title": "Neutral Color"
96
+ },
97
+ "primaryColor": {
98
+ "desc": "Custom theme color",
99
+ "title": "Theme Color"
100
+ },
101
+ "themeMode": {
102
+ "auto": "Auto",
103
+ "dark": "Dark",
104
+ "light": "Light",
105
+ "title": "Theme"
106
+ },
107
+ "title": "Theme Settings"
96
108
  }
97
109
  }
@@ -13,6 +13,7 @@
13
13
  "agentTagPlaceholder": "请输入标签",
14
14
  "archive": "归档",
15
15
  "autoGenerate": "自动补全",
16
+ "autoGenerateTooltip": "基于提示词自动补全助手描述",
16
17
  "cancel": "取消",
17
18
  "close": "关闭",
18
19
  "confirmRemoveSessionItemAlert": "即将删除该助手,删除后该将无法找回,请确认你的操作",
@@ -29,7 +30,7 @@
29
30
  "newAgent": "新建助手",
30
31
  "noDescription": "暂无描述",
31
32
  "ok": "确定",
32
- "profile": "身份卡",
33
+ "profile": "助手身份",
33
34
  "reset": "重置",
34
35
  "searchAgentPlaceholder": "搜索助手和对话...",
35
36
  "sessionSetting": "会话设置",
@@ -1,98 +1,109 @@
1
1
  {
2
2
  "danger": {
3
+ "clear": {
4
+ "action": "立即清除",
5
+ "confirm": "确认清除所有聊天、设置数据?",
6
+ "desc": "清除所有聊天、设置数据",
7
+ "title": "清除所有数据"
8
+ },
3
9
  "reset": {
4
- "title": "重置所有设置",
5
- "desc": "重置所有设置项回默认值",
6
10
  "action": "立即重置",
7
11
  "confirm": "确认重置所有设置?",
8
- "currentVersion": "当前版本"
9
- },
10
- "clear": {
11
- "title": "清除所有数据",
12
- "desc": "清除所有聊天、设置数据",
13
- "action": "立即清除",
14
- "confirm": "确认清除所有聊天、设置数据?"
12
+ "currentVersion": "当前版本",
13
+ "desc": "重置所有设置项回默认值",
14
+ "title": "重置所有设置"
15
15
  }
16
16
  },
17
-
18
17
  "header": "设置",
19
18
  "settingChat": {
20
- "title": "聊天设置",
21
- "inputTemplate": {
22
- "title": "用户输入预处理",
23
- "desc": "用户最新的一条消息会填充到此模板"
24
- },
25
19
  "compressThreshold": {
26
- "title": "历史消息长度压缩阈值",
27
- "desc": "当未压缩的历史消息超过该值时,将进行压缩"
20
+ "desc": "当未压缩的历史消息超过该值时,将进行压缩",
21
+ "title": "历史消息长度压缩阈值"
28
22
  },
29
23
  "historyCount": {
30
- "title": "附带历史消息数",
31
- "desc": "每次请求携带的历史消息数"
24
+ "desc": "每次请求携带的历史消息数",
25
+ "title": "附带历史消息数"
26
+ },
27
+ "inputTemplate": {
28
+ "desc": "用户最新的一条消息会填充到此模板",
29
+ "title": "用户输入预处理"
32
30
  },
33
31
  "maxTokens": {
34
- "title": "单次回复限制 (max_tokens)",
35
- "desc": "单次交互所用的最大 Token 数"
32
+ "desc": "单次交互所用的最大 Token 数",
33
+ "title": "单次回复限制 (max_tokens)"
36
34
  },
37
35
  "sendKey": {
38
36
  "title": "发送键"
39
- }
37
+ },
38
+ "title": "聊天设置"
40
39
  },
41
40
  "settingModel": {
42
- "title": "模型设置",
41
+ "frequencyPenalty": {
42
+ "desc": "值越大,越有可能降低重复字词",
43
+ "title": "频率惩罚度 (frequency_penalty)"
44
+ },
43
45
  "model": {
44
46
  "title": "模型"
45
47
  },
48
+ "presencePenalty": {
49
+ "desc": "值越大,越有可能扩展到新话题",
50
+ "title": "话题新鲜度 (presence_penalty)"
51
+ },
46
52
  "temperature": {
47
- "title": "随机性 (temperature)",
48
- "desc": "值越大,回复越随机"
53
+ "desc": "值越大,回复越随机",
54
+ "title": "随机性 (temperature)"
49
55
  },
56
+ "title": "模型设置",
50
57
  "topP": {
51
- "title": "核采样 (top_p)",
52
- "desc": "与随机性类似,但不要和随机性一起更改"
53
- },
54
- "presencePenalty": {
55
- "title": "话题新鲜度 (presence_penalty)",
56
- "desc": "值越大,越有可能扩展到新话题"
57
- },
58
- "frequencyPenalty": {
59
- "title": "频率惩罚度 (frequency_penalty)",
60
- "desc": "值越大,越有可能降低重复字词"
58
+ "desc": "与随机性类似,但不要和随机性一起更改",
59
+ "title": "核采样 (top_p)"
61
60
  }
62
61
  },
63
62
  "settingOpenAI": {
63
+ "endpoint": {
64
+ "desc": "除默认地址外,必须包含 http(s)://",
65
+ "title": "接口地址"
66
+ },
64
67
  "title": "OpenAI 设置",
65
68
  "token": {
66
- "title": "API Key",
67
69
  "desc": "使用自己的 Key 可绕过密码访问限制",
68
- "placeholder": "OpenAI API Key"
69
- },
70
- "endpoint": {
71
- "title": "接口地址",
72
- "desc": "除默认地址外,必须包含 http(s)://"
70
+ "placeholder": "OpenAI API Key",
71
+ "title": "API Key"
73
72
  }
74
73
  },
75
74
  "settingSystem": {
76
- "title": "系统设置",
77
75
  "accessCode": {
78
- "title": "访问密码",
79
76
  "desc": "管理员已开启加密访问",
80
- "placeholder": "请输入访问密码"
81
- }
77
+ "placeholder": "请输入访问密码",
78
+ "title": "访问密码"
79
+ },
80
+ "title": "系统设置"
82
81
  },
83
82
  "settingTheme": {
84
- "title": "主题设置",
85
83
  "avatar": {
86
- "title": "头像",
87
- "desc": "支持 URL / Base64 / Emoji 表情符号"
84
+ "title": "头像"
88
85
  },
89
86
  "fontSize": {
90
- "title": "字体大小",
91
- "desc": "聊天内容的字体大小"
87
+ "desc": "聊天内容的字体大小",
88
+ "title": "字体大小"
92
89
  },
93
90
  "lang": {
94
- "name": "语言设置",
95
- "all": "所有语言"
96
- }
91
+ "title": "语言设置"
92
+ },
93
+ "neutralColor": {
94
+ "desc": "不同色彩倾向的灰阶自定义",
95
+ "title": "中性色"
96
+ },
97
+ "primaryColor": {
98
+ "desc": "自定义主题色",
99
+ "title": "主题色"
100
+ },
101
+ "themeMode": {
102
+ "auto": "自动",
103
+ "dark": "深色",
104
+ "light": "浅色",
105
+ "title": "主题"
106
+ },
107
+ "title": "主题设置"
97
108
  }
98
109
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Lobe Chat is an open-source chatbot client using LangChain, Typescript and Next.js",
5
5
  "keywords": [
6
6
  "chatbot",
@@ -23,7 +23,8 @@
23
23
  "scripts": {
24
24
  "build": "next build",
25
25
  "dev": "next dev -p 3010",
26
- "i18n": "lobe-i18n",
26
+ "i18n": "npm run i18n:toc && lobe-i18n",
27
+ "i18n:toc": "ts-node --project ./tsconfig.json scripts/toc.ts",
27
28
  "lint": "eslint \"{src,tests}/**/*.{js,jsx,ts,tsx}\" --fix",
28
29
  "lint:md": "remark . --quiet --frail --output",
29
30
  "lint:style": "stylelint \"{src,tests}/**/*.{js,jsx,ts,tsx}\" --fix",
@@ -35,7 +36,6 @@
35
36
  "test": "vitest --passWithNoTests",
36
37
  "test:coverage": "vitest run --coverage --passWithNoTests",
37
38
  "test:update": "vitest -u",
38
- "toc": "node scripts/toc.mjs",
39
39
  "type-check": "tsc --noEmit"
40
40
  },
41
41
  "lint-staged": {
@@ -107,6 +107,7 @@
107
107
  "@umijs/lint": "^4",
108
108
  "@vitest/coverage-v8": "latest",
109
109
  "commitlint": "^17",
110
+ "consola": "^3",
110
111
  "eslint": "^8",
111
112
  "husky": "^8",
112
113
  "i18next-browser-languagedetector": "^7",
@@ -122,6 +123,7 @@
122
123
  "semantic-release": "^21",
123
124
  "semantic-release-config-gitmoji": "^1",
124
125
  "stylelint": "^15",
126
+ "ts-node": "^10",
125
127
  "typescript": "^5",
126
128
  "vitest": "latest"
127
129
  },
@@ -0,0 +1,18 @@
1
+ import { consola } from 'consola';
2
+ import { colors } from 'consola/utils';
3
+ import fs from 'node:fs';
4
+ import { resolve } from 'node:path';
5
+
6
+ import i18nConfig from '../.i18nrc';
7
+
8
+ export const genDefaultLocale = (input: string) => {
9
+ consola.info(`Default locale is ${i18nConfig.entryLocale}...`);
10
+ const resources = require(`../${input}/${i18nConfig.entryLocale}`);
11
+ const data = Object.entries(resources.default);
12
+ consola.start(`Generate default locale json, found ${data.length} namespaces...`);
13
+ for (const [ns, value] of data) {
14
+ const filepath = resolve(i18nConfig.output, i18nConfig.entryLocale, `${ns}.json`);
15
+ fs.writeFileSync(filepath, JSON.stringify(value, null, 2));
16
+ consola.success(colors.bgWhiteBright(colors.black(` ${ns} `)), colors.gray(filepath));
17
+ }
18
+ };
@@ -0,0 +1,51 @@
1
+ import { consola } from 'consola';
2
+ import { colors } from 'consola/utils';
3
+ import { tocForResources } from 'i18next-resources-for-ts';
4
+ import fs from 'node:fs';
5
+ import { resolve } from 'node:path';
6
+
7
+ import i18nConfig from '../.i18nrc.js';
8
+
9
+ const locales = [i18nConfig.entryLocale, ...i18nConfig.outputLocales];
10
+ export const genResources = (output: string) => {
11
+ let index = '';
12
+ let indexObj = '';
13
+ consola.start(`Generate locale resources and types, found ${locales.length} locales...`);
14
+ for (const locale of locales) {
15
+ const files = fs
16
+ .readdirSync(resolve(i18nConfig.output, locale))
17
+ .filter((name) => name.includes('.json'));
18
+ index += `import ${locale} from "./${locale}";\n`;
19
+ indexObj += ` "${locale.replace('_', '-')}": ${locale},\n`;
20
+ const ns = [];
21
+ for (const file of files) {
22
+ ns.push({
23
+ name: file.replace('.json', ''),
24
+ path: resolve(i18nConfig.output, locale, file),
25
+ });
26
+ }
27
+ let toc = tocForResources(ns, resolve(output)).replaceAll('\\', '/');
28
+ if (locale === i18nConfig.entryLocale) {
29
+ toc = toc.replaceAll('.json', '').replaceAll('../../../locales/zh_CN', '../default');
30
+ }
31
+ const filepath = resolve(output, `${locale}.ts`);
32
+ fs.writeFileSync(filepath, toc);
33
+ consola.success(colors.bgBlue(colors.black(` ${locale} `)), colors.gray(filepath));
34
+ }
35
+ const indexFilepath = resolve(output, `index.ts`);
36
+ fs.writeFileSync(
37
+ indexFilepath,
38
+ `${index}
39
+ const resources = {
40
+ ${indexObj}} as const;
41
+ export default resources;
42
+ export const defaultResources = ${i18nConfig.entryLocale};
43
+ export type Resources = typeof resources;
44
+ export type DefaultResources = typeof defaultResources;
45
+ export type Namespaces = keyof DefaultResources;
46
+ export type Locales = keyof Resources;
47
+ `,
48
+ );
49
+
50
+ consola.success(colors.bgGreen(colors.black(` INDEX `)), colors.gray(indexFilepath));
51
+ };
package/scripts/toc.ts ADDED
@@ -0,0 +1,7 @@
1
+ import { genDefaultLocale } from './genDefaultLocale';
2
+ import { genResources } from './genResources';
3
+
4
+ const RES_OUTPUT = 'src/locales/resources';
5
+
6
+ genDefaultLocale(RES_OUTPUT);
7
+ genResources(RES_OUTPUT);
@@ -0,0 +1,27 @@
1
+ import { Avatar, Logo } from '@lobehub/ui';
2
+ import { Upload } from 'antd';
3
+ import { memo } from 'react';
4
+ import { shallow } from 'zustand/shallow';
5
+
6
+ import { useSettings } from '@/store/settings';
7
+ import { createUploadImageHandler } from '@/utils/uploadFIle';
8
+
9
+ interface AvatarWithUploadProps {
10
+ size?: number;
11
+ }
12
+
13
+ export default memo<AvatarWithUploadProps>(({ size = 40 }) => {
14
+ const [avatar, setSettings] = useSettings((st) => [st.settings.avatar, st.setSettings], shallow);
15
+
16
+ const handleUploadAvatar = createUploadImageHandler((avatar) => {
17
+ setSettings({ avatar });
18
+ });
19
+
20
+ return (
21
+ <div style={{ maxHeight: size, maxWidth: size }}>
22
+ <Upload beforeUpload={handleUploadAvatar} itemRender={() => void 0} maxCount={1}>
23
+ {avatar ? <Avatar avatar={avatar} size={size} /> : <Logo size={size} />}
24
+ </Upload>
25
+ </div>
26
+ );
27
+ });
@@ -1,7 +1,7 @@
1
1
  import { DraggablePanel } from '@lobehub/ui';
2
2
  import { createStyles } from 'antd-style';
3
3
  import isEqual from 'fast-deep-equal';
4
- import { PropsWithChildren, useState } from 'react';
4
+ import { PropsWithChildren, memo, useState } from 'react';
5
5
  import { shallow } from 'zustand/shallow';
6
6
 
7
7
  import { useSettings } from '@/store/settings';
@@ -14,7 +14,7 @@ export const useStyles = createStyles(({ css, token }) => ({
14
14
  `,
15
15
  }));
16
16
 
17
- export default ({ children }: PropsWithChildren) => {
17
+ export default memo<PropsWithChildren>(({ children }) => {
18
18
  const { styles } = useStyles();
19
19
  const [sessionsWidth, sessionExpandable] = useSettings(
20
20
  (s) => [s.sessionsWidth, s.sessionExpandable],
@@ -52,4 +52,4 @@ export default ({ children }: PropsWithChildren) => {
52
52
  {children}
53
53
  </DraggablePanel>
54
54
  );
55
- };
55
+ });
@@ -1,16 +1,17 @@
1
- import { ActionIcon, Logo, SideNav } from '@lobehub/ui';
1
+ import { ActionIcon, SideNav } from '@lobehub/ui';
2
2
  import { MessageSquare, Settings2, Sticker } from 'lucide-react';
3
3
  import Router from 'next/router';
4
4
  import { memo } from 'react';
5
5
  import { shallow } from 'zustand/shallow';
6
6
 
7
+ import AvatarWithUpload from '@/features/AvatarWithUpload';
7
8
  import { useSettings } from '@/store/settings';
8
9
 
9
- const Sidebar = memo(() => {
10
+ export default memo(() => {
10
11
  const [tab, setTab] = useSettings((s) => [s.sidebarKey, s.switchSideBar], shallow);
11
12
  return (
12
13
  <SideNav
13
- avatar={<Logo size={40} />}
14
+ avatar={<AvatarWithUpload />}
14
15
  bottomActions={<ActionIcon icon={Settings2} onClick={() => Router.push('/setting')} />}
15
16
  style={{ height: '100vh' }}
16
17
  topActions={
@@ -32,5 +33,3 @@ const Sidebar = memo(() => {
32
33
  />
33
34
  );
34
35
  });
35
-
36
- export default Sidebar;
@@ -1,8 +1,10 @@
1
- import { ThemeProvider } from '@lobehub/ui';
1
+ import { ThemeProvider, lobeCustomTheme } from '@lobehub/ui';
2
2
  import { App, ConfigProvider } from 'antd';
3
+ import { useThemeMode } from 'antd-style';
3
4
  import 'antd/dist/reset.css';
4
5
  import Zh_CN from 'antd/locale/zh_CN';
5
- import { PropsWithChildren, useEffect } from 'react';
6
+ import { PropsWithChildren, useCallback, useEffect } from 'react';
7
+ import { shallow } from 'zustand/shallow';
6
8
 
7
9
  import { useSessionStore } from '@/store/session';
8
10
  import { useSettings } from '@/store/settings';
@@ -27,14 +29,27 @@ const Layout = ({ children }: PropsWithChildren) => {
27
29
  };
28
30
 
29
31
  export default ({ children }: PropsWithChildren) => {
32
+ const themeMode = useSettings((s) => s.settings.themeMode, shallow);
33
+ const { primaryColor, neutralColor } = useSettings(
34
+ (s) => ({ neutralColor: s.settings.neutralColor, primaryColor: s.settings.primaryColor }),
35
+ shallow,
36
+ );
37
+ const { browserPrefers } = useThemeMode();
38
+ const isDarkMode = themeMode === 'auto' ? browserPrefers === 'dark' : themeMode === 'dark';
39
+
30
40
  useEffect(() => {
31
41
  // refs: https://github.com/pmndrs/zustand/blob/main/docs/integrations/persisting-store-data.md#hashydrated
32
42
  useSessionStore.persist.rehydrate();
33
43
  useSettings.persist.rehydrate();
34
44
  }, []);
35
45
 
46
+ const genCustomToken: any = useCallback(
47
+ () => lobeCustomTheme({ isDarkMode, neutralColor, primaryColor }),
48
+ [primaryColor, neutralColor, isDarkMode],
49
+ );
50
+
36
51
  return (
37
- <ThemeProvider themeMode={'auto'}>
52
+ <ThemeProvider customToken={genCustomToken || {}} themeMode={themeMode}>
38
53
  <GlobalStyle />
39
54
  <Layout>{children}</Layout>
40
55
  </ThemeProvider>
@@ -3,20 +3,20 @@ import LanguageDetector from 'i18next-browser-languagedetector';
3
3
  import { isArray } from 'lodash-es';
4
4
  import { initReactI18next } from 'react-i18next';
5
5
 
6
- import type { Namespaces, Resources } from '@/types/locale';
6
+ import type { Namespaces } from '@/types/locale';
7
7
 
8
8
  import resources from './resources';
9
9
 
10
- const getRes = (res: Resources, namespace: Namespaces[]) => {
11
- const newRes: any = {};
12
- for (const [locale, value] of Object.entries(res)) {
13
- newRes[locale] = {};
14
- for (const ns of namespace) {
15
- newRes[locale][ns] = value[ns];
16
- }
17
- }
18
- return newRes;
19
- };
10
+ // const getRes = (res: Resources, namespace: Namespaces[]) => {
11
+ // const newRes: any = {};
12
+ // for (const [locale, value] of Object.entries(res)) {
13
+ // newRes[locale] = {};
14
+ // for (const ns of namespace) {
15
+ // newRes[locale][ns] = value[ns];
16
+ // }
17
+ // }
18
+ // return newRes;
19
+ // };
20
20
 
21
21
  export const createI18nNext = (namespace?: Namespaces[] | Namespaces) => {
22
22
  const ns: Namespaces[] = namespace
@@ -42,7 +42,8 @@ export const createI18nNext = (namespace?: Namespaces[] | Namespaces) => {
42
42
  escapeValue: false, // not needed for react as it escapes by default
43
43
  },
44
44
  ns,
45
- resources: getRes(resources, ns),
45
+ // resources: getRes(resources, ns),
46
+ resources,
46
47
  })
47
48
  );
48
49
  };
@@ -81,7 +81,6 @@ export default {
81
81
  },
82
82
  settingTheme: {
83
83
  avatar: {
84
- desc: '支持 URL / Base64 / Emoji 表情符号',
85
84
  title: '头像',
86
85
  },
87
86
  fontSize: {
@@ -89,8 +88,21 @@ export default {
89
88
  title: '字体大小',
90
89
  },
91
90
  lang: {
92
- all: '所有语言',
93
- name: '语言设置',
91
+ title: '语言设置',
92
+ },
93
+ neutralColor: {
94
+ desc: '不同色彩倾向的灰阶自定义',
95
+ title: '中性色',
96
+ },
97
+ primaryColor: {
98
+ desc: '自定义主题色',
99
+ title: '主题色',
100
+ },
101
+ themeMode: {
102
+ auto: '自动',
103
+ dark: '深色',
104
+ light: '浅色',
105
+ title: '主题',
94
106
  },
95
107
  title: '主题设置',
96
108
  },
@@ -0,0 +1,19 @@
1
+ import type { SelectProps } from 'antd';
2
+
3
+ import type { Locales } from '@/types/locale';
4
+
5
+ type LocaleOptions = SelectProps['options'] &
6
+ {
7
+ value: Locales;
8
+ }[];
9
+
10
+ export const options: LocaleOptions = [
11
+ {
12
+ label: '简体中文',
13
+ value: 'zh-CN',
14
+ },
15
+ {
16
+ label: 'English',
17
+ value: 'en-US',
18
+ },
19
+ ] as LocaleOptions;
@@ -1,9 +1,7 @@
1
1
  import common from '../../../locales/en_US/common.json';
2
- import setting from '../../../locales/en_US/setting.json';
3
2
 
4
3
  const resources = {
5
4
  common,
6
- setting,
7
5
  } as const;
8
6
 
9
7
  export default resources;
@@ -1,7 +1,13 @@
1
1
  import en_US from './en_US';
2
2
  import zh_CN from './zh_CN';
3
3
 
4
- export default {
4
+ const resources = {
5
5
  'en-US': en_US,
6
6
  'zh-CN': zh_CN,
7
- };
7
+ } as const;
8
+ export default resources;
9
+ export const defaultResources = zh_CN;
10
+ export type Resources = typeof resources;
11
+ export type DefaultResources = typeof defaultResources;
12
+ export type Namespaces = keyof DefaultResources;
13
+ export type Locales = keyof Resources;
@@ -3,11 +3,11 @@ import { PropsWithChildren, memo, useEffect } from 'react';
3
3
  import { Flexbox } from 'react-layout-kit';
4
4
  import { shallow } from 'zustand/shallow';
5
5
 
6
+ import SideBar from '@/features/SideBar';
6
7
  import { createI18nNext } from '@/locales/create';
7
8
  import { useSessionStore } from '@/store/session';
8
9
  import { useSettings } from '@/store/settings';
9
10
 
10
- import Sidebar from '../Sidebar';
11
11
  import { Sessions } from './SessionList';
12
12
 
13
13
  const initI18n = createI18nNext();
@@ -41,7 +41,7 @@ const ChatLayout = memo<PropsWithChildren>(({ children }) => {
41
41
 
42
42
  return (
43
43
  <Flexbox horizontal width={'100%'}>
44
- <Sidebar />
44
+ <SideBar />
45
45
  <Sessions />
46
46
  {children}
47
47
  </Flexbox>
@@ -1,20 +1,31 @@
1
- import { Form, Input, type ItemGroup } from '@lobehub/ui';
1
+ import { Form, type ItemGroup, ThemeSwitch } from '@lobehub/ui';
2
+ import { Select, Slider } from 'antd';
2
3
  import isEqual from 'fast-deep-equal';
4
+ import { debounce } from 'lodash-es';
3
5
  import { Palette } from 'lucide-react';
4
6
  import { memo, useMemo } from 'react';
5
7
  import { useTranslation } from 'react-i18next';
8
+ import { shallow } from 'zustand/shallow';
6
9
 
10
+ import AvatarWithUpload from '@/features/AvatarWithUpload';
11
+ import { options } from '@/locales/options';
7
12
  import { settingsSelectors, useSettings } from '@/store/settings';
8
13
  import { ConfigKeys } from '@/types/exportConfig';
9
14
 
15
+ import { ThemeSwatchesNeutral, ThemeSwatchesPrimary } from './ThemeSwatches';
16
+
10
17
  type SettingItemGroup = ItemGroup & {
11
18
  children: {
12
- name: ConfigKeys;
19
+ name?: ConfigKeys;
13
20
  }[];
14
21
  };
15
22
 
16
23
  const SettingForm = memo(() => {
17
24
  const settings = useSettings(settingsSelectors.currentSettings, isEqual);
25
+ const { setThemeMode, setSettings } = useSettings(
26
+ (s) => ({ setSettings: s.setSettings, setThemeMode: s.setThemeMode }),
27
+ shallow,
28
+ );
18
29
 
19
30
  const { t } = useTranslation('setting');
20
31
 
@@ -22,10 +33,47 @@ const SettingForm = memo(() => {
22
33
  () => ({
23
34
  children: [
24
35
  {
25
- children: <Input />,
26
- desc: t('settingTheme.avatar.desc'),
36
+ children: <AvatarWithUpload />,
27
37
  label: t('settingTheme.avatar.title'),
28
- name: 'avatar',
38
+ minWidth: undefined,
39
+ },
40
+ {
41
+ children: (
42
+ <ThemeSwitch
43
+ labels={{
44
+ auto: t('settingTheme.themeMode.auto'),
45
+ dark: t('settingTheme.themeMode.dark'),
46
+ light: t('settingTheme.themeMode.light'),
47
+ }}
48
+ onThemeSwitch={setThemeMode}
49
+ themeMode={settings.themeMode}
50
+ type={'select'}
51
+ />
52
+ ),
53
+ label: t('settingTheme.themeMode.title'),
54
+ },
55
+ {
56
+ children: <Select options={options} />,
57
+ label: t('settingTheme.lang.title'),
58
+ name: 'language',
59
+ },
60
+ {
61
+ children: <Slider max={18} min={12} />,
62
+ desc: t('settingTheme.fontSize.desc'),
63
+ label: t('settingTheme.fontSize.title'),
64
+ name: 'fontSize',
65
+ },
66
+ {
67
+ children: <ThemeSwatchesPrimary />,
68
+ desc: t('settingTheme.primaryColor.desc'),
69
+ label: t('settingTheme.primaryColor.title'),
70
+ minWidth: undefined,
71
+ },
72
+ {
73
+ children: <ThemeSwatchesNeutral />,
74
+ desc: t('settingTheme.neutralColor.desc'),
75
+ label: t('settingTheme.neutralColor.title'),
76
+ minWidth: undefined,
29
77
  },
30
78
  ],
31
79
  icon: Palette,
@@ -35,7 +83,13 @@ const SettingForm = memo(() => {
35
83
  );
36
84
 
37
85
  return (
38
- <Form initialValues={settings} items={[theme]} style={{ maxWidth: 1024, width: '100%' }} />
86
+ <Form
87
+ initialValues={settings}
88
+ itemMinWidth="min(30%,200px)"
89
+ items={[theme]}
90
+ onValuesChange={debounce(setSettings, 100)}
91
+ style={{ maxWidth: 1024, width: '100%' }}
92
+ />
39
93
  );
40
94
  });
41
95
 
@@ -0,0 +1,33 @@
1
+ import {
2
+ NeutralColors,
3
+ Swatches,
4
+ findCustomThemeName,
5
+ neutralColors,
6
+ neutralColorsSwatches,
7
+ } from '@lobehub/ui';
8
+ import { memo } from 'react';
9
+ import { shallow } from 'zustand/shallow';
10
+
11
+ import { useSettings } from '@/store/settings';
12
+
13
+ const ThemeSwatchesNeutral = memo(() => {
14
+ const { neutralColor, setSettings } = useSettings(
15
+ (s) => ({ neutralColor: s.settings.neutralColor, setSettings: s.setSettings }),
16
+ shallow,
17
+ );
18
+
19
+ const handleSelect = (v: any) => {
20
+ const name = findCustomThemeName('neutral', v) as NeutralColors;
21
+ setSettings({ neutralColor: name || '' });
22
+ };
23
+
24
+ return (
25
+ <Swatches
26
+ activeColor={neutralColor ? neutralColors[neutralColor] : undefined}
27
+ colors={neutralColorsSwatches}
28
+ onSelect={handleSelect}
29
+ />
30
+ );
31
+ });
32
+
33
+ export default ThemeSwatchesNeutral;
@@ -0,0 +1,33 @@
1
+ import {
2
+ PrimaryColors,
3
+ Swatches,
4
+ findCustomThemeName,
5
+ primaryColors,
6
+ primaryColorsSwatches,
7
+ } from '@lobehub/ui';
8
+ import { memo } from 'react';
9
+ import { shallow } from 'zustand/shallow';
10
+
11
+ import { useSettings } from '@/store/settings';
12
+
13
+ const ThemeSwatchesPrimary = memo(() => {
14
+ const { primaryColor, setSettings } = useSettings(
15
+ (s) => ({ primaryColor: s.settings.primaryColor, setSettings: s.setSettings }),
16
+ shallow,
17
+ );
18
+
19
+ const handleSelect = (v: any) => {
20
+ const name = findCustomThemeName('primary', v) as PrimaryColors;
21
+ setSettings({ primaryColor: name || '' });
22
+ };
23
+
24
+ return (
25
+ <Swatches
26
+ activeColor={primaryColor ? primaryColors[primaryColor] : undefined}
27
+ colors={primaryColorsSwatches}
28
+ onSelect={handleSelect}
29
+ />
30
+ );
31
+ });
32
+
33
+ export default ThemeSwatchesPrimary;
@@ -0,0 +1,2 @@
1
+ export { default as ThemeSwatchesNeutral } from './ThemeSwatchesNeutral';
2
+ export { default as ThemeSwatchesPrimary } from './ThemeSwatchesPrimary';
@@ -3,10 +3,10 @@ import { memo, useEffect } from 'react';
3
3
  import { useTranslation } from 'react-i18next';
4
4
  import { Flexbox } from 'react-layout-kit';
5
5
 
6
+ import SideBar from '@/features/SideBar';
6
7
  import { createI18nNext } from '@/locales/create';
7
8
  import { Sessions } from '@/pages/chat/SessionList';
8
9
 
9
- import Sidebar from '../Sidebar';
10
10
  import Header from './Header';
11
11
  import SettingForm from './SettingForm';
12
12
 
@@ -25,7 +25,7 @@ const SettingLayout = memo(() => {
25
25
  <title>{pageTitle}</title>
26
26
  </Head>
27
27
  <Flexbox horizontal width={'100%'}>
28
- <Sidebar />
28
+ <SideBar />
29
29
  <Sessions />
30
30
  <Flexbox flex={1}>
31
31
  <Header />
@@ -1,15 +1,17 @@
1
+ import { ThemeMode } from 'antd-style';
1
2
  import { merge } from 'lodash-es';
2
3
  import type { StateCreator } from 'zustand/vanilla';
3
4
 
4
5
  import type { ConfigSettings } from '@/types/exportConfig';
5
6
 
6
7
  import type { SidebarTabKey } from './initialState';
7
- import { SettingsState } from './initialState';
8
+ import { GlobalSettingsState } from './initialState';
8
9
  import type { SettingsStore } from './store';
9
10
 
10
11
  export interface SettingsAction {
11
- importSettings: (settings: SettingsState) => void;
12
- saveSettings: (settings: ConfigSettings) => void;
12
+ setGlobalSettings: (settings: GlobalSettingsState) => void;
13
+ setSettings: (settings: { [keys in keyof ConfigSettings]?: any }) => void;
14
+ setThemeMode: (themeMode: ThemeMode) => void;
13
15
  switchSideBar: (key: SidebarTabKey) => void;
14
16
  }
15
17
 
@@ -19,12 +21,15 @@ export const createSettings: StateCreator<
19
21
  [],
20
22
  SettingsAction
21
23
  > = (set, get) => ({
22
- importSettings: (settings) => {
24
+ setGlobalSettings: (settings) => {
23
25
  set({ ...settings });
24
26
  },
25
- saveSettings: (settings) => {
27
+ setSettings: (settings) => {
26
28
  set({ settings: merge(get().settings, settings) });
27
29
  },
30
+ setThemeMode: (themeMode) => {
31
+ get().setSettings({ themeMode });
32
+ },
28
33
  switchSideBar: (key) => {
29
34
  set({ sidebarKey: key });
30
35
  },
@@ -1,22 +1,24 @@
1
- import type { ThemeMode } from 'antd-style';
2
-
3
1
  import type { ConfigSettings } from '@/types/exportConfig';
4
2
 
5
3
  export type SidebarTabKey = 'chat' | 'market';
6
4
  export const DEFAULT_SETTINGS: ConfigSettings = {
7
5
  avatar: '',
6
+ fontSize: 14,
7
+ language: 'zh-CN',
8
+ neutralColor: '',
9
+ primaryColor: '',
10
+ themeMode: 'auto',
8
11
  };
9
12
 
10
- export interface SettingsState {
13
+ export interface GlobalSettingsState {
11
14
  inputHeight: number;
12
15
  sessionExpandable?: boolean;
13
16
  sessionsWidth: number;
14
17
  settings: ConfigSettings;
15
18
  sidebarKey: SidebarTabKey;
16
- themeMode?: ThemeMode;
17
19
  }
18
20
 
19
- export const initialState: SettingsState = {
21
+ export const initialState: GlobalSettingsState = {
20
22
  inputHeight: 200,
21
23
  sessionExpandable: true,
22
24
  sessionsWidth: 320,
@@ -1,9 +1,17 @@
1
+ import { merge } from 'lodash-es';
2
+
1
3
  import { DEFAULT_SETTINGS } from '@/store/settings/initialState';
2
4
 
3
5
  import { SettingsStore } from './store';
4
6
 
5
- const currentSettings = (s: SettingsStore) => s.settings || DEFAULT_SETTINGS;
7
+ const currentSettings = (s: SettingsStore) => merge(DEFAULT_SETTINGS, s.settings);
8
+
9
+ const selecThemeMode = (s: SettingsStore) => ({
10
+ setThemeMode: s.setThemeMode,
11
+ themeMode: s.settings.themeMode,
12
+ });
6
13
 
7
14
  export const settingsSelectors = {
8
15
  currentSettings,
16
+ selecThemeMode,
9
17
  };
@@ -1,9 +1,9 @@
1
1
  import { StateCreator } from 'zustand/vanilla';
2
2
 
3
3
  import { type SettingsAction, createSettings } from './action';
4
- import { type SettingsState, initialState } from './initialState';
4
+ import { type GlobalSettingsState, initialState } from './initialState';
5
5
 
6
- export type SettingsStore = SettingsAction & SettingsState;
6
+ export type SettingsStore = SettingsAction & GlobalSettingsState;
7
7
 
8
8
  export const createStore: StateCreator<SettingsStore, [['zustand/devtools', never]]> = (
9
9
  ...parameters
@@ -1,5 +1,5 @@
1
1
  import { Theme, css } from 'antd-style';
2
- import { rgba } from 'polished';
2
+ import { readableColor } from 'polished';
3
3
 
4
4
  export default (token: Theme) => css`
5
5
  .ant-btn {
@@ -10,20 +10,24 @@ export default (token: Theme) => css`
10
10
  z-index: 1100;
11
11
  }
12
12
 
13
- .ant-notification .ant-notification-notice.notification-primary-info {
13
+ .ant-slider-track,
14
+ .ant-tabs-ink-bar,
15
+ .ant-switch-checked {
16
+ background: ${token.colorPrimary} !important;
17
+ }
18
+
19
+ .ant-btn-primary {
20
+ color: ${readableColor(token.colorPrimary)};
14
21
  background: ${token.colorPrimary};
15
- box-shadow: 0 6px 16px 0 ${rgba(token.colorPrimary, 0.1)},
16
- 0 3px 6px -4px ${rgba(token.colorPrimary, 0.2)},
17
- 0 9px 28px 8px ${rgba(token.colorPrimary, 0.1)};
18
22
 
19
- .anticon {
20
- color: ${token.colorTextLightSolid};
23
+ &:hover {
24
+ color: ${readableColor(token.colorPrimary)} !important;
25
+ background: ${token.colorPrimaryHover} !important;
21
26
  }
22
27
 
23
- .ant-notification-notice-message {
24
- margin-bottom: 0;
25
- padding-right: 0;
26
- color: ${token.colorTextLightSolid};
28
+ &:active {
29
+ color: ${readableColor(token.colorPrimaryActive)} !important;
30
+ background: ${token.colorPrimaryActive} !important;
27
31
  }
28
32
  }
29
33
  `;
@@ -1,11 +1,18 @@
1
+ import type { NeutralColors, PrimaryColors } from '@lobehub/ui';
2
+ import { ThemeMode } from 'antd-style';
3
+
4
+ import { Locales } from './locale';
5
+
1
6
  /**
2
7
  * 配置设置
3
8
  */
4
9
  export interface ConfigSettings {
5
- /**
6
- * 头像链接
7
- */
8
10
  avatar?: string;
11
+ fontSize: number;
12
+ language: Locales;
13
+ neutralColor?: NeutralColors | '';
14
+ primaryColor?: PrimaryColors | '';
15
+ themeMode: ThemeMode;
9
16
  }
10
17
 
11
18
  export type ConfigKeys = keyof ConfigSettings;
@@ -1,7 +1 @@
1
- import resources from '@/locales/resources';
2
- import defaultResources from '@/locales/resources/zh_CN';
3
-
4
- export type Resources = typeof resources;
5
- export type DefaultResources = typeof defaultResources;
6
- export type Namespaces = keyof DefaultResources;
7
- export type Locales = keyof Resources;
1
+ export type * from '@/locales/resources';
package/tsconfig.json CHANGED
@@ -22,5 +22,10 @@
22
22
  }
23
23
  },
24
24
  "exclude": ["node_modules"],
25
- "include": ["next-env.d.ts", "**/*.ts", "**/*.d.ts", "**/*.tsx"]
25
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.d.ts", "**/*.tsx"],
26
+ "ts-node": {
27
+ "compilerOptions": {
28
+ "module": "commonjs"
29
+ }
30
+ }
26
31
  }
@@ -1,12 +0,0 @@
1
- import fs from 'node:fs';
2
- import { resolve } from 'node:path';
3
-
4
- import i18nConfig from '../.i18nrc.js';
5
- import resources from '../src/locales/resources/zh_CN.ts';
6
-
7
- for (const [ns, value] of Object.entries(resources)) {
8
- fs.writeFileSync(
9
- resolve(i18nConfig.output, i18nConfig.entryLocale, `${ns}.json`),
10
- JSON.stringify(value, null, 2),
11
- );
12
- }
package/scripts/toc.mjs DELETED
@@ -1,40 +0,0 @@
1
- import { tocForResources } from 'i18next-resources-for-ts';
2
- import fs from 'node:fs';
3
- import { resolve } from 'node:path';
4
-
5
- import i18nConfig from '../.i18nrc.js';
6
-
7
- const locales = [i18nConfig.entryLocale, ...i18nConfig.outputLocales];
8
-
9
- const RES_OUTPUT = 'src/locales/resources';
10
-
11
- let index = '';
12
- let indexObj = '';
13
- locales.forEach((locale) => {
14
- const files = fs
15
- .readdirSync(resolve(i18nConfig.output, locale))
16
- .filter((name) => name.includes('.json'));
17
- index += `import ${locale} from "./${locale}";\n`;
18
- indexObj += ` "${locale.replace('_', '-')}": ${locale},\n`;
19
- const ns = [];
20
- files.forEach((file) => {
21
- ns.push({
22
- name: file.replace('.json', ''),
23
- path: resolve(i18nConfig.output, locale, file),
24
- });
25
- });
26
- let toc = tocForResources(ns, resolve(RES_OUTPUT)).replaceAll('\\', '/');
27
- if (locale === i18nConfig.entryLocale) {
28
- toc = toc.replaceAll('.json', '').replaceAll('../../../locales/zh_CN', '../default');
29
- }
30
- fs.writeFileSync(resolve(RES_OUTPUT, `${locale}.ts`), toc);
31
- });
32
-
33
- fs.writeFileSync(
34
- resolve(RES_OUTPUT, `index.ts`),
35
- `${index}
36
-
37
- export default {
38
- ${indexObj}};
39
- `,
40
- );