Skip to content

Dev#5817

Merged
KouShenhai merged 10 commits intomasterfrom
dev
Mar 6, 2026
Merged

Dev#5817
KouShenhai merged 10 commits intomasterfrom
dev

Conversation

@KouShenhai
Copy link
Owner

@KouShenhai KouShenhai commented Mar 6, 2026

Summary by Sourcery

在整个 UI 中引入作用域权限检查和国际化支持,包括本地化路由、布局、登录以及错误处理。

新功能:

  • 通过将后端权限与读/写作用域结合,为所有主要资源新增基于作用域的访问控制。
  • 启用完整的 UI 国际化支持,提供 zh-CNen-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:

  • Add scope-based access control by combining backend permissions with read/write scopes for all major resources.
  • Enable full UI internationalization with zh-CN and en-US locale files, including menu labels, login flows, and common texts.
  • Expose a language switcher in the layout and login page, and propagate the current locale to the backend via request headers.

Enhancements:

  • Refactor route and layout configuration, login page, and login log list to use localization keys instead of hard-coded strings.
  • Localize global error messages returned from the request interceptor for a consistent multilingual experience.
  • Extend initial application state to carry user scopes alongside permissions.

Summary by CodeRabbit

  • New Features
    • Added comprehensive multi-language support with English and Chinese translations throughout the application
    • Introduced a language switcher component enabling users to dynamically change the UI language at runtime
    • Enhanced the permission system with scope-based access control for more granular authorization management
    • Localized all navigation menus, login interface, browser titles, and system messages for improved user experience

@netlify
Copy link

netlify bot commented Mar 6, 2026

Deploy Preview for kcloud-platform-iot ready!

Name Link
🔨 Latest commit 82c697b
🔍 Latest deploy log https://app.netlify.com/projects/kcloud-platform-iot/deploys/69aa69bd3916690008ad984d
😎 Deploy Preview https://deploy-preview-5817--kcloud-platform-iot.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@KouShenhai KouShenhai merged commit e27336a into master Mar 6, 2026
12 of 16 checks passed
@sourcery-ai
Copy link

sourcery-ai bot commented Mar 6, 2026

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
Loading

初始状态、访问 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
Loading

文件级变更

Change Details Files
在现有基于权限的访问控制之上叠加基于 scope 的检查。
  • 从初始状态中同时读取 scopes 和 permissions
  • 对读取类能力,要求存在 "read" scope
  • 对写入类能力(创建/更新/删除/导出/上传),要求存在 "write" scope
ui/src/access.ts
ui/src/app.tsx
对登录页 UI 和校验文案进行国际化,并新增按页面切换语言的能力。
  • 将硬编码的中文标签/占位符/消息替换为 intl message ID 和 formatMessage 调用
  • 在登录页上提供简单的 t(id, values) 辅助方法,对 useIntl 进行封装
  • 在登录页上新增固定位置的 SelectLang 语言选择器
ui/src/pages/Login/index.tsx
通过切换到 message ID,使路由/菜单元数据可被翻译。
  • 将硬编码的中文路由名称替换为 menu.* i18n key
  • 新增对应的 title 属性,并使用相同的 key,以便更好地与布局和面包屑集成
ui/config/routes.ts
ui/src/app.tsx
在应用壳层启用全局 i18n 支持,并将 locale 传递到后端和运行时 UI。
  • 在非 hook 场景下,基于 getIntl() 新增运行时 t() 辅助函数
  • 对布局标题和用户下拉菜单中的登出文案进行国际化
  • 在请求头中向后端发送当前 locale,并将请求错误信息调整为使用 i18n key
ui/src/app.tsx
对登录日志页面的展示与登录类型筛选项进行国际化。
  • 在登录日志页面中接入 useIntl 和 t() 辅助函数
  • 将硬编码的登录类型标签替换为 i18n 消息
ui/src/pages/Sys/Log/login.tsx
配置 umi 的 locale 支持,并提供英文和中文的翻译资源。
  • 启用 umi/max 的 locale 配置,包括默认语言、Ant Design 集成以及标题同步
  • 新增 en-US 和 zh-CN 语言字典,覆盖应用标题、通用操作、错误、登录及菜单文案
