Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1e4d53c
feat: init i18n
nyzss Mar 10, 2025
780c644
feat: added configs and fetching user locale
nyzss Mar 10, 2025
3b568d5
feat: added different locales and ability to change locale in settings
nyzss Mar 10, 2025
7562aef
feat: added translation in sidebar navigation and settings
nyzss Mar 10, 2025
6b1e14e
feat: enhance i18n with deepmerge for fallback and restructured trans…
nyzss Mar 10, 2025
32ea875
feat: complete internationalization for settings pages
nyzss Mar 10, 2025
c4f39d5
feat(i18n): command palette, theme switcher, and nav user component t…
nyzss Mar 10, 2025
25b2d79
feat(i18n): add translations for mail components, search-filter
nyzss Mar 10, 2025
7868131
feat: create email translations added
nyzss Mar 10, 2025
763e7d1
Merge branch 'staging' of https://github.com/nizzyabi/Mail0 into feat…
nyzss Mar 11, 2025
5e0460c
chore: changed wording
nyzss Mar 11, 2025
439084c
fix: coderabbit potential issues fixed
nyzss Mar 11, 2025
fb78245
Merge branch 'staging' of https://github.com/nizzyabi/Mail0 into feat…
nyzss Mar 12, 2025
bfc9f07
feat: added more coverage on error pages, loading and toasts
nyzss Mar 12, 2025
8b5022c
fix: bad translation on turkish 'clearSearch'
nyzss Mar 12, 2025
4d60074
Merge branch 'staging' of https://github.com/nizzyabi/Mail0 into feat…
nyzss Mar 13, 2025
f10e0c3
Merge branch 'feat/i18n' of https://github.com/nyzss/mail0 into feat/…
nyzss Mar 13, 2025
a734fd4
Merge branch 'staging' of https://github.com/nizzyabi/Mail0 into feat…
nyzss Mar 13, 2025
da6b9c0
fix: connection button translation too big for smaller screens
nyzss Mar 13, 2025
3f34e80
feat: added spanish tl, checking locale with region id and added supp…
nyzss Mar 13, 2025
b3fe194
chore: rename /messages folder to /locales
nyzss Mar 13, 2025
a24aab5
feat: added TL for signing out and fixed spanish attachments tl
nyzss Mar 13, 2025
2c4aec2
minor
hiheyhello123 Mar 13, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 12 additions & 10 deletions apps/mail/app/(error)/not-found.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,39 @@
import { AlertCircle, ArrowLeft } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";

