- To
+ {t("common.mailDisplay.to")}
{toEmails.map((email, index) => (
@@ -302,7 +305,7 @@ export function CreateEmail() {
setToInput(e.target.value)}
onKeyDown={(e) => {
@@ -325,12 +328,12 @@ export function CreateEmail() {
- Subject
+ {t("common.searchBar.subject")}
{
setSubjectInput(e.target.value);
@@ -341,7 +344,7 @@ export function CreateEmail() {
- Body
+ {t("pages.createEmail.body")}
{defaultValue && (
@@ -349,7 +352,7 @@ export function CreateEmail() {
initialValue={defaultValue}
onChange={(newContent) => setMessageContent(newContent)}
key={resetEditorKey}
- placeholder="Write your message here..."
+ placeholder={t("pages.createEmail.writeYourMessageHere")}
/>
)}
@@ -366,16 +369,20 @@ export function CreateEmail() {
- {attachments.length} attachment{attachments.length !== 1 ? "s" : ""}
+ {attachments.length}{" "}
+ {t("common.replyCompose.fileCount", { count: attachments.length })}
-
Attachments
+
+ {t("pages.createEmail.attachments")}
+
- {attachments.length} file{attachments.length !== 1 ? "s" : ""} attached
+ {attachments.length}{" "}
+ {t("common.replyCompose.fileCount", { count: attachments.length })}
@@ -418,7 +425,7 @@ export function CreateEmail() {
onClick={() => document?.getElementById("file-upload")?.click()}
>
- Attachments
+ {t("pages.createEmail.attachments")}
- Send
+ {t("common.replyCompose.send")}
diff --git a/apps/mail/components/mail/mail-display.tsx b/apps/mail/components/mail/mail-display.tsx
index b52f9bc2b6..93cef877ef 100644
--- a/apps/mail/components/mail/mail-display.tsx
+++ b/apps/mail/components/mail/mail-display.tsx
@@ -6,6 +6,7 @@ import AttachmentDialog from "./attachment-dialog";
import { useSummary } from "@/hooks/use-summary";
import { TextShimmer } from "../ui/text-shimmer";
import { Separator } from "../ui/separator";
+import { useTranslations } from "next-intl";
import { useEffect, useState } from "react";
import { MailIframe } from "./mail-iframe";
import { ParsedMessage } from "@/types";
@@ -86,6 +87,8 @@ const MailDisplay = ({ emailData, isMuted, index, demo }: Props) => {
url: string;
}>(null);
const [openDetailsPopover, setOpenDetailsPopover] = useState(false);
+ const t = useTranslations();
+
const { data } = demo
? {
data: {
@@ -143,7 +146,7 @@ const MailDisplay = ({ emailData, isMuted, index, demo }: Props) => {
className="h-auto p-0 text-xs underline hover:bg-transparent"
onClick={() => setOpenDetailsPopover(true)}
>
- Details
+ {t("common.mailDisplay.details")}
{
>
-
From:
+
+ {t("common.mailDisplay.from")}:
+
{emailData?.sender?.name}
@@ -163,39 +168,52 @@ const MailDisplay = ({ emailData, isMuted, index, demo }: Props) => {
- To:
+
+ {t("common.mailDisplay.to")}:
+
{emailData?.sender?.email}
- Cc:
+
+ {t("common.mailDisplay.cc")}:
+
{emailData?.sender?.email}
- Date:
+
+ {t("common.mailDisplay.date")}:
+
{format(new Date(emailData?.receivedOn), "PPpp")}
- Mailed-By:
+
+ {t("common.mailDisplay.mailedBy")}:
+
{emailData?.sender?.email}
- Signed-By:
+
+ {t("common.mailDisplay.signedBy")}:
+
{emailData?.sender?.email}
-
Security:
+
+ {t("common.mailDisplay.security")}:
+
- Standard encryption (TLS)
+ {" "}
+ {t("common.mailDisplay.standardEncryption")} (TLS)
diff --git a/apps/mail/components/mail/mail-iframe.tsx b/apps/mail/components/mail/mail-iframe.tsx
index e6f33ca876..6b2bf71dc7 100644
--- a/apps/mail/components/mail/mail-iframe.tsx
+++ b/apps/mail/components/mail/mail-iframe.tsx
@@ -1,5 +1,6 @@
import { fixNonReadableColors, template } from "@/lib/email-utils";
import { useEffect, useMemo, useRef, useState } from "react";
+import { useTranslations } from "next-intl";
import { Loader2 } from "lucide-react";
import { useTheme } from "next-themes";
import { cn } from "@/lib/utils";
@@ -12,6 +13,8 @@ export function MailIframe({ html }: { html: string }) {
const iframeDoc = useMemo(() => template(html), [html]);
+ const t = useTranslations();
+
useEffect(() => {
if (!iframeRef.current) return;
const url = URL.createObjectURL(new Blob([iframeDoc], { type: "text/html" }));
@@ -44,7 +47,7 @@ export function MailIframe({ html }: { html: string }) {
{!loaded && (
- Loading email content...
+ {t("common.mailDisplay.loadingMailContent")}
)}
@@ -175,7 +174,12 @@ export function MailLayout() {
}
}, [session?.user, isPending]);
- const { isLoading, isValidating } = useThreads(folder, undefined, searchValue.value, defaultPageSize);
+ const { isLoading, isValidating } = useThreads(
+ folder,
+ undefined,
+ searchValue.value,
+ defaultPageSize,
+ );
const [open, setOpen] = useState(false);
const isDesktop = useMediaQuery("(min-width: 768px)");
@@ -244,7 +248,7 @@ export function MailLayout() {
/>
@@ -260,7 +264,7 @@ export function MailLayout() {
)}
-
+
{!searchMode && (
<>
{mail.bulkSelected.length > 0 ? (
@@ -288,7 +292,7 @@ export function MailLayout() {
) : (
<>
-
+
) : (
-
+
)}
@@ -395,70 +397,79 @@ function BulkSelectActions() {
const categories = [
{
- name: "Primary",
+ name: "common.mailCategories.primary",
searchValue: "",
icon:
,
- colors: "border-0 bg-gray-200 text-gray-700 dark:bg-gray-800/50 dark:text-gray-400 dark:hover:bg-gray-800/70"
+ colors:
+ "border-0 bg-gray-200 text-gray-700 dark:bg-gray-800/50 dark:text-gray-400 dark:hover:bg-gray-800/70",
},
{
- name: "Important",
+ name: "common.mailCategories.important",
searchValue: "is:important",
icon:
,
- colors: "border-0 text-amber-800 bg-amber-100 dark:bg-amber-900/20 dark:text-amber-500 dark:hover:bg-amber-900/30"
+ colors:
+ "border-0 text-amber-800 bg-amber-100 dark:bg-amber-900/20 dark:text-amber-500 dark:hover:bg-amber-900/30",
},
{
- name: "Personal",
+ name: "common.mailCategories.personal",
searchValue: "is:personal",
icon:
,
- colors: "border-0 text-green-800 bg-green-100 dark:bg-green-900/20 dark:text-green-500 dark:hover:bg-green-900/30"
+ colors:
+ "border-0 text-green-800 bg-green-100 dark:bg-green-900/20 dark:text-green-500 dark:hover:bg-green-900/30",
},
{
- name: "Updates",
+ name: "common.mailCategories.updates",
searchValue: "is:updates",
icon:
,
- colors: "border-0 text-purple-800 bg-purple-100 dark:bg-purple-900/20 dark:text-purple-500 dark:hover:bg-purple-900/30"
+ colors:
+ "border-0 text-purple-800 bg-purple-100 dark:bg-purple-900/20 dark:text-purple-500 dark:hover:bg-purple-900/30",
},
{
- name: "Promotions",
+ name: "common.mailCategories.promotions",
searchValue: "is:promotions",
icon:
,
- colors: "border-0 text-red-800 bg-red-100 dark:bg-red-900/20 dark:text-red-500 dark:hover:bg-red-900/30"
+ colors:
+ "border-0 text-red-800 bg-red-100 dark:bg-red-900/20 dark:text-red-500 dark:hover:bg-red-900/30",
},
];
-function MailCategoryTabs({
- iconsOnly = false,
+function MailCategoryTabs({
+ iconsOnly = false,
isLoading = false,
onCategoryChange,
- initialCategory
-}: {
- iconsOnly?: boolean,
- isLoading?: boolean,
- onCategoryChange?: (category: string) => void,
- initialCategory?: string
+ initialCategory,
+}: {
+ iconsOnly?: boolean;
+ isLoading?: boolean;
+ onCategoryChange?: (category: string) => void;
+ initialCategory?: string;
}) {
const [, setSearchValue] = useSearchValue();
-
+ const t = useTranslations();
+
// Initialize with just the initialCategory or "Primary"
const [activeCategory, setActiveCategory] = useState(initialCategory || "Primary");
-
+
// Move localStorage logic to a useEffect
useEffect(() => {
// Check localStorage only after initial render
- const savedCategory = localStorage.getItem('mailActiveCategory');
+ const savedCategory = localStorage.getItem("mailActiveCategory");
if (savedCategory && !initialCategory) {
setActiveCategory(savedCategory);
}
}, [initialCategory]);
-
+
const containerRef = useRef
(null);
const activeTabElementRef = useRef(null);
- const activeTab = useMemo(() => categories.find(cat => cat.name === activeCategory), [activeCategory]);
+ const activeTab = useMemo(
+ () => categories.find((cat) => cat.name === activeCategory),
+ [activeCategory],
+ );
// Save to localStorage when activeCategory changes
useEffect(() => {
- localStorage.setItem('mailActiveCategory', activeCategory);
+ localStorage.setItem("mailActiveCategory", activeCategory);
if (onCategoryChange) {
onCategoryChange(activeCategory);
}
@@ -484,7 +495,7 @@ function MailCategoryTabs({
const clipLeft = Math.max(0, offsetLeft - 2);
const clipRight = Math.min(container.offsetWidth, offsetLeft + offsetWidth + 2);
const containerWidth = container.offsetWidth;
-
+
if (containerWidth) {
container.style.clipPath = `inset(0 ${Number(100 - (clipRight / containerWidth) * 100).toFixed(2)}% 0 ${Number((clipLeft / containerWidth) * 100).toFixed(2)}%)`;
}
@@ -502,7 +513,7 @@ function MailCategoryTabs({
const timer = setTimeout(() => {
updateClipPath();
}, 10);
-
+
return () => clearTimeout(timer);
}, [iconsOnly, updateClipPath]);
@@ -512,12 +523,12 @@ function MailCategoryTabs({
updateClipPath();
};
- window.addEventListener('resize', handleResize);
- return () => window.removeEventListener('resize', handleResize);
+ window.addEventListener("resize", handleResize);
+ return () => window.removeEventListener("resize", handleResize);
}, [updateClipPath]);
return (
-
+
{categories.map((category) => (
@@ -530,21 +541,21 @@ function MailCategoryTabs({
setActiveCategory(category.name);
}}
className={cn(
- "flex h-7 items-center gap-1.5 px-2 text-xs font-medium rounded-md transition-all duration-200",
- activeCategory === category.name
+ "flex h-7 items-center gap-1.5 rounded-full px-2 text-xs font-medium transition-all duration-200",
+ activeCategory === category.name
? category.colors
- : "text-muted-foreground hover:text-foreground hover:bg-muted/50"
+ : "text-muted-foreground hover:text-foreground hover:bg-muted/50",
)}
>
- {category.icon}
-
- {category.name}
-
+ {category.icon}
+
+ {t(category.name as MessageKey)}
+
{iconsOnly && (
- {category.name}
+ {t(category.name as MessageKey)}
)}
@@ -552,9 +563,9 @@ function MailCategoryTabs({
))}
-
@@ -566,14 +577,14 @@ function MailCategoryTabs({
setActiveCategory(category.name);
}}
className={cn(
- "flex h-7 items-center gap-1.5 px-2 text-xs font-medium rounded-md",
- category.colors
+ "flex h-7 items-center gap-1.5 rounded-full px-2 text-xs font-medium",
+ category.colors,
)}
tabIndex={-1}
>
{category.icon}
-
- {category.name}
+
+ {t(category.name as MessageKey)}
diff --git a/apps/mail/components/mail/reply-composer.tsx b/apps/mail/components/mail/reply-composer.tsx
index bd8f1c5e23..127e3fe975 100644
--- a/apps/mail/components/mail/reply-composer.tsx
+++ b/apps/mail/components/mail/reply-composer.tsx
@@ -1,15 +1,16 @@
+import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
+import { Dispatch, SetStateAction, useRef, useState, useEffect } from "react";
+import { UploadedFileIcon } from "@/components/create/uploaded-file-icon";
import { ArrowUp, Paperclip, Reply, X, Plus } from "lucide-react";
import { cleanEmailAddress, truncateFileName } from "@/lib/utils";
+import { Separator } from "@/components/ui/separator";
import Editor from "@/components/create/editor";
import { Button } from "@/components/ui/button";
+import { useTranslations } from "next-intl";
import { sendEmail } from "@/actions/send";
-import { Dispatch, SetStateAction, useRef, useState, useEffect } from "react";
import { ParsedMessage } from "@/types";
import { cn } from "@/lib/utils";
import { toast } from "sonner";
-import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
-import { Separator } from "@/components/ui/separator";
-import { UploadedFileIcon } from "@/components/create/uploaded-file-icon";
interface ReplyComposeProps {
emailData: ParsedMessage[];
@@ -17,11 +18,7 @@ interface ReplyComposeProps {
setIsOpen?: Dispatch>;
}
-export default function ReplyCompose({
- emailData,
- isOpen,
- setIsOpen
-}: ReplyComposeProps) {
+export default function ReplyCompose({ emailData, isOpen, setIsOpen }: ReplyComposeProps) {
const editorRef = useRef(null);
const [attachments, setAttachments] = useState([]);
const [isUploading, setIsUploading] = useState(false);
@@ -29,6 +26,7 @@ export default function ReplyCompose({
const [isComposerOpen, setIsComposerOpen] = useState(false);
const [isDragging, setIsDragging] = useState(false);
const [isEditorFocused, setIsEditorFocused] = useState(false);
+ const t = useTranslations();
// Use external state if provided, otherwise use internal state
const composerIsOpen = isOpen !== undefined ? isOpen : isComposerOpen;
@@ -43,7 +41,7 @@ export default function ReplyCompose({
// Handle keyboard shortcuts for sending email
const handleKeyDown = (e: React.KeyboardEvent) => {
// Check for Cmd/Ctrl + Enter
- if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
+ if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
e.preventDefault();
if (isFormValid) {
handleSendEmail(e as unknown as React.MouseEvent);
@@ -68,7 +66,7 @@ export default function ReplyCompose({
};
const handleDragOver = (e: React.DragEvent) => {
- if (!e.target || !(e.target as HTMLElement).closest('.ProseMirror')) {
+ if (!e.target || !(e.target as HTMLElement).closest(".ProseMirror")) {
e.preventDefault();
e.stopPropagation();
setIsDragging(true);
@@ -76,7 +74,7 @@ export default function ReplyCompose({
};
const handleDragLeave = (e: React.DragEvent) => {
- if (!e.target || !(e.target as HTMLElement).closest('.ProseMirror')) {
+ if (!e.target || !(e.target as HTMLElement).closest(".ProseMirror")) {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
@@ -84,11 +82,11 @@ export default function ReplyCompose({
};
const handleDrop = (e: React.DragEvent) => {
- if (!e.target || !(e.target as HTMLElement).closest('.ProseMirror')) {
+ if (!e.target || !(e.target as HTMLElement).closest(".ProseMirror")) {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
-
+
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
setAttachments([...attachments, ...Array.from(e.dataTransfer.files)]);
// Open the composer if it's not already open
@@ -169,10 +167,10 @@ export default function ReplyCompose({
setMessageContent("");
setComposerIsOpen(false);
- toast.success("Email sent successfully!");
+ toast.success(t("pages.createEmail.emailSentSuccessfully"));
} catch (error) {
console.error("Error sending email:", error);
- toast.error("Failed to send email. Please try again.");
+ toast.error(t("pages.createEmail.failedToSendEmail"));
}
};
@@ -189,26 +187,29 @@ export default function ReplyCompose({
// Give the editor time to render before focusing
const timer = setTimeout(() => {
// Focus the editor - Novel editor typically has a ProseMirror element
- const editorElement = document.querySelector('.ProseMirror');
+ const editorElement = document.querySelector(".ProseMirror");
if (editorElement instanceof HTMLElement) {
editorElement.focus();
}
}, 100);
-
+
return () => clearTimeout(timer);
}
}, [composerIsOpen]);
// Check if the message is empty
- const isMessageEmpty = !messageContent || messageContent === JSON.stringify({
- type: "doc",
- content: [
- {
- type: "paragraph",
- content: [],
- },
- ],
- });
+ const isMessageEmpty =
+ !messageContent ||
+ messageContent ===
+ JSON.stringify({
+ type: "doc",
+ content: [
+ {
+ type: "paragraph",
+ content: [],
+ },
+ ],
+ });
// Check if form is valid for submission
const isFormValid = !isMessageEmpty || attachments.length > 0;
@@ -222,7 +223,10 @@ export default function ReplyCompose({
variant="outline"
>
- Reply to {emailData[emailData.length - 1]?.sender?.name || "this email"}
+
+ {t("common.replyCompose.replyTo")}{" "}
+ {emailData[emailData.length - 1]?.sender?.name || t("common.replyCompose.thisEmail")}
+
);
@@ -232,8 +236,8 @@ export default function ReplyCompose({