diff --git a/app/(dashboard)/calendar/calendar-client.tsx b/app/(dashboard)/calendar/calendar-client.tsx index a2bab19..0118868 100644 --- a/app/(dashboard)/calendar/calendar-client.tsx +++ b/app/(dashboard)/calendar/calendar-client.tsx @@ -1,5 +1,7 @@ "use client"; +import { getDocumentIntlLocale } from "@/lib/i18n/browser"; +import { useTranslations } from "@/components/i18n/i18n-provider"; import { createCalendarEventRecord, deleteCalendarEventRecord, @@ -73,6 +75,7 @@ type CalendarClientProps = { }; export function CalendarClient({ events, clients, projects, tasks }: CalendarClientProps) { + const t = useTranslations(); const [monthDate, setMonthDate] = useState(() => new Date()); const [selectedDate, setSelectedDate] = useState(() => toDateKey(new Date())); const days = useMemo(() => buildMonthDays(monthDate), [monthDate]); @@ -90,7 +93,7 @@ export function CalendarClient({ events, clients, projects, tasks }: CalendarCli
-

Takvim

+

{t("calendar.title")}

([]); const [activeSessionId, setActiveSessionId] = useState(null); const [input, setInput] = useState(""); @@ -235,7 +237,7 @@ export default function AIChatPage() {
-

AI Asistan

+

{t("chat.title")}

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