Skip to content

fix(web): localization and UX honesty fixes from UI testing#156

Merged
ZingerLittleBee merged 36 commits into
mainfrom
testloop/web-localization-and-ux-fixes
Jul 4, 2026
Merged

fix(web): localization and UX honesty fixes from UI testing#156
ZingerLittleBee merged 36 commits into
mainfrom
testloop/web-localization-and-ux-fixes

Conversation

@ZingerLittleBee

Copy link
Copy Markdown
Owner

Summary

A batch of frontend localization and UX-honesty fixes surfaced through systematic UI testing (testloop). The theme is making the Chinese UI fully localized and the interface honest about what it can do. Each fix was cross-verified with a non-browser method (locale-file diff, code reading, or live rendering) to avoid false findings.

Localization (i18n)

  • Dates now render in the active app language via a locale-aware formatDate/formatDateTime helper (activeLocale() → BCP-47).
  • Dashboard custom widgets: metric labels, titles and empty states (Top-N Top CPU内存 排行, gauge, multi-line … Comparison, line-chart, Server not found, No online servers) routed through localized dashboard:common.metrics.* keys via a shared metricLabel() helper.
  • Docker overview stat-card labels (Running/Stopped/Total CPU/…).
  • Traffic chart inbound/outbound legend labels.
  • Relative time & uptime tooltips via Intl.RelativeTimeFormat.
  • Server-map country server-count tooltip (with plural forms).
  • Public status page processes/connections detail labels (namespace mismatch fix).
  • Missing translations added: custom-dashboard CRUD toasts, scheduled-task toasts, security attack-detected toast, remote-command server count, user-role label in the user list.
  • Appearance page logo/favicon preview alt text (screen-reader visible).

UX / correctness

  • Repair broken brand logo/favicon upload — the frontend was sending multipart FormData to a JSON-only endpoint (415) and a text-only save wiped the logo path; now uploads to the correct POST /settings/brand/{logo,favicon} endpoints and preserves paths.
  • Styled, localized 404 not-found page.
  • Tab param validation — unknown ip-quality / network-probes tab params fall back to the default tab instead of rendering blank.
  • Notification delivery failures classified as 502 (upstream failure) instead of 500.

Verification

  • bun run typecheck, bun x ultracite check, and affected vitest suites pass.
  • Locale coverage re-checked with two detector scripts (missing zh keys / English fallbacks) — clean.
  • Key fixes verified live in the browser against seeded + one real Docker agent (e.g. Top-N widget title renders 内存 排行; brand upload returns 200 and preview renders).

A failed notification channel test (unreachable webhook, wrong token,
non-2xx upstream response) was mapped to AppError::Internal, surfacing to
the user as HTTP 500 "Internal error: ...". That is misleading: the test
button exists to probe a user-supplied external endpoint, so a failure is
almost always the user's config or a third party at fault, not ServerBee.

Add an AppError::BadGateway variant (502, bare self-descriptive message)
and use it for webhook/telegram/bark/email delivery failures in dispatch.
The HTTP-client-builder error stays Internal (a genuine server fault).

On the web side the channel test toast now shows a localized headline
(notifications.test_failed) with the raw upstream detail as description,
instead of dumping the raw English "Internal error: ..." string as the
whole toast in an otherwise Chinese UI.
The user management list rendered user.role verbatim ("admin" / "member")
in the read-only row, while the role picker and the sidebar footer both
localize it (管理员 / 普通成员). Map the raw role to its localized label so
the list is consistent with the rest of the Chinese UI.
Unmatched routes (mistyped URLs, stale bookmarks, renamed paths) fell
through to TanStack Router's built-in fallback, which renders a bare
unstyled English "Not Found" with no app chrome and no way back — jarring
in an otherwise polished, fully localized app.

