Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@kne-components/components-admin",
"version": "1.1.45",
"version": "1.1.47",
"description": "用于实现一个后台管理系统的必要组件",
"scripts": {
"init": "husky",
Expand Down Expand Up @@ -64,7 +64,7 @@
}
},
"devDependencies": {
"@kne/modules-dev": "^2.2.1",
"@kne/modules-dev": "^2.3.4",
"@kne/react-fetch": "^1.4.3",
"@kne/react-scripts": "^5.1.5",
"@kne/remote-loader": "^1.2.3",
Expand All @@ -88,6 +88,7 @@
"@kne/axios-fetch": "^1.2.0",
"@kne/column-split": "^1.0.5",
"@kne/count-down": "^0.2.2",
"@kne/ensure-slash": "^0.1.0",
"@kne/is-empty": "^1.0.1",
"@kne/json-view": "^0.1.1",
"@kne/react-box": "^0.1.9",
Expand All @@ -99,6 +100,7 @@
"@kne/use-control-value": "^0.1.9",
"@kne/use-ref-callback": "^0.1.2",
"@kne/use-refer-navigate": "^1.0.0",
"dayjs": "^1.11.21",
"md5": "^2.3.0",
"xlsx": "^0.18.5"
}
Expand Down
129 changes: 129 additions & 0 deletions prompts/prompts-remote-components/国际化.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,139 @@ const columns = getColumns({formatMessage});
formatMessage({ id: 'KeyWithParam' }, { name: value })
```

### 6. 两种可复用国际化模式

#### 模式一:提供 `locale` 的 function(常用于枚举国际化)

适用场景:枚举、静态配置、非 React 组件函数等不能直接使用 `useIntl` 的地方。参考 `EnumLoader`。

核心做法是在 `withLocale.js` 中额外导出 `createFormatMessage(locale)`,由普通函数接收 `locale` 后创建当前语言的 `formatMessage`:

```javascript
import { createWithIntlProvider, createIntl } from '@kne/react-intl';
import zhCN from './locale/zh-CN';
import enUS from './locale/en-US';

const withLocale = createWithIntlProvider({
defaultLocale: 'zh-CN',
messages: {
'zh-CN': zhCN,
'en-US': enUS
},
namespace: 'ai-talent-saas'
});

export const createFormatMessage = locale => {
const { formatMessage } = createIntl({
locale,
messages: {
'zh-CN': zhCN,
'en-US': enUS
},
namespace: 'ai-talent-saas'
});
return formatMessage;
};

export default withLocale;
```

枚举函数只负责接收 `locale` 并返回国际化后的枚举项,`value` 保持业务值稳定,只翻译 `description`:

```javascript
import { createFormatMessage } from './withLocale';

const employeeStatus = ({ locale }) => {
const formatMessage = createFormatMessage(locale);
return [
{ description: formatMessage({ id: 'enumLoader.employeeStatusActive' }), value: 'ACTIVE' },
{ description: formatMessage({ id: 'enumLoader.employeeStatusResign' }), value: 'RESIGN' }
];
};

export default employeeStatus;
```

使用时由调用方传入当前语言:

```javascript
const statusList = enums.employeeStatus({ locale });
```

#### 模式二:将国际化内容作为 JSX(常用于配置项、列、卡片内容)

适用场景:`label`、`title`、`content` 等字段支持 `ReactNode`,并且这些配置可能在普通函数中返回,不能直接调用 Hook。参考 `TenantUserPlugin`。

核心做法是在 `withLocale.js` 中额外导出一个被 `withLocale` 包裹的 `FormatMessage` 组件:

```javascript
import { createWithIntlProvider, useIntl } from '@kne/react-intl';
import zhCN from './locale/zh-CN';
import enUS from './locale/en-US';

const withLocale = createWithIntlProvider({
defaultLocale: 'zh-CN',
messages: {
'zh-CN': zhCN,
'en-US': enUS
},
namespace: 'ai-talent-saas'
});

export const FormatMessage = withLocale(props => {
const { formatMessage } = useIntl();
return formatMessage(props);
});

export default withLocale;
```

普通配置函数中直接把翻译内容作为 JSX 返回:

```javascript
import withLocale, { FormatMessage } from './withLocale';

export const personalCard = ({ moreInfo }) => {
return [
...moreInfo,
{
key: 'position',
label: <FormatMessage id="tenantUser.position" />,
content: 'xxx'
}
];
};

