diff --git a/src/catalogue/components/catalogue.tsx b/src/catalogue/components/catalogue.tsx index 8a961ffa1..a72cf7bb6 100644 --- a/src/catalogue/components/catalogue.tsx +++ b/src/catalogue/components/catalogue.tsx @@ -3,6 +3,7 @@ import { Dropdown, DropdownProps, Form, Input, Tabs } from 'antd'; import { BlockHeader, EllipsisText } from 'dt-react-component'; import { IBlockHeaderProps } from 'dt-react-component/blockHeader'; +import useLocale from '../../locale/useLocale'; import { ITreeNode } from '../useTreeData'; import { CatalogIcon, CloseIcon, DragIcon, EllipsisIcon, SearchIcon } from './icon'; import CatalogueTree, { ICatalogueTree } from './tree'; @@ -49,12 +50,13 @@ function isTabMode = {}, T extends readOnlyTab = a const Catalogue = = {}, T extends readOnlyTab = any>( props: CatalogueProps ) => { + const locale = useLocale('Catalogue'); const { title, addonBefore = , tooltip = false, showSearch = false, - placeholder = '搜索目录名称', + placeholder = locale.searchPlaceholder, addonAfter, edit = true, treeData, @@ -78,7 +80,7 @@ const Catalogue = = {}, T extends readOnlyTab = a form.setFields([{ name: 'catalog_input', errors: [] }])} diff --git a/src/configProvider/__tests__/index.test.tsx b/src/configProvider/__tests__/index.test.tsx new file mode 100644 index 000000000..708a29fc8 --- /dev/null +++ b/src/configProvider/__tests__/index.test.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; + +import enUS from '../../locale/en-US'; +import { LocaleContext } from '../../locale/useLocale'; +import zhCN from '../../locale/zh-CN'; +import ConfigProvider from '..'; + +describe('ConfigProvider', () => { + it('renders children correctly', () => { + const { getByText } = render( + +
Test Content
+
+ ); + expect(getByText('Test Content')).toBeInTheDocument(); + }); + + it('provides locale context correctly', () => { + const TestComponent = () => { + const { locale } = React.useContext(LocaleContext); + return
{locale.locale}
; + }; + + const { getByTestId } = render( + + + + ); + + expect(getByTestId('locale-value').textContent).toBe(zhCN.locale); + }); + + it('updates context when locale changes', () => { + const TestComponent = () => { + const { locale } = React.useContext(LocaleContext); + return
{locale.locale}
; + }; + + const { getByTestId, rerender } = render( + + + + ); + + expect(getByTestId('locale-value').textContent).toBe(zhCN.locale); + + // 重新渲染,使用英文语言包 + rerender( + + + + ); + + expect(getByTestId('locale-value').textContent).toBe(enUS.locale); + }); +}); diff --git a/src/configProvider/demos/basic.tsx b/src/configProvider/demos/basic.tsx new file mode 100644 index 000000000..54f83d555 --- /dev/null +++ b/src/configProvider/demos/basic.tsx @@ -0,0 +1,38 @@ +import React, { useState } from 'react'; +import { Radio, Space } from 'antd'; +import { BlockHeader, ConfigProvider, Copy, enUS, Input, zhCN } from 'dt-react-component'; + +export default function Basic() { + const [locale, setLocale] = useState(zhCN); + + return ( + + setLocale(e.target.value === 'zh-CN' ? zhCN : enUS)} + > + 中文 + English + + + + +
+

Copy 组件

+ +
+
+

BlockHeader 组件

+ +

内容区域

+
+
+
+

Input.Match 组件

+ +
+
+
+
+ ); +} diff --git a/src/configProvider/demos/custom.tsx b/src/configProvider/demos/custom.tsx new file mode 100644 index 000000000..0269b142e --- /dev/null +++ b/src/configProvider/demos/custom.tsx @@ -0,0 +1,101 @@ +import React, { useState } from 'react'; +import { Radio, Space } from 'antd'; +import { BlockHeader, ConfigProvider, Copy, enUS, useLocale, zhCN } from 'dt-react-component'; + +// 自定义组件,使用useLocale获取当前语言环境 +const LocaleConsumer = () => { + const copyLocale = useLocale('Copy'); + const blockHeaderLocale = useLocale('BlockHeader'); + + return ( +
+

当前语言环境

+
+

+ Copy组件: +

+
    +
  • copy: {copyLocale.copy}
  • +
  • copied: {copyLocale.copied}
  • +
+ +

+ BlockHeader组件: +

+
    +
  • expand: {blockHeaderLocale.expand}
  • +
  • collapse: {blockHeaderLocale.collapse}
  • +
+
+
+ ); +}; + +export default function Custom() { + // 创建自定义语言包 + const [customLocale, setCustomLocale] = useState({ + ...zhCN, + locale: 'custom-zh', + Copy: { + copy: '点击复制', + copied: '已复制到剪贴板', + }, + BlockHeader: { + expand: '展开全部', + collapse: '收起全部', + }, + }); + + const [localeType, setLocaleType] = useState('custom'); + + const handleLocaleChange = (type: string) => { + setLocaleType(type); + if (type === 'zh-CN') { + setCustomLocale(zhCN); + } else if (type === 'en-US') { + setCustomLocale(enUS); + } else { + // 恢复自定义语言包 + setCustomLocale({ + ...zhCN, + locale: 'custom-zh', + Copy: { + copy: '点击复制', + copied: '已复制到剪贴板', + }, + BlockHeader: { + expand: '展开全部', + collapse: '收起全部', + }, + }); + } + }; + + return ( + + handleLocaleChange(e.target.value)}> + 自定义语言 + 默认中文 + 默认英文 + + + + +
+

使用自定义语言包

+ +
+ +
+

BlockHeader 示例

+ +

内容区域

+
+
+ + +
+
+
+ ); +} diff --git a/src/configProvider/demos/nested.tsx b/src/configProvider/demos/nested.tsx new file mode 100644 index 000000000..1cbc03adf --- /dev/null +++ b/src/configProvider/demos/nested.tsx @@ -0,0 +1,86 @@ +import React, { useState } from 'react'; +import { Divider, Radio, Space } from 'antd'; +import { ConfigProvider, Copy, enUS, useLocale, zhCN } from 'dt-react-component'; + +// 自定义组件,使用useLocale获取当前语言环境 +const LocaleDisplay = () => { + const copyLocale = useLocale('Copy'); + return ( +
+