Register a defaultNotFoundComponent that renders a themed, centered 404
page with a localized title/description and a 'back to home' link. Applies
router-wide, so both authed and public bad URLs land on it.
The network-probes route validated its tab search param as
`(search.tab as string) || 'targets'`, which only fell back when the value
was empty. An unrecognized non-empty value such as `?tab=global` (a stale
bookmark or mistyped URL) passed through, matched no TabsContent, and
rendered a blank page. Validate against the known {targets, settings} set
and coerce anything else to 'targets', mirroring the status-pages route.
Same blank-page bug as the network-probes route: the ip-quality tab search
param used `(search.tab as string) || 'catalog'`, so an unrecognized value
passed through and matched no TabsContent, rendering a blank page. Validate
against the {catalog, settings} set and coerce unknown values to 'catalog'.
Raw .toLocaleDateString()/.toLocaleString() calls followed the browser
locale, so a user viewing the app in Chinese with an en-US browser saw
US-style dates (7/4/2026 instead of 2026/7/4). Add locale-aware helpers
in lib/format.ts that map the active i18n language to a BCP-47 locale,
and migrate all date/datetime call sites to them. Time-only axis labels
and numeric grouping stay locale-neutral.
The appearance page's logo/favicon upload never worked: it PUT multipart
FormData to /api/settings/brand, which only accepts JSON, so every upload
failed with 415 Unsupported Media Type. The real multipart endpoints
(POST /api/settings/brand/logo|favicon, field name "file") were never
called. Separately, the form read logo_url/favicon_url while the API
returns logo_path/favicon_path, so a saved logo never previewed, and a
title-only save overwrote the whole config and wiped an uploaded logo.

Upload via the dedicated POST endpoints first, then persist the config
with a JSON PUT that preserves the existing logo/favicon paths. Read the
correct *_path fields, restrict the file picker to PNG/ICO, and show the
accepted formats and size limit.
The traffic overview and per-server traffic charts hardcoded English
'Inbound'/'Outbound' series labels in their ChartConfig, so the legend
and tooltips stayed English even in Chinese UI while the table headers
were already localized. Build the chart config from the existing
traffic_inbound/traffic_outbound locale keys.
The create/update/delete dashboard mutations showed hardcoded English
toast messages regardless of UI language. Move them to the dashboard
locale namespace.
formatRelativeTime returned hardcoded English ('Never', '5m ago') and
formatUptimeTooltip returned 'No data'/'downtime'/'incident(s)', shown in
Chinese UI across dashboard widgets (alert list, service status), docker
events, and the uptime timeline tooltip. Use Intl.RelativeTimeFormat
against the active locale for the relative parts and localize the uptime
labels via the status namespace with proper pluralization.
The world-map widget tooltip hardcoded the English 'server(s)' plural in
its per-country count. Use a pluralized dashboard locale key instead.
The scheduled task create/update/delete/trigger toasts fell back to
their English defaultValue because the tasks.scheduled.toast_* keys were
absent from both locale files, so they showed English in Chinese UI. Add
the keys to both locales.
The high/critical security-event WebSocket toast referenced
security:toast.attack_detected, which existed in neither locale, so it
always rendered the English defaultValue. Add the nested toast key.
The public server status page reads detail_processes/detail_connections
from the status namespace, but those keys only existed in the servers
namespace, so the page fell back to the English defaultValue. Add the
keys to the status namespace.
The Docker overview cards (Running, Stopped, Total CPU, Total Memory,
Docker Version) and the version 'Unknown' fallback were hardcoded English
with no useTranslation, so they stayed English in Chinese UI while the
rest of the Docker page was localized. Move them to the docker namespace.
Top-N, gauge, multi-line and line-chart widgets rendered hardcoded English
metric labels (Top CPU, CPU, ...), title fallbacks ("CPU Comparison") and
empty states ("No online servers", "Server not found") regardless of the
active language. Route them through the localized dashboard:common.metrics.*
keys via a shared metricLabel() helper and add the missing widget keys to
both locale files.
The brand image preview thumbnails on the Appearance settings page used
hardcoded English alt attributes ("Logo preview", "Favicon preview"),
surfacing English to screen readers in the Chinese UI. Route them through
settings namespace keys.
@ZingerLittleBee
ZingerLittleBee merged commit 9eb840b into main Jul 4, 2026
2 of 3 checks passed
@ZingerLittleBee
ZingerLittleBee deleted the testloop/web-localization-and-ux-fixes branch July 4, 2026 18:04
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