Skip to content

fix(web): complete i18n coverage#92

Merged
ZingerLittleBee merged 4 commits intomainfrom
net-i18n
Apr 12, 2026
Merged

fix(web): complete i18n coverage#92
ZingerLittleBee merged 4 commits intomainfrom
net-i18n

Conversation

@ZingerLittleBee
Copy link
Copy Markdown
Owner

@ZingerLittleBee ZingerLittleBee commented Apr 12, 2026

Summary

  • complete frontend i18n coverage across the web app, including dashboard widgets, status pages, settings flows, server detail surfaces, and shared accessibility labels
  • add dedicated docker and service-monitors locale namespaces and wire them into the i18n resource setup
  • localize remaining toasts, placeholders, table headers, aria labels, and helper copy while keeping technical terms like Docker, HTTP, TCP, CPU, and GeoIP intact where appropriate

Test Plan

  • make web-typecheck
  • bun x ultracite check apps/web/src
  • frontend test suite not run

Summary by CodeRabbit

  • New Features

    • App-wide internationalization: UI text, placeholders, empty states and accessibility labels are now translation-driven (English & Chinese).
    • Added many new translation resources for dashboard widgets, networking, Docker, servers, service monitors, settings, and status pages.
  • Bug Fixes / Improvements

    • Widget and component labels, empty messages and ARIA texts updated for consistent localized UX.
  • Tests

    • Added tests covering network details, probe settings, and target display with i18n integration.

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 12, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
server-bee-docs Ready Ready Preview, Comment Apr 12, 2026 3:24pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 12, 2026

📝 Walkthrough

Walkthrough

Adds comprehensive i18n across the web app: replaces hard-coded UI strings with useTranslation hooks, threads translation functions through components and helpers, introduces helper utilities and new locale namespaces/files (docker, service‑monitors), and updates tests to assert localized labels.

Changes

Cohort / File(s) Summary
Dashboard Widget Configuration
apps/web/src/components/dashboard/widget-config-dialog.tsx
Replaced hard-coded metric/time option arrays with translation-driven option builders; extended selection components with new translation props; updated widget config forms to use i18n labels/placeholders/messages.
Dashboard Widgets
apps/web/src/components/dashboard/widgets/alert-list.tsx, apps/web/src/components/dashboard/widgets/disk-io.tsx, apps/web/src/components/dashboard/widgets/server-map.tsx, apps/web/src/components/dashboard/widgets/service-status.tsx, apps/web/src/components/dashboard/widgets/traffic-bar.tsx, apps/web/src/components/dashboard/widgets/uptime-timeline-widget.tsx
Added useTranslation('dashboard') and replaced hard-coded titles, loading and empty-state texts with translation keys; updated helpers (e.g., useTrafficConfig, getStatusLabel) to accept t.
UI & Layout Components
apps/web/src/components/layout/theme-toggle.tsx, apps/web/src/components/ui/breadcrumb.tsx, apps/web/src/components/ui/dialog.tsx, apps/web/src/components/ui/sheet.tsx, apps/web/src/components/ui/sidebar.tsx
Integrated i18n for accessibility labels and control text; replaced fixed aria/sr-only labels with t(...) lookups from common namespace.
Network Components
apps/web/src/components/network/anomaly-table.tsx, apps/web/src/components/network/latency-chart.tsx, apps/web/src/components/network/target-card.tsx, apps/web/src/components/network/target-card.test.tsx
Replaced static column headers and empty-state text with getColumns(t); localized packet-loss label and chart titles; added test verifying i18n rendering.
Server Components
apps/web/src/components/server/server-card.tsx, apps/web/src/components/server/capabilities-dialog.tsx, apps/web/src/components/server/traffic-card.tsx, apps/web/src/components/server/traffic-tab.tsx
Threaded t into tooltips/aria labels and traffic UI; converted DAY_RANGES labels to keys; removed inline fallback strings from i18n calls.
Settings Components
apps/web/src/components/settings/geoip-card.tsx
Added useTranslation('settings') and localized GeoIP UI (status, download/update, license/provider labels); passed t through StatusDetails and DownloadButton.
Settings Routes
apps/web/src/routes/_authed/settings/alerts.tsx, api-keys.tsx, appearance.tsx, capabilities.tsx, mobile-devices.tsx, notifications.tsx, security.tsx, service-monitors.tsx, status-pages.tsx, users.tsx
Replaced toast messages and UI strings with i18n lookups; threaded t through helpers (e.g., formatRelativeTime, placeholders) and updated error fallback messages.
Server Routes
apps/web/src/routes/_authed/servers/$id.tsx, apps/web/src/routes/_authed/servers/index.tsx
Localized server detail labels (IPv4/IPv6/kernel/region/agent), tab titles, uptime title, toasts, and view-mode aria labels.
Docker Components
apps/web/src/routes/_authed/servers/$serverId/docker/index.tsx, .../container-detail-dialog.tsx, .../container-list.tsx, .../container-logs.tsx, .../docker-events.tsx, .../docker-networks-dialog.tsx, .../docker-volumes-dialog.tsx
Integrated useTranslation('docker') across Docker UI: titles, table headers, filters, empty states, logs, dialogs; adjusted helpers (e.g., formatCreatedDate) to accept t.
Network Routes
apps/web/src/routes/_authed/network/$serverId.tsx, apps/web/src/routes/_authed/network/$server-id.test.tsx
Added getNetworkProbeTypeLabel usage for localized probe-type labels; localized CSV export and target-update toasts and server info labels; added test for probe-type rendering.
Service Monitors Route
apps/web/src/routes/_authed/service-monitors/$id.tsx
Removed static type labels; introduced useTypeLabels(t) and useMonitorTypes; threaded t/tCommon into many presentational components; localized stats, charts, detail fields, history table and toast messages.
Network Probes Routes
apps/web/src/routes/_authed/settings/network-probes.tsx, apps/web/src/routes/_authed/settings/network-probes.test.tsx
Localized probe-type display via getNetworkProbeTypeLabel(t, ...); updated table headers, built-in/custom labels, action aria-labels; added test coverage.
Status Routes
apps/web/src/routes/status.tsx, apps/web/src/routes/status.$slug.tsx
Localized metric titles and relative-time formatting; updated formatRelative, ServerStatusDot, SeverityBadge, and propagation of t through Incident/Server rows.
Traffic Route
apps/web/src/routes/_authed/traffic/index.tsx
Added useTranslation('servers') and localized traffic overview title, stat labels, table headers, badges, empty states, and chart titles.
I18n Configuration & Utilities
apps/web/src/lib/i18n.ts, apps/web/src/lib/network-i18n.ts
Registered new locale namespaces (docker, service-monitors) in i18next resources; added getNetworkProbeTypeLabel utility and Translate type.
Locale Files - English
apps/web/src/locales/en/common.json, dashboard.json, docker.json, network.json, servers.json, service-monitors.json, settings.json, status.json
Added extensive new translation keys across namespaces for accessibility, widgets, forms, tables, toasts, statuses, and relative-time strings.
Locale Files - Chinese
apps/web/src/locales/zh/common.json, dashboard.json, docker.json, network.json, servers.json, service-monitors.json, settings.json, status.json
Added corresponding Chinese translations for the new keys to maintain parity with English locales.
Tests & New Files
apps/web/src/components/network/target-card.test.tsx, apps/web/src/routes/_authed/network/$server-id.test.tsx, apps/web/src/routes/_authed/settings/network-probes.test.tsx
Added tests that mock i18n and assert translated labels/values are rendered for TargetCard, NetworkDetailPage, and NetworkProbeSettingsPage.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 Hop, hop — I swapped strings for keys and song,

