Skip to content

feat: 添加多语言支持(中/英)#45

Open
sunnylqm wants to merge 2 commits into
mainfrom
feat/i18n
Open

feat: 添加多语言支持(中/英)#45
sunnylqm wants to merge 2 commits into
mainfrom
feat/i18n

Conversation

@sunnylqm

@sunnylqm sunnylqm commented Jun 28, 2026

Copy link
Copy Markdown
Collaborator

变更内容

  • 安装 i18next + react-i18next + 语言检测插件
  • 创建 i18n 配置,包含 en 和 zh-CN 两个 locale
  • 替换 36 个组件文件中的硬编码字符串为 t() 调用
  • 添加 Ant Design ConfigProvider 动态 locale 切换
  • 在顶部导航添加语言切换器(桌面端 + 移动端)

待完成

  • user.tsx 的 i18n(文件较大,单独处理中)

Summary by CodeRabbit

  • New Features

    • Added full multilingual support across the app, including login, registration, reset password, app management, admin pages, and error states.
    • The interface now switches language automatically and matches the selected locale in the UI framework.
  • Bug Fixes

    • Replaced hardcoded text with localized strings throughout headers, modals, tables, tooltips, buttons, and empty states.
    • Improved fallback text handling so missing app names and status labels display correctly in each language.

- Install i18next, react-i18next, i18next-browser-languagedetector
- Create i18n config with en and zh-CN locales
- Replace hardcoded strings across 36 component/page files
- Add Ant Design ConfigProvider with dynamic locale switching
- Add language switcher in top navigation (desktop + mobile)

Note: user.tsx i18n pending (complex file, separate commit)
@netlify

netlify Bot commented Jun 28, 2026

Copy link
Copy Markdown

Deploy Preview for pushy ready!

Name Link
🔨 Latest commit bcd0c19
🔍 Latest deploy log https://app.netlify.com/projects/pushy/deploys/6a40d15a1f4fb30008fbb2cc
😎 Deploy Preview https://deploy-preview-45--pushy.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.

@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@sunnylqm, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 47 minutes and 2 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 46dc24c3-4c73-479c-bc1b-79e023c58dbd

📥 Commits

Reviewing files that changed from the base of the PR and between d5bc2b7 and bcd0c19.

📒 Files selected for processing (3)
  • src/i18n/locales/en.json
  • src/i18n/locales/zh-CN.json
  • src/pages/user.tsx
📝 Walkthrough

Walkthrough

Adds i18next, i18next-browser-languagedetector, and react-i18next as dependencies. Creates src/i18n/index.ts to initialize i18next with browser language detection and registers complete en.json and zh-CN.json locale files (~1,450 lines combined). Updates src/index.tsx to dynamically select the Ant Design ConfigProvider locale based on detected language. Replaces all hardcoded Chinese strings with useTranslation/t(...) calls across every page and component in the codebase.

Changes

Full-app i18n via i18next + react-i18next

