feat: add core i18n runtime and catalogs
This commit is contained in:
@@ -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());
|
||||
}
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
@@ -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}`;
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./catalog";
|
||||
export * from "./format";
|
||||
export * from "./translator";
|
||||
export * from "./types";
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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}}",
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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}}",
|
||||
};
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user