export const getUserListColumns = ({ columns }) => {
return [
...columns,
{
title: <FormatMessage id="tenantUser.position" />,
name: 'options.position'
}
];
};
```

如果组件内部本身需要立即得到字符串(如表单 `label`、按钮文本、错误信息、modal 标题),仍然在被 `withLocale` 包裹的组件内使用 `useIntl`:

```javascript
const TenantUserPlugin = createWithRemoteLoader({...})(
withLocale(({ remoteModules, ...props }) => {
const { formatMessage } = useIntl();
return <Button>{formatMessage({ id: 'tenantUser.addPosition' })}</Button>;
})
);
```

## 四、注意事项

1. **所有使用 `useIntl` 的组件必须用 `withLocale` 包裹**
2. **`getColumns` 等工具函数通过参数接收 `formatMessage`,不使用 `useIntl`**
3. **语言包中避免重复的 key**,命名规则:`模块名 + 功能名`,如 `UserName`、`UserRole`
4. **`createWithRemoteLoader` 创建的组件内部使用 useIntl 时,外层需要重命名并用 withLocale 包裹**
5. **枚举/普通函数国际化优先使用 `createFormatMessage(locale)` 模式**,函数入参显式接收 `locale`,返回值中只翻译展示字段
6. **配置项需要返回 ReactNode 时使用 `<FormatMessage id="..." />` 模式**,不要在普通函数中直接调用 `useIntl`

---

Expand Down Expand Up @@ -177,6 +304,8 @@ const getColumns = ({formatMessage}) => {
- [ ] `getColumns` 等工具函数通过参数接收 `formatMessage`
- [ ] 语言包中无重复 key
- [ ] `createWithRemoteLoader` 组件必须使用 `createWithRemoteLoader({...})(withLocale(...))` 链式调用格式,**禁止**先定义中间变量再包裹
- [ ] 枚举/普通函数如需国际化,使用 `createFormatMessage(locale)`,不要在函数中使用 Hook
- [ ] `label`、`title`、`content` 等支持 JSX 的配置项可使用 `<FormatMessage id="..." />`

### 5. 最后检查
运行命令找到所有使用 useIntl 的文件,确保都已正确包裹:
Expand Down
31 changes: 14 additions & 17 deletions src/components/Task/enums.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
const TASK_STATUS_ENUM = [
{ value: 'pending', description: '等待执行', type: 'info' },
{
value: 'running',
description: '执行中',
type: 'progress'
},
{ value: 'waiting', description: '等待操作', type: 'info' },
{
value: 'success',
description: '成功',
type: 'success'
},
{ value: 'failed', description: '失败', type: 'danger' },
{ value: 'canceled', description: '取消' }
];
import { createFormatMessage } from './withLocale';

const enums = { taskStatus: TASK_STATUS_ENUM };
const taskStatus = ({ locale }) => {
const formatMessage = createFormatMessage(locale);
return [
{ description: formatMessage({ id: 'Pending' }), value: 'pending', type: 'info' },
{ description: formatMessage({ id: 'Running' }), value: 'running', type: 'progress' },
{ description: formatMessage({ id: 'Waiting' }), value: 'waiting', type: 'info' },
{ description: formatMessage({ id: 'Success' }), value: 'success', type: 'success' },
{ description: formatMessage({ id: 'Failed' }), value: 'failed', type: 'danger' },
{ description: formatMessage({ id: 'Canceled' }), value: 'canceled' }
];
};

const enums = { taskStatus };

export default enums;
14 changes: 13 additions & 1 deletion src/components/Task/withLocale.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createWithIntlProvider } from '@kne/react-intl';
import { createWithIntlProvider, createIntl } from '@kne/react-intl';
import zhCN from './locale/zh-CN';
import enUS from './locale/en-US';

Expand All @@ -11,4 +11,16 @@ const withLocale = createWithIntlProvider({
namespace: 'components-admin:Task'
});

export const createFormatMessage = locale => {
const { formatMessage } = createIntl({
locale,
messages: {
'zh-CN': zhCN,
'en-US': enUS
},
namespace: 'components-admin:Task'
});
return formatMessage;
};

export default withLocale;
7 changes: 3 additions & 4 deletions src/components/Tenant/CompanyInfo/DevelopmentHistory/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createWithRemoteLoader } from '@kne/remote-loader';
import { Empty } from 'antd';
import dayjs from 'dayjs';
import Timeline from '@kne/timeline';
import ensureSlash from '@kne/ensure-slash';
import withLocale from '../../withLocale';
import style from '../style.module.scss';

Expand All @@ -16,7 +17,7 @@ const resolveImageSrc = (id, origin) => {
if (typeof id === 'string' && /^https?:\/\//i.test(id)) {
return id;
}
return `${origin}/api/v1/static/file-id/${id}`;
return `${ensureSlash(origin) || window.location.origin}/api/v1/static/file-id/${id}`;
};

const DevelopmentHistory = createWithRemoteLoader({
Expand All @@ -31,16 +32,14 @@ const DevelopmentHistory = createWithRemoteLoader({
return <Empty />;
}

const origin = (typeof staticUrl === 'string' && staticUrl) || (typeof window !== 'undefined' ? window.location.origin : '');

const timelineData = list.map(item => {
const row = {
title: formatTime(item.time),
content: item.event
};
if (item.images && item.images.length > 0) {
row.images = item.images.map(id => ({
src: resolveImageSrc(id, origin)
src: resolveImageSrc(id, staticUrl)
}));
}
if (item.extra) {
Expand Down
1 change: 1 addition & 0 deletions src/components/Tenant/Tenant.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const Tenant = createWithRemoteLoader({
navigation={{
base: `${baseUrl}/tenant`,
showIndex: false,
defaultTitle: navigation.defaultTitle,
list: [
...(navigation.list || []),
{
Expand Down