fix(i18n): localize discovery and accounts

This commit is contained in:
cesnimda
2026-08-29 22:01:26 +02:00
parent 457f0601c0
commit f89e37bb4f
7 changed files with 195 additions and 56 deletions
@@ -4,6 +4,7 @@ import { Box, Button, Checkbox, Chip, Divider, FormControlLabel, Paper, Stack, T
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
import { api, getApiErrorMessage } from "../api";
import { useI18n } from "../i18n/I18nProvider";
import { useToast } from "../toast";
import type { GmailStatus, ImapStatus, MicrosoftGraphStatus } from "../types";
@@ -13,6 +14,7 @@ import type { GmailStatus, ImapStatus, MicrosoftGraphStatus } from "../types";
// form submitted to POST /api/imap/connect, which verifies the connection before storing it.
export default function EmailProviderConnections() {
const { toast } = useToast();
const { t } = useI18n();
const [gmailStatus, setGmailStatus] = useState<GmailStatus | null>(null);
const [microsoftStatus, setMicrosoftStatus] = useState<MicrosoftGraphStatus | null>(null);
@@ -59,32 +61,32 @@ export default function EmailProviderConnections() {
const data = event.data as { source?: string; status?: string; message?: string };
if (data?.source === "jobtracker-gmail-oauth") {
if (data.status === "connected") {
toast(data.message || "Gmail connected.", "success");
toast(data.message || t("emailConnectionsGmailConnected"), "success");
void loadGmailStatus();
} else {
toast(data.message || "Gmail connection failed.", "error");
toast(data.message || t("emailConnectionsGmailFailed"), "error");
}
} else if (data?.source === "jobtracker-microsoft-oauth") {
if (data.status === "connected") {
toast(data.message || "Outlook connected.", "success");
toast(data.message || t("emailConnectionsOutlookConnected"), "success");
void loadMicrosoftStatus();
} else {
toast(data.message || "Outlook connection failed.", "error");
toast(data.message || t("emailConnectionsOutlookFailed"), "error");
}
}
};
window.addEventListener("message", onMessage);
return () => window.removeEventListener("message", onMessage);
}, [loadGmailStatus, loadMicrosoftStatus, toast]);
}, [loadGmailStatus, loadMicrosoftStatus, t, toast]);
const connectViaPopup = async (connectUrlPath: string, popupName: string, providerLabel: string) => {
try {
const res = await api.get<{ url: string }>(connectUrlPath);
const popup = window.open(res.data.url, popupName, "width=620,height=760,resizable=yes,scrollbars=yes");
if (!popup) toast("Your browser blocked the connect popup. Allow popups and try again.", "error");
if (!popup) toast(t("emailConnectionsPopupBlocked"), "error");
} catch (error) {
toast(getApiErrorMessage(error, `Failed to start ${providerLabel} connection.`), "error");
toast(getApiErrorMessage(error, t("emailConnectionsStartFailed", { provider: providerLabel })), "error");
}
};
@@ -92,15 +94,15 @@ export default function EmailProviderConnections() {
try {
await api.delete(path);
await reload();
toast(`${providerLabel} disconnected.`, "success");
toast(t("emailConnectionsDisconnected", { provider: providerLabel }), "success");
} catch (error) {
toast(getApiErrorMessage(error, `Failed to disconnect ${providerLabel}.`), "error");
toast(getApiErrorMessage(error, t("emailConnectionsDisconnectFailed", { provider: providerLabel })), "error");
}
};
const connectImap = async () => {
if (!imapForm.host.trim() || !imapForm.username.trim() || !imapForm.password) {
toast("Host, username, and password are required.", "error");
toast(t("emailConnectionsRequired"), "error");
return;
}
setImapConnecting(true);
@@ -108,9 +110,9 @@ export default function EmailProviderConnections() {
await api.post("/imap/connect", imapForm);
setImapForm((prev) => ({ ...prev, password: "" }));
await loadImapStatus();
toast("IMAP account connected.", "success");
toast(t("emailConnectionsImapConnected"), "success");
} catch (error) {
toast(getApiErrorMessage(error, "Failed to connect IMAP account."), "error");
toast(getApiErrorMessage(error, t("emailConnectionsImapFailed")), "error");
} finally {
setImapConnecting(false);
}
@@ -118,9 +120,9 @@ export default function EmailProviderConnections() {
return (
<Paper sx={{ p: 2 }}>
<Typography sx={{ fontWeight: 950, mb: 0.5 }}>Linked email accounts</Typography>
<Typography sx={{ fontWeight: 950, mb: 0.5 }}>{t("emailConnectionsTitle")}</Typography>
<Typography sx={{ color: "text.secondary", mb: 2 }}>
Connect a mailbox so recruiter correspondence can be linked to jobs automatically.
{t("emailConnectionsBody")}
</Typography>
<Stack spacing={2}>
@@ -146,7 +148,7 @@ export default function EmailProviderConnections() {
<Box>
<ProviderRow
label="Other (IMAP)"
label={t("emailConnectionsOtherImap")}
connected={Boolean(imapStatus?.connected)}
address={imapStatus?.username ?? null}
onDisconnect={() => void disconnect("/imap/connection", loadImapStatus, "IMAP")}
@@ -155,28 +157,28 @@ export default function EmailProviderConnections() {
<Box sx={{ mt: 1.5, display: "grid", gap: 1.25, gridTemplateColumns: { xs: "1fr", sm: "2fr 1fr" }, maxWidth: 520 }}>
<TextField
size="small"
label="IMAP host"
label={t("emailConnectionsImapHost")}
placeholder="imap.example.com"
value={imapForm.host}
onChange={(e) => setImapForm((prev) => ({ ...prev, host: e.target.value }))}
/>
<TextField
size="small"
label="Port"
label={t("emailConnectionsPort")}
type="number"
value={imapForm.port}
onChange={(e) => setImapForm((prev) => ({ ...prev, port: Number(e.target.value) || 993 }))}
/>
<TextField
size="small"
label="Username"
label={t("emailConnectionsUsername")}
value={imapForm.username}
onChange={(e) => setImapForm((prev) => ({ ...prev, username: e.target.value }))}
sx={{ gridColumn: "1 / -1" }}
/>
<TextField
size="small"
label="Password"
label={t("emailConnectionsPassword")}
type="password"
value={imapForm.password}
onChange={(e) => setImapForm((prev) => ({ ...prev, password: e.target.value }))}
@@ -185,7 +187,7 @@ export default function EmailProviderConnections() {
<FormControlLabel
sx={{ gridColumn: "1 / -1" }}
control={<Checkbox checked={imapForm.useSsl} onChange={(e) => setImapForm((prev) => ({ ...prev, useSsl: e.target.checked }))} />}
label="Use SSL/TLS"
label={t("emailConnectionsUseSsl")}
/>
<Button
variant="contained"
@@ -193,7 +195,7 @@ export default function EmailProviderConnections() {
disabled={imapConnecting}
sx={{ gridColumn: "1 / -1", justifySelf: "start" }}
>
{imapConnecting ? "Connecting" : "Connect IMAP account"}
{imapConnecting ? t("emailConnectionsConnecting") : t("emailConnectionsConnectImap")}
</Button>
</Box>
)}
@@ -216,6 +218,7 @@ function ProviderRow({
onConnect?: () => void;
onDisconnect: () => void;
}) {
const { t } = useI18n();
return (
<Stack direction="row" alignItems="center" justifyContent="space-between" flexWrap="wrap" gap={1}>
<Box>
@@ -226,17 +229,17 @@ function ProviderRow({
icon={<CheckCircleIcon fontSize="small" />}
color="success"
variant="outlined"
label={address || "Connected"}
label={address || t("emailConnectionsConnected")}
sx={{ mt: 0.5 }}
/>
) : (
<Typography variant="caption" sx={{ color: "text.secondary" }}>Not connected</Typography>
<Typography variant="caption" sx={{ color: "text.secondary" }}>{t("emailConnectionsNotConnected")}</Typography>
)}
</Box>
{connected ? (
<Button size="small" variant="outlined" color="error" onClick={onDisconnect}>Disconnect</Button>
<Button size="small" variant="outlined" color="error" onClick={onDisconnect}>{t("emailConnectionsDisconnect")}</Button>
) : (
onConnect && <Button size="small" variant="outlined" onClick={onConnect}>Connect</Button>
onConnect && <Button size="small" variant="outlined" onClick={onConnect}>{t("emailConnectionsConnect")}</Button>
)}
</Stack>
);