@lobehub/chat 1.52.11 → 1.52.12

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 (52) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/changelog/v1.json +9 -0
  3. package/docs/self-hosting/environment-variables/auth.zh-CN.mdx +2 -2
  4. package/package.json +1 -1
  5. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/InboxWelcome/AgentsSuggest.tsx +3 -3
  6. package/src/app/[variants]/(main)/chat/@session/features/SessionListContent/DefaultMode.tsx +1 -1
  7. package/src/app/[variants]/(main)/chat/settings/features/SubmitAgentButton/SubmitAgentModal.tsx +3 -3
  8. package/src/app/[variants]/(main)/settings/common/features/Theme/index.tsx +4 -5
  9. package/src/chains/__tests__/summaryAgentName.test.ts +2 -2
  10. package/src/chains/__tests__/summaryDescription.test.ts +2 -2
  11. package/src/chains/__tests__/summaryHistory.test.ts +1 -1
  12. package/src/chains/__tests__/summaryTags.test.ts +2 -2
  13. package/src/chains/__tests__/summaryTitle.test.ts +2 -2
  14. package/src/chains/summaryAgentName.ts +1 -1
  15. package/src/chains/summaryDescription.ts +1 -1
  16. package/src/chains/summaryTags.ts +1 -1
  17. package/src/chains/summaryTitle.ts +1 -1
  18. package/src/const/settings/common.ts +0 -1
  19. package/src/features/AgentSetting/AgentMeta/AutoGenerateAvatar.tsx +3 -3
  20. package/src/features/AgentSetting/AgentTTS/index.tsx +4 -4
  21. package/src/features/ChatInput/STT/browser.tsx +4 -2
  22. package/src/features/ChatInput/STT/openai.tsx +4 -2
  23. package/src/features/Conversation/Extras/TTS/index.tsx +3 -3
  24. package/src/features/PluginDevModal/LocalForm.tsx +3 -3
  25. package/src/features/User/UserPanel/LangButton.tsx +12 -6
  26. package/src/hooks/useTTS.ts +4 -2
  27. package/src/layout/GlobalProvider/AppTheme.tsx +1 -1
  28. package/src/middleware.ts +3 -4
  29. package/src/services/__tests__/assistant.test.ts +2 -2
  30. package/src/services/__tests__/tool.test.ts +2 -2
  31. package/src/services/assistant.ts +1 -1
  32. package/src/services/tool.ts +1 -1
  33. package/src/store/global/{action.ts → actions/general.ts} +11 -62
  34. package/src/store/global/actions/workspacePane.ts +73 -0
  35. package/src/store/global/helpers.ts +6 -0
  36. package/src/store/global/initialState.ts +2 -0
  37. package/src/store/global/selectors/general.test.ts +18 -0
  38. package/src/store/global/selectors/general.ts +25 -0
  39. package/src/store/global/selectors/index.ts +2 -0
  40. package/src/store/global/{selectors.ts → selectors/systemStatus.ts} +2 -2
  41. package/src/store/global/store.ts +11 -3
  42. package/src/store/user/slices/common/action.test.ts +3 -15
  43. package/src/store/user/slices/common/action.ts +0 -8
  44. package/src/store/user/slices/settings/action.ts +0 -8
  45. package/src/store/user/slices/settings/selectors/general.test.ts +0 -14
  46. package/src/store/user/slices/settings/selectors/general.ts +0 -19
  47. package/src/types/user/settings/general.ts +0 -3
  48. package/src/utils/client/cookie.test.ts +85 -0
  49. package/src/utils/client/cookie.ts +22 -0
  50. package/src/utils/client/switchLang.ts +5 -0
  51. package/src/store/user/helpers.ts +0 -9
  52. package/src/utils/cookie.ts +0 -10
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 1.52.12](https://github.com/lobehub/lobe-chat/compare/v1.52.11...v1.52.12)
6
+
7
+ <sup>Released on **2025-02-10**</sup>
8
+
9
+ #### 🐛 Bug Fixes
10
+
11
+ - **misc**: Fix language incorrect on page hydration.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's fixed
19
+
20
+ - **misc**: Fix language incorrect on page hydration, closes [#5970](https://github.com/lobehub/lobe-chat/issues/5970) ([91912cf](https://github.com/lobehub/lobe-chat/commit/91912cf))
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
+
5
30
  ### [Version 1.52.11](https://github.com/lobehub/lobe-chat/compare/v1.52.10...v1.52.11)
6
31
 
7
32
  <sup>Released on **2025-02-10**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,13 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "fixes": [
5
+ "Fix language incorrect on page hydration."
6
+ ]
7
+ },
8
+ "date": "2025-02-10",
9
+ "version": "1.52.12"
10
+ },
2
11
  {
3
12
  "children": {
4
13
  "improvements": [
@@ -47,7 +47,7 @@ LobeChat 在部署时提供了完善的身份验证服务能力,以下是相
47
47
  - 默认值: `-`
48
48
  - 示例: `evCnOJP1UX8FMnXR9Xkj5t0NyFn5p70P`
49
49
 
50
- #### `AUTH_AUTH_SECRET`
50
+ #### `AUTH_AUTH0_SECRET`
51
51
 
52
52
  - 类型:必选
53
53
  - 描述: Auth0 应用程序的 Client Secret
@@ -280,4 +280,4 @@ LobeChat 在部署时提供了完善的身份验证服务能力,以下是相
280
280
  - 类型:必选
281
281
  - 描述: Clerk 应用程序的 Secret key。您可以在[这里](https://dashboard.clerk.com)访问,并导航到 API Keys 以查看。
282
282
  - 默认值:`-`
283
- - 示例: `sk_test_513Ma0P7IAWM1XMv4waxZjRYRajWTaCfJLjpEO3SD2` (测试环境) / `sk_live_eMMlHjwJvZFUfczFljSKqZdwQtLvmczmsJSNmdrpeZ`(生产环境)
283
+ - 示例: `sk_test_513Ma0P7IAWM1XMv4waxZjRYRajWTaCfJLjpEO3SD2` (测试环境) / `sk_live_eMMlHjwJvZFUfczFljSKqZdwQtLvmczmsJSNmdrpeZ`(生产环境)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.52.11",
3
+ "version": "1.52.12",
4
4
  "description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -12,8 +12,8 @@ import useSWR from 'swr';
12
12
  import urlJoin from 'url-join';
13
13
 
14
14
  import { assistantService } from '@/services/assistant';
15
- import { useUserStore } from '@/store/user';
16
- import { userGeneralSettingsSelectors } from '@/store/user/selectors';
15
+ import { useGlobalStore } from '@/store/global';
16
+ import { globalGeneralSelectors } from '@/store/global/selectors';
17
17
  import { DiscoverAssistantItem } from '@/types/discover';
18
18
 
19
19
  const { Paragraph } = Typography;
@@ -60,7 +60,7 @@ const useStyles = createStyles(({ css, token, responsive }) => ({
60
60
 
61
61
  const AgentsSuggest = memo<{ mobile?: boolean }>(({ mobile }) => {
62
62
  const { t } = useTranslation('welcome');
63
- const locale = useUserStore(userGeneralSettingsSelectors.currentLanguage);
63
+ const locale = useGlobalStore(globalGeneralSelectors.currentLanguage);
64
64
  const [sliceStart, setSliceStart] = useState(0);
65
65
 
66
66
  const { data: assistantList, isLoading } = useSWR(
@@ -68,7 +68,7 @@ const DefaultMode = memo(() => {
68
68
  label: t('defaultList'),
69
69
  },
70
70
  ].filter(Boolean) as CollapseProps['items'],
71
- [customSessionGroups, pinnedSessions, defaultSessions],
71
+ [t, customSessionGroups, pinnedSessions, defaultSessions],
72
72
  );
73
73
 
74
74
  return (
@@ -14,10 +14,10 @@ import { AGENTS_INDEX_GITHUB_ISSUE } from '@/const/url';
14
14
  import AgentInfo from '@/features/AgentInfo';
15
15
  import { useAgentStore } from '@/store/agent';
16
16
  import { agentSelectors } from '@/store/agent/selectors';
17
+ import { useGlobalStore } from '@/store/global';
18
+ import { globalGeneralSelectors } from '@/store/global/selectors';
17
19
  import { useSessionStore } from '@/store/session';
18
20
  import { sessionMetaSelectors } from '@/store/session/selectors';
19
- import { useUserStore } from '@/store/user';
20
- import { userGeneralSettingsSelectors } from '@/store/user/selectors';
21
21
 
22
22
  const SubmitAgentModal = memo<ModalProps>(({ open, onCancel }) => {
23
23
  const { t } = useTranslation('setting');
@@ -25,7 +25,7 @@ const SubmitAgentModal = memo<ModalProps>(({ open, onCancel }) => {
25
25
  const systemRole = useAgentStore(agentSelectors.currentAgentSystemRole);
26
26
  const theme = useTheme();
27
27
  const meta = useSessionStore(sessionMetaSelectors.currentAgentMeta, isEqual);
28
- const language = useUserStore((s) => userGeneralSettingsSelectors.currentLanguage(s));
28
+ const language = useGlobalStore(globalGeneralSelectors.currentLanguage);
29
29
 
30
30
  const isMetaPass = Boolean(
31
31
  meta && meta.title && meta.description && (meta.tags as string[])?.length > 0 && meta.avatar,
@@ -4,7 +4,6 @@ import { Form, type ItemGroup, SelectWithImg, SliderWithInput } from '@lobehub/u
4
4
  import { Select } from 'antd';
5
5
  import isEqual from 'fast-deep-equal';
6
6
  import { Monitor, Moon, Sun } from 'lucide-react';
7
- import { useRouter } from 'next/navigation';
8
7
  import { memo } from 'react';
9
8
  import { useTranslation } from 'react-i18next';
10
9
 
@@ -12,9 +11,9 @@ import { useSyncSettings } from '@/app/[variants]/(main)/settings/hooks/useSyncS
12
11
  import { FORM_STYLE } from '@/const/layoutTokens';
13
12
  import { imageUrl } from '@/const/url';
14
13
  import { Locales, localeOptions } from '@/locales/resources';
14
+ import { useGlobalStore } from '@/store/global';
15
15
  import { useUserStore } from '@/store/user';
16
16
  import { settingsSelectors, userGeneralSettingsSelectors } from '@/store/user/selectors';
17
- import { switchLang } from '@/utils/client/switchLang';
18
17
 
19
18
  import { ThemeSwatchesNeutral, ThemeSwatchesPrimary } from './ThemeSwatches';
20
19
 
@@ -22,17 +21,17 @@ type SettingItemGroup = ItemGroup;
22
21
 
23
22
  const Theme = memo(() => {
24
23
  const { t } = useTranslation('setting');
25
- const router = useRouter();
24
+
26
25
  const [form] = Form.useForm();
27
26
  const settings = useUserStore(settingsSelectors.currentSettings, isEqual);
28
27
  const themeMode = useUserStore(userGeneralSettingsSelectors.currentThemeMode);
29
28
  const [setThemeMode, setSettings] = useUserStore((s) => [s.switchThemeMode, s.setSettings]);
30
29
 
31
30
  useSyncSettings(form);
31
+ const [switchLocale] = useGlobalStore((s) => [s.switchLocale]);
32
32
 
33
33
  const handleLangChange = (value: Locales) => {
34
- switchLang(value);
35
- router.refresh();
34
+ switchLocale(value);
36
35
  };
37
36
 
38
37
  const theme: SettingItemGroup = {
@@ -1,11 +1,11 @@
1
1
  import { Mock, describe, expect, it } from 'vitest';
2
2
 
3
- import { globalHelpers } from '@/store/user/helpers';
3
+ import { globalHelpers } from '@/store/global/helpers';
4
4
 
5
5
  import { chainSummaryAgentName } from '../summaryAgentName';
6
6
 
7
7
  // Mock the getCurrentLanguage function
8
- vi.mock('@/store/user/helpers', () => ({
8
+ vi.mock('@/store/global/helpers', () => ({
9
9
  globalHelpers: {
10
10
  getCurrentLanguage: vi.fn(),
11
11
  },
@@ -1,11 +1,11 @@
1
1
  import { Mock, describe, expect, it, vi } from 'vitest';
2
2
 
3
- import { globalHelpers } from '@/store/user/helpers';
3
+ import { globalHelpers } from '@/store/global/helpers';
4
4
 
5
5
  import { chainSummaryDescription } from '../summaryDescription';
6
6
 
7
7
  // Mock the globalHelpers.getCurrentLanguage function
8
- vi.mock('@/store/user/helpers', () => ({
8
+ vi.mock('@/store/global/helpers', () => ({
9
9
  globalHelpers: {
10
10
  getCurrentLanguage: vi.fn(() => 'en-US'),
11
11
  },
@@ -1,7 +1,7 @@
1
1
  import { Mock, describe, expect, it, vi } from 'vitest';
2
2
 
3
3
  import { chatHelpers } from '@/store/chat/helpers';
4
- import { globalHelpers } from '@/store/user/helpers';
4
+ import { globalHelpers } from '@/store/global/helpers';
5
5
  import { ChatMessage } from '@/types/message';
6
6
  import { OpenAIChatMessage } from '@/types/openai/chat';
7
7
 
@@ -1,11 +1,11 @@
1
1
  import { Mock, describe, expect, it } from 'vitest';
2
2
 
3
- import { globalHelpers } from '@/store/user/helpers';
3
+ import { globalHelpers } from '@/store/global/helpers';
4
4
 
5
5
  import { chainSummaryTags } from '../summaryTags';
6
6
 
7
7
  // Mock the getCurrentLanguage function
8
- vi.mock('@/store/user/helpers', () => ({
8
+ vi.mock('@/store/global/helpers', () => ({
9
9
  globalHelpers: {
10
10
  getCurrentLanguage: vi.fn(),
11
11
  },
@@ -1,13 +1,13 @@
1
1
  import { Mock, describe, expect, it, vi } from 'vitest';
2
2
 
3
3
  import { chatHelpers } from '@/store/chat/helpers';
4
- import { globalHelpers } from '@/store/user/helpers';
4
+ import { globalHelpers } from '@/store/global/helpers';
5
5
  import { OpenAIChatMessage } from '@/types/openai/chat';
6
6
 
7
7
  import { chainSummaryTitle } from '../summaryTitle';
8
8
 
9
9
  // Mock the getCurrentLanguage function
10
- vi.mock('@/store/user/helpers', () => ({
10
+ vi.mock('@/store/global/helpers', () => ({
11
11
  globalHelpers: {
12
12
  getCurrentLanguage: vi.fn(),
13
13
  },
@@ -1,4 +1,4 @@
1
- import { globalHelpers } from '@/store/user/helpers';
1
+ import { globalHelpers } from '@/store/global/helpers';
2
2
  import { ChatStreamPayload } from '@/types/openai/chat';
3
3
 
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { globalHelpers } from '@/store/user/helpers';
1
+ import { globalHelpers } from '@/store/global/helpers';
2
2
  import { ChatStreamPayload } from '@/types/openai/chat';
3
3
 
4
4
  export const chainSummaryDescription = (content: string): Partial<ChatStreamPayload> => ({
@@ -1,4 +1,4 @@
1
- import { globalHelpers } from '@/store/user/helpers';
1
+ import { globalHelpers } from '@/store/global/helpers';
2
2
  import { ChatStreamPayload } from '@/types/openai/chat';
3
3
 
4
4
  export const chainSummaryTags = (content: string): Partial<ChatStreamPayload> => ({
@@ -1,4 +1,4 @@
1
- import { globalHelpers } from '@/store/user/helpers';
1
+ import { globalHelpers } from '@/store/global/helpers';
2
2
  import { ChatStreamPayload, OpenAIChatMessage } from '@/types/openai/chat';
3
3
 
4
4
  export const chainSummaryTitle = (messages: OpenAIChatMessage[]): Partial<ChatStreamPayload> => {
@@ -2,6 +2,5 @@ import { UserGeneralConfig } from '@/types/user/settings';
2
2
 
3
3
  export const DEFAULT_COMMON_SETTINGS: UserGeneralConfig = {
4
4
  fontSize: 14,
5
- language: 'auto',
6
5
  themeMode: 'auto',
7
6
  };
@@ -6,8 +6,8 @@ import { memo } from 'react';
6
6
  import { useTranslation } from 'react-i18next';
7
7
  import { Flexbox } from 'react-layout-kit';
8
8
 
9
- import { useUserStore } from '@/store/user';
10
- import { userGeneralSettingsSelectors } from '@/store/user/selectors';
9
+ import { useGlobalStore } from '@/store/global';
10
+ import { globalGeneralSelectors } from '@/store/global/selectors';
11
11
 
12
12
  const EmojiPicker = dynamic(() => import('@lobehub/ui/es/EmojiPicker'), { ssr: false });
13
13
 
@@ -24,7 +24,7 @@ const AutoGenerateAvatar = memo<AutoGenerateAvatarProps>(
24
24
  ({ loading, background, value, onChange, onGenerate, canAutoGenerate }) => {
25
25
  const { t } = useTranslation('common');
26
26
  const theme = useTheme();
27
- const locale = useUserStore(userGeneralSettingsSelectors.currentLanguage);
27
+ const locale = useGlobalStore(globalGeneralSelectors.currentLanguage);
28
28
 
29
29
  return (
30
30
  <Flexbox>
@@ -9,8 +9,8 @@ import { memo, useMemo } from 'react';
9
9
  import { useTranslation } from 'react-i18next';
10
10
 
11
11
  import { FORM_STYLE } from '@/const/layoutTokens';
12
- import { useUserStore } from '@/store/user';
13
- import { userGeneralSettingsSelectors } from '@/store/user/selectors';
12
+ import { useGlobalStore } from '@/store/global';
13
+ import { globalGeneralSelectors } from '@/store/global/selectors';
14
14
 
15
15
  import { useStore } from '../store';
16
16
  import { useAgentSyncSettings } from '../useSyncAgemtSettings';
@@ -23,8 +23,8 @@ const { openaiVoiceOptions, localeOptions } = VoiceList;
23
23
  const AgentTTS = memo(() => {
24
24
  const { t } = useTranslation('setting');
25
25
  const [form] = Form.useForm();
26
- const voiceList = useUserStore((s) => {
27
- const locale = userGeneralSettingsSelectors.currentLanguage(s);
26
+ const voiceList = useGlobalStore((s) => {
27
+ const locale = globalGeneralSelectors.currentLanguage(s);
28
28
  return (all?: boolean) => new VoiceList(all ? undefined : locale);
29
29
  });
30
30
  const [showAllLocaleVoice, ttsService, updateConfig] = useStore((s) => [
@@ -8,8 +8,10 @@ import { useAgentStore } from '@/store/agent';
8
8
  import { agentSelectors } from '@/store/agent/slices/chat';
9
9
  import { useChatStore } from '@/store/chat';
10
10
  import { chatSelectors } from '@/store/chat/selectors';
11
+ import { useGlobalStore } from '@/store/global';
12
+ import { globalGeneralSelectors } from '@/store/global/selectors';
11
13
  import { useUserStore } from '@/store/user';
12
- import { settingsSelectors, userGeneralSettingsSelectors } from '@/store/user/selectors';
14
+ import { settingsSelectors } from '@/store/user/selectors';
13
15
  import { ChatMessageError } from '@/types/message';
14
16
  import { getMessageError } from '@/utils/fetch';
15
17
 
@@ -22,7 +24,7 @@ interface STTConfig extends SWRConfiguration {
22
24
  const useBrowserSTT = (config: STTConfig) => {
23
25
  const ttsSettings = useUserStore(settingsSelectors.currentTTS, isEqual);
24
26
  const ttsAgentSettings = useAgentStore(agentSelectors.currentAgentTTS, isEqual);
25
- const locale = useUserStore(userGeneralSettingsSelectors.currentLanguage);
27
+ const locale = useGlobalStore(globalGeneralSelectors.currentLanguage);
26
28
 
27
29
  const autoStop = ttsSettings.sttAutoStop;
28
30
  const sttLocale =
@@ -11,8 +11,10 @@ import { useAgentStore } from '@/store/agent';
11
11
  import { agentSelectors } from '@/store/agent/selectors';
12
12
  import { useChatStore } from '@/store/chat';
13
13
  import { chatSelectors } from '@/store/chat/slices/message/selectors';
14
+ import { useGlobalStore } from '@/store/global';
15
+ import { globalGeneralSelectors } from '@/store/global/selectors';
14
16
  import { useUserStore } from '@/store/user';
15
- import { settingsSelectors, userGeneralSettingsSelectors } from '@/store/user/selectors';
17
+ import { settingsSelectors } from '@/store/user/selectors';
16
18
  import { ChatMessageError } from '@/types/message';
17
19
  import { getMessageError } from '@/utils/fetch';
18
20
 
@@ -25,7 +27,7 @@ interface STTConfig extends SWRConfiguration {
25
27
  const useOpenaiSTT = (config: STTConfig) => {
26
28
  const ttsSettings = useUserStore(settingsSelectors.currentTTS, isEqual);
27
29
  const ttsAgentSettings = useAgentStore(agentSelectors.currentAgentTTS, isEqual);
28
- const locale = useUserStore(userGeneralSettingsSelectors.currentLanguage);
30
+ const locale = useGlobalStore(globalGeneralSelectors.currentLanguage);
29
31
 
30
32
  const autoStop = ttsSettings.sttAutoStop;
31
33
  const sttLocale =
@@ -3,8 +3,8 @@ import { Md5 } from 'ts-md5';
3
3
 
4
4
  import { useAgentStore } from '@/store/agent';
5
5
  import { agentSelectors } from '@/store/agent/slices/chat';
6
- import { useUserStore } from '@/store/user';
7
- import { userGeneralSettingsSelectors } from '@/store/user/selectors';
6
+ import { useGlobalStore } from '@/store/global';
7
+ import { globalGeneralSelectors } from '@/store/global/selectors';
8
8
 
9
9
  import FilePlayer from './FilePlayer';
10
10
  import InitPlayer, { TTSProps } from './InitPlayer';
@@ -12,7 +12,7 @@ import InitPlayer, { TTSProps } from './InitPlayer';
12
12
  const TTS = memo<TTSProps>(
13
13
  (props) => {
14
14
  const { file, voice, content, contentMd5 } = props;
15
- const lang = useUserStore(userGeneralSettingsSelectors.currentLanguage);
15
+ const lang = useGlobalStore(globalGeneralSelectors.currentLanguage);
16
16
  const currentVoice = useAgentStore(agentSelectors.currentAgentTTSVoice(lang));
17
17
 
18
18
  const md5 = useMemo(() => Md5.hashStr(content).toString(), [content]);
@@ -4,16 +4,16 @@ import dynamic from 'next/dynamic';
4
4
  import { memo } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
6
6
 
7
+ import { useGlobalStore } from '@/store/global';
8
+ import { globalGeneralSelectors } from '@/store/global/selectors';
7
9
  import { useToolStore } from '@/store/tool';
8
10
  import { pluginSelectors } from '@/store/tool/selectors';
9
- import { useUserStore } from '@/store/user';
10
- import { userGeneralSettingsSelectors } from '@/store/user/selectors';
11
11
 
12
12
  const EmojiPicker = dynamic(() => import('@lobehub/ui/es/EmojiPicker'), { ssr: false });
13
13
 
14
14
  const LocalForm = memo<{ form: FormInstance; mode?: 'edit' | 'create' }>(({ form, mode }) => {
15
15
  const isEditMode = mode === 'edit';
16
- const locale = useUserStore(userGeneralSettingsSelectors.currentLanguage);
16
+ const locale = useGlobalStore(globalGeneralSelectors.currentLanguage);
17
17
  const { t } = useTranslation('plugin');
18
18
 
19
19
  const pluginIds = useToolStore(pluginSelectors.storeAndInstallPluginsIdList);
@@ -7,16 +7,22 @@ import { useTranslation } from 'react-i18next';
7
7
 
8
8
  import Menu, { type MenuProps } from '@/components/Menu';
9
9
  import { localeOptions } from '@/locales/resources';
10
- import { useUserStore } from '@/store/user';
11
- import { userGeneralSettingsSelectors } from '@/store/user/selectors';
10
+ import { useGlobalStore } from '@/store/global';
11
+ import { globalGeneralSelectors } from '@/store/global/selectors';
12
+ import { LocaleMode } from '@/types/locale';
12
13
 
13
14
  const LangButton = memo<{ placement?: PopoverProps['placement'] }>(({ placement = 'right' }) => {
14
15
  const theme = useTheme();
15
- const [language, switchLocale] = useUserStore((s) => [
16
- userGeneralSettingsSelectors.language(s),
16
+
17
+ const [language, switchLocale] = useGlobalStore((s) => [
18
+ globalGeneralSelectors.language(s),
17
19
  s.switchLocale,
18
20
  ]);
19
21
 
22
+ const handleLangChange = (value: LocaleMode) => {
23
+ switchLocale(value);
24
+ };
25
+
20
26
  const { t } = useTranslation('setting');
21
27
 
22
28
  const items: MenuProps['items'] = useMemo(
@@ -24,12 +30,12 @@ const LangButton = memo<{ placement?: PopoverProps['placement'] }>(({ placement
24
30
  {
25
31
  key: 'auto',
26
32
  label: t('settingTheme.lang.autoMode'),
27
- onClick: () => switchLocale('auto'),
33
+ onClick: () => handleLangChange('auto'),
28
34
  },
29
35
  ...localeOptions.map((item) => ({
30
36
  key: item.value,
31
37
  label: item.label,
32
- onClick: () => switchLocale(item.value),
38
+ onClick: () => handleLangChange(item.value),
33
39
  })),
34
40
  ],
35
41
  [t],
@@ -13,8 +13,10 @@ import { createHeaderWithOpenAI } from '@/services/_header';
13
13
  import { API_ENDPOINTS } from '@/services/_url';
14
14
  import { useAgentStore } from '@/store/agent';
15
15
  import { agentSelectors } from '@/store/agent/slices/chat';
16
+ import { useGlobalStore } from '@/store/global';
17
+ import { globalGeneralSelectors } from '@/store/global/selectors';
16
18
  import { useUserStore } from '@/store/user';
17
- import { settingsSelectors, userGeneralSettingsSelectors } from '@/store/user/selectors';
19
+ import { settingsSelectors } from '@/store/user/selectors';
18
20
  import { TTSServer } from '@/types/agent';
19
21
 
20
22
  interface TTSConfig extends TTSOptions {
@@ -26,7 +28,7 @@ interface TTSConfig extends TTSOptions {
26
28
  export const useTTS = (content: string, config?: TTSConfig) => {
27
29
  const ttsSettings = useUserStore(settingsSelectors.currentTTS, isEqual);
28
30
  const ttsAgentSettings = useAgentStore(agentSelectors.currentAgentTTS, isEqual);
29
- const lang = useUserStore(userGeneralSettingsSelectors.currentLanguage);
31
+ const lang = useGlobalStore(globalGeneralSelectors.currentLanguage);
30
32
  const voice = useAgentStore(agentSelectors.currentAgentTTSVoice(lang));
31
33
  let useSelectedTTS;
32
34
  let options: any = {};
@@ -22,7 +22,7 @@ import {
22
22
  import { useUserStore } from '@/store/user';
23
23
  import { userGeneralSettingsSelectors } from '@/store/user/selectors';
24
24
  import { GlobalStyle } from '@/styles';
25
- import { setCookie } from '@/utils/cookie';
25
+ import { setCookie } from '@/utils/client/cookie';
26
26
 
27
27
  const useStyles = createStyles(({ css, token }) => ({
28
28
  app: css`
package/src/middleware.ts CHANGED
@@ -5,6 +5,7 @@ import urlJoin from 'url-join';
5
5
 
6
6
  import { appEnv } from '@/config/app';
7
7
  import { authEnv } from '@/config/auth';
8
+ import { LOBE_LOCALE_COOKIE } from '@/const/locale';
8
9
  import { LOBE_THEME_APPEARANCE } from '@/const/theme';
9
10
  import NextAuthEdge from '@/libs/next-auth/edge';
10
11
  import { Locales } from '@/locales/resources';
@@ -54,10 +55,8 @@ const defaultMiddleware = (request: NextRequest) => {
54
55
 
55
56
  // if it's a new user, there's no cookie
56
57
  // So we need to use the fallback language parsed by accept-language
57
- const locale = parseBrowserLanguage(request.headers) as Locales;
58
- // const locale =
59
- // request.cookies.get(LOBE_LOCALE_COOKIE)?.value ||
60
- // browserLanguage;
58
+ const browserLanguage = parseBrowserLanguage(request.headers);
59
+ const locale = (request.cookies.get(LOBE_LOCALE_COOKIE)?.value || browserLanguage) as Locales;
61
60
 
62
61
  const ua = request.headers.get('user-agent');
63
62
 
@@ -1,12 +1,12 @@
1
1
  import { Mock, beforeEach, describe, expect, it, vi } from 'vitest';
2
2
 
3
- import { globalHelpers } from '@/store/user/helpers';
3
+ import { globalHelpers } from '@/store/global/helpers';
4
4
 
5
5
  import { assistantService } from '../assistant';
6
6
 
7
7
  // Mocking modules and functions
8
8
 
9
- vi.mock('@/store/user/helpers', () => ({
9
+ vi.mock('@/store/global/helpers', () => ({
10
10
  globalHelpers: {
11
11
  getCurrentLanguage: vi.fn(),
12
12
  },
@@ -1,6 +1,6 @@
1
1
  import { Mock, beforeEach, describe, expect, it, vi } from 'vitest';
2
2
 
3
- import { globalHelpers } from '@/store/user/helpers';
3
+ import { globalHelpers } from '@/store/global/helpers';
4
4
 
5
5
  import { toolService } from '../tool';
6
6
  import openAPIV3 from './openai/OpenAPI_V3.json';
@@ -8,7 +8,7 @@ import OpenAIPlugin from './openai/plugin.json';
8
8
 
9
9
  // Mocking modules and functions
10
10
 
11
- vi.mock('@/store/user/helpers', () => ({
11
+ vi.mock('@/store/global/helpers', () => ({
12
12
  globalHelpers: {
13
13
  getCurrentLanguage: vi.fn(),
14
14
  },
@@ -1,7 +1,7 @@
1
1
  import { cloneDeep, merge } from 'lodash-es';
2
2
 
3
3
  import { DEFAULT_DISCOVER_ASSISTANT_ITEM } from '@/const/discover';
4
- import { globalHelpers } from '@/store/user/helpers';
4
+ import { globalHelpers } from '@/store/global/helpers';
5
5
  import { DiscoverAssistantItem } from '@/types/discover';
6
6
 
7
7
  import { API_ENDPOINTS } from './_url';
@@ -1,4 +1,4 @@
1
- import { globalHelpers } from '@/store/user/helpers';
1
+ import { globalHelpers } from '@/store/global/helpers';
2
2
  import { DiscoverPlugintem } from '@/types/discover';
3
3
  import { convertOpenAIManifestToLobeManifest, getToolManifest } from '@/utils/toolManifest';
4
4
 
@@ -1,89 +1,38 @@
1
1
  import isEqual from 'fast-deep-equal';
2
- import { produce } from 'immer';
3
2
  import { gt, parse, valid } from 'semver';
4
3
  import { SWRResponse } from 'swr';
5
4
  import type { StateCreator } from 'zustand/vanilla';
6
5
 
7
- import { INBOX_SESSION_ID } from '@/const/session';
8
- import { SESSION_CHAT_URL } from '@/const/url';
9
6
  import { CURRENT_VERSION } from '@/const/version';
10
7
  import { useOnlyFetchOnceSWR } from '@/libs/swr';
11
8
  import { globalService } from '@/services/global';
12
- import type { GlobalStore } from '@/store/global/index';
9
+ import type { SystemStatus } from '@/store/global/initialState';
10
+ import { LocaleMode } from '@/types/locale';
11
+ import { switchLang } from '@/utils/client/switchLang';
13
12
  import { merge } from '@/utils/merge';
14
13
  import { setNamespace } from '@/utils/storeDebug';
15
14
 
16
- import type { SystemStatus } from './initialState';
15
+ import type { GlobalStore } from '../store';
17
16
 
18
17
  const n = setNamespace('g');
19
18
 
20
- /**
21
- * 设置操作
22
- */
23
- export interface GlobalStoreAction {
24
- switchBackToChat: (sessionId?: string) => void;
25
- toggleChatSideBar: (visible?: boolean) => void;
26
- toggleExpandSessionGroup: (id: string, expand: boolean) => void;
27
- toggleMobilePortal: (visible?: boolean) => void;
28
- toggleMobileTopic: (visible?: boolean) => void;
29
- toggleSystemRole: (visible?: boolean) => void;
30
- toggleZenMode: () => void;
19
+ export interface GlobalGeneralAction {
20
+ switchLocale: (locale: LocaleMode) => void;
31
21
  updateSystemStatus: (status: Partial<SystemStatus>, action?: any) => void;
32
22
  useCheckLatestVersion: (enabledCheck?: boolean) => SWRResponse<string>;
33
23
  useInitSystemStatus: () => SWRResponse;
34
24
  }
35
25
 
36
- export const globalActionSlice: StateCreator<
26
+ export const generalActionSlice: StateCreator<
37
27
  GlobalStore,
38
28
  [['zustand/devtools', never]],
39
29
  [],
40
- GlobalStoreAction
30
+ GlobalGeneralAction
41
31
  > = (set, get) => ({
42
- switchBackToChat: (sessionId) => {
43
- get().router?.push(SESSION_CHAT_URL(sessionId || INBOX_SESSION_ID, get().isMobile));
44
- },
45
-
46
- toggleChatSideBar: (newValue) => {
47
- const showChatSideBar =
48
- typeof newValue === 'boolean' ? newValue : !get().status.showChatSideBar;
49
-
50
- get().updateSystemStatus({ showChatSideBar }, n('toggleAgentPanel', newValue));
51
- },
52
- toggleExpandSessionGroup: (id, expand) => {
53
- const { status } = get();
54
- const nextExpandSessionGroup = produce(status.expandSessionGroupKeys, (draft: string[]) => {
55
- if (expand) {
56
- if (draft.includes(id)) return;
57
- draft.push(id);
58
- } else {
59
- const index = draft.indexOf(id);
60
- if (index !== -1) draft.splice(index, 1);
61
- }
62
- });
63
- get().updateSystemStatus({ expandSessionGroupKeys: nextExpandSessionGroup });
64
- },
65
- toggleMobilePortal: (newValue) => {
66
- const mobileShowPortal =
67
- typeof newValue === 'boolean' ? newValue : !get().status.mobileShowPortal;
68
-
69
- get().updateSystemStatus({ mobileShowPortal }, n('toggleMobilePortal', newValue));
70
- },
71
- toggleMobileTopic: (newValue) => {
72
- const mobileShowTopic =
73
- typeof newValue === 'boolean' ? newValue : !get().status.mobileShowTopic;
74
-
75
- get().updateSystemStatus({ mobileShowTopic }, n('toggleMobileTopic', newValue));
76
- },
77
- toggleSystemRole: (newValue) => {
78
- const showSystemRole = typeof newValue === 'boolean' ? newValue : !get().status.mobileShowTopic;
79
-
80
- get().updateSystemStatus({ showSystemRole }, n('toggleMobileTopic', newValue));
81
- },
82
- toggleZenMode: () => {
83
- const { status } = get();
84
- const nextZenMode = !status.zenMode;
32
+ switchLocale: (locale) => {
33
+ get().updateSystemStatus({ language: locale });
85
34
 
86
- get().updateSystemStatus({ zenMode: nextZenMode }, n('toggleZenMode'));
35
+ switchLang(locale);
87
36
  },
88
37
  updateSystemStatus: (status, action) => {
89
38
  // Status cannot be modified when it is not initialized
@@ -0,0 +1,73 @@
1
+ import { produce } from 'immer';
2
+ import type { StateCreator } from 'zustand/vanilla';
3
+
4
+ import { INBOX_SESSION_ID } from '@/const/session';
5
+ import { SESSION_CHAT_URL } from '@/const/url';
6
+ import type { GlobalStore } from '@/store/global';
7
+ import { setNamespace } from '@/utils/storeDebug';
8
+
9
+ const n = setNamespace('w');
10
+
11
+ export interface GlobalWorkspacePaneAction {
12
+ switchBackToChat: (sessionId?: string) => void;
13
+ toggleChatSideBar: (visible?: boolean) => void;
14
+ toggleExpandSessionGroup: (id: string, expand: boolean) => void;
15
+ toggleMobilePortal: (visible?: boolean) => void;
16
+ toggleMobileTopic: (visible?: boolean) => void;
17
+ toggleSystemRole: (visible?: boolean) => void;
18
+ toggleZenMode: () => void;
19
+ }
20
+
21
+ export const globalWorkspaceSlice: StateCreator<
22
+ GlobalStore,
23
+ [['zustand/devtools', never]],
24
+ [],
25
+ GlobalWorkspacePaneAction
26
+ > = (set, get) => ({
27
+ switchBackToChat: (sessionId) => {
28
+ get().router?.push(SESSION_CHAT_URL(sessionId || INBOX_SESSION_ID, get().isMobile));
29
+ },
30
+
31
+ toggleChatSideBar: (newValue) => {
32
+ const showChatSideBar =
33
+ typeof newValue === 'boolean' ? newValue : !get().status.showChatSideBar;
34
+
35
+ get().updateSystemStatus({ showChatSideBar }, n('toggleAgentPanel', newValue));
36
+ },
37
+ toggleExpandSessionGroup: (id, expand) => {
38
+ const { status } = get();
39
+ const nextExpandSessionGroup = produce(status.expandSessionGroupKeys, (draft: string[]) => {
40
+ if (expand) {
41
+ if (draft.includes(id)) return;
42
+ draft.push(id);
43
+ } else {
44
+ const index = draft.indexOf(id);
45
+ if (index !== -1) draft.splice(index, 1);
46
+ }
47
+ });
48
+ get().updateSystemStatus({ expandSessionGroupKeys: nextExpandSessionGroup });
49
+ },
50
+ toggleMobilePortal: (newValue) => {
51
+ const mobileShowPortal =
52
+ typeof newValue === 'boolean' ? newValue : !get().status.mobileShowPortal;
53
+
54
+ get().updateSystemStatus({ mobileShowPortal }, n('toggleMobilePortal', newValue));
55
+ },
56
+ toggleMobileTopic: (newValue) => {
57
+ const mobileShowTopic =
58
+ typeof newValue === 'boolean' ? newValue : !get().status.mobileShowTopic;
59
+
60
+ get().updateSystemStatus({ mobileShowTopic }, n('toggleMobileTopic', newValue));
61
+ },
62
+ toggleSystemRole: (newValue) => {
63
+ const showSystemRole = typeof newValue === 'boolean' ? newValue : !get().status.mobileShowTopic;
64
+
65
+ get().updateSystemStatus({ showSystemRole }, n('toggleMobileTopic', newValue));
66
+ },
67
+ toggleZenMode: () => {
68
+ const { status } = get();
69
+ const nextZenMode = !status.zenMode;
70
+
71
+ get().updateSystemStatus({ zenMode: nextZenMode }, n('toggleZenMode'));
72
+ },
73
+ });
@@ -0,0 +1,6 @@
1
+ import { useGlobalStore } from '@/store/global/index';
2
+ import { globalGeneralSelectors } from '@/store/global/selectors';
3
+
4
+ const getCurrentLanguage = () => globalGeneralSelectors.currentLanguage(useGlobalStore.getState());
5
+
6
+ export const globalHelpers = { getCurrentLanguage };
@@ -1,6 +1,7 @@
1
1
  import { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime';
2
2
 
3
3
  import { DatabaseLoadingState } from '@/types/clientDB';
4
+ import { LocaleMode } from '@/types/locale';
4
5
  import { SessionDefaultGroup } from '@/types/session';
5
6
  import { AsyncLocalStorage } from '@/utils/localStorage';
6
7
 
@@ -49,6 +50,7 @@ export interface SystemStatus {
49
50
  * 应用初始化时不启用 PGLite,只有当用户手动开启时才启用
50
51
  */
51
52
  isEnablePglite?: boolean;
53
+ language?: LocaleMode;
52
54
  latestChangelogId?: string;
53
55
  mobileShowPortal?: boolean;
54
56
  mobileShowTopic?: boolean;
@@ -0,0 +1,18 @@
1
+ import { GlobalState, initialState } from '@/store/global/initialState';
2
+ import { merge } from '@/utils/merge';
3
+
4
+ import { globalGeneralSelectors } from './general';
5
+
6
+ describe('settingsSelectors', () => {
7
+ describe('currentLanguage', () => {
8
+ it('should return the correct language setting', () => {
9
+ const s: GlobalState = merge(initialState, {
10
+ status: { language: 'fr' },
11
+ });
12
+
13
+ const result = globalGeneralSelectors.currentLanguage(s);
14
+
15
+ expect(result).toBe('fr');
16
+ });
17
+ });
18
+ });
@@ -0,0 +1,25 @@
1
+ import { DEFAULT_LANG } from '@/const/locale';
2
+ import { Locales } from '@/locales/resources';
3
+ import { isOnServerSide } from '@/utils/env';
4
+
5
+ import { GlobalState } from '../initialState';
6
+ import { systemStatus } from './systemStatus';
7
+
8
+ const language = (s: GlobalState) => systemStatus(s).language || 'auto';
9
+
10
+ const currentLanguage = (s: GlobalState) => {
11
+ const locale = language(s);
12
+
13
+ if (locale === 'auto') {
14
+ if (isOnServerSide) return DEFAULT_LANG;
15
+
16
+ return navigator.language as Locales;
17
+ }
18
+
19
+ return locale as Locales;
20
+ };
21
+
22
+ export const globalGeneralSelectors = {
23
+ currentLanguage,
24
+ language,
25
+ };
@@ -0,0 +1,2 @@
1
+ export * from './general';
2
+ export * from './systemStatus';
@@ -2,9 +2,9 @@ import { isServerMode, isUsePgliteDB } from '@/const/version';
2
2
  import { GlobalStore } from '@/store/global';
3
3
  import { DatabaseLoadingState } from '@/types/clientDB';
4
4
 
5
- import { INITIAL_STATUS } from './initialState';
5
+ import { GlobalState, INITIAL_STATUS } from '../initialState';
6
6
 
7
- const systemStatus = (s: GlobalStore) => s.status;
7
+ export const systemStatus = (s: GlobalState) => s.status;
8
8
 
9
9
  const sessionGroupKeys = (s: GlobalStore): string[] =>
10
10
  s.status.expandSessionGroupKeys || INITIAL_STATUS.expandSessionGroupKeys;
@@ -4,18 +4,26 @@ import { createWithEqualityFn } from 'zustand/traditional';
4
4
  import { StateCreator } from 'zustand/vanilla';
5
5
 
6
6
  import { createDevtools } from '../middleware/createDevtools';
7
- import { type GlobalStoreAction, globalActionSlice } from './action';
8
7
  import { type GlobalClientDBAction, clientDBSlice } from './actions/clientDb';
8
+ import { type GlobalGeneralAction, generalActionSlice } from './actions/general';
9
+ import { type GlobalWorkspacePaneAction, globalWorkspaceSlice } from './actions/workspacePane';
9
10
  import { type GlobalState, initialState } from './initialState';
10
11
 
11
12
  // =============== 聚合 createStoreFn ============ //
12
13
 
13
- export type GlobalStore = GlobalState & GlobalStoreAction & GlobalClientDBAction;
14
+ export interface GlobalStore
15
+ extends GlobalState,
16
+ GlobalWorkspacePaneAction,
17
+ GlobalClientDBAction,
18
+ GlobalGeneralAction {
19
+ /* empty */
20
+ }
14
21
 
15
22
  const createStore: StateCreator<GlobalStore, [['zustand/devtools', never]]> = (...parameters) => ({
16
23
  ...initialState,
17
- ...globalActionSlice(...parameters),
24
+ ...globalWorkspaceSlice(...parameters),
18
25
  ...clientDBSlice(...parameters),
26
+ ...generalActionSlice(...parameters),
19
27
  });
20
28
 
21
29
  // =============== 实装 useStore ============ //
@@ -1,5 +1,4 @@
1
1
  import { act, renderHook, waitFor } from '@testing-library/react';
2
- import { mutate } from 'swr';
3
2
  import { afterEach, describe, expect, it, vi } from 'vitest';
4
3
  import { withSWR } from '~test-utils';
5
4
 
@@ -10,14 +9,9 @@ import { useUserStore } from '@/store/user';
10
9
  import { preferenceSelectors } from '@/store/user/selectors';
11
10
  import { GlobalServerConfig } from '@/types/serverConfig';
12
11
  import { UserInitializationState, UserPreference } from '@/types/user';
13
- import { switchLang } from '@/utils/client/switchLang';
14
12
 
15
13
  vi.mock('zustand/traditional');
16
14
 
17
- vi.mock('@/utils/client/switchLang', () => ({
18
- switchLang: vi.fn(),
19
- }));
20
-
21
15
  vi.mock('swr', async (importOriginal) => {
22
16
  const modules = await importOriginal();
23
17
  return {
@@ -87,7 +81,7 @@ describe('createCommonSlice', () => {
87
81
  telemetry: true,
88
82
  },
89
83
  settings: {
90
- general: { language: 'en-US' },
84
+ general: { fontSize: 14 },
91
85
  },
92
86
  };
93
87
 
@@ -111,9 +105,6 @@ describe('createCommonSlice', () => {
111
105
  expect(useUserStore.getState().user?.avatar).toBe(mockUserState.avatar);
112
106
  expect(useUserStore.getState().settings).toEqual(mockUserState.settings);
113
107
  expect(successCallback).toHaveBeenCalledWith(mockUserState);
114
-
115
- // 验证是否正确处理了语言设置
116
- expect(switchLang).not.toHaveBeenCalledWith('auto');
117
108
  });
118
109
 
119
110
  it('should call switch language when language is auto', async () => {
@@ -134,9 +125,6 @@ describe('createCommonSlice', () => {
134
125
 
135
126
  // 等待 SWR 完成数据获取
136
127
  await waitFor(() => expect(result.current.data).toEqual(mockUserState));
137
-
138
- // 验证是否正确处理了语言设置
139
- expect(switchLang).toHaveBeenCalledWith('auto');
140
128
  });
141
129
 
142
130
  it('should fetch use server config correctly', async () => {
@@ -169,7 +157,7 @@ describe('createCommonSlice', () => {
169
157
  isOnboard: true,
170
158
  preference: savedPreference,
171
159
  settings: {
172
- general: { language: 'en-US' },
160
+ general: { fontSize: 14 },
173
161
  },
174
162
  };
175
163
  vi.spyOn(userService, 'getUserState').mockResolvedValueOnce(mockUserState);
@@ -219,7 +207,7 @@ describe('createCommonSlice', () => {
219
207
  isOnboard: true,
220
208
  preference: {} as any,
221
209
  settings: {
222
- general: { language: 'en-US' },
210
+ general: { fontSize: 12 },
223
211
  },
224
212
  };
225
213
 
@@ -9,12 +9,10 @@ import type { UserStore } from '@/store/user';
9
9
  import type { GlobalServerConfig } from '@/types/serverConfig';
10
10
  import { UserInitializationState } from '@/types/user';
11
11
  import type { UserSettings } from '@/types/user/settings';
12
- import { switchLang } from '@/utils/client/switchLang';
13
12
  import { merge } from '@/utils/merge';
14
13
  import { setNamespace } from '@/utils/storeDebug';
15
14
 
16
15
  import { preferenceSelectors } from '../preference/selectors';
17
- import { userGeneralSettingsSelectors } from '../settings/selectors';
18
16
 
19
17
  const n = setNamespace('common');
20
18
 
@@ -114,12 +112,6 @@ export const createCommonSlice: StateCreator<
114
112
  );
115
113
 
116
114
  get().refreshDefaultModelProviderList({ trigger: 'fetchUserState' });
117
-
118
- // auto switch language
119
- const language = userGeneralSettingsSelectors.config(get()).language;
120
- if (language === 'auto') {
121
- switchLang('auto');
122
- }
123
115
  }
124
116
  },
125
117
  },
@@ -7,7 +7,6 @@ import { MESSAGE_CANCEL_FLAT } from '@/const/message';
7
7
  import { shareService } from '@/services/share';
8
8
  import { userService } from '@/services/user';
9
9
  import type { UserStore } from '@/store/user';
10
- import { LocaleMode } from '@/types/locale';
11
10
  import { LobeAgentSettings } from '@/types/session';
12
11
  import {
13
12
  SystemAgentItem,
@@ -16,7 +15,6 @@ import {
16
15
  UserSettings,
17
16
  UserSystemAgentConfigKey,
18
17
  } from '@/types/user/settings';
19
- import { switchLang } from '@/utils/client/switchLang';
20
18
  import { difference } from '@/utils/difference';
21
19
  import { merge } from '@/utils/merge';
22
20
 
@@ -26,7 +24,6 @@ export interface UserSettingsAction {
26
24
  internal_createSignal: () => AbortController;
27
25
  resetSettings: () => Promise<void>;
28
26
  setSettings: (settings: DeepPartial<UserSettings>) => Promise<void>;
29
- switchLocale: (locale: LocaleMode) => Promise<void>;
30
27
  switchThemeMode: (themeMode: ThemeMode) => Promise<void>;
31
28
  updateDefaultAgent: (agent: DeepPartial<LobeAgentSettings>) => Promise<void>;
32
29
  updateGeneralConfig: (settings: Partial<UserGeneralConfig>) => Promise<void>;
@@ -95,11 +92,6 @@ export const createSettingsSlice: StateCreator<
95
92
  await userService.updateUserSettings(diffs, abortController.signal);
96
93
  await get().refreshUserState();
97
94
  },
98
- switchLocale: async (locale) => {
99
- await get().updateGeneralConfig({ language: locale });
100
-
101
- switchLang(locale);
102
- },
103
95
  switchThemeMode: async (themeMode) => {
104
96
  await get().updateGeneralConfig({ themeMode });
105
97
  },
@@ -5,20 +5,6 @@ import { merge } from '@/utils/merge';
5
5
  import { userGeneralSettingsSelectors } from './general';
6
6
 
7
7
  describe('settingsSelectors', () => {
8
- describe('currentLanguage', () => {
9
- it('should return the correct language setting', () => {
10
- const s: UserState = merge(initialState, {
11
- settings: {
12
- general: { language: 'fr' },
13
- },
14
- });
15
-
16
- const result = userGeneralSettingsSelectors.currentLanguage(s as UserStore);
17
-
18
- expect(result).toBe('fr');
19
- });
20
- });
21
-
22
8
  describe('currentThemeMode', () => {
23
9
  it('should return the correct theme', () => {
24
10
  const s: UserState = merge(initialState, {
@@ -1,24 +1,8 @@
1
- import { DEFAULT_LANG } from '@/const/locale';
2
- import { Locales } from '@/locales/resources';
3
- import { isOnServerSide } from '@/utils/env';
4
-
5
1
  import { UserStore } from '../../../store';
6
2
  import { currentSettings } from './settings';
7
3
 
8
4
  const generalConfig = (s: UserStore) => currentSettings(s).general || {};
9
5
 
10
- const currentLanguage = (s: UserStore) => {
11
- const locale = generalConfig(s).language;
12
-
13
- if (locale === 'auto') {
14
- if (isOnServerSide) return DEFAULT_LANG;
15
-
16
- return navigator.language as Locales;
17
- }
18
-
19
- return locale;
20
- };
21
-
22
6
  const currentThemeMode = (s: UserStore) => {
23
7
  const themeMode = generalConfig(s).themeMode;
24
8
  return themeMode || 'auto';
@@ -27,14 +11,11 @@ const currentThemeMode = (s: UserStore) => {
27
11
  const neutralColor = (s: UserStore) => generalConfig(s).neutralColor;
28
12
  const primaryColor = (s: UserStore) => generalConfig(s).primaryColor;
29
13
  const fontSize = (s: UserStore) => generalConfig(s).fontSize;
30
- const language = (s: UserStore) => generalConfig(s).language;
31
14
 
32
15
  export const userGeneralSettingsSelectors = {
33
16
  config: generalConfig,
34
- currentLanguage,
35
17
  currentThemeMode,
36
18
  fontSize,
37
- language,
38
19
  neutralColor,
39
20
  primaryColor,
40
21
  };
@@ -1,11 +1,8 @@
1
1
  import type { NeutralColors, PrimaryColors } from '@lobehub/ui';
2
2
  import type { ThemeMode } from 'antd-style';
3
3
 
4
- import { LocaleMode } from '@/types/locale';
5
-
6
4
  export interface UserGeneralConfig {
7
5
  fontSize: number;
8
- language: LocaleMode;
9
6
  neutralColor?: NeutralColors;
10
7
  primaryColor?: PrimaryColors;
11
8
  themeMode: ThemeMode;
@@ -0,0 +1,85 @@
1
+ import dayjs from 'dayjs';
2
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ import { COOKIE_CACHE_DAYS } from '@/const/settings';
5
+
6
+ import { setCookie } from './cookie';
7
+
8
+ describe('setCookie', () => {
9
+ // Mock document.cookie since we're in a test environment
10
+ beforeEach(() => {
11
+ // Create a getter/setter for document.cookie
12
+ Object.defineProperty(document, 'cookie', {
13
+ writable: true,
14
+ value: '',
15
+ });
16
+ });
17
+
18
+ it('should set a cookie with default expiration', () => {
19
+ const key = 'testKey';
20
+ const value = 'testValue';
21
+
22
+ // Mock the current date
23
+ const mockDate = new Date('2024-01-01T00:00:00Z');
24
+ vi.setSystemTime(mockDate);
25
+
26
+ // Calculate expected expiration date
27
+ const expectedExpires = dayjs(mockDate).add(COOKIE_CACHE_DAYS, 'day').toDate().toUTCString();
28
+
29
+ setCookie(key, value);
30
+
31
+ expect(document.cookie).toBe(`${key}=${value};expires=${expectedExpires};path=/;`);
32
+
33
+ // Reset system time
34
+ vi.useRealTimers();
35
+ });
36
+
37
+ it('should set a cookie with custom expiration days', () => {
38
+ const key = 'testKey';
39
+ const value = 'testValue';
40
+ const customDays = 7;
41
+
42
+ // Mock the current date
43
+ const mockDate = new Date('2024-01-01T00:00:00Z');
44
+ vi.setSystemTime(mockDate);
45
+
46
+ // Calculate expected expiration date
47
+ const expectedExpires = dayjs(mockDate).add(customDays, 'day').toDate().toUTCString();
48
+
49
+ setCookie(key, value, customDays);
50
+
51
+ expect(document.cookie).toBe(`${key}=${value};expires=${expectedExpires};path=/;`);
52
+
53
+ // Reset system time
54
+ vi.useRealTimers();
55
+ });
56
+
57
+ it('should remove cookie when value is undefined', () => {
58
+ const key = 'testKey';
59
+
60
+ // Expected expiration date for removal (1970-01-01T00:00:00Z)
61
+ const expectedExpires = new Date(0).toUTCString();
62
+
63
+ setCookie(key, undefined);
64
+
65
+ expect(document.cookie).toBe(`${key}=; expires=${expectedExpires}; path=/;`);
66
+ });
67
+
68
+ it('should handle special characters in key and value', () => {
69
+ const key = 'test@Key';
70
+ const value = 'test Value with spaces';
71
+
72
+ // Mock the current date
73
+ const mockDate = new Date('2024-01-01T00:00:00Z');
74
+ vi.setSystemTime(mockDate);
75
+
76
+ const expectedExpires = dayjs(mockDate).add(COOKIE_CACHE_DAYS, 'day').toDate().toUTCString();
77
+
78
+ setCookie(key, value);
79
+
80
+ expect(document.cookie).toBe(`${key}=${value};expires=${expectedExpires};path=/;`);
81
+
82
+ // Reset system time
83
+ vi.useRealTimers();
84
+ });
85
+ });
@@ -0,0 +1,22 @@
1
+ import dayjs from 'dayjs';
2
+
3
+ import { COOKIE_CACHE_DAYS } from '@/const/settings';
4
+
5
+ export const setCookie = (
6
+ key: string,
7
+ value: string | undefined,
8
+ expireDays = COOKIE_CACHE_DAYS,
9
+ ) => {
10
+ if (typeof value === 'undefined') {
11
+ // Set the expiration time to yesterday (expire immediately)
12
+ const expiredDate = new Date(0).toUTCString(); // 1970-01-01T00:00:00Z
13
+
14
+ // eslint-disable-next-line unicorn/no-document-cookie
15
+ document.cookie = `${key}=; expires=${expiredDate}; path=/;`;
16
+ } else {
17
+ const expires = dayjs().add(expireDays, 'day').toDate().toUTCString();
18
+
19
+ // eslint-disable-next-line unicorn/no-document-cookie
20
+ document.cookie = `${key}=${value};expires=${expires};path=/;`;
21
+ }
22
+ };
@@ -1,10 +1,15 @@
1
1
  import { changeLanguage } from 'i18next';
2
2
 
3
+ import { LOBE_LOCALE_COOKIE } from '@/const/locale';
3
4
  import { LocaleMode } from '@/types/locale';
4
5
 
6
+ import { setCookie } from './cookie';
7
+
5
8
  export const switchLang = (locale: LocaleMode) => {
6
9
  const lang = locale === 'auto' ? navigator.language : locale;
7
10
 
8
11
  changeLanguage(lang);
9
12
  document.documentElement.lang = lang;
13
+
14
+ setCookie(LOBE_LOCALE_COOKIE, locale === 'auto' ? undefined : locale, 365);
10
15
  };
@@ -1,9 +0,0 @@
1
- import { userGeneralSettingsSelectors } from './slices/settings/selectors';
2
- import { useUserStore } from './store';
3
-
4
- const getCurrentLanguage = () =>
5
- userGeneralSettingsSelectors.currentLanguage(useUserStore.getState());
6
-
7
- export const globalHelpers = {
8
- getCurrentLanguage,
9
- };
@@ -1,10 +0,0 @@
1
- import dayjs from 'dayjs';
2
-
3
- import { COOKIE_CACHE_DAYS } from '@/const/settings';
4
-
5
- export const setCookie = (key: string, value: string | undefined) => {
6
- const expires = dayjs().add(COOKIE_CACHE_DAYS, 'day').toISOString();
7
-
8
- // eslint-disable-next-line unicorn/no-document-cookie
9
- document.cookie = `${key}=${value};expires=${expires};path=/;`;
10
- };