From b3cbaee16c52e43344c9324181b89f5b0e9d4095 Mon Sep 17 00:00:00 2001 From: cesnimda Date: Mon, 23 Mar 2026 20:20:06 +0100 Subject: [PATCH] Translate correspondence flow and localize AI system details --- .../src/components/Correspondence.tsx | 58 ++++---- job-tracker-ui/src/i18n/translations.ts | 138 ++++++++++++++++++ job-tracker-ui/src/pages/AdminSystemPage.tsx | 90 ++++++------ 3 files changed, 213 insertions(+), 73 deletions(-) diff --git a/job-tracker-ui/src/components/Correspondence.tsx b/job-tracker-ui/src/components/Correspondence.tsx index 383498c..0746e0f 100644 --- a/job-tracker-ui/src/components/Correspondence.tsx +++ b/job-tracker-ui/src/components/Correspondence.tsx @@ -31,6 +31,7 @@ import { api, getApiErrorMessage } from "../api"; import { useToast } from "../toast"; import { CorrespondenceMessage, GmailMessageSummary, GmailStatus } from "../types"; import { useDialogActions } from "../dialogs"; +import { useI18n } from "../i18n/I18nProvider"; function parseRawEmail(raw: string): { subject?: string; date?: string; from?: string; to?: string; body: string } { const lines = raw.replace(/\r\n/g, "\n").split("\n"); @@ -80,6 +81,7 @@ function scoreMessage(message: GmailMessageSummary, query: string, messages: Cor export default function Correspondence({ jobId }: { jobId: number }) { const theme = useTheme(); const { toast } = useToast(); + const { t } = useI18n(); const { confirmAction } = useDialogActions(); const [messages, setMessages] = useState([]); const [from, setFrom] = useState<"Me" | "Company">("Me"); @@ -157,18 +159,18 @@ export default function Correspondence({ jobId }: { jobId: number }) { const data = event.data as { source?: string; status?: string; message?: string }; if (data?.source !== "jobtracker-gmail-oauth") return; if (data.status === "connected") { - toast(data.message || "Gmail connected.", "success"); + toast(data.message || t("googleLinkedSuccess"), "success"); void loadGmailStatus(); setImportTab(1); void loadGmailMessages(); } else { - toast(data.message || "Gmail connection failed.", "error"); + toast(data.message || t("googleAuthFailed"), "error"); } }; window.addEventListener("message", onMessage); return () => window.removeEventListener("message", onMessage); - }, [loadGmailMessages, loadGmailStatus, toast]); + }, [loadGmailMessages, loadGmailStatus, t, toast]); const canSend = useMemo(() => text.trim().length > 0, [text]); @@ -213,7 +215,7 @@ export default function Correspondence({ jobId }: { jobId: number }) { const importEmail = async () => { const parsed = parseRawEmail(rawEmail); if (!parsed.body && !parsed.subject && !rawEmail.trim()) { - toast("Paste an email first.", "error"); + toast(t("addJobModalPasteUrlFirst"), "error"); return; } @@ -229,7 +231,7 @@ export default function Correspondence({ jobId }: { jobId: number }) { setImportOpen(false); setRawEmail(""); await load(); - toast("Email logged.", "success"); + toast(t("correspondenceLogEmail"), "success"); } catch (error) { toast(getApiErrorMessage(error, "Failed to import email."), "error"); } @@ -250,14 +252,14 @@ export default function Correspondence({ jobId }: { jobId: number }) { await api.delete("/gmail/connection"); setGmailStatus({ connected: false }); setGmailMessages([]); - toast("Gmail disconnected.", "success"); + toast(t("googleUnlinked"), "success"); } catch (error) { toast(getApiErrorMessage(error, "Failed to disconnect Gmail."), "error"); } }; const deleteMessage = async (messageId: number) => { - if (!(await confirmAction("Remove this correspondence message?", { title: "Delete message", confirmLabel: "Delete", destructive: true }))) return; + if (!(await confirmAction("Remove this correspondence message?", { title: "Delete message", confirmLabel: t("jobTableDeleteSelected"), destructive: true }))) return; try { await api.delete(`/correspondence/${messageId}`); await load(); @@ -272,7 +274,7 @@ export default function Correspondence({ jobId }: { jobId: number }) { setImportingMessageId(messageId); await api.post("/gmail/import", { jobApplicationId: jobId, messageId }); await load(); - toast("Email imported from Gmail.", "success"); + toast(t("correspondenceImportEmail"), "success"); } catch (error: any) { toast(getApiErrorMessage(error, "Failed to import Gmail message."), "error"); } finally { @@ -310,7 +312,7 @@ export default function Correspondence({ jobId }: { jobId: number }) { {m.content} - {isMe ? "Me" : "Company"}{m.channel ? ` - ${m.channel}` : ""}{m.date ? ` - ${new Date(m.date).toLocaleString()}` : ""} + {isMe ? t("correspondenceMe") : t("correspondenceCompany")}{m.channel ? ` - ${m.channel}` : ""}{m.date ? ` - ${new Date(m.date).toLocaleString()}` : ""} void deleteMessage(m.id)} sx={{ color: "text.secondary" }}> @@ -326,47 +328,47 @@ export default function Correspondence({ jobId }: { jobId: number }) { v && setFrom(v)} size="small"> - Me - Company + {t("correspondenceMe")} + {t("correspondenceCompany")} - + - setText(e.target.value)} multiline minRows={3} sx={{ flex: "1 1 320px" }} helperText={`${text.length} characters`} /> + setText(e.target.value)} multiline minRows={3} sx={{ flex: "1 1 320px" }} helperText={t("correspondenceCharacters", { count: text.length })} /> - + setImportOpen(false)} fullWidth maxWidth="md"> - Import Email + {t("correspondenceImportTitle")} setImportTab(v)} sx={{ mb: 2 }}> - - + + {importTab === 0 ? ( <> - Paste raw email text (headers optional). We parse Subject and Date when present. + {t("correspondencePasteEmailHelp")} setRawEmail(e.target.value)} placeholder={"Subject: ...\nDate: ...\nFrom: ...\nTo: ...\n\nBody..."} /> ) : ( - Google Gmail + {t("correspondenceGoogleGmail")} - {gmailLoading ? "Checking connection..." : gmailStatus?.connected ? `Connected as ${gmailStatus.gmailAddress}` : "Connect your Gmail account to browse recent emails."} + {gmailLoading ? t("correspondenceCheckingConnection") : gmailStatus?.connected ? t("correspondenceConnectedAs", { email: gmailStatus.gmailAddress || "" }) : t("correspondenceConnectGmailHint")} {gmailStatus?.connected ? ( <> - - + + ) : ( - + )} @@ -379,8 +381,8 @@ export default function Correspondence({ jobId }: { jobId: number }) { ))} - setGmailQuery(e.target.value)} placeholder="from:company@example.com OR interview" size="small" fullWidth /> - + setGmailQuery(e.target.value)} placeholder={t("correspondenceSearchGmailPlaceholder")} size="small" fullWidth /> + {gmailStatus.lastSyncedAt ? : null} @@ -409,7 +411,7 @@ export default function Correspondence({ jobId }: { jobId: number }) { {message.subject || "(No subject)"}{message.date ? new Date(message.date).toLocaleString() : ""}} - secondary={From: {message.from || "Unknown"}{message.snippet}} + secondary={From: {message.from || t("correspondenceUnknown")}{message.snippet}} /> - {importTab === 0 ? : null} + + {importTab === 0 ? : null} diff --git a/job-tracker-ui/src/i18n/translations.ts b/job-tracker-ui/src/i18n/translations.ts index 4f860e4..e8f0a3e 100644 --- a/job-tracker-ui/src/i18n/translations.ts +++ b/job-tracker-ui/src/i18n/translations.ts @@ -277,6 +277,75 @@ export const translations = { adminSystemRunningProbe: "Running probe...", adminSystemRefresh: "Refresh", adminSystemRefreshing: "Refreshing...", + adminSystemProvider: "Provider", + adminSystemTarget: "Target", + adminSystemConfigured: "Configured", + adminSystemCanConnect: "Can connect", + adminSystemUsesFileStorage: "Uses file storage", + adminSystemDataRoot: "Data root", + adminSystemDbPath: "DB path", + adminSystemDbFileExists: "DB file exists", + adminSystemDbSize: "DB size", + adminSystemJobs: "Jobs", + adminSystemDeletedJobs: "Deleted jobs", + adminSystemFramework: "Framework", + adminSystemOs: "OS", + adminSystemArchitecture: "Architecture", + adminSystemMachine: "Machine", + adminSystemContentRoot: "Content root", + adminSystemBuildStamp: "Build stamp", + adminSystemAuthRequired: "Auth required", + adminSystemJwtConfigured: "JWT key configured", + adminSystemGoogleConfigured: "Google login configured", + adminSystemGmailConfigured: "Gmail integration configured", + adminSystemFrom: "From", + adminSystemFromName: "From name", + adminSystemHost: "Host", + adminSystemPort: "Port", + adminSystemSsl: "SSL", + adminSystemModel: "Model", + adminSystemDevice: "Device", + adminSystemGpuAvailable: "GPU available", + adminSystemGpuName: "GPU name", + adminSystemHealthLatency: "Health latency", + adminSystemProbeLatency: "Probe latency", + adminSystemLastProbe: "Last probe", + adminSystemLastSuccessfulProbe: "Last successful probe", + adminSystemLastSummarizationSuccess: "Last summarization success", + adminSystemRequests: "Requests", + adminSystemCacheHits: "Cache hits", + adminSystemCacheMisses: "Cache misses", + adminSystemFailures: "Failures", + adminSystemProbeFailures: "Probe failures", + adminSystemAvgLatency: "Avg latency", + adminSystemOcrRequests: "OCR requests", + adminSystemOcrAvgLatency: "OCR avg latency", + adminSystemOcrUnavailable: "OCR unavailable", + adminSystemAiProbeFailed: "Failed to run AI service probe.", + correspondenceNoMessages: "No messages yet.", + correspondenceMe: "Me", + correspondenceCompany: "Company", + correspondenceImportEmail: "Import email", + correspondenceLogNoteOrMessage: "Log note or message", + correspondenceCharacters: "{count} characters", + correspondenceAdd: "Add", + correspondenceImportTitle: "Import email", + correspondencePasteEmail: "Paste email", + correspondencePasteEmailHelp: "Paste raw email text (headers optional). We parse Subject and Date when present.", + correspondenceGoogleGmail: "Google Gmail", + correspondenceCheckingConnection: "Checking connection...", + correspondenceConnectedAs: "Connected as {email}", + correspondenceConnectGmailHint: "Connect your Gmail account to browse recent emails.", + correspondenceRefresh: "Refresh", + correspondenceDisconnect: "Disconnect", + correspondenceConnectGmail: "Connect Gmail", + correspondenceSearchGmail: "Search Gmail", + correspondenceSearchGmailPlaceholder: "from:company@example.com OR interview", + correspondenceSearch: "Search", + correspondenceNoGmailMessages: "No Gmail messages found.", + correspondenceUnknown: "Unknown", + correspondenceClose: "Close", + correspondenceLogEmail: "Log email", adminSystemEnvironment: "Environment", adminSystemDatabase: "Database", adminSystemConnected: "Connected", @@ -697,6 +766,75 @@ export const translations = { adminSystemRunningProbe: "Kjører probe...", adminSystemRefresh: "Oppdater", adminSystemRefreshing: "Oppdaterer...", + adminSystemProvider: "Leverandør", + adminSystemTarget: "Mål", + adminSystemConfigured: "Konfigurert", + adminSystemCanConnect: "Kan koble til", + adminSystemUsesFileStorage: "Bruker fillagring", + adminSystemDataRoot: "Datarot", + adminSystemDbPath: "DB-sti", + adminSystemDbFileExists: "DB-fil finnes", + adminSystemDbSize: "DB-størrelse", + adminSystemJobs: "Jobber", + adminSystemDeletedJobs: "Slettede jobber", + adminSystemFramework: "Rammeverk", + adminSystemOs: "OS", + adminSystemArchitecture: "Arkitektur", + adminSystemMachine: "Maskin", + adminSystemContentRoot: "Innholdsrot", + adminSystemBuildStamp: "Byggestempel", + adminSystemAuthRequired: "Autentisering kreves", + adminSystemJwtConfigured: "JWT-nøkkel konfigurert", + adminSystemGoogleConfigured: "Google-innlogging konfigurert", + adminSystemGmailConfigured: "Gmail-integrasjon konfigurert", + adminSystemFrom: "Fra", + adminSystemFromName: "Fra-navn", + adminSystemHost: "Vert", + adminSystemPort: "Port", + adminSystemSsl: "SSL", + adminSystemModel: "Modell", + adminSystemDevice: "Enhet", + adminSystemGpuAvailable: "GPU tilgjengelig", + adminSystemGpuName: "GPU-navn", + adminSystemHealthLatency: "Helselatens", + adminSystemProbeLatency: "Probelatens", + adminSystemLastProbe: "Siste probe", + adminSystemLastSuccessfulProbe: "Siste vellykkede probe", + adminSystemLastSummarizationSuccess: "Siste vellykkede oppsummering", + adminSystemRequests: "Forespørsler", + adminSystemCacheHits: "Cache-treff", + adminSystemCacheMisses: "Cache-miss", + adminSystemFailures: "Feil", + adminSystemProbeFailures: "Probefeil", + adminSystemAvgLatency: "Snittlatens", + adminSystemOcrRequests: "OCR-forespørsler", + adminSystemOcrAvgLatency: "OCR snittlatens", + adminSystemOcrUnavailable: "OCR utilgjengelig", + adminSystemAiProbeFailed: "Kunne ikke kjøre AI-tjenesteprobe.", + correspondenceNoMessages: "Ingen meldinger ennå.", + correspondenceMe: "Meg", + correspondenceCompany: "Selskap", + correspondenceImportEmail: "Importer e-post", + correspondenceLogNoteOrMessage: "Loggfør notat eller melding", + correspondenceCharacters: "{count} tegn", + correspondenceAdd: "Legg til", + correspondenceImportTitle: "Importer e-post", + correspondencePasteEmail: "Lim inn e-post", + correspondencePasteEmailHelp: "Lim inn rå e-posttekst (overskrifter valgfritt). Vi tolker emne og dato når de finnes.", + correspondenceGoogleGmail: "Google Gmail", + correspondenceCheckingConnection: "Sjekker tilkobling...", + correspondenceConnectedAs: "Tilkoblet som {email}", + correspondenceConnectGmailHint: "Koble til Gmail-kontoen din for å bla gjennom nylige e-poster.", + correspondenceRefresh: "Oppdater", + correspondenceDisconnect: "Koble fra", + correspondenceConnectGmail: "Koble til Gmail", + correspondenceSearchGmail: "Søk i Gmail", + correspondenceSearchGmailPlaceholder: "from:company@example.com OR interview", + correspondenceSearch: "Søk", + correspondenceNoGmailMessages: "Ingen Gmail-meldinger funnet.", + correspondenceUnknown: "Ukjent", + correspondenceClose: "Lukk", + correspondenceLogEmail: "Loggfør e-post", adminSystemEnvironment: "Miljø", adminSystemDatabase: "Database", adminSystemConnected: "Tilkoblet", diff --git a/job-tracker-ui/src/pages/AdminSystemPage.tsx b/job-tracker-ui/src/pages/AdminSystemPage.tsx index d8c7052..66a0d54 100644 --- a/job-tracker-ui/src/pages/AdminSystemPage.tsx +++ b/job-tracker-ui/src/pages/AdminSystemPage.tsx @@ -247,34 +247,34 @@ export default function AdminSystemPage() { {t("adminSystemDatabaseStorage")} - - - - - - - - - + + + + + + + + + - - + + {t("adminSystemRuntimeAuth")} - - - - - - - - - - + + + + + + + + + + @@ -283,27 +283,27 @@ export default function AdminSystemPage() { {t("adminSystemEmailConfig")} - - - - - - + + + + + + {t("adminSystemSummarizerRuntime")} - - - - - - - - - + + + + + + + + + @@ -328,14 +328,14 @@ export default function AdminSystemPage() { {t("adminSystemSummarizerTelemetry")} - Requests{status?.ai.requests ?? 0} - Cache hits{status?.ai.cacheHits ?? 0} - Cache misses{status?.ai.cacheMisses ?? 0} - Failures{status?.ai.failures ?? 0} - Probe failures{status?.ai.probeFailures ?? 0} - Avg latency{status?.ai.averageLatencyMs != null ? `${status.ai.averageLatencyMs} ms` : "-"} - OCR requests{status?.ai.ocrRequests ?? 0} - OCR avg latency{status?.ai.averageOcrLatencyMs != null ? `${status.ai.averageOcrLatencyMs} ms` : "-"} + {t("adminSystemRequests")}{status?.ai.requests ?? 0} + {t("adminSystemCacheHits")}{status?.ai.cacheHits ?? 0} + {t("adminSystemCacheMisses")}{status?.ai.cacheMisses ?? 0} + {t("adminSystemFailures")}{status?.ai.failures ?? 0} + {t("adminSystemProbeFailures")}{status?.ai.probeFailures ?? 0} + {t("adminSystemAvgLatency")}{status?.ai.averageLatencyMs != null ? `${status.ai.averageLatencyMs} ms` : "-"} + {t("adminSystemOcrRequests")}{status?.ai.ocrRequests ?? 0} + {t("adminSystemOcrAvgLatency")}{status?.ai.averageOcrLatencyMs != null ? `${status.ai.averageOcrLatencyMs} ms` : "-"} @@ -343,7 +343,7 @@ export default function AdminSystemPage() { - +