feat: localize dashboard shell

This commit is contained in:
poyrazavsever
2026-07-19 03:07:10 +03:00
parent 5d3e720280
commit c7a4f3de51
11 changed files with 303 additions and 77 deletions
+8 -5
View File
@@ -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
<div className="mx-auto flex max-w-7xl flex-col gap-6">
<div className="flex flex-col gap-4 border-b border-border pb-5 lg:flex-row lg:items-end lg:justify-between">
<div>
<h1 className="text-3xl font-semibold tracking-normal text-foreground">Takvim</h1>
<h1 className="text-3xl font-semibold tracking-normal text-foreground">{t("calendar.title")}</h1>
</div>
<CalendarEventDialog
@@ -442,16 +445,16 @@ function startOfToday() {
}
function formatMonth(date: Date) {
return new Intl.DateTimeFormat("tr-TR", { month: "long", year: "numeric" }).format(date);
return new Intl.DateTimeFormat(getDocumentIntlLocale(), { month: "long", year: "numeric" }).format(date);
}
function formatDateLabel(dateKey: string) {
return new Intl.DateTimeFormat("tr-TR", { day: "2-digit", month: "long", year: "numeric" }).format(new Date(`${dateKey}T00:00:00`));
return new Intl.DateTimeFormat(getDocumentIntlLocale(), { day: "2-digit", month: "long", year: "numeric" }).format(new Date(`${dateKey}T00:00:00`));
}
function formatTimeRange(event: CalendarEventItem) {
const start = new Intl.DateTimeFormat("tr-TR", { hour: "2-digit", minute: "2-digit" }).format(new Date(event.starts_at));
const end = event.ends_at ? new Intl.DateTimeFormat("tr-TR", { hour: "2-digit", minute: "2-digit" }).format(new Date(event.ends_at)) : null;
const start = new Intl.DateTimeFormat(getDocumentIntlLocale(), { hour: "2-digit", minute: "2-digit" }).format(new Date(event.starts_at));
const end = event.ends_at ? new Intl.DateTimeFormat(getDocumentIntlLocale(), { hour: "2-digit", minute: "2-digit" }).format(new Date(event.ends_at)) : null;
return end ? `${start} - ${end}` : start;
}
+3 -1
View File
@@ -1,6 +1,7 @@
"use client";
import { useChat } from "@ai-sdk/react";
import { useTranslations } from "@/components/i18n/i18n-provider";
import { DefaultChatTransport, type UIMessage } from "ai";
import { Brain, Loader2, MessageSquare, Plus, Send, Trash2 } from "lucide-react";
import { Button } from "poyraz-ui/atoms";
@@ -43,6 +44,7 @@ type ChatSession = {
};
export default function AIChatPage() {
const t = useTranslations();
const [sessions, setSessions] = useState<ChatSession[]>([]);
const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
const [input, setInput] = useState("");
@@ -235,7 +237,7 @@ export default function AIChatPage() {
<Brain className="h-4 w-4" />
</div>
<div>
<h1 className="text-sm font-semibold text-foreground">AI Asistan</h1>
<h1 className="text-sm font-semibold text-foreground">{t("chat.title")}</h1>
</div>
</div>
<Button effect="shine" variant="secondary" size="sm" className="md:hidden text-xs px-3" onClick={() => setIsMobileSessionsOpen(true)}>
+25 -22
View File
@@ -1,5 +1,7 @@
"use client";
import { getDocumentIntlLocale } from "@/lib/i18n/browser";
import { useTranslations } from "@/components/i18n/i18n-provider";
import { useRouter, usePathname, useSearchParams } from "next/navigation";
import { PendingLink } from "@/components/ui/pending-link";
import { StatCard } from "@/components/system/stat-card";
@@ -27,6 +29,7 @@ type DashboardClientProps = {
};
export function DashboardClient({ data }: DashboardClientProps) {
const t = useTranslations();
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
@@ -41,20 +44,20 @@ export function DashboardClient({ data }: DashboardClientProps) {
// Format dates for Recharts using local timezone
const incomeTrendData = (financeTrend || []).map(f => ({
name: new Date(f.date).toLocaleDateString("tr-TR", { month: "short", day: "numeric" }),
name: new Date(f.date).toLocaleDateString(getDocumentIntlLocale(), { month: "short", day: "numeric" }),
income: f.income,
expense: f.expense
}));
const moodTrendData = (moodTrend || []).map(l => ({
date: new Date(l.date).toLocaleDateString("tr-TR", { month: "short", day: "numeric" }),
date: new Date(l.date).toLocaleDateString(getDocumentIntlLocale(), { month: "short", day: "numeric" }),
mood: l.mood,
energy: l.energy,
}));
// Format currency
const formatCurrency = (val: number) => {
return new Intl.NumberFormat("tr-TR", {
return new Intl.NumberFormat(getDocumentIntlLocale(), {
style: "currency",
currency: "USD",
maximumFractionDigits: 0,
@@ -67,19 +70,19 @@ export function DashboardClient({ data }: DashboardClientProps) {
<div className="flex flex-col gap-4 border-b border-border pb-5 lg:flex-row lg:items-end lg:justify-between">
<div>
<h1 className="text-3xl font-semibold tracking-normal text-foreground">
Dashboard
{t("dashboard.title")}
</h1>
</div>
<div className="flex items-center gap-3">
<Select value={data.range} onValueChange={handleRangeChange}>
<SelectTrigger className="w-[160px]">
<SelectValue placeholder="Tarih aralığı" />
<SelectValue placeholder={t("dashboard.filters.range")} />
</SelectTrigger>
<SelectContent>
<SelectItem value="today">Bugün</SelectItem>
<SelectItem value="this_week">Bu Hafta</SelectItem>
<SelectItem value="this_month">Bu Ay</SelectItem>
<SelectItem value="today">{t("dashboard.filters.today")}</SelectItem>
<SelectItem value="this_week">{t("dashboard.filters.thisWeek")}</SelectItem>
<SelectItem value="this_month">{t("dashboard.filters.thisMonth")}</SelectItem>
</SelectContent>
</Select>
@@ -88,17 +91,17 @@ export function DashboardClient({ data }: DashboardClientProps) {
{/* KPI Cards */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<StatCard label="Net Kazanç" value={formatCurrency(netProfit)} icon={Wallet} tone="green" />
<StatCard label="Aktif Projeler" value={activeProjectsCount.toString()} icon={FolderKanban} tone="blue" />
<StatCard label="Tamamlanan Görev" value={completedTasksCount.toString()} icon={CheckCircle2} tone="amber" />
<StatCard label="Ortalama Mood" value={avgMood} icon={Activity} tone="red" />
<StatCard label={t("dashboard.stats.netEarnings")} value={formatCurrency(netProfit)} icon={Wallet} tone="green" />
<StatCard label={t("dashboard.stats.activeProjects")} value={activeProjectsCount.toString()} icon={FolderKanban} tone="blue" />
<StatCard label={t("dashboard.stats.completedTasks")} value={completedTasksCount.toString()} icon={CheckCircle2} tone="amber" />
<StatCard label={t("dashboard.stats.averageMood")} value={avgMood} icon={Activity} tone="red" />
</div>
{/* Charts */}
<div className="grid gap-6 lg:grid-cols-2">
<Card>
<CardContent className="p-6">
<h3 className="mb-6 text-sm font-semibold text-foreground">Gelir / Gider Özeti</h3>
<h3 className="mb-6 text-sm font-semibold text-foreground">{t("dashboard.sections.financeSummary")}</h3>
<div className="h-[300px] w-full">
{incomeTrendData.length > 0 ? (
<ResponsiveContainer width="100%" height="100%">
@@ -160,7 +163,7 @@ export function DashboardClient({ data }: DashboardClientProps) {
</BarChart>
</ResponsiveContainer>
) : (
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">Bu tarih aralığında finansal veri yok.</div>
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">{t("dashboard.empty.finance")}</div>
)}
</div>
</CardContent>
@@ -168,7 +171,7 @@ export function DashboardClient({ data }: DashboardClientProps) {
<Card>
<CardContent className="p-6">
<h3 className="mb-6 text-sm font-semibold text-foreground">Mood & Enerji Trendi</h3>
<h3 className="mb-6 text-sm font-semibold text-foreground">{t("dashboard.sections.moodTrend")}</h3>
<div className="h-[300px] w-full">
{moodTrendData.length > 0 ? (
<ResponsiveContainer width="100%" height="100%">
@@ -216,7 +219,7 @@ export function DashboardClient({ data }: DashboardClientProps) {
</LineChart>
</ResponsiveContainer>
) : (
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">Bu tarih aralığında günlük verisi yok.</div>
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">{t("dashboard.empty.journal")}</div>
)}
</div>
</CardContent>
@@ -229,7 +232,7 @@ export function DashboardClient({ data }: DashboardClientProps) {
<Card>
<CardContent className="p-6">
<div className="mb-4 flex items-center justify-between">
<h3 className="text-sm font-semibold text-foreground">Son Eklenen Projeler</h3>
<h3 className="text-sm font-semibold text-foreground">{t("dashboard.sections.recentProjects")}</h3>
<FolderKanban className="h-4 w-4 text-muted-foreground" />
</div>
<div className="space-y-4">
@@ -240,7 +243,7 @@ export function DashboardClient({ data }: DashboardClientProps) {
<div>
<p className="text-sm font-medium">{project.name}</p>
<p className="text-xs text-muted-foreground">
{new Date(project.created_at).toLocaleDateString("tr-TR", { month: "short", day: "numeric" })}
{new Date(project.created_at).toLocaleDateString(getDocumentIntlLocale(), { month: "short", day: "numeric" })}
</p>
</div>
<Badge variant={project.status === 'completed' ? 'secondary' : 'default'} className="capitalize text-[10px] px-1.5 py-0">
@@ -250,7 +253,7 @@ export function DashboardClient({ data }: DashboardClientProps) {
</PendingLink>
))}
{data.projects.length === 0 && (
<div className="text-sm text-muted-foreground py-4 text-center border border-dashed rounded-sm border-border bg-muted/20">Henüz proje yok.</div>
<div className="text-sm text-muted-foreground py-4 text-center border border-dashed rounded-sm border-border bg-muted/20">{t("dashboard.empty.projects")}</div>
)}
</div>
</CardContent>
@@ -260,7 +263,7 @@ export function DashboardClient({ data }: DashboardClientProps) {
<Card>
<CardContent className="p-6">
<div className="mb-4 flex items-center justify-between">
<h3 className="text-sm font-semibold text-foreground">Son Eklenen Müşteriler</h3>
<h3 className="text-sm font-semibold text-foreground">{t("dashboard.sections.recentClients")}</h3>
<Users className="h-4 w-4 text-muted-foreground" />
</div>
<div className="space-y-4">
@@ -274,11 +277,11 @@ export function DashboardClient({ data }: DashboardClientProps) {
<p className="text-xs text-muted-foreground">{client.company_name || "Bireysel"}</p>
</div>
<div className="text-xs text-muted-foreground">
{new Date(client.created_at).toLocaleDateString("tr-TR", { month: "short", day: "numeric" })}
{new Date(client.created_at).toLocaleDateString(getDocumentIntlLocale(), { month: "short", day: "numeric" })}
</div>
</PendingLink>
)) : (
<div className="text-sm text-muted-foreground py-4 text-center border border-dashed rounded-sm border-border bg-muted/20">Henüz müşteri yok.</div>
<div className="text-sm text-muted-foreground py-4 text-center border border-dashed rounded-sm border-border bg-muted/20">{t("dashboard.empty.clients")}</div>
)}
</div>
</CardContent>
+6 -3
View File
@@ -1,5 +1,7 @@
"use client";
import { getDocumentIntlLocale } from "@/lib/i18n/browser";
import { useTranslations } from "@/components/i18n/i18n-provider";
import {
createFinanceTransactionRecord,
deleteFinanceTransactionRecord,
@@ -103,6 +105,7 @@ type FinanceClientProps = {
};
export function FinanceClient({ transactions, clients, projects }: FinanceClientProps) {
const t = useTranslations();
const [query, setQuery] = useState("");
const [monthFilter, setMonthFilter] = useState(() => new Date().toISOString().slice(0, 7));
const summaryTrackRef = useRef<HTMLDivElement>(null);
@@ -146,7 +149,7 @@ export function FinanceClient({ transactions, clients, projects }: FinanceClient
<div className="flex flex-col gap-4 border-b border-border pb-5 lg:flex-row lg:items-end lg:justify-between">
<div>
<h1 className="text-3xl font-semibold tracking-normal text-foreground">
Finans işlemleri
{t("finance.title")}
</h1>
</div>
<div className="flex gap-2">
@@ -730,7 +733,7 @@ function calculateExpenseCategories(transactions: FinanceTransactionItem[]) {
}
function formatCurrency(value: number, currency = "USD") {
return new Intl.NumberFormat("tr-TR", {
return new Intl.NumberFormat(getDocumentIntlLocale(), {
style: "currency",
currency,
maximumFractionDigits: 0,
@@ -738,7 +741,7 @@ function formatCurrency(value: number, currency = "USD") {
}
function formatDate(value: string) {
return new Intl.DateTimeFormat("tr-TR", {
return new Intl.DateTimeFormat(getDocumentIntlLocale(), {
day: "2-digit",
month: "short",
year: "numeric",
+7 -4
View File
@@ -1,5 +1,7 @@
"use client";
import { getDocumentIntlLocale } from "@/lib/i18n/browser";
import { useTranslations } from "@/components/i18n/i18n-provider";
import {
createDailyLogRecord,
deleteDailyLogRecord,
@@ -60,6 +62,7 @@ const scoreLabels: Record<number, string> = {
};
export function JournalClient({ logs }: JournalClientProps) {
const t = useTranslations();
const summary = useMemo(() => calculateSummary(logs), [logs]);
const chartData = useMemo(
() =>
@@ -79,7 +82,7 @@ export function JournalClient({ logs }: JournalClientProps) {
<div className="flex flex-col gap-4 border-b border-border pb-5 lg:flex-row lg:items-end lg:justify-between">
<div>
<h1 className="text-3xl font-semibold tracking-normal text-foreground">
Mood ve enerji
{t("journal.title")}
</h1>
</div>
@@ -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`));
}
+49
View File
@@ -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,