diff --git a/src/components/compose/compose-form.tsx b/src/components/compose/compose-form.tsx index 7582d54..c9d7c0c 100644 --- a/src/components/compose/compose-form.tsx +++ b/src/components/compose/compose-form.tsx @@ -4,6 +4,7 @@ import { useEffect, useMemo, useRef, useState } from "react"; import type { DragEvent } from "react"; import { ChevronUp, FileText, Forward, Maximize2, Minimize2, Minus, Paperclip, Reply, Trash2, X } from "lucide-react"; import { useRouter } from "next/navigation"; +import { useLanguage } from "@/components/language-provider"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -42,6 +43,7 @@ export function ComposeForm({ draftIdToLoad?: string | null; onClose?: () => void; }) { + const { t } = useLanguage(); const router = useRouter(); const { selectedMailbox, setSelectedMailbox, mailboxes } = useSelectedMailbox(); const [draftId, setDraftId] = useState(null); @@ -161,7 +163,7 @@ export function ComposeForm({ }) .catch((err) => { if (cancelled) return; - const message = err instanceof Error ? err.message : "Failed to load draft"; + const message = err instanceof Error ? err.message : t("compose.error.loadDraft"); setToast({ type: "error", message }); }) .finally(() => { @@ -239,16 +241,16 @@ export function ComposeForm({ async function onSubmit(event: React.FormEvent) { event.preventDefault(); if (to.length === 0) { - setToast({ type: "error", message: "Add at least one recipient" }); + setToast({ type: "error", message: t("compose.error.noRecipient") }); return; } const invalid = [...to, ...cc, ...bcc].find((entry) => !isValidRecipient(entry)); if (invalid) { - setToast({ type: "error", message: `"${invalid}" is not a valid email address` }); + setToast({ type: "error", message: t("compose.error.invalidAddress", { address: invalid }) }); return; } if (!hasMeaningfulHtml(html) && !quotedHtml) { - setToast({ type: "error", message: "Write a message before sending" }); + setToast({ type: "error", message: t("compose.error.emptyBody") }); return; } setLoading(true); @@ -261,20 +263,20 @@ export function ComposeForm({ for (const attachment of attachments) form.append("attachments", attachment.file); const uploaded = await authFetch(`/api/drafts/${draftId}/attachments`, { method: "POST", body: form }); const result = await uploaded.json() as { attachments?: ComposeStoredAttachment[]; error?: string }; - if (!uploaded.ok) throw new Error(result.error || "Could not add attachments to the draft"); + if (!uploaded.ok) throw new Error(result.error || t("compose.error.draftAttachments")); setStoredAttachments((current) => [...current, ...(result.attachments ?? [])]); setAttachments([]); } const updated = await authFetch(`/api/drafts/${draftId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ mailboxId: selectedMailbox?.id, from: fromAddr, to: recipientsToHeader(to), cc: recipientsToHeader(cc), bcc: recipientsToHeader(bcc), subject, html: fullHtml, text: htmlToPlainText(fullHtml), inReplyTo: threading?.inReplyTo ?? null, references: threading?.references ?? null, threadId: threading?.threadId ?? null, scheduledAt: scheduledAt?.toISOString() ?? null }) }); - if (!updated.ok) throw new Error("Could not save the draft for review"); + if (!updated.ok) throw new Error(t("compose.error.saveForReview")); const current = await fetchDraft(draftId); - if (!current.agent) throw new Error("AI draft metadata is missing"); + if (!current.agent) throw new Error(t("compose.error.aiMetadata")); setAgentRevision(current.agent.revision); const response = await authFetch("/api/agent/approvals", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ draftId, expectedRevision: current.agent.revision }) }); const result = await response.json() as { approvalId?: string; snapshot?: ReviewSnapshot; error?: string }; - if (!response.ok || !result.approvalId || !result.snapshot) throw new Error(result.error || "Could not create review"); + if (!response.ok || !result.approvalId || !result.snapshot) throw new Error(result.error || t("compose.error.createReview")); setAgentReview({ approvalId: result.approvalId, snapshot: result.snapshot }); - } catch (cause) { setToast({ type: "error", message: cause instanceof Error ? cause.message : "Could not review draft" }); } + } catch (cause) { setToast({ type: "error", message: cause instanceof Error ? cause.message : t("compose.error.reviewDraft") }); } finally { setLoading(false); } return; } @@ -299,7 +301,7 @@ export function ComposeForm({ setLoading(false); if (!res.ok) { - setToast({ type: "error", message: data.error ?? "Send failed" }); + setToast({ type: "error", message: data.error ?? t("compose.error.sendFailed") }); return; } @@ -321,7 +323,7 @@ export function ComposeForm({ setQuotedHtml(null); setAttachments([]); setScheduledAt(null); - setToast({ type: "success", message: data.scheduled ? "Message scheduled" : "Message sent" }); + setToast({ type: "success", message: data.scheduled ? t("compose.scheduled") : t("compose.sent") }); window.dispatchEvent(new Event("mailflare:messages-changed")); } @@ -334,7 +336,7 @@ export function ComposeForm({ const res = await authFetch(`/api/drafts/${draftId}`, { method: "DELETE" }); if (!res.ok) { setDeletingDraft(false); - setToast({ type: "error", message: "Could not delete draft" }); + setToast({ type: "error", message: t("compose.error.deleteDraft") }); return; } } @@ -366,7 +368,7 @@ export function ComposeForm({ if (!draftId) return; const res = await authFetch(`/api/drafts/${draftId}/attachments/${attachmentId}`, { method: "DELETE" }); if (!res.ok) { - setToast({ type: "error", message: "Could not remove attachment" }); + setToast({ type: "error", message: t("compose.error.removeAttachment") }); return; } setStoredAttachments((current) => current.filter((item) => item.id !== attachmentId)); @@ -384,15 +386,15 @@ export function ComposeForm({ ); if (nextCount > 10) { - setToast({ type: "error", message: "A message can include at most 10 attachments" }); + setToast({ type: "error", message: t("compose.error.maxAttachments") }); return; } if (nextFiles.some((file) => file.size > attachmentPolicy.maxMb * 1_000_000)) { - setToast({ type: "error", message: `Each attachment must be ${attachmentPolicy.maxMb} MB or smaller` }); + setToast({ type: "error", message: t("compose.error.attachmentSize", { max: attachmentPolicy.maxMb }) }); return; } if (totalSize > attachmentPolicy.maxMb * 1_000_000) { - setToast({ type: "error", message: `Attachments must total ${attachmentPolicy.maxMb} MB or less` }); + setToast({ type: "error", message: t("compose.error.totalSize", { max: attachmentPolicy.maxMb }) }); return; } @@ -443,7 +445,7 @@ export function ComposeForm({
{attachment.filename} @@ -454,7 +456,7 @@ export function ComposeForm({ className="rounded-full p-1 text-neutral-400 hover:bg-neutral-200 hover:text-neutral-700" > - Remove attachment + {t("compose.removeAttachment")}
))} @@ -478,12 +480,12 @@ export function ComposeForm({ className="rounded-full p-1 text-neutral-400 hover:bg-neutral-200 hover:text-neutral-700" > - Remove attachment + {t("compose.removeAttachment")} ))} -

Cloudflare limits general email messages to 5 MiB including encoding. Files over 3 MB{([...attachments.map((item) => item.file.size), ...storedAttachments.map((item) => item.size)].some((size) => size > attachmentPolicy.cloudThresholdBytes)) ? " here will" : " or files that exceed the message budget may"} be sent as 30-day R2 download links.

+

{[...attachments.map((item) => item.file.size), ...storedAttachments.map((item) => item.size)].some((size) => size > attachmentPolicy.cloudThresholdBytes) ? t("compose.cloudWarning.will") : t("compose.cloudWarning.may")}

); @@ -510,10 +512,10 @@ export function ComposeForm({ {toast.message} )} -
{ if (modalMode && !minimized && event.key === "Escape") { event.preventDefault(); setModalMode(false); } }} onDragEnterCapture={minimized ? undefined : onFileDragEnter} onDragOverCapture={minimized ? undefined : onFileDragOver} onDragLeaveCapture={minimized ? undefined : onFileDragLeave} onDropCapture={minimized ? undefined : onFileDrop}> + { if (modalMode && !minimized && event.key === "Escape") { event.preventDefault(); setModalMode(false); } }} onDragEnterCapture={minimized ? undefined : onFileDragEnter} onDragOverCapture={minimized ? undefined : onFileDragOver} onDragLeaveCapture={minimized ? undefined : onFileDragLeave} onDropCapture={minimized ? undefined : onFileDrop}> {draggingFiles && !minimized && ( )}
@@ -521,24 +523,24 @@ export function ComposeForm({ {threading?.inReplyTo && } {!threading?.inReplyTo && /^fwd?:/i.test(subject) && } {loadingDraft - ? "Loading draft" + ? t("compose.title.loading") : threading?.inReplyTo - ? "Reply" + ? t("compose.title.reply") : /^fwd?:/i.test(subject) - ? "Forward" + ? t("compose.title.forward") : draftId - ? "Draft saved" - : "New Message"} + ? t("compose.title.saved") + : t("compose.title.new")} {mode === "popup" && (
- - -
@@ -546,7 +548,7 @@ export function ComposeForm({
- + setSubject(event.target.value)} - placeholder="Subject" + placeholder={t("compose.subject")} required disabled={loadingDraft} className="h-8 border-0 px-0 py-1 shadow-none focus-visible:ring-0" />
- + @@ -638,7 +640,7 @@ export function ComposeForm({ disabled={loading || loadingDraft || !fromAddr} className="rounded-r-none px-4" > - {loading ? "Preparing…" : scheduledAt ? "Schedule" : agentRevision !== null ? "Review send" : "Send"} + {loading ? t("compose.preparing") : scheduledAt ? t("compose.schedule") : agentRevision !== null ? t("compose.reviewSend") : t("compose.send")} addAttachments(event.target.files)} /> - + - +