feat: add core i18n runtime and catalogs

This commit is contained in:
poyrazavsever
2026-07-19 03:06:38 +03:00
parent 0548820584
commit f4a2342ac4
12 changed files with 776 additions and 0 deletions
+10
View File
@@ -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());
}
+53
View File
@@ -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<string, TranslationCatalog> = {
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<TranslationCatalog> {
return Object.fromEntries(
namespaces.map((namespace) => [namespace, catalog[namespace] ?? {}]),
) as Partial<TranslationCatalog>;
}
export function flattenCatalog(
catalog: Partial<TranslationCatalog>,
namespaces: readonly I18nNamespace[],
): Record<string, string> {
const entries: Record<string, string> = {};
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(),
};
}
+50
View File
@@ -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<string, Record<string, string | null>>;
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<ContentTranslationEntityType, ContentTranslationField[]>;
export function contentInputName(locale: string, field: string) {
return `i18n.${locale}.${field}`;
}
+13
View File
@@ -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");
}
+72
View File
@@ -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;
}
+4
View File
@@ -0,0 +1,4 @@
export * from "./catalog";
export * from "./format";
export * from "./translator";
export * from "./types";
+22
View File
@@ -0,0 +1,22 @@
import { interpolateMessage } from "./format";
import type { TranslationValues } from "./types";
export type Translator = {
locale: string;
messages: Record<string, string>;
t: (key: string, values?: TranslationValues) => string;
};
export function createTranslatorFromMessages(
locale: string,
messages: Record<string, string>,
): Translator {
return {
locale,
messages,
t(key, values) {
const message = messages[key] ?? key;
return interpolateMessage(message, values, locale);
},
};
}
+34
View File
@@ -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<string, string | number | boolean | Date | null | undefined>;
export type TranslationCatalog = Record<I18nNamespace, Record<string, string>>;
export type LocaleDescriptor = {
code: LocaleCode;
name: string;
nativeName: string;
status: "draft" | "active" | "archived" | "test";
fallbackLocale: LocaleCode | null;
textDirection: TextDirection;
builtIn: boolean;
};
+15
View File
@@ -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}}",
};
+244
View File
@@ -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;
+15
View File
@@ -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}}",
};
+244
View File
@@ -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;