@lobehub/chat 1.97.0 → 1.97.2
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/CHANGELOG.md +50 -0
- package/README.md +90 -32
- package/README.zh-CN.md +90 -34
- package/changelog/v1.json +18 -0
- package/docs/usage/features/desktop.mdx +53 -0
- package/docs/usage/features/desktop.zh-CN.mdx +49 -0
- package/docs/usage/features/mcp-market.mdx +26 -0
- package/docs/usage/features/mcp-market.zh-CN.mdx +22 -0
- package/docs/usage/features/mcp.mdx +58 -0
- package/docs/usage/features/mcp.zh-CN.mdx +54 -0
- package/docs/usage/features/search.mdx +56 -0
- package/docs/usage/features/search.zh-CN.mdx +52 -0
- package/package.json +3 -2
- package/src/app/(backend)/trpc/async/[trpc]/route.ts +4 -2
- package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/index.tsx +2 -0
- package/src/app/[variants]/(main)/chat/(workspace)/_layout/Mobile/index.tsx +2 -0
- package/src/app/[variants]/(main)/chat/@session/features/SessionListContent/List/index.tsx +35 -2
- package/src/components/Analytics/LobeAnalyticsProvider.tsx +68 -0
- package/src/components/Analytics/LobeAnalyticsProviderWrapper.tsx +23 -0
- package/src/components/Analytics/MainInterfaceTracker.tsx +52 -0
- package/src/components/Analytics/index.tsx +0 -8
- package/src/const/analytics.ts +3 -0
- package/src/database/schemas/relations.ts +44 -0
- package/src/features/AgentSetting/store/action.ts +26 -1
- package/src/features/ChatInput/useSend.ts +29 -1
- package/src/features/User/UserLoginOrSignup/Community.tsx +14 -1
- package/src/layout/GlobalProvider/Locale.tsx +2 -1
- package/src/layout/GlobalProvider/index.tsx +4 -1
- package/src/libs/analytics/index.ts +25 -0
- package/src/locales/create.ts +8 -3
- package/src/server/routers/async/caller.ts +2 -2
- package/src/server/services/user/index.test.ts +8 -0
- package/src/server/services/user/index.ts +18 -0
- package/src/store/session/slices/session/action.ts +28 -2
- package/src/store/user/slices/common/action.ts +9 -2
- package/src/utils/locale.ts +16 -1
- package/tests/setup.ts +11 -0
@@ -8,7 +8,6 @@ import Google from './Google';
|
|
8
8
|
import Vercel from './Vercel';
|
9
9
|
|
10
10
|
const Plausible = dynamic(() => import('./Plausible'));
|
11
|
-
const Posthog = dynamic(() => import('./Posthog'));
|
12
11
|
const Umami = dynamic(() => import('./Umami'));
|
13
12
|
const Clarity = dynamic(() => import('./Clarity'));
|
14
13
|
const ReactScan = dynamic(() => import('./ReactScan'));
|
@@ -24,13 +23,6 @@ const Analytics = () => {
|
|
24
23
|
scriptBaseUrl={analyticsEnv.PLAUSIBLE_SCRIPT_BASE_URL}
|
25
24
|
/>
|
26
25
|
)}
|
27
|
-
{analyticsEnv.ENABLED_POSTHOG_ANALYTICS && (
|
28
|
-
<Posthog
|
29
|
-
debug={analyticsEnv.DEBUG_POSTHOG_ANALYTICS}
|
30
|
-
host={analyticsEnv.POSTHOG_HOST!}
|
31
|
-
token={analyticsEnv.POSTHOG_KEY}
|
32
|
-
/>
|
33
|
-
)}
|
34
26
|
{analyticsEnv.ENABLED_UMAMI_ANALYTICS && (
|
35
27
|
<Umami
|
36
28
|
scriptUrl={analyticsEnv.UMAMI_SCRIPT_URL}
|
@@ -122,6 +122,17 @@ export const agentsToSessionsRelations = relations(agentsToSessions, ({ one }) =
|
|
122
122
|
}),
|
123
123
|
}));
|
124
124
|
|
125
|
+
export const filesToSessionsRelations = relations(filesToSessions, ({ one }) => ({
|
126
|
+
file: one(files, {
|
127
|
+
fields: [filesToSessions.fileId],
|
128
|
+
references: [files.id],
|
129
|
+
}),
|
130
|
+
session: one(sessions, {
|
131
|
+
fields: [filesToSessions.sessionId],
|
132
|
+
references: [sessions.id],
|
133
|
+
}),
|
134
|
+
}));
|
135
|
+
|
125
136
|
export const agentsKnowledgeBasesRelations = relations(agentsKnowledgeBases, ({ one }) => ({
|
126
137
|
knowledgeBase: one(knowledgeBases, {
|
127
138
|
fields: [agentsKnowledgeBases.knowledgeBaseId],
|
@@ -133,6 +144,39 @@ export const agentsKnowledgeBasesRelations = relations(agentsKnowledgeBases, ({
|
|
133
144
|
}),
|
134
145
|
}));
|
135
146
|
|
147
|
+
export const agentsFilesRelations = relations(agentsFiles, ({ one }) => ({
|
148
|
+
file: one(files, {
|
149
|
+
fields: [agentsFiles.fileId],
|
150
|
+
references: [files.id],
|
151
|
+
}),
|
152
|
+
agent: one(agents, {
|
153
|
+
fields: [agentsFiles.agentId],
|
154
|
+
references: [agents.id],
|
155
|
+
}),
|
156
|
+
}));
|
157
|
+
|
158
|
+
export const messagesFilesRelations = relations(messagesFiles, ({ one }) => ({
|
159
|
+
file: one(files, {
|
160
|
+
fields: [messagesFiles.fileId],
|
161
|
+
references: [files.id],
|
162
|
+
}),
|
163
|
+
message: one(messages, {
|
164
|
+
fields: [messagesFiles.messageId],
|
165
|
+
references: [messages.id],
|
166
|
+
}),
|
167
|
+
}));
|
168
|
+
|
169
|
+
export const fileChunksRelations = relations(fileChunks, ({ one }) => ({
|
170
|
+
file: one(files, {
|
171
|
+
fields: [fileChunks.fileId],
|
172
|
+
references: [files.id],
|
173
|
+
}),
|
174
|
+
chunk: one(chunks, {
|
175
|
+
fields: [fileChunks.chunkId],
|
176
|
+
references: [chunks.id],
|
177
|
+
}),
|
178
|
+
}));
|
179
|
+
|
136
180
|
export const sessionsRelations = relations(sessions, ({ many, one }) => ({
|
137
181
|
filesToSessions: many(filesToSessions),
|
138
182
|
agentsToSessions: many(agentsToSessions),
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import { getSingletonAnalyticsOptional } from '@lobehub/analytics';
|
1
2
|
import { DeepPartial } from 'utility-types';
|
2
3
|
import { StateCreator } from 'zustand/vanilla';
|
3
4
|
|
@@ -260,7 +261,31 @@ export const store: StateCreator<Store, [['zustand/devtools', never]]> = (set, g
|
|
260
261
|
await get().dispatchConfig({ config, type: 'update' });
|
261
262
|
},
|
262
263
|
setAgentMeta: async (meta) => {
|
263
|
-
|
264
|
+
const { dispatchMeta, id, meta: currentMeta } = get();
|
265
|
+
const mergedMeta = merge(currentMeta, meta);
|
266
|
+
|
267
|
+
try {
|
268
|
+
const analytics = getSingletonAnalyticsOptional();
|
269
|
+
if (analytics) {
|
270
|
+
analytics.track({
|
271
|
+
name: 'agent_meta_updated',
|
272
|
+
properties: {
|
273
|
+
assistant_avatar: mergedMeta.avatar,
|
274
|
+
assistant_background_color: mergedMeta.backgroundColor,
|
275
|
+
assistant_description: mergedMeta.description,
|
276
|
+
assistant_name: mergedMeta.title,
|
277
|
+
assistant_tags: mergedMeta.tags,
|
278
|
+
is_inbox: id === 'inbox',
|
279
|
+
session_id: id || 'unknown',
|
280
|
+
timestamp: Date.now(),
|
281
|
+
user_id: useUserStore.getState().user?.id || 'anonymous',
|
282
|
+
},
|
283
|
+
});
|
284
|
+
}
|
285
|
+
} catch (error) {
|
286
|
+
console.warn('Failed to track agent meta update:', error);
|
287
|
+
}
|
288
|
+
await dispatchMeta({ type: 'update', value: meta });
|
264
289
|
},
|
265
290
|
|
266
291
|
setChatConfig: async (config) => {
|
@@ -1,8 +1,12 @@
|
|
1
|
+
import { useAnalytics } from '@lobehub/analytics/react';
|
1
2
|
import { useCallback, useMemo } from 'react';
|
2
3
|
|
4
|
+
import { getAgentStoreState } from '@/store/agent';
|
5
|
+
import { agentSelectors } from '@/store/agent/selectors';
|
3
6
|
import { useChatStore } from '@/store/chat';
|
4
|
-
import { chatSelectors } from '@/store/chat/selectors';
|
7
|
+
import { chatSelectors, topicSelectors } from '@/store/chat/selectors';
|
5
8
|
import { fileChatSelectors, useFileStore } from '@/store/file';
|
9
|
+
import { getUserStoreState } from '@/store/user';
|
6
10
|
import { SendMessageParams } from '@/types/message';
|
7
11
|
|
8
12
|
export type UseSendMessageParams = Pick<
|
@@ -15,6 +19,7 @@ export const useSendMessage = () => {
|
|
15
19
|
s.sendMessage,
|
16
20
|
s.updateInputMessage,
|
17
21
|
]);
|
22
|
+
const { analytics } = useAnalytics();
|
18
23
|
|
19
24
|
const clearChatUploadFileList = useFileStore((s) => s.clearChatUploadFileList);
|
20
25
|
|
@@ -49,6 +54,29 @@ export const useSendMessage = () => {
|
|
49
54
|
updateInputMessage('');
|
50
55
|
clearChatUploadFileList();
|
51
56
|
|
57
|
+
// 获取分析数据
|
58
|
+
const userStore = getUserStoreState();
|
59
|
+
const agentStore = getAgentStoreState();
|
60
|
+
|
61
|
+
// 直接使用现有数据结构判断消息类型
|
62
|
+
const hasImages = fileList.some((file) => file.file?.type?.startsWith('image'));
|
63
|
+
const messageType = fileList.length === 0 ? 'text' : hasImages ? 'image' : 'file';
|
64
|
+
|
65
|
+
analytics?.track({
|
66
|
+
name: 'send_message',
|
67
|
+
properties: {
|
68
|
+
chat_id: store.activeId || 'unknown',
|
69
|
+
current_topic: topicSelectors.currentActiveTopic(store)?.title || null,
|
70
|
+
has_attachments: fileList.length > 0,
|
71
|
+
history_message_count: chatSelectors.activeBaseChats(store).length,
|
72
|
+
message: store.inputMessage,
|
73
|
+
message_length: store.inputMessage.length,
|
74
|
+
message_type: messageType,
|
75
|
+
selected_model: agentSelectors.currentAgentModel(agentStore),
|
76
|
+
session_id: store.activeId || 'inbox', // 当前活跃的会话ID
|
77
|
+
user_id: userStore.user?.id || 'anonymous',
|
78
|
+
},
|
79
|
+
});
|
52
80
|
// const hasSystemRole = agentSelectors.hasSystemRole(useAgentStore.getState());
|
53
81
|
// const agentSetting = useAgentStore.getState().agentSettingInstance;
|
54
82
|
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import { useAnalytics } from '@lobehub/analytics/react';
|
1
2
|
import { Button } from '@lobehub/ui';
|
2
3
|
import { memo } from 'react';
|
3
4
|
import { useTranslation } from 'react-i18next';
|
@@ -7,12 +8,24 @@ import UserInfo from '../UserInfo';
|
|
7
8
|
|
8
9
|
const UserLoginOrSignup = memo<{ onClick: () => void }>(({ onClick }) => {
|
9
10
|
const { t } = useTranslation('auth');
|
11
|
+
const { analytics } = useAnalytics();
|
12
|
+
|
13
|
+
const handleClick = () => {
|
14
|
+
analytics?.track({
|
15
|
+
name: 'login_or_signup_clicked',
|
16
|
+
properties: {
|
17
|
+
spm: 'homepage.login_or_signup.click',
|
18
|
+
},
|
19
|
+
});
|
20
|
+
|
21
|
+
onClick();
|
22
|
+
};
|
10
23
|
|
11
24
|
return (
|
12
25
|
<>
|
13
26
|
<UserInfo />
|
14
27
|
<Flexbox paddingBlock={12} paddingInline={16} width={'100%'}>
|
15
|
-
<Button block onClick={
|
28
|
+
<Button block onClick={handleClick} type={'primary'}>
|
16
29
|
{t('loginOrSignup')}
|
17
30
|
</Button>
|
18
31
|
</Flexbox>
|
@@ -40,7 +40,8 @@ const Locale = memo<LocaleLayoutProps>(({ children, defaultLang, antdLocale }) =
|
|
40
40
|
|
41
41
|
// if run on server side, init i18n instance everytime
|
42
42
|
if (isOnServerSide) {
|
43
|
-
|
43
|
+
// use sync mode to init instantly
|
44
|
+
i18n.init({ initAsync: false });
|
44
45
|
|
45
46
|
// load the dayjs locale
|
46
47
|
// if (lang) {
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import { ReactNode, Suspense } from 'react';
|
2
2
|
|
3
|
+
import { LobeAnalyticsProviderWrapper } from '@/components/Analytics/LobeAnalyticsProviderWrapper';
|
3
4
|
import { getServerFeatureFlagsValue } from '@/config/featureFlags';
|
4
5
|
import { appEnv } from '@/envs/app';
|
5
6
|
import DevPanel from '@/features/DevPanel';
|
@@ -54,7 +55,9 @@ const GlobalLayout = async ({
|
|
54
55
|
isMobile={isMobile}
|
55
56
|
serverConfig={serverConfig}
|
56
57
|
>
|
57
|
-
<QueryProvider>
|
58
|
+
<QueryProvider>
|
59
|
+
<LobeAnalyticsProviderWrapper>{children}</LobeAnalyticsProviderWrapper>
|
60
|
+
</QueryProvider>
|
58
61
|
<StoreInitialization />
|
59
62
|
<Suspense>
|
60
63
|
<ImportSettings />
|
@@ -0,0 +1,25 @@
|
|
1
|
+
import { createServerAnalytics } from '@lobehub/analytics/server';
|
2
|
+
|
3
|
+
import { analyticsEnv } from '@/config/analytics';
|
4
|
+
import { BUSINESS_LINE } from '@/const/analytics';
|
5
|
+
import { isDev } from '@/utils/env';
|
6
|
+
|
7
|
+
export const serverAnalytics = createServerAnalytics({
|
8
|
+
business: BUSINESS_LINE,
|
9
|
+
debug: isDev,
|
10
|
+
providers: {
|
11
|
+
posthogNode: {
|
12
|
+
debug: analyticsEnv.DEBUG_POSTHOG_ANALYTICS,
|
13
|
+
enabled: analyticsEnv.ENABLED_POSTHOG_ANALYTICS,
|
14
|
+
host: analyticsEnv.POSTHOG_HOST,
|
15
|
+
key: analyticsEnv.POSTHOG_KEY ?? '',
|
16
|
+
},
|
17
|
+
},
|
18
|
+
});
|
19
|
+
|
20
|
+
export const initializeServerAnalytics = async () => {
|
21
|
+
await serverAnalytics.initialize();
|
22
|
+
return serverAnalytics;
|
23
|
+
};
|
24
|
+
|
25
|
+
export default serverAnalytics;
|
package/src/locales/create.ts
CHANGED
@@ -31,8 +31,10 @@ export const createI18nNext = (lang?: string) => {
|
|
31
31
|
}
|
32
32
|
});
|
33
33
|
return {
|
34
|
-
init: () =>
|
35
|
-
|
34
|
+
init: (params: { initAsync?: boolean } = {}) => {
|
35
|
+
const { initAsync = true } = params;
|
36
|
+
|
37
|
+
return instance.init({
|
36
38
|
debug: debugMode,
|
37
39
|
defaultNS: ['error', 'common', 'chat'],
|
38
40
|
// detection: {
|
@@ -50,11 +52,14 @@ export const createI18nNext = (lang?: string) => {
|
|
50
52
|
// lookupCookie: LOBE_LOCALE_COOKIE,
|
51
53
|
// },
|
52
54
|
fallbackLng: DEFAULT_LANG,
|
55
|
+
|
56
|
+
initAsync,
|
53
57
|
interpolation: {
|
54
58
|
escapeValue: false,
|
55
59
|
},
|
56
60
|
lng: lang,
|
57
|
-
})
|
61
|
+
});
|
62
|
+
},
|
58
63
|
instance,
|
59
64
|
};
|
60
65
|
};
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { createTRPCClient,
|
1
|
+
import { createTRPCClient, httpLink } from '@trpc/client';
|
2
2
|
import superjson from 'superjson';
|
3
3
|
import urlJoin from 'url-join';
|
4
4
|
|
@@ -26,7 +26,7 @@ export const createAsyncServerClient = async (userId: string, payload: JWTPayloa
|
|
26
26
|
|
27
27
|
return createTRPCClient<AsyncRouter>({
|
28
28
|
links: [
|
29
|
-
|
29
|
+
httpLink({
|
30
30
|
headers,
|
31
31
|
transformer: superjson,
|
32
32
|
url: urlJoin(appEnv.APP_URL!, '/trpc/async'),
|
@@ -8,6 +8,14 @@ import { AgentService } from '@/server/services/agent';
|
|
8
8
|
|
9
9
|
import { UserService } from './index';
|
10
10
|
|
11
|
+
// Mock @/libs/analytics to avoid server-side environment variable access in client test environment
|
12
|
+
vi.mock('@/libs/analytics', () => ({
|
13
|
+
initializeServerAnalytics: vi.fn().mockResolvedValue({
|
14
|
+
identify: vi.fn(),
|
15
|
+
track: vi.fn(),
|
16
|
+
}),
|
17
|
+
}));
|
18
|
+
|
11
19
|
// Mock dependencies
|
12
20
|
vi.mock('@/database/models/user', () => {
|
13
21
|
const MockUserModel = vi.fn();
|
@@ -2,6 +2,7 @@ import { UserJSON } from '@clerk/backend';
|
|
2
2
|
|
3
3
|
import { UserModel } from '@/database/models/user';
|
4
4
|
import { serverDB } from '@/database/server';
|
5
|
+
import { initializeServerAnalytics } from '@/libs/analytics';
|
5
6
|
import { pino } from '@/libs/logger';
|
6
7
|
import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
|
7
8
|
import { S3 } from '@/server/modules/S3';
|
@@ -51,6 +52,23 @@ export class UserService {
|
|
51
52
|
|
52
53
|
/* ↑ cloud slot ↑ */
|
53
54
|
|
55
|
+
//analytics
|
56
|
+
const analytics = await initializeServerAnalytics();
|
57
|
+
analytics?.identify(id, {
|
58
|
+
email: email?.email_address,
|
59
|
+
firstName: params.first_name,
|
60
|
+
lastName: params.last_name,
|
61
|
+
phone: phone?.phone_number,
|
62
|
+
username: params.username,
|
63
|
+
});
|
64
|
+
analytics?.track({
|
65
|
+
name: 'user_register_completed',
|
66
|
+
properties: {
|
67
|
+
spm: 'user_service.create_user.user_created',
|
68
|
+
},
|
69
|
+
userId: id,
|
70
|
+
});
|
71
|
+
|
54
72
|
return { message: 'user created', success: true };
|
55
73
|
};
|
56
74
|
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import { getSingletonAnalyticsOptional } from '@lobehub/analytics';
|
1
2
|
import isEqual from 'fast-deep-equal';
|
2
3
|
import { t } from 'i18next';
|
3
4
|
import useSWR, { SWRResponse, mutate } from 'swr';
|
@@ -10,8 +11,8 @@ import { DEFAULT_AGENT_LOBE_SESSION, INBOX_SESSION_ID } from '@/const/session';
|
|
10
11
|
import { useClientDataSWR } from '@/libs/swr';
|
11
12
|
import { sessionService } from '@/services/session';
|
12
13
|
import { SessionStore } from '@/store/session';
|
13
|
-
import { useUserStore } from '@/store/user';
|
14
|
-
import { settingsSelectors } from '@/store/user/selectors';
|
14
|
+
import { getUserStoreState, useUserStore } from '@/store/user';
|
15
|
+
import { settingsSelectors, userProfileSelectors } from '@/store/user/selectors';
|
15
16
|
import { MetaData } from '@/types/meta';
|
16
17
|
import {
|
17
18
|
ChatSessionList,
|
@@ -24,6 +25,7 @@ import {
|
|
24
25
|
import { merge } from '@/utils/merge';
|
25
26
|
import { setNamespace } from '@/utils/storeDebug';
|
26
27
|
|
28
|
+
import { sessionGroupSelectors } from '../sessionGroup/selectors';
|
27
29
|
import { SessionDispatch, sessionsReducer } from './reducers';
|
28
30
|
import { sessionSelectors } from './selectors';
|
29
31
|
import { sessionMetaSelectors } from './selectors/meta';
|
@@ -114,6 +116,30 @@ export const createSessionSlice: StateCreator<
|
|
114
116
|
const id = await sessionService.createSession(LobeSessionType.Agent, newSession);
|
115
117
|
await refreshSessions();
|
116
118
|
|
119
|
+
// Track new agent creation analytics
|
120
|
+
const analytics = getSingletonAnalyticsOptional();
|
121
|
+
if (analytics) {
|
122
|
+
const userStore = getUserStoreState();
|
123
|
+
const userId = userProfileSelectors.userId(userStore);
|
124
|
+
|
125
|
+
// Get group information
|
126
|
+
const groupId = newSession.group || 'default';
|
127
|
+
const group = sessionGroupSelectors.getGroupById(groupId)(get());
|
128
|
+
const groupName = group?.name || (groupId === 'default' ? 'Default' : 'Unknown');
|
129
|
+
|
130
|
+
analytics.track({
|
131
|
+
name: 'new_agent_created',
|
132
|
+
properties: {
|
133
|
+
assistant_name: newSession.meta?.title || 'Untitled Agent',
|
134
|
+
assistant_tags: newSession.meta?.tags || [],
|
135
|
+
group_id: groupId,
|
136
|
+
group_name: groupName,
|
137
|
+
session_id: id,
|
138
|
+
user_id: userId || 'anonymous',
|
139
|
+
},
|
140
|
+
});
|
141
|
+
}
|
142
|
+
|
117
143
|
// Whether to goto to the new session after creation, the default is to switch to
|
118
144
|
if (isSwitchSession) switchSession(id);
|
119
145
|
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import { getSingletonAnalyticsOptional } from '@lobehub/analytics';
|
1
2
|
import useSWR, { SWRResponse, mutate } from 'swr';
|
2
3
|
import { DeepPartial } from 'utility-types';
|
3
4
|
import type { StateCreator } from 'zustand/vanilla';
|
@@ -22,7 +23,6 @@ const GET_USER_STATE_KEY = 'initUserState';
|
|
22
23
|
*/
|
23
24
|
export interface CommonAction {
|
24
25
|
refreshUserState: () => Promise<void>;
|
25
|
-
|
26
26
|
updateAvatar: (avatar: string) => Promise<void>;
|
27
27
|
useCheckTrace: (shouldFetch: boolean) => SWRResponse;
|
28
28
|
useInitUserState: (
|
@@ -118,7 +118,14 @@ export const createCommonSlice: StateCreator<
|
|
118
118
|
false,
|
119
119
|
n('initUserState'),
|
120
120
|
);
|
121
|
-
|
121
|
+
//analytics
|
122
|
+
const analytics = getSingletonAnalyticsOptional();
|
123
|
+
analytics?.identify(data.userId || '', {
|
124
|
+
email: data.email,
|
125
|
+
firstName: data.firstName,
|
126
|
+
lastName: data.lastName,
|
127
|
+
username: data.username,
|
128
|
+
});
|
122
129
|
get().refreshDefaultModelProviderList({ trigger: 'fetchUserState' });
|
123
130
|
}
|
124
131
|
},
|
package/src/utils/locale.ts
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
import { resolveAcceptLanguage } from 'resolve-accept-language';
|
2
2
|
|
3
3
|
import { DEFAULT_LANG } from '@/const/locale';
|
4
|
-
import { locales, normalizeLocale } from '@/locales/resources';
|
4
|
+
import { Locales, locales, normalizeLocale } from '@/locales/resources';
|
5
|
+
import { RouteVariants } from '@/utils/server/routeVariants';
|
5
6
|
|
6
7
|
export const getAntdLocale = async (lang?: string) => {
|
7
8
|
let normalLang: any = normalizeLocale(lang);
|
@@ -44,3 +45,17 @@ export const parseBrowserLanguage = (headers: Headers, defaultLang: string = DEF
|
|
44
45
|
|
45
46
|
return browserLang;
|
46
47
|
};
|
48
|
+
|
49
|
+
/**
|
50
|
+
* Parse the page locale from the URL and search
|
51
|
+
* used in cloud
|
52
|
+
*/
|
53
|
+
export const parsePageLocale = async (props: {
|
54
|
+
params: Promise<{ variants: string }>;
|
55
|
+
searchParams: Promise<any>;
|
56
|
+
}) => {
|
57
|
+
const searchParams = await props.searchParams;
|
58
|
+
|
59
|
+
const browserLocale = await RouteVariants.getLocale(props);
|
60
|
+
return normalizeLocale(searchParams?.hl || browserLocale) as Locales;
|
61
|
+
};
|
package/tests/setup.ts
CHANGED
@@ -5,6 +5,17 @@ import { theme } from 'antd';
|
|
5
5
|
// refs: https://github.com/dumbmatter/fakeIndexedDB#dexie-and-other-indexeddb-api-wrappers
|
6
6
|
import 'fake-indexeddb/auto';
|
7
7
|
import React from 'react';
|
8
|
+
import { vi } from 'vitest';
|
9
|
+
|
10
|
+
// Global mock for @lobehub/analytics/react to avoid AnalyticsProvider dependency
|
11
|
+
// This prevents tests from failing when components use useAnalytics hook
|
12
|
+
vi.mock('@lobehub/analytics/react', () => ({
|
13
|
+
useAnalytics: () => ({
|
14
|
+
analytics: {
|
15
|
+
track: vi.fn(),
|
16
|
+
},
|
17
|
+
}),
|
18
|
+
}));
|
8
19
|
|
9
20
|
// only inject in the dom environment
|
10
21
|
if (
|