- Mood ve enerji
+ {t("journal.title")}
@@ -436,7 +439,7 @@ function average(values: number[]) {
}
function formatDate(value: string) {
- return new Intl.DateTimeFormat("tr-TR", {
+ return new Intl.DateTimeFormat(getDocumentIntlLocale(), {
day: "2-digit",
month: "short",
year: "numeric",
@@ -444,14 +447,14 @@ function formatDate(value: string) {
}
function formatShortDate(value: string) {
- return new Intl.DateTimeFormat("tr-TR", {
+ return new Intl.DateTimeFormat(getDocumentIntlLocale(), {
day: "2-digit",
month: "short",
}).format(new Date(`${value}T00:00:00`));
}
function formatWeekday(value: string) {
- return new Intl.DateTimeFormat("tr-TR", {
+ return new Intl.DateTimeFormat(getDocumentIntlLocale(), {
weekday: "long",
}).format(new Date(`${value}T00:00:00`));
}
diff --git a/app/(dashboard)/layout.tsx b/app/(dashboard)/layout.tsx
index 59b7cd6..7584b22 100644
--- a/app/(dashboard)/layout.tsx
+++ b/app/(dashboard)/layout.tsx
@@ -2,6 +2,8 @@ import { DashboardShell } from "@/components/layout/dashboard-shell";
import { domainActorFromSession } from "@/server/auth/domain-actor";
import { requireFreelancer } from "@/server/auth/session";
import { getPublicBranding } from "@/server/branding/runtime";
+import { resolveRequestLocale } from "@/server/i18n/resolver";
+import { createTranslator, getClientI18nPayload } from "@/server/i18n/translator";
import { getUserPreferences } from "@/server/settings/preferences";
export default async function DashboardLayout({
@@ -13,6 +15,23 @@ export default async function DashboardLayout({
const { user, profile } = context;
const branding = getPublicBranding();
const preferences = getUserPreferences(domainActorFromSession(context));
+ const resolvedLocale = await resolveRequestLocale();
+ const translator = createTranslator(resolvedLocale.locale, [
+ "common",
+ "navigation",
+ "dashboard",
+ "clients",
+ "projects",
+ "tasks",
+ "calendar",
+ "finance",
+ "journal",
+ "chat",
+ "settings",
+ "status",
+ "validation",
+ ]);
+ const t = translator.t;
const displayName = profile.displayName || user.name || user.email.split("@")[0] || "Neta Kullanıcısı";
const shortName =
@@ -33,6 +52,36 @@ export default async function DashboardLayout({
darkLogoUrl: branding.darkLogoUrl,
}}
colorMode={preferences.colorMode}
+ i18n={getClientI18nPayload(resolvedLocale.locale, [
+ "common",
+ "navigation",
+ "dashboard",
+ "clients",
+ "projects",
+ "tasks",
+ "calendar",
+ "finance",
+ "journal",
+ "chat",
+ "settings",
+ "status",
+ "validation",
+ ])}
+ labels={{
+ skipToContent: t("navigation.shell.skipToContent"),
+ homeAriaLabel: t("navigation.shell.homeAriaLabel", { app: branding.applicationName }),
+ mobileMenuAriaLabel: t("navigation.shell.mobileMenuAriaLabel"),
+ mobileMenuTooltip: t("navigation.shell.mobileMenuTooltip"),
+ logoAlt: t("navigation.shell.logoAlt", { app: branding.applicationName }),
+ progressTitle: t("navigation.shell.progressTitle"),
+ progressValue: t("navigation.shell.progressValue"),
+ progressAriaLabel: t("navigation.shell.progressAriaLabel"),
+ accountMenuAriaLabel: t("navigation.shell.accountMenuAriaLabel"),
+ settings: t("navigation.items.settings"),
+ signOut: t("navigation.account.signOut"),
+ signingOut: t("navigation.account.signingOut"),
+ signOutError: t("navigation.account.signOutError"),
+ }}
user={{
email: user.email,
displayName,
diff --git a/app/layout.tsx b/app/layout.tsx
index df4e72e..84efb89 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -9,6 +9,7 @@ import {
} from "@/lib/color-mode";
import { Toaster } from "poyraz-ui/molecules";
import { getPublicBranding } from "@/server/branding/runtime";
+import { resolveRequestLocale } from "@/server/i18n/resolver";
const colorModeScript = `(() => {
const root = document.documentElement;
@@ -52,10 +53,12 @@ export default async function RootLayout({
const colorMode = isColorMode(cookieColorMode)
? cookieColorMode
: branding.defaultColorMode;
+ const locale = await resolveRequestLocale();
return (
@@ -100,7 +118,7 @@ export function AppShell({
href="#main-content"
className="sr-only focus:not-sr-only focus:fixed focus:left-4 focus:top-4 focus:z-[90] focus:rounded-md focus:bg-surface focus:px-4 focus:py-2 focus:text-sm focus:font-medium focus:shadow-lg focus:ring-2 focus:ring-focus-ring"
>
- Ana içeriğe geç
+ {labels.skipToContent}
@@ -118,9 +136,26 @@ export function AppShell({
);
}
+const defaultAppShellLabels: AppShellLabels = {
+ skipToContent: "Ana içeriğe geç",
+ homeAriaLabel: "Ana sayfa",
+ mobileMenuAriaLabel: "Ana menüyü aç veya kapat",
+ mobileMenuTooltip: "Menü",
+ logoAlt: "Logo",
+ progressTitle: "Proje ilerlemesi",
+ progressValue: "%{progress} tamamlandı",
+ progressAriaLabel: "Proje ilerlemesi",
+ accountMenuAriaLabel: "{name} için hesap menüsünü aç",
+ settings: "Ayarlar",
+ signOut: "Çıkış yap",
+ signingOut: "Çıkış yapılıyor",
+ signOutError: "Çıkış yapılamadı. Lütfen tekrar deneyin.",
+};
+
type SidebarCompositionProps = {
branding: AppShellBranding;
homeHref: string;
+ labels: AppShellLabels;
navGroups: AppShellNavGroup[];
pathname: string;
progress?: number;
@@ -145,15 +180,15 @@ function MobileSidebar(props: SidebarCompositionProps) {
-
+
- Menü
+ {props.labels.mobileMenuTooltip}
@@ -167,6 +202,7 @@ function MobileSidebar(props: SidebarCompositionProps) {
function SidebarComposition({
branding,
homeHref,
+ labels,
navGroups,
pathname,
progress,
@@ -179,7 +215,7 @@ function SidebarComposition({
@@ -194,8 +230,8 @@ function SidebarComposition({
- {typeof progress === "number" ? : null}
-
+ {typeof progress === "number" ? : null}
+
>
);
@@ -257,7 +293,7 @@ function WorkspaceLogo({
- Proje ilerlemesi
+ {labels.progressTitle}
- %{normalizedProgress} tamamlandı
+ {progressValue}
- Ayarlar
+ {labels.settings}
@@ -385,7 +422,7 @@ function AccountMenu({ user, settingsHref }: { user: ShellUser; settingsHref: st
className="w-full justify-start gap-2 text-left text-destructive"
>
- {isSigningOut ? "Çıkış yapılıyor" : "Çıkış yap"}
+ {isSigningOut ? labels.signingOut : labels.signOut}
diff --git a/components/layout/dashboard-shell.tsx b/components/layout/dashboard-shell.tsx
index 15d95f3..e9b87d6 100644
--- a/components/layout/dashboard-shell.tsx
+++ b/components/layout/dashboard-shell.tsx
@@ -1,13 +1,20 @@
"use client";
-import { AppShell, type AppShellBranding } from "@/components/layout/app-shell";
-import { sidebarData } from "@/config/sidebar";
+import { I18nProvider } from "@/components/i18n/i18n-provider";
+import { AppShell, type AppShellBranding, type AppShellLabels } from "@/components/layout/app-shell";
+import { localizeSidebarData } from "@/config/sidebar";
import type { ColorMode } from "@/lib/color-mode";
+import { createTranslatorFromMessages } from "@/lib/i18n";
type DashboardShellProps = {
branding: AppShellBranding;
children: React.ReactNode;
colorMode: ColorMode;
+ i18n: {
+ locale: string;
+ messages: Record;
+ };
+ labels: AppShellLabels;
user: {
email: string;
displayName: string;
@@ -16,17 +23,23 @@ type DashboardShellProps = {
};
};
-export function DashboardShell({ branding, children, colorMode, user }: DashboardShellProps) {
+export function DashboardShell({ branding, children, colorMode, i18n, labels, user }: DashboardShellProps) {
+ const translator = createTranslatorFromMessages(i18n.locale, i18n.messages);
+ const navGroups = localizeSidebarData(translator.t);
+
return (
-
- {children}
-
+
+
+ {children}
+
+
);
}
diff --git a/config/sidebar.ts b/config/sidebar.ts
index 0d4fa9c..5d49c10 100644
--- a/config/sidebar.ts
+++ b/config/sidebar.ts
@@ -13,6 +13,7 @@ import {
export type SidebarNavItem = {
title: string;
+ titleKey: string;
href?: string;
icon?: LucideIcon;
items?: SidebarNavItem[];
@@ -20,33 +21,49 @@ export type SidebarNavItem = {
export type SidebarNavGroup = {
title: string;
+ titleKey: string;
items: SidebarNavItem[];
};
export const sidebarData: SidebarNavGroup[] = [
{
title: "GENEL BAKIŞ",
+ titleKey: "navigation.groups.overview",
items: [
- { title: "Dashboard", href: "/", icon: Sparkles },
- { title: "Takvim", href: "/calendar", icon: Calendar },
- { title: "Analizler", href: "/analytics", icon: BarChart3 },
+ { title: "Dashboard", titleKey: "navigation.items.dashboard", href: "/", icon: Sparkles },
+ { title: "Takvim", titleKey: "navigation.items.calendar", href: "/calendar", icon: Calendar },
+ { title: "Analizler", titleKey: "navigation.items.analytics", href: "/analytics", icon: BarChart3 },
],
},
{
title: "OPERASYON",
+ titleKey: "navigation.groups.operations",
items: [
- { title: "Müşteriler", href: "/clients", icon: Building2 },
- { title: "Projeler", href: "/projects", icon: FolderKanban },
- { title: "Görevler", href: "/tasks", icon: CheckSquare2 },
- { title: "Finans", href: "/finance", icon: Wallet },
+ { title: "Müşteriler", titleKey: "navigation.items.clients", href: "/clients", icon: Building2 },
+ { title: "Projeler", titleKey: "navigation.items.projects", href: "/projects", icon: FolderKanban },
+ { title: "Görevler", titleKey: "navigation.items.tasks", href: "/tasks", icon: CheckSquare2 },
+ { title: "Finans", titleKey: "navigation.items.finance", href: "/finance", icon: Wallet },
],
},
{
title: "KİŞİSEL",
- items: [{ title: "Günlük", href: "/journal", icon: BookOpenText }],
+ titleKey: "navigation.groups.personal",
+ items: [{ title: "Günlük", titleKey: "navigation.items.journal", href: "/journal", icon: BookOpenText }],
},
{
title: "AI ASİSTAN",
- items: [{ title: "Sohbet", href: "/chat", icon: MessageCircleHeart }],
+ titleKey: "navigation.groups.ai",
+ items: [{ title: "Sohbet", titleKey: "navigation.items.chat", href: "/chat", icon: MessageCircleHeart }],
},
];
+
+export function localizeSidebarData(t: (key: string) => string): SidebarNavGroup[] {
+ return sidebarData.map((group) => ({
+ ...group,
+ title: t(group.titleKey),
+ items: group.items.map((item) => ({
+ ...item,
+ title: t(item.titleKey),
+ })),
+ }));
+}
diff --git a/scripts/i18n-phase4-boundary.mjs b/scripts/i18n-phase4-boundary.mjs
new file mode 100644
index 0000000..196e85a
--- /dev/null
+++ b/scripts/i18n-phase4-boundary.mjs
@@ -0,0 +1,93 @@
+import assert from "node:assert/strict";
+import fs from "node:fs";
+import path from "node:path";
+
+const root = process.cwd();
+const checkedRoots = [
+ "app/(dashboard)",
+ "components/layout",
+ "components/system",
+ "config/sidebar.ts",
+];
+
+const files = checkedRoots.flatMap((entry) => {
+ const absolute = path.join(root, entry);
+ if (fs.statSync(absolute).isFile()) return [absolute];
+ return walk(absolute).filter((file) => /\.(ts|tsx)$/.test(file));
+});
+
+const forbiddenFormatterPattern = /"tr-TR"|'tr-TR'|from "date-fns\/locale"|from 'date-fns\/locale'|locale:\s*tr\b/;
+const formatterViolations = files
+ .map((file) => ({
+ file,
+ lines: fs.readFileSync(file, "utf8")
+ .split("\n")
+ .map((line, index) => ({ line, number: index + 1 }))
+ .filter(({ line }) => forbiddenFormatterPattern.test(line)),
+ }))
+ .filter((entry) => entry.lines.length > 0);
+
+assert.deepEqual(
+ formatterViolations,
+ [],
+ `Hardcoded Turkish formatter usage remains:\n${formatterViolations.map(formatViolation).join("\n")}`,
+);
+
+const dashboardLayout = fs.readFileSync(path.join(root, "app/(dashboard)/layout.tsx"), "utf8");
+const dashboardShell = fs.readFileSync(path.join(root, "components/layout/dashboard-shell.tsx"), "utf8");
+assert.match(dashboardLayout, /getClientI18nPayload/, "Dashboard layout must provide client i18n payload.");
+assert.match(dashboardShell, /localizeSidebarData/, "Dashboard shell must localize sidebar with client-safe icons.");
+
+const dashboardClient = fs.readFileSync(path.join(root, "app/(dashboard)/dashboard-client.tsx"), "utf8");
+assert.match(dashboardClient, /useTranslations/, "Dashboard page must consume translations.");
+assert.doesNotMatch(dashboardClient, />Dashboard, "Dashboard title must not be hardcoded.");
+
+const turkishLiteralPattern = /["'`][^"'`\n]*(?:ğ|ü|ş|ö|ç|ı|İ|Ğ|Ü|Ş|Ö|Ç)[^"'`\n]*["'`]/;
+const remainingTurkishLiterals = files
+ .flatMap((file) =>
+ fs.readFileSync(file, "utf8")
+ .split("\n")
+ .map((line, index) => ({ file, line, number: index + 1 }))
+ .filter(({ line }) => turkishLiteralPattern.test(line)),
+ );
+
+const reportPath = path.join(root, "docs", "self-hosted-redesign", "i18n-phase-4-hardcoded-text-report.md");
+fs.writeFileSync(
+ reportPath,
+ [
+ "---",
+ "title: Faz 4 Kalan Hardcoded Metin Raporu",
+ "phase: 4",
+ "status: generated",
+ `last_updated: ${new Date().toISOString()}`,
+ "---",
+ "",
+ "# Faz 4 Kalan Hardcoded Metin Raporu",
+ "",
+ "Bu rapor Faz-4 boundary script'i tarafindan uretilir. Formatter sabitleri release blocker kabul edilir; kalan Turkce stringler Faz-4 kapsaminda raporlanir ve sonraki UI migration dalgalarinda eritilir.",
+ "",
+ `Toplam kalan Turkce literal satiri: ${remainingTurkishLiterals.length}`,
+ "",
+ ...remainingTurkishLiterals.slice(0, 250).map(({ file, line, number }) => (
+ `- \`${path.relative(root, file)}:${number}\` ${line.trim()}`
+ )),
+ remainingTurkishLiterals.length > 250 ? "" : null,
+ remainingTurkishLiterals.length > 250 ? `Ilk 250 satir listelendi; kalan: ${remainingTurkishLiterals.length - 250}` : null,
+ "",
+ ].filter((line) => line !== null).join("\n"),
+);
+
+console.log(`I18n phase 4 boundary passed. Hardcoded text report: ${path.relative(root, reportPath)}`);
+
+function walk(dir) {
+ return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
+ const entryPath = path.join(dir, entry.name);
+ if (entry.isDirectory()) return walk(entryPath);
+ if (entry.isFile()) return [entryPath];
+ return [];
+ });
+}
+
+function formatViolation(entry) {
+ return `${path.relative(root, entry.file)}\n${entry.lines.map(({ line, number }) => ` ${number}: ${line}`).join("\n")}`;
+}