@alicloud/appflow-chat 0.0.4-beta.4 → 0.0.4-beta.6

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 (36) hide show
  1. package/dist/appflow-chat.cjs.js +2028 -1
  2. package/dist/appflow-chat.esm.js +38352 -23
  3. package/dist/types/index.d.ts +169 -0
  4. package/package.json +3 -15
  5. package/src/components/HumanVerify/CustomParamsRenderer/ArrayField.tsx +6 -4
  6. package/src/components/HumanVerify/CustomParamsRenderer/EnumField.tsx +5 -3
  7. package/src/components/HumanVerify/CustomParamsRenderer/FieldRenderer.tsx +6 -4
  8. package/src/components/HumanVerify/CustomParamsRenderer/FileField.tsx +38 -30
  9. package/src/components/HumanVerify/CustomParamsRenderer/ObjectField.tsx +4 -2
  10. package/src/components/HumanVerify/CustomParamsRenderer/TimeField.tsx +38 -101
  11. package/src/components/HumanVerify/HistoryCard.tsx +6 -4
  12. package/src/components/HumanVerify/HumanVerify.tsx +5 -3
  13. package/src/components/MessageBubble.tsx +4 -2
  14. package/src/components/RichMessageBubble.tsx +4 -2
  15. package/src/core/RichBubbleContent.tsx +4 -2
  16. package/src/core/SourceContent.tsx +5 -3
  17. package/src/core/WebSearchContent.tsx +3 -1
  18. package/src/i18n/LocaleContext.tsx +70 -0
  19. package/src/i18n/index.ts +16 -0
  20. package/src/i18n/translate.ts +42 -0
  21. package/src/i18n/useTranslation.ts +39 -0
  22. package/src/i18n/utils.ts +67 -0
  23. package/src/index.ts +25 -0
  24. package/src/locales/en-US.ts +77 -0
  25. package/src/locales/index.ts +7 -0
  26. package/src/locales/types.ts +35 -0
  27. package/src/locales/zh-CN.ts +78 -0
  28. package/src/markdown/components/Chart.tsx +3 -1
  29. package/src/markdown/components/Mermaid.tsx +8 -6
  30. package/src/markdown/index.tsx +11 -8
  31. package/dist/dayjs.min-L7v6Rjvs.cjs +0 -1
  32. package/dist/dayjs.min-MIkW36mC.js +0 -301
  33. package/dist/index-CsdPG_CH.cjs +0 -2032
  34. package/dist/index-DuGIPL7L.js +0 -37902
  35. package/dist/moment-BOHN1eRP.js +0 -2578
  36. package/dist/moment-BdH06dpW.cjs +0 -10
@@ -1,72 +1,24 @@
1
- import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
- import { DatePicker, version } from 'antd';
1
+ import React, { useCallback, useMemo } from 'react';
2
+ import { DatePicker } from 'antd';
3
+ import dayjs, { Dayjs } from 'dayjs';
3
4
  import { TimeFieldProps, TimeSubType } from './types';
4
5
  import styled from 'styled-components';
6
+ import { useTranslation } from '../../../i18n';
5
7
 
8
+ // ==================== Styled Components ====================
9
+
10
+ // 时间选择器容器
6
11
  const TimeFieldContainer = styled.div`
7
12
  width: 100%;
13
+
8
14
  .ant-picker {
9
15
  width: 100%;
10
16
  }
11
17
  `;
12
18
 