当前Copy组件的文案:

+
    +
  • copy: {copyLocale.copy}
  • +
  • copied: {copyLocale.copied}
  • +
+
+ ); +}; + +export default function Nested() { + const [outerLocale, setOuterLocale] = useState(zhCN); + const [innerLocale, setInnerLocale] = useState(enUS); + + return ( + +
+

外层ConfigProvider

+ setOuterLocale(e.target.value === 'zh-CN' ? zhCN : enUS)} + > + 中文 + English + +
+ + + +
+

外层ConfigProvider的组件

+ + +
+ + 嵌套的ConfigProvider + +
+

内层ConfigProvider

+ + setInnerLocale(e.target.value === 'zh-CN' ? zhCN : enUS) + } + > + 中文 + English + + + +
+

内层ConfigProvider的组件

+ + +
+
+
+
+
+ +
+

无ConfigProvider的组件

+

当组件不在ConfigProvider内时,将使用默认的中文语言包

+ + +
+
+ ); +} diff --git a/src/configProvider/index.md b/src/configProvider/index.md new file mode 100644 index 000000000..a0c575a9d --- /dev/null +++ b/src/configProvider/index.md @@ -0,0 +1,61 @@ +--- +title: ConfigProvider 全局化配置 +group: 组件 +toc: content +demo: + cols: 1 +--- + +# ConfigProvider 全局化配置 + +## 何时使用 + +ConfigProvider 用于全局配置组件库的默认属性,目前主要用于配置国际化文案。 + +## 代码演示 + + + + + + + +## API + +### ConfigProvider + +| 参数 | 说明 | 类型 | 默认值 | +| -------- | ---------- | ----------------- | ------ | +| locale | 语言包配置 | [Locale](#locale) | - | +| children | 子组件 | ReactNode | - | + +### Locale + +```typescript +interface Locale { + locale: string; + BlockHeader: { expand: string; collapse: string }; + Catalogue: { searchPlaceholder: string; inputPlaceholder: string }; + Chat: { + stopped: string; + stop: string; + regenerate: string; + }; + Copy: { copied: string; copy: string }; + Dropdown: { selectAll: string; resetText: string; okText: string }; + ErrorBoundary: { + please: string; + get: string; + refresh: string; + title: string; + }; + // ... 其他组件的文案配置 +} +``` + +## 注意事项 + +- 组件库默认使用中文(zh-CN)语言包。 +- 当组件不在 ConfigProvider 内时,将使用默认的中文语言包。 +- 可以通过 `useLocale` hook 在组件内部获取当前的语言环境。 +- 自定义语言包时,可以只覆盖需要修改的部分,其他部分会使用默认语言包。 diff --git a/src/errorBoundary/loadError.tsx b/src/errorBoundary/loadError.tsx index b10fbd491..b46e99b95 100644 --- a/src/errorBoundary/loadError.tsx +++ b/src/errorBoundary/loadError.tsx @@ -3,7 +3,7 @@ import React from 'react'; import useLocale from '../locale/useLocale'; const LoadError: React.FC = function () { - const locale = useLocale('LoadError'); + const locale = useLocale('ErrorBoundary'); return (
diff --git a/src/filterRules/index.tsx b/src/filterRules/index.tsx index 381bf8045..327500b12 100644 --- a/src/filterRules/index.tsx +++ b/src/filterRules/index.tsx @@ -4,6 +4,7 @@ import { InternalNamePath } from 'antd/lib/form/interface'; import { clone } from 'lodash-es'; import shortId from 'shortid'; +import useLocale from '../locale/useLocale'; import { RulesController } from './ruleController'; export enum ROW_PERMISSION_RELATION { @@ -56,11 +57,12 @@ function isDisabled(props: IProps): props is IDisabledProps { export type IProps = IDisabledProps | INormalProps; const FilterRules = (props: IProps) => { + const locale = useLocale('FilterRules'); const { component, disabled = false, value } = props; const { maxLevel = 5, maxSize = 100, - notEmpty = { data: true, message: '必须有一条数据' }, + notEmpty = { data: true, message: locale.message }, initValues, onChange, } = (!isDisabled(props) && props) as INormalProps; diff --git a/src/input/match.tsx b/src/input/match.tsx index 020ee844e..51cc2d8c3 100644 --- a/src/input/match.tsx +++ b/src/input/match.tsx @@ -71,9 +71,9 @@ export default function Match({ }, ] as const; - const filterOptions = propFilterOptions || searchTypeList.map((i) => i.key); - - const options = searchTypeList.filter((i) => filterOptions.includes(i.key)); + const options = searchTypeList.filter((i) => + (propFilterOptions || searchTypeList.map((i) => i.key)).includes(i.key) + ); const realSearchType = searchType || internalSearchType; const realValue = value || internalValue; diff --git a/src/locale/__tests__/LocaleContext.test.tsx b/src/locale/__tests__/LocaleContext.test.tsx new file mode 100644 index 000000000..e93e081a7 --- /dev/null +++ b/src/locale/__tests__/LocaleContext.test.tsx @@ -0,0 +1,94 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; + +import enUS from '../en-US'; +import { LocaleContext } from '../useLocale'; +import zhCN from '../zh-CN'; + +describe('LocaleContext', () => { + it('should provide locale context to children', () => { + const TestComponent = () => { + const { locale } = React.useContext(LocaleContext); + return
{locale.locale}
; + }; + + const { getByTestId } = render( + + + + ); + + expect(getByTestId('locale-value').textContent).toBe('zh-CN'); + }); + + it('should update context when locale changes', () => { + const TestComponent = () => { + const { locale } = React.useContext(LocaleContext); + return
{locale.locale}
; + }; + + const { getByTestId, rerender } = render( + + + + ); + + expect(getByTestId('locale-value').textContent).toBe('zh-CN'); + + // 重新渲染,使用英文语言包 + rerender( + + + + ); + + expect(getByTestId('locale-value').textContent).toBe('en-US'); + }); + + it('should provide nested locale context correctly', () => { + const InnerTestComponent = () => { + const { locale } = React.useContext(LocaleContext); + return
{locale.locale}
; + }; + + const OuterTestComponent = () => { + const { locale } = React.useContext(LocaleContext); + return ( +
+
{locale.locale}
+ + + +
+ ); + }; + + const { getByTestId } = render( + + + + ); + + expect(getByTestId('outer-locale-value').textContent).toBe('zh-CN'); + expect(getByTestId('inner-locale-value').textContent).toBe('en-US'); + }); + + it('should handle empty locale object', () => { + // 创建一个空的语言包 + const emptyLocale = {} as any; + + const TestComponent = () => { + const { locale } = React.useContext(LocaleContext); + return
{locale?.locale || 'undefined'}
; + }; + + const { getByTestId } = render( + + + + ); + + expect(getByTestId('locale-value').textContent).toBe('undefined'); + }); +}); diff --git a/src/locale/__tests__/useLocale.test.tsx b/src/locale/__tests__/useLocale.test.tsx new file mode 100644 index 000000000..ed3ab210c --- /dev/null +++ b/src/locale/__tests__/useLocale.test.tsx @@ -0,0 +1,112 @@ +import React from 'react'; +import { renderHook } from '@testing-library/react-hooks'; +import '@testing-library/jest-dom/extend-expect'; + +import enUS from '../en-US'; +import useLocale, { Locale, LocaleComponentName, LocaleContext } from '../useLocale'; +import zhCN from '../zh-CN'; + +describe('useLocale', () => { + it('should return default locale when no provider is used', () => { + const { result } = renderHook(() => useLocale('BlockHeader')); + + expect(result.current).toEqual({ + expand: '展开', + collapse: '收起', + }); + }); + + it('should return locale from context when provider is used', () => { + const wrapper = ({ children }: { children: React.ReactNode; [key: string]: any }) => ( + {children} + ); + + const { result } = renderHook(() => useLocale('BlockHeader'), { wrapper }); + + expect(result.current).toEqual({ + expand: 'Expand', + collapse: 'Collapse', + }); + }); + + it('should merge default locale with context locale', () => { + const customLocale: Locale = { + ...zhCN, + BlockHeader: { + expand: '自定义展开', + collapse: '自定义收起', + }, + }; + + const wrapper = ({ children }: { children: React.ReactNode; [key: string]: any }) => ( + + {children} + + ); + + const { result } = renderHook(() => useLocale('BlockHeader'), { wrapper }); + + expect(result.current).toEqual({ + expand: '自定义展开', + collapse: '自定义收起', + }); + }); + + it('should handle missing component in locale', () => { + const incompleteLocale = { + locale: 'incomplete', + } as Locale; + + const wrapper = ({ children }: { children: React.ReactNode; [key: string]: any }) => ( + + {children} + + ); + + const { result } = renderHook(() => useLocale('BlockHeader'), { wrapper }); + + expect(result.current).toEqual({ + expand: '展开', + collapse: '收起', + }); + }); + + it('should handle component name changes', () => { + const wrapper = ({ children }: { children: React.ReactNode; [key: string]: any }) => ( + {children} + ); + + const { result, rerender } = renderHook( + (props) => useLocale(props.componentName as LocaleComponentName), + { + wrapper, + initialProps: { componentName: 'BlockHeader', children:
}, + } + ); + + expect(result.current).toEqual({ + expand: 'Expand', + collapse: 'Collapse', + }); + + rerender({ componentName: 'Copy', children:
}); + + expect(result.current).toEqual({ + copied: 'Copied', + copy: 'Copy', + }); + }); + + it('should memoize the result', () => { + const wrapper = ({ children }: { children: React.ReactNode; [key: string]: any }) => ( + {children} + ); + + const { result, rerender } = renderHook(() => useLocale('BlockHeader'), { wrapper }); + const firstResult = JSON.stringify(result.current); + + rerender(); + + expect(JSON.stringify(result.current)).toBe(firstResult); + }); +}); diff --git a/src/locale/en-US.ts b/src/locale/en-US.ts index 19b569508..d5e90024b 100644 --- a/src/locale/en-US.ts +++ b/src/locale/en-US.ts @@ -6,12 +6,28 @@ const localeValues: Locale = { expand: 'Expand', collapse: 'Collapse', }, - + Catalogue: { + searchPlaceholder: 'Search catalog name', + inputPlaceholder: `Please enter`, + }, + Chat: { + stopped: 'Answer Stopped', + stop: 'Stop Answering', + regenerate: 'Regenerate', + }, Copy: { copied: 'Copied', - copy: 'Copy', }, + ErrorBoundary: { + please: 'A new version has been found. Please', + get: 'to get the new version.', + refresh: ' refresh ', + title: 'If this prompt persists for a long time, please contact the administrator.', + }, + FilterRules: { + message: 'Must have one data item', + }, Fullscreen: { exitFull: 'Exit Full Screen', full: 'Full Screen', @@ -19,16 +35,6 @@ const localeValues: Locale = { GlobalLoading: { loading: 'The application is loading, please wait~', }, - LoadError: { - please: 'A new version has been found. Please', - get: 'to get the new version.', - refresh: ' refresh ', - title: 'If this prompt persists for a long time, please contact the administrator.', - }, - Modal: { - okText: 'Ok', - cancelText: 'Cancel', - }, Dropdown: { resetText: 'Cancel', okText: 'Ok', @@ -40,9 +46,6 @@ const localeValues: Locale = { front: 'Head match', tail: 'Tail match', }, - MxGraph: { - newNode: 'New node', - }, NotFound: { description: 'Sorry, the page you visited does not exist', }, diff --git a/src/locale/useLocale.tsx b/src/locale/useLocale.tsx index d9ecc517f..e4681d6e5 100644 --- a/src/locale/useLocale.tsx +++ b/src/locale/useLocale.tsx @@ -5,8 +5,23 @@ import defaultLocaleData from './zh-CN'; export interface Locale { locale: string; BlockHeader: { expand: string; collapse: string }; + Catalogue: { searchPlaceholder: string; inputPlaceholder: string }; + Chat: { + stopped: string; + stop: string; + regenerate: string; + }; Copy: { copied: string; copy: string }; Dropdown: { selectAll: string; resetText: string; okText: string }; + ErrorBoundary: { + please: string; + get: string; + refresh: string; + title: string; + }; + FilterRules: { + message: string; + }; Fullscreen: { exitFull: string; full: string }; GlobalLoading: { loading: string }; Input: { @@ -15,17 +30,6 @@ export interface Locale { front: string; tail: string; }; - LoadError: { - please: string; - get: string; - refresh: string; - title: string; - }; - Modal: { - okText: string; - cancelText: string; - }; - MxGraph: { newNode: string }; NotFound: { description: string; }; diff --git a/src/locale/zh-CN.ts b/src/locale/zh-CN.ts index 465a17fce..418ac6331 100644 --- a/src/locale/zh-CN.ts +++ b/src/locale/zh-CN.ts @@ -6,11 +6,29 @@ const localeValues: Locale = { expand: '展开', collapse: '收起', }, + Catalogue: { + searchPlaceholder: '搜索目录名称', + inputPlaceholder: `请输入`, + }, + Chat: { + stopped: '回答已停止', + stop: '停止回答', + regenerate: '重新生成', + }, Copy: { copied: '复制成功', copy: '复制', }, Dropdown: { selectAll: '全选', resetText: '重置', okText: '确定' }, + ErrorBoundary: { + please: '发现新版本,请', + get: '获取新版本。', + refresh: '刷新', + title: '若该提示长时间存在,请联系管理员。', + }, + FilterRules: { + message: '必须有一条数据', + }, Fullscreen: { exitFull: '退出全屏', full: '全屏', @@ -18,25 +36,12 @@ const localeValues: Locale = { GlobalLoading: { loading: '应用加载中,请等候~', }, - LoadError: { - please: '发现新版本,请', - get: '获取新版本。', - refresh: '刷新', - title: '若该提示长时间存在,请联系管理员。', - }, Input: { case: '区分大小写匹配', precise: '精确匹配', front: '头部匹配', tail: '尾部匹配', }, - Modal: { - okText: '确定', - cancelText: '取消', - }, - MxGraph: { - newNode: '新节点', - }, NotFound: { description: '抱歉,您访问的页面不存在', }, diff --git a/src/notFound/index.tsx b/src/notFound/index.tsx index 2571e706e..ea03e2470 100644 --- a/src/notFound/index.tsx +++ b/src/notFound/index.tsx @@ -12,6 +12,7 @@ interface INotFoundProps { const NotFound: React.FC = function ({ className, style }) { const locale = useLocale('NotFound'); + return (
= forwardRef( const { copyTypes = [], ...restProps } = options || {}; useImperativeHandle(ref, () => tableRef.current); + const locale = useLocale('SpreadSheet'); useEffect(() => {