feat: centralize API error localization
Replace the err instanceof ApiError ? err.message : t(fallback) anti-
pattern -- which showed raw English backend text for the common case and
only used the localized fallback for the rare network-failure case -- at
all 13 call sites across 7 files.
New frontend/src/api/errorMessages.ts (describeApiError) resolves a
caught error to a localized {title, explanation, nextStep?, technical}
by checking the 32 known AppError codes first, then known HTTP statuses
(401/403/404/409/422/500), then a fully generic fallback. New
ApiErrorNotice (PageChrome.tsx) renders title/explanation/nextStep with
the raw text demoted to a "Technical details"/"Details techniques"
disclosure -- never shown as the primary message.
ApiError itself is split out of client.ts into a standalone
api/apiError.ts with no import.meta.env dependency, so errorMessages.ts
(and its tests) can be loaded outside a Vite/browser context.
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { api, ApiError } from "../api/client";
|
||||
import { api } from "../api/client";
|
||||
import { describeApiError, type ApiErrorInfo } from "../api/errorMessages";
|
||||
import type { DataQualityIssue, ScanResult } from "../api/types";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import { SeverityBadge, StatusBadge } from "../components/Badge";
|
||||
import { EmptyState, ErrorState, LoadingState, PageHeader } from "../components/PageChrome";
|
||||
import { ApiErrorNotice, EmptyState, ErrorState, LoadingState, PageHeader } from "../components/PageChrome";
|
||||
|
||||
const RULE_TYPES = [
|
||||
"possible_duplicate_customer",
|
||||
@@ -23,7 +24,7 @@ export function DataQuality() {
|
||||
const [status, setStatus] = useState("open");
|
||||
const [ruleType, setRuleType] = useState("");
|
||||
const [scanning, setScanning] = useState(false);
|
||||
const [scanError, setScanError] = useState<string | null>(null);
|
||||
const [scanError, setScanError] = useState<ApiErrorInfo | null>(null);
|
||||
const [scanResult, setScanResult] = useState<ScanResult | null>(null);
|
||||
const [confirmingScan, setConfirmingScan] = useState(false);
|
||||
const [demoScenariosOnly, setDemoScenariosOnly] = useState(false);
|
||||
@@ -54,7 +55,7 @@ export function DataQuality() {
|
||||
setConfirmingScan(false);
|
||||
load();
|
||||
} catch (err) {
|
||||
setScanError(err instanceof ApiError ? err.message : t("list.scanFailed"));
|
||||
setScanError(describeApiError(t, err, "list.scanFailed"));
|
||||
} finally {
|
||||
setScanning(false);
|
||||
}
|
||||
@@ -101,7 +102,7 @@ export function DataQuality() {
|
||||
}
|
||||
/>
|
||||
|
||||
{scanError && <p className="error" role="alert">{scanError}</p>}
|
||||
<ApiErrorNotice error={scanError} />
|
||||
{scanResult && (
|
||||
<p className="quiet-empty" role="status">
|
||||
{t("list.scanComplete", {
|
||||
|
||||
Reference in New Issue
Block a user