t() now helps each label sing along.
From dashboards, Docker, charts that chime,
The rabbit hops across each line.
Tiny hops, translated, one at a time.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'fix(web): complete i18n coverage' directly and clearly summarizes the main objective: adding comprehensive internationalization (i18n) support across the web application.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch net-i18n

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

# Conflicts:
#	apps/web/src/components/network/latency-chart.tsx
#	apps/web/src/components/server/server-card.tsx
#	apps/web/src/routes/_authed/settings/network-probes.tsx
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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 (4)
apps/web/src/components/dashboard/widgets/server-map.tsx (1)

141-143: ⚠️ Potential issue | 🟡 Minor

Localize marker tooltip count label to complete i18n coverage.

server/server(s) is still hardcoded English in the SVG tooltip. This leaves one user-visible string outside translations.

Suggested patch
-                  <title>
-                    {group.name}: {group.count} server{group.count > 1 ? 's' : ''}
+                  <title>
+                    {group.name}: {t('widgets.serverMap.serverCount', { count: group.count })}
                     {'\n'}
                     {group.serverNames.join(', ')}
                   </title>

Also add widgets.serverMap.serverCount with plural forms in both apps/web/src/locales/en/dashboard.json and apps/web/src/locales/zh/dashboard.json.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/dashboard/widgets/server-map.tsx` around lines 141 -
143, The tooltip still uses a hardcoded English plural "server/servers" in
server-map.tsx; change the title construction to use the i18n pluralized string
(e.g., call the translation function with a key like
widgets.serverMap.serverCount and pass group.count as the count) instead of the
inline "server{...}" logic (reference group.name and group.count in the title).
Then add the new locale key widgets.serverMap.serverCount with proper plural
forms to both apps/web/src/locales/en/dashboard.json and
apps/web/src/locales/zh/dashboard.json so the tooltip label is fully localized.
apps/web/src/components/server/traffic-tab.tsx (1)

226-228: ⚠️ Potential issue | 🟡 Minor

Localize the remaining empty-state copy.

Line 227 is still hardcoded English, so this screen is not fully localized yet.

🌐 Proposed fix
-          <div className="flex h-[200px] items-center justify-center text-muted-foreground text-sm">
-            No daily traffic data available.
-          </div>
+          <div className="flex h-[200px] items-center justify-center text-muted-foreground text-sm">
+            {t('traffic_daily_no_data', { defaultValue: 'No daily traffic data available.' })}
+          </div>

Also add traffic_daily_no_data to apps/web/src/locales/en/servers.json and apps/web/src/locales/zh/servers.json.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/server/traffic-tab.tsx` around lines 226 - 228,
Replace the hardcoded "No daily traffic data available." string in the
ServerTrafficTab component (traffic-tab.tsx) with a localized lookup (e.g., use
the project's translations hook such as useTranslations or t) and render the
translation key "traffic_daily_no_data" instead; then add the
"traffic_daily_no_data" entry to both the English and Chinese servers locale
JSON files with appropriate translations so the component displays the localized
empty-state copy.
apps/web/src/routes/_authed/settings/service-monitors.tsx (1)

482-488: ⚠️ Potential issue | 🟡 Minor

Variable shadowing: t in map callback shadows translation function.

The loop variable t shadows the t translation function from useTranslation. While this doesn't cause a bug in the current code (since t.label and t.value are intentional), it's confusing and error-prone if future changes need the translation function inside this callback.

♻️ Recommended fix: rename loop variable
                 <SelectContent>
-                  {MONITOR_TYPES.map((t) => (
-                    <SelectItem key={t.value} value={t.value}>
-                      {t.label}
+                  {MONITOR_TYPES.map((monitorType) => (
+                    <SelectItem key={monitorType.value} value={monitorType.value}>
+                      {monitorType.label}
                     </SelectItem>
                   ))}
                 </SelectContent>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/routes/_authed/settings/service-monitors.tsx` around lines 482 -
488, The map callback in the SelectContent rendering shadows the translation
function named t from useTranslation; change the loop variable name used in
MONITOR_TYPES.map (e.g., from t to typeItem or monitorType) and update
references inside the callback (key, value, and label) to the new name so you no
longer shadow the t translation function used elsewhere; ensure SelectItem
key={...}, value={...} and content use the renamed identifier.
apps/web/src/components/dashboard/widget-config-dialog.tsx (1)

221-221: ⚠️ Potential issue | 🟠 Major

Use <ScrollArea> instead of native overflow classes in this component.

Both scrollable containers still use overflow-y-auto / overflow-auto, which violates the component guideline for this path.

♻️ Guideline-aligned refactor sketch
 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
+import { ScrollArea } from '@/components/ui/scroll-area'
@@
-      <div className="max-h-40 space-y-1 overflow-y-auto rounded-lg border p-2">
+      <ScrollArea className="max-h-40 rounded-lg border p-2">
+        <div className="space-y-1">
         {servers.map((s) => (
           ...
         ))}
         {servers.length === 0 && <p className="py-2 text-center text-muted-foreground text-xs">{emptyMessage}</p>}
-      </div>
+        </div>
+      </ScrollArea>
@@
-          <div
-            className="prose prose-sm dark:prose-invert max-h-32 max-w-none overflow-auto text-sm"
+          <ScrollArea className="max-h-32">
+            <div
+              className="prose prose-sm dark:prose-invert max-w-none text-sm"
             // biome-ignore lint/security/noDangerouslySetInnerHtml: renderMarkdown escapes all raw HTML and validates URLs
             dangerouslySetInnerHTML={{ __html: html }}
-          />
+            />
+          </ScrollArea>

As per coding guidelines, apps/web/src/components/**/*.{ts,tsx}: All scrollable containers must use shadcn <ScrollArea> instead of native overflow-y-auto or overflow-auto.

Also applies to: 544-544

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/dashboard/widget-config-dialog.tsx` at line 221, The
div using native scrolling ("<div className=\"max-h-40 space-y-1 overflow-y-auto
rounded-lg border p-2\">") should be replaced with the shadcn ScrollArea pattern
inside the WidgetConfigDialog component: import ScrollArea (and
ScrollAreaViewport if your UI exposes it), wrap the scrollable content with
<ScrollArea> and place sizing/rounded/border/padding classes on the
ScrollAreaViewport (remove overflow-y-auto/overflow-auto from classNames), and
keep spacing classes (space-y-1) on the inner content container; apply the same
change for the other scrollable container in this file. Ensure imports are added
and no native overflow classes remain.
🧹 Nitpick comments (8)
apps/web/src/routes/_authed/servers/$serverId/docker/components/container-detail-dialog.tsx (1)

17-23: Avoid string sentinel ('None') for ports state.

Using 'None' as a control-flow sentinel is brittle. Prefer returning an array (or null) and branch on length.

Suggested patch
-function formatPortMapping(container: DockerContainer): string {
-  const mappings = container.ports
+function getPortMappings(container: DockerContainer): string[] {
+  return container.ports
     .filter((p) => p.public_port != null)
     .map((p) => `${p.ip ?? '0.0.0.0'}:${p.public_port} -> ${p.private_port}/${p.port_type}`)
-
-  return mappings.length > 0 ? mappings.join(', ') : 'None'
 }
@@
-  const portsDisplay = formatPortMapping(container)
+  const portMappings = getPortMappings(container)
@@
-              <p className="mt-0.5 font-mono text-sm">{portsDisplay === 'None' ? t('detail.noPorts') : portsDisplay}</p>
+              <p className="mt-0.5 font-mono text-sm">
+                {portMappings.length === 0 ? t('detail.noPorts') : portMappings.join(', ')}
+              </p>

Also applies to: 69-69

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/web/src/routes/_authed/servers/`$serverId/docker/components/container-detail-dialog.tsx
around lines 17 - 23, The formatPortMapping function currently returns the
string sentinel 'None' for no-port cases; change its signature to return an
array of strings or null (e.g., string[] | null), return mappings (empty case ->
null) instead of 'None', and update all call sites (notably the component render
that uses formatPortMapping and the other helper at the same file around the
other occurrence) to branch on a null/length check and join the array into a
display string only when non-null; ensure callers handle null consistently
rather than relying on the 'None' literal.
apps/web/src/lib/network-i18n.ts (1)

7-16: Normalize probeType before switching.

Case-sensitive matching can miss translation for values like HTTP/Tcp and leak raw strings.

♻️ Proposed fix
 export function getNetworkProbeTypeLabel(t: Translate, probeType: string): string {
-  switch (probeType) {
+  const normalized = probeType.trim().toLowerCase()
+  switch (normalized) {
     case 'icmp':
       return t('probe_type_icmp', { defaultValue: 'ICMP (Ping)' })
     case 'tcp':
       return t('probe_type_tcp', { defaultValue: 'TCP' })
     case 'http':
       return t('probe_type_http', { defaultValue: 'HTTP' })
     default:
       return probeType
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/lib/network-i18n.ts` around lines 7 - 16, Normalize the incoming
probeType before the switch in getNetworkProbeTypeLabel: create a normalized
string (e.g., normalizedProbeType = String(probeType).toLowerCase()) and switch
on that instead of the raw probeType so values like "HTTP" or "Tcp" match the
cases; keep returning the original probeType when falling through to the default
case to avoid changing output semantics.
apps/web/src/components/dashboard/widgets/service-status.tsx (1)

88-114: Use ScrollArea for the scrollable monitor list.

This block still relies on native overflow-auto on Line 91. Please swap the scrollable wrapper to the shared ScrollArea component so this widget follows the repo’s scrolling contract.

As per coding guidelines, "All scrollable containers must use shadcn <ScrollArea> instead of native overflow-y-auto or overflow-auto".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/dashboard/widgets/service-status.tsx` around lines 88
- 114, Replace the native scroll container (the div with classes "flex flex-1
flex-wrap content-start gap-2 overflow-auto" that maps over filtered to render
monitor items) with the shared shadcn ScrollArea component: import ScrollArea
from the shared UI module, remove the "overflow-auto" class, wrap the same
children (the filtered.map(...) block and the filtered.length === 0 fallback)
inside <ScrollArea> (and its viewport element if your ScrollArea API exposes
one), keeping the existing layout classes (flex flex-1 flex-wrap content-start
gap-2) so getStatusLabel/getStatusColor/formatRelativeTime usages and the
monitor mapping remain unchanged.
apps/web/src/routes/_authed/settings/service-monitors.tsx (1)

70-88: Consider renaming helper functions to follow conventions.

useMonitorTypes and useTypeLabels are named like React hooks but don't use any React hooks internally. This violates the naming convention that use* functions should only be hooks. Consider renaming to getMonitorTypes and getTypeLabels, or converting them to useMemo-based hooks if memoization is desired.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/routes/_authed/settings/service-monitors.tsx` around lines 70 -
88, Rename the helpers that are improperly named like hooks: change
useMonitorTypes to getMonitorTypes and useTypeLabels to getTypeLabels (or
alternatively wrap their returned values in useMemo inside new hook variants) so
they no longer use the "use*" prefix without being React hooks; update all
references to these functions (e.g., calls to useMonitorTypes and useTypeLabels)
to the new names to keep naming conventions consistent and avoid the false
hook-like signature.
apps/web/src/locales/en/network.json (1)

98-99: Consider consolidating duplicate "no data" keys.

latency_no_data and latency_chart_no_data both have identical values ("No data available"). If they're used in different contexts that might need different wording in other languages, this is fine. Otherwise, consider using a single key.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/locales/en/network.json` around lines 98 - 99, The two locale
keys latency_no_data and latency_chart_no_data are duplicates; either
consolidate them into a single key (e.g., keep latency_no_data and remove
latency_chart_no_data) and update all usages to reference the remaining key, or
if they represent different contexts keep both but rename one to be
context-specific and update translations accordingly; locate references to
latency_no_data and latency_chart_no_data in the UI components and replace or
rename them consistently (e.g., in the chart component use the chosen
chart-specific key or the consolidated key) so the JSON and code stay in sync.
apps/web/src/routes/_authed/service-monitors/$id.tsx (1)

57-65: Same naming convention note applies here.

useTypeLabels is duplicated from the settings page and has the same naming issue - it's named like a hook but doesn't use React hooks. Consider extracting to a shared module if both files need the same logic.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/routes/_authed/service-monitors/`$id.tsx around lines 57 - 65,
The function useTypeLabels is named like a React hook but doesn't use hooks and
is duplicated; rename it (e.g., getTypeLabels or typeLabelMap) and move the
implementation into a shared utility module, then update imports in both files
(replace useTypeLabels references with the new name) to avoid duplication and
follow hook naming conventions; ensure the exported symbol from the new module
accepts the same t: (key: string) => string signature so existing call sites
need only an import change.
apps/web/src/routes/status.$slug.tsx (1)

131-139: Consider type-safe severity key mapping.

The dynamic key construction incidents_severity_${severity} assumes severity always matches defined translation keys (critical, major, minor). If an unknown severity value is received from the API, the translation lookup will return the raw key.

🛡️ Optional: Add fallback handling
 function SeverityBadge({ severity, t }: { severity: string; t: (key: string, options?: { count: number }) => string }) {
   const variants: Record<string, 'default' | 'destructive' | 'secondary'> = {
     critical: 'destructive',
     major: 'destructive',
     minor: 'secondary'
   }
-  const severityKey = `incidents_severity_${severity}` as const
-  return <Badge variant={variants[severity] ?? 'default'}>{t(severityKey)}</Badge>
+  const knownSeverities = ['critical', 'major', 'minor']
+  const severityKey = knownSeverities.includes(severity) 
+    ? `incidents_severity_${severity}` 
+    : null
+  return <Badge variant={variants[severity] ?? 'default'}>{severityKey ? t(severityKey) : severity}</Badge>
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/routes/status`.$slug.tsx around lines 131 - 139, SeverityBadge
constructs a translation key using the raw severity, which can yield
untranslated keys for unknown values; update SeverityBadge to validate severity
against a known set or mapping (e.g., a const map like { critical:
'incidents_severity_critical', major: 'incidents_severity_major', minor:
'incidents_severity_minor' }) and use that mapped key or a safe fallback
key/string when severity is not recognized, and keep the existing Badge variant
mapping (variants[severity] ?? 'default') while passing the validated/mapped key
to t instead of the raw `severityKey`.
apps/web/src/locales/en/dashboard.json (1)

87-110: Minor duplication in translation keys.

widgets.common.labels.titleOptional (line 97) and dialogs.widgetConfig.labels.titleOptional (line 147) both contain "Title (optional)". Similarly for the widget title placeholder keys. If these are always meant to be identical, consider consolidating or referencing a single key to avoid divergence during future translation updates.

Also applies to: 142-157

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/locales/en/dashboard.json` around lines 87 - 110, Duplicate
translation values exist for widgets.common.labels.titleOptional and
dialogs.widgetConfig.labels.titleOptional (and the matching placeholders
widgetTitle); consolidate by creating a single shared key (e.g.,
common.labels.titleOptional and common.placeholders.widgetTitle) and update all
usages to reference that single key so translators edit one entry and values
cannot drift; ensure you remove the duplicate keys or replace their values with
references to the shared keys in the same JSON structure.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/src/components/dashboard/widget-config-dialog.tsx`:
- Around line 80-88: The time-range helper useRangeOptions is missing the 30-day
option, which makes components like traffic-bar (default hours: 720) have an
invalid/missing select value; update useRangeOptions to include a { label:
t('common.timeRange.30days'), value: '720' } entry (and similarly add the 30-day
option where the duplicate range list exists around lines 459-463) so the
RangeSelect can represent/select the 30-day (720 hours) choice and match
traffic-bar's default.

In `@apps/web/src/components/dashboard/widgets/alert-list.tsx`:
- Around line 58-60: Replace the native scrollable div that has class "flex
flex-1 flex-col gap-1.5 overflow-auto" with the shared shadcn ScrollArea
component: remove the overflow-auto class, import ScrollArea from the shared UI
module, wrap the scrollable content inside <ScrollArea> (using the ScrollArea
viewport pattern required by your UI library if applicable) and preserve the
existing layout classes (e.g., flex, flex-1, gap-1.5) on the ScrollArea or its
inner wrapper so visual layout remains the same; update the JSX in
alert-list.tsx where that div is defined (and add the corresponding import).

In `@apps/web/src/components/dashboard/widgets/disk-io.tsx`:
- Line 34: The widget currently uses hardcoded strings ("Read", "Write",
"Unknown" and other labels shown around the Disk I/O rendering) instead of
translation keys; update the Disk I/O component to replace those literals with
calls to the i18n hook (use the existing const { t } =
useTranslation('dashboard')) using descriptive keys like t('diskIo.read'),
t('diskIo.write'), t('diskIo.unknown') (and add analogous keys for the labels
referenced around the same area), and add corresponding entries in the dashboard
translation JSON so the UI uses the localized strings.

In `@apps/web/src/components/dashboard/widgets/service-status.tsx`:
- Around line 93-103: The tooltip/title currently concatenates a partially
translated string; replace the hardcoded English "Last check:" by using a single
translation key (e.g. "monitor.tooltip" or similar) with interpolation for name,
status and lastChecked time, then call t(...) to build the string used in the
div title and the tooltip body; update the title attribute and the tooltip
content where monitor, getStatusLabel(monitor, t) and
formatRelativeTime(monitor.last_checked_at) are used so both use the same i18n
key (pass { name: monitor.name, status: getStatusLabel(monitor, t), lastChecked:
formatRelativeTime(monitor.last_checked_at) }) instead of concatenation.

In `@apps/web/src/components/dashboard/widgets/traffic-bar.tsx`:
- Around line 83-88: The fallback translation used in the serverName useMemo
(the 'unknown' key at return servers.find(... )?.name ?? t('unknown')) is not
present in the widget's locale files; either change that call to use an existing
shared key (e.g., t('widgets.common.placeholders.unknown') or whatever shared
key exists) or add the 'unknown' entry to both dashboard locale JSONs under the
same namespace used by this widget so the subtitle is translated; update the
serverName useMemo call or the locale files accordingly.

In `@apps/web/src/components/dashboard/widgets/uptime-timeline-widget.tsx`:
- Line 78: Replace the native scrollable div (the JSX element with className
"flex-1 space-y-3 overflow-auto" in the UptimeTimelineWidget component) with the
shadcn ScrollArea wrapper: import the ScrollArea component and wrap the same
contents using ScrollArea (and its viewport if your project pattern requires
it), move the "flex-1 space-y-3" classes to the ScrollArea/viewport element and
remove the native "overflow-auto" class; ensure the surrounding layout and
children remain unchanged so visual behavior is preserved.

In `@apps/web/src/components/server/traffic-card.tsx`:
- Line 40: The component renders hardcoded legend labels ("In", "Out", "Total")
instead of using i18n; update the TrafficCard UI to call the translation
function for those strings (replace the literal "In"/"Out"/"Total" used near
CardTitle and the legend elements with t('traffic_in'), t('traffic_out'),
t('traffic_total') or the existing locale keys), ensuring both the legend items
and any other hardcoded labels in the component are wired through t(...) for
full localization.

In `@apps/web/src/routes/_authed/settings/mobile-devices.tsx`:
- Around line 35-41: formatRelativeTime currently constructs a Date from dateStr
and may return "Invalid Date" to users; update the function (formatRelativeTime)
to validate the parsed date by checking date.getTime() for NaN (or use
Number.isFinite) after creating const date = new Date(dateStr) and if invalid
return t('mobile.time_never'); keep the existing null check for dateStr and
proceed to compute diffMs only when the date is valid.

In `@apps/web/src/routes/_authed/settings/network-probes.tsx`:
- Around line 231-243: The column with accessorKey 'source' currently uses
header: t('target_status', ...) but displays "Built-in"/"Custom"; change the
header to use a matching i18n key (e.g. t('source' or t('origin') with
defaultValue 'Source'/'Origin') so the label reflects the rendered values,
update the translation files for that new key, and ensure the header replacement
is applied where the column is defined (the header property on the column with
accessorKey 'source') to keep locales consistent.

In `@apps/web/src/routes/_authed/traffic/index.tsx`:
- Line 188: The chart legend labels are hardcoded as "Inbound"/"Outbound"
despite bringing in useTranslation; update the legend labels in the chart
configuration (where "Inbound" and "Outbound" are used) to use the translation
function `t` instead (for example `t('inbound')` / `t('outbound')` or the
appropriate keys in the 'servers' namespace) so the chart uses localized
strings; ensure the same translation keys are added to the i18n resource files
and that the component still passes the translated values to the chart's
legend/series labels (look for the variables/props that currently contain
"Inbound"/"Outbound").

---

Outside diff comments:
In `@apps/web/src/components/dashboard/widget-config-dialog.tsx`:
- Line 221: The div using native scrolling ("<div className=\"max-h-40 space-y-1
overflow-y-auto rounded-lg border p-2\">") should be replaced with the shadcn
ScrollArea pattern inside the WidgetConfigDialog component: import ScrollArea
(and ScrollAreaViewport if your UI exposes it), wrap the scrollable content with
<ScrollArea> and place sizing/rounded/border/padding classes on the
ScrollAreaViewport (remove overflow-y-auto/overflow-auto from classNames), and
keep spacing classes (space-y-1) on the inner content container; apply the same
change for the other scrollable container in this file. Ensure imports are added
and no native overflow classes remain.

In `@apps/web/src/components/dashboard/widgets/server-map.tsx`:
- Around line 141-143: The tooltip still uses a hardcoded English plural
"server/servers" in server-map.tsx; change the title construction to use the
i18n pluralized string (e.g., call the translation function with a key like
widgets.serverMap.serverCount and pass group.count as the count) instead of the
inline "server{...}" logic (reference group.name and group.count in the title).
Then add the new locale key widgets.serverMap.serverCount with proper plural
forms to both apps/web/src/locales/en/dashboard.json and
apps/web/src/locales/zh/dashboard.json so the tooltip label is fully localized.

In `@apps/web/src/components/server/traffic-tab.tsx`:
- Around line 226-228: Replace the hardcoded "No daily traffic data available."
string in the ServerTrafficTab component (traffic-tab.tsx) with a localized
lookup (e.g., use the project's translations hook such as useTranslations or t)
and render the translation key "traffic_daily_no_data" instead; then add the
"traffic_daily_no_data" entry to both the English and Chinese servers locale
JSON files with appropriate translations so the component displays the localized
empty-state copy.

In `@apps/web/src/routes/_authed/settings/service-monitors.tsx`:
- Around line 482-488: The map callback in the SelectContent rendering shadows
the translation function named t from useTranslation; change the loop variable
name used in MONITOR_TYPES.map (e.g., from t to typeItem or monitorType) and
update references inside the callback (key, value, and label) to the new name so
you no longer shadow the t translation function used elsewhere; ensure
SelectItem key={...}, value={...} and content use the renamed identifier.

---

Nitpick comments:
In `@apps/web/src/components/dashboard/widgets/service-status.tsx`:
- Around line 88-114: Replace the native scroll container (the div with classes
"flex flex-1 flex-wrap content-start gap-2 overflow-auto" that maps over
filtered to render monitor items) with the shared shadcn ScrollArea component:
import ScrollArea from the shared UI module, remove the "overflow-auto" class,
wrap the same children (the filtered.map(...) block and the filtered.length ===
0 fallback) inside <ScrollArea> (and its viewport element if your ScrollArea API
exposes one), keeping the existing layout classes (flex flex-1 flex-wrap
content-start gap-2) so getStatusLabel/getStatusColor/formatRelativeTime usages
and the monitor mapping remain unchanged.

In `@apps/web/src/lib/network-i18n.ts`:
- Around line 7-16: Normalize the incoming probeType before the switch in
getNetworkProbeTypeLabel: create a normalized string (e.g., normalizedProbeType
= String(probeType).toLowerCase()) and switch on that instead of the raw
probeType so values like "HTTP" or "Tcp" match the cases; keep returning the
original probeType when falling through to the default case to avoid changing
output semantics.

In `@apps/web/src/locales/en/dashboard.json`:
- Around line 87-110: Duplicate translation values exist for
widgets.common.labels.titleOptional and
dialogs.widgetConfig.labels.titleOptional (and the matching placeholders
widgetTitle); consolidate by creating a single shared key (e.g.,
common.labels.titleOptional and common.placeholders.widgetTitle) and update all
usages to reference that single key so translators edit one entry and values
cannot drift; ensure you remove the duplicate keys or replace their values with
references to the shared keys in the same JSON structure.

In `@apps/web/src/locales/en/network.json`:
- Around line 98-99: The two locale keys latency_no_data and
latency_chart_no_data are duplicates; either consolidate them into a single key
(e.g., keep latency_no_data and remove latency_chart_no_data) and update all
usages to reference the remaining key, or if they represent different contexts
keep both but rename one to be context-specific and update translations
accordingly; locate references to latency_no_data and latency_chart_no_data in
the UI components and replace or rename them consistently (e.g., in the chart
component use the chosen chart-specific key or the consolidated key) so the JSON
and code stay in sync.

In
`@apps/web/src/routes/_authed/servers/`$serverId/docker/components/container-detail-dialog.tsx:
- Around line 17-23: The formatPortMapping function currently returns the string
sentinel 'None' for no-port cases; change its signature to return an array of
strings or null (e.g., string[] | null), return mappings (empty case -> null)
instead of 'None', and update all call sites (notably the component render that
uses formatPortMapping and the other helper at the same file around the other
occurrence) to branch on a null/length check and join the array into a display
string only when non-null; ensure callers handle null consistently rather than
relying on the 'None' literal.

In `@apps/web/src/routes/_authed/service-monitors/`$id.tsx:
- Around line 57-65: The function useTypeLabels is named like a React hook but
doesn't use hooks and is duplicated; rename it (e.g., getTypeLabels or
typeLabelMap) and move the implementation into a shared utility module, then
update imports in both files (replace useTypeLabels references with the new
name) to avoid duplication and follow hook naming conventions; ensure the
exported symbol from the new module accepts the same t: (key: string) => string
signature so existing call sites need only an import change.

In `@apps/web/src/routes/_authed/settings/service-monitors.tsx`:
- Around line 70-88: Rename the helpers that are improperly named like hooks:
change useMonitorTypes to getMonitorTypes and useTypeLabels to getTypeLabels (or
alternatively wrap their returned values in useMemo inside new hook variants) so
they no longer use the "use*" prefix without being React hooks; update all
references to these functions (e.g., calls to useMonitorTypes and useTypeLabels)
to the new names to keep naming conventions consistent and avoid the false
hook-like signature.

In `@apps/web/src/routes/status`.$slug.tsx:
- Around line 131-139: SeverityBadge constructs a translation key using the raw
severity, which can yield untranslated keys for unknown values; update
SeverityBadge to validate severity against a known set or mapping (e.g., a const
map like { critical: 'incidents_severity_critical', major:
'incidents_severity_major', minor: 'incidents_severity_minor' }) and use that
mapped key or a safe fallback key/string when severity is not recognized, and
keep the existing Badge variant mapping (variants[severity] ?? 'default') while
passing the validated/mapped key to t instead of the raw `severityKey`.
🪄 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: 49b4c098-6fba-4cf9-bbbf-8fc35c42691f

📥 Commits

Reviewing files that changed from the base of the PR and between 68347fb and f6483aa.

📒 Files selected for processing (67)
  • apps/web/src/components/dashboard/widget-config-dialog.tsx
  • apps/web/src/components/dashboard/widgets/alert-list.tsx
  • apps/web/src/components/dashboard/widgets/disk-io.tsx
  • apps/web/src/components/dashboard/widgets/server-map.tsx
  • apps/web/src/components/dashboard/widgets/service-status.tsx
  • apps/web/src/components/dashboard/widgets/traffic-bar.tsx
  • apps/web/src/components/dashboard/widgets/uptime-timeline-widget.tsx
  • apps/web/src/components/layout/theme-toggle.tsx
  • apps/web/src/components/network/anomaly-table.tsx
  • apps/web/src/components/network/latency-chart.tsx
  • apps/web/src/components/network/target-card.test.tsx
  • apps/web/src/components/network/target-card.tsx
  • apps/web/src/components/server/capabilities-dialog.tsx
  • apps/web/src/components/server/server-card.tsx
  • apps/web/src/components/server/traffic-card.tsx
  • apps/web/src/components/server/traffic-tab.tsx
  • apps/web/src/components/settings/geoip-card.tsx
  • apps/web/src/components/ui/breadcrumb.tsx
  • apps/web/src/components/ui/dialog.tsx
  • apps/web/src/components/ui/sheet.tsx
  • apps/web/src/components/ui/sidebar.tsx
  • apps/web/src/components/uptime/uptime-timeline.tsx
  • apps/web/src/lib/i18n.ts
  • apps/web/src/lib/network-i18n.ts
  • apps/web/src/locales/en/common.json
  • apps/web/src/locales/en/dashboard.json
  • apps/web/src/locales/en/docker.json
  • apps/web/src/locales/en/network.json
  • apps/web/src/locales/en/servers.json
  • apps/web/src/locales/en/service-monitors.json
  • apps/web/src/locales/en/settings.json
  • apps/web/src/locales/en/status.json
  • apps/web/src/locales/zh/common.json
  • apps/web/src/locales/zh/dashboard.json
  • apps/web/src/locales/zh/docker.json
  • apps/web/src/locales/zh/network.json
  • apps/web/src/locales/zh/servers.json
  • apps/web/src/locales/zh/service-monitors.json
  • apps/web/src/locales/zh/settings.json
  • apps/web/src/locales/zh/status.json
  • apps/web/src/routes/_authed/network/$server-id.test.tsx
  • apps/web/src/routes/_authed/network/$serverId.tsx
  • apps/web/src/routes/_authed/servers/$id.tsx
  • apps/web/src/routes/_authed/servers/$serverId/docker/components/container-detail-dialog.tsx
  • apps/web/src/routes/_authed/servers/$serverId/docker/components/container-list.tsx
  • apps/web/src/routes/_authed/servers/$serverId/docker/components/container-logs.tsx
  • apps/web/src/routes/_authed/servers/$serverId/docker/components/docker-events.tsx
  • apps/web/src/routes/_authed/servers/$serverId/docker/components/docker-networks-dialog.tsx
  • apps/web/src/routes/_authed/servers/$serverId/docker/components/docker-volumes-dialog.tsx
  • apps/web/src/routes/_authed/servers/$serverId/docker/index.tsx
  • apps/web/src/routes/_authed/servers/index.tsx
  • apps/web/src/routes/_authed/service-monitors/$id.tsx
  • apps/web/src/routes/_authed/settings/alerts.tsx
  • apps/web/src/routes/_authed/settings/api-keys.tsx
  • apps/web/src/routes/_authed/settings/appearance.tsx
  • apps/web/src/routes/_authed/settings/capabilities.tsx
  • apps/web/src/routes/_authed/settings/mobile-devices.tsx
  • apps/web/src/routes/_authed/settings/network-probes.test.tsx
  • apps/web/src/routes/_authed/settings/network-probes.tsx
  • apps/web/src/routes/_authed/settings/notifications.tsx
  • apps/web/src/routes/_authed/settings/security.tsx
  • apps/web/src/routes/_authed/settings/service-monitors.tsx
  • apps/web/src/routes/_authed/settings/status-pages.tsx
  • apps/web/src/routes/_authed/settings/users.tsx
  • apps/web/src/routes/_authed/traffic/index.tsx
  • apps/web/src/routes/status.$slug.tsx
  • apps/web/src/routes/status.tsx

Comment on lines +80 to +88
function useRangeOptions(t: (key: string) => string): { label: string; value: string }[] {
return [
{ label: t('common.timeRange.1hour'), value: '1' },
{ label: t('common.timeRange.6hours'), value: '6' },
{ label: t('common.timeRange.12hours'), value: '12' },
{ label: t('common.timeRange.24hours'), value: '24' },
{ label: t('common.timeRange.3days'), value: '72' },
{ label: t('common.timeRange.7days'), value: '168' }
]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

RangeSelect dropped the 30-day option while traffic-bar still defaults to 720.

This creates an invalid/default-missing select state for traffic-bar (hours: 720) and prevents selecting 30 days in that form.

💡 Proposed fix
function useRangeOptions(t: (key: string) => string): { label: string; value: string }[] {
  return [
    { label: t('common.timeRange.1hour'), value: '1' },
    { label: t('common.timeRange.6hours'), value: '6' },
    { label: t('common.timeRange.12hours'), value: '12' },
    { label: t('common.timeRange.24hours'), value: '24' },
    { label: t('common.timeRange.3days'), value: '72' },
-    { label: t('common.timeRange.7days'), value: '168' }
+    { label: t('common.timeRange.7days'), value: '168' },
+    { label: t('common.timeRange.30days'), value: '720' }
  ]
}

Also applies to: 459-463

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/dashboard/widget-config-dialog.tsx` around lines 80 -
88, The time-range helper useRangeOptions is missing the 30-day option, which
makes components like traffic-bar (default hours: 720) have an invalid/missing
select value; update useRangeOptions to include a { label:
t('common.timeRange.30days'), value: '720' } entry (and similarly add the 30-day
option where the duplicate range list exists around lines 459-463) so the
RangeSelect can represent/select the 30-day (720 hours) choice and match
traffic-bar's default.

Comment on lines 58 to 60
<div className="flex h-full flex-col rounded-lg border bg-card p-4">
<h3 className="mb-3 font-semibold text-sm">Alerts</h3>
<h3 className="mb-3 font-semibold text-sm">{t('widgets.alertList.title')}</h3>
<div className="flex flex-1 flex-col gap-1.5 overflow-auto">
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Replace native scroll overflow with <ScrollArea>.

Line 60 still uses overflow-auto in a scrollable container. This should use the shared shadcn ScrollArea wrapper.

Proposed fix
+import { ScrollArea } from '@/components/ui/scroll-area'
...
-      <div className="flex flex-1 flex-col gap-1.5 overflow-auto">
-        {filtered.map((event) => {
+      <ScrollArea className="flex-1">
+        <div className="flex flex-col gap-1.5">
+          {filtered.map((event) => {
           ...
-        {filtered.length === 0 && (
-          <div className="flex flex-1 items-center justify-center text-muted-foreground text-xs">
-            {t('widgets.alertList.empty.noEvents')}
-          </div>
-        )}
-      </div>
+          {filtered.length === 0 && (
+            <div className="flex min-h-[120px] items-center justify-center text-muted-foreground text-xs">
+              {t('widgets.alertList.empty.noEvents')}
+            </div>
+          )}
+        </div>
+      </ScrollArea>
As per coding guidelines `apps/web/src/components/**/*.{ts,tsx}`: All scrollable containers must use shadcn `` instead of native `overflow-y-auto` or `overflow-auto`.
📝 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
<div className="flex h-full flex-col rounded-lg border bg-card p-4">
<h3 className="mb-3 font-semibold text-sm">Alerts</h3>
<h3 className="mb-3 font-semibold text-sm">{t('widgets.alertList.title')}</h3>
<div className="flex flex-1 flex-col gap-1.5 overflow-auto">
import { ScrollArea } from '@/components/ui/scroll-area'
<div className="flex h-full flex-col rounded-lg border bg-card p-4">
<h3 className="mb-3 font-semibold text-sm">{t('widgets.alertList.title')}</h3>
<ScrollArea className="flex-1">
<div className="flex flex-col gap-1.5">
{filtered.map((event) => {
...
})}
{filtered.length === 0 && (
<div className="flex min-h-[120px] items-center justify-center text-muted-foreground text-xs">
{t('widgets.alertList.empty.noEvents')}
</div>
)}
</div>
</ScrollArea>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/dashboard/widgets/alert-list.tsx` around lines 58 -
60, Replace the native scrollable div that has class "flex flex-1 flex-col
gap-1.5 overflow-auto" with the shared shadcn ScrollArea component: remove the
overflow-auto class, import ScrollArea from the shared UI module, wrap the
scrollable content inside <ScrollArea> (using the ScrollArea viewport pattern
required by your UI library if applicable) and preserve the existing layout
classes (e.g., flex, flex-1, gap-1.5) on the ScrollArea or its inner wrapper so
visual layout remains the same; update the JSX in alert-list.tsx where that div
is defined (and add the corresponding import).

} satisfies ChartConfig

export function DiskIoWidget({ config, servers }: DiskIoWidgetProps) {
const { t } = useTranslation('dashboard')
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

I18n coverage is still partial in this widget.

Good progress on title/empty-state, but this component still shows hardcoded user-facing strings (Read, Write, Unknown). Please move those to translations too so locale output is fully consistent.

Also applies to: 64-67, 76-76

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/dashboard/widgets/disk-io.tsx` at line 34, The widget
currently uses hardcoded strings ("Read", "Write", "Unknown" and other labels
shown around the Disk I/O rendering) instead of translation keys; update the
Disk I/O component to replace those literals with calls to the i18n hook (use
the existing const { t } = useTranslation('dashboard')) using descriptive keys
like t('diskIo.read'), t('diskIo.write'), t('diskIo.unknown') (and add analogous
keys for the labels referenced around the same area), and add corresponding
entries in the dashboard translation JSON so the UI uses the localized strings.

Comment on lines 93 to +103
<div
className="group relative inline-flex items-center justify-center"
key={monitor.id}
title={`${monitor.name} - ${getStatusLabel(monitor)} - Last check: ${formatRelativeTime(monitor.last_checked_at)}`}
title={`${monitor.name} - ${getStatusLabel(monitor, t)} - Last check: ${formatRelativeTime(monitor.last_checked_at)}`}
>
<span className={`inline-block size-3.5 rounded-full ${getStatusColor(monitor)} cursor-default`} />
<div className="pointer-events-none absolute bottom-full left-1/2 z-10 mb-2 hidden -translate-x-1/2 rounded-md bg-popover px-2.5 py-1.5 shadow-md ring-1 ring-border group-hover:block">
<div className="whitespace-nowrap text-xs">
<p className="font-medium">{monitor.name}</p>
<p className="text-muted-foreground">
{getStatusLabel(monitor)} &middot; {formatRelativeTime(monitor.last_checked_at)}
{getStatusLabel(monitor, t)} &middot; {formatRelativeTime(monitor.last_checked_at)}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Localize the remaining tooltip copy.

Line 96 still hardcodes Last check: in English, so this widget is not fully translated yet. Please move the full title format into a translation key with interpolation instead of concatenating a partially translated string.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/dashboard/widgets/service-status.tsx` around lines 93
- 103, The tooltip/title currently concatenates a partially translated string;
replace the hardcoded English "Last check:" by using a single translation key
(e.g. "monitor.tooltip" or similar) with interpolation for name, status and
lastChecked time, then call t(...) to build the string used in the div title and
the tooltip body; update the title attribute and the tooltip content where
monitor, getStatusLabel(monitor, t) and
formatRelativeTime(monitor.last_checked_at) are used so both use the same i18n
key (pass { name: monitor.name, status: getStatusLabel(monitor, t), lastChecked:
formatRelativeTime(monitor.last_checked_at) }) instead of concatenation.

Comment on lines 83 to +88
const serverName = useMemo(() => {
if (!hasServerId) {
return 'All Servers'
return t('widgets.common.placeholders.allServers')
}
return servers.find((s) => s.id === server_id)?.name ?? 'Unknown'
}, [hasServerId, server_id, servers])
return servers.find((s) => s.id === server_id)?.name ?? t('unknown')
}, [hasServerId, server_id, servers, t])
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add a locale entry for the unknown-server fallback.

Line 87 introduces a new translated fallback, but the locale additions shown for this widget only cover the widgets.* keys plus widgets.common.placeholders.allServers. Please wire this fallback to an existing shared key or add it to both dashboard locale files so the subtitle does not stay untranslated.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/dashboard/widgets/traffic-bar.tsx` around lines 83 -
88, The fallback translation used in the serverName useMemo (the 'unknown' key
at return servers.find(... )?.name ?? t('unknown')) is not present in the
widget's locale files; either change that call to use an existing shared key
(e.g., t('widgets.common.placeholders.unknown') or whatever shared key exists)
or add the 'unknown' entry to both dashboard locale JSONs under the same
namespace used by this widget so the subtitle is translated; update the
serverName useMemo call or the locale files accordingly.

<div className="flex h-full flex-col rounded-lg border bg-card p-4">
<h3 className="mb-3 font-semibold text-sm">Uptime Timeline</h3>
<h3 className="mb-3 font-semibold text-sm">{t('widgets.uptimeTimeline.title')}</h3>
<div className="flex-1 space-y-3 overflow-auto">
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Replace native overflow-auto with <ScrollArea> in widget list.

The scrollable list container is using native overflow styles instead of the required shadcn scroll wrapper.

Suggested patch
+import { ScrollArea } from '@/components/ui/scroll-area'
@@
-      <div className="flex-1 space-y-3 overflow-auto">
-        {serverIds.map((id, i) => {
+      <ScrollArea className="flex-1">
+        <div className="space-y-3">
+          {serverIds.map((id, i) => {
           const uptimeData = queries[i]?.data ?? []
           const pct = computeAggregateUptime(uptimeData)
           const name = serverNameMap.get(id) ?? id
           return (
             <div key={id}>
@@
             </div>
           )
-        })}
-      </div>
+          })}
+        </div>
+      </ScrollArea>

As per coding guidelines apps/web/src/components/**/*.{ts,tsx}: All scrollable containers must use shadcn <ScrollArea> instead of native overflow-y-auto or overflow-auto.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/dashboard/widgets/uptime-timeline-widget.tsx` at line
78, Replace the native scrollable div (the JSX element with className "flex-1
space-y-3 overflow-auto" in the UptimeTimelineWidget component) with the shadcn
ScrollArea wrapper: import the ScrollArea component and wrap the same contents
using ScrollArea (and its viewport if your project pattern requires it), move
the "flex-1 space-y-3" classes to the ScrollArea/viewport element and remove the
native "overflow-auto" class; ensure the surrounding layout and children remain
unchanged so visual behavior is preserved.

<Tabs className="gap-0" defaultValue={defaultTab}>
<CardHeader>
<CardTitle>{t('traffic_title', { defaultValue: 'Traffic Statistics' })}</CardTitle>
<CardTitle>{t('traffic_title')}</CardTitle>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

This card still has untranslated UI labels.

Nice cleanup removing defaultValue, but this component still renders hardcoded In/Out/Total (including legend labels). Since locale keys already exist, please wire those through t(...) as well for complete coverage.

Also applies to: 44-45

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/server/traffic-card.tsx` at line 40, The component
renders hardcoded legend labels ("In", "Out", "Total") instead of using i18n;
update the TrafficCard UI to call the translation function for those strings
(replace the literal "In"/"Out"/"Total" used near CardTitle and the legend
elements with t('traffic_in'), t('traffic_out'), t('traffic_total') or the
existing locale keys), ensuring both the legend items and any other hardcoded
labels in the component are wired through t(...) for full localization.

Comment on lines +35 to 41
function formatRelativeTime(dateStr: string | null, t: (key: string, options?: { count: number }) => string): string {
if (!dateStr) {
return 'Never'
return t('mobile.time_never')
}
const date = new Date(dateStr)
const now = new Date()
const diffMs = now.getTime() - date.getTime()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard malformed timestamps in formatRelativeTime.

If dateStr is invalid, the current path can surface "Invalid Date" to users.

🛡️ Proposed fix
 function formatRelativeTime(dateStr: string | null, t: (key: string, options?: { count: number }) => string): string {
   if (!dateStr) {
     return t('mobile.time_never')
   }
   const date = new Date(dateStr)
+  if (Number.isNaN(date.getTime())) {
+    return t('mobile.time_never')
+  }
   const now = new Date()
   const diffMs = now.getTime() - date.getTime()
📝 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 formatRelativeTime(dateStr: string | null, t: (key: string, options?: { count: number }) => string): string {
if (!dateStr) {
return 'Never'
return t('mobile.time_never')
}
const date = new Date(dateStr)
const now = new Date()
const diffMs = now.getTime() - date.getTime()
function formatRelativeTime(dateStr: string | null, t: (key: string, options?: { count: number }) => string): string {
if (!dateStr) {
return t('mobile.time_never')
}
const date = new Date(dateStr)
if (Number.isNaN(date.getTime())) {
return t('mobile.time_never')
}
const now = new Date()
const diffMs = now.getTime() - date.getTime()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/routes/_authed/settings/mobile-devices.tsx` around lines 35 -
41, formatRelativeTime currently constructs a Date from dateStr and may return
"Invalid Date" to users; update the function (formatRelativeTime) to validate
the parsed date by checking date.getTime() for NaN (or use Number.isFinite)
after creating const date = new Date(dateStr) and if invalid return
t('mobile.time_never'); keep the existing null check for dateStr and proceed to
compute diffMs only when the date is valid.

Comment on lines 231 to 243
{
accessorKey: 'source',
header: 'Status',
header: t('target_status', { defaultValue: 'Status' }),
enableSorting: false,
cell: ({ row }) =>
row.original.source ? (
<span className="flex items-center gap-1 text-muted-foreground text-xs">
<Lock aria-hidden="true" className="size-3" />
{row.original.source_name ?? t('preset')}
{row.original.source_name ?? t('builtin', { defaultValue: 'Built-in' })}
</span>
) : (
<span className="text-muted-foreground text-xs">{t('custom')}</span>
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Rename this header to match the values it renders.

This column shows whether a target is built-in or custom, not its runtime status. Labeling it as Status is misleading in every locale and makes the table harder to scan; a dedicated source/origin key would fit the rendered values.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/routes/_authed/settings/network-probes.tsx` around lines 231 -
243, The column with accessorKey 'source' currently uses header:
t('target_status', ...) but displays "Built-in"/"Custom"; change the header to
use a matching i18n key (e.g. t('source' or t('origin') with defaultValue
'Source'/'Origin') so the label reflects the rendered values, update the
translation files for that new key, and ensure the header replacement is applied
where the column is defined (the header property on the column with accessorKey
'source') to keep locales consistent.

// ---------------------------------------------------------------------------

function TrafficPage() {
const { t } = useTranslation('servers')
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Chart legend labels are still hardcoded English.

You introduced t in this page, but Line 81 and Line 82 still use "Inbound" / "Outbound", so localized UIs can still leak English text.

🌐 Proposed fix
-const trendConfig = {
-  bytes_in: { label: 'Inbound', color: 'var(--chart-1)' },
-  bytes_out: { label: 'Outbound', color: 'var(--chart-2)' }
-} satisfies ChartConfig
-
 function TrafficPage() {
   const { t } = useTranslation('servers')
+  const trendConfig = useMemo<ChartConfig>(
+    () => ({
+      bytes_in: { label: t('traffic_inbound'), color: 'var(--chart-1)' },
+      bytes_out: { label: t('traffic_outbound'), color: 'var(--chart-2)' }
+    }),
+    [t]
+  )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/routes/_authed/traffic/index.tsx` at line 188, The chart legend
labels are hardcoded as "Inbound"/"Outbound" despite bringing in useTranslation;
update the legend labels in the chart configuration (where "Inbound" and
"Outbound" are used) to use the translation function `t` instead (for example
`t('inbound')` / `t('outbound')` or the appropriate keys in the 'servers'
namespace) so the chart uses localized strings; ensure the same translation keys
are added to the i18n resource files and that the component still passes the
translated values to the chart's legend/series labels (look for the
variables/props that currently contain "Inbound"/"Outbound").

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
apps/web/src/routes/_authed/settings/network-probes.tsx (1)

234-244: ⚠️ Potential issue | 🟡 Minor

Rename the source column header; it still says “Status.”

Line 234 labels provenance values (Built-in / Custom) as status, which is misleading.

Suggested adjustment
-        header: t('target_status', { defaultValue: 'Status' }),
+        header: t('target_source', { defaultValue: 'Source' }),

Also add target_source to apps/web/src/locales/en/network.json and apps/web/src/locales/zh/network.json.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/routes/_authed/settings/network-probes.tsx` around lines 234 -
244, The column header is mislabeled as "Status"; update the column definition's
header call from t('target_status', { defaultValue: 'Status' }) to
t('target_source', { defaultValue: 'Source' }) so it correctly reflects
provenance, and add the new translation key "target_source" with appropriate
English and Chinese values into the network.json locale files to ensure the
label resolves in both languages.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@apps/web/src/routes/_authed/settings/network-probes.tsx`:
- Around line 234-244: The column header is mislabeled as "Status"; update the
column definition's header call from t('target_status', { defaultValue: 'Status'
}) to t('target_source', { defaultValue: 'Source' }) so it correctly reflects
provenance, and add the new translation key "target_source" with appropriate
English and Chinese values into the network.json locale files to ensure the
label resolves in both languages.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1fd93ea3-b0a1-4e83-83a3-e068dd5eb619

📥 Commits

Reviewing files that changed from the base of the PR and between f6483aa and 747a11a.

📒 Files selected for processing (7)
  • apps/web/src/components/network/latency-chart.tsx
  • apps/web/src/components/server/server-card.tsx
  • apps/web/src/lib/network-i18n.ts
  • apps/web/src/routes/_authed/network/$server-id.test.tsx
  • apps/web/src/routes/_authed/network/$serverId.tsx
  • apps/web/src/routes/_authed/settings/network-probes.test.tsx
  • apps/web/src/routes/_authed/settings/network-probes.tsx
✅ Files skipped from review due to trivial changes (3)
  • apps/web/src/lib/network-i18n.ts
  • apps/web/src/components/server/server-card.tsx
  • apps/web/src/routes/_authed/network/$server-id.test.tsx

@ZingerLittleBee ZingerLittleBee merged commit 62b8132 into main Apr 12, 2026
10 checks passed
@ZingerLittleBee ZingerLittleBee deleted the net-i18n branch April 12, 2026 15:30
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