Conversation
✅ Deploy Preview for kcloud-platform-iot ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Reviewer's Guide在整个 UI 中引入国际化(i18n),在现有权限检查的基础上增加基于 scope 的访问控制,并在布局、路由、错误处理和 API 请求中贯通 locale 信息,同时新增 zh-CN 和 en-US 语言文件作为支撑。 支持本地化的 API 请求与错误处理的时序图sequenceDiagram
actor User
participant Browser
participant FrontendApp
participant RequestInterceptor
participant BackendAPI
User->>Browser: Trigger API call (e.g., submit login form)
Browser->>FrontendApp: onSubmit(values)
FrontendApp->>RequestInterceptor: request(config)
RequestInterceptor->>RequestInterceptor: getLocale()
RequestInterceptor->>RequestInterceptor: headers.Language = locale
RequestInterceptor->>RequestInterceptor: add Authorization from getAccessToken()
RequestInterceptor->>BackendAPI: send HTTP request with headers
BackendAPI-->>RequestInterceptor: HTTP response (success or error)
RequestInterceptor-->>FrontendApp: response or error
alt error response
FrontendApp->>FrontendApp: errorHandler(error)
FrontendApp->>FrontendApp: t(error.key) via getIntl()
FrontendApp->>Browser: show localized error message
Browser-->>User: Localized toast/notification
else success
FrontendApp->>Browser: render success UI (localized)
Browser-->>User: Localized page content
end
初始状态、访问 scope 以及支持本地化的辅助方法的类图classDiagram
class InitialState {
string username
string avatar
string[] permissions
string[] scopes
}
class AccessContext {
+boolean canMenuGetDetail
+boolean canMenuModify
+boolean canMenuRemove
+boolean canMenuSave
+boolean canDeptGetDetail
+boolean canDeptModify
+boolean canDeptRemove
+boolean canDeptSave
+boolean canRoleGetDetail
+boolean canRoleModify
+boolean canRoleRemove
+boolean canRoleSave
+boolean canUserGetDetail
+boolean canUserModify
+boolean canUserRemove
+boolean canUserSave
+boolean canOssUpload
+boolean canOssGetDetail
+boolean canOssModify
+boolean canOssRemove
+boolean canOssSave
+boolean canOssLogExport
+boolean canDeviceGetDetail
+boolean canDeviceModify
+boolean canDeviceRemove
+boolean canDeviceSave
+boolean canProductGetDetail
+boolean canProductModify
+boolean canProductRemove
+boolean canProductSave
+boolean canThingModelGetDetail
+boolean canThingModelModify
+boolean canThingModelRemove
+boolean canThingModelSave
+boolean canProductCategoryGetDetail
+boolean canProductCategoryModify
+boolean canProductCategoryRemove
+boolean canProductCategorySave
+boolean canOperateLogGetDetail
+boolean canOperateLogExport
+boolean canNoticeLogGetDetail
+boolean canNoticeLogExport
+boolean canLoginLogExport
}
class AccessFactory {
+createAccessContext(initialState InitialState) AccessContext
}
class LocaleService {
+string currentLocale
+string t(id string, values Record_string_any)
+string getLocale()
+void setLocale(locale string)
}
class RequestInterceptorConfig {
+number timeout
+ErrorConfig errorConfig
+RequestInterceptor[] requestInterceptors
+ResponseInterceptor[] responseInterceptors
}
class ErrorConfig {
+void errorHandler(error any)
}
class RequestInterceptor {
+onRequest(config any) any
}
class LayoutConfigRuntime {
+string title
+any headerContentRender()
+any[] actionsRender()
+AvatarProps avatarProps
+MenuItem[] menu
}
InitialState --> AccessFactory : input
AccessFactory --> AccessContext : create
LocaleService <.. LayoutConfigRuntime : uses t
LocaleService <.. ErrorConfig : uses t
LocaleService <.. RequestInterceptor : uses getLocale
RequestInterceptorConfig *-- ErrorConfig
RequestInterceptorConfig *-- RequestInterceptor
LayoutConfigRuntime ..> InitialState : reads
class RouteItem {
string name
string title
string path
string icon
RouteItem[] routes
}
LayoutConfigRuntime --> RouteItem : builds dynamic menu
class LoginPage {
+LoginType loginType
+void onSubmit(values any)
+void sendMobileCaptcha(mobile string)
+void sendMailCaptcha(mail string)
}
LoginPage ..> LocaleService : uses useIntl
LoginPage ..> AccessContext : reads permissions and scopes via useAccess
LoginPage ..> RouteItem : navigates using history
文件级变更
Tips and commandsInteracting with Sourcery
Customizing Your Experience打开你的 dashboard 以:
Getting HelpOriginal review guide in EnglishReviewer's GuideIntroduce internationalization (i18n) across the UI, add scope-based access control on top of existing permission checks, and wire locale information through layout, routing, error handling, and API requests, backed by new zh-CN and en-US locale files. Sequence diagram for locale-aware API request and error handlingsequenceDiagram
actor User
participant Browser
participant FrontendApp
participant RequestInterceptor
participant BackendAPI
User->>Browser: Trigger API call (e.g., submit login form)
Browser->>FrontendApp: onSubmit(values)
FrontendApp->>RequestInterceptor: request(config)
RequestInterceptor->>RequestInterceptor: getLocale()
RequestInterceptor->>RequestInterceptor: headers.Language = locale
RequestInterceptor->>RequestInterceptor: add Authorization from getAccessToken()
RequestInterceptor->>BackendAPI: send HTTP request with headers
BackendAPI-->>RequestInterceptor: HTTP response (success or error)
RequestInterceptor-->>FrontendApp: response or error
alt error response
FrontendApp->>FrontendApp: errorHandler(error)
FrontendApp->>FrontendApp: t(error.key) via getIntl()
FrontendApp->>Browser: show localized error message
Browser-->>User: Localized toast/notification
else success
FrontendApp->>Browser: render success UI (localized)
Browser-->>User: Localized page content
end
Class diagram for initial state, access scopes, and locale-aware helpersclassDiagram
class InitialState {
string username
string avatar
string[] permissions
string[] scopes
}
class AccessContext {
+boolean canMenuGetDetail
+boolean canMenuModify
+boolean canMenuRemove
+boolean canMenuSave
+boolean canDeptGetDetail
+boolean canDeptModify
+boolean canDeptRemove
+boolean canDeptSave
+boolean canRoleGetDetail
+boolean canRoleModify
+boolean canRoleRemove
+boolean canRoleSave
+boolean canUserGetDetail
+boolean canUserModify
+boolean canUserRemove
+boolean canUserSave
+boolean canOssUpload
+boolean canOssGetDetail
+boolean canOssModify
+boolean canOssRemove
+boolean canOssSave
+boolean canOssLogExport
+boolean canDeviceGetDetail
+boolean canDeviceModify
+boolean canDeviceRemove
+boolean canDeviceSave
+boolean canProductGetDetail
+boolean canProductModify
+boolean canProductRemove
+boolean canProductSave
+boolean canThingModelGetDetail
+boolean canThingModelModify
+boolean canThingModelRemove
+boolean canThingModelSave
+boolean canProductCategoryGetDetail
+boolean canProductCategoryModify
+boolean canProductCategoryRemove
+boolean canProductCategorySave
+boolean canOperateLogGetDetail
+boolean canOperateLogExport
+boolean canNoticeLogGetDetail
+boolean canNoticeLogExport
+boolean canLoginLogExport
}
class AccessFactory {
+createAccessContext(initialState InitialState) AccessContext
}
class LocaleService {
+string currentLocale
+string t(id string, values Record_string_any)
+string getLocale()
+void setLocale(locale string)
}
class RequestInterceptorConfig {
+number timeout
+ErrorConfig errorConfig
+RequestInterceptor[] requestInterceptors
+ResponseInterceptor[] responseInterceptors
}
class ErrorConfig {
+void errorHandler(error any)
}
class RequestInterceptor {
+onRequest(config any) any
}
class LayoutConfigRuntime {
+string title
+any headerContentRender()
+any[] actionsRender()
+AvatarProps avatarProps
+MenuItem[] menu
}
InitialState --> AccessFactory : input
AccessFactory --> AccessContext : create
LocaleService <.. LayoutConfigRuntime : uses t
LocaleService <.. ErrorConfig : uses t
LocaleService <.. RequestInterceptor : uses getLocale
RequestInterceptorConfig *-- ErrorConfig
RequestInterceptorConfig *-- RequestInterceptor
LayoutConfigRuntime ..> InitialState : reads
class RouteItem {
string name
string title
string path
string icon
RouteItem[] routes
}
LayoutConfigRuntime --> RouteItem : builds dynamic menu
class LoginPage {
+LoginType loginType
+void onSubmit(values any)
+void sendMobileCaptcha(mobile string)
+void sendMailCaptcha(mail string)
}
LoginPage ..> LocaleService : uses useIntl
LoginPage ..> AccessContext : reads permissions and scopes via useAccess
LoginPage ..> RouteItem : navigates using history
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (8)
WalkthroughThis PR introduces internationalization (i18n) support to the UI configuration and pages by replacing hardcoded Chinese strings with translatable keys. It also implements scope-based permission checking by combining existing permissions with additional scopes (read/write/etc.). New English and Chinese locale files are added alongside updated UI components. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Poem
✨ Finishing Touches
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 OpenGrep (1.16.2)ui/config/config.ts┌──────────────┐ �[32m✔�[39m �[1mOpengrep OSS�[0m �[1m Loading rules from local config...�[0m ui/src/access.ts┌──────────────┐ �[32m✔�[39m �[1mOpengrep OSS�[0m �[1m Loading rules from local config...�[0m ui/config/routes.ts┌──────────────┐ �[32m✔�[39m �[1mOpengrep OSS�[0m �[1m Loading rules from local config...�[0m
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Review Summary by QodoAdd frontend internationalization with i18n and scope-based access control
WalkthroughsDescription• Add frontend internationalization (i18n) support with Chinese and English locales • Replace hardcoded UI strings with i18n keys across routes, login, and app configuration • Implement UMI plugin-locale integration with language switcher component • Enhance access control by adding scopes validation alongside permissions Diagramflowchart LR
A["UMI Config"] -->|"locale settings"| B["i18n Plugin"]
C["Routes"] -->|"i18n keys"| B
D["Locale Files"] -->|"zh-CN, en-US"| B
B -->|"translations"| E["UI Components"]
F["SelectLang"] -->|"language switch"| E
G["Access Control"] -->|"permissions + scopes"| H["Authorization"]
I["App Runtime"] -->|"integrates i18n"| E
File Changes1. ui/config/config.ts
|
Code Review by Qodo
1. Headers not persisted
|
There was a problem hiding this comment.
嗨,我发现了 4 个问题,并给出了一些整体性的反馈:
- 在
src/app.tsx中,layout在t被定义为const函数表达式之前就引用了它,这会在运行时抛错;建议将t辅助函数移动到layout之上,或者直接在layout内联使用getIntl().formatMessage。 - 在
src/access.ts中的 scope 校验多处重复使用了scopes?.includes('read'/'write');可以考虑抽成一些小的辅助函数(例如hasReadScope、hasWriteScope)或者配置映射表,这样可以减少重复代码以及 scope 使用不一致的风险。 - 在
src/pages/Sys/Log/login.tsx中,getLoginType和下拉框的options里仍然为部分登录类型(例如mail、authorization_code)保留了硬编码的中文文案;请将这些替换为对应的 i18n key,以确保登录日志列表在所有语言环境下都能完整本地化。
给予 AI Agent 的提示词
请根据本次代码评审中的评论进行修改:
## 总体评论
- 在 `src/app.tsx` 中,`layout` 在 `t` 被定义为 `const` 函数表达式之前就引用了它,这会在运行时抛错;建议将 `t` 辅助函数移动到 `layout` 之上,或者直接在 `layout` 内联使用 `getIntl().formatMessage`。
- 在 `src/access.ts` 中的 scope 校验多处重复使用了 `scopes?.includes('read'/'write')`;可以考虑抽成一些小的辅助函数(例如 `hasReadScope`、`hasWriteScope`)或者配置映射表,这样可以减少重复代码以及 scope 使用不一致的风险。
- 在 `src/pages/Sys/Log/login.tsx` 中,`getLoginType` 和下拉框的 `options` 里仍然为部分登录类型(例如 `mail`、`authorization_code`)保留了硬编码的中文文案;请将这些替换为对应的 i18n key,以确保登录日志列表在所有语言环境下都能完整本地化。
## 具体评论
### 评论 1
<location path="ui/src/pages/Sys/Log/login.tsx" line_range="36-39" />
<code_context>
const getLoginType = (type: string) => {
return {
- 'username_password': '用户名密码登录',
- 'mobile': '手机号登录',
+ 'username_password': t('login.usernamePassword'),
+ 'mobile': t('login.mobile'),
'mail': '邮箱登录',
'authorization_code': '授权码登录',
}[type]
</code_context>
<issue_to_address>
**suggestion:** 登录类型映射目前只做了部分国际化。
`getLoginType` 现在对 `username_password` 和 `mobile` 使用了 `t(...)`,但 `mail` 和 `authorization_code` 仍然是硬编码的中文,这会导致在非 zh 语言环境下出现混合语言的界面。请也将它们改为使用对应的翻译 key(例如 `t('login.mail')` 以及适合 `authorization_code` 的翻译 key)。
建议实现:
```typescript
const getLoginType = (type: string) => {
return {
'username_password': t('login.usernamePassword'),
'mobile': t('login.mobile'),
'mail': t('login.mail'),
'authorization_code': t('login.authorizationCode'),
}[type]
```
1. 确保在 i18n 消息配置中,为所有支持的语言都定义了 `login.mail` 和 `login.authorizationCode` 这两个 key。
2. 如果该文件(或其他文件)中还有其他地方直接硬编码了 `"邮箱登录"` 或 `"授权码登录"`(例如在筛选/下拉框的 `options` label 中),也请统一改为使用 `t('login.mail')` 和 `t('login.authorizationCode')`,以保持一致性。
</issue_to_address>
### 评论 2
<location path="ui/src/pages/Sys/Log/login.tsx" line_range="176" />
<code_context>
mode: 'single',
options: [
{
- value: "username_password",
</code_context>
<issue_to_address>
**suggestion:** 登录类型选项列表混用了已翻译和硬编码的文案。
第一个选项已经使用了 `t('login.usernamePassword')`,但其他选项(例如 `mail`)仍然是硬编码的中文 label。请将所有选项的 label 都改为使用 locale 文件中的对应翻译 key,以实现一致的本地化。
建议实现:
```typescript
return {
'username_password': t('login.usernamePassword'),
'mobile': t('login.mobile'),
'mail': t('login.mail'),
'authorization_code': t('login.authorizationCode'),
}[type]
```
```typescript
{
value: 'mail',
label: t('login.mail'),
```
我目前只能看到 options 数组的一部分。为了完全保持一致性,你应该:
1. 将剩余的登录类型选项(例如 `mobile`、`authorization_code` 等)的 label 也统一改为 `label: t('login.xxx')`,并与 `getLoginType` 中使用的 key 保持一致。
2. 确保相应的翻译 key(`login.mail`、`login.authorizationCode` 以及你新增的其它 key)已经在所有支持语言的 locale 文件中正确定义。
</issue_to_address>
### 评论 3
<location path="ui/src/app.tsx" line_range="261-264" />
<code_context>
// 请求拦截
- requestInterceptors: [
+ requestInterceptors: [
async (config: any) => {
const headers = config.headers ? config.headers : [];
+ // 国际化:携带语言到后端(优先使用 umi plugin-locale 的 current locale)
+ const { getLocale } = require('@@/exports');
+ const locale = getLocale?.() || 'zh-CN';
+ if (locale) {
</code_context>
<issue_to_address>
**suggestion:** 在请求拦截器中使用动态 `require` 可能带来不必要的每次请求开销。
在每个请求里调用 `require('@@/exports')` 是没有必要的,即使有缓存,也可能干扰打包和类型检查。建议在文件顶部使用静态导入 `getLocale`,并在拦截器中直接使用。
建议实现:
```typescript
const headers = config.headers ? config.headers : [];
// 国际化:携带语言到后端(优先使用 umi plugin-locale 的 current locale)
const locale = getLocale() || 'zh-CN';
```
为了完整应用这一改动,请在 `ui/src/app.tsx` 顶部增加静态导入:
```ts
import { getLocale } from '@@/exports';
```
将其放在其他 import 附近(例如已有的 Umi 相关 import 旁边)。同时确保文件中不存在与 `getLocale` 冲突的其他声明。
</issue_to_address>
### 评论 4
<location path="ui/src/app.tsx" line_range="216" />
<code_context>
};
};
+const t = (id: string, values?: Record<string, any>) => {
+ // 新写法:在非 React 组件/Hook 环境下(如 request errorHandler)使用 getIntl()
+ // getIntl 来自 umi plugin-locale(@@/exports 导出)
</code_context>
<issue_to_address>
**issue (complexity):** 建议将与语言环境相关的辅助函数移动到顶层 import,提前定义 `t`,并简化拦截器中对 headers 的处理逻辑,在保留 i18n 行为的前提下让代码更清晰、更具声明性。
在保持所有新增 i18n 行为不变的前提下,可以通过以下方式降低复杂度:
1. 在顶层统一导入 locale 工具,而不是在函数内部使用 `require`
2. 在使用 `t` 之前先定义它(并保持它为简单纯函数辅助)
3. 清理拦截器中对 headers 的处理逻辑
### 1. 用顶层 import 替代动态 `require`
与其写成:
```ts
const t = (id: string, values?: Record<string, any>) => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { getIntl } = require('@@/exports');
return getIntl().formatMessage({ id }, values);
};
```
以及:
```ts
requestInterceptors: [
async (config: any) => {
const headers = config.headers ? config.headers : [];
// 国际化:携带语言到后端(优先使用 umi plugin-locale 的 current locale)
const { getLocale } = require('@@/exports');
const locale = getLocale?.() || 'zh-CN';
if (locale) {
headers['Language'] = locale;
}
// ...
},
],
```
不如把依赖注入统一放在文件顶部,复用这些工具函数:
```ts
// 顶部 import 区域
import { history, SelectLang, getIntl, getLocale } from '@@/exports';
// 顶部定义 t,避免使用前未定义
const t = (id: string, values?: Record<string, any>) =>
getIntl().formatMessage({ id }, values);
```
拦截器就可以写得更简单、更具声明性:
```ts
requestInterceptors: [
async (config: any) => {
const headers = config.headers ?? {};
const locale = getLocale?.() || 'zh-CN';
if (locale) {
headers['Language'] = locale;
}
const accessToken = getAccessToken();
if (!headers['Skip-Token'] && accessToken) {
headers['Authorization'] = `Bearer ${accessToken}`;
}
config.headers = headers;
return config;
},
(error: any) => error,
];
```
### 2. 在 `layout` 使用 `t` 之前定义它
现在的代码中,`layout` 在 `const t` 声明之前就引用了它,这既让人困惑,也可能比较脆弱。按照上面方式移动并简化 `t` 之后:
```ts
// 顶部
const t = (id: string, values?: Record<string, any>) =>
getIntl().formatMessage({ id }, values);
export const layout: RunTimeLayoutConfig = ({ initialState }: any) => {
return {
title: t('app.title'),
// ...
avatarProps: {
// ...
render: (_props, dom) => (
<Dropdown
menu={{
items: [
{
key: 'logout',
icon: <LogoutOutlined />,
label: t('user.logout'),
onClick: async () => {
// ...
},
},
],
}}
>
{dom}
</Dropdown>
),
},
};
};
```
这样既保持了你新增的 i18n 行为完全不变,同时:
- 移除了运行时的 `require` 调用;
- 明确暴露了依赖(`getIntl`、`getLocale`);
- 避免了在 `t` 定义之前使用它;
- 简化了请求拦截器中对 header 的处理逻辑。
</issue_to_address>帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据反馈改进评审质量。
Original comment in English
Hey - I've found 4 issues, and left some high level feedback:
- In
src/app.tsx,layoutreferencestbefore it is defined as aconstfunction expression, which will throw at runtime; consider moving thethelper abovelayoutor inlininggetIntl().formatMessageinsidelayoutinstead. - The scope checks in
src/access.tsrepeatscopes?.includes('read'/'write')across many entries; factoring these into small helpers (e.g.hasReadScope,hasWriteScope) or a config map would reduce duplication and the risk of inconsistent scope usage. - In
src/pages/Sys/Log/login.tsx,getLoginTypeand the selectoptionsstill contain hard-coded Chinese labels for some login types (e.g.mail,authorization_code); replace these with the corresponding i18n keys to keep the login log list fully localized.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `src/app.tsx`, `layout` references `t` before it is defined as a `const` function expression, which will throw at runtime; consider moving the `t` helper above `layout` or inlining `getIntl().formatMessage` inside `layout` instead.
- The scope checks in `src/access.ts` repeat `scopes?.includes('read'/'write')` across many entries; factoring these into small helpers (e.g. `hasReadScope`, `hasWriteScope`) or a config map would reduce duplication and the risk of inconsistent scope usage.
- In `src/pages/Sys/Log/login.tsx`, `getLoginType` and the select `options` still contain hard-coded Chinese labels for some login types (e.g. `mail`, `authorization_code`); replace these with the corresponding i18n keys to keep the login log list fully localized.
## Individual Comments
### Comment 1
<location path="ui/src/pages/Sys/Log/login.tsx" line_range="36-39" />
<code_context>
const getLoginType = (type: string) => {
return {
- 'username_password': '用户名密码登录',
- 'mobile': '手机号登录',
+ 'username_password': t('login.usernamePassword'),
+ 'mobile': t('login.mobile'),
'mail': '邮箱登录',
'authorization_code': '授权码登录',
}[type]
</code_context>
<issue_to_address>
**suggestion:** The login type mapping is only partially internationalized.
`getLoginType` now uses `t(...)` for `username_password` and `mobile`, but `mail` and `authorization_code` remain hard-coded in Chinese, causing mixed-language UI in non-zh locales. Please update those to use translation keys as well (e.g. `t('login.mail')` and an appropriate key for `authorization_code`).
Suggested implementation:
```typescript
const getLoginType = (type: string) => {
return {
'username_password': t('login.usernamePassword'),
'mobile': t('login.mobile'),
'mail': t('login.mail'),
'authorization_code': t('login.authorizationCode'),
}[type]
```
1. Ensure that the i18n message catalog defines the keys `login.mail` and `login.authorizationCode` for all supported locales.
2. If there are other places in this file (or elsewhere) where `"邮箱登录"` or `"授权码登录"` are hard-coded (e.g., in `options` labels for filters or selects), update those to use `t('login.mail')` and `t('login.authorizationCode')` as well for consistency.
</issue_to_address>
### Comment 2
<location path="ui/src/pages/Sys/Log/login.tsx" line_range="176" />
<code_context>
mode: 'single',
options: [
{
- value: "username_password",
</code_context>
<issue_to_address>
**suggestion:** Login type options list mixes translated and hard-coded labels.
The first option now uses `t('login.usernamePassword')`, but others (e.g. `mail`) still have hard-coded Chinese labels. Please update all option labels to use the corresponding translation keys from the locale files for consistent localization.
Suggested implementation:
```typescript
return {
'username_password': t('login.usernamePassword'),
'mobile': t('login.mobile'),
'mail': t('login.mail'),
'authorization_code': t('login.authorizationCode'),
}[type]
```
```typescript
{
value: 'mail',
label: t('login.mail'),
```
I only see part of the options array. For full consistency, you should:
1. Update any remaining login type options (e.g. `mobile`, `authorization_code`, etc.) to use `label: t('login.xxx')` instead of hard-coded strings, matching the keys used in `getLoginType`.
2. Ensure the corresponding translation keys (`login.mail`, `login.authorizationCode`, and any others you introduce) exist and are properly defined in your locale files for all supported languages.
</issue_to_address>
### Comment 3
<location path="ui/src/app.tsx" line_range="261-264" />
<code_context>
// 请求拦截
- requestInterceptors: [
+ requestInterceptors: [
async (config: any) => {
const headers = config.headers ? config.headers : [];
+ // 国际化:携带语言到后端(优先使用 umi plugin-locale 的 current locale)
+ const { getLocale } = require('@@/exports');
+ const locale = getLocale?.() || 'zh-CN';
+ if (locale) {
</code_context>
<issue_to_address>
**suggestion:** Dynamic `require` in the request interceptor may add unnecessary per-request overhead.
Calling `require('@@/exports')` on every request is unnecessary, even if cached, and may interfere with bundling and typing. Prefer a static import of `getLocale` at the top of the file and use it directly in the interceptor.
Suggested implementation:
```typescript
const headers = config.headers ? config.headers : [];
// 国际化:携带语言到后端(优先使用 umi plugin-locale 的 current locale)
const locale = getLocale() || 'zh-CN';
```
To fully apply this change, also add a static import at the top of `ui/src/app.tsx`:
```ts
import { getLocale } from '@@/exports';
```
Place this alongside the other imports (e.g., near existing Umi-related imports). Ensure there are no conflicting declarations of `getLocale` in this file.
</issue_to_address>
### Comment 4
<location path="ui/src/app.tsx" line_range="216" />
<code_context>
};
};
+const t = (id: string, values?: Record<string, any>) => {
+ // 新写法:在非 React 组件/Hook 环境下(如 request errorHandler)使用 getIntl()
+ // getIntl 来自 umi plugin-locale(@@/exports 导出)
</code_context>
<issue_to_address>
**issue (complexity):** Consider moving locale helpers to top-level imports, defining `t` earlier, and simplifying the interceptor headers to keep i18n behavior while making the code clearer and more declarative.
You can keep all the new i18n behavior while reducing complexity by:
1. Importing locale utilities once at the top-level instead of `require` inside functions
2. Defining `t` before it’s used (and keeping it a simple pure helper)
3. Cleaning up the headers handling in the interceptor
### 1. Replace dynamic `require` with top-level imports
Instead of:
```ts
const t = (id: string, values?: Record<string, any>) => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { getIntl } = require('@@/exports');
return getIntl().formatMessage({ id }, values);
};
```
and
```ts
requestInterceptors: [
async (config: any) => {
const headers = config.headers ? config.headers : [];
// 国际化:携带语言到后端(优先使用 umi plugin-locale 的 current locale)
const { getLocale } = require('@@/exports');
const locale = getLocale?.() || 'zh-CN';
if (locale) {
headers['Language'] = locale;
}
// ...
},
],
```
move the dependency wiring to the top of the file and reuse it:
```ts
// 顶部 import 区域
import { history, SelectLang, getIntl, getLocale } from '@@/exports';
// 顶部定义 t,避免使用前未定义
const t = (id: string, values?: Record<string, any>) =>
getIntl().formatMessage({ id }, values);
```
Then the interceptor becomes simpler and declarative:
```ts
requestInterceptors: [
async (config: any) => {
const headers = config.headers ?? {};
const locale = getLocale?.() || 'zh-CN';
if (locale) {
headers['Language'] = locale;
}
const accessToken = getAccessToken();
if (!headers['Skip-Token'] && accessToken) {
headers['Authorization'] = `Bearer ${accessToken}`;
}
config.headers = headers;
return config;
},
(error: any) => error,
];
```
### 2. Define `t` before `layout` uses it
Right now `layout` references `t` before the `const t` declaration, which is confusing and can be fragile. With `t` moved and simplified as above:
```ts
// 顶部
const t = (id: string, values?: Record<string, any>) =>
getIntl().formatMessage({ id }, values);
export const layout: RunTimeLayoutConfig = ({ initialState }: any) => {
return {
title: t('app.title'),
// ...
avatarProps: {
// ...
render: (_props, dom) => (
<Dropdown
menu={{
items: [
{
key: 'logout',
icon: <LogoutOutlined />,
label: t('user.logout'),
onClick: async () => {
// ...
},
},
],
}}
>
{dom}
</Dropdown>
),
},
};
};
```
This keeps the i18n behavior exactly as you added it, but:
- removes runtime `require` calls,
- makes dependencies (`getIntl`, `getLocale`) explicit,
- avoids using `t` before it’s defined, and
- simplifies the request interceptor’s header logic.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| return { | ||
| 'username_password': '用户名密码登录', | ||
| 'mobile': '手机号登录', | ||
| 'username_password': t('login.usernamePassword'), | ||
| 'mobile': t('login.mobile'), | ||
| 'mail': '邮箱登录', |
There was a problem hiding this comment.
suggestion: 登录类型映射目前只做了部分国际化。
getLoginType 现在对 username_password 和 mobile 使用了 t(...),但 mail 和 authorization_code 仍然是硬编码的中文,这会导致在非 zh 语言环境下出现混合语言的界面。请也将它们改为使用对应的翻译 key(例如 t('login.mail') 以及适合 authorization_code 的翻译 key)。
建议实现:
const getLoginType = (type: string) => {
return {
'username_password': t('login.usernamePassword'),
'mobile': t('login.mobile'),
'mail': t('login.mail'),
'authorization_code': t('login.authorizationCode'),
}[type]- 确保在 i18n 消息配置中,为所有支持的语言都定义了
login.mail和login.authorizationCode这两个 key。 - 如果该文件(或其他文件)中还有其他地方直接硬编码了
"邮箱登录"或"授权码登录"(例如在筛选/下拉框的optionslabel 中),也请统一改为使用t('login.mail')和t('login.authorizationCode'),以保持一致性。
Original comment in English
suggestion: The login type mapping is only partially internationalized.
getLoginType now uses t(...) for username_password and mobile, but mail and authorization_code remain hard-coded in Chinese, causing mixed-language UI in non-zh locales. Please update those to use translation keys as well (e.g. t('login.mail') and an appropriate key for authorization_code).
Suggested implementation:
const getLoginType = (type: string) => {
return {
'username_password': t('login.usernamePassword'),
'mobile': t('login.mobile'),
'mail': t('login.mail'),
'authorization_code': t('login.authorizationCode'),
}[type]- Ensure that the i18n message catalog defines the keys
login.mailandlogin.authorizationCodefor all supported locales. - If there are other places in this file (or elsewhere) where
"邮箱登录"or"授权码登录"are hard-coded (e.g., inoptionslabels for filters or selects), update those to uset('login.mail')andt('login.authorizationCode')as well for consistency.
| @@ -173,8 +175,8 @@ export default () => { | |||
| mode: 'single', | |||
| options: [ | |||
There was a problem hiding this comment.
suggestion: 登录类型选项列表混用了已翻译和硬编码的文案。
第一个选项已经使用了 t('login.usernamePassword'),但其他选项(例如 mail)仍然是硬编码的中文 label。请将所有选项的 label 都改为使用 locale 文件中的对应翻译 key,以实现一致的本地化。
建议实现:
return {
'username_password': t('login.usernamePassword'),
'mobile': t('login.mobile'),
'mail': t('login.mail'),
'authorization_code': t('login.authorizationCode'),
}[type] {
value: 'mail',
label: t('login.mail'),我目前只能看到 options 数组的一部分。为了完全保持一致性,你应该:
- 将剩余的登录类型选项(例如
mobile、authorization_code等)的 label 也统一改为label: t('login.xxx'),并与getLoginType中使用的 key 保持一致。 - 确保相应的翻译 key(
login.mail、login.authorizationCode以及你新增的其它 key)已经在所有支持语言的 locale 文件中正确定义。
Original comment in English
suggestion: Login type options list mixes translated and hard-coded labels.
The first option now uses t('login.usernamePassword'), but others (e.g. mail) still have hard-coded Chinese labels. Please update all option labels to use the corresponding translation keys from the locale files for consistent localization.
Suggested implementation:
return {
'username_password': t('login.usernamePassword'),
'mobile': t('login.mobile'),
'mail': t('login.mail'),
'authorization_code': t('login.authorizationCode'),
}[type] {
value: 'mail',
label: t('login.mail'),I only see part of the options array. For full consistency, you should:
- Update any remaining login type options (e.g.
mobile,authorization_code, etc.) to uselabel: t('login.xxx')instead of hard-coded strings, matching the keys used ingetLoginType. - Ensure the corresponding translation keys (
login.mail,login.authorizationCode, and any others you introduce) exist and are properly defined in your locale files for all supported languages.
| async (config: any) => { | ||
| const headers = config.headers ? config.headers : []; | ||
| // 国际化:携带语言到后端(优先使用 umi plugin-locale 的 current locale) | ||
| const { getLocale } = require('@@/exports'); |
There was a problem hiding this comment.
suggestion: 在请求拦截器中使用动态 require 可能带来不必要的每次请求开销。
在每个请求里调用 require('@@/exports') 是没有必要的,即使有缓存,也可能干扰打包和类型检查。建议在文件顶部使用静态导入 getLocale,并在拦截器中直接使用。
建议实现:
const headers = config.headers ? config.headers : [];
// 国际化:携带语言到后端(优先使用 umi plugin-locale 的 current locale)
const locale = getLocale() || 'zh-CN';为了完整应用这一改动,请在 ui/src/app.tsx 顶部增加静态导入:
import { getLocale } from '@@/exports';将其放在其他 import 附近(例如已有的 Umi 相关 import 旁边)。同时确保文件中不存在与 getLocale 冲突的其他声明。
Original comment in English
suggestion: Dynamic require in the request interceptor may add unnecessary per-request overhead.
Calling require('@@/exports') on every request is unnecessary, even if cached, and may interfere with bundling and typing. Prefer a static import of getLocale at the top of the file and use it directly in the interceptor.
Suggested implementation:
const headers = config.headers ? config.headers : [];
// 国际化:携带语言到后端(优先使用 umi plugin-locale 的 current locale)
const locale = getLocale() || 'zh-CN';To fully apply this change, also add a static import at the top of ui/src/app.tsx:
import { getLocale } from '@@/exports';Place this alongside the other imports (e.g., near existing Umi-related imports). Ensure there are no conflicting declarations of getLocale in this file.
| }; | ||
| }; | ||
|
|
||
| const t = (id: string, values?: Record<string, any>) => { |
There was a problem hiding this comment.
issue (complexity): 建议将与语言环境相关的辅助函数移动到顶层 import,提前定义 t,并简化拦截器中对 headers 的处理逻辑,在保留 i18n 行为的前提下让代码更清晰、更具声明性。
在保持所有新增 i18n 行为不变的前提下,可以通过以下方式降低复杂度:
- 在顶层统一导入 locale 工具,而不是在函数内部使用
require - 在使用
t之前先定义它(并保持它为简单纯函数辅助) - 清理拦截器中对 headers 的处理逻辑
1. 用顶层 import 替代动态 require
与其写成:
const t = (id: string, values?: Record<string, any>) => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { getIntl } = require('@@/exports');
return getIntl().formatMessage({ id }, values);
};以及:
requestInterceptors: [
async (config: any) => {
const headers = config.headers ? config.headers : [];
// 国际化:携带语言到后端(优先使用 umi plugin-locale 的 current locale)
const { getLocale } = require('@@/exports');
const locale = getLocale?.() || 'zh-CN';
if (locale) {
headers['Language'] = locale;
}
// ...
},
],不如把依赖注入统一放在文件顶部,复用这些工具函数:
// 顶部 import 区域
import { history, SelectLang, getIntl, getLocale } from '@@/exports';
// 顶部定义 t,避免使用前未定义
const t = (id: string, values?: Record<string, any>) =>
getIntl().formatMessage({ id }, values);拦截器就可以写得更简单、更具声明性:
requestInterceptors: [
async (config: any) => {
const headers = config.headers ?? {};
const locale = getLocale?.() || 'zh-CN';
if (locale) {
headers['Language'] = locale;
}
const accessToken = getAccessToken();
if (!headers['Skip-Token'] && accessToken) {
headers['Authorization'] = `Bearer ${accessToken}`;
}
config.headers = headers;
return config;
},
(error: any) => error,
];2. 在 layout 使用 t 之前定义它
现在的代码中,layout 在 const t 声明之前就引用了它,这既让人困惑,也可能比较脆弱。按照上面方式移动并简化 t 之后:
// 顶部
const t = (id: string, values?: Record<string, any>) =>
getIntl().formatMessage({ id }, values);
export const layout: RunTimeLayoutConfig = ({ initialState }: any) => {
return {
title: t('app.title'),
// ...
avatarProps: {
// ...
render: (_props, dom) => (
<Dropdown
menu={{
items: [
{
key: 'logout',
icon: <LogoutOutlined />,
label: t('user.logout'),
onClick: async () => {
// ...
},
},
],
}}
>
{dom}
</Dropdown>
),
},
};
};这样既保持了你新增的 i18n 行为完全不变,同时:
- 移除了运行时的
require调用; - 明确暴露了依赖(
getIntl、getLocale); - 避免了在
t定义之前使用它; - 简化了请求拦截器中对 header 的处理逻辑。
Original comment in English
issue (complexity): Consider moving locale helpers to top-level imports, defining t earlier, and simplifying the interceptor headers to keep i18n behavior while making the code clearer and more declarative.
You can keep all the new i18n behavior while reducing complexity by:
- Importing locale utilities once at the top-level instead of
requireinside functions - Defining
tbefore it’s used (and keeping it a simple pure helper) - Cleaning up the headers handling in the interceptor
1. Replace dynamic require with top-level imports
Instead of:
const t = (id: string, values?: Record<string, any>) => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { getIntl } = require('@@/exports');
return getIntl().formatMessage({ id }, values);
};and
requestInterceptors: [
async (config: any) => {
const headers = config.headers ? config.headers : [];
// 国际化:携带语言到后端(优先使用 umi plugin-locale 的 current locale)
const { getLocale } = require('@@/exports');
const locale = getLocale?.() || 'zh-CN';
if (locale) {
headers['Language'] = locale;
}
// ...
},
],move the dependency wiring to the top of the file and reuse it:
// 顶部 import 区域
import { history, SelectLang, getIntl, getLocale } from '@@/exports';
// 顶部定义 t,避免使用前未定义
const t = (id: string, values?: Record<string, any>) =>
getIntl().formatMessage({ id }, values);Then the interceptor becomes simpler and declarative:
requestInterceptors: [
async (config: any) => {
const headers = config.headers ?? {};
const locale = getLocale?.() || 'zh-CN';
if (locale) {
headers['Language'] = locale;
}
const accessToken = getAccessToken();
if (!headers['Skip-Token'] && accessToken) {
headers['Authorization'] = `Bearer ${accessToken}`;
}
config.headers = headers;
return config;
},
(error: any) => error,
];2. Define t before layout uses it
Right now layout references t before the const t declaration, which is confusing and can be fragile. With t moved and simplified as above:
// 顶部
const t = (id: string, values?: Record<string, any>) =>
getIntl().formatMessage({ id }, values);
export const layout: RunTimeLayoutConfig = ({ initialState }: any) => {
return {
title: t('app.title'),
// ...
avatarProps: {
// ...
render: (_props, dom) => (
<Dropdown
menu={{
items: [
{
key: 'logout',
icon: <LogoutOutlined />,
label: t('user.logout'),
onClick: async () => {
// ...
},
},
],
}}
>
{dom}
</Dropdown>
),
},
};
};This keeps the i18n behavior exactly as you added it, but:
- removes runtime
requirecalls, - makes dependencies (
getIntl,getLocale) explicit, - avoids using
tbefore it’s defined, and - simplifies the request interceptor’s header logic.
| async (config: any) => { | ||
| const headers = config.headers ? config.headers : []; | ||
| // 国际化:携带语言到后端(优先使用 umi plugin-locale 的 current locale) | ||
| const { getLocale } = require('@@/exports'); | ||
| const locale = getLocale?.() || 'zh-CN'; | ||
| if (locale) { | ||
| // 若后端使用自定义 header,也可以同时带上(按需保留/改名) | ||
| headers['Language'] = locale; | ||
| } | ||
| const accessToken = getAccessToken() | ||
| if (!headers['Skip-Token'] && accessToken) { | ||
| headers['Authorization'] = `Bearer ${accessToken}` |
There was a problem hiding this comment.
1. Headers not persisted 🐞 Bug ✓ Correctness
The request interceptor writes Language/Authorization into a local headers variable but never assigns it back to config.headers, and it initializes missing headers as an array. Requests that don’t pass headers explicitly can miss auth and locale headers, causing 401s and incorrect server-side localization.
Agent Prompt
### Issue description
The request interceptor currently builds a `headers` variable but doesn’t guarantee it becomes `config.headers`. When `config.headers` is initially missing, `Language` and `Authorization` may not be attached to the outgoing request.
### Issue Context
This impacts any request that doesn’t explicitly set `headers` (e.g. `getUserProfile`).
### Fix Focus Areas
- ui/src/app.tsx[260-275]
### Suggested change
- Replace the array fallback with an object.
- Ensure `config.headers = headers` (or `config.headers = { ...config.headers, ...newHeaders }`) before returning.
- Example pattern:
- `const headers = { ...(config.headers || {}) };`
- mutate `headers`
- `config.headers = headers;`
- `return config;`
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| const scopes = initialState?.scopes || [] | ||
| return { | ||
|
|
||
| canMenuGetDetail: permissions?.includes('sys:menu:detail'), | ||
| canMenuModify: permissions?.includes('sys:menu:modify'), | ||
| canMenuRemove: permissions?.includes('sys:menu:remove'), | ||
| canMenuSave: permissions?.includes('sys:menu:save'), | ||
|
|
||
| canDeptGetDetail: permissions?.includes('sys:dept:detail'), | ||
| canDeptModify: permissions?.includes('sys:dept:modify'), | ||
| canDeptRemove: permissions?.includes('sys:dept:remove'), | ||
| canDeptSave: permissions?.includes('sys:dept:save'), | ||
|
|
||
| canRoleGetDetail: permissions?.includes('sys:role:detail'), | ||
| canRoleModify: permissions?.includes('sys:role:modify'), | ||
| canRoleRemove: permissions?.includes('sys:role:remove'), | ||
| canRoleSave: permissions?.includes('sys:role:save'), | ||
|
|
||
| canUserGetDetail: permissions?.includes('sys:user:detail'), | ||
| canUserModify: permissions?.includes('sys:user:modify'), | ||
| canUserRemove: permissions?.includes('sys:user:remove'), | ||
| canUserSave: permissions?.includes('sys:user:save'), | ||
|
|
||
| canOssUpload: permissions?.includes('sys:oss:upload'), | ||
| canOssGetDetail: permissions?.includes('sys:oss:detail'), | ||
| canOssModify: permissions?.includes('sys:oss:modify'), | ||
| canOssRemove: permissions?.includes('sys:oss:remove'), | ||
| canOssSave: permissions?.includes('sys:oss:save'), | ||
|
|
||
| canOssLogExport: permissions?.includes('sys:oss-log:export'), | ||
|
|
||
| canDeviceGetDetail: permissions?.includes('iot:device:detail'), | ||
| canDeviceModify: permissions?.includes('iot:device:modify'), | ||
| canDeviceRemove: permissions?.includes('iot:device:remove'), | ||
| canDeviceSave: permissions?.includes('iot:device:save'), | ||
|
|
||
| canProductGetDetail: permissions?.includes('iot:product:detail'), | ||
| canProductModify: permissions?.includes('iot:product:modify'), | ||
| canProductRemove: permissions?.includes('iot:product:remove'), | ||
| canProductSave: permissions?.includes('iot:product:save'), | ||
|
|
||
| canThingModelGetDetail: permissions?.includes('iot:thing-model:detail'), | ||
| canThingModelModify: permissions?.includes('iot:thing-model:modify'), | ||
| canThingModelRemove: permissions?.includes('iot:thing-model:remove'), | ||
| canThingModelSave: permissions?.includes('iot:thing-model:save'), | ||
|
|
||
| canProductCategoryGetDetail: permissions?.includes('iot:product-category:detail'), | ||
| canProductCategoryModify: permissions?.includes('iot:product-category:modify'), | ||
| canProductCategoryRemove: permissions?.includes('iot:product-category:remove'), | ||
| canProductCategorySave: permissions?.includes('iot:product-category:save'), | ||
|
|
||
| canOperateLogGetDetail: permissions?.includes('sys:operate-log:detail'), | ||
| canOperateLogExport: permissions?.includes('sys:operate-log:export'), | ||
|
|
||
| canNoticeLogGetDetail: permissions?.includes('sys:notice-log:detail'), | ||
| canNoticeLogExport: permissions?.includes('sys:notice-log:export'), | ||
|
|
||
| canLoginLogExport: permissions?.includes('sys:login-log:export'), | ||
| canMenuGetDetail: permissions?.includes('sys:menu:detail') && scopes?.includes('read'), | ||
| canMenuModify: permissions?.includes('sys:menu:modify') && scopes?.includes('write'), | ||
| canMenuRemove: permissions?.includes('sys:menu:remove') && scopes?.includes('write'), | ||
| canMenuSave: permissions?.includes('sys:menu:save') && scopes?.includes('write'), | ||
|
|
There was a problem hiding this comment.
2. Scopes can disable access 🐞 Bug ⛯ Reliability
Access flags now require both permission and a matching read/write scope; if scopes isn’t present or is unexpectedly shaped in the profile response, previously-authorized users may lose UI capabilities (buttons/routes).
Agent Prompt
### Issue description
UI access control now depends on `initialState.scopes` containing `read`/`write`. If the backend doesn’t return `scopes` (or returns an unexpected shape), many capabilities will disappear.
### Issue Context
`scopes` is only sourced from `getUserProfile()` and immediately used to gate all permissions.
### Fix Focus Areas
- ui/src/app.tsx[99-113]
- ui/src/access.ts[4-12]
### Fix options
- Normalize `scopes` in `getInitialState()`:
- If `result.data.scopes` is missing, default to `['read','write']` (or whatever matches your legacy behavior) until backend rollout is complete.
- If it can be a string, split/parse it into an array.
- Alternatively, change access checks to:
- `permissions.includes(...) && (scopes.length === 0 || scopes.includes('read'))`
- so legacy backends don’t lock users out.
- Add a clear log/warning when scopes are missing to detect misconfiguration early.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #5817 +/- ##
=========================================
Coverage 58.31% 58.31%
Complexity 1144 1144
=========================================
Files 270 270
Lines 5357 5357
Branches 339 339
=========================================
Hits 3124 3124
Misses 2057 2057
Partials 176 176 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|



Summary by Sourcery
在整个 UI 中引入作用域权限检查和国际化支持,包括本地化路由、布局、登录以及错误处理。
新功能:
zh-CN和en-US语言文件,涵盖菜单文案、登录流程以及通用文本。功能增强:
Original summary in English
Summary by Sourcery
Introduce scoped permission checks and internationalization support across the UI, including localized routing, layout, login, and error handling.
New Features:
Enhancements:
Summary by CodeRabbit