fix(i18n): finish career activity copy

This commit is contained in:
cesnimda
2026-08-29 21:57:40 +02:00
parent a704c75440
commit 457f0601c0
13 changed files with 272 additions and 91 deletions
+21 -20
View File
@@ -12,6 +12,7 @@ import {
} from "@mui/material";
import { api, getApiErrorMessage } from "../api";
import { useI18n } from "../i18n/I18nProvider";
import { notificationDateLabel, UserNotification } from "../notifications";
type Operation = {
@@ -30,9 +31,9 @@ type Operation = {
};
const statusLabel = (value: string) => value.replaceAll("_", " ");
const dateLabel = notificationDateLabel;
export default function OperationsPage() {
const { language, t } = useI18n();
const [operations, setOperations] = useState<Operation[]>([]);
const [notifications, setNotifications] = useState<UserNotification[]>([]);
const [loading, setLoading] = useState(true);
@@ -50,11 +51,11 @@ export default function OperationsPage() {
setNotifications(notificationResponse.data ?? []);
setError(null);
} catch (requestError) {
setError(getApiErrorMessage(requestError, "Operations could not be loaded."));
setError(getApiErrorMessage(requestError, t("operationsLoadFailed")));
} finally {
setLoading(false);
}
}, []);
}, [language]);
useEffect(() => {
void load();
@@ -70,7 +71,7 @@ export default function OperationsPage() {
await load();
if (notificationsChanged) window.dispatchEvent(new Event("notifications-changed"));
} catch (requestError) {
setError(getApiErrorMessage(requestError, "The action could not be completed."));
setError(getApiErrorMessage(requestError, t("operationsActionFailed")));
} finally {
setBusyKey(null);
}
@@ -79,30 +80,30 @@ export default function OperationsPage() {
return (
<Stack spacing={2}>
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 2, flexWrap: "wrap" }}>
<Typography color="text.secondary">Background work survives navigation and refresh.</Typography>
<Button variant="outlined" onClick={() => void load(true)} disabled={loading}>Refresh</Button>
<Typography color="text.secondary">{t("operationsSubtitle")}</Typography>
<Button variant="outlined" onClick={() => void load(true)} disabled={loading}>{t("refresh")}</Button>
</Box>
{error ? <Alert severity="error" aria-live="polite">{error}</Alert> : null}
{loading ? <LinearProgress aria-label="Loading operations" /> : null}
{loading ? <LinearProgress aria-label={t("operationsLoading")} /> : null}
<Paper component="section" aria-labelledby="notifications-heading" sx={{ p: { xs: 2, sm: 3 } }}>
<Typography id="notifications-heading" variant="h6" sx={{ mb: 2 }}>Notifications</Typography>
{notifications.length === 0 && !loading ? <Typography color="text.secondary">No notifications.</Typography> : null}
<Typography id="notifications-heading" variant="h6" sx={{ mb: 2 }}>{t("notifications")}</Typography>
{notifications.length === 0 && !loading ? <Typography color="text.secondary">{t("operationsNoNotifications")}</Typography> : null}
<Stack spacing={1.5}>
{notifications.map((notification) => (
<Box key={notification.id} sx={{ p: 2, border: "1px solid", borderColor: "divider", borderRadius: 2, opacity: notification.readAtUtc ? 0.75 : 1 }}>
<Typography sx={{ fontWeight: notification.readAtUtc ? 600 : 800 }}>{notification.title}</Typography>
<Typography color="text.secondary">{notification.message}</Typography>
<Typography variant="caption" color="text.secondary">{dateLabel(notification.createdAtUtc)}</Typography>
<Typography variant="caption" color="text.secondary">{notificationDateLabel(notification.createdAtUtc, language === "nb" ? "nb-NO" : "en")}</Typography>
<Stack direction="row" spacing={1} sx={{ mt: 1, flexWrap: "wrap" }}>
{!notification.readAtUtc ? (
<Button size="small" disabled={busyKey !== null} onClick={() => void runAction(`read-${notification.id}`, () => api.post(`/notifications/${notification.id}/read`), true)}>
Mark read
{t("notificationsMarkRead")}
</Button>
) : null}
<Button size="small" color="inherit" disabled={busyKey !== null} onClick={() => void runAction(`dismiss-${notification.id}`, () => api.delete(`/notifications/${notification.id}`), true)}>
Dismiss
{t("notificationsDismiss")}
</Button>
</Stack>
</Box>
@@ -111,8 +112,8 @@ export default function OperationsPage() {
</Paper>
<Paper component="section" aria-labelledby="operations-heading" sx={{ p: { xs: 2, sm: 3 } }}>
<Typography id="operations-heading" variant="h6" sx={{ mb: 2 }}>Operations</Typography>
{operations.length === 0 && !loading ? <Typography color="text.secondary">No background operations yet.</Typography> : null}
<Typography id="operations-heading" variant="h6" sx={{ mb: 2 }}>{t("operations")}</Typography>
{operations.length === 0 && !loading ? <Typography color="text.secondary">{t("operationsEmpty")}</Typography> : null}
<Stack spacing={1.5}>
{operations.map((operation) => (
<Box key={operation.id} sx={{ p: 2, border: "1px solid", borderColor: "divider", borderRadius: 2 }}>
@@ -120,20 +121,20 @@ export default function OperationsPage() {
<Typography sx={{ fontWeight: 800, overflowWrap: "anywhere" }}>{operation.taskType}</Typography>
<Chip size="small" label={statusLabel(operation.status)} />
</Box>
<Typography variant="caption" color="text.secondary">Started {dateLabel(operation.createdAtUtc)}</Typography>
<Typography variant="caption" color="text.secondary">{t("operationsStarted", { date: notificationDateLabel(operation.createdAtUtc, language === "nb" ? "nb-NO" : "en") })}</Typography>
{operation.progressStage ? <Typography sx={{ mt: 1 }}>{operation.progressStage}</Typography> : null}
{operation.progressPercent != null ? <LinearProgress variant="determinate" value={operation.progressPercent} aria-label={`${operation.taskType} progress`} sx={{ mt: 1 }} /> : null}
{operation.cancellationRequestedAtUtc ? <Typography color="text.secondary" sx={{ mt: 1 }}>Cancellation requested.</Typography> : null}
{operation.failureCategory ? <Alert severity="error" sx={{ mt: 1 }}>Failed: {statusLabel(operation.failureCategory)}</Alert> : null}
{operation.progressPercent != null ? <LinearProgress variant="determinate" value={operation.progressPercent} aria-label={t("operationsProgress", { task: operation.taskType })} sx={{ mt: 1 }} /> : null}
{operation.cancellationRequestedAtUtc ? <Typography color="text.secondary" sx={{ mt: 1 }}>{t("operationsCancellationRequested")}</Typography> : null}
{operation.failureCategory ? <Alert severity="error" sx={{ mt: 1 }}>{t("operationsFailed", { category: statusLabel(operation.failureCategory) })}</Alert> : null}
<Stack direction="row" spacing={1} sx={{ mt: 1, flexWrap: "wrap" }}>
{operation.canCancel ? (
<Button size="small" color="error" disabled={busyKey !== null} onClick={() => void runAction(`cancel-${operation.id}`, () => api.post(`/operations/${operation.id}/cancel`))}>
Cancel
{t("cancel")}
</Button>
) : null}
{operation.canRetry ? (
<Button size="small" variant="outlined" disabled={busyKey !== null} onClick={() => void runAction(`retry-${operation.id}`, () => api.post(`/operations/${operation.id}/retry`), true)}>
Retry
{t("retry")}
</Button>
) : null}
</Stack>