Layer / File(s) Summary
i18n setup: packages, instance, locale files, and root wiring
package.json, src/i18n/index.ts, src/i18n/locales/en.json, src/i18n/locales/zh-CN.json, src/index.tsx
Adds three i18n npm packages, creates the i18next instance with browser language detection and react-i18next plugin, defines complete English and Chinese locale JSONs (~1,450 translation keys), and refactors the root App component to pick the Ant Design ConfigProvider locale dynamically from i18n.language.
Navigation shell and shared layout components
src/components/top-navigation.tsx, src/components/app-drawer.tsx, src/components/footer.tsx, src/components/error-boundary.tsx
Localizes TopNavigation (all menu items, app switcher, app row, sidebar toggles via getExternalItems(t)), AppDrawer (header, placement radios, search, icon buttons, formatCheckCount(app, t)), Footer (copyright via interpolation), and ErrorBoundary (result title/subtitle/buttons).
App detail header, settings modal, and create modal
src/components/app-detail-header.tsx, src/components/app-settings-modal.tsx, src/components/create-app-modal.tsx
Localizes AppDetailHeader (fallback name, paused tag, tab labels, settings button), AppSettingsModal (form field labels, switches, delete confirmation), and showCreateAppModal (labels, validation message via i18n.t.bind(i18n)).
Auth pages
src/pages/login.tsx, src/pages/register.tsx, src/pages/reset-password/..., src/pages/activate.tsx, src/pages/welcome.tsx, src/pages/inactivated.tsx
Applies useTranslation to all authentication flows: login, register (including agreement text and validation errors), reset-password (send-email, set-password, success, stepper), activate, welcome (resend countdown), and inactivated.
User-facing app pages and quota component
src/pages/apps.tsx, src/pages/api-tokens.tsx, src/components/daily-check-quota.tsx
Localizes the apps list page (cards, status tags, empty states, formatAppKey), the API tokens page (table columns, create/revoke modals, copy button), and the daily-check-quota component (tier tooltips, status tags, panel metrics, hint text).
Manage page and all subcomponents
src/pages/manage/index.tsx, src/pages/manage/components/*
Applies useTranslation to manage/index (filter controls, tab labels, AppDetailHeader props), version-table (getColumns(t) factory, QR/deep-link validation), package-list (status map, delete/edit helpers accepting t), bind-package (dependency columns, publish/unpublish menus, Modal.confirm), setting-modal, deps-table, commit, and publish-feature-table.
Admin pages and realtime metrics
src/pages/audit-logs.tsx, src/pages/admin-metrics.tsx, src/pages/admin-service-status.tsx, src/pages/admin-users.tsx, src/pages/admin-apps.tsx, src/pages/admin-config.tsx, src/pages/realtime-metrics.tsx
Localizes all admin pages: audit-logs (action map factory, table/drawer labels, Excel export headers), admin-metrics (getModeLabels(t), chart/UI strings), admin-service-status (getCounterLabels(t), series builders accepting label map, getEndpointColumns(t)/getApi5xxEventColumns(t), statistic cards, sidebar), admin-users (getTierOptions(t), table/modal), admin-apps (table columns, edit modal), admin-config (save/delete toasts, table/modal), and realtime-metrics (formatCategory(t), tooltip suffix, chart labels).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • reactnativecn/pushy-admin#10: Overlaps in src/pages/manage/components/bind-package.tsx — the retrieved PR refactors BindPackage props and logic while this PR internalationalizes the same component's UI strings.
  • reactnativecn/pushy-admin#32: Introduced src/components/create-app-modal.tsx logic that this PR now localizes with i18n.t.bind(i18n).

Poem

🐇 Hop, hop — no more hardcoded 中文!
The bunny typed keys in JSON with glee,
t('login.slogan') where 登录 used to be,
English and Chinese now dance paw in paw,
i18next wired — the best change I saw! 🌍

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.08% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding bilingual i18n support for Chinese and English.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/i18n

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.

- Replace all 63 Chinese strings with t() calls
- Convert module-level constants to functions accepting t
- Add useTranslation to 7 components
- TypeScript and biome checks pass

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/pages/manage/components/version-table.tsx (1)

153-173: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Finish localizing the delete dialog body.

This function now translates the modal title, but the body still uses versionNames.join(','), which leaves Chinese punctuation in English mode. Rendering the names as a list avoids that locale-specific leak.

Suggested fix
   Modal.confirm({
     title: t('version_table.delete_title'),
-    content: versionNames.join(','),
+    content: (
+      <div className="max-h-48 overflow-y-auto">
+        {versionNames.map((name) => (
+          <div key={name}>{name}</div>
+        ))}
+      </div>
+    ),
     maskClosable: true,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/manage/components/version-table.tsx` around lines 153 - 173, The
delete dialog body in removeSelectedVersions is still building a locale-specific
string with versionNames.join(','), which leaks Chinese punctuation into
non-Chinese locales. Update the Modal.confirm content to render the selected
version names as a locale-aware list instead of joining with a hardcoded
delimiter, keeping the existing translation flow used by
t('version_table.delete_title') and the versionNames collection logic.
🧹 Nitpick comments (4)
src/pages/register.tsx (1)

129-129: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use rootRouterPath.login for this link.

This file already uses rootRouterPath for navigation, so hardcoding "/login" creates a second route source that can drift later.

♻️ Proposed fix
-            <Link to="/login">{t('register.has_account')}</Link>
+            <Link to={rootRouterPath.login}>{t('register.has_account')}</Link>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/register.tsx` at line 129, The register page link is hardcoded to
"/login" instead of using the shared router constant, which can drift from the
app’s routing source of truth. Update the Link in register page to use
rootRouterPath.login, following the existing rootRouterPath usage in this
component, so navigation stays consistent and centralized.
src/pages/inactivated.tsx (1)

49-50: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse rootRouterPath.login for the back button target.

The guard above already navigates with rootRouterPath.login; hardcoding "/user" here makes the destination easier to drift away from the translated “back login” label.

♻️ Proposed fix
-        <Button key="back" href="/user">
+        <Button key="back" href={rootRouterPath.login}>
           {t('inactivated.back_login')}
         </Button>,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/inactivated.tsx` around lines 49 - 50, The back button in the
inactivated page is hardcoding the login destination instead of reusing the
shared router constant. Update the Button in the inactivated page to use
rootRouterPath.login, matching the existing navigation guard and keeping the
target consistent with the “back login” action.
src/pages/reset-password/components/success.tsx (1)

10-12: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the shared login route instead of hardcoding /#/login.

This introduces a second login URL shape in the same flow. Keeping the CTA on the router-managed login path avoids route drift and the forced full-page reload here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/reset-password/components/success.tsx` around lines 10 - 12, The
reset-password success CTA is hardcoding the login URL, which bypasses the
shared router path and can cause route drift. Update the Button in the Success
component to use the existing login route constant/helper used elsewhere in the
app instead of the literal /#/login, so the reset flow stays aligned with the
router-managed login path.
src/pages/manage/components/publish-feature-table.tsx (1)

20-37: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Keep tag state separate from translated copy.

The tag color now depends on text.includes('✓') / text.includes('⚠'). That makes every locale string responsible for preserving those glyphs. Store a semantic status in the row data and translate only the displayed label.

Also applies to: 57-89

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/manage/components/publish-feature-table.tsx` around lines 20 - 37,
The publish feature table is coupling tag color to translated text via the
`text.includes('✓')` / `text.includes('⚠')` logic, so the state should be made
semantic instead of inferred from localized copy. Update the row data used in
`PublishFeatureTable` (and the related entries in the table definition block) to
carry an explicit status field for each tag, and keep `t(...)` only for the
visible label. Then adjust the tag rendering logic to use that status field for
color/variant selection instead of inspecting the translated string.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/app-drawer.tsx`:
- Around line 136-142: The app count and check-count translations in app-drawer
should keep their count values numeric for i18next pluralization, rather than
passing locale-formatted strings. Update the calls in the app list and the
checkbox label to pass the raw numeric values from apps.length and the active
check count, and handle any locale-specific formatting only in the rendered text
layer. Use the translation keys and the surrounding AppDrawer rendering logic to
locate the affected count labels.

In `@src/components/daily-check-quota.tsx`:
- Around line 37-40: The tier label in daily-check-quota still falls back to
localized data from quotas.title, which can surface Chinese plan names in
English mode. Update the rendering in daily-check-quota (including the other
affected tier display path) to resolve known tiers through t(...) first, then
fall back to a custom server-provided title, and only use user.tier as the last
resort. Use the existing tier lookup points around quota, quotas[user.tier], and
the tier text rendering to keep the fix consistent.

In `@src/components/top-navigation.tsx`:
- Around line 72-89: The `top-navigation` menu items for documentation and about
are using translated labels but fixed `href`s, so the destination does not match
the active locale. Update the `TopNavigation` link setup for the `document` and
`about` entries to choose the locale-specific URL based on the current language
(using the same i18n/lang source already available in this component), or
otherwise ensure each label points to the matching language page.

In `@src/i18n/locales/zh-CN.json`:
- Around line 49-135: The zh-CN locale bundle is missing several keys that the
new UI expects, so align it with the English key set by adding the absent
entries under user, deps_table, and the full publish_feature_table namespace in
zh-CN.json. Use the existing translation patterns in that file to add localized
strings for symbols like user.fetching_addon_quote, user.per_year,
user.upgrade_button, deps_table.cli_required,
deps_table.native_package_with_name, deps_table.ota_version_with_name,
bind_package.publish, and publish_feature_table so the localized screens don’t
fall back to raw keys.

In `@src/index.tsx`:
- Around line 68-70: The Antd locale lookup in App is using i18n.language
directly, which can remain an unresolved detector value and miss the normalized
translation language. Update the locale selection to use the resolved language
from the i18n instance when indexing antdLocaleMap, so the App component picks
the correct Antd locale instead of falling back to zhCN for English. Keep the
fix localized to the App function and the antdLocaleMap lookup.

In `@src/pages/audit-logs.tsx`:
- Line 217: The audit logs page is using a fixed Day.js locale at module load,
so relative timestamps from date.fromNow() can stay in the wrong language even
after useTranslation switches the UI language. Update the audit-logs page logic
around useTranslation and the time rendering to sync Day.js with the active i18n
language, preferably by setting dayjs.locale from the current language inside
the component lifecycle instead of once at import time.
- Around line 123-129: The audit log action filter is using translated text as
the query value, so `selectedAction` can break across locales and shared URLs.
Update `getActionOptions` (and the related select/query handling in the audit
logs page) to store a stable action key derived from `getActionMap` output, such
as `METHOD + normalizedPath`, and use the translated string only for the
displayed `label`. Make sure the same stable key is used wherever the action
value is written to or read from the URL/query param so filtering and selection
remain consistent after language changes.

In `@src/pages/manage/index.tsx`:
- Line 73: The bulk-select aria-label in the Manage page is collapsing to just
filterLabel because the conditional adds nothing, so screen readers miss the
action description. Update the label in the manage page component to use a
descriptive bulk-select text for the checkbox action, and remove the dead
t('common.save') branch from that aria-label expression.

In `@src/pages/realtime-metrics.tsx`:
- Around line 95-100: The tooltip text in formatTooltipItem is assembling
localized output by concatenating translation fragments, which can produce
awkward spacing and word order. Update the realtime-metrics translations and the
code paths using formatTooltipItem/current_dimension to use full interpolated
messages instead of stitching together checks_suffix or similar fragments, so
the locale controls the complete phrase naturally.

In `@src/pages/reset-password/index.tsx`:
- Around line 21-25: The reset-password success route is still using a step
value that is outside the Steps range, so the progress indicator becomes out of
sync on the success screen. Update the reset-password page logic in the
component that renders Steps so the success route is mapped to display index 2
before passing the value into current, while keeping the existing step routes
and content rendering unchanged.

---

Outside diff comments:
In `@src/pages/manage/components/version-table.tsx`:
- Around line 153-173: The delete dialog body in removeSelectedVersions is still
building a locale-specific string with versionNames.join(','), which leaks
Chinese punctuation into non-Chinese locales. Update the Modal.confirm content
to render the selected version names as a locale-aware list instead of joining
with a hardcoded delimiter, keeping the existing translation flow used by
t('version_table.delete_title') and the versionNames collection logic.

---

Nitpick comments:
In `@src/pages/inactivated.tsx`:
- Around line 49-50: The back button in the inactivated page is hardcoding the
login destination instead of reusing the shared router constant. Update the
Button in the inactivated page to use rootRouterPath.login, matching the
existing navigation guard and keeping the target consistent with the “back
login” action.

In `@src/pages/manage/components/publish-feature-table.tsx`:
- Around line 20-37: The publish feature table is coupling tag color to
translated text via the `text.includes('✓')` / `text.includes('⚠')` logic, so
the state should be made semantic instead of inferred from localized copy.
Update the row data used in `PublishFeatureTable` (and the related entries in
the table definition block) to carry an explicit status field for each tag, and
keep `t(...)` only for the visible label. Then adjust the tag rendering logic to
use that status field for color/variant selection instead of inspecting the
translated string.

In `@src/pages/register.tsx`:
- Line 129: The register page link is hardcoded to "/login" instead of using the
shared router constant, which can drift from the app’s routing source of truth.
Update the Link in register page to use rootRouterPath.login, following the
existing rootRouterPath usage in this component, so navigation stays consistent
and centralized.

In `@src/pages/reset-password/components/success.tsx`:
- Around line 10-12: The reset-password success CTA is hardcoding the login URL,
which bypasses the shared router path and can cause route drift. Update the
Button in the Success component to use the existing login route constant/helper
used elsewhere in the app instead of the literal /#/login, so the reset flow
stays aligned with the router-managed login path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4bcc56f0-c6ea-4efe-9a44-0ed365d39ffa

📥 Commits

Reviewing files that changed from the base of the PR and between 3b47bb2 and d5bc2b7.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (39)
  • package.json
  • src/components/app-detail-header.tsx
  • src/components/app-drawer.tsx
  • src/components/app-settings-modal.tsx
  • src/components/create-app-modal.tsx
  • src/components/daily-check-quota.tsx
  • src/components/error-boundary.tsx
  • src/components/footer.tsx
  • src/components/top-navigation.tsx
  • src/i18n/index.ts
  • src/i18n/locales/en.json
  • src/i18n/locales/zh-CN.json
  • src/index.tsx
  • src/pages/activate.tsx
  • src/pages/admin-apps.tsx
  • src/pages/admin-config.tsx
  • src/pages/admin-metrics.tsx
  • src/pages/admin-service-status.tsx
  • src/pages/admin-users.tsx
  • src/pages/api-tokens.tsx
  • src/pages/apps.tsx
  • src/pages/audit-logs.tsx
  • src/pages/inactivated.tsx
  • src/pages/login.tsx
  • src/pages/manage/components/bind-package.tsx
  • src/pages/manage/components/commit.tsx
  • src/pages/manage/components/deps-table.tsx
  • src/pages/manage/components/package-list.tsx
  • src/pages/manage/components/publish-feature-table.tsx
  • src/pages/manage/components/setting-modal.tsx
  • src/pages/manage/components/version-table.tsx
  • src/pages/manage/index.tsx
  • src/pages/realtime-metrics.tsx
  • src/pages/register.tsx
  • src/pages/reset-password/components/send-email.tsx
  • src/pages/reset-password/components/set-password.tsx
  • src/pages/reset-password/components/success.tsx
  • src/pages/reset-password/index.tsx
  • src/pages/welcome.tsx

Comment on lines +136 to +142
<div className="font-medium text-slate-900">
{t('app_drawer.app_list')}
</div>
<div className="text-slate-500 text-xs">
共 {apps.length.toLocaleString()} 个应用
{t('app_drawer.apps_count', {
count: apps.length.toLocaleString(),
})}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

For i18next/react-i18next pluralization, does the special count option need to stay numeric instead of a formatted string?

💡 Result:

Yes, the special count option must remain a numeric value (a JavaScript number) to function correctly for pluralization [1][2]. i18next uses the provided count value to determine which plural form (such as _one, _other, or language-specific forms) to select based on the rules for the active language [3][4]. Because the library relies on these numeric rules to perform calculations, passing a formatted string will prevent it from correctly identifying the appropriate translation key [2]. Key considerations include: 1. Numeric Requirement: The count must be a number [1][2]. If you need to display a formatted number (e.g., "1,000" or "1.000"), you should pass the raw numeric value to the count option for pluralization logic, and use i18next's interpolation formatting or a separate formatting library to handle the display string independently [3][5][4]. 2. Inference Limitations: In react-i18next, while the Trans component can automatically infer the count if it is present as {{count}} in the children, this inference also requires the value to be a JavaScript number [2]. String values will not be correctly inferred [2]. 3. Fallback Behavior: If a valid numeric count is not provided, i18next cannot determine the plural form and will fail to resolve the key correctly, often returning the raw key name instead of the translated string [3][4].

Citations:


Keep count numeric and format the display separately

count must stay a number for i18next plural selection; apps.length.toLocaleString() breaks that contract. The same applies to the check-count label below, which should use the active UI locale only for rendering.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/app-drawer.tsx` around lines 136 - 142, The app count and
check-count translations in app-drawer should keep their count values numeric
for i18next pluralization, rather than passing locale-formatted strings. Update
the calls in the app list and the checkbox label to pass the raw numeric values
from apps.length and the active check count, and handle any locale-specific
formatting only in the rendered text layer. Use the translation keys and the
surrounding AppDrawer rendering logic to locate the affected count labels.

Comment on lines +37 to 40
{t('daily_check_quota.tier')}
{quota?.title ??
quotas[user.tier as keyof typeof quotas]?.title ??
user.tier}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Translate the tier name instead of reading title from quotas.

These paths still fall back to quota?.title / quotas[user.tier].title, and src/constants/quotas.ts:1-65 stores those titles in Chinese. English mode will still show mixed-language plan names here. Resolve known tiers through t(...) first, then only fall back to a custom server title or user.tier.

Also applies to: 101-105

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/daily-check-quota.tsx` around lines 37 - 40, The tier label in
daily-check-quota still falls back to localized data from quotas.title, which
can surface Chinese plan names in English mode. Update the rendering in
daily-check-quota (including the other affected tier display path) to resolve
known tiers through t(...) first, then fall back to a custom server-provided
title, and only use user.tier as the last resort. Use the existing tier lookup
points around quota, quotas[user.tier], and the tier text rendering to keep the
fix consistent.

Comment on lines +72 to +89
{
key: 'document',
icon: <ReadOutlined />,
label: (
<ExtLink href="https://pushy.reactnative.cn/docs/getting-started.html">
{t('nav.documentation')}
</ExtLink>
),
},
{
key: 'about',
icon: <InfoCircleOutlined />,
label: (
<ExtLink href="https://reactnative.cn/about.html">
{t('nav.about_us')}
</ExtLink>
),
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use locale-aware docs/about destinations.

These labels are translated, but the hrefs stay fixed. That means the bilingual nav only changes the text; “Documentation” / “About us” still send every user to the same target. Please branch the destination by the active language, or keep the label aligned with the actual page language.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/top-navigation.tsx` around lines 72 - 89, The `top-navigation`
menu items for documentation and about are using translated labels but fixed
`href`s, so the destination does not match the active locale. Update the
`TopNavigation` link setup for the `document` and `about` entries to choose the
locale-specific URL based on the current language (using the same i18n/lang
source already available in this component), or otherwise ensure each label
points to the matching language page.

Comment on lines +49 to +135
"user": {
"invoice_hint": "请发送邮件至 <1>hi@charmlot.com</1>,并写明:",
"invoice_company": "公司名称、税号、注册邮箱、接收发票邮箱(不写则发送到注册邮箱),附带支付截图。",
"invoice_default": "我们默认会回复普通电子发票到接收邮箱(请同时留意垃圾邮件),类目为软件服务。",
"purchasing_note": "只可续费相同服务版本或升级更高版本,如果您需要购买较低的服务版本,请等待当前版本过期,或联系 QQ 客服 34731408 手动处理。",
"view_invoice": "点此查看如何申请发票",
"current_expire": "当前到期日",
"price_month": "个月",
"annual_billing": "个月(年付)",
"quoting": "报价中",
"order_settle": "按订单结算",
"renewal_unavailable": "当前版本暂未返回可续费价格",
"fetching_renewal_quote": "正在获取续费报价",
"renew": "续费",
"renew_after_expire": "续费后到期日",
"about_discount": "约{{discount}}折优惠",
"addon_price_monthly": "当前价格含加购费用每月 {{price}}",
"jumping": "跳转中",
"upgrade_purchase": "升级购买",
"upgrade_hint_free": "选择目标版本后按年付开通服务。",
"upgrade_hint_paid": "补差价由后端按剩余有效期报价,未超过优惠阈值按月费差额折算,超过后按年费优惠折算。",
"purchase_after_pay": "购买后从支付日起开通服务",
"annual_pay": "年付",
"proration": "补差价",
"days_unit": "天",
"check_quota_daily": "检查次数每日",
"app_count": "应用个数",
"native_pkg_count": "原生包数",
"hotfix_count": "热更包数",
"count_unit": "个",
"wan_unit": "万",
"account_info": "账户信息",
"username": "用户名",
"email": "邮箱",
"service_version": "服务版本",
"service_expire": "服务有效期至",
"no_expire": "无",
"purchase_note": "购买说明",
"quota_details": "额度详情",
"view_pricing": "查看价格表",
"bank_transfer": "使用网银转账",
"logout": "退出登录",
"logged_out": "您已退出登录",
"app_count_label": "应用数量",
"app_count_note": "当前账户下应用总数",
"hotfix_count_label": "热更包数量",
"native_pkg_count_label": "原生包数量",
"counting_hotfix": "正在统计各应用热更包数量",
"counting_native": "正在统计各应用原生包数量",
"max_single_app": "最高单应用使用量",
"counting": "统计中",
"over_quota": "超额",
"spec_limits": "规格限制",
"single_native_size": "单个原生包大小",
"single_hotfix_size": "单个热更包大小",
"check_quota_limit": "检查额度上限",
"per_day": "次 / 日",
"no_7day_details": "暂无 7 天明细",
"payment_invalid": "支付链接无效",
"addon_eligible_hint": "仅高级版及以上可加购检查额度,当前版本可先升级后再加购。",
"addon_empty": "暂无可购买项目",
"check_quota_addon": "检查额度加购",
"addon_price_desc": "每增加 {{quota}} 次 / 日,每月额外收费 {{price}}。",
"addon_proration_hint": "按剩余天数补差价。收费基准为:{{quota}}次/日,每月加收 {{price}},可叠加购买。",
"addon_purchase_hint": "收费基准为:{{quota}}次/日,每月加收 {{price}},可叠加购买。",
"addon_title_with_expire": "加购检查额度(当前有效期不变:至 {{date}})",
"addon_title": "加购检查额度",
"daily_check_title": "每日检查额度",
"daily_check_desc": "客户端检查热更新时消耗,按账户全部应用汇总。",
"remaining_today": "今日剩余额度",
"quota_limit_info": "上限 {{dailyQuota}} 次 / 日(套餐内 {{included}} 次 + 加购 {{extra}} 次)",
"exceeded_by": "已超出 {{count}} 次",
"low_below": "低于 {{percent}}%,请留意检查频率",
"recent_7day_avg": "最近 7 天平均剩余额度 {{avg}}({{range}})",
"count_suffix": "次",
"already_exceeded": "已超额",
"already_exhausted": "已用尽",
"low": "偏低",
"purchasable_tiers": {
"standard": "标准版",
"premium": "高级版",
"pro": "专业版",
"vip1": "大客户VIP1版",
"vip2": "大客户VIP2版",
"vip3": "大客户VIP3版"
}
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Sync the zh-CN bundle with the English key set.

src/i18n/locales/zh-CN.json is missing several keys that src/i18n/locales/en.json defines and the newly localized screens depend on, including user.fetching_addon_quote, user.per_year, user.upgrade_button, deps_table.cli_required, deps_table.native_package_with_name, deps_table.ota_version_with_name, bind_package.publish, and the entire publish_feature_table.* namespace. Chinese users will get fallback/raw keys in those views.

Also applies to: 633-688, 714-719

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/i18n/locales/zh-CN.json` around lines 49 - 135, The zh-CN locale bundle
is missing several keys that the new UI expects, so align it with the English
key set by adding the absent entries under user, deps_table, and the full
publish_feature_table namespace in zh-CN.json. Use the existing translation
patterns in that file to add localized strings for symbols like
user.fetching_addon_quote, user.per_year, user.upgrade_button,
deps_table.cli_required, deps_table.native_package_with_name,
deps_table.ota_version_with_name, bind_package.publish, and
publish_feature_table so the localized screens don’t fall back to raw keys.

Comment thread src/index.tsx
Comment on lines +68 to +70
function App() {
const { i18n } = useTranslation();
const antdLocale = antdLocaleMap[i18n.language] ?? zhCN;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use the resolved i18n language for the Antd locale lookup. Line 70 keys antdLocaleMap with i18n.language, which can stay as a detector value like en-US even when translations resolve to en. That misses the en entry and falls back to zhCN, so English pages can render with Chinese Antd locale strings.

Suggested fix
 function App() {
   const { i18n } = useTranslation();
-  const antdLocale = antdLocaleMap[i18n.language] ?? zhCN;
+  const language = i18n.resolvedLanguage ?? i18n.language;
+  const antdLocale = antdLocaleMap[language] ?? zhCN;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function App() {
const { i18n } = useTranslation();
const antdLocale = antdLocaleMap[i18n.language] ?? zhCN;
function App() {
const { i18n } = useTranslation();
const language = i18n.resolvedLanguage ?? i18n.language;
const antdLocale = antdLocaleMap[language] ?? zhCN;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/index.tsx` around lines 68 - 70, The Antd locale lookup in App is using
i18n.language directly, which can remain an unresolved detector value and miss
the normalized translation language. Update the locale selection to use the
resolved language from the i18n instance when indexing antdLocaleMap, so the App
component picks the correct Antd locale instead of falling back to zhCN for
English. Keep the fix localized to the App function and the antdLocaleMap
lookup.

Comment thread src/pages/audit-logs.tsx
Comment on lines +123 to +129
function getActionOptions(t: TranslateFn) {
return Object.values(getActionMap(t))
.sort()
.map((value) => ({
label: value,
value,
}));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Store a stable action key in the query param.

The select now uses the translated action text as its value, so changing languages (or opening a shared URL in another locale) leaves selectedAction unmatched and the filter stops working. Keep the URL value stable — e.g. METHOD + normalizedPath — and translate only the display label.

Also applies to: 236-238, 271-271

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/audit-logs.tsx` around lines 123 - 129, The audit log action filter
is using translated text as the query value, so `selectedAction` can break
across locales and shared URLs. Update `getActionOptions` (and the related
select/query handling in the audit logs page) to store a stable action key
derived from `getActionMap` output, such as `METHOD + normalizedPath`, and use
the translated string only for the displayed `label`. Make sure the same stable
key is used wherever the action value is written to or read from the URL/query
param so filtering and selection remain consistent after language changes.

Comment thread src/pages/audit-logs.tsx
};

export const AuditLogs = () => {
const { t } = useTranslation();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Sync Day.js locale with the active i18n language.

This page now translates its UI, but dayjs.locale('zh-cn') is still fixed at module load. date.fromNow() in the time column will stay Chinese in English mode, and the global Day.js locale can remain wrong after this page is opened.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/audit-logs.tsx` at line 217, The audit logs page is using a fixed
Day.js locale at module load, so relative timestamps from date.fromNow() can
stay in the wrong language even after useTranslation switches the UI language.
Update the audit-logs page logic around useTranslation and the time rendering to
sync Day.js with the active i18n language, preferably by setting dayjs.locale
from the current language inside the component lifecycle instead of once at
import time.

<span className="inline-flex items-center gap-2">
<Checkbox
aria-label={`${filterLabel}全选`}
aria-label={`${filterLabel}${t('common.save') ? '' : ''}`}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a descriptive bulk-select label here.

Line 73 always collapses to filterLabel, so the extra t('common.save') lookup is dead code and screen readers only get the filter name instead of the checkbox action.

Suggested fix
-        aria-label={`${filterLabel}${t('common.save') ? '' : ''}`}
+        aria-label={t('manage.select_visible_packages', {
+          filter: filterLabel,
+        })}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
aria-label={`${filterLabel}${t('common.save') ? '' : ''}`}
aria-label={t('manage.select_visible_packages', {
filter: filterLabel,
})}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/manage/index.tsx` at line 73, The bulk-select aria-label in the
Manage page is collapsing to just filterLabel because the conditional adds
nothing, so screen readers miss the action description. Update the label in the
manage page component to use a descriptive bulk-select text for the checkbox
action, and remove the dead t('common.save') branch from that aria-label
expression.

Comment on lines +95 to 100
const formatTooltipItem = (point: ChartDataPoint, t: (key: string) => string) => {
const countLabel = `${point.value.toLocaleString()}${t('realtime_metrics.checks_suffix')}`;
if (point.isTotal || point.sharePercent === undefined) {
return countLabel;
}
return `${countLabel} (${point.sharePercent.toFixed(1)}%)`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Avoid building localized phrases by concatenation.

checks_suffix and current_dimension currently rely on the translation text to carry the right spacing and word order, so English will render awkwardly (for example 1,234checks or Current dimensionBundle). These should be full interpolated messages instead of stitched fragments.

Also applies to: 493-499

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/realtime-metrics.tsx` around lines 95 - 100, The tooltip text in
formatTooltipItem is assembling localized output by concatenating translation
fragments, which can produce awkward spacing and word order. Update the
realtime-metrics translations and the code paths using
formatTooltipItem/current_dimension to use full interpolated messages instead of
stitching together checks_suffix or similar fragments, so the locale controls
the complete phrase naturally.

Comment on lines 21 to +25
current={Number(step)}
items={[
{ title: '输入绑定邮箱' },
{ title: '设置新密码' },
{ title: '设置成功' },
{ title: t('reset_password.step_email') },
{ title: t('reset_password.step_password') },
{ title: t('reset_password.step_success') },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The success route is one step past the Steps list.

SetPassword still routes to step '3', but this Steps component only has indices 0..2. On the success screen, current={Number(step)} becomes 3, so the indicator falls out of sync with the rendered content. Map the success route to display index 2 before passing it to current.

Suggested fix
-        current={Number(step)}
+        current={step === '3' ? 2 : Number(step)}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
current={Number(step)}
items={[
{ title: '输入绑定邮箱' },
{ title: '设置新密码' },
{ title: '设置成功' },
{ title: t('reset_password.step_email') },
{ title: t('reset_password.step_password') },
{ title: t('reset_password.step_success') },
current={step === '3' ? 2 : Number(step)}
items={[
{ title: t('reset_password.step_email') },
{ title: t('reset_password.step_password') },
{ title: t('reset_password.step_success') },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/reset-password/index.tsx` around lines 21 - 25, The reset-password
success route is still using a step value that is outside the Steps range, so
the progress indicator becomes out of sync on the success screen. Update the
reset-password page logic in the component that renders Steps so the success
route is mapped to display index 2 before passing the value into current, while
keeping the existing step routes and content rendering unchanged.

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