export function NotFound() {
const router = useRouter();
const t = useTranslations();

return (
<div className="flex w-full items-center justify-center bg-white text-center dark:bg-background">
<div className="flex-col items-center justify-center dark:text-gray-100 md:flex">
<div className="dark:bg-background flex w-full items-center justify-center bg-white text-center">
<div className="flex-col items-center justify-center md:flex dark:text-gray-100">
<div className="relative">
<h1 className="select-none text-[150px] font-bold text-muted-foreground/20">404</h1>
<h1 className="text-muted-foreground/20 select-none text-[150px] font-bold">404</h1>
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
<AlertCircle className="h-20 w-20 text-muted-foreground" />
<AlertCircle className="text-muted-foreground h-20 w-20" />
</div>
</div>

{/* Message */}
<div className="space-y-2">
<h2 className="text-2xl font-semibold tracking-tight">Page Not Found</h2>
<p className="text-muted-foreground">
Oops! The page you&apos;re looking for doesn&apos;t exist or has been moved.
</p>
<h2 className="text-2xl font-semibold tracking-tight">
{t("pages.error.notFound.title")}
</h2>
<p className="text-muted-foreground">{t("pages.error.notFound.description")}</p>
</div>

{/* Buttons */}
<div className="mt-2 flex gap-2">
<Button
variant="outline"
onClick={() => router.back()}
className="gap-2 text-muted-foreground"
className="text-muted-foreground gap-2"
>
<ArrowLeft className="h-4 w-4" />
Go Back
{t("pages.error.notFound.goBack")}
</Button>
</div>
</div>
Expand Down
4 changes: 3 additions & 1 deletion apps/mail/app/(routes)/settings/[...settings]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import AppearancePage from "../appearance/page";
import ShortcutsPage from "../shortcuts/page";
import SecurityPage from "../security/page";
import { useParams } from "next/navigation";
import { useTranslations } from "next-intl";
import GeneralPage from "../general/page";

const settingsPages: Record<string, React.ComponentType> = {
Expand All @@ -20,11 +21,12 @@ const settingsPages: Record<string, React.ComponentType> = {
export default function SettingsPage() {
const params = useParams();
const section = params.settings?.[0] || "general";
const t = useTranslations();

const SettingsComponent = settingsPages[section];

if (!SettingsComponent) {
return <div>404 - Settings page not found</div>;
return <div>{t("pages.error.settingsNotFound")}</div>;
}

return <SettingsComponent />;
Expand Down
15 changes: 11 additions & 4 deletions apps/mail/app/(routes)/settings/appearance/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ModeToggle } from "@/components/theme/theme-switcher";
import { zodResolver } from "@hookform/resolvers/zod";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { useTranslations } from "next-intl";
import { useForm } from "react-hook-form";
import { useState } from "react";
import * as z from "zod";
Expand All @@ -18,6 +19,7 @@ const formSchema = z.object({

export default function AppearancePage() {
const [isSaving, setIsSaving] = useState(false);
const t = useTranslations();

const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
Expand All @@ -37,15 +39,20 @@ export default function AppearancePage() {
return (
<div className="grid gap-6">
<SettingsCard
title="Appearance"
description="Customize colors, fonts and view options."
title={t("pages.settings.appearance.title")}
description={t("pages.settings.appearance.description")}
footer={
<Button type="submit" form="appearance-form" disabled={isSaving}>
{isSaving ? t("common.actions.saving") : t("common.actions.saveChanges")}
</Button>
}
>
<Form {...form}>
<form id="appearance-form" onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<div className="space-y-4">
<div className="space-y-2">
<Label>Theme</Label>
<ModeToggle className="w-36 bg-popover" />
<Label>{t("pages.settings.appearance.theme")}</Label>
<ModeToggle className="bg-popover w-36" />
</div>
</div>
</form>
Expand Down
49 changes: 29 additions & 20 deletions apps/mail/app/(routes)/settings/connections/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ import {
} from "@/components/ui/dialog";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { SettingsCard } from "@/components/settings/settings-card";
import { AddConnectionDialog } from "@/components/connection/add";
import { emailProviders } from "@/constants/emailProviders";
import { useConnections } from "@/hooks/use-connections";
import { deleteConnection } from "@/actions/connections";
import { AddConnectionDialog } from "@/components/connection/add";
import { Skeleton } from "@/components/ui/skeleton";
import { Button } from "@/components/ui/button";
import { useSession } from "@/lib/auth-client";
import { useTranslations } from "next-intl";
import { Trash, Plus } from "lucide-react";
import { useState } from "react";
import Image from "next/image";
Expand All @@ -27,47 +28,51 @@ export default function ConnectionsPage() {
const { refetch } = useSession();
const { data: connections, mutate, isLoading } = useConnections();
const [openTooltip, setOpenTooltip] = useState<string | null>(null);
const t = useTranslations();

const disconnectAccount = async (connectionId: string) => {
try {
await deleteConnection(connectionId);
toast.success("Account disconnected successfully");
toast.success(t("pages.settings.connections.disconnectSuccess"));
mutate();
refetch();
} catch (error) {
console.error("Error disconnecting account:", error);
toast.error("Failed to disconnect account");
toast.error(t("pages.settings.connections.disconnectError"));
}
};

return (
<div className="grid gap-6">
<SettingsCard title="Email Connections" description="Connect your email accounts to Zero.">
<SettingsCard
title={t("pages.settings.connections.title")}
description={t("pages.settings.connections.description")}
>
<div className="space-y-6">
{isLoading ? (
<div className="grid md:grid-cols-3 gap-4">
<div className="grid gap-4 md:grid-cols-3">
{[...Array(3)].map((_, i) => (
<div
key={i}
className="flex items-center justify-between rounded-lg border p-4 bg-popover"
className="bg-popover flex items-center justify-between rounded-lg border p-4"
>
<div className="flex min-w-0 items-center gap-4">
<Skeleton className="h-12 w-12 rounded-lg" />
<div className="flex-col gap-1 flex">
<div className="flex flex-col gap-1">
<Skeleton className="h-4 w-full lg:w-32" />
<Skeleton className="h-3 w-full lg:w-48" />
</div>
</div>
<Skeleton className="h-8 w-8 rounded-full ml-4" />
<Skeleton className="ml-4 h-8 w-8 rounded-full" />
</div>
))}
</div>
) : connections?.length ? (
<div className="grid md:grid-cols-3 gap-4">
<div className="grid gap-4 md:grid-cols-3">
{connections.map((connection) => (
<div
key={connection.id}
className="flex items-center justify-between rounded-lg border p-4 bg-popover"
className="bg-popover flex items-center justify-between rounded-lg border p-4"
>
<div className="flex min-w-0 items-center gap-4">
{connection.picture ? (
Expand All @@ -81,7 +86,7 @@ export default function ConnectionsPage() {
) : (
<div className="bg-primary/10 flex h-12 w-12 shrink-0 items-center justify-center rounded-lg">
<svg viewBox="0 0 24 24" className="text-primary h-6 w-6">
<path fill="currentColor" d={emailProviders[0]!.icon} />
<path fill="currentColor" d={emailProviders[0]!.icon} />
</svg>
</div>
)}
Expand Down Expand Up @@ -123,24 +128,28 @@ export default function ConnectionsPage() {
<Button
variant="ghost"
size="icon"
className="text-muted-foreground hover:text-primary shrink-0 ml-4"
className="text-muted-foreground hover:text-primary ml-4 shrink-0"
>
<Trash className="h-4 w-4" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Disconnect Email Account</DialogTitle>
<DialogTitle>{t("pages.settings.connections.disconnectTitle")}</DialogTitle>
<DialogDescription>
Are you sure you want to disconnect this email?
{t("pages.settings.connections.disconnectDescription")}
</DialogDescription>
</DialogHeader>
<div className="flex justify-end gap-4">
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
<Button variant="outline">
{t("pages.settings.connections.cancel")}
</Button>
</DialogClose>
<DialogClose asChild>
<Button onClick={() => disconnectAccount(connection.id)}>Remove</Button>
<Button onClick={() => disconnectAccount(connection.id)}>
{t("pages.settings.connections.remove")}
</Button>
</DialogClose>
</div>
</DialogContent>
Expand All @@ -152,13 +161,13 @@ export default function ConnectionsPage() {

<div className="flex items-center justify-start">
<AddConnectionDialog>
<Button
variant="outline"
className="group relative w-9 overflow-hidden transition-all duration-200 hover:w-40"
<Button
variant="outline"
className="group relative w-9 overflow-hidden transition-all duration-200 hover:w-full sm:hover:w-[32.5%]"
>
<Plus className="absolute left-2 h-4 w-4" />
<span className="whitespace-nowrap pl-7 opacity-0 transition-opacity duration-200 group-hover:opacity-100">
Add Connection
{t("pages.settings.connections.addEmail")}
</span>
</Button>
</AddConnectionDialog>
Expand Down
52 changes: 35 additions & 17 deletions apps/mail/app/(routes)/settings/general/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@ import {
SelectValue,
} from "@/components/ui/select";
import { SettingsCard } from "@/components/settings/settings-card";
import { availableLocales, defaultLocale } from "@/i18n/config";
import { zodResolver } from "@hookform/resolvers/zod";
import { Globe, Clock, LogOut } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import { signOut } from "@/lib/auth-client";
import { useRouter } from "next/navigation";
import { changeLocale } from "@/i18n/utils";
import { useTranslations, useLocale } from "next-intl";
import { useForm } from "react-hook-form";
import { useState } from "react";
import { toast } from "sonner";
Expand All @@ -37,11 +40,12 @@ const formSchema = z.object({
export default function GeneralPage() {
const router = useRouter();
const [isSaving, setIsSaving] = useState(false);
const locale = useLocale();

const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
language: "en",
language: locale,
timezone: "UTC",
dynamicContent: false,
externalImages: true,
Expand All @@ -53,6 +57,8 @@ export default function GeneralPage() {

// TODO: Save settings in user's account

changeLocale(values.language);

// Simulate API call
setTimeout(() => {
console.log(values);
Expand All @@ -70,26 +76,28 @@ export default function GeneralPage() {
},
}),
{
loading: "Signing out...",
success: () => "Signed out successfully!",
error: "Error signing out",
loading: t("common.actions.signingOut"),
success: () => t("common.actions.signedOutSuccess"),
error: t("common.actions.signOutError"),
Comment on lines +79 to +81
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Use translation function before defining it.

The translation function t is used in these toast message lines before it's actually defined on line 85. This could potentially cause errors.

Move the translation hook declaration above its first usage:

- toast.promise(
-   signOut({
-     fetchOptions: {
-       onSuccess: () => {
-         router.push("/");
-       },
-     },
-   }),
-   {
-     loading: t("common.actions.signingOut"),
-     success: () => t("common.actions.signedOutSuccess"),
-     error: t("common.actions.signOutError"),
-   },
- );
-};
-
-const t = useTranslations();

+ const t = useTranslations();
+
+ toast.promise(
+   signOut({
+     fetchOptions: {
+       onSuccess: () => {
+         router.push("/");
+       },
+     },
+   }),
+   {
+     loading: t("common.actions.signingOut"),
+     success: () => t("common.actions.signedOutSuccess"),
+     error: t("common.actions.signOutError"),
+   },
+ );
+};
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
loading: t("common.actions.signingOut"),
success: () => t("common.actions.signedOutSuccess"),
error: t("common.actions.signOutError"),
const t = useTranslations();
toast.promise(
signOut({
fetchOptions: {
onSuccess: () => {
router.push("/");
},
},
}),
{
loading: t("common.actions.signingOut"),
success: () => t("common.actions.signedOutSuccess"),
error: t("common.actions.signOutError"),
},
);
};

},
);
};

const t = useTranslations();

return (
<div className="grid gap-6">
<SettingsCard
title="General Settings"
description="Manage settings for your language and email display preferences."
title={t("pages.settings.general.title")}
description={t("pages.settings.general.description")}
footer={
<div className="flex gap-4">
<Button variant="destructive" onClick={handleSignOut}>
<LogOut className="mr-2 h-4 w-4" />
Log out
{t("common.actions.logout")}
</Button>
<Button type="submit" form="general-form" disabled={isSaving}>
{isSaving ? "Saving..." : "Save changes"}
{isSaving ? t("common.actions.saving") : t("common.actions.saveChanges")}
</Button>
</div>
}
Expand All @@ -102,7 +110,7 @@ export default function GeneralPage() {
name="language"
render={({ field }) => (
<FormItem>
<FormLabel>Language</FormLabel>
<FormLabel>{t("pages.settings.general.language")}</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger className="w-36">
Expand All @@ -111,7 +119,11 @@ export default function GeneralPage() {
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="en">English</SelectItem>
{availableLocales.map((locale) => (
<SelectItem key={locale.code} value={locale.code}>
{locale.name}
</SelectItem>
))}
</SelectContent>
</Select>
</FormItem>
Expand All @@ -123,7 +135,7 @@ export default function GeneralPage() {
render={({ field }) => (
// TODO: Add all timezones
<FormItem>
<FormLabel>Timezone</FormLabel>
<FormLabel>{t("pages.settings.general.timezone")}</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger className="w-36">
Expand Down Expand Up @@ -151,10 +163,14 @@ export default function GeneralPage() {
control={form.control}
name="dynamicContent"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border bg-popover p-4">
<FormItem className="bg-popover flex flex-row items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel className="text-base">Dynamic Content</FormLabel>
<FormDescription>Allow emails to display dynamic content.</FormDescription>
<FormLabel className="text-base">
{t("pages.settings.general.dynamicContent")}
</FormLabel>
<FormDescription>
{t("pages.settings.general.dynamicContentDescription")}
</FormDescription>
</div>
<FormControl className="ml-4">
<Switch checked={field.value} onCheckedChange={field.onChange} />
Expand All @@ -166,11 +182,13 @@ export default function GeneralPage() {
control={form.control}
name="externalImages"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4 bg-popover">
<FormItem className="bg-popover flex flex-row items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel className="text-base">Display External Images</FormLabel>
<FormLabel className="text-base">
{t("pages.settings.general.externalImages")}
</FormLabel>
<FormDescription>
Allow emails to display images from external sources.
{t("pages.settings.general.externalImagesDescription")}
</FormDescription>
</div>
<FormControl className="ml-4">
Expand Down
Loading