fix(web): localization and UX honesty fixes from UI testing#156
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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)
formatDate/formatDateTimehelper (activeLocale()→ BCP-47).Top CPU→内存 排行, gauge, multi-line… Comparison, line-chart,Server not found,No online servers) routed through localizeddashboard:common.metrics.*keys via a sharedmetricLabel()helper.Running/Stopped/Total CPU/…).Intl.RelativeTimeFormat.alttext (screen-reader visible).UX / correctness
POST /settings/brand/{logo,favicon}endpoints and preserves paths.ip-quality/network-probestab params fall back to the default tab instead of rendering blank.502(upstream failure) instead of500.Verification
bun run typecheck,bun x ultracite check, and affected vitest suites pass.内存 排行; brand upload returns 200 and preview renders).