Add i18n support with Next.js App Router and Dictionaries

This commit is contained in:
AyrisAI
2026-05-14 12:56:43 +03:00
parent 89d74ce3fe
commit 4c9a07e3ef
24 changed files with 244 additions and 91 deletions

View File

@@ -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 (
<Providers>
<div className="app-layout">
<Sidebar dict={dict.sidebar} lang={params.lang} />
<div className="main-content">{children}</div>
</div>
</Providers>
);
}

30
app/[lang]/layout.tsx Normal file
View File

@@ -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 (
<html lang={params.lang}>
<body>{children}</body>
</html>
);
}

View File

@@ -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() {
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
</svg>
</div>
<h1 className="login-title">AyrisMail Central</h1>
<p className="login-sub">Mail sunucunuzu kolayca yönetin</p>
<h1 className="login-title">{dict.title || "AyrisMail Central"}</h1>
<p className="login-sub">{dict.subtitle || "Mail sunucunuzu kolayca yönetin"}</p>
</div>
<div className="card">
<form onSubmit={handleSubmit} className="form-group">
{error && <div className="error-msg">{error}</div>}
<div>
<label htmlFor="email" className="label">E-posta</label>
<label htmlFor="email" className="label">{dict.emailLabel || "E-posta"}</label>
<input
id="email"
type="email"
className="input"
placeholder="admin@ayristech.com"
placeholder={dict.emailPlaceholder || "admin@ayristech.com"}
value={email}
onChange={(e) => setEmail(e.target.value)}
required
@@ -62,12 +62,12 @@ export default function LoginPage() {
/>
</div>
<div>
<label htmlFor="password" className="label">Şifre</label>
<label htmlFor="password" className="label">{dict.passwordLabel || "Şifre"}</label>
<input
id="password"
type="password"
className="input"
placeholder="••••••••"
placeholder={dict.passwordPlaceholder || "••••••••"}
value={password}
onChange={(e) => setPassword(e.target.value)}
required
@@ -76,7 +76,7 @@ export default function LoginPage() {
</div>
<button type="submit" className="btn btn-primary" disabled={isPending} style={{ width: "100%", justifyContent: "center", padding: "10px" }}>
{isPending ? <span className="spinner" /> : null}
{isPending ? "Giriş yapılıyor..." : "Giriş Yap"}
{isPending ? (dict.signingIn || "Giriş yapılıyor...") : (dict.signInButton || "Giriş Yap")}
</button>
</form>
</div>

13
app/[lang]/login/page.tsx Normal file
View File

@@ -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 <LoginForm dict={dict.login} lang={params.lang} />;
}

17
app/[lang]/page.tsx Normal file
View File

@@ -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`);
}
}

View File

@@ -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 (
<Providers>
<div className="app-layout">
<Sidebar />
<div className="main-content">{children}</div>
</div>
</Providers>
);
}

10
app/dictionaries.ts Normal file
View File

@@ -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]()

18
app/dictionaries/en.json Normal file
View File

@@ -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"
}
}

18
app/dictionaries/tr.json Normal file
View File

@@ -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"
}
}

View File

@@ -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 (
<html lang="tr">
<body>{children}</body>
</html>
);
}

View File

@@ -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");
}
}