From 8b3de350c79375a218ec4a5f5f3399da46fb7f03 Mon Sep 17 00:00:00 2001 From: poyrazavsever Date: Tue, 21 Jul 2026 12:48:59 +0300 Subject: [PATCH] feat(i18n): localize core dashboard flows --- app/(dashboard)/analytics/page.tsx | 2 +- app/(dashboard)/calendar/calendar-client.tsx | 16 +- app/(dashboard)/calendar/page.tsx | 4 +- app/(dashboard)/clients/[id]/actions.ts | 2 +- .../clients/[id]/client-detail-client.tsx | 63 ++-- app/(dashboard)/clients/[id]/page.tsx | 10 +- app/(dashboard)/clients/clients-client.tsx | 7 +- app/(dashboard)/clients/page.tsx | 4 +- app/(dashboard)/page.tsx | 2 +- app/(dashboard)/projects/[id]/page.tsx | 27 +- .../projects/[id]/project-detail-client.tsx | 277 +++++++++--------- app/(dashboard)/projects/page.tsx | 15 +- app/(dashboard)/projects/projects-client.tsx | 179 +++++------ app/(dashboard)/tasks/page.tsx | 10 +- app/(dashboard)/tasks/tasks-client.tsx | 128 ++++---- 15 files changed, 395 insertions(+), 351 deletions(-) diff --git a/app/(dashboard)/analytics/page.tsx b/app/(dashboard)/analytics/page.tsx index a13174b..87273f1 100644 --- a/app/(dashboard)/analytics/page.tsx +++ b/app/(dashboard)/analytics/page.tsx @@ -15,7 +15,7 @@ export default async function AnalyticsPage({ }) { const context = await requireFreelancer(); const resolvedLocale = await resolveFreelancerLocale(context); - const payload = getClientI18nPayload(resolvedLocale.locale, ["analytics"]); + const payload = getClientI18nPayload(resolvedLocale.locale, ["analytics", "common"]); const params = await searchParams; const range = parseDashboardRange(params.range); diff --git a/app/(dashboard)/calendar/calendar-client.tsx b/app/(dashboard)/calendar/calendar-client.tsx index 7cf000a..36a1005 100644 --- a/app/(dashboard)/calendar/calendar-client.tsx +++ b/app/(dashboard)/calendar/calendar-client.tsx @@ -123,15 +123,9 @@ export function CalendarClient({ events, clients, projects, tasks, activeLocales

{t("common.itemsCount", { count: events.length })}

- - - + + +
@@ -210,7 +204,7 @@ export function CalendarClient({ events, clients, projects, tasks, activeLocales -

Yaklaşan etkinlikler

+

{t("calendar.upcoming")}

@@ -237,7 +231,7 @@ function EventList({ }) { const t = useTranslations(); if (events.length === 0) { - return

Etkinlik yok.

; + return

{t("calendar.noEvents")}

; } return ( diff --git a/app/(dashboard)/calendar/page.tsx b/app/(dashboard)/calendar/page.tsx index e24b55e..5e37672 100644 --- a/app/(dashboard)/calendar/page.tsx +++ b/app/(dashboard)/calendar/page.tsx @@ -20,7 +20,7 @@ function buildTranslations(rows: ContentTranslationRow[] | undefined) { export default async function CalendarPage() { const { context, actor, service } = await requireFreelancerBackend(); const resolvedLocale = await resolveFreelancerLocale(context); - const payload = getClientI18nPayload(resolvedLocale.locale, ["calendar"]); + const payload = getClientI18nPayload(resolvedLocale.locale, ["calendar", "common"]); const i18n = new I18nService(getSqliteConnection().db); const activeLocales = i18n.listLocales(actor).filter(l => l.status !== "archived").map(l => ({ code: l.code, name: l.nativeName })); @@ -61,7 +61,7 @@ export default async function CalendarPage() { .map(({ id, title }) => ({ id, title })); return ( - + ); diff --git a/app/(dashboard)/clients/[id]/actions.ts b/app/(dashboard)/clients/[id]/actions.ts index 259eaa4..e8481fe 100644 --- a/app/(dashboard)/clients/[id]/actions.ts +++ b/app/(dashboard)/clients/[id]/actions.ts @@ -22,7 +22,7 @@ export async function addClientActivity(clientId: string, formData: FormData) { service.addClientActivity(actor, { clientId, type, - title: defaultTitle || requiredText(formData.get("title"), "Aktivite başlığı zorunludur."), + title: defaultTitle || requiredText(formData.get("title"), "clients.detail.activityTitleRequired"), content: defaultContent || cleanText(formData.get("content")), activityDate: optionalDate(formData.get("activity_date")) ?? new Date(), translations, diff --git a/app/(dashboard)/clients/[id]/client-detail-client.tsx b/app/(dashboard)/clients/[id]/client-detail-client.tsx index 899c0b0..1a60d1c 100644 --- a/app/(dashboard)/clients/[id]/client-detail-client.tsx +++ b/app/(dashboard)/clients/[id]/client-detail-client.tsx @@ -22,6 +22,7 @@ export type ClientDetailData = { notes: string | null; client_auth_id: string | null; portal_locale: string; + translations?: Record>; }; export type ClientActivity = { @@ -38,10 +39,12 @@ export function ClientDetailClient({ client, activities, locales, + currentLocale, }: { client: ClientDetailData; activities: ClientActivity[]; locales: Array<{ code: string; nativeName: string; name: string }>; + currentLocale: string; }) { const [isAddingActivity, setIsAddingActivity] = useState(false); const [openDialog, setOpenDialog] = useState(false); @@ -95,13 +98,13 @@ export function ClientDetailClient({ }); const data = await res.json(); if (!res.ok || data.error) { - throw new Error(data.error || "Kullanıcı oluşturulamadı."); + throw new Error(data.error || "clients.detail.portalInviteFailed"); } setInvitationUrl(data.invitation.invitationUrl); setPortalLocale(data.invitation.locale ?? locale); - toast.success("Güvenli portal daveti oluşturuldu."); + toast.success(t("clients.detail.portalInviteCreated")); } catch (error: unknown) { - toast.error(error instanceof Error ? error.message : "Davet oluşturulamadı."); + toast.error(resolveTranslatedError(t, error, "clients.detail.portalInviteFailed")); } finally { setIsCreatingUser(false); } @@ -116,11 +119,11 @@ export function ClientDetailClient({ body: JSON.stringify({ locale: nextLocale }), }); const data = await response.json(); - if (!response.ok || data.error) throw new Error(data.error || "Portal dili güncellenemedi."); - toast.success("Portal dili güncellendi."); + if (!response.ok || data.error) throw new Error(data.error || "clients.detail.portalLocaleUpdateFailed"); + toast.success(t("clients.detail.portalLocaleUpdated")); } catch (error) { setPortalLocale(client.portal_locale); - toast.error(error instanceof Error ? error.message : "Portal dili güncellenemedi."); + toast.error(resolveTranslatedError(t, error, "clients.detail.portalLocaleUpdateFailed")); } } @@ -151,21 +154,21 @@ export function ClientDetailClient({
- Müşteri Portalına Davet Et + {t("clients.detail.invitePortal")} - Müşterin bağlantıyı açıp kendi şifresini belirler. Davet 72 saat geçerlidir ve yalnızca bir kez kullanılabilir. + {t("clients.detail.portalInviteDescription")}
- +
-

Bağlantı yalnızca bu ekranda düz metin olarak gösterilir.

+

{t("clients.detail.invitationUrlHelp")}

) : null} - +
@@ -216,7 +219,7 @@ export function ClientDetailClient({ -
- - -
@@ -362,14 +361,14 @@ export function ClientDetailClient({
-

{activity.translations?.[locales[0].code]?.title ?? activity.title}

+

{activity.translations?.[currentLocale]?.title ?? activity.title}

{getActivityBadge(activity.type)}
- {(activity.translations?.[locales[0].code]?.content ?? activity.content) && ( -

{activity.translations?.[locales[0].code]?.content ?? activity.content}

+ {(activity.translations?.[currentLocale]?.content ?? activity.content) && ( +

{activity.translations?.[currentLocale]?.content ?? activity.content}

)}
@@ -384,3 +383,13 @@ export function ClientDetailClient({
); } + +function resolveTranslatedError( + t: ReturnType, + error: unknown, + fallbackKey: string, +) { + if (!(error instanceof Error)) return t(fallbackKey); + if (/^clients\./.test(error.message)) return t(error.message); + return error.message || t(fallbackKey); +} diff --git a/app/(dashboard)/clients/[id]/page.tsx b/app/(dashboard)/clients/[id]/page.tsx index 9b2daaf..b7e4d18 100644 --- a/app/(dashboard)/clients/[id]/page.tsx +++ b/app/(dashboard)/clients/[id]/page.tsx @@ -27,12 +27,12 @@ export default async function ClientDetailPage({ params }: { params: Promise<{ i const defaultLocale = i18n.getSettings(actor).defaultLocale; const resolvedLocale = await resolveFreelancerLocale(context); - const payload = getClientI18nPayload(resolvedLocale.locale, ["clients"]); + const payload = getClientI18nPayload(resolvedLocale.locale, ["clients", "common"]); let data: { client: ClientDetailData; activities: ClientActivity[] }; try { const row = service.getClient(actor, id); - const clientTranslations = service.contentTranslations.list("client", id); + const clientTranslationsMap = service.contentTranslations.listBatch("client", [id]); const client: ClientDetailData = { id: row.id, @@ -46,7 +46,7 @@ export default async function ClientDetailPage({ params }: { params: Promise<{ i notes: row.notes, client_auth_id: row.authUserId, portal_locale: row.portalLocale ?? defaultLocale, - translations: buildTranslations(clientTranslations), + translations: buildTranslations(clientTranslationsMap.get(id) ?? []), }; const rawActivities = service.listClientActivities(actor, id); @@ -69,8 +69,8 @@ export default async function ClientDetailPage({ params }: { params: Promise<{ i } return ( - - + + ); } diff --git a/app/(dashboard)/clients/clients-client.tsx b/app/(dashboard)/clients/clients-client.tsx index 05f2d2b..6c9089c 100644 --- a/app/(dashboard)/clients/clients-client.tsx +++ b/app/(dashboard)/clients/clients-client.tsx @@ -621,15 +621,14 @@ function PhoneInput({ id, name, defaultValue }: { id: string; name: string; defa } function EmptyState({ hasQuery }: { hasQuery: boolean }) { + const t = useTranslations(); return (

- {hasQuery ? "Aramana uygun müşteri yok" : "Henüz müşteri eklenmedi"} + {hasQuery ? t("clients.empty.noMatchTitle") : t("clients.empty.noClientTitle")}

-

- İlk müşterini ekleyerek potansiyel satışlarını takip etmeye başla. -

+

{t("clients.empty.noClientDesc")}

); } diff --git a/app/(dashboard)/clients/page.tsx b/app/(dashboard)/clients/page.tsx index 810e4e3..92d412b 100644 --- a/app/(dashboard)/clients/page.tsx +++ b/app/(dashboard)/clients/page.tsx @@ -20,7 +20,7 @@ function buildTranslations(rows: ContentTranslationRow[] | undefined) { export default async function ClientsPage() { const { context, actor, service } = await requireFreelancerBackend(); const resolvedLocale = await resolveFreelancerLocale(context); - const payload = getClientI18nPayload(resolvedLocale.locale, ["clients"]); + const payload = getClientI18nPayload(resolvedLocale.locale, ["clients", "common"]); const i18n = new I18nService(getSqliteConnection().db); const activeLocales = i18n.listLocales(actor).filter(l => l.status !== "archived").map(l => ({ code: l.code, name: l.nativeName })); @@ -78,7 +78,7 @@ export default async function ClientsPage() { }); return ( - + sum + client.revenueTotal, 0)} diff --git a/app/(dashboard)/page.tsx b/app/(dashboard)/page.tsx index 7e45374..459bd7e 100644 --- a/app/(dashboard)/page.tsx +++ b/app/(dashboard)/page.tsx @@ -15,7 +15,7 @@ export default async function DashboardPage({ }) { const context = await requireFreelancer(); const resolvedLocale = await resolveFreelancerLocale(context); - const payload = getClientI18nPayload(resolvedLocale.locale, ["dashboard"]); + const payload = getClientI18nPayload(resolvedLocale.locale, ["dashboard", "common"]); const params = await searchParams; const range = parseDashboardRange(params.range); diff --git a/app/(dashboard)/projects/[id]/page.tsx b/app/(dashboard)/projects/[id]/page.tsx index 3a823f8..0dfaef8 100644 --- a/app/(dashboard)/projects/[id]/page.tsx +++ b/app/(dashboard)/projects/[id]/page.tsx @@ -12,11 +12,14 @@ import { DomainError } from "@/server/domain/errors"; import { ContentTranslationService, type ContentTranslationRow } from "@/server/i18n/content"; import { resolveFreelancerLocale } from "@/server/i18n/resolver"; import { requireFreelancerBackend } from "@/server/web/freelancer"; +import { getClientI18nPayload } from "@/server/i18n/translator"; +import { I18nProvider } from "@/components/i18n/i18n-provider"; export default async function ProjectDetailPage({ params }: { params: Promise<{ id: string }> }) { const { id } = await params; - const locale = await resolveFreelancerLocale(); - const { actor, service } = await requireFreelancerBackend(); + const { context, actor, service } = await requireFreelancerBackend(); + const locale = await resolveFreelancerLocale(context); + const payload = getClientI18nPayload(locale.locale, ["projects", "tasks", "common"]); const content = new ContentTranslationService(getSqliteConnection().db); const localization = content.getLocalizationContext(actor); @@ -120,15 +123,19 @@ export default async function ProjectDetailPage({ params }: { params: Promise<{ throw error; } + const i18nPayload = await getClientI18nPayload(locale.locale, ["projects", "tasks", "common"]); + return ( - + + + ); } diff --git a/app/(dashboard)/projects/[id]/project-detail-client.tsx b/app/(dashboard)/projects/[id]/project-detail-client.tsx index 1cf0eaa..39a0fb8 100644 --- a/app/(dashboard)/projects/[id]/project-detail-client.tsx +++ b/app/(dashboard)/projects/[id]/project-detail-client.tsx @@ -12,6 +12,7 @@ import { updateTaskStatusRecord, } from "@/app/(dashboard)/tasks/actions"; import { LocalizedFields, type LocalizedFieldLocale, type LocalizedFieldValues } from "@/components/i18n/localized-fields"; +import { useTranslations } from "@/components/i18n/i18n-provider"; import { PendingLink } from "@/components/ui/pending-link"; import { PendingSubmitButton } from "@/components/ui/pending-submit-button"; import { contentTranslationRegistry } from "@/lib/i18n/content"; @@ -132,19 +133,6 @@ type ProjectDetailClientProps = { }; }; -const typeLabels = { - client_project: "Müşteri projesi", - side_project: "Side project", -}; - -const statusLabels = { - planning: "Planlama", - active: "Aktif", - paused: "Duraklatıldı", - completed: "Tamamlandı", - cancelled: "İptal edildi", -}; - const statusClasses = { planning: "border-blue-200 bg-blue-50 text-blue-700", active: "border-emerald-200 bg-emerald-50 text-emerald-700", @@ -160,18 +148,18 @@ const priorityClasses = { urgent: "border-rose-200 bg-rose-50 text-rose-700", }; -const sectionLabels: Record = { - overview: "Genel bakış", - problem: "Çözdüğü problem", - goal: "Amaç", - audience: "Hedef kitle", - scope: "Kapsam", - design_system: "Design system", - color_palette: "Renk paleti", - typography: "Tipografi", - assets: "Görsel varlıklar", - notes: "Notlar", -}; +const sectionCategoryOptions: ProjectPlanningSectionItem["category"][] = [ + "overview", + "problem", + "goal", + "audience", + "scope", + "design_system", + "color_palette", + "typography", + "assets", + "notes", +]; const planningCategories: ProjectPlanningSectionItem["category"][] = [ "overview", @@ -197,6 +185,7 @@ export function ProjectDetailClient({ revisions, localization, }: ProjectDetailClientProps) { + const t = useTranslations(); const [activeTab, setActiveTab] = useState<"planning" | "design" | "tasks" | "finance" | "revisions">( "planning", ); @@ -221,7 +210,7 @@ export function ProjectDetailClient({
@@ -230,7 +219,7 @@ export function ProjectDetailClient({ {project.name} - {statusLabels[project.status]} + {t(`projects.status.${project.status}`)}
@@ -246,9 +235,9 @@ export function ProjectDetailClient({ variant="secondary" className="gap-2" idleIcon={} - pendingChildren="Tamamlanıyor" + pendingChildren={t("projects.detail.completing")} > - Tamamla + {t("projects.detail.complete")} ) : null} @@ -271,27 +260,27 @@ export function ProjectDetailClient({ ) : (
- Kapak görseli yok + {t("projects.card.noCover")}
)}
- + @@ -300,10 +289,10 @@ export function ProjectDetailClient({
- - + + @@ -312,19 +301,19 @@ export function ProjectDetailClient({
setActiveTab("planning")}> - Planlama + {t("projects.detail.planning")} setActiveTab("design")}> - Design system + {t("projects.detail.designSystem")} setActiveTab("tasks")}> - Görevler + {t("projects.detail.tasks")} setActiveTab("finance")}> - Finans + {t("projects.detail.finance")} setActiveTab("revisions")}> - Revizyonlar + {t("projects.detail.revisions")} {revisions.filter(r => r.status === 'pending').length > 0 && ( {revisions.filter(r => r.status === 'pending').length} @@ -336,8 +325,8 @@ export function ProjectDetailClient({ {activeTab === "planning" ? ( -

Müşteri Revizyon Talepleri

+

{t("projects.detail.revisionsTitle")}

{revisions.length === 0 ? ( -

Bu proje için henüz bir revizyon talebi oluşturulmamış.

+

{t("projects.detail.revisionsEmpty")}

) : (
{revisions.map(rev => ( @@ -420,10 +410,10 @@ function RevisionsPanel({ - Bekliyor - İşleniyor - Tamamlandı - Reddedildi + {t("projects.status.pending")} + {t("projects.status.in_progress")} + {t("projects.status.completed")} + {t("projects.status.rejected")}
@@ -452,6 +442,7 @@ function SectionGrid({ defaultCategory: ProjectPlanningSectionItem["category"]; localization: ProjectDetailClientProps["localization"]; }) { + const t = useTranslations(); return ( @@ -472,10 +463,9 @@ function SectionGrid({ ) : (
-

Henüz kayıt yok

+

{t("projects.detail.noRecords")}

- Bu proje için ilk planlama veya design system alanını ekleyerek proje bilgisini - görevlerden bağımsız hale getir. + {t("projects.detail.noRecordsDesc")}

)} @@ -491,12 +481,13 @@ function PlanningSectionCard({ section: ProjectPlanningSectionItem; localization: ProjectDetailClientProps["localization"]; }) { + const t = useTranslations(); return (
- {sectionLabels[section.category]} + {t(`projects.sections.${section.category}`)}

{section.title}

@@ -508,13 +499,13 @@ function PlanningSectionCard({ variant="secondary" className="px-3 text-rose-600" idleIcon={} - aria-label="Sil" + aria-label={t("projects.detail.delete")} />

- {section.content || "İçerik eklenmedi."} + {section.content || t("projects.detail.noContent")}

@@ -534,6 +525,7 @@ function SectionDialog({ section?: ProjectPlanningSectionItem; localization: ProjectDetailClientProps["localization"]; }) { + const t = useTranslations(); const [open, setOpen] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const action = @@ -560,7 +552,7 @@ function SectionDialog({ className="gap-2 px-3" > {mode === "create" ? : } - {mode === "create" ? "Alan ekle" : null} + {mode === "create" ? t("projects.detail.addPlan") : null} @@ -569,24 +561,24 @@ function SectionDialog({ {section ? : null} - {mode === "create" ? "Planlama alanı ekle" : "Planlama alanını düzenle"} + {mode === "create" ? t("projects.detail.planCreateTitle") : t("projects.detail.planEditTitle")} - Projenin görev dışı bilgisini yapılandırılmış alanlarda sakla. + {t("projects.detail.planDesc")}
- + @@ -636,6 +634,7 @@ function TaskPanel({ tasks: ProjectDetailTaskItem[]; localization: ProjectDetailClientProps["localization"]; }) { + const t = useTranslations(); const [view, setView] = useState<"list" | "kanban">("list"); const [statusOverrides, setStatusOverrides] = useState< Partial> @@ -665,7 +664,7 @@ function TaskPanel({ toast.error( error instanceof Error ? error.message - : "Görev durumu güncellenemedi.", + : t("projects.detail.taskUpdateFailed"), ); }) .finally(() => { @@ -693,9 +692,9 @@ function TaskPanel({
-

Proje görevleri

+

{t("projects.detail.tasksTitle")}

- Bu proje ile bağlantılı görevler aynı task modülünden beslenir. + {t("projects.detail.tasksDesc")}

@@ -707,7 +706,7 @@ function TaskPanel({ onClick={() => setView("list")} > - Liste + {t("projects.detail.list")}
@@ -726,10 +725,10 @@ function TaskPanel({ {localTasks.length > 0 && view === "list" ? (
- Görev - Öncelik - Son tarih - İşlem + {t("projects.detail.colTask")} + {t("projects.detail.colPriority")} + {t("projects.detail.colDue")} + {t("projects.detail.colAction")}
{localTasks.map((task) => ( @@ -749,22 +748,18 @@ function TaskPanel({ {task.title}
{task.is_public_to_client && ( - Müşteriye Açık + {t("projects.detail.taskPublic")} )}
- {task.status === "done" - ? "Tamamlandı" - : task.status === "in_progress" - ? "Devam ediyor" - : "Yapılacak"} + {t(`projects.status.${task.status}`)}
- {task.priority} + {t(`tasks.priority.${task.priority}`)}
- {task.due_at ? formatDateTime(task.due_at) : "Yok"} + {task.due_at ? formatDateTime(task.due_at) : t("projects.detail.taskNone")}
{task.status !== "done" ? ( @@ -781,7 +776,7 @@ function TaskPanel({ ) : ( )} - {pendingTaskIds.has(task.id) ? "Tamamlanıyor" : "Tamamla"} + {pendingTaskIds.has(task.id) ? t("projects.detail.completing") : t("projects.detail.complete")} ) : null}
@@ -800,7 +795,7 @@ function TaskPanel({ ) : null} {localTasks.length === 0 ? ( - + ) : null}
@@ -816,6 +811,7 @@ function ProjectTaskKanban({ pendingTaskIds: Set; onTaskStatusChange: (taskId: string, status: ProjectDetailTaskItem["status"]) => void; }) { + const t = useTranslations(); const columns = ["todo", "in_progress", "done"] as const; const [draggedTaskId, setDraggedTaskId] = useState(null); @@ -850,7 +846,7 @@ function ProjectTaskKanban({ >

- {getTaskStatusLabel(status)} + {t(`projects.status.${status}`)}

{columnTasks.length}
@@ -871,11 +867,11 @@ function ProjectTaskKanban({
{task.title}
- {task.due_at ? formatDateTime(task.due_at) : "Son tarih yok"} + {task.due_at ? formatDateTime(task.due_at) : t("projects.detail.noDeadline")}
- {task.priority} + {t(`tasks.priority.${task.priority}`)} {task.status !== "done" ? (
- Proje ayarları + {t("projects.detail.settingsTitle")} - İlerleme hesaplama yöntemi ve revizyon kotasını belirle. + {t("projects.detail.settingsDesc")}
- +
{progressType === "manual" && (
- +
)} {progressType === "auto" && ( -

İlerleme yüzdesi "Görevler" sekmesindeki görevlerin tamamlanma durumuna göre otomatik hesaplanacaktır.

+

{t("projects.detail.progressAutoHint")}

)}
- + setRevisionQuota(Number(e.target.value))} /> -

Müşterinin portal üzerinden talep edebileceği toplam revizyon hakkı.

+

{t("projects.detail.revisionQuotaHint")}

@@ -1011,6 +1008,7 @@ function ProjectTaskDialog({ clientId: string | null; localization: ProjectDetailClientProps["localization"]; }) { + const t = useTranslations(); const [open, setOpen] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); @@ -1030,7 +1028,7 @@ function ProjectTaskDialog({ @@ -1038,9 +1036,9 @@ function ProjectTaskDialog({ {clientId ? : null} - Projeye görev ekle + {t("projects.detail.addTaskTitle")} - Yeni görev bu proje ile ilişkilendirilerek görev modülüne kaydedilir. + {t("projects.detail.addTaskDesc")} @@ -1049,33 +1047,39 @@ function ProjectTaskDialog({ idPrefix="project-task" defaultLocale={localization.defaultLocale} locales={localization.locales} - fields={contentTranslationRegistry.task} + fields={contentTranslationRegistry.task.map((field) => ({ + ...field, + label: t(`tasks.fields.${field.name}`), + placeholder: "placeholder" in field && typeof field.placeholder === "string" + ? t(`tasks.placeholders.${field.name}`) + : undefined, + }))} />
- +
- +
@@ -1093,37 +1097,37 @@ function ProjectTaskDialog({ htmlFor="is_public_to_client" className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" > - Müşteri Portalında Göster + {t("projects.detail.publicToClient")}

- Eğer müşteri hesabı varsa, bu görev müşteri portalındaki proje detayında da görünür olur. + {t("projects.detail.publicToClientHint")}

- +
- +
- +
@@ -1132,7 +1136,7 @@ function ProjectTaskDialog({ @@ -1142,13 +1146,14 @@ function ProjectTaskDialog({ } function FinancePanel({ transactions }: { transactions: ProjectFinanceItem[] }) { + const t = useTranslations(); return (
-

Finans bağlantıları

+

{t("projects.detail.financeTitle")}

- Bu projeye bağlanan gelir ve gider kayıtları. + {t("projects.detail.financeDesc")}

@@ -1158,7 +1163,7 @@ function FinancePanel({ transactions }: { transactions: ProjectFinanceItem[] })
- {transaction.category || (transaction.type === "income" ? "Gelir" : "Gider")} + {transaction.category || (transaction.type === "income" ? t("projects.detail.income") : t("projects.detail.expense"))}
{formatDate(transaction.transaction_date)} · {transaction.payment_status} @@ -1178,7 +1183,7 @@ function FinancePanel({ transactions }: { transactions: ProjectFinanceItem[] }) ))}
) : ( - + )} @@ -1284,11 +1289,7 @@ function formatDateTime(value: string) { }).format(new Date(value)); } -function getTaskStatusLabel(status: ProjectDetailTaskItem["status"]) { - if (status === "done") return "Tamamlandı"; - if (status === "in_progress") return "Devam ediyor"; - return "Yapılacak"; -} +// Removed function since it's localized inline now or no longer needed function formatCurrency(value: number, currency: string) { return new Intl.NumberFormat(getDocumentIntlLocale(), { diff --git a/app/(dashboard)/projects/page.tsx b/app/(dashboard)/projects/page.tsx index c60952e..afd137e 100644 --- a/app/(dashboard)/projects/page.tsx +++ b/app/(dashboard)/projects/page.tsx @@ -3,10 +3,13 @@ import { getSqliteConnection } from "@/server/db/client"; import { ContentTranslationService, type ContentTranslationRow } from "@/server/i18n/content"; import { resolveFreelancerLocale } from "@/server/i18n/resolver"; import { requireFreelancerBackend } from "@/server/web/freelancer"; +import { getClientI18nPayload } from "@/server/i18n/translator"; +import { I18nProvider } from "@/components/i18n/i18n-provider"; export default async function ProjectsPage() { - const locale = await resolveFreelancerLocale(); - const { actor, service } = await requireFreelancerBackend(); + const { context, actor, service } = await requireFreelancerBackend(); + const locale = await resolveFreelancerLocale(context); + const payload = getClientI18nPayload(locale.locale, ["projects", "common"]); const content = new ContentTranslationService(getSqliteConnection().db); const localization = content.getLocalizationContext(actor); const projectRows = service.listProjects(actor); @@ -58,7 +61,13 @@ export default async function ProjectsPage() { .sort((a, b) => a.name.localeCompare(b.name, locale.locale)) .map(({ id, name }) => ({ id, name })); - return ; + const i18nPayload = await getClientI18nPayload(locale.locale, ["projects", "tasks", "common"]); + + return ( + + + + ); } function toLocalizedValues(rows: ContentTranslationRow[]) { diff --git a/app/(dashboard)/projects/projects-client.tsx b/app/(dashboard)/projects/projects-client.tsx index 3d3114a..0cf1d39 100644 --- a/app/(dashboard)/projects/projects-client.tsx +++ b/app/(dashboard)/projects/projects-client.tsx @@ -73,18 +73,18 @@ export type ProjectListItem = { translations?: LocalizedFieldValues; }; -const typeLabels = { - client_project: "Müşteri projesi", - side_project: "Side project", -}; +const typeLabels = (t: any) => ({ + client_project: t("projects.types.client"), + side_project: t("projects.types.side"), +}); -const statusLabels = { - planning: "Planlama", - active: "Aktif", - paused: "Duraklatıldı", - completed: "Tamamlandı", - cancelled: "İptal edildi", -}; +const statusLabels = (t: any) => ({ + planning: t("projects.status.planning"), + active: t("projects.status.active"), + paused: t("projects.status.paused"), + completed: t("projects.status.completed"), + cancelled: t("projects.status.cancelled"), +}); const statusClasses = { planning: "border-blue-200 bg-blue-50 text-blue-700", @@ -108,9 +108,11 @@ export function ProjectsClient({ projects, clients, localization }: ProjectsClie const [query, setQuery] = useState(""); const [view, setView] = useState<"grid" | "list">("grid"); const normalizedQuery = query.trim().toLowerCase(); + const types = typeLabels(t); + const filteredProjects = normalizedQuery ? projects.filter((project) => - [project.name, project.description, project.clientName, typeLabels[project.type]] + [project.name, project.description, project.clientName, types[project.type]] .filter(Boolean) .some((value) => value!.toLowerCase().includes(normalizedQuery)), ) @@ -140,7 +142,7 @@ export function ProjectsClient({ projects, clients, localization }: ProjectsClie
- +
@@ -149,16 +151,16 @@ export function ProjectsClient({ projects, clients, localization }: ProjectsClie
-

Proje listesi

+

{t("projects.list.title")}

- {filteredProjects.length} kayıt görüntüleniyor. + {t("projects.list.count", { count: filteredProjects.length })}

setQuery(event.target.value)} - placeholder="Proje, müşteri veya açıklama ara" + placeholder={t("projects.list.search")} className="sm:w-80" />
@@ -169,7 +171,7 @@ export function ProjectsClient({ projects, clients, localization }: ProjectsClie onClick={() => setView("grid")} > - Kart + {t("projects.list.grid")}
@@ -195,11 +197,11 @@ export function ProjectsClient({ projects, clients, localization }: ProjectsClie
- Proje - Tür - Durum - İlerleme - İşlem + {t("projects.list.columns.project")} + {t("projects.list.columns.type")} + {t("projects.list.columns.status")} + {t("projects.list.columns.budgetDeadline")} + İşlemler
{filteredProjects.map((project) => ( @@ -227,6 +229,7 @@ function ProjectCard({ clients: ProjectClientOption[]; localization: ProjectsClientProps["localization"]; }) { + const t = useTranslations(); const router = useRouter(); const [isNavigating, startNavigation] = useTransition(); const detailHref = `/projects/${project.id}`; @@ -273,10 +276,12 @@ function ProjectCard({

{project.name}

- {project.description || "Açıklama eklenmedi."} + {project.description || t("projects.card.noDescription")}

- {statusLabels[project.status]} + + {statusLabels(t)[project.status]} +
@@ -284,7 +289,7 @@ function ProjectCard({
- {project.doneTaskCount}/{project.taskCount} görev tamamlandı + {t("projects.card.taskProgress", { done: project.doneTaskCount, total: project.taskCount })}
@@ -294,6 +299,7 @@ function ProjectCard({ } function ProjectCover({ project }: { project: ProjectListItem }) { + const t = useTranslations(); if (project.coverImageUrl) { return (
@@ -311,7 +317,7 @@ function ProjectCover({ project }: { project: ProjectListItem }) { return (
- Kapak görseli yok + {t("projects.card.noCover")}
); } @@ -325,17 +331,18 @@ function ProjectRow({ clients: ProjectClientOption[]; localization: ProjectsClientProps["localization"]; }) { + const t = useTranslations(); return (
{project.name}
- {project.clientName || "Bağımsız side project"} + {project.clientName || t("projects.card.noClient")}
-
{typeLabels[project.type]}
+
{typeLabels(t)[project.type]}
- {statusLabels[project.status]} + {statusLabels(t)[project.status]}
@@ -348,15 +355,16 @@ function ProjectRow({ } function ProjectMeta({ project }: { project: ProjectListItem }) { + const t = useTranslations(); return (
-
{typeLabels[project.type]}
-
{project.clientName || "Müşteri bağlantısı yok"}
+
{typeLabels(t)[project.type]}
+
{project.clientName || t("projects.card.noClient")}
- {project.due_date ? formatDate(project.due_date) : "Deadline yok"} + {project.due_date ? formatDate(project.due_date) : t("projects.card.noDeadline")}
-
{project.budget_amount ? formatCurrency(project.budget_amount) : "Bütçe yok"}
+
{project.budget_amount ? formatCurrency(project.budget_amount) : t("projects.card.noBudget")}
); } @@ -372,6 +380,7 @@ function ProjectActions({ localization: ProjectsClientProps["localization"]; showDetail: boolean; }) { + const t = useTranslations(); return (
@@ -399,8 +408,8 @@ function ProjectActions({ } > @@ -423,6 +432,7 @@ function ProjectDialog({ localization: ProjectsClientProps["localization"]; iconOnly?: boolean; }) { + const t = useTranslations(); const [open, setOpen] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [projectType, setProjectType] = useState(project?.type || "client_project"); @@ -434,12 +444,12 @@ function ProjectDialog({ try { await action(formData); setOpen(false); - toast.success(mode === "create" ? "Proje eklendi." : "Proje güncellendi."); + toast.success(mode === "create" ? t("projects.messages.created") : t("projects.messages.updated")); } catch (error) { toast.error( error instanceof Error ? error.message - : "Proje kaydedilirken beklenmeyen bir hata oluştu.", + : t("projects.errors.saveFailed"), ); } finally { setIsSubmitting(false); @@ -453,20 +463,20 @@ function ProjectDialog({ variant={mode === "create" ? "default" : "secondary"} size={iconOnly ? "icon" : "default"} className={iconOnly ? undefined : "min-w-24 gap-2 px-3"} - title={mode === "create" ? "Proje ekle" : "Düzenle"} - aria-label={mode === "create" ? "Proje ekle" : "Düzenle"} + title={mode === "create" ? t("projects.actions.add") : t("projects.actions.edit")} + aria-label={mode === "create" ? t("projects.actions.add") : t("projects.actions.edit")} > {mode === "create" ? : } - {iconOnly ? null : mode === "create" ? "Proje ekle" : "Düzenle"} + {iconOnly ? null : mode === "create" ? t("projects.actions.add") : t("projects.actions.edit")}
{project ? : null} - {mode === "create" ? "Yeni proje" : "Projeyi düzenle"} + {mode === "create" ? t("projects.form.createTitle") : t("projects.form.editTitle")} - Müşteri projelerini ve kişisel side projectleri aynı modelde takip et. + {t("projects.form.description")} @@ -484,10 +494,10 @@ function ProjectDialog({
@@ -497,6 +507,7 @@ function ProjectDialog({ } function CoverImageInput({ project }: { project?: ProjectListItem }) { + const t = useTranslations(); const inputId = `cover-${project?.id || "new"}`; const [previewUrl, setPreviewUrl] = useState(project?.coverImageUrl || ""); @@ -528,7 +539,7 @@ function CoverImageInput({ project }: { project?: ProjectListItem }) { return (
- +
-
Kapak görseli seç
-
PNG, JPG, WebP veya GIF
+
{t("projects.form.coverImageSelect")}
+
{t("projects.form.coverImageFormat")}
)} {previewUrl ? (
- Görseli değiştirmek için tıkla. + {t("projects.form.coverImageChange")}
) : null} @@ -585,6 +596,7 @@ function ProjectFormFields({ projectType: ProjectListItem["type"]; onProjectTypeChange: (value: ProjectListItem["type"]) => void; }) { + const t = useTranslations(); return (
@@ -593,7 +605,7 @@ function ProjectFormFields({ idPrefix={`project-${project?.id || "new"}`} defaultLocale={localization.defaultLocale} locales={localization.locales} - fields={contentTranslationRegistry.project} + fields={contentTranslationRegistry.project.map((f: any) => ({ ...f, label: (t as any)(`projects.fields.${f.name}`) || f.label, placeholder: f.placeholder ? (t as any)(`projects.placeholders.${f.name}`) || f.placeholder : undefined }))} values={project?.translations} fallbackValues={{ name: project?.name, @@ -604,30 +616,30 @@ function ProjectFormFields({
- +
- + - + - Planlama - Aktif - Duraklatıldı - Tamamlandı - İptal edildi + {t("projects.status.planning")} + {t("projects.status.active")} + {t("projects.status.paused")} + {t("projects.status.completed")} + {t("projects.status.cancelled")}
- +
- +
- +
- +
- +
{!compact ? (
- İlerleme + {t("projects.card.progress")} {progress}%
) : null} @@ -729,17 +742,14 @@ function ProgressBar({ progress, compact = false }: { progress: number; compact? } function EmptyState({ hasQuery }: { hasQuery: boolean }) { + const t = useTranslations(); return ( -
- -

- {hasQuery ? "Aramana uygun proje yok" : "Henüz proje eklenmedi"} -

-

- {hasQuery - ? "Arama metnini sadeleştirerek tekrar deneyebilirsin." - : "İlk müşteri projen veya side project kaydınla operasyon akışını kurmaya başlayabilirsin."} -

+
+ +
{t("projects.empty.title")}
+
+ {t("projects.empty.description")} +
); } @@ -761,6 +771,7 @@ function formatCurrency(value: number) { } function AIProjectRiskDialog({ projectId }: { projectId?: string }) { + const t = useTranslations(); const [open, setOpen] = useState(false); const [loading, setLoading] = useState(false); const [result, setResult] = useState(null); @@ -793,9 +804,7 @@ function AIProjectRiskDialog({ projectId }: { projectId?: string }) { + {t("projects.actions.ai")} diff --git a/app/(dashboard)/tasks/page.tsx b/app/(dashboard)/tasks/page.tsx index 0c3fa59..26a5ec7 100644 --- a/app/(dashboard)/tasks/page.tsx +++ b/app/(dashboard)/tasks/page.tsx @@ -3,6 +3,8 @@ import { getSqliteConnection } from "@/server/db/client"; import { ContentTranslationService, type ContentTranslationRow } from "@/server/i18n/content"; import { resolveFreelancerLocale } from "@/server/i18n/resolver"; import { requireFreelancerBackend } from "@/server/web/freelancer"; +import { getClientI18nPayload } from "@/server/i18n/translator"; +import { I18nProvider } from "@/components/i18n/i18n-provider"; export default async function TasksPage() { const locale = await resolveFreelancerLocale(); @@ -55,7 +57,13 @@ export default async function TasksPage() { .filter((project) => project.status !== "cancelled") .map(({ id, name, clientId }) => ({ id, name, client_id: clientId })); - return ; + const i18nPayload = await getClientI18nPayload(locale.locale, ["tasks", "projects", "common"]); + + return ( + + + + ); } function toLocalizedValues(rows: ContentTranslationRow[]) { diff --git a/app/(dashboard)/tasks/tasks-client.tsx b/app/(dashboard)/tasks/tasks-client.tsx index 9979685..ba3c189 100644 --- a/app/(dashboard)/tasks/tasks-client.tsx +++ b/app/(dashboard)/tasks/tasks-client.tsx @@ -126,7 +126,7 @@ export function TasksClient({ tasks, clients, projects, localization }: TasksCli toast.error( error instanceof Error ? error.message - : "Görev durumu güncellenemedi.", + : t("tasks.messages.updateFailed") || "Görev durumu güncellenemedi.", ); }) .finally(() => { @@ -211,35 +211,35 @@ export function TasksClient({ tasks, clients, projects, localization }: TasksCli
- - - - + + + +
-

Görev listesi

+

{t("tasks.list.title")}

- {filteredTasks.length} kayıt görüntüleniyor. + {t("tasks.list.showing", { count: filteredTasks.length.toString() })}

setQuery(event.target.value)} - placeholder="Görev, proje veya müşteri ara" + placeholder={t("tasks.list.search")} className="sm:w-80" /> : null} - {mode === "create" ? "Yeni görev" : "Görevi düzenle"} + {mode === "create" ? t("tasks.form.createTitle") : t("tasks.form.editTitle")} - Görevi proje, müşteri, öncelik ve son tarih bilgileriyle kaydet. + {t("tasks.form.desc")} @@ -619,10 +624,10 @@ function TaskDialog({ @@ -642,6 +647,7 @@ function TaskFormFields({ projects: TaskRelationOption[]; localization: TasksClientProps["localization"]; }) { + const t = useTranslations(); const [clientId, setClientId] = useState(task?.client_id || "__none"); const [projectId, setProjectId] = useState(task?.project_id || "__none"); const selectedProject = @@ -681,7 +687,7 @@ function TaskFormFields({ idPrefix={`task-${task?.id || "new"}`} defaultLocale={localization.defaultLocale} locales={localization.locales} - fields={contentTranslationRegistry.task} + fields={contentTranslationRegistry.task.map((f: any) => ({ ...f, label: (t as any)(`tasks.fields.${f.name}`) || f.label, placeholder: f.placeholder ? (t as any)(`tasks.placeholders.${f.name}`) || f.placeholder : undefined }))} values={task?.translations} fallbackValues={{ title: task?.title, @@ -690,22 +696,22 @@ function TaskFormFields({ />
- - Yapılacak - Devam ediyor - Tamamlandı + + {t("tasks.status.todo")} + {t("tasks.status.in_progress")} + {t("tasks.status.done")} - - Düşük - Orta - Yüksek - Acil + + {t("tasks.priority.low")} + {t("tasks.priority.medium")} + {t("tasks.priority.high")} + {t("tasks.priority.urgent")}
- + {shouldLockClient ? : null}
- +
- +
- +
@@ -792,12 +798,13 @@ function SelectField({ defaultValue: string; children: React.ReactNode; }) { + const t = useTranslations(); return (
@@ -822,16 +829,17 @@ function StatCard({ label, value }: { label: string; value: string }) { } function EmptyState({ hasQuery }: { hasQuery: boolean }) { + const t = useTranslations(); return (

- {hasQuery ? "Aramana uygun görev yok" : "Henüz görev eklenmedi"} + {hasQuery ? t("tasks.empty.noMatchTitle") : t("tasks.empty.noTaskTitle")}

{hasQuery - ? "Arama metnini sadeleştirerek tekrar deneyebilirsin." - : "İlk görevini ekleyerek proje ve müşteri operasyonunu takip etmeye başlayabilirsin."} + ? t("tasks.empty.noMatchDesc") + : t("tasks.empty.noTaskDesc")}

);