ui/config/config.ts
ui/src/locales/en-US.ts
ui/src/locales/zh-CN.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: 在 pull request 中评论 @sourcery-ai review
  • Continue discussions: 直接回复 Sourcery 的评审评论以继续讨论。
  • Generate a GitHub issue from a review comment: 通过回复某条评审评论,要求 Sourcery 根据该评论创建一个 issue。你也可以在该评论下回复 @sourcery-ai issue 来从中创建 issue。
  • Generate a pull request title: 在 pull request 标题的任意位置写上 @sourcery-ai,即可随时生成标题。你也可以在 pull request 中评论 @sourcery-ai title 来(重新)生成标题。
  • Generate a pull request summary: 在 pull request 正文的任意位置写上 @sourcery-ai summary,即可在你想要的位置生成 PR 总结。你也可以在 pull request 中评论 @sourcery-ai summary 来(重新)生成总结。
  • Generate reviewer's guide: 在 pull request 中评论 @sourcery-ai guide,即可随时(重新)生成评审指南。
  • Resolve all Sourcery comments: 在 pull request 中评论 @sourcery-ai resolve,即可将所有 Sourcery 评论标记为已解决。如果你已经处理完所有评论且不希望再看到它们时非常有用。
  • Dismiss all Sourcery reviews: 在 pull request 中评论 @sourcery-ai dismiss,即可驳回所有现有的 Sourcery 评审。尤其适合你想从一个全新的评审开始时——别忘了随后评论 @sourcery-ai review 来触发新的评审!

Customizing Your Experience

打开你的 dashboard 以:

  • 启用或禁用评审功能,例如 Sourcery 自动生成的 pull request 总结、评审指南等。
  • 更改评审语言。
  • 添加、移除或编辑自定义评审说明。
  • 调整其他评审设置。

Getting Help

Original review guide in English

Reviewer's Guide

Introduce 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 handling

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
Loading

Class diagram for initial state, access scopes, and locale-aware helpers

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
Loading

File-Level Changes

Change Details Files
Layer scope-based checks on top of existing permission-based access control.
  • Read scopes from initial state alongside permissions
  • Gate read-type abilities on presence of a "read" scope
  • Gate write-type abilities (create/update/delete/export/upload) on presence of a "write" scope
ui/src/access.ts
ui/src/app.tsx
Internationalize the login page UI and validation text, and add per-page language switching.
  • Replace hard-coded Chinese labels/placeholders/messages with intl message IDs and formatMessage calls
  • Expose a simple t(id, values) helper around useIntl on the login page
  • Add a fixed-position SelectLang language selector on the login page
ui/src/pages/Login/index.tsx
Make route/menu metadata translatable by switching to message IDs.
  • Replace hard-coded Chinese route names with menu.* i18n keys
  • Add matching title properties using the same keys for better integration with layout and breadcrumbs
ui/config/routes.ts
ui/src/app.tsx
Enable global i18n support in the app shell and propagate locale to backend and runtime UI.
  • Add runtime t() helper using getIntl() for non-hook contexts
  • Internationalize layout title and user dropdown logout label
  • Send current locale to backend in request headers and adjust request error messages to use i18n keys
ui/src/app.tsx
Internationalize login-log page display and filters for login types.
  • Wire in useIntl and t() helper in login log page
  • Replace hard-coded login type labels with i18n messages
ui/src/pages/Sys/Log/login.tsx
Configure umi locale support and supply English and Chinese translation resources.
  • Enable umi/max locale configuration with default locale, Ant Design integration, and title sync
  • Add en-US and zh-CN locale dictionaries covering app title, common actions, errors, login, and menu labels
ui/config/config.ts
ui/src/locales/en-US.ts
ui/src/locales/zh-CN.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai
Copy link

coderabbitai bot commented Mar 6, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 91e5daa2-924d-4956-ac50-7401faeb6655

📥 Commits

Reviewing files that changed from the base of the PR and between b0f51bc and 82c697b.

📒 Files selected for processing (8)
  • ui/config/config.ts
  • ui/config/routes.ts
  • ui/src/access.ts
  • ui/src/app.tsx
  • ui/src/locales/en-US.ts
  • ui/src/locales/zh-CN.ts
  • ui/src/pages/Login/index.tsx
  • ui/src/pages/Sys/Log/login.tsx

