@lobehub/chat 1.46.3 → 1.46.4
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 +25 -0
- package/changelog/v1.json +9 -0
- package/docs/usage/features/plugin-system.mdx +1 -1
- package/docs/usage/features/plugin-system.zh-CN.mdx +1 -1
- package/package.json +1 -1
- package/src/app/(auth)/layout.tsx +0 -5
- package/src/app/(auth)/login/[[...login]]/page.tsx +4 -0
- package/src/app/{(backend)/api/auth → (auth)/next-auth}/error/AuthErrorPage.tsx +2 -0
- package/src/app/(auth)/next-auth/error/page.tsx +11 -0
- package/src/app/(auth)/signup/[[...signup]]/page.tsx +4 -1
- package/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/index.tsx +2 -2
- package/src/app/(main)/chat/@session/features/SessionListContent/DefaultMode.tsx +1 -1
- package/src/app/(main)/discover/(list)/assistants/features/Card.tsx +1 -1
- package/src/app/(main)/files/[id]/page.tsx +1 -1
- package/src/app/(main)/profile/stats/features/ShareButton/Preview.tsx +2 -2
- package/src/app/(main)/profile/stats/features/Welcome.tsx +3 -2
- package/src/app/(main)/repos/[id]/evals/evaluation/CreateEvaluation/index.tsx +1 -1
- package/src/features/InitClientDB/PGliteIcon.tsx +2 -2
- package/src/layout/GlobalProvider/{Debug.tsx → ReactScan.tsx} +2 -2
- package/src/layout/GlobalProvider/index.tsx +10 -33
- package/src/libs/next-auth/auth.config.ts +3 -0
- package/src/utils/locale.test.ts +61 -0
- package/src/utils/locale.ts +30 -1
- package/src/app/(backend)/api/auth/error/page.tsx +0 -5
package/CHANGELOG.md
CHANGED
@@ -2,6 +2,31 @@
|
|
2
2
|
|
3
3
|
# Changelog
|
4
4
|
|
5
|
+
### [Version 1.46.4](https://github.com/lobehub/lobe-chat/compare/v1.46.3...v1.46.4)
|
6
|
+
|
7
|
+
<sup>Released on **2025-01-16**</sup>
|
8
|
+
|
9
|
+
#### ♻ Code Refactoring
|
10
|
+
|
11
|
+
- **misc**: Refactor some implement for the next performance improvement.
|
12
|
+
|
13
|
+
<br/>
|
14
|
+
|
15
|
+
<details>
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
17
|
+
|
18
|
+
#### Code refactoring
|
19
|
+
|
20
|
+
- **misc**: Refactor some implement for the next performance improvement, closes [#5462](https://github.com/lobehub/lobe-chat/issues/5462) ([b5e1146](https://github.com/lobehub/lobe-chat/commit/b5e1146))
|
21
|
+
|
22
|
+
</details>
|
23
|
+
|
24
|
+
<div align="right">
|
25
|
+
|
26
|
+
[](#readme-top)
|
27
|
+
|
28
|
+
</div>
|
29
|
+
|
5
30
|
### [Version 1.46.3](https://github.com/lobehub/lobe-chat/compare/v1.46.2...v1.46.3)
|
6
31
|
|
7
32
|
<sup>Released on **2025-01-15**</sup>
|
package/changelog/v1.json
CHANGED
@@ -32,7 +32,7 @@ By utilizing plugins, LobeChat assistants can obtain and process real-time infor
|
|
32
32
|
|
33
33
|
In addition, these plugins are not limited to news aggregation, but can also extend to other practical functions, such as quickly searching documents, generating images, obtaining data from various platforms like Bilibili, Steam, and interacting with various third-party services.
|
34
34
|
|
35
|
-
Learn more about [plugin usage](/docs/usage/plugins/basic) by checking it out.
|
35
|
+
Learn more about [plugin usage](/docs/usage/plugins/basic-usage) by checking it out.
|
36
36
|
|
37
37
|
<Callout type={'tip'}>
|
38
38
|
To help developers better participate in this ecosystem, we provide comprehensive development
|
@@ -27,7 +27,7 @@ LobeChat 的插件生态系统是其核心功能的重要扩展,它极大地
|
|
27
27
|
|
28
28
|
此外,这些插件不仅局限于新闻聚合,还可以扩展到其他实用的功能,如快速检索文档、生成图片、获取 Bilibili 、Steam 等各种平台数据,以及与其他各式各样的第三方服务交互。
|
29
29
|
|
30
|
-
通过查看 [插件使用](/zh/docs/usage/plugins/basic) 了解更多。
|
30
|
+
通过查看 [插件使用](/zh/docs/usage/plugins/basic-usage) 了解更多。
|
31
31
|
|
32
32
|
<Callout type={'tip'}>
|
33
33
|
为了帮助开发者更好地参与到这个生态中来,我们在提供了全面的开发资源。这包括详尽的组件开发文档、功能齐全的软件开发工具包(SDK),以及样板示例,这些都是为了简化开发过程,降低开发者的入门门槛。
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@lobehub/chat",
|
3
|
-
"version": "1.46.
|
3
|
+
"version": "1.46.4",
|
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",
|
@@ -1,12 +1,7 @@
|
|
1
|
-
import { notFound } from 'next/navigation';
|
2
1
|
import { PropsWithChildren } from 'react';
|
3
2
|
import { Center, Flexbox } from 'react-layout-kit';
|
4
3
|
|
5
|
-
import { enableClerk } from '@/const/auth';
|
6
|
-
|
7
4
|
const Page = ({ children }: PropsWithChildren) => {
|
8
|
-
if (!enableClerk) return notFound();
|
9
|
-
|
10
5
|
return (
|
11
6
|
<Flexbox height={'100%'} width={'100%'}>
|
12
7
|
<Center height={'100%'} width={'100%'}>
|
@@ -1,5 +1,7 @@
|
|
1
1
|
import { SignIn } from '@clerk/nextjs';
|
2
|
+
import { notFound } from 'next/navigation';
|
2
3
|
|
4
|
+
import { enableClerk } from '@/const/auth';
|
3
5
|
import { BRANDING_NAME } from '@/const/branding';
|
4
6
|
import { metadataModule } from '@/server/metadata';
|
5
7
|
import { translation } from '@/server/translation';
|
@@ -14,6 +16,8 @@ export const generateMetadata = async () => {
|
|
14
16
|
};
|
15
17
|
|
16
18
|
const Page = () => {
|
19
|
+
if (!enableClerk) return notFound();
|
20
|
+
|
17
21
|
return <SignIn path="/login" />;
|
18
22
|
};
|
19
23
|
|
@@ -1,7 +1,8 @@
|
|
1
1
|
import { SignUp } from '@clerk/nextjs';
|
2
|
-
import { redirect } from 'next/navigation';
|
2
|
+
import { notFound, redirect } from 'next/navigation';
|
3
3
|
|
4
4
|
import { serverFeatureFlags } from '@/config/featureFlags';
|
5
|
+
import { enableClerk } from '@/const/auth';
|
5
6
|
import { metadataModule } from '@/server/metadata';
|
6
7
|
import { translation } from '@/server/translation';
|
7
8
|
|
@@ -15,6 +16,8 @@ export const generateMetadata = async () => {
|
|
15
16
|
};
|
16
17
|
|
17
18
|
const Page = () => {
|
19
|
+
if (!enableClerk) return notFound();
|
20
|
+
|
18
21
|
const enableClerkSignUp = serverFeatureFlags().enableClerkSignUp;
|
19
22
|
|
20
23
|
if (!enableClerkSignUp) {
|
package/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/index.tsx
CHANGED
@@ -6,13 +6,13 @@ import { useTranslation } from 'react-i18next';
|
|
6
6
|
import { Flexbox } from 'react-layout-kit';
|
7
7
|
|
8
8
|
import StopLoadingIcon from '@/components/StopLoading';
|
9
|
+
import LocalFiles from '@/features/ChatInput/Desktop/FilePreview';
|
10
|
+
import SaveTopic from '@/features/ChatInput/Topic';
|
9
11
|
import { useSendMessage } from '@/features/ChatInput/useSend';
|
10
12
|
import { useChatStore } from '@/store/chat';
|
11
13
|
import { chatSelectors } from '@/store/chat/selectors';
|
12
14
|
import { isMacOS } from '@/utils/platform';
|
13
15
|
|
14
|
-
import LocalFiles from '../../../../../../../../../features/ChatInput/Desktop/FilePreview';
|
15
|
-
import SaveTopic from '../../../../../../../../../features/ChatInput/Topic';
|
16
16
|
import SendMore from './SendMore';
|
17
17
|
import ShortcutHint from './ShortcutHint';
|
18
18
|
|
@@ -10,8 +10,8 @@ import { useSessionStore } from '@/store/session';
|
|
10
10
|
import { sessionSelectors } from '@/store/session/selectors';
|
11
11
|
import { SessionDefaultGroup } from '@/types/session';
|
12
12
|
|
13
|
-
import Actions from '../SessionListContent/CollapseGroup/Actions';
|
14
13
|
import CollapseGroup from './CollapseGroup';
|
14
|
+
import Actions from './CollapseGroup/Actions';
|
15
15
|
import Inbox from './Inbox';
|
16
16
|
import SessionList from './List';
|
17
17
|
import ConfigGroupModal from './Modals/ConfigGroupModal';
|
@@ -12,7 +12,7 @@ import { DiscoverAssistantItem } from '@/types/discover';
|
|
12
12
|
|
13
13
|
import CardBanner from '../../../components/CardBanner';
|
14
14
|
import GitHubAvatar from '../../../components/GitHubAvatar';
|
15
|
-
import { useCategoryItem } from '
|
15
|
+
import { useCategoryItem } from './useCategory';
|
16
16
|
|
17
17
|
const Link = dynamic(() => import('next/link'), {
|
18
18
|
loading: () => <Skeleton.Button size={'small'} style={{ height: 22 }} />,
|
@@ -1,13 +1,13 @@
|
|
1
1
|
import { notFound } from 'next/navigation';
|
2
2
|
import { Flexbox } from 'react-layout-kit';
|
3
3
|
|
4
|
-
import FileDetail from '@/app/(main)/files/features/FileDetail';
|
5
4
|
import FileViewer from '@/features/FileViewer';
|
6
5
|
import { createCallerFactory } from '@/libs/trpc';
|
7
6
|
import { lambdaRouter } from '@/server/routers/lambda';
|
8
7
|
import { PagePropsWithId } from '@/types/next';
|
9
8
|
import { getUserAuth } from '@/utils/server/auth';
|
10
9
|
|
10
|
+
import FileDetail from '../features/FileDetail';
|
11
11
|
import Header from './Header';
|
12
12
|
|
13
13
|
const createCaller = createCallerFactory(lambdaRouter);
|
@@ -10,9 +10,9 @@ import { OFFICIAL_URL, imageUrl } from '@/const/url';
|
|
10
10
|
import { isServerMode } from '@/const/version';
|
11
11
|
import UserAvatar from '@/features/User/UserAvatar';
|
12
12
|
|
13
|
-
import TotalMessages from '..//TotalMessages';
|
14
|
-
import TotalWords from '..//TotalWords';
|
15
13
|
import AiHeatmaps from '../AiHeatmaps';
|
14
|
+
import TotalMessages from '../TotalMessages';
|
15
|
+
import TotalWords from '../TotalWords';
|
16
16
|
|
17
17
|
const useStyles = createStyles(({ css, token, stylish, cx, responsive }) => ({
|
18
18
|
avatar: css`
|
@@ -6,14 +6,15 @@ import { memo } from 'react';
|
|
6
6
|
import { Trans, useTranslation } from 'react-i18next';
|
7
7
|
import { Flexbox } from 'react-layout-kit';
|
8
8
|
|
9
|
-
import TimeLabel from '@/app/(main)/profile/stats/features/TimeLabel';
|
10
9
|
import { BRANDING_NAME } from '@/const/branding';
|
11
10
|
import { useClientDataSWR } from '@/libs/swr';
|
12
11
|
import { userService } from '@/services/user';
|
13
12
|
import { useUserStore } from '@/store/user';
|
14
|
-
import { userProfileSelectors } from '@/store/user/
|
13
|
+
import { userProfileSelectors } from '@/store/user/selectors';
|
15
14
|
import { formatIntergerNumber } from '@/utils/format';
|
16
15
|
|
16
|
+
import TimeLabel from './TimeLabel';
|
17
|
+
|
17
18
|
const formatEnglishNumber = (number: number) => {
|
18
19
|
if (number === 1) return '1st';
|
19
20
|
if (number === 2) return '2nd';
|
@@ -4,7 +4,7 @@ import { Button } from 'antd';
|
|
4
4
|
import { memo } from 'react';
|
5
5
|
import { useTranslation } from 'react-i18next';
|
6
6
|
|
7
|
-
import { useCreateDatasetModal } from '
|
7
|
+
import { useCreateDatasetModal } from './useModal';
|
8
8
|
|
9
9
|
interface CreateEvaluationProps {
|
10
10
|
knowledgeBaseId: string;
|
@@ -3,7 +3,7 @@
|
|
3
3
|
import type { IconType } from '@lobehub/icons';
|
4
4
|
import { forwardRef } from 'react';
|
5
5
|
|
6
|
-
const PGliteIcon: IconType = forwardRef(({ size = '1em', style
|
6
|
+
const PGliteIcon: IconType = forwardRef(({ size = '1em', style }, ref) => {
|
7
7
|
return (
|
8
8
|
<svg
|
9
9
|
fill="currentColor"
|
@@ -14,12 +14,12 @@ const PGliteIcon: IconType = forwardRef(({ size = '1em', style, ...rest }, ref)
|
|
14
14
|
viewBox="0 0 1024 1024"
|
15
15
|
width={size}
|
16
16
|
xmlns="http://www.w3.org/2000/svg"
|
17
|
-
{...rest}
|
18
17
|
>
|
19
18
|
<title>PGlite</title>
|
20
19
|
<path
|
21
20
|
clip-rule="evenodd"
|
22
21
|
d="M941.581 335.737v460.806c0 15.926-12.913 28.836-28.832 28.818l-115.283-.137c-15.243-.018-27.706-11.88-28.703-26.877.011-.569.018-1.138.018-1.711l-.004-172.904c0-47.745-38.736-86.451-86.454-86.451-46.245 0-84.052-36.359-86.342-82.068V191.496l201.708.149c79.484.058 143.892 64.553 143.892 144.092zm-576-144.281v201.818c0 47.746 38.682 86.456 86.4 86.456h86.4v-5.796c0 66.816 54.13 120.98 120.902 120.98 28.617 0 51.815 23.213 51.815 51.848v149.644c0 .688.011 1.372.025 2.057-.943 15.065-13.453 26.992-28.746 26.992l-144.982-.007.986-201.586c.079-15.915-12.755-28.88-28.66-28.959-15.904-.079-28.861 12.763-28.94 28.678l-.986 201.741v.118l-172.174-.01V623.722c0-15.915-12.895-28.819-28.8-28.819-15.906 0-28.8 12.904-28.8 28.819v201.704l-143.642-.007c-15.905-.004-28.798-12.904-28.798-28.819V335.547c0-79.58 64.471-144.093 144.001-144.092l143.999.001zm446.544 173.693c0-23.874-19.343-43.228-43.2-43.228-23.861 0-43.2 19.354-43.2 43.228 0 23.875 19.339 43.226 43.2 43.226 23.857 0 43.2-19.351 43.2-43.226z"
|
22
|
+
fill-rule="evenodd"
|
23
23
|
/>
|
24
24
|
</svg>
|
25
25
|
);
|
@@ -4,7 +4,7 @@ import { useSearchParams } from 'next/navigation';
|
|
4
4
|
import Script from 'next/script';
|
5
5
|
import React, { memo } from 'react';
|
6
6
|
|
7
|
-
const
|
7
|
+
const ReactScan = memo(() => {
|
8
8
|
const searchParams = useSearchParams();
|
9
9
|
|
10
10
|
const debug = searchParams.get('debug');
|
@@ -12,4 +12,4 @@ const Debug = memo(() => {
|
|
12
12
|
return !!debug && <Script src="https://unpkg.com/react-scan/dist/auto.global.js" />;
|
13
13
|
});
|
14
14
|
|
15
|
-
export default
|
15
|
+
export default ReactScan;
|
@@ -1,54 +1,28 @@
|
|
1
1
|
import { cookies, headers } from 'next/headers';
|
2
|
-
import { PropsWithChildren } from 'react';
|
3
|
-
import { resolveAcceptLanguage } from 'resolve-accept-language';
|
2
|
+
import { PropsWithChildren, Suspense } from 'react';
|
4
3
|
|
5
4
|
import { appEnv } from '@/config/app';
|
6
5
|
import { getServerFeatureFlagsValue } from '@/config/featureFlags';
|
7
|
-
import {
|
6
|
+
import { LOBE_LOCALE_COOKIE } from '@/const/locale';
|
8
7
|
import {
|
9
8
|
LOBE_THEME_APPEARANCE,
|
10
9
|
LOBE_THEME_NEUTRAL_COLOR,
|
11
10
|
LOBE_THEME_PRIMARY_COLOR,
|
12
11
|
} from '@/const/theme';
|
13
12
|
import DebugUI from '@/features/DebugUI';
|
14
|
-
import { locales } from '@/locales/resources';
|
15
13
|
import { getServerGlobalConfig } from '@/server/globalConfig';
|
16
14
|
import { ServerConfigStoreProvider } from '@/store/serverConfig';
|
17
|
-
import { getAntdLocale } from '@/utils/locale';
|
15
|
+
import { getAntdLocale, parseBrowserLanguage } from '@/utils/locale';
|
18
16
|
import { isMobileDevice } from '@/utils/server/responsive';
|
19
17
|
|
20
18
|
import AntdV5MonkeyPatch from './AntdV5MonkeyPatch';
|
21
19
|
import AppTheme from './AppTheme';
|
22
|
-
import Debug from './Debug';
|
23
20
|
import Locale from './Locale';
|
24
21
|
import QueryProvider from './Query';
|
22
|
+
import ReactScan from './ReactScan';
|
25
23
|
import StoreInitialization from './StoreInitialization';
|
26
24
|
import StyleRegistry from './StyleRegistry';
|
27
25
|
|
28
|
-
const parserFallbackLang = async () => {
|
29
|
-
// if the default language is not 'en-US', just return the default language as fallback lang
|
30
|
-
if (DEFAULT_LANG !== 'en-US') return DEFAULT_LANG;
|
31
|
-
|
32
|
-
const header = await headers();
|
33
|
-
/**
|
34
|
-
* The arguments are as follows:
|
35
|
-
*
|
36
|
-
* 1) The HTTP accept-language header.
|
37
|
-
* 2) The available locales (they must contain the default locale).
|
38
|
-
* 3) The default locale.
|
39
|
-
*/
|
40
|
-
let fallbackLang: string = resolveAcceptLanguage(
|
41
|
-
header.get('accept-language') || '',
|
42
|
-
// Invalid locale identifier 'ar'. A valid locale should follow the BCP 47 'language-country' format.
|
43
|
-
locales.map((locale) => (locale === 'ar' ? 'ar-EG' : locale)),
|
44
|
-
DEFAULT_LANG,
|
45
|
-
);
|
46
|
-
// if match the ar-EG then fallback to ar
|
47
|
-
if (fallbackLang === 'ar-EG') fallbackLang = 'ar';
|
48
|
-
|
49
|
-
return fallbackLang;
|
50
|
-
};
|
51
|
-
|
52
26
|
const GlobalLayout = async ({ children }: PropsWithChildren) => {
|
53
27
|
// get default theme config to use with ssr
|
54
28
|
const cookieStore = await cookies();
|
@@ -58,7 +32,8 @@ const GlobalLayout = async ({ children }: PropsWithChildren) => {
|
|
58
32
|
|
59
33
|
// get default locale config to use with ssr
|
60
34
|
const defaultLang = cookieStore.get(LOBE_LOCALE_COOKIE);
|
61
|
-
const
|
35
|
+
const header = await headers();
|
36
|
+
const fallbackLang = parseBrowserLanguage(header);
|
62
37
|
|
63
38
|
// if it's a new user, there's no cookie
|
64
39
|
// So we need to use the fallback language parsed by accept-language
|
@@ -87,10 +62,12 @@ const GlobalLayout = async ({ children }: PropsWithChildren) => {
|
|
87
62
|
serverConfig={serverConfig}
|
88
63
|
>
|
89
64
|
<QueryProvider>{children}</QueryProvider>
|
90
|
-
<
|
65
|
+
<Suspense>
|
66
|
+
<StoreInitialization />
|
67
|
+
<ReactScan />
|
68
|
+
</Suspense>
|
91
69
|
</ServerConfigStoreProvider>
|
92
70
|
<DebugUI />
|
93
|
-
<Debug />
|
94
71
|
</AppTheme>
|
95
72
|
<AntdV5MonkeyPatch />
|
96
73
|
</Locale>
|
@@ -40,6 +40,9 @@ export default {
|
|
40
40
|
},
|
41
41
|
},
|
42
42
|
debug: authEnv.NEXT_AUTH_DEBUG,
|
43
|
+
pages: {
|
44
|
+
error: '/next-auth/error',
|
45
|
+
},
|
43
46
|
providers: initSSOProviders(),
|
44
47
|
secret: authEnv.NEXT_AUTH_SECRET,
|
45
48
|
trustHost: process.env?.AUTH_TRUST_HOST ? process.env.AUTH_TRUST_HOST === 'true' : true,
|
@@ -0,0 +1,61 @@
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
2
|
+
|
3
|
+
import { parseBrowserLanguage } from './locale';
|
4
|
+
|
5
|
+
describe('parseBrowserLanguage', () => {
|
6
|
+
// Helper function to create Headers with accept-language
|
7
|
+
const createHeaders = (acceptLanguage?: string) => {
|
8
|
+
const headers = new Headers();
|
9
|
+
if (acceptLanguage) {
|
10
|
+
headers.set('accept-language', acceptLanguage);
|
11
|
+
}
|
12
|
+
return headers;
|
13
|
+
};
|
14
|
+
|
15
|
+
describe('when DEFAULT_LANG is en-US', () => {
|
16
|
+
it('should return en-US for empty accept-language header', () => {
|
17
|
+
const headers = createHeaders();
|
18
|
+
expect(parseBrowserLanguage(headers)).toBe('en-US');
|
19
|
+
});
|
20
|
+
|
21
|
+
it('should return en-US for English language preference', () => {
|
22
|
+
const headers = createHeaders('en-US,en;q=0.9');
|
23
|
+
expect(parseBrowserLanguage(headers)).toBe('en-US');
|
24
|
+
});
|
25
|
+
|
26
|
+
it('should handle Arabic language special case', () => {
|
27
|
+
const headers = createHeaders('ar-SA,ar;q=0.9');
|
28
|
+
expect(parseBrowserLanguage(headers)).toBe('ar');
|
29
|
+
});
|
30
|
+
|
31
|
+
it('should convert ar-EG to ar', () => {
|
32
|
+
const headers = createHeaders('ar-EG,ar;q=0.9');
|
33
|
+
expect(parseBrowserLanguage(headers)).toBe('ar');
|
34
|
+
});
|
35
|
+
|
36
|
+
it('should handle multiple language preferences', () => {
|
37
|
+
const headers = createHeaders('zh-CN,zh;q=0.9,en;q=0.8');
|
38
|
+
// This expectation might need to be adjusted based on your locales configuration
|
39
|
+
expect(parseBrowserLanguage(headers)).toBe('zh-CN');
|
40
|
+
});
|
41
|
+
});
|
42
|
+
|
43
|
+
describe('when DEFAULT_LANG is not en-US', () => {
|
44
|
+
it('should return the non-en-US DEFAULT_LANG regardless of accept-language', () => {
|
45
|
+
const headers = createHeaders('en-US,en;q=0.9');
|
46
|
+
expect(parseBrowserLanguage(headers, 'zh-CN')).toBe('zh-CN');
|
47
|
+
});
|
48
|
+
});
|
49
|
+
|
50
|
+
describe('error handling', () => {
|
51
|
+
it('should handle invalid accept-language header format', () => {
|
52
|
+
const headers = createHeaders('invalid-format');
|
53
|
+
expect(parseBrowserLanguage(headers)).toBe('en-US');
|
54
|
+
});
|
55
|
+
|
56
|
+
it('should handle empty Headers object', () => {
|
57
|
+
const headers = new Headers();
|
58
|
+
expect(parseBrowserLanguage(headers)).toBe('en-US');
|
59
|
+
});
|
60
|
+
});
|
61
|
+
});
|
package/src/utils/locale.ts
CHANGED
@@ -1,4 +1,7 @@
|
|
1
|
-
import {
|
1
|
+
import { resolveAcceptLanguage } from 'resolve-accept-language';
|
2
|
+
|
3
|
+
import { DEFAULT_LANG } from '@/const/locale';
|
4
|
+
import { locales, normalizeLocale } from '@/locales/resources';
|
2
5
|
|
3
6
|
export const getAntdLocale = async (lang?: string) => {
|
4
7
|
let normalLang = normalizeLocale(lang);
|
@@ -14,3 +17,29 @@ export const getAntdLocale = async (lang?: string) => {
|
|
14
17
|
|
15
18
|
return locale;
|
16
19
|
};
|
20
|
+
|
21
|
+
/**
|
22
|
+
* Parse the browser language and return the fallback language
|
23
|
+
*/
|
24
|
+
export const parseBrowserLanguage = (headers: Headers, defaultLang: string = DEFAULT_LANG) => {
|
25
|
+
// if the default language is not 'en-US', just return the default language as fallback lang
|
26
|
+
if (defaultLang !== 'en-US') return defaultLang;
|
27
|
+
|
28
|
+
/**
|
29
|
+
* The arguments are as follows:
|
30
|
+
*
|
31
|
+
* 1) The HTTP accept-language header.
|
32
|
+
* 2) The available locales (they must contain the default locale).
|
33
|
+
* 3) The default locale.
|
34
|
+
*/
|
35
|
+
let fallbackLang: string = resolveAcceptLanguage(
|
36
|
+
headers.get('accept-language') || '',
|
37
|
+
// Invalid locale identifier 'ar'. A valid locale should follow the BCP 47 'language-country' format.
|
38
|
+
locales.map((locale) => (locale === 'ar' ? 'ar-EG' : locale)),
|
39
|
+
defaultLang,
|
40
|
+
);
|
41
|
+
// if match the ar-EG then fallback to ar
|
42
|
+
if (fallbackLang === 'ar-EG') fallbackLang = 'ar';
|
43
|
+
|
44
|
+
return fallbackLang;
|
45
|
+
};
|