13
- const getAntdMajorVersion = (): number => {
14
- try {
15
- return parseInt(version.split('.')[0], 10);
16
- } catch {
17
- return 5;
18
- }
19
- };
20
-
21
- const isAntd5OrAbove = getAntdMajorVersion() >= 5;
22
-
23
- // 兼容 ESM 和 CJS 环境的时间库加载
24
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
- let timeLib: any = null;
26
-
27
- // 同步尝试加载时间库(CJS 环境下可用)
28
- const loadTimeLibSync = (): any => {
29
- if (timeLib) return timeLib;
30
-
31
- if (isAntd5OrAbove) {
32
- try {
33
- // eslint-disable-next-line @typescript-eslint/no-require-imports
34
- timeLib = require('dayjs');
35
- } catch {
36
- // require 在 ESM 环境中可能失败,后续通过异步 import 兜底
37
- }
38
- } else {
39
- try {
40
- // eslint-disable-next-line @typescript-eslint/no-require-imports
41
- timeLib = require('moment');
42
- } catch {
43
- // require 在 ESM 环境中可能失败,后续通过异步 import 兜底
44
- }
45
- }
46
- return timeLib;
47
- };
48
-
49
- // 异步加载时间库(ESM 环境下的兜底方案)
50
- const loadTimeLibAsync = async (): Promise<any> => {
51
- if (timeLib) return timeLib;
52
-
53
- try {
54
- if (isAntd5OrAbove) {
55
- const dayjs = await import('dayjs');
56
- timeLib = dayjs.default || dayjs;
57
- } else {
58
- const moment = await import('moment');
59
- timeLib = moment.default || moment;
60
- }
61
- } catch {
62
- console.warn('Failed to load time library (both sync and async)');
63
- }
64
- return timeLib;
65
- };
66
-
67
- // 先尝试同步加载
68
- loadTimeLibSync();
69
-
19
+ /**
20
+ * 根据时间子类型获取日期格式
21
+ */
70
22
  const getDateFormat = (subType?: TimeSubType): string => {
71
23
  switch (subType) {
72
24
  case 'year-month':
@@ -80,40 +32,34 @@ const getDateFormat = (subType?: TimeSubType): string => {
80
32
  }
81
33
  };
82
34
 
35
+ /**
36
+ * 根据时间子类型获取 picker 类型
37
+ */
83
38
  const getPickerType = (subType?: TimeSubType): 'date' | 'month' | undefined => {
84
39
  switch (subType) {
85
40
  case 'year-month':
86
41
  return 'month';
42
+ case 'year-month-day':
43
+ case 'datetime':
87
44
  default:
88
45
  return 'date';
89
46
  }
90
47
  };
91
48
 
49
+ /**
50
+ * 时间字段组件
51
+ * 根据 SubType 渲染不同的时间选择器
52
+ * 优先从 AssociationPropertyMetadata.SubType 读取,兼容旧的 TimeSubType 字段
53
+ */
92
54
  export const TimeField: React.FC<TimeFieldProps> = ({
93
55
  schema,
94
56
  value,
95
57
  onChange,
96
58
  disabled = false,
97
59
  }) => {
98
- // 使用 state 管理时间库引用,确保异步加载完成后能触发重新渲染
99
- const [lib, setLib] = useState<any>(() => timeLib);
100
-
101
- // 如果同步加载失败,通过异步 import 兜底加载
102
- useEffect(() => {
103
- if (lib) return;
104
-
105
- let cancelled = false;
106
- loadTimeLibAsync().then((loaded) => {
107
- if (!cancelled && loaded) {
108
- setLib(loaded);
109
- }
110
- });
111
-
112
- return () => {
113
- cancelled = true;
114
- };
115
- }, [lib]);
116
-
60
+ const { t } = useTranslation();
61
+ // 优先从 AssociationPropertyMetadata.SubType 读取,取数组第一个元素
62
+ // 兼容旧的 TimeSubType 字段
117
63
  const subType = useMemo((): TimeSubType | undefined => {
118
64
  const subTypeArray = schema.AssociationPropertyMetadata?.SubType;
119
65
  if (Array.isArray(subTypeArray) && subTypeArray.length > 0) {
@@ -126,44 +72,35 @@ export const TimeField: React.FC<TimeFieldProps> = ({
126
72
  const picker = getPickerType(subType);
127
73
  const showTime = subType === 'datetime';
128
74
 
129
- // 将字符串值转换为时间对象
130
- const dateValue = useMemo(() => {
131
- if (!value || !lib) return null;
132
-
133
- try {
134
- const parsed = lib(value, format);
135
- if (parsed && typeof parsed.isValid === 'function' && !parsed.isValid()) {
136
- // 如果严格解析失败,尝试宽松解析
137
- return lib(value);
138
- }
139
- return parsed;
140
- } catch {
141
- return null;
142
- }
143
- }, [value, format, lib]);
144
-
75
+ // 处理值变化
145
76
  const handleChange = useCallback(
146
- (_date: any, dateString: string | string[]) => {
147
- const stringValue = Array.isArray(dateString) ? dateString[0] : dateString;
148
- onChange?.(stringValue || null);
77
+ (date: Dayjs | null) => {
78
+ if (date) {
79
+ onChange?.(date.format(format));
80
+ } else {
81
+ onChange?.(null);
82
+ }
149
83
  },
150
- [onChange]
84
+ [onChange, format]
151
85
  );
152
86
 
87
+ // 将字符串值转换为 dayjs 对象
88
+ const dayjsValue = value ? dayjs(value, format) : null;
89
+
153
90
  return (
154
91
  <TimeFieldContainer>
155
92
  <DatePicker
156
- value={dateValue}
93
+ value={dayjsValue}
157
94
  onChange={handleChange}
158
95
  format={format}
159
96
  picker={picker}
160
97
  showTime={showTime ? { format: 'HH:mm:ss' } : false}
161
98
  disabled={disabled}
162
99
  style={{ width: '100%' }}
163
- placeholder="请选择"
100
+ placeholder={t('common.placeholderSelect')}
164
101
  />
165
102
  </TimeFieldContainer>
166
103
  );
167
104
  };
168
105
 
169
- export default TimeField;
106
+ export default TimeField;
@@ -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>暂无内容</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>步骤{index + 1}:</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>已搜索到{webSearchArray.length}个网页</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>回答来源:</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>图片来源:</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 }}>搜索结果</Title>
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
+ }