Walkthrough

This 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

Cohort / File(s) Summary
Configuration & Routes
ui/config/config.ts, ui/config/routes.ts
Removed static title from layout config; added locale configuration block. Replaced hardcoded Chinese route names/labels with i18n keys (menu.*) across all routes (home, system management, logs, storage, IoT) and added corresponding title fields.
Localization Files
ui/src/locales/en-US.ts, ui/src/locales/zh-CN.ts
Added complete English and Chinese translation objects (~78 lines each) mapping localization keys across app, common, error, login, and menu categories, providing the source strings for i18n support.
App Core & Permissions
ui/src/app.tsx, ui/src/access.ts
Enhanced app.tsx with internationalization integration (language selector, locale-based title, i18n error messages, Language header in requests), extended initial state with scopes array. Modified access.ts to gate all permission checks with corresponding scopes (read/write/etc.).
Page Localization
ui/src/pages/Login/index.tsx, ui/src/pages/Sys/Log/login.tsx
Replaced hardcoded Chinese strings in login page and login log page with i18n translation keys for labels, placeholders, validation messages, and UI text; added SelectLang component to login page for language switching.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Dev #3243: Modifies ui/config/routes.ts and ui/src/app.tsx layout configuration—directly related to this PR's route i18n refactoring and app layout enhancements.
  • feat: 修改ui的父菜单样式 #3635: Modifies layout-related configuration in ui/src/app.tsx, aligning with this PR's layout and UI behavior changes.
  • Dev #3315: Modifies login-related files (ui/src/pages/Login/index.tsx and ui/src/pages/Sys/Log/login.tsx), overlapping with this PR's login page internationalization updates.

Suggested labels

review-effort/3-moderate, feature/internationalization, security/permissions

Poem

🐰 A rabbit's ode to global tongues

Where once were Chinese words so bright,
Now keys unlock each tongue with might,
With scopes and reads and writes so clear,
The world's translations now appear! ✨

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dev

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

┌──────────────┐
│ Opengrep CLI │
└──────────────┘

