@alicloud/appflow-chat 0.0.4 → 0.0.5
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/dist/appflow-chat.cjs.js +156 -161
- package/dist/appflow-chat.esm.js +12038 -11628
- package/dist/types/index.d.ts +169 -0
- package/package.json +1 -1
- package/src/components/HumanVerify/CustomParamsRenderer/ArrayField.tsx +6 -4
- package/src/components/HumanVerify/CustomParamsRenderer/EnumField.tsx +5 -3
- package/src/components/HumanVerify/CustomParamsRenderer/FieldRenderer.tsx +6 -4
- package/src/components/HumanVerify/CustomParamsRenderer/FileField.tsx +38 -30
- package/src/components/HumanVerify/CustomParamsRenderer/ObjectField.tsx +4 -2
- package/src/components/HumanVerify/CustomParamsRenderer/TimeField.tsx +3 -1
- package/src/components/HumanVerify/HistoryCard.tsx +6 -4
- package/src/components/HumanVerify/HumanVerify.tsx +5 -3
- package/src/components/MessageBubble.tsx +4 -2
- package/src/components/RichMessageBubble.tsx +4 -2
- package/src/core/RichBubbleContent.tsx +4 -2
- package/src/core/SourceContent.tsx +5 -3
- package/src/core/WebSearchContent.tsx +3 -1
- package/src/i18n/LocaleContext.tsx +70 -0
- package/src/i18n/index.ts +16 -0
- package/src/i18n/translate.ts +42 -0
- package/src/i18n/useTranslation.ts +39 -0
- package/src/i18n/utils.ts +67 -0
- package/src/index.ts +25 -0
- package/src/locales/en-US.ts +77 -0
- package/src/locales/index.ts +7 -0
- package/src/locales/types.ts +35 -0
- package/src/locales/zh-CN.ts +78 -0
- package/src/markdown/components/Chart.tsx +3 -1
- package/src/markdown/components/Mermaid.tsx +8 -6
- package/src/markdown/index.tsx +11 -8
|
@@ -3,6 +3,7 @@ import { Button } from 'antd';
|
|
|
3
3
|
import styled from 'styled-components';
|
|
4
4
|
import CustomParamsRenderer from './CustomParamsRenderer';
|
|
5
5
|
import { CustomParamSchema } from './CustomParamsRenderer/types';
|
|
6
|
+
import { useTranslation } from '../../i18n';
|
|
6
7
|
|
|
7
8
|
// ==================== Styled Components ====================
|
|
8
9
|
|
|
@@ -114,6 +115,7 @@ export const convertSchemaToUpperCase = (schema: any): CustomParamSchema | undef
|
|
|
114
115
|
* 用于展示历史对话中的 card 类型消息(只读模式)
|
|
115
116
|
*/
|
|
116
117
|
export const HistoryCard: React.FC<HistoryCardProps> = ({ data }) => {
|
|
118
|
+
const { t } = useTranslation();
|
|
117
119
|
// 从data中提取需要的参数
|
|
118
120
|
const approvalStatus = data?.approvalStatus;
|
|
119
121
|
|
|
@@ -131,7 +133,7 @@ export const HistoryCard: React.FC<HistoryCardProps> = ({ data }) => {
|
|
|
131
133
|
<StatusContainer $approved={isApproved}>
|
|
132
134
|
<StatusContent>
|
|
133
135
|
<StatusText>
|
|
134
|
-
{isApproved ? '
|
|
136
|
+
{isApproved ? t('humanVerify.submitted') : t('humanVerify.pending')}
|
|
135
137
|
</StatusText>
|
|
136
138
|
</StatusContent>
|
|
137
139
|
<Button
|
|
@@ -139,7 +141,7 @@ export const HistoryCard: React.FC<HistoryCardProps> = ({ data }) => {
|
|
|
139
141
|
variant="filled"
|
|
140
142
|
disabled={true}
|
|
141
143
|
>
|
|
142
|
-
{isApproved ? '
|
|
144
|
+
{isApproved ? t('humanVerify.submitted') : t('common.submit')}
|
|
143
145
|
</Button>
|
|
144
146
|
</StatusContainer>
|
|
145
147
|
</HistoryCardContainer>
|
|
@@ -156,7 +158,7 @@ export const HistoryCard: React.FC<HistoryCardProps> = ({ data }) => {
|
|
|
156
158
|
<StatusContainer $approved={isApproved}>
|
|
157
159
|
<StatusContent>
|
|
158
160
|
<StatusText>
|
|
159
|
-
{isApproved ? '
|
|
161
|
+
{isApproved ? t('humanVerify.submitted') : t('humanVerify.pending')}
|
|
160
162
|
</StatusText>
|
|
161
163
|
</StatusContent>
|
|
162
164
|
<Button
|
|
@@ -164,7 +166,7 @@ export const HistoryCard: React.FC<HistoryCardProps> = ({ data }) => {
|
|
|
164
166
|
variant="filled"
|
|
165
167
|
disabled={true}
|
|
166
168
|
>
|
|
167
|
-
{isApproved ? '
|
|
169
|
+
{isApproved ? t('humanVerify.submitted') : t('common.submit')}
|
|
168
170
|
</Button>
|
|
169
171
|
</StatusContainer>
|
|
170
172
|
</HistoryCardContainer>
|
|
@@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useState } from 'react';
|
|
|
2
2
|
import { Button, message } from 'antd';
|
|
3
3
|
import styled from 'styled-components';
|
|
4
4
|
import CustomParamsRenderer, { validateCustomParams, UploadSender, FileUploader } from './CustomParamsRenderer';
|
|
5
|
+
import { useTranslation } from '../../i18n';
|
|
5
6
|
|
|
6
7
|
// ==================== Styled Components ====================
|
|
7
8
|
|
|
@@ -61,6 +62,7 @@ export const HumanVerify: React.FC<HumanVerifyProps> = ({
|
|
|
61
62
|
fileUploader,
|
|
62
63
|
onSubmit
|
|
63
64
|
}) => {
|
|
65
|
+
const { t } = useTranslation();
|
|
64
66
|
// 从 data 中提取需要的参数
|
|
65
67
|
const verifyId = data?.verifyId;
|
|
66
68
|
const sessionWebhook = data?.sessionWebhook;
|
|
@@ -110,7 +112,7 @@ export const HumanVerify: React.FC<HumanVerifyProps> = ({
|
|
|
110
112
|
setValidationErrors(errors);
|
|
111
113
|
|
|
112
114
|
// 提示用户
|
|
113
|
-
message.error('
|
|
115
|
+
message.error(t('humanVerify.requiredAll'));
|
|
114
116
|
return;
|
|
115
117
|
}
|
|
116
118
|
|
|
@@ -143,7 +145,7 @@ export const HumanVerify: React.FC<HumanVerifyProps> = ({
|
|
|
143
145
|
<StatusContainer $approved={approved}>
|
|
144
146
|
<StatusContent>
|
|
145
147
|
<StatusText>
|
|
146
|
-
{approved ? '
|
|
148
|
+
{approved ? t('humanVerify.submitted') : t('humanVerify.pending')}
|
|
147
149
|
</StatusText>
|
|
148
150
|
</StatusContent>
|
|
149
151
|
<Button
|
|
@@ -152,7 +154,7 @@ export const HumanVerify: React.FC<HumanVerifyProps> = ({
|
|
|
152
154
|
onClick={handleApprove}
|
|
153
155
|
disabled={approved}
|
|
154
156
|
>
|
|
155
|
-
{'
|
|
157
|
+
{t('common.submit')}
|
|
156
158
|
</Button>
|
|
157
159
|
</StatusContainer>
|
|
158
160
|
</HumanVerifyContainer>
|
|
@@ -12,6 +12,7 @@ import { DocReferences, DocReferenceItem } from './DocReferences';
|
|
|
12
12
|
import { WebSearchPanel } from './WebSearchPanel';
|
|
13
13
|
import { HumanVerify, HistoryCard, CustomParamSchema } from './HumanVerify';
|
|
14
14
|
import { BubbleContent } from '@/core';
|
|
15
|
+
import { useTranslation } from '@/i18n';
|
|
15
16
|
|
|
16
17
|
/** HumanVerify 提交数据类型 */
|
|
17
18
|
export interface HumanVerifySubmitData {
|
|
@@ -271,6 +272,7 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({
|
|
|
271
272
|
historyCardData,
|
|
272
273
|
onHumanVerifySubmit,
|
|
273
274
|
}) => {
|
|
275
|
+
const { t } = useTranslation();
|
|
274
276
|
const [modal, contextHolder] = Modal.useModal();
|
|
275
277
|
|
|
276
278
|
// 网页搜索抽屉状态
|
|
@@ -293,7 +295,7 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({
|
|
|
293
295
|
modal.confirm({
|
|
294
296
|
bodyStyle: { maxHeight: '80vh', overflow: 'auto' },
|
|
295
297
|
icon: null,
|
|
296
|
-
title: '
|
|
298
|
+
title: t('source.title'),
|
|
297
299
|
destroyOnClose: true,
|
|
298
300
|
maskClosable: true,
|
|
299
301
|
closable: true,
|
|
@@ -317,7 +319,7 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({
|
|
|
317
319
|
),
|
|
318
320
|
footer: null,
|
|
319
321
|
});
|
|
320
|
-
}, [modal]);
|
|
322
|
+
}, [modal, t]);
|
|
321
323
|
|
|
322
324
|
// 默认的网页搜索点击处理
|
|
323
325
|
const defaultWebSearchClick = useCallback((items: DocReferenceItem[]) => {
|
|
@@ -12,6 +12,7 @@ import { loadEchartsScript } from '../utils/loadEcharts';
|
|
|
12
12
|
import { DocReferences, DocReferenceItem } from './DocReferences';
|
|
13
13
|
import { WebSearchPanel } from './WebSearchPanel';
|
|
14
14
|
import { RichBubbleContent } from '../core';
|
|
15
|
+
import { useTranslation } from '../i18n';
|
|
15
16
|
|
|
16
17
|
export interface RichMessageBubbleProps {
|
|
17
18
|
/** 消息内容 */
|
|
@@ -177,6 +178,7 @@ export const RichMessageBubble: React.FC<RichMessageBubbleProps> = ({
|
|
|
177
178
|
onReferenceClick,
|
|
178
179
|
onWebSearchClick,
|
|
179
180
|
}) => {
|
|
181
|
+
const { t } = useTranslation();
|
|
180
182
|
const [modal, contextHolder] = Modal.useModal();
|
|
181
183
|
|
|
182
184
|
// 网页搜索抽屉状态
|
|
@@ -199,7 +201,7 @@ export const RichMessageBubble: React.FC<RichMessageBubbleProps> = ({
|
|
|
199
201
|
modal.confirm({
|
|
200
202
|
bodyStyle: { maxHeight: '80vh', overflow: 'auto' },
|
|
201
203
|
icon: null,
|
|
202
|
-
title: '
|
|
204
|
+
title: t('source.title'),
|
|
203
205
|
destroyOnClose: true,
|
|
204
206
|
maskClosable: true,
|
|
205
207
|
closable: true,
|
|
@@ -223,7 +225,7 @@ export const RichMessageBubble: React.FC<RichMessageBubbleProps> = ({
|
|
|
223
225
|
),
|
|
224
226
|
footer: null,
|
|
225
227
|
});
|
|
226
|
-
}, [modal]);
|
|
228
|
+
}, [modal, t]);
|
|
227
229
|
|
|
228
230
|
// 默认的网页搜索点击处理
|
|
229
231
|
const defaultWebSearchClick = useCallback((items: DocReferenceItem[]) => {
|
|
@@ -19,6 +19,7 @@ import { Collapse } from 'antd';
|
|
|
19
19
|
import { MarkdownView } from '../markdown';
|
|
20
20
|
import { convertTableDataToMarkdown, processStepContent } from '../markdown/utils/dataProcessor';
|
|
21
21
|
import { RichBubbleProvider } from '../context/RichBubble';
|
|
22
|
+
import { useTranslation } from '../i18n';
|
|
22
23
|
|
|
23
24
|
export interface RichBubbleContentProps {
|
|
24
25
|
/** 消息内容 */
|
|
@@ -110,6 +111,7 @@ export const RichBubbleContent: React.FC<RichBubbleContentProps> = ({
|
|
|
110
111
|
children,
|
|
111
112
|
waitingMessage,
|
|
112
113
|
}) => {
|
|
114
|
+
const { t } = useTranslation();
|
|
113
115
|
// step消息折叠面板激活的索引
|
|
114
116
|
const [activeKey, setActiveKey] = useState<string | string[]>([]);
|
|
115
117
|
|
|
@@ -150,7 +152,7 @@ export const RichBubbleContent: React.FC<RichBubbleContentProps> = ({
|
|
|
150
152
|
} else {
|
|
151
153
|
const contentToRender = detail || '';
|
|
152
154
|
if (!contentToRender) {
|
|
153
|
-
return <StyledStepContent
|
|
155
|
+
return <StyledStepContent>{t('rich.emptyContent')}</StyledStepContent>;
|
|
154
156
|
}
|
|
155
157
|
|
|
156
158
|
return (
|
|
@@ -250,7 +252,7 @@ export const RichBubbleContent: React.FC<RichBubbleContentProps> = ({
|
|
|
250
252
|
<StyledStatusIcon $success={status === 'Success'}>
|
|
251
253
|
{status === 'Success' ? '✓' : '⟳'}
|
|
252
254
|
</StyledStatusIcon>
|
|
253
|
-
<StyledStepNumber
|
|
255
|
+
<StyledStepNumber>{t('rich.stepLabel', { index: index + 1 })}</StyledStepNumber>
|
|
254
256
|
<StyledStepText>{itemContent?.title}</StyledStepText>
|
|
255
257
|
</StyledStepTitleWrapper>
|
|
256
258
|
),
|
|
@@ -11,6 +11,7 @@ import styled from 'styled-components';
|
|
|
11
11
|
import { Image } from 'antd';
|
|
12
12
|
import { SearchOutlined } from '@ant-design/icons';
|
|
13
13
|
import { flatten, uniq } from 'lodash-es';
|
|
14
|
+
import { useTranslation } from '../i18n';
|
|
14
15
|
|
|
15
16
|
// 参考资料项类型
|
|
16
17
|
export interface SourceItem {
|
|
@@ -195,6 +196,7 @@ export const SourceContent: React.FC<SourceContentProps> = ({
|
|
|
195
196
|
className,
|
|
196
197
|
style,
|
|
197
198
|
}) => {
|
|
199
|
+
const { t } = useTranslation();
|
|
198
200
|
// 提取唯一图片
|
|
199
201
|
const uniqueImages = useMemo(() => {
|
|
200
202
|
let images;
|
|
@@ -244,14 +246,14 @@ export const SourceContent: React.FC<SourceContentProps> = ({
|
|
|
244
246
|
{webSearchArray.length > 0 && (
|
|
245
247
|
<StyledWebSearchSource onClick={handleWebSearchClick}>
|
|
246
248
|
<SearchOutlined />
|
|
247
|
-
<div
|
|
249
|
+
<div>{t('webSearch.foundPages', { count: webSearchArray.length })}</div>
|
|
248
250
|
</StyledWebSearchSource>
|
|
249
251
|
)}
|
|
250
252
|
|
|
251
253
|
{/* RAG参考资料 */}
|
|
252
254
|
{ragArray.length > 0 && (
|
|
253
255
|
<StyledAnswerSource>
|
|
254
|
-
<StyledLabel
|
|
256
|
+
<StyledLabel>{t('source.answerFrom')}</StyledLabel>
|
|
255
257
|
<div style={{ width: 'calc(100% - 70px)' }}>
|
|
256
258
|
{ragArray.map((item, index) => (
|
|
257
259
|
<SourceItemComponent
|
|
@@ -268,7 +270,7 @@ export const SourceContent: React.FC<SourceContentProps> = ({
|
|
|
268
270
|
{/* 图片来源 */}
|
|
269
271
|
{uniqueImages.length > 0 && (
|
|
270
272
|
<StyledAnswerSource>
|
|
271
|
-
<StyledLabel
|
|
273
|
+
<StyledLabel>{t('source.imageFrom')}</StyledLabel>
|
|
272
274
|
<StyledWrapSpace>
|
|
273
275
|
{uniqueImages.map((image, index) => (
|
|
274
276
|
<StyledImageBox key={index}>
|
|
@@ -6,6 +6,7 @@ import React from 'react';
|
|
|
6
6
|
import styled from 'styled-components';
|
|
7
7
|
import { Drawer, List, Typography } from 'antd';
|
|
8
8
|
import { CloseOutlined } from '@ant-design/icons';
|
|
9
|
+
import { useTranslation } from '../i18n';
|
|
9
10
|
|
|
10
11
|
const { Title } = Typography;
|
|
11
12
|
|
|
@@ -136,6 +137,7 @@ export const WebSearchContent: React.FC<WebSearchContentProps> = ({
|
|
|
136
137
|
className,
|
|
137
138
|
style,
|
|
138
139
|
}) => {
|
|
140
|
+
const { t } = useTranslation();
|
|
139
141
|
// 判断是否移动端
|
|
140
142
|
const isMobile = typeof window !== 'undefined' && window.innerWidth <= 500;
|
|
141
143
|
const panelWidth = isMobile ? '100%' : width;
|
|
@@ -174,7 +176,7 @@ export const WebSearchContent: React.FC<WebSearchContentProps> = ({
|
|
|
174
176
|
<StyledContainer $isMobile={isMobile}>
|
|
175
177
|
<StyledContent>
|
|
176
178
|
<StyledHeader>
|
|
177
|
-
<Title level={5} style={{ margin: 0 }}
|
|
179
|
+
<Title level={5} style={{ margin: 0 }}>{t('webSearch.title')}</Title>
|
|
178
180
|
<StyledCloseButton onClick={handleClose}>
|
|
179
181
|
<CloseOutlined />
|
|
180
182
|
</StyledCloseButton>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locale Context 与 Provider
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import React, { createContext, useEffect, useMemo } from 'react';
|
|
6
|
+
import zhCN from '../locales/zh-CN';
|
|
7
|
+
import type { DeepPartial, Locale } from '../locales/types';
|
|
8
|
+
import { setGlobalLocale } from './translate';
|
|
9
|
+
import { deepMerge } from './utils';
|
|
10
|
+
|
|
11
|
+
export interface LocaleContextValue {
|
|
12
|
+
locale: Locale;
|
|
13
|
+
localeName: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const LocaleContext = createContext<LocaleContextValue>({
|
|
17
|
+
locale: zhCN as unknown as Locale,
|
|
18
|
+
localeName: 'zh-CN',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export interface LocaleProviderProps {
|
|
22
|
+
/** 完整 locale 对象,默认使用内置 zhCN */
|
|
23
|
+
locale?: Locale;
|
|
24
|
+
/** 语言标识,如 'zh-CN' / 'en-US' */
|
|
25
|
+
localeName?: string;
|
|
26
|
+
/** 部分覆盖:在选定 locale 基础上 deep merge 自定义文案 */
|
|
27
|
+
overrides?: DeepPartial<Locale>;
|
|
28
|
+
children: React.ReactNode;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* LocaleProvider
|
|
33
|
+
*
|
|
34
|
+
* 用法示例:
|
|
35
|
+
* ```tsx
|
|
36
|
+
* import { LocaleProvider, enUS } from '@alicloud/appflow-chat';
|
|
37
|
+
*
|
|
38
|
+
* <LocaleProvider locale={enUS} localeName="en-US">
|
|
39
|
+
* <App />
|
|
40
|
+
* </LocaleProvider>
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export const LocaleProvider: React.FC<LocaleProviderProps> = ({
|
|
44
|
+
locale,
|
|
45
|
+
localeName = 'zh-CN',
|
|
46
|
+
overrides,
|
|
47
|
+
children,
|
|
48
|
+
}) => {
|
|
49
|
+
const baseLocale = (locale ?? (zhCN as unknown as Locale)) as Locale;
|
|
50
|
+
|
|
51
|
+
const mergedLocale = useMemo<Locale>(() => {
|
|
52
|
+
if (!overrides) return baseLocale;
|
|
53
|
+
return deepMerge(
|
|
54
|
+
baseLocale as unknown as Record<string, unknown>,
|
|
55
|
+
overrides as Record<string, unknown>
|
|
56
|
+
) as unknown as Locale;
|
|
57
|
+
}, [baseLocale, overrides]);
|
|
58
|
+
|
|
59
|
+
// 同步到全局,供非 React 场景(如 ChatService)使用
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
setGlobalLocale(mergedLocale, localeName);
|
|
62
|
+
}, [mergedLocale, localeName]);
|
|
63
|
+
|
|
64
|
+
const contextValue = useMemo<LocaleContextValue>(
|
|
65
|
+
() => ({ locale: mergedLocale, localeName }),
|
|
66
|
+
[mergedLocale, localeName]
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
return <LocaleContext.Provider value={contextValue}>{children}</LocaleContext.Provider>;
|
|
70
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* i18n 模块统一导出
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { LocaleProvider, LocaleContext } from './LocaleContext';
|
|
6
|
+
export type { LocaleProviderProps, LocaleContextValue } from './LocaleContext';
|
|
7
|
+
|
|
8
|
+
export { useTranslation } from './useTranslation';
|
|
9
|
+
export type { UseTranslationResult } from './useTranslation';
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
translate,
|
|
13
|
+
setGlobalLocale,
|
|
14
|
+
getGlobalLocale,
|
|
15
|
+
getGlobalLocaleName,
|
|
16
|
+
} from './translate';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 全局 translate 函数
|
|
3
|
+
*
|
|
4
|
+
* 用于非 React 组件场景(如 ChatService、工具函数)。
|
|
5
|
+
* 在 LocaleProvider 挂载/更新时会自动同步全局 locale。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import zhCN from '../locales/zh-CN';
|
|
9
|
+
import type { Locale, TranslationKey, TranslationParams } from '../locales/types';
|
|
10
|
+
import { getByPath, interpolate } from './utils';
|
|
11
|
+
|
|
12
|
+
let globalLocale: Locale = zhCN as unknown as Locale;
|
|
13
|
+
let globalLocaleName = 'zh-CN';
|
|
14
|
+
|
|
15
|
+
/** 设置全局 locale,由 LocaleProvider 在挂载/更新时调用 */
|
|
16
|
+
export function setGlobalLocale(locale: Locale, localeName?: string): void {
|
|
17
|
+
globalLocale = locale;
|
|
18
|
+
if (localeName) globalLocaleName = localeName;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 获取当前全局 locale */
|
|
22
|
+
export function getGlobalLocale(): Locale {
|
|
23
|
+
return globalLocale;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 获取当前全局 locale 名称 */
|
|
27
|
+
export function getGlobalLocaleName(): string {
|
|
28
|
+
return globalLocaleName;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 全局翻译函数(非 Hook 版本)
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* translate('common.loading') // '加载中...'
|
|
36
|
+
* translate('humanVerify.placeholder.input', { title: '名称' }) // '请输入名称'
|
|
37
|
+
*/
|
|
38
|
+
export function translate(key: TranslationKey, params?: TranslationParams): string {
|
|
39
|
+
const value = getByPath(globalLocale, key);
|
|
40
|
+
if (typeof value !== 'string') return key;
|
|
41
|
+
return params ? interpolate(value, params) : value;
|
|
42
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useTranslation Hook
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useCallback, useContext } from 'react';
|
|
6
|
+
import type { Locale, TranslationKey, TranslationParams } from '../locales/types';
|
|
7
|
+
import { LocaleContext } from './LocaleContext';
|
|
8
|
+
import { getByPath, interpolate } from './utils';
|
|
9
|
+
|
|
10
|
+
export interface UseTranslationResult {
|
|
11
|
+
/** 翻译函数 */
|
|
12
|
+
t: (key: TranslationKey, params?: TranslationParams) => string;
|
|
13
|
+
/** 当前 locale 对象 */
|
|
14
|
+
locale: Locale;
|
|
15
|
+
/** 当前语言标识 */
|
|
16
|
+
localeName: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 在 React 组件内消费国际化文案
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* const { t } = useTranslation();
|
|
24
|
+
* <Input placeholder={t('humanVerify.placeholder.input', { title: '名称' })} />
|
|
25
|
+
*/
|
|
26
|
+
export function useTranslation(): UseTranslationResult {
|
|
27
|
+
const { locale, localeName } = useContext(LocaleContext);
|
|
28
|
+
|
|
29
|
+
const t = useCallback(
|
|
30
|
+
(key: TranslationKey, params?: TranslationParams): string => {
|
|
31
|
+
const value = getByPath(locale, key);
|
|
32
|
+
if (typeof value !== 'string') return key;
|
|
33
|
+
return params ? interpolate(value, params) : value;
|
|
34
|
+
},
|
|
35
|
+
[locale]
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
return { t, locale, localeName };
|
|
39
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* i18n 工具函数:插值、路径取值、深度合并
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { TranslationParams } from '../locales/types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 字符串插值
|
|
9
|
+
* @example
|
|
10
|
+
* interpolate('请输入{title}', { title: '名称' }) // '请输入名称'
|
|
11
|
+
*/
|
|
12
|
+
export function interpolate(template: string, params: TranslationParams): string {
|
|
13
|
+
return template.replace(/\{(\w+)\}/g, (_, key: string) => {
|
|
14
|
+
const value = params[key];
|
|
15
|
+
return value === undefined || value === null ? `{${key}}` : String(value);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 按点分路径从对象中取值
|
|
21
|
+
* @example
|
|
22
|
+
* getByPath({ a: { b: 1 } }, 'a.b') // 1
|
|
23
|
+
*/
|
|
24
|
+
export function getByPath(obj: unknown, path: string): unknown {
|
|
25
|
+
if (!obj || typeof obj !== 'object') return undefined;
|
|
26
|
+
return path.split('.').reduce<unknown>((acc, key) => {
|
|
27
|
+
if (acc && typeof acc === 'object' && key in acc) {
|
|
28
|
+
return (acc as Record<string, unknown>)[key];
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
}, obj);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 深度合并两个对象(不修改原对象)
|
|
36
|
+
* 仅合并普通对象,数组和其他类型直接覆盖。
|
|
37
|
+
*/
|
|
38
|
+
export function deepMerge<T extends Record<string, unknown>>(
|
|
39
|
+
target: T,
|
|
40
|
+
source: Record<string, unknown> | undefined
|
|
41
|
+
): T {
|
|
42
|
+
if (!source) return target;
|
|
43
|
+
|
|
44
|
+
const result: Record<string, unknown> = { ...target };
|
|
45
|
+
|
|
46
|
+
for (const key of Object.keys(source)) {
|
|
47
|
+
const sourceValue = source[key];
|
|
48
|
+
const targetValue = result[key];
|
|
49
|
+
|
|
50
|
+
if (isPlainObject(targetValue) && isPlainObject(sourceValue)) {
|
|
51
|
+
result[key] = deepMerge(
|
|
52
|
+
targetValue as Record<string, unknown>,
|
|
53
|
+
sourceValue as Record<string, unknown>
|
|
54
|
+
);
|
|
55
|
+
} else if (sourceValue !== undefined) {
|
|
56
|
+
result[key] = sourceValue;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return result as T;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
64
|
+
if (value === null || typeof value !== 'object') return false;
|
|
65
|
+
const proto = Object.getPrototypeOf(value);
|
|
66
|
+
return proto === null || proto === Object.prototype;
|
|
67
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -79,3 +79,28 @@ export type {
|
|
|
79
79
|
// ==================== 工具函数导出 ====================
|
|
80
80
|
export { loadEchartsScript } from './utils/loadEcharts';
|
|
81
81
|
export { loadMermaidScript } from './utils/loadMermaid';
|
|
82
|
+
|
|
83
|
+
// ==================== 国际化(i18n)导出 ====================
|
|
84
|
+
export {
|
|
85
|
+
LocaleProvider,
|
|
86
|
+
LocaleContext,
|
|
87
|
+
useTranslation,
|
|
88
|
+
translate,
|
|
89
|
+
setGlobalLocale,
|
|
90
|
+
getGlobalLocale,
|
|
91
|
+
getGlobalLocaleName,
|
|
92
|
+
} from './i18n';
|
|
93
|
+
export type {
|
|
94
|
+
LocaleProviderProps,
|
|
95
|
+
LocaleContextValue,
|
|
96
|
+
UseTranslationResult,
|
|
97
|
+
} from './i18n';
|
|
98
|
+
|
|
99
|
+
// ==================== 国际化词条导出 ====================
|
|
100
|
+
export { zhCN, enUS } from './locales';
|
|
101
|
+
export type {
|
|
102
|
+
Locale,
|
|
103
|
+
TranslationKey,
|
|
104
|
+
TranslationParams,
|
|
105
|
+
DeepPartial,
|
|
106
|
+
} from './locales';
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 英文词条
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Locale } from './types';
|
|
6
|
+
|
|
7
|
+
const enUS: Locale = {
|
|
8
|
+
common: {
|
|
9
|
+
loading: 'Loading...',
|
|
10
|
+
confirm: 'OK',
|
|
11
|
+
cancel: 'Cancel',
|
|
12
|
+
submit: 'Submit',
|
|
13
|
+
retry: 'Retry',
|
|
14
|
+
copy: 'Copy',
|
|
15
|
+
copied: 'Copied',
|
|
16
|
+
placeholderSelect: 'Please select',
|
|
17
|
+
},
|
|
18
|
+
humanVerify: {
|
|
19
|
+
requiredAll: 'Please fill in all required fields',
|
|
20
|
+
submitted: 'Submitted',
|
|
21
|
+
pending: 'Pending',
|
|
22
|
+
placeholder: {
|
|
23
|
+
input: 'Please enter {title}',
|
|
24
|
+
select: 'Please select',
|
|
25
|
+
},
|
|
26
|
+
file: {
|
|
27
|
+
supportAll: 'Supports all file formats',
|
|
28
|
+
supportFormats: 'Supports {formats} formats',
|
|
29
|
+
uploadButton: 'Select File',
|
|
30
|
+
uploading: 'Uploading',
|
|
31
|
+
upload: 'Upload',
|
|
32
|
+
defaultFileName: 'File',
|
|
33
|
+
maxSizeError: 'File size cannot exceed {size}',
|
|
34
|
+
uploadFailed: 'File upload failed',
|
|
35
|
+
tokenFailed: 'Failed to get upload credentials',
|
|
36
|
+
uploaderNotConfigured: 'Upload feature is not configured',
|
|
37
|
+
uploadMethodNotConfigured: 'File upload method is not configured',
|
|
38
|
+
getFileIdFailed: 'Failed to get file ID',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
webSearch: {
|
|
42
|
+
title: 'Search Results',
|
|
43
|
+
foundPages: 'Found {count} web pages',
|
|
44
|
+
},
|
|
45
|
+
source: {
|
|
46
|
+
title: 'References',
|
|
47
|
+
answerFrom: 'Sources:',
|
|
48
|
+
imageFrom: 'Images:',
|
|
49
|
+
},
|
|
50
|
+
rich: {
|
|
51
|
+
stepLabel: 'Step {index}:',
|
|
52
|
+
emptyContent: 'No content',
|
|
53
|
+
},
|
|
54
|
+
markdown: {
|
|
55
|
+
copyCode: 'Copy code',
|
|
56
|
+
copied: 'Copied!',
|
|
57
|
+
copy: 'Copy',
|
|
58
|
+
copiedShort: 'Copied',
|
|
59
|
+
deepThinking: 'Deep Thinking',
|
|
60
|
+
chartLoading: 'Loading chart...',
|
|
61
|
+
tableLoading: 'Loading table...',
|
|
62
|
+
tableLoadFailed: 'Failed to load table data, please check the data format',
|
|
63
|
+
chartLoadFailed: 'Failed to load chart data, please check the data format',
|
|
64
|
+
mermaidLoadFailed: 'Failed to load Mermaid library',
|
|
65
|
+
mermaidNotLoaded: 'Mermaid is not loaded correctly',
|
|
66
|
+
mermaidRenderFailed: 'Mermaid chart rendering failed',
|
|
67
|
+
},
|
|
68
|
+
message: {
|
|
69
|
+
like: 'Like',
|
|
70
|
+
dislike: 'Dislike',
|
|
71
|
+
regenerate: 'Regenerate',
|
|
72
|
+
copy: 'Copy',
|
|
73
|
+
copied: 'Copied',
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export default enUS;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locale 类型定义
|
|
3
|
+
*
|
|
4
|
+
* 以 zh-CN 词条结构作为类型基准,所有其他语言必须实现相同的 key 结构。
|
|
5
|
+
* - 对象结构(哪些 key、嵌套层级)严格等同 zhCN
|
|
6
|
+
* - 叶子节点的值放宽为 string,避免英文字面量被中文字面量类型卡住
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type zhCN from './zh-CN';
|
|
10
|
+
|
|
11
|
+
/** 词条对象类型:保留 zhCN 的结构,但叶子节点统一为 string */
|
|
12
|
+
export type Locale = DeepStringify<typeof zhCN>;
|
|
13
|
+
|
|
14
|
+
/** 所有合法的扁平 key 路径,如 'common.loading' | 'humanVerify.placeholder.input' */
|
|
15
|
+
export type TranslationKey = DeepKey<typeof zhCN>;
|
|
16
|
+
|
|
17
|
+
/** 插值参数 */
|
|
18
|
+
export type TranslationParams = Record<string, string | number>;
|
|
19
|
+
|
|
20
|
+
/** 深度可选,用于 LocaleProvider 的 overrides 字段 */
|
|
21
|
+
export type DeepPartial<T> = {
|
|
22
|
+
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** 把所有叶子节点的值类型统一为 string,并去掉 readonly */
|
|
26
|
+
type DeepStringify<T> = {
|
|
27
|
+
-readonly [K in keyof T]: T[K] extends object ? DeepStringify<T[K]> : string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** 递归生成所有叶子节点的扁平路径联合类型 */
|
|
31
|
+
type DeepKey<T, P extends string = ''> = {
|
|
32
|
+
[K in keyof T & string]: T[K] extends object
|
|
33
|
+
? DeepKey<T[K], `${P}${P extends '' ? '' : '.'}${K}`>
|
|
34
|
+
: `${P}${P extends '' ? '' : '.'}${K}`;
|
|
35
|
+
}[keyof T & string];
|