This is an automated email from the ASF dual-hosted git repository.

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new ce8acd080 fix(web): promote the byte unit at the rounding boundary and 
localize MiniBar labels (#4680)
ce8acd080 is described below

commit ce8acd0806f908702dc5bb8cd5af4da684e84612
Author: btlqql <[email protected]>
AuthorDate: Mon Sep 21 20:17:36 2026 +0800

    fix(web): promote the byte unit at the rounding boundary and localize 
MiniBar labels (#4680)
    
    Two web display fixes from the same author, consolidated into one change.
    
    1. `formatBytes` chose the unit from the unrounded value, so 1023.96 
rendered as "1024.0 B". It now re-checks after `toFixed` and promotes to the 
next unit at the boundary.
    2. `MiniBar` hard-coded Chinese `aria-label` strings for the trend and 
empty states; the dashboard renders it without a label, so screen readers got 
Chinese text in an English UI. Both now come from `charts.trendData` / 
`charts.noTrendData` with zh/en entries.
    
    Consolidates #4679 and #4680 (same author, both web display layer).
---
 web/src/components/MiniBar.tsx                | 14 ++++++--
 web/src/components/__tests__/MiniBar.test.tsx | 51 ++++++++++++++++++++++++++-
 web/src/i18n/translations.ts                  |  3 ++
 web/src/utils/format.test.ts                  |  7 ++++
 web/src/utils/format.ts                       |  9 ++++-
 5 files changed, 80 insertions(+), 4 deletions(-)

diff --git a/web/src/components/MiniBar.tsx b/web/src/components/MiniBar.tsx
index ee895a728..3d3454cab 100644
--- a/web/src/components/MiniBar.tsx
+++ b/web/src/components/MiniBar.tsx
@@ -15,6 +15,8 @@
  * limitations under the License.
  */
 
+import { useLang } from '../i18n/LangContext';
+
 interface MiniBarProps {
   data: number[];
   color?: string;
@@ -24,9 +26,15 @@ interface MiniBarProps {
 }
 
 const MiniBar = ({ data, color = '#1677ff', height = 32, width = 120, label }: 
MiniBarProps) => {
+  const { lang, t } = useLang();
+
   if (!data.length) {
     return (
-      <span aria-label={label || '暂无趋势数据'} style={{ color: '#8c8c8c' }}>
+      <span
+        role="img"
+        aria-label={label || t('charts.noTrendData')}
+        style={{ color: '#8c8c8c' }}
+      >
         —
       </span>
     );
@@ -38,7 +46,9 @@ const MiniBar = ({ data, color = '#1677ff', height = 32, 
width = 120, label }: M
   return (
     <div
       role="img"
-      aria-label={label || `趋势数据:${data.join('、')}`}
+      aria-label={
+        label || t('charts.trendData', { values: data.join(lang === 'zh' ? '、' 
: ', ') })
+      }
       style={{
         display: 'inline-flex',
         alignItems: 'flex-end',
diff --git a/web/src/components/__tests__/MiniBar.test.tsx 
b/web/src/components/__tests__/MiniBar.test.tsx
index 931deed5e..b22617e5f 100644
--- a/web/src/components/__tests__/MiniBar.test.tsx
+++ b/web/src/components/__tests__/MiniBar.test.tsx
@@ -15,13 +15,24 @@
  * limitations under the License.
  */
 
-import { describe, expect, it } from 'vitest';
+import { beforeEach, describe, expect, it } from 'vitest';
 import { render, screen } from '@testing-library/react';
 import MiniBar from '../MiniBar';
+import { LangProvider } from '../../i18n/LangContext';
+import { LANGUAGE_STORAGE_KEY } from '../../i18n/languagePreference';
 
 const getBarHeights = () =>
   Array.from(screen.getByRole('img').children, (bar) => (bar as 
HTMLElement).style.height);
 
+const getTrendLabel = () => screen.getByRole('img').getAttribute('aria-label');
+
+const renderTrend = (data: number[], label?: string) =>
+  render(
+    <LangProvider>
+      <MiniBar data={data} height={20} {...(label === undefined ? {} : { label 
})} />
+    </LangProvider>,
+  );
+
 describe('MiniBar', () => {
   it('renders zero values without a visible bar', () => {
     render(<MiniBar data={[0, 0, 0]} height={20} label="Throughput trend" />);
@@ -35,3 +46,41 @@ describe('MiniBar', () => {
     expect(getBarHeights()).toEqual(['0px', '4px', '20px']);
   });
 });
+
+describe('MiniBar accessible trend labels', () => {
+  beforeEach(() => {
+    localStorage.clear();
+  });
+
+  it('describes the trend in the active language', () => {
+    localStorage.setItem(LANGUAGE_STORAGE_KEY, 'en');
+
+    renderTrend([1, 2, 3]);
+
+    expect(getTrendLabel()).toBe('Trend: 1, 2, 3');
+  });
+
+  it('keeps the Chinese label and separator for the Chinese UI', () => {
+    localStorage.setItem(LANGUAGE_STORAGE_KEY, 'zh');
+
+    renderTrend([1, 2, 3]);
+
+    expect(getTrendLabel()).toBe('趋势数据:1、2、3');
+  });
+
+  it('describes an empty trend in the active language', () => {
+    localStorage.setItem(LANGUAGE_STORAGE_KEY, 'en');
+
+    renderTrend([]);
+
+    expect(getTrendLabel()).toBe('No trend data yet');
+  });
+
+  it('prefers an explicit label over the localized default', () => {
+    localStorage.setItem(LANGUAGE_STORAGE_KEY, 'en');
+
+    renderTrend([4], 'Throughput');
+
+    expect(getTrendLabel()).toBe('Throughput');
+  });
+});
diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts
index 9a7bbe368..aeb4aff90 100644
--- a/web/src/i18n/translations.ts
+++ b/web/src/i18n/translations.ts
@@ -116,6 +116,9 @@ const translations: Record<string, Record<Lang, string>> = {
   'dashboard.consumerGroups': { zh: '{n} 消费组', en: '{n} Groups' },
   'dashboard.healthy': { zh: '健康', en: 'Healthy' },
   'dashboard.last12h': { zh: '近 12 小时', en: 'Last 12 hours' },
+  // ─── Shared mini charts ───
+  'charts.noTrendData': { zh: '暂无趋势数据', en: 'No trend data yet' },
+  'charts.trendData': { zh: '趋势数据:{values}', en: 'Trend: {values}' },
   'dashboardTraffic.title': { zh: '流量洞察', en: 'Traffic Insights' },
   'dashboardTraffic.activeClusters': { zh: '活跃集群', en: 'Active Clusters' },
   'dashboardTraffic.topClusterShare': { zh: '最高流量占比', en: 'Top Traffic Share' 
},
diff --git a/web/src/utils/format.test.ts b/web/src/utils/format.test.ts
index efaadc28f..e76c8031b 100644
--- a/web/src/utils/format.test.ts
+++ b/web/src/utils/format.test.ts
@@ -28,6 +28,13 @@ describe('formatBytes', () => {
     expect(huge).not.toContain('undefined');
   });
 
+  it('promotes to the next unit when rounding reaches the boundary', () => {
+    expect(formatBytes(1023.96)).toBe('1.0 KB');
+    expect(formatBytes(1024 * 1024 - 1)).toBe('1.0 MB');
+    expect(formatBytes(1024 * 1024 - 1, 0)).toBe('1 MB');
+    expect(formatBytes(1024 * 1024 * 1024 - 1, 2)).toBe('1.00 GB');
+    expect(formatBytes(1024 ** 6)).toBe('1024.0 PB');
+  });
   it('handles non-finite input', () => {
     expect(formatBytes(Number.NaN)).toBe('-');
     expect(formatBytes(Number.POSITIVE_INFINITY)).toBe('-');
diff --git a/web/src/utils/format.ts b/web/src/utils/format.ts
index f73baf09c..defeee373 100644
--- a/web/src/utils/format.ts
+++ b/web/src/utils/format.ts
@@ -146,13 +146,20 @@ export function formatBytes(bytes: number, decimals = 1): 
string {
 
   const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
   const k = 1024;
+  const digits = safeDecimals(decimals);
   let i = 0;
   let value = Math.abs(bytes);
   while (value >= k && i < units.length - 1) {
     value /= k;
     i += 1;
   }
-  return `${value.toFixed(safeDecimals(decimals))} ${units[i]}`;
+  // The unit is chosen from the unrounded value, so a value just below a 
boundary used to render
+  // as 1024.0 KB - a mantissa of 1024 that the loop above exists to avoid.
+  while (i < units.length - 1 && Number(value.toFixed(digits)) >= k) {
+    value /= k;
+    i += 1;
+  }
+  return `${value.toFixed(digits)} ${units[i]}`;
 }
 
 /**

Reply via email to