�[32m✔�[39m �[1mOpengrep OSS�[0m
�[32m✔�[39m Basic security coverage for first-party code vulnerabilities.

�[1m Loading rules from local config...�[0m

ui/src/access.ts

┌──────────────┐
│ Opengrep CLI │
└──────────────┘

�[32m✔�[39m �[1mOpengrep OSS�[0m
�[32m✔�[39m Basic security coverage for first-party code vulnerabilities.

�[1m Loading rules from local config...�[0m

ui/config/routes.ts

┌──────────────┐
│ Opengrep CLI │
└──────────────┘

�[32m✔�[39m �[1mOpengrep OSS�[0m
�[32m✔�[39m Basic security coverage for first-party code vulnerabilities.

�[1m Loading rules from local config...�[0m

  • 5 others

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@qodo-code-review
Copy link

Review Summary by Qodo

Add frontend internationalization with i18n and scope-based access control

✨ Enhancement

Grey Divider

Walkthroughs

Description
• 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
Diagram
flowchart 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
Loading

Grey Divider

File Changes

1. ui/config/config.ts ⚙️ Configuration changes +8/-1

Configure UMI i18n plugin with locale settings

• Removed hardcoded title from layout config with comment to use runtime i18n
• Added locale configuration for UMI i18n plugin with Chinese default language
• Enabled Ant Design locale and browser title internationalization

ui/config/config.ts


2. ui/config/routes.ts ✨ Enhancement +42/-21

Internationalize all route names and titles

• Replaced all hardcoded Chinese menu names with i18n keys (e.g., '首页' → 'menu.home')
• Added title property to all routes matching the name property for consistency
• Updated nested routes across system management, IoT, and permission modules

ui/config/routes.ts


3. ui/src/access.ts ✨ Enhancement +56/-55

Add scope-based access control validation

• Added scopes array extraction from initialState alongside permissions
• Enhanced all permission checks to validate both permissions and scopes ('read'/'write')
• Scopes now required for all CRUD operations for additional security layer

ui/src/access.ts


View more (5)
4. ui/src/locales/en-US.ts ✨ Enhancement +78/-0

Create English language translation file

• Created new English locale file with 78 translation keys
• Includes app title, common actions, login form labels, error messages, and menu items
• Covers all UI text for system management, IoT, and authentication features

ui/src/locales/en-US.ts


5. ui/src/locales/zh-CN.ts ✨ Enhancement +78/-0

Create Chinese language translation file

• Created new Chinese locale file with 78 translation keys matching English structure
• Provides Chinese translations for app, common, login, error, and menu sections
• Maintains consistency with existing Chinese UI terminology

ui/src/locales/zh-CN.ts


6. ui/src/app.tsx ✨ Enhancement +35/-10

Integrate i18n throughout app runtime configuration

• Imported SelectLang and getIntl from UMI plugin-locale for language switching
• Added scopes to initial state and user profile data structure
• Implemented t() helper function for non-React component i18n usage
• Added SelectLang component to layout actions for language switching
• Replaced all hardcoded error messages and UI strings with i18n keys
• Added Language header to request interceptor for backend locale awareness
• Updated logout label and app title to use i18n

ui/src/app.tsx


7. ui/src/pages/Login/index.tsx ✨ Enhancement +46/-42

Internationalize login page with i18n integration

• Imported SelectLang and useIntl hook from UMI plugin-locale
• Replaced hardcoded login type labels with i18n keys
• Implemented intl helper for all form placeholders, validation messages, and UI text
• Added SelectLang component to login page header for language switching
• Updated login form title and subtitle to use i18n
• Internationalized all form field labels, error messages, and captcha countdown text

ui/src/pages/Login/index.tsx


8. ui/src/pages/Sys/Log/login.tsx ✨ Enhancement +7/-5

Add i18n support to login log page

• Imported useIntl hook for internationalization support
• Implemented t() helper function using intl.formatMessage
• Replaced hardcoded login type labels with i18n keys in getLoginType function
• Updated table column filter options to use i18n for login type labels

ui/src/pages/Sys/Log/login.tsx


Grey Divider

Qodo Logo

@qodo-code-review
Copy link

qodo-code-review bot commented Mar 6, 2026

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Headers not persisted 🐞 Bug ✓ Correctness
Description
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.
Code

ui/src/app.tsx[R261-272]

		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}`
Evidence
The interceptor uses a non-object fallback ([]) and returns config without setting
config.headers, so when config.headers is initially undefined, header mutations are not
guaranteed to be applied. At least one call site (getUserProfile) makes requests without providing
headers, so it can hit this path.

ui/src/app.tsx[260-275]
ui/src/services/admin/user.ts[111-116]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### 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


2. Scopes can disable access 🐞 Bug ⛯ Reliability
Description
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).
Code

ui/src/access.ts[R5-12]

+	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'),
+
Evidence
All access checks became conjunctions with scopes.includes('read'|'write'). scopes is sourced
only from getUserProfile() and no other UI code populates/normalizes it, so missing or mismatched
scopes will cause widespread access denials.

ui/src/access.ts[4-12]
ui/src/app.tsx[99-113]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### 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 `[&#x27;read&#x27;,&#x27;write&#x27;]` (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(...) &amp;&amp; (scopes.length === 0 || scopes.includes(&#x27;read&#x27;))`
 - 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



Remediation recommended

3. Home menu may show key 🐞 Bug ✓ Correctness
Description
The home menu item is now emitted as the i18n id menu.home, but the menu config disables
localization (menu.locale: false). This can result in the menu displaying the raw key instead of a
translated label.
Code

ui/src/app.tsx[R31-35]

+		name: 'menu.home',
+		title: 'menu.home',
		path: '/home',
		icon: <HomeOutlined/>
	}]
Evidence
The menu data returned by menu.request is built by getRouters, which now sets name/title to
menu.home. At the same time, menu localization is explicitly disabled, so menu items are likely
rendered as-is (depending on ProLayout behavior).

ui/src/app.tsx[29-35]
ui/src/app.tsx[124-131]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The home item in `getRouters()` now uses i18n ids (`menu.home`) while the layout menu disables localization (`menu.locale: false`). This may render `menu.home` literally.

### Issue Context
Menu items are sourced via `menu.request` and returned from `getRouters()`.

### Fix Focus Areas
- ui/src/app.tsx[29-35]
- ui/src/app.tsx[124-131]

### Fix options
1) Keep `menu.locale: false` and translate the label when building the item:
- Set `name: t(&#x27;menu.home&#x27;)` (and omit/adjust `title`) for the home item.

2) Enable menu localization:
- Set `menu.locale: true` and ensure menu item `name` values align with the expected i18n key scheme (including server-provided menu items, if needed).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

嗨,我发现了 4 个问题,并给出了一些整体性的反馈:

  • src/app.tsx 中,layoutt 被定义为 const 函数表达式之前就引用了它,这会在运行时抛错;建议将 t 辅助函数移动到 layout 之上,或者直接在 layout 内联使用 getIntl().formatMessage
  • src/access.ts 中的 scope 校验多处重复使用了 scopes?.includes('read'/'write');可以考虑抽成一些小的辅助函数(例如 hasReadScopehasWriteScope)或者配置映射表,这样可以减少重复代码以及 scope 使用不一致的风险。
  • src/pages/Sys/Log/login.tsx 中,getLoginType 和下拉框的 options 里仍然为部分登录类型(例如 mailauthorization_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>

Sourcery 对开源项目免费 —— 如果你觉得这次评审有帮助,欢迎分享 ✨
帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据反馈改进评审质量。
Original comment in English

Hey - I've found 4 issues, and left some high level feedback:

  • 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.
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>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines 36 to 39
return {
'username_password': '用户名密码登录',
'mobile': '手机号登录',
'username_password': t('login.usernamePassword'),
'mobile': t('login.mobile'),
'mail': '邮箱登录',
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: 登录类型映射目前只做了部分国际化。

getLoginType 现在对 username_passwordmobile 使用了 t(...),但 mailauthorization_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]
  1. 确保在 i18n 消息配置中,为所有支持的语言都定义了 login.maillogin.authorizationCode 这两个 key。
  2. 如果该文件(或其他文件)中还有其他地方直接硬编码了 "邮箱登录""授权码登录"(例如在筛选/下拉框的 options label 中),也请统一改为使用 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]
  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.

@@ -173,8 +175,8 @@ export default () => {
mode: 'single',
options: [
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 数组的一部分。为了完全保持一致性,你应该:

  1. 将剩余的登录类型选项(例如 mobileauthorization_code 等)的 label 也统一改为 label: t('login.xxx'),并与 getLoginType 中使用的 key 保持一致。
  2. 确保相应的翻译 key(login.maillogin.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:

  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.

Comment on lines 261 to +264
async (config: any) => {
const headers = config.headers ? config.headers : [];
// 国际化:携带语言到后端(优先使用 umi plugin-locale 的 current locale)
const { getLocale } = require('@@/exports');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>) => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): 建议将与语言环境相关的辅助函数移动到顶层 import,提前定义 t,并简化拦截器中对 headers 的处理逻辑,在保留 i18n 行为的前提下让代码更清晰、更具声明性。

在保持所有新增 i18n 行为不变的前提下,可以通过以下方式降低复杂度:

  1. 在顶层统一导入 locale 工具,而不是在函数内部使用 require
  2. 在使用 t 之前先定义它(并保持它为简单纯函数辅助)
  3. 清理拦截器中对 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 之前定义它

现在的代码中,layoutconst 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 调用;
  • 明确暴露了依赖(getIntlgetLocale);
  • 避免了在 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:

  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:

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 require calls,
  • makes dependencies (getIntl, getLocale) explicit,
  • avoids using t before it’s defined, and
  • simplifies the request interceptor’s header logic.

Comment on lines 261 to 272
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}`

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

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

Comment on lines +5 to +12
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'),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

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

@sonarqubecloud
Copy link

sonarqubecloud bot commented Mar 6, 2026

@codecov
Copy link

codecov bot commented Mar 6, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 58.31%. Comparing base (41544c0) to head (82c697b).
⚠️ Report is 22 commits behind head on master.

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant