diff --git a/app/dashboard/domains/page.tsx b/app/[lang]/dashboard/domains/page.tsx
similarity index 100%
rename from app/dashboard/domains/page.tsx
rename to app/[lang]/dashboard/domains/page.tsx
diff --git a/app/[lang]/dashboard/layout.tsx b/app/[lang]/dashboard/layout.tsx
new file mode 100644
index 0000000..2a8f11e
--- /dev/null
+++ b/app/[lang]/dashboard/layout.tsx
@@ -0,0 +1,32 @@
+import { auth } from "@/auth";
+import { redirect } from "next/navigation";
+import Providers from "@/components/Providers";
+import Sidebar from "@/components/Sidebar";
+import { getDictionary, Locale } from "@/app/dictionaries";
+
+export default async function DashboardLayout(
+ props: {
+ children: React.ReactNode;
+ params: Promise<{ lang: string }>;
+ }
+) {
+ const params = await props.params;
+
+ const {
+ children
+ } = props;
+
+ const session = await auth();
+ if (!session) redirect(`/${params.lang}/login`);
+
+ const dict = await getDictionary(params.lang as Locale);
+
+ return (
+
+
+
+ );
+}
diff --git a/app/dashboard/mail/page.tsx b/app/[lang]/dashboard/mail/page.tsx
similarity index 100%
rename from app/dashboard/mail/page.tsx
rename to app/[lang]/dashboard/mail/page.tsx
diff --git a/app/dashboard/mailboxes/page.tsx b/app/[lang]/dashboard/mailboxes/page.tsx
similarity index 100%
rename from app/dashboard/mailboxes/page.tsx
rename to app/[lang]/dashboard/mailboxes/page.tsx
diff --git a/app/dashboard/page.tsx b/app/[lang]/dashboard/page.tsx
similarity index 100%
rename from app/dashboard/page.tsx
rename to app/[lang]/dashboard/page.tsx
diff --git a/app/dashboard/users/page.tsx b/app/[lang]/dashboard/users/page.tsx
similarity index 100%
rename from app/dashboard/users/page.tsx
rename to app/[lang]/dashboard/users/page.tsx
diff --git a/app/[lang]/layout.tsx b/app/[lang]/layout.tsx
new file mode 100644
index 0000000..5101cb1
--- /dev/null
+++ b/app/[lang]/layout.tsx
@@ -0,0 +1,30 @@
+import type { Metadata } from "next";
+import "../globals.css";
+
+export const metadata: Metadata = {
+ title: "AyrisMail Central",
+ description: "Multi-tenant Mailcow yönetim paneli — AyrisTech",
+};
+
+export async function generateStaticParams() {
+ return [{ lang: "tr" }, { lang: "en" }];
+}
+
+export default async function RootLayout(
+ props: {
+ children: React.ReactNode;
+ params: Promise<{ lang: string }>;
+ }
+) {
+ const params = await props.params;
+
+ const {
+ children
+ } = props;
+
+ return (
+
+
{children}
+
+ );
+}
diff --git a/app/login/page.tsx b/app/[lang]/login/LoginForm.tsx
similarity index 74%
rename from app/login/page.tsx
rename to app/[lang]/login/LoginForm.tsx
index c256ea1..a2ad9f6 100644
--- a/app/login/page.tsx
+++ b/app/[lang]/login/LoginForm.tsx
@@ -4,7 +4,7 @@ import { useState, useTransition } from "react";
import { signIn } from "next-auth/react";
import { useRouter } from "next/navigation";
-export default function LoginPage() {
+export default function LoginForm({ dict, lang }: { dict: any; lang: string }) {
const router = useRouter();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
@@ -23,9 +23,9 @@ export default function LoginPage() {
});
if (res?.error) {
- setError("E-posta veya şifre hatalı.");
+ setError("E-posta veya şifre hatalı."); // We can translate this later
} else {
- router.push("/dashboard");
+ router.push(`/${lang}/dashboard`);
router.refresh();
}
});
@@ -41,20 +41,20 @@ export default function LoginPage() {
- AyrisMail Central
- Mail sunucunuzu kolayca yönetin
+ {dict.title || "AyrisMail Central"}
+ {dict.subtitle || "Mail sunucunuzu kolayca yönetin"}
diff --git a/app/[lang]/login/page.tsx b/app/[lang]/login/page.tsx
new file mode 100644
index 0000000..aee8d30
--- /dev/null
+++ b/app/[lang]/login/page.tsx
@@ -0,0 +1,13 @@
+import { getDictionary, Locale } from "@/app/dictionaries";
+import LoginForm from "./LoginForm";
+
+export default async function LoginPage(
+ props: {
+ params: Promise<{ lang: string }>;
+ }
+) {
+ const params = await props.params;
+
+ const dict = await getDictionary(params.lang as Locale);
+ return ;
+}
diff --git a/app/[lang]/page.tsx b/app/[lang]/page.tsx
new file mode 100644
index 0000000..30e1e58
--- /dev/null
+++ b/app/[lang]/page.tsx
@@ -0,0 +1,17 @@
+import { redirect } from "next/navigation";
+import { auth } from "@/auth";
+
+export default async function HomePage(
+ props: {
+ params: Promise<{ lang: string }>;
+ }
+) {
+ const params = await props.params;
+
+ const session = await auth();
+ if (session) {
+ redirect(`/${params.lang}/dashboard`);
+ } else {
+ redirect(`/${params.lang}/login`);
+ }
+}
diff --git a/app/dashboard/layout.tsx b/app/dashboard/layout.tsx
deleted file mode 100644
index 7382cf8..0000000
--- a/app/dashboard/layout.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import { auth } from "@/auth";
-import { redirect } from "next/navigation";
-import Providers from "@/components/Providers";
-import Sidebar from "@/components/Sidebar";
-
-export default async function DashboardLayout({
- children,
-}: {
- children: React.ReactNode;
-}) {
- const session = await auth();
- if (!session) redirect("/login");
-
- return (
-
-
-
- );
-}
diff --git a/app/dictionaries.ts b/app/dictionaries.ts
new file mode 100644
index 0000000..160661f
--- /dev/null
+++ b/app/dictionaries.ts
@@ -0,0 +1,10 @@
+import 'server-only'
+
+const dictionaries = {
+ en: () => import('./dictionaries/en.json').then((module) => module.default),
+ tr: () => import('./dictionaries/tr.json').then((module) => module.default),
+}
+
+export type Locale = keyof typeof dictionaries
+
+export const getDictionary = async (locale: Locale) => dictionaries[locale]()
diff --git a/app/dictionaries/en.json b/app/dictionaries/en.json
new file mode 100644
index 0000000..fd1d254
--- /dev/null
+++ b/app/dictionaries/en.json
@@ -0,0 +1,18 @@
+{
+ "login": {
+ "title": "Welcome Back",
+ "subtitle": "Please log in to AyrisMail Central",
+ "emailLabel": "Email Address",
+ "emailPlaceholder": "name@domain.com",
+ "passwordLabel": "Password",
+ "passwordPlaceholder": "Enter your password",
+ "signInButton": "Sign In",
+ "signingIn": "Signing In..."
+ },
+ "sidebar": {
+ "mailboxes": "Mailboxes",
+ "mailClient": "Mail Client",
+ "users": "User Management",
+ "logout": "Log Out"
+ }
+}
diff --git a/app/dictionaries/tr.json b/app/dictionaries/tr.json
new file mode 100644
index 0000000..0c1696b
--- /dev/null
+++ b/app/dictionaries/tr.json
@@ -0,0 +1,18 @@
+{
+ "login": {
+ "title": "Tekrar Hoş Geldiniz",
+ "subtitle": "Lütfen AyrisMail Central'a giriş yapın",
+ "emailLabel": "E-posta Adresi",
+ "emailPlaceholder": "isim@domain.com",
+ "passwordLabel": "Şifre",
+ "passwordPlaceholder": "Şifrenizi girin",
+ "signInButton": "Giriş Yap",
+ "signingIn": "Giriş yapılıyor..."
+ },
+ "sidebar": {
+ "mailboxes": "Mail Hesapları",
+ "mailClient": "Web Mail Client",
+ "users": "Kullanıcı Yönetimi",
+ "logout": "Çıkış Yap"
+ }
+}
diff --git a/app/layout.tsx b/app/layout.tsx
deleted file mode 100644
index e202888..0000000
--- a/app/layout.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import type { Metadata } from "next";
-import "./globals.css";
-
-export const metadata: Metadata = {
- title: "AyrisMail Central",
- description: "Multi-tenant Mailcow yönetim paneli — AyrisTech",
-};
-
-export default function RootLayout({
- children,
-}: Readonly<{
- children: React.ReactNode;
-}>) {
- return (
-
- {children}
-
- );
-}
diff --git a/app/page.tsx b/app/page.tsx
deleted file mode 100644
index dc602ef..0000000
--- a/app/page.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import { redirect } from "next/navigation";
-import { auth } from "@/auth";
-
-export default async function HomePage() {
- const session = await auth();
- if (session) {
- redirect("/dashboard");
- } else {
- redirect("/login");
- }
-}
diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx
index 3c8fe3f..aacbcb1 100644
--- a/components/Sidebar.tsx
+++ b/components/Sidebar.tsx
@@ -4,31 +4,31 @@ import { useSession, signOut } from "next-auth/react";
import Link from "next/link";
import { usePathname } from "next/navigation";
-const navItems = [
- {
- section: "GENEL",
- items: [
- { href: "/dashboard", label: "Dashboard", icon: HomeIcon, roles: ["SUPER_ADMIN", "DOMAIN_ADMIN"] },
- { href: "/dashboard/mail", label: "Mail", icon: InboxIcon, roles: ["SUPER_ADMIN", "DOMAIN_ADMIN"] },
- ],
- },
- {
- section: "YÖNETİM",
- items: [
- { href: "/dashboard/domains", label: "Domainler", icon: GlobeIcon, roles: ["SUPER_ADMIN"] },
- { href: "/dashboard/users", label: "Kullanıcılar", icon: UsersIcon, roles: ["SUPER_ADMIN"] },
- { href: "/dashboard/mailboxes", label: "Mail Hesapları", icon: MailIcon, roles: ["SUPER_ADMIN", "DOMAIN_ADMIN"] },
- ],
- },
-];
-
-export default function Sidebar() {
+export default function Sidebar({ dict, lang }: { dict: any; lang: string }) {
const { data: session } = useSession();
const pathname = usePathname();
const role = session?.user?.role ?? "";
const name = session?.user?.name ?? "";
const email = session?.user?.email ?? "";
+ const navItems = [
+ {
+ section: "GENEL",
+ items: [
+ { href: `/${lang}/dashboard`, label: "Dashboard", icon: HomeIcon, roles: ["SUPER_ADMIN", "DOMAIN_ADMIN"] },
+ { href: `/${lang}/dashboard/mail`, label: dict.mailClient || "Mail", icon: InboxIcon, roles: ["SUPER_ADMIN", "DOMAIN_ADMIN"] },
+ ],
+ },
+ {
+ section: "YÖNETİM",
+ items: [
+ { href: `/${lang}/dashboard/domains`, label: "Domainler", icon: GlobeIcon, roles: ["SUPER_ADMIN"] },
+ { href: `/${lang}/dashboard/users`, label: dict.users || "Kullanıcılar", icon: UsersIcon, roles: ["SUPER_ADMIN"] },
+ { href: `/${lang}/dashboard/mailboxes`, label: dict.mailboxes || "Mail Hesapları", icon: MailIcon, roles: ["SUPER_ADMIN", "DOMAIN_ADMIN"] },
+ ],
+ },
+ ];
+
return (
diff --git a/components/mail/ComposeModal.tsx b/components/mail/ComposeModal.tsx
index 0fc05dc..8657487 100644
--- a/components/mail/ComposeModal.tsx
+++ b/components/mail/ComposeModal.tsx
@@ -1,6 +1,6 @@
"use client";
import { useState, useRef, useCallback } from "react";
-import type { MailMessage } from "@/app/dashboard/mail/page";
+import type { MailMessage } from "@/app/[lang]/dashboard/mail/page";
import { formatBytes } from "@/lib/format";
interface AttachmentFile {
diff --git a/components/mail/FolderList.tsx b/components/mail/FolderList.tsx
index c954e14..8e707ad 100644
--- a/components/mail/FolderList.tsx
+++ b/components/mail/FolderList.tsx
@@ -1,5 +1,5 @@
"use client";
-import type { MailFolder } from "@/app/dashboard/mail/page";
+import type { MailFolder } from "@/app/[lang]/dashboard/mail/page";
const FOLDER_ICONS: Record = {
"\\Inbox": "📥",
diff --git a/components/mail/MessageList.tsx b/components/mail/MessageList.tsx
index bb6520e..5337c53 100644
--- a/components/mail/MessageList.tsx
+++ b/components/mail/MessageList.tsx
@@ -1,5 +1,5 @@
"use client";
-import type { MailEnvelope } from "@/app/dashboard/mail/page";
+import type { MailEnvelope } from "@/app/[lang]/dashboard/mail/page";
function timeAgo(dateStr: string): string {
const now = new Date();
diff --git a/components/mail/MessageView.tsx b/components/mail/MessageView.tsx
index 412b78b..ebeb141 100644
--- a/components/mail/MessageView.tsx
+++ b/components/mail/MessageView.tsx
@@ -1,5 +1,5 @@
"use client";
-import type { MailMessage } from "@/app/dashboard/mail/page";
+import type { MailMessage } from "@/app/[lang]/dashboard/mail/page";
import { formatBytes } from "@/lib/format";
function getFileIcon(contentType: string, filename: string): string {
diff --git a/package-lock.json b/package-lock.json
index 8fe76fc..9ae91cc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,11 +8,13 @@
"name": "mailserver",
"version": "0.1.0",
"dependencies": {
+ "@formatjs/intl-localematcher": "^0.8.7",
"@tanstack/react-query": "^5.100.10",
"bcryptjs": "^3.0.3",
"imapflow": "^1.3.3",
"lucide-react": "^1.14.0",
"mailparser": "^3.9.8",
+ "negotiator": "^1.0.0",
"next": "16.2.6",
"next-auth": "^5.0.0-beta.31",
"nodemailer": "^8.0.7",
@@ -24,6 +26,7 @@
"@tailwindcss/postcss": "^4",
"@types/bcryptjs": "^2.4.6",
"@types/mailparser": "^3.4.6",
+ "@types/negotiator": "^0.6.4",
"@types/node": "^20",
"@types/nodemailer": "^8.0.0",
"@types/react": "^19",
@@ -937,6 +940,21 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
+ "node_modules/@formatjs/fast-memoize": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.5.tgz",
+ "integrity": "sha512-KLi3fan6WnCHmigd9pmEEN8Hid0v4wiFBW576M/d07KMWYecf1CvyMI3n34vCmHT4AoVqG2n702kiHbXjzZX2A==",
+ "license": "MIT"
+ },
+ "node_modules/@formatjs/intl-localematcher": {
+ "version": "0.8.7",
+ "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.8.7.tgz",
+ "integrity": "sha512-1R/ljfRKG1fUhKG4F0lUmrEKPkr/PlHqbgQ8xeYQYYunXu5/0+vbQeeVgGAgydp13Tq+S1X5Qjn6L90hijXjHg==",
+ "license": "MIT",
+ "dependencies": {
+ "@formatjs/fast-memoize": "3.1.5"
+ }
+ },
"node_modules/@humanfs/core": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
@@ -2128,6 +2146,13 @@
"node": ">=0.10.0"
}
},
+ "node_modules/@types/negotiator": {
+ "version": "0.6.4",
+ "resolved": "https://registry.npmjs.org/@types/negotiator/-/negotiator-0.6.4.tgz",
+ "integrity": "sha512-elf6BsTq+AkyNsb2h5cGNst2Mc7dPliVoAPm1fXglC/BM3f2pFA40BaSSv3E5lyHteEawVKLP+8TwiY1DMNb3A==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/node": {
"version": "20.19.41",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz",
@@ -5944,6 +5969,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/negotiator": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/next": {
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/next/-/next-16.2.6.tgz",
diff --git a/package.json b/package.json
index d739af6..81eb51e 100644
--- a/package.json
+++ b/package.json
@@ -13,11 +13,13 @@
"seed": "tsx prisma/seed.ts"
},
"dependencies": {
+ "@formatjs/intl-localematcher": "^0.8.7",
"@tanstack/react-query": "^5.100.10",
"bcryptjs": "^3.0.3",
"imapflow": "^1.3.3",
"lucide-react": "^1.14.0",
"mailparser": "^3.9.8",
+ "negotiator": "^1.0.0",
"next": "16.2.6",
"next-auth": "^5.0.0-beta.31",
"nodemailer": "^8.0.7",
@@ -29,6 +31,7 @@
"@tailwindcss/postcss": "^4",
"@types/bcryptjs": "^2.4.6",
"@types/mailparser": "^3.4.6",
+ "@types/negotiator": "^0.6.4",
"@types/node": "^20",
"@types/nodemailer": "^8.0.0",
"@types/react": "^19",
diff --git a/proxy.ts b/proxy.ts
index 2034e40..d3c3c08 100644
--- a/proxy.ts
+++ b/proxy.ts
@@ -1,22 +1,52 @@
import { NextResponse } from "next/server";
import { auth } from "./auth";
+import { match } from "@formatjs/intl-localematcher";
+import Negotiator from "negotiator";
+
+const locales = ["tr", "en"];
+const defaultLocale = "tr";
+
+function getLocale(request: Request): string {
+ const headers = { "accept-language": request.headers.get("accept-language") || "" };
+ const languages = new Negotiator({ headers }).languages();
+ try {
+ return match(languages, locales, defaultLocale);
+ } catch {
+ return defaultLocale;
+ }
+}
export default auth((req) => {
const { nextUrl } = req;
const isLoggedIn = !!req.auth;
- const isLoginPage = nextUrl.pathname === "/login";
+ const pathname = nextUrl.pathname;
+
+ // Check if there is any supported locale in the pathname
+ const pathnameIsMissingLocale = locales.every(
+ (locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`
+ );
- // Proxy arkasında çalışırken doğru adresi alabilmek için
const host = req.headers.get("x-forwarded-host") || req.headers.get("host") || "localhost:3000";
const proto = req.headers.get("x-forwarded-proto") || "http";
const baseUrl = `${proto}://${host}`;
+ // Redirect if there is no locale
+ if (pathnameIsMissingLocale) {
+ const locale = getLocale(req as any);
+ return NextResponse.redirect(
+ new URL(`/${locale}${pathname === '/' ? '' : pathname}`, baseUrl)
+ );
+ }
+
+ const currentLocale = pathname.split('/')[1] || defaultLocale;
+ const isLoginPage = pathname === `/${currentLocale}/login`;
+
if (!isLoggedIn && !isLoginPage) {
- return NextResponse.redirect(new URL("/login", baseUrl));
+ return NextResponse.redirect(new URL(`/${currentLocale}/login`, baseUrl));
}
if (isLoggedIn && isLoginPage) {
- return NextResponse.redirect(new URL("/dashboard", baseUrl));
+ return NextResponse.redirect(new URL(`/${currentLocale}/dashboard`, baseUrl));
}
return NextResponse.next();