diff --git a/lib/i18n/browser.ts b/lib/i18n/browser.ts new file mode 100644 index 0000000..8de6e8f --- /dev/null +++ b/lib/i18n/browser.ts @@ -0,0 +1,10 @@ +import { toIntlLocale } from "./format"; + +export function getDocumentLocale(): string { + if (typeof document === "undefined") return "tr"; + return document.documentElement.lang || "tr"; +} + +export function getDocumentIntlLocale(): string { + return toIntlLocale(getDocumentLocale()); +} diff --git a/lib/i18n/catalog.ts b/lib/i18n/catalog.ts new file mode 100644 index 0000000..02bcebb --- /dev/null +++ b/lib/i18n/catalog.ts @@ -0,0 +1,53 @@ +import { enCatalog } from "../../locales/en"; +import { trCatalog } from "../../locales/tr"; +import type { I18nNamespace, TranslationCatalog } from "./types"; + +export const DEFAULT_LOCALE = "tr"; +export const BUILT_IN_LOCALES = ["tr", "en"] as const; + +export const BUILT_IN_CATALOGS: Record = { + tr: trCatalog, + en: enCatalog, +}; + +export function getBuiltInCatalog(locale: string): TranslationCatalog | null { + return BUILT_IN_CATALOGS[locale] ?? null; +} + +export function pickNamespaces( + catalog: TranslationCatalog, + namespaces: readonly I18nNamespace[], +): Partial { + return Object.fromEntries( + namespaces.map((namespace) => [namespace, catalog[namespace] ?? {}]), + ) as Partial; +} + +export function flattenCatalog( + catalog: Partial, + namespaces: readonly I18nNamespace[], +): Record { + const entries: Record = {}; + + for (const namespace of namespaces) { + const namespaceCatalog = catalog[namespace] ?? {}; + for (const [key, value] of Object.entries(namespaceCatalog)) { + entries[`${namespace}.${key}`] = value; + } + } + + return entries; +} + +export function compareCatalogKeys( + left: TranslationCatalog, + right: TranslationCatalog, + namespaces: readonly I18nNamespace[], +): { missingInLeft: string[]; missingInRight: string[] } { + const leftKeys = new Set(Object.keys(flattenCatalog(left, namespaces))); + const rightKeys = new Set(Object.keys(flattenCatalog(right, namespaces))); + return { + missingInLeft: [...rightKeys].filter((key) => !leftKeys.has(key)).sort(), + missingInRight: [...leftKeys].filter((key) => !rightKeys.has(key)).sort(), + }; +} diff --git a/lib/i18n/content.ts b/lib/i18n/content.ts new file mode 100644 index 0000000..5035eef --- /dev/null +++ b/lib/i18n/content.ts @@ -0,0 +1,50 @@ +export type ContentTranslationFieldKind = "text" | "textarea"; +export type ContentTranslationEntityType = + | "branding" + | "calendar_event" + | "client" + | "planning_section" + | "project" + | "task"; + +export type ContentTranslationField = { + name: string; + label: string; + kind?: ContentTranslationFieldKind; + required?: boolean; + maxLength?: number; + placeholder?: string; +}; + +export type ContentTranslationInput = Record>; + +export const contentTranslationRegistry = { + project: [ + { name: "name", label: "Proje adı", required: true, maxLength: 200, placeholder: "Örn. Marka web sitesi" }, + { name: "description", label: "Açıklama", kind: "textarea", maxLength: 20_000, placeholder: "Kapsam, hedef veya teslimat notları..." }, + { name: "coverImageAlt", label: "Görsel alt metni", maxLength: 500, placeholder: "Görseli kısaca açıkla" }, + ], + planning_section: [ + { name: "title", label: "Başlık", required: true, maxLength: 300, placeholder: "Örn. Başarı kriterleri" }, + { name: "content", label: "İçerik", kind: "textarea", maxLength: 50_000, placeholder: "Kısa notlar, kriterler, renkler, tipografi kararları..." }, + ], + task: [ + { name: "title", label: "Başlık", required: true, maxLength: 300, placeholder: "Örn. Ana sayfa wireframe revizyonu" }, + { name: "description", label: "Açıklama", kind: "textarea", maxLength: 20_000, placeholder: "Kapsam, not veya teslim kriterleri..." }, + ], + branding: [ + { name: "portalWelcome", label: "Portal karşılama metni", kind: "textarea", maxLength: 10_000 }, + { name: "portalFooter", label: "Portal footer metni", kind: "textarea", maxLength: 5_000 }, + ], + calendar_event: [ + { name: "title", label: "Başlık", required: true, maxLength: 300 }, + { name: "description", label: "Açıklama", kind: "textarea", maxLength: 20_000 }, + ], + client: [ + { name: "notes", label: "Notlar", kind: "textarea", maxLength: 10_000 }, + ], +} satisfies Record; + +export function contentInputName(locale: string, field: string) { + return `i18n.${locale}.${field}`; +} diff --git a/lib/i18n/date-fns.ts b/lib/i18n/date-fns.ts new file mode 100644 index 0000000..8872b17 --- /dev/null +++ b/lib/i18n/date-fns.ts @@ -0,0 +1,13 @@ +import { enUS, fr, tr } from "date-fns/locale"; + +export function getDateFnsLocale(locale: string) { + const normalized = locale.split("-")[0]; + if (normalized === "tr") return tr; + if (normalized === "fr") return fr; + return enUS; +} + +export function getDocumentDateFnsLocale() { + if (typeof document === "undefined") return tr; + return getDateFnsLocale(document.documentElement.lang || "tr"); +} diff --git a/lib/i18n/format.ts b/lib/i18n/format.ts new file mode 100644 index 0000000..df023e6 --- /dev/null +++ b/lib/i18n/format.ts @@ -0,0 +1,72 @@ +import type { TranslationValues } from "./types"; + +const PLURAL_PATTERN = /\{(\w+),\s*plural,\s*one\s*\{([^{}]*)\}\s*other\s*\{([^{}]*)\}\s*\}/g; +const VALUE_PATTERN = /\{(\w+)\}/g; + +export function interpolateMessage( + message: string, + values: TranslationValues = {}, + locale = "tr", +): string { + const pluralized = message.replace( + PLURAL_PATTERN, + (_match, key: string, one: string, other: string) => { + const count = Number(values[key] ?? 0); + const category = new Intl.PluralRules(toIntlLocale(locale)).select(count); + const template = category === "one" ? one : other; + return template.replaceAll("#", String(count)); + }, + ); + + return pluralized.replace(VALUE_PATTERN, (_match, key: string) => { + const value = values[key]; + if (value === null || value === undefined) return ""; + if (value instanceof Date) return formatDate(value, locale); + return String(value); + }); +} + +export function formatDate( + value: Date | string | number, + locale: string, + options: Intl.DateTimeFormatOptions = { day: "2-digit", month: "long", year: "numeric" }, +): string { + return new Intl.DateTimeFormat(toIntlLocale(locale), options).format(new Date(value)); +} + +export function formatDateTime( + value: Date | string | number, + locale: string, + options: Intl.DateTimeFormatOptions = { + day: "2-digit", + month: "long", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + }, +): string { + return new Intl.DateTimeFormat(toIntlLocale(locale), options).format(new Date(value)); +} + +export function formatNumber( + value: number, + locale: string, + options?: Intl.NumberFormatOptions, +): string { + return new Intl.NumberFormat(toIntlLocale(locale), options).format(value); +} + +export function formatMoney( + amountMinor: number, + currency: string, + locale: string, +): string { + return new Intl.NumberFormat(toIntlLocale(locale), { + style: "currency", + currency, + }).format(amountMinor / 100); +} + +export function toIntlLocale(locale: string): string { + return locale === "tr" ? "tr-TR" : locale === "en" ? "en-US" : locale; +} diff --git a/lib/i18n/index.ts b/lib/i18n/index.ts new file mode 100644 index 0000000..c656bc8 --- /dev/null +++ b/lib/i18n/index.ts @@ -0,0 +1,4 @@ +export * from "./catalog"; +export * from "./format"; +export * from "./translator"; +export * from "./types"; diff --git a/lib/i18n/translator.ts b/lib/i18n/translator.ts new file mode 100644 index 0000000..e1c0674 --- /dev/null +++ b/lib/i18n/translator.ts @@ -0,0 +1,22 @@ +import { interpolateMessage } from "./format"; +import type { TranslationValues } from "./types"; + +export type Translator = { + locale: string; + messages: Record; + t: (key: string, values?: TranslationValues) => string; +}; + +export function createTranslatorFromMessages( + locale: string, + messages: Record, +): Translator { + return { + locale, + messages, + t(key, values) { + const message = messages[key] ?? key; + return interpolateMessage(message, values, locale); + }, + }; +} diff --git a/lib/i18n/types.ts b/lib/i18n/types.ts new file mode 100644 index 0000000..4a9544c --- /dev/null +++ b/lib/i18n/types.ts @@ -0,0 +1,34 @@ +export const I18N_NAMESPACES = [ + "common", + "auth", + "navigation", + "dashboard", + "clients", + "projects", + "tasks", + "calendar", + "finance", + "journal", + "chat", + "settings", + "portal", + "status", + "validation", + "api", +] as const; + +export type I18nNamespace = (typeof I18N_NAMESPACES)[number]; +export type LocaleCode = string; +export type TextDirection = "ltr" | "rtl"; +export type TranslationValues = Record; +export type TranslationCatalog = Record>; + +export type LocaleDescriptor = { + code: LocaleCode; + name: string; + nativeName: string; + status: "draft" | "active" | "archived" | "test"; + fallbackLocale: LocaleCode | null; + textDirection: TextDirection; + builtIn: boolean; +}; diff --git a/locales/en/common.ts b/locales/en/common.ts new file mode 100644 index 0000000..1cc9ae1 --- /dev/null +++ b/locales/en/common.ts @@ -0,0 +1,15 @@ +export const common = { + "actions.save": "Save", + "actions.cancel": "Cancel", + "actions.delete": "Delete", + "actions.edit": "Edit", + "actions.create": "Create", + "actions.close": "Close", + "actions.search": "Search", + "states.loading": "Loading...", + "states.empty": "No records yet.", + "states.error": "Something went wrong.", + "pagination.previous": "Previous", + "pagination.next": "Next", + "plural.item": "{count, plural, one {# item} other {# items}}", +}; diff --git a/locales/en/index.ts b/locales/en/index.ts new file mode 100644 index 0000000..6d56e31 --- /dev/null +++ b/locales/en/index.ts @@ -0,0 +1,244 @@ +import type { TranslationCatalog } from "@/lib/i18n/types"; +import { common } from "./common"; + +export const enCatalog = { + common: { + ...common, + "notFound.title": "Page not found", + "notFound.description": "The page you are looking for may have moved, been deleted or never existed.", + "notFound.backHome": "Go home", + "error.title": "Something went wrong", + "error.description": "An unexpected error occurred. Please try again.", + "error.retry": "Try again", + "maintenance.title": "Maintenance mode", + "maintenance.description": "This workspace is briefly under maintenance. Please try again soon.", + "itemsCount": "{count, plural, one {# item} other {# items}}", + }, + auth: { + "login.title": "Sign in", + "login.description": "Sign in to access your Neta workspace.", + "login.email": "Email", + "login.emailPlaceholder": "example@mail.com", + "login.password": "Password", + "login.forgotPassword": "Forgot password", + "login.submit": "Sign in", + "login.pending": "Signing in...", + "login.setupPrompt": "Haven't completed first setup?", + "login.createAdmin": "Create admin account", + "register.title": "Create admin account", + "register.firstAdminTitle": "Create the first admin account", + "register.description": "Create the first admin account for this Neta workspace.", + "register.submit": "Create admin account", + "register.pending": "Creating...", + "register.hasAccount": "Already have an account?", + "register.closed": "Registration is closed. The first admin account has already been created for this Neta instance.", + "register.failed": "Could not create user.", + "forgot.title": "Password reset", + "forgot.description": "This flow becomes active when an email provider is connected in self-hosted deployments.", + "forgot.helper": "For now, set a new password from the admin side or manage the user directly in the database.", + "forgot.back": "Back to sign in", + "reset.title": "Set a new password", + "reset.description": "Password reset link integration has not been configured yet.", + "invite.title": "Accept portal invitation", + "invite.description": "Set your name and password for the invited account.", + "invite.email": "Email", + "invite.displayName": "Full name", + "invite.password": "Password", + "invite.passwordHelp": "Use at least 8 characters.", + "invite.submit": "Create portal account", + "invite.pending": "Creating account...", + "invite.backToLogin": "Back to sign in", + "invite.expired": "This invitation has expired. Ask the freelancer for a new link.", + "invite.accepted": "This invitation was already used. You can sign in with your account.", + "invite.revoked": "This invitation was revoked. Ask the freelancer for a new link.", + "invite.success": "Your portal account has been created. You can sign in now.", + "language": "Language", + "marketing.headline": "Manage freelance work, clients and finance in one place.", + "marketing.description": "{app} is designed to help you track daily operations, projects, side projects and core finance with simple reports.", + "marketing.openSource": "Open source and self-hostable.", + "marketing.github": "GitHub", + "marketing.via": "is where you can find it.", + "marketing.builtBy": "built by", + "highlights.clients": "Clients", + "highlights.calendar": "Calendar", + "highlights.finance": "Finance", + "highlights.reports": "Reports", + "messages.invalidCredentials": "Email or password is incorrect.", + "messages.setupUnavailable": "Registration is closed. The first freelancer account has already been created for this Neta instance.", + "messages.setupStateError": "Could not read setup state.", + "messages.signupFailed": "Could not create user.", + "messages.portalInviteFailed": "Could not create portal account.", + }, + navigation: { + "groups.overview": "OVERVIEW", + "groups.operations": "OPERATIONS", + "groups.personal": "PERSONAL", + "groups.ai": "AI ASSISTANT", + "groups.processes": "PROCESSES", + "items.dashboard": "Dashboard", + "items.calendar": "Calendar", + "items.analytics": "Analytics", + "items.clients": "Clients", + "items.projects": "Projects", + "items.tasks": "Tasks", + "items.finance": "Finance", + "items.journal": "Journal", + "items.chat": "Chat", + "items.settings": "Settings", + "items.portalProjects": "Your projects", + "items.portalTasks": "Completed tasks", + "items.portalRevisions": "Revision requests", + "shell.skipToContent": "Skip to main content", + "shell.homeAriaLabel": "{app} home", + "shell.mobileMenuAriaLabel": "Open or close main menu", + "shell.mobileMenuTooltip": "Menu", + "shell.logoAlt": "{app} logo", + "shell.progressTitle": "Project progress", + "shell.progressValue": "{progress}% complete", + "shell.progressAriaLabel": "Project progress", + "shell.accountMenuAriaLabel": "Open account menu for {name}", + "account.signOut": "Sign out", + "account.signingOut": "Signing out", + "account.signOutError": "Could not sign out. Please try again.", + }, + dashboard: { + "title": "Dashboard", + "description": "Track your work performance, revenue and daily status.", + "stats.netEarnings": "Net earnings", + "stats.activeProjects": "Active projects", + "stats.completedTasks": "Completed tasks", + "stats.averageMood": "Average mood", + "sections.financeSummary": "Income / expense summary", + "sections.moodTrend": "Mood & energy trend", + "sections.recentProjects": "Recent projects", + "sections.recentClients": "Recent clients", + "empty.finance": "No financial data in this date range.", + "empty.journal": "No journal data in this date range.", + "empty.projects": "No projects yet.", + "empty.clients": "No clients yet.", + "filters.range": "Date range", + "filters.today": "Today", + "filters.thisWeek": "This week", + "filters.thisMonth": "This month", + }, + clients: { + "title": "Clients", + "description": "Manage client relationships, projects and follow-ups.", + "actions.add": "Add client", + "fields.name": "Client name", + "fields.email": "Email", + }, + projects: { + "title": "Projects", + "description": "Track project status, deadlines and client relationships.", + "actions.add": "Add project", + "actions.risk": "AI risk analysis", + "stats.active": "Active projects", + "stats.progress": "Average progress", + "stats.budget": "Total budget", + "fields.name": "Project name", + "fields.description": "Description", + }, + tasks: { + "title": "Tasks", + "description": "Manage work items, priorities and project relationships.", + "actions.add": "Add task", + "view.kanban": "Kanban", + "view.list": "List", + "fields.title": "Task title", + "fields.description": "Description", + }, + calendar: { + "title": "Calendar", + "description": "Plan meetings, focus time and deadlines.", + "actions.add": "Add event", + "event.title": "Event", + }, + finance: { + "title": "Finance", + "description": "Track income, expenses, payment status and project/client links.", + "actions.add": "Add transaction", + "actions.ai": "AI analysis", + }, + journal: { + "title": "Journal", + "description": "Track mood, energy and work notes.", + "actions.add": "Add journal entry", + "fields.note": "Note", + }, + chat: { + "title": "Chat", + "description": "Have contextual AI assistant conversations with your work data.", + "actions.new": "New chat", + "errors.invalid": "Chat request is invalid.", + }, + settings: { + "title": "Settings", + "language.title": "Languages and translations", + "language.default": "Default language", + }, + portal: { + "dashboard.title": "Client dashboard", + "dashboard.activeProjects": "Active projects", + "dashboard.completed": "Completed", + "dashboard.averageProgress": "Average progress", + "dashboard.allProjects": "All your projects", + "projects.title": "Your projects", + "projects.empty": "No projects have been assigned to you yet.", + "tasks.title": "Completed tasks", + "tasks.empty": "No shared tasks yet.", + "revisions.title": "Revision requests", + "revisions.mine": "My revision requests", + "revisions.empty": "You have not created a revision request yet.", + "actions.requestRevision": "Request revision", + "actions.noRevisionQuota": "No revision quota left", + "actions.sendRequest": "Send request", + "actions.cancel": "Cancel", + "actions.newRequest": "Create new request", + "labels.delivery": "Delivery", + "labels.deadline": "Due", + "labels.progress": "Progress", + "labels.project": "Project", + "labels.remainingQuota": "Remaining quota", + "labels.unlimited": "Unlimited", + "tabs.overview": "Overview", + "tabs.plan": "Plan & stages", + "tabs.revisions": "Revisions", + "sections.progress": "Progress", + "sections.doneTasks": "Completed work", + "empty.tasks": "No tasks to list.", + "empty.plan": "No plan has been uploaded yet.", + "empty.revisions": "You have not created a revision request yet.", + "revision.title": "New revision request", + "revision.pendingWarning": "You currently have {count} unresolved revision requests. Are you sure you want to add another?", + "revision.descriptionLabel": "Describe the changes you would like in detail", + "revision.descriptionPlaceholder": "Could this section be blue? Also, let's update the copy...", + "revision.success": "Your revision request has been sent.", + "revision.error": "Could not create revision request.", + "status.project.active": "Active", + "status.project.completed": "Completed", + "status.project.waiting": "Waiting", + "status.task.todo": "Waiting", + "status.task.inProgress": "In progress", + "status.task.done": "Completed", + "status.revision.pending": "Waiting", + "status.revision.inProgress": "In progress", + "status.revision.completed": "Completed", + "status.revision.rejected": "Rejected", + }, + status: { + "project.active": "Active", + "project.completed": "Completed", + "task.todo": "To do", + "task.done": "Done", + }, + validation: { + "required": "This field is required.", + "invalidLocale": "Locale code is invalid.", + "unsupportedLocale": "This language is not supported.", + }, + api: { + "errors.unauthenticated": "A valid session is required.", + "errors.forbidden": "You are not allowed to perform this action.", + }, +} as const satisfies TranslationCatalog; diff --git a/locales/tr/common.ts b/locales/tr/common.ts new file mode 100644 index 0000000..da4ad4f --- /dev/null +++ b/locales/tr/common.ts @@ -0,0 +1,15 @@ +export const common = { + "actions.save": "Kaydet", + "actions.cancel": "İptal", + "actions.delete": "Sil", + "actions.edit": "Düzenle", + "actions.create": "Oluştur", + "actions.close": "Kapat", + "actions.search": "Ara", + "states.loading": "Yükleniyor...", + "states.empty": "Henüz kayıt yok.", + "states.error": "Bir hata oluştu.", + "pagination.previous": "Önceki", + "pagination.next": "Sonraki", + "plural.item": "{count, plural, one {# kayıt} other {# kayıt}}", +}; diff --git a/locales/tr/index.ts b/locales/tr/index.ts new file mode 100644 index 0000000..2f9a425 --- /dev/null +++ b/locales/tr/index.ts @@ -0,0 +1,244 @@ +import type { TranslationCatalog } from "@/lib/i18n/types"; +import { common } from "./common"; + +export const trCatalog = { + common: { + ...common, + "notFound.title": "Sayfa bulunamadı", + "notFound.description": "Aradığın sayfa taşınmış, silinmiş veya hiç var olmamış olabilir.", + "notFound.backHome": "Ana sayfaya dön", + "error.title": "Bir şeyler ters gitti", + "error.description": "Beklenmeyen bir hata oluştu. Lütfen tekrar dene.", + "error.retry": "Tekrar dene", + "maintenance.title": "Bakım modu", + "maintenance.description": "Bu çalışma alanı kısa süreli bakımda. Lütfen birazdan tekrar dene.", + "itemsCount": "{count, plural, one {# öğe} other {# öğe}}", + }, + auth: { + "login.title": "Giriş yap", + "login.description": "Neta çalışma alanına erişmek için hesabına giriş yap.", + "login.email": "E-posta", + "login.emailPlaceholder": "ornek@mail.com", + "login.password": "Şifre", + "login.forgotPassword": "Şifremi unuttum", + "login.submit": "Giriş yap", + "login.pending": "Giriş yapılıyor...", + "login.setupPrompt": "İlk kurulumu yapmadın mı?", + "login.createAdmin": "Admin hesabını oluştur", + "register.title": "Admin hesabı oluştur", + "register.firstAdminTitle": "İlk admin hesabını oluştur", + "register.description": "Bu Neta çalışma alanının ilk yönetici hesabını oluştur.", + "register.submit": "Admin hesabını oluştur", + "register.pending": "Oluşturuluyor...", + "register.hasAccount": "Zaten hesabın var mı?", + "register.closed": "Kayıt kapalı. Bu Neta kurulumunda ilk admin hesabı zaten oluşturulmuş.", + "register.failed": "Kullanıcı oluşturulamadı.", + "forgot.title": "Şifre sıfırlama", + "forgot.description": "Self-host kurulumda e-posta sağlayıcısı bağlandığında bu akış aktif edilir.", + "forgot.helper": "Şimdilik admin panelinden yeni şifre belirleyebilir veya veritabanındaki kullanıcıyı yönetebilirsin.", + "forgot.back": "Giriş sayfasına dön", + "reset.title": "Yeni şifre belirle", + "reset.description": "Şifre sıfırlama bağlantısı entegrasyonu henüz yapılandırılmadı.", + "invite.title": "Portal davetini kabul et", + "invite.description": "Davet edilen hesabın için adını ve şifreni belirle.", + "invite.email": "E-posta", + "invite.displayName": "Ad soyad", + "invite.password": "Şifre", + "invite.passwordHelp": "En az 8 karakter kullan.", + "invite.submit": "Portal hesabını oluştur", + "invite.pending": "Hesap oluşturuluyor...", + "invite.backToLogin": "Giriş sayfasına dön", + "invite.expired": "Bu davetin süresi dolmuş. Freelancer'dan yeni bir bağlantı istemelisin.", + "invite.accepted": "Bu davet daha önce kullanılmış. Hesabınla giriş yapabilirsin.", + "invite.revoked": "Bu davet iptal edilmiş. Freelancer'dan yeni bir bağlantı istemelisin.", + "invite.success": "Portal hesabın oluşturuldu. Şimdi giriş yapabilirsin.", + "language": "Dil", + "marketing.headline": "Freelancer işlerini, müşterilerini ve finansını tek yerde yönet.", + "marketing.description": "{app}, günlük operasyonunu, projelerini, side projectlerini ve temel finans durumunu sade raporlarla takip etmen için tasarlanır.", + "marketing.openSource": "Açık kaynak ve self-host edilebilir.", + "marketing.github": "GitHub", + "marketing.via": "üzerinden ulaşabilirsin.", + "marketing.builtBy": "tarafından kodlandı.", + "highlights.clients": "Müşteriler", + "highlights.calendar": "Takvim", + "highlights.finance": "Finans", + "highlights.reports": "Raporlar", + "messages.invalidCredentials": "E-posta veya şifre hatalı.", + "messages.setupUnavailable": "Kayıt kapalı. Bu Neta kurulumunda ilk freelancer hesabı zaten oluşturulmuş.", + "messages.setupStateError": "Kurulum durumu okunamadı.", + "messages.signupFailed": "Kullanıcı oluşturulamadı.", + "messages.portalInviteFailed": "Portal hesabı oluşturulamadı.", + }, + navigation: { + "groups.overview": "GENEL BAKIŞ", + "groups.operations": "OPERASYON", + "groups.personal": "KİŞİSEL", + "groups.ai": "AI ASİSTAN", + "groups.processes": "SÜREÇLER", + "items.dashboard": "Dashboard", + "items.calendar": "Takvim", + "items.analytics": "Analizler", + "items.clients": "Müşteriler", + "items.projects": "Projeler", + "items.tasks": "Görevler", + "items.finance": "Finans", + "items.journal": "Günlük", + "items.chat": "Sohbet", + "items.settings": "Ayarlar", + "items.portalProjects": "Projeleriniz", + "items.portalTasks": "Yapılan Görevler", + "items.portalRevisions": "Revizyon Talepleri", + "shell.skipToContent": "Ana içeriğe geç", + "shell.homeAriaLabel": "{app} ana sayfa", + "shell.mobileMenuAriaLabel": "Ana menüyü aç veya kapat", + "shell.mobileMenuTooltip": "Menü", + "shell.logoAlt": "{app} logosu", + "shell.progressTitle": "Proje ilerlemesi", + "shell.progressValue": "%{progress} tamamlandı", + "shell.progressAriaLabel": "Proje ilerlemesi", + "shell.accountMenuAriaLabel": "{name} için hesap menüsünü aç", + "account.signOut": "Çıkış yap", + "account.signingOut": "Çıkış yapılıyor", + "account.signOutError": "Çıkış yapılamadı. Lütfen tekrar deneyin.", + }, + dashboard: { + "title": "Dashboard", + "description": "İş performansını, gelirlerini ve günlük durumunu takip et.", + "stats.netEarnings": "Net Kazanç", + "stats.activeProjects": "Aktif Projeler", + "stats.completedTasks": "Tamamlanan Görev", + "stats.averageMood": "Ortalama Mood", + "sections.financeSummary": "Gelir / Gider Özeti", + "sections.moodTrend": "Mood & Enerji Trendi", + "sections.recentProjects": "Son Eklenen Projeler", + "sections.recentClients": "Son Eklenen Müşteriler", + "empty.finance": "Bu tarih aralığında finansal veri yok.", + "empty.journal": "Bu tarih aralığında günlük verisi yok.", + "empty.projects": "Henüz proje yok.", + "empty.clients": "Henüz müşteri yok.", + "filters.range": "Tarih aralığı", + "filters.today": "Bugün", + "filters.thisWeek": "Bu Hafta", + "filters.thisMonth": "Bu Ay", + }, + clients: { + "title": "Müşteriler", + "description": "Müşteri ilişkilerini, projeleri ve takipleri yönetin.", + "actions.add": "Müşteri ekle", + "fields.name": "Müşteri adı", + "fields.email": "E-posta", + }, + projects: { + "title": "Projeler", + "description": "Proje durumlarını, teslim tarihlerini ve müşteri bağlantılarını takip edin.", + "actions.add": "Proje ekle", + "actions.risk": "AI Risk Analizi", + "stats.active": "Aktif proje", + "stats.progress": "Ortalama ilerleme", + "stats.budget": "Toplam bütçe", + "fields.name": "Proje adı", + "fields.description": "Açıklama", + }, + tasks: { + "title": "Görevler", + "description": "Yapılacak işleri, öncelikleri ve proje bağlantılarını yönetin.", + "actions.add": "Görev ekle", + "view.kanban": "Kanban", + "view.list": "Liste", + "fields.title": "Görev başlığı", + "fields.description": "Açıklama", + }, + calendar: { + "title": "Takvim", + "description": "Toplantı, odak zamanı ve teslim tarihlerini planlayın.", + "actions.add": "Etkinlik ekle", + "event.title": "Etkinlik", + }, + finance: { + "title": "Finans İşlemleri", + "description": "Gelir, gider, ödeme durumu ve proje/müşteri bağlantılarını takip et.", + "actions.add": "İşlem ekle", + "actions.ai": "AI Analizi", + }, + journal: { + "title": "Günlük", + "description": "Mood, enerji ve çalışma notlarını takip edin.", + "actions.add": "Günlük ekle", + "fields.note": "Not", + }, + chat: { + "title": "Sohbet", + "description": "İş verilerinle bağlamlı AI asistan görüşmeleri yap.", + "actions.new": "Yeni sohbet", + "errors.invalid": "Sohbet isteği geçersiz.", + }, + settings: { + "title": "Ayarlar", + "language.title": "Diller ve çeviriler", + "language.default": "Varsayılan dil", + }, + portal: { + "dashboard.title": "Müşteri Paneli", + "dashboard.activeProjects": "Aktif Projeler", + "dashboard.completed": "Tamamlanan", + "dashboard.averageProgress": "Ortalama İlerleme", + "dashboard.allProjects": "Tüm Projeleriniz", + "projects.title": "Projeleriniz", + "projects.empty": "Henüz size atanmış bir proje bulunmuyor.", + "tasks.title": "Yapılan Görevler", + "tasks.empty": "Henüz sizinle paylaşılan bir görev bulunmuyor.", + "revisions.title": "Revizyon Talepleri", + "revisions.mine": "Revizyon Taleplerim", + "revisions.empty": "Henüz bir revizyon talebinde bulunmadınız.", + "actions.requestRevision": "Revizyon Talep Et", + "actions.noRevisionQuota": "Revizyon Hakkı Bitti", + "actions.sendRequest": "Talebi Gönder", + "actions.cancel": "İptal", + "actions.newRequest": "Yeni Talep Oluştur", + "labels.delivery": "Teslim", + "labels.deadline": "Son Teslim", + "labels.progress": "İlerleme", + "labels.project": "Proje", + "labels.remainingQuota": "Kalan Hak", + "labels.unlimited": "Sınırsız", + "tabs.overview": "Genel Bakış", + "tabs.plan": "Plan & Aşamalar", + "tabs.revisions": "Revizyonlar", + "sections.progress": "İlerleme Durumu", + "sections.doneTasks": "Yapılan İşler", + "empty.tasks": "Listelenecek görev bulunmuyor.", + "empty.plan": "Henüz bir plan yüklenmemiş.", + "empty.revisions": "Henüz bir revizyon talebi oluşturmadınız.", + "revision.title": "Yeni Revizyon Talebi", + "revision.pendingWarning": "Şu anda sonuçlanmamış {count} adet revizyon talebiniz var. Yeni bir tane eklemek istediğinize emin misiniz?", + "revision.descriptionLabel": "Lütfen yapılmasını istediğiniz değişiklikleri detaylıca açıklayın", + "revision.descriptionPlaceholder": "Şu kısmın rengi mavi olabilir mi? Ayrıca metinleri güncelleyelim...", + "revision.success": "Revizyon talebiniz başarıyla iletildi.", + "revision.error": "Revizyon talebi oluşturulamadı.", + "status.project.active": "Aktif", + "status.project.completed": "Tamamlandı", + "status.project.waiting": "Beklemede", + "status.task.todo": "Bekliyor", + "status.task.inProgress": "İşleniyor", + "status.task.done": "Tamamlandı", + "status.revision.pending": "Bekliyor", + "status.revision.inProgress": "İşleniyor", + "status.revision.completed": "Tamamlandı", + "status.revision.rejected": "Reddedildi", + }, + status: { + "project.active": "Aktif", + "project.completed": "Tamamlandı", + "task.todo": "Yapılacak", + "task.done": "Tamamlandı", + }, + validation: { + "required": "Bu alan zorunludur.", + "invalidLocale": "Dil kodu geçersiz.", + "unsupportedLocale": "Bu dil desteklenmiyor.", + }, + api: { + "errors.unauthenticated": "Geçerli bir oturum gerekli.", + "errors.forbidden": "Bu işlem için yetkiniz yok.", + }, +} as const satisfies TranslationCatalog;