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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@
"git-url-parse": "^16.1.0",
"hash-wasm": "^4.12.0",
"history": "^5.3.0",
"i18next": "^26.3.3",
"i18next-browser-languagedetector": "^8.2.1",
"json-diff-kit": "^1.0.35",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-i18next": "^17.0.8",
"react-router-dom": "^7.18.0",
"ua-parser-js": "^2.0.10",
"vanilla-jsoneditor": "^3.12.0",
Expand Down
18 changes: 12 additions & 6 deletions src/components/app-detail-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
} from '@ant-design/icons';
import { Breadcrumb, Button, Tag } from 'antd';
import type { ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import { cn } from '@/utils/helper';
import PlatformIcon from './platform-icon';

Expand All @@ -17,7 +18,7 @@ export interface AppDetailHeaderApp {
export function AppDetailHeader({
activeView,
app,
appNameFallback = '选择应用',
appNameFallback,
managementDisabled,
metricsDisabled,
onManagementClick,
Expand All @@ -37,6 +38,9 @@ export function AppDetailHeader({
sectionLabel: string;
settingsDisabled?: boolean;
}) {
const { t } = useTranslation();
const fallbackName = appNameFallback ?? t('app_detail_header.select_app');

return (
<div className="mb-4 grid grid-cols-1 items-center gap-3 md:grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)]">
<div className="flex min-w-0 items-center justify-between gap-3 md:contents">
Expand All @@ -51,9 +55,11 @@ export function AppDetailHeader({
<span className="inline-flex max-w-full items-center gap-1">
<PlatformIcon platform={app?.platform} className="mr-1" />
<span className="max-w-[160px] truncate md:max-w-none">
{app?.name || appNameFallback}
{app?.name || fallbackName}
</span>
{app?.status === 'paused' && <Tag className="ml-2">暂停</Tag>}
{app?.status === 'paused' && (
<Tag className="ml-2">{t('app_detail_header.paused')}</Tag>
)}
</span>
),
},
Expand All @@ -68,7 +74,7 @@ export function AppDetailHeader({
disabled={settingsDisabled}
onClick={onSettingsClick}
>
应用设置
{t('app_detail_header.app_settings')}
</Button>
)}
</div>
Expand All @@ -82,14 +88,14 @@ export function AppDetailHeader({
active={activeView === 'management'}
disabled={managementDisabled}
icon={<AppstoreOutlined />}
label="应用发布"
label={t('app_detail_header.tab_releases')}
onClick={onManagementClick}
/>
<AppDetailTab
active={activeView === 'metrics'}
disabled={metricsDisabled}
icon={<LineChartOutlined />}
label="实时数据"
label={t('app_detail_header.tab_metrics')}
onClick={onMetricsClick}
/>
</div>
Expand Down
48 changes: 32 additions & 16 deletions src/components/app-drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import { Empty, Grid, Input, Radio, Tag } from 'antd';
import type { ReactNode } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
cn,
getManageAppDrawerCollapsed,
Expand Down Expand Up @@ -74,6 +75,7 @@ export function AppDrawer({
onSettings?: (app: AppDrawerItem) => void;
placement: Exclude<ManageAppDrawerPlacement, 'hidden'>;
}) {
const { t } = useTranslation();
const [query, setQuery] = useState('');
const normalizedQuery = query.trim().toLowerCase();
const filteredApps = useMemo(() => {
Expand Down Expand Up @@ -111,13 +113,15 @@ export function AppDrawer({
>
<div className="flex h-full flex-col">
<button
aria-label={collapsed ? '展开应用列表' : '收起应用列表'}
aria-label={
collapsed ? t('app_drawer.expand') : t('app_drawer.collapse')
}
className={cn(
'flex w-full items-center border-0 border-slate-100 border-b bg-transparent text-left transition-colors hover:bg-slate-50',
collapsed ? 'h-14 justify-center' : 'justify-between p-3',
)}
onClick={() => onCollapsedChange(!collapsed)}
title={collapsed ? '展开应用列表' : '收起应用列表'}
title={collapsed ? t('app_drawer.expand') : t('app_drawer.collapse')}
type="button"
>
{collapsed ? (
Expand All @@ -129,9 +133,13 @@ export function AppDrawer({
<AppstoreOutlined className="text-slate-500" />
</span>
<div className="min-w-0">
<div className="font-medium text-slate-900">应用列表</div>
<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(),
})}
Comment on lines +136 to +142

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.

</div>
</div>
</div>
Expand All @@ -154,16 +162,22 @@ export function AppDrawer({
size="small"
value={placement}
>
<Radio.Button value="left">左侧</Radio.Button>
<Radio.Button value="right">右侧</Radio.Button>
<Radio.Button value="hidden">隐藏</Radio.Button>
<Radio.Button value="left">
{t('app_drawer.placement_left')}
</Radio.Button>
<Radio.Button value="right">
{t('app_drawer.placement_right')}
</Radio.Button>
<Radio.Button value="hidden">
{t('app_drawer.placement_hide')}
</Radio.Button>
</Radio.Group>
</div>
<div className="border-slate-100 border-b p-3">
<Input
allowClear
prefix={<SearchOutlined />}
placeholder="搜索应用"
placeholder={t('app_drawer.search_apps')}
value={query}
onChange={(event) => setQuery(event.target.value)}
/>
Expand Down Expand Up @@ -199,7 +213,7 @@ export function AppDrawer({
!isLoading && (
<Empty
className="my-8"
description="没有匹配的应用"
description={t('app_drawer.no_matching_apps')}
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
)
Expand Down Expand Up @@ -330,14 +344,15 @@ function AppIconButton({
isActive: boolean;
onSelect: (app: AppDrawerItem) => void;
}) {
const { t } = useTranslation();
return (
<button
className={cn(
'flex h-11 w-11 shrink-0 cursor-pointer items-center justify-center rounded-xl border-0 bg-transparent transition-colors hover:bg-slate-100',
isActive ? 'bg-blue-600 text-white hover:bg-blue-600' : undefined,
)}
onClick={() => onSelect(app)}
title={`${app.name} · ${formatCheckCount(app)}`}
title={`${app.name} · ${formatCheckCount(app, t)}`}
type="button"
>
<PlatformIcon platform={app.platform} />
Expand All @@ -356,6 +371,7 @@ function AppDrawerRow({
onSelect: (app: AppDrawerItem) => void;
onSettings?: (app: AppDrawerItem) => void;
}) {
const { t } = useTranslation();
return (
<div
aria-current={isActive ? 'page' : undefined}
Expand Down Expand Up @@ -386,7 +402,7 @@ function AppDrawerRow({
{app.name}
</span>
{app.status === 'paused' && (
<Tag className="m-0 shrink-0">暂停</Tag>
<Tag className="m-0 shrink-0">{t('app_drawer.paused')}</Tag>
)}
</span>
<span
Expand All @@ -395,19 +411,19 @@ function AppDrawerRow({
isActive ? 'text-blue-700' : 'text-slate-500',
)}
>
{formatCheckCount(app)}
{formatCheckCount(app, t)}
</span>
</span>
</button>
{onSettings && (
<button
aria-label={`打开 ${app.name} 应用设置`}
aria-label={t('app_drawer.open_app_settings', { name: app.name })}
className={cn(
'mr-2 flex h-8 w-8 shrink-0 cursor-pointer items-center justify-center rounded-md border-0 bg-transparent text-slate-400 opacity-0 transition-all hover:bg-white/80 hover:text-blue-600 hover:opacity-100 focus-visible:opacity-100 group-hover:opacity-100',
isActive ? 'text-blue-600 hover:bg-blue-50' : undefined,
)}
onClick={() => onSettings(app)}
title="应用设置"
title={t('app_drawer.app_settings')}
type="button"
>
<SettingOutlined />
Expand All @@ -417,6 +433,6 @@ function AppDrawerRow({
);
}

function formatCheckCount(app: AppDrawerItem) {
return `${(app.checkCount ?? 0).toLocaleString()} 次检查`;
function formatCheckCount(app: AppDrawerItem, t: (key: string) => string) {
return `${(app.checkCount ?? 0).toLocaleString()} ${t('app_drawer.checks')}`;
}
Loading