From 5082bfb1766d00a4fefa4f042df27c8b129f6d5d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?YAL=C3=87IN=20G=C3=96RMEZ?=
<3812908+GormYa@users.noreply.github.com>
Date: Thu, 27 Aug 2026 20:56:24 +0300
Subject: [PATCH 1/4] fix(store): preserve user profile on upsert
Ensure UpsertUser does not overwrite existing name and avatar_url with empty values on conflict (such as during OTP login or Stripe sync). Also default missing user role to 'user' on insert.
---
internal/store/store.go | 16 ++++++++++++----
1 file changed, 12 insertions(+), 4 deletions(-)
diff --git a/internal/store/store.go b/internal/store/store.go
index 612a816..0c4d008 100644
--- a/internal/store/store.go
+++ b/internal/store/store.go
@@ -204,10 +204,18 @@ func (s *Store) UpsertUser(ctx context.Context, u *model.User) error {
if u.ID == "" {
u.ID = newID()
}
- _, err := s.DB.NewInsert().Model(u).
- On("CONFLICT (email) DO UPDATE").
- Set("name = EXCLUDED.name, avatar_url = EXCLUDED.avatar_url, updated_at = now()").
- Exec(ctx)
+ role := u.Role
+ if role == "" {
+ role = model.RoleUser
+ }
+ _, err := s.DB.NewRaw(`
+ INSERT INTO users (id, email, name, avatar_url, role, created_at, updated_at)
+ VALUES (?, ?, ?, ?, ?, now(), now())
+ ON CONFLICT (email) DO UPDATE SET
+ name = CASE WHEN EXCLUDED.name != '' THEN EXCLUDED.name ELSE users.name END,
+ avatar_url = CASE WHEN EXCLUDED.avatar_url != '' THEN EXCLUDED.avatar_url ELSE users.avatar_url END,
+ updated_at = now()
+ `, u.ID, u.Email, u.Name, u.AvatarURL, role).Exec(ctx)
return err
}
From 1f8fbf6e47f1bbf6926fb409574b7167d3f6708b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?YAL=C3=87IN=20G=C3=96RMEZ?=
<3812908+GormYa@users.noreply.github.com>
Date: Thu, 27 Aug 2026 22:00:00 +0300
Subject: [PATCH 2/4] feat(i18n): add Turkish localization support
Add full Turkish (tr) localization across all frontend pages and wire up i18n support in the releases management interface.
- Create web/src/i18n/locales/tr.ts with 623 translation keys.
- Update web/src/i18n/index.tsx to register the Turkish locale and support browser language detection for Turkish.
- Add Turkish language selection to admin settings and persist it in backend configuration.
- Extract all hardcoded strings in the releases page into i18n keys across English, Turkish, and Chinese translation files.
- Update README.md to list Turkish under built-in i18n languages.
---
README.md | 2 +-
web/src/i18n/index.tsx | 6 +-
web/src/i18n/locales/en.ts | 114 +++++
web/src/i18n/locales/tr.ts | 713 +++++++++++++++++++++++++++++++
web/src/i18n/locales/zh.ts | 114 +++++
web/src/pages/admin/releases.tsx | 327 +++++++-------
web/src/pages/admin/settings.tsx | 42 +-
7 files changed, 1134 insertions(+), 184 deletions(-)
create mode 100644 web/src/i18n/locales/tr.ts
diff --git a/README.md b/README.md
index 2715607..e83ae32 100644
--- a/README.md
+++ b/README.md
@@ -86,7 +86,7 @@ Email OTP login with constant-time hash verification, role-based access checked
### 🌍 Self-Hosted
-Single Go binary + PostgreSQL + (optional) S3-compatible storage for release artifacts. No Redis, no microservices. Auto-migration on startup. Setup wizard for first run. Custom branding, email templates, and i18n (English/Chinese built-in).
+Single Go binary + PostgreSQL + (optional) S3-compatible storage for release artifacts. No Redis, no microservices. Auto-migration on startup. Setup wizard for first run. Custom branding, email templates, and i18n (English/Chinese/Turkish built-in).
diff --git a/web/src/i18n/index.tsx b/web/src/i18n/index.tsx
index b18b9e6..7393d9c 100644
--- a/web/src/i18n/index.tsx
+++ b/web/src/i18n/index.tsx
@@ -1,12 +1,13 @@
import { createContext, type ReactNode, useCallback, useContext, useEffect, useState } from "react"
import en from "./locales/en"
+import tr from "./locales/tr"
import zh from "./locales/zh"
-type Locale = "en" | "zh"
+type Locale = "en" | "zh" | "tr"
type TranslationKeys = keyof typeof en
type Translations = Record
-const locales: Record = { en, zh }
+const locales: Record = { en, zh, tr }
interface I18nContextType {
locale: Locale
@@ -24,6 +25,7 @@ export function I18nProvider({ children }: { children: ReactNode }) {
if (saved && locales[saved]) return saved
const browserLang = navigator.language.toLowerCase()
if (browserLang.startsWith("zh")) return "zh"
+ if (browserLang.startsWith("tr")) return "tr"
return "en"
})
diff --git a/web/src/i18n/locales/en.ts b/web/src/i18n/locales/en.ts
index 1ea490e..7024865 100644
--- a/web/src/i18n/locales/en.ts
+++ b/web/src/i18n/locales/en.ts
@@ -594,6 +594,120 @@ const en = {
"error.unexpected": "An unexpected error occurred.",
"error.tryAgain": "Try Again",
"error.reload": "Reload Page",
+
+ // Releases
+ "releases.title": "Releases",
+ "releases.subtitle": "Distribute software updates to your customers via Sparkle, Velopack, or Tauri.",
+ "releases.signingKeys": "Signing keys",
+ "releases.new": "New release",
+ "releases.noEligibleProducts": "No release-eligible products",
+ "releases.noEligibleProductsDesc":
+ "Release feeds are available for desktop and hybrid products only. Your existing products are all SaaS — change a product's type or create a new desktop/hybrid one.",
+ "releases.noProducts": "No products yet",
+ "releases.noProductsDesc": "Create a product before publishing releases.",
+ "releases.manageProducts": "Manage products",
+ "releases.createProduct": "Create product",
+ "releases.allChannels": "All channels",
+ "releases.allStatuses": "All statuses",
+ "releases.version": "Version",
+ "releases.channel": "Channel",
+ "releases.platforms": "Platforms",
+ "releases.empty": 'No releases yet. Click "New release" to start.',
+ "releases.belowLatest": "below latest",
+ "releases.platformsCount": "{count} platform",
+ "releases.platformsCountPlural": "{count} platforms",
+ "releases.openDetail": "Open release detail",
+ "releases.viewManageArtifacts": "View / manage artifacts",
+ "releases.publish": "Publish",
+ "releases.awaitingArtifacts": "Awaiting artifacts ({ready}/{total} ready)",
+ "releases.yank": "Yank",
+ "releases.unyank": "Unyank",
+ "releases.deleteDraft": "Delete draft",
+ "releases.deleteYankInstead": "Delete (yank instead)",
+ "releases.statusDraft": "draft",
+ "releases.statusPublished": "published",
+ "releases.statusYanked": "yanked",
+ "releases.statusReady": "ready",
+ "releases.statusPending": "pending",
+ "releases.unyankTitle": "Unyank v{version}?",
+ "releases.unyankDesc":
+ "This restores the release to the public update feed. SDK clients on the affected channel will start receiving v{version} as a valid update again.",
+ "releases.deleteDraftTitle": "Delete draft release?",
+ "releases.deleteDraftDesc":
+ "Permanently removes the draft and its uploaded artifacts. Published or yanked releases cannot be deleted.",
+ "releases.publishBelowLatestTitle": "Publish a version below current latest?",
+ "releases.publishBelowLatestDesc":
+ "Publishing {version}, older than {latest}. Velopack and Tauri reject downgrades by default; Sparkle's standard comparator is not SemVer-aware. This release will appear in your history out of order — appropriate for backports.",
+ "releases.publishAnyway": "Publish anyway",
+ "releases.createTitle": "New release",
+ "releases.createDesc":
+ "Create the release record. You'll add platform-specific binaries (artifacts) in the next step.",
+ "releases.displayNameOptional": "Display name (optional)",
+ "releases.displayNamePlaceholder": "MyApp Pro",
+ "releases.releaseNotesOptional": "Release notes (optional, markdown)",
+ "releases.releaseNotesPlaceholder": "What's new in this version...",
+ "releases.createDraftBtn": "Create draft",
+ "releases.creating": "Creating...",
+ "releases.draftCreatedToast": "Draft {version} created. Add artifacts to publish.",
+ "releases.detailDraftDesc": "Add platform binaries below. Publish when ready.",
+ "releases.detailNonDraftDesc": "This release is {status}. Artifacts cannot be modified.",
+ "releases.artifactsHeading": "Artifacts ({count})",
+ "releases.noArtifacts": "No artifacts yet — add at least one platform before publishing.",
+ "releases.addArtifactRemaining": "Add artifact ({count} platforms remaining)",
+ "releases.notUploadedYet": "Not uploaded yet",
+ "releases.addArtifactTitle": "Add artifact",
+ "releases.addArtifactDesc":
+ "Upload a platform binary for {version}. The file goes directly to storage; we sign + finalize on publish.",
+ "releases.platform": "Platform",
+ "releases.artifactFile": "Artifact file",
+ "releases.platformAndFileRequired": "Platform and file are required",
+ "releases.uploadFailed": "Upload failed: {status} {statusText}",
+ "releases.reservingSlot": "Reserving artifact slot...",
+ "releases.uploadingToStorage": "Uploading to storage...",
+ "releases.computingSha": "Computing SHA-256 + finalizing...",
+ "releases.artifactUploadedToast": "Artifact for {platform} uploaded",
+ "releases.upload": "Upload",
+ "releases.working": "Working...",
+ "releases.yankTitle": "Yank {version}?",
+ "releases.yankDesc":
+ "Yanking removes this release (and ALL its artifacts) from update feeds. Existing installs continue working. Provide a reason — recorded in the audit log.",
+ "releases.reason": "Reason",
+ "releases.yankReasonPlaceholder": "Critical bug in v1.2.3 affecting Windows users; rollback recommended.",
+ "releases.releaseYankedToast": "Release yanked",
+ "releases.releaseUnyankedToast": "Release unyanked",
+ "releases.releasePublishedToast": "Release published",
+ "releases.draftDeletedToast": "Draft deleted",
+ "releases.signingKeysTitle": "Release signing keys",
+ "releases.signingKeysDesc":
+ "Generate an Ed25519 keypair per product. The public key is embedded in your client app; the server signs every release artifact with the private key on publish.",
+ "releases.noActiveKey": "No active signing key",
+ "releases.noActiveKeyDesc": "Releases for this product will be unsigned until you generate a key.",
+ "releases.generateKey": "Generate signing key",
+ "releases.generating": "Generating...",
+ "releases.keyGeneratedToast": "Signing key generated",
+ "releases.activeSigningKey": "Active signing key",
+ "releases.createdDate": "Created {date}",
+ "releases.publicKeyLabel": "Public key (Ed25519, base64)",
+ "releases.publicKeyEmbedHint":
+ "Embed this in your client app's update verifier (Sparkle SUPublicEDKey in Info.plist, or Tauri pubkey).",
+ "releases.downloadPem": "Download .pem",
+ "releases.rotate": "Rotate",
+ "releases.deactivate": "Deactivate",
+ "releases.pastKeysHeading": "Past keys ({count})",
+ "releases.rotatedAt": "rotated {date}",
+ "releases.rotateTitle": "Rotate signing key?",
+ "releases.rotateDesc":
+ "New keypair generated. Old key is preserved in history but inactive. Existing installs with only the old public key embedded will fail to verify new releases.",
+ "releases.rotateReasonLabel": "Reason (audit log)",
+ "releases.rotateReasonPlaceholder": "Routine rotation; no key compromise.",
+ "releases.keyRotatedToast": "Signing key rotated",
+ "releases.rotating": "Rotating...",
+ "releases.deactivateTitle": "Deactivate signing key?",
+ "releases.deactivateDesc":
+ "Future releases will be UNSIGNED until you generate a new key. Sparkle / Tauri / Velopack clients with strict signature checking will reject unsigned updates.",
+ "releases.deactivateReasonPlaceholder": "Why are you deactivating?",
+ "releases.keyDeactivatedToast": "Signing key deactivated",
+ "releases.deactivating": "Deactivating...",
} as const
export default en
diff --git a/web/src/i18n/locales/tr.ts b/web/src/i18n/locales/tr.ts
new file mode 100644
index 0000000..385d29c
--- /dev/null
+++ b/web/src/i18n/locales/tr.ts
@@ -0,0 +1,713 @@
+const tr = {
+ // Ortak (Common)
+ "common.save": "Kaydet",
+ "common.cancel": "İptal",
+ "common.delete": "Sil",
+ "common.edit": "Düzenle",
+ "common.create": "Oluştur",
+ "common.search": "Ara",
+ "common.filter": "Filtrele",
+ "common.loading": "Yükleniyor...",
+ "common.noData": "Veri yok",
+ "common.confirm": "Onayla",
+ "common.optional": "isteğe bağlı",
+
+ // Davet kabul sayfası (Accept-invite landing page)
+ "acceptInvite.processing": "Davetiniz kabul ediliyor…",
+ "acceptInvite.missingToken": "Davet belirteci (token) eksik",
+ "acceptInvite.missingTokenDesc": "Bu bağlantı geçerli bir davet belirteci içermiyor. E-postanızdaki bağlantıyı tekrar açın.",
+ "acceptInvite.invalidTitle": "Davet geçersiz veya süresi dolmuş",
+ "acceptInvite.invalidDesc":
+ "Bu davet zaten kullanılmış, iptal edilmiş veya süresi dolmuş. Ekip yöneticinizden yeni bir davet göndermesini isteyin.",
+ "acceptInvite.acceptedTitle": "{product} uygulamasına hoş geldiniz",
+ "acceptInvite.acceptedDesc": "{role} rolüyle katıldınız. Lisansları ve kullanıcıları yönetmek için portala giriş yapın.",
+ "acceptInvite.openPortal": "Portalı aç",
+ "acceptInvite.goHome": "Ana sayfaya dön",
+ "common.actions": "İşlemler",
+ "common.status": "Durum",
+ "common.name": "Ad",
+ "common.email": "E-posta",
+ "common.created": "Oluşturulma",
+ "common.updated": "Güncellenme",
+ "common.type": "Tür",
+ "common.active": "Aktif",
+ "common.inactive": "Pasif",
+ "common.all": "Tümü",
+ "common.product": "Ürün",
+ "common.plan": "Plan",
+ "common.yes": "Evet",
+ "common.no": "Hayır",
+ "common.unlimited": "Sınırsız",
+ "common.unlimitedSymbol": "∞",
+
+ // Gezinme (Nav)
+ "nav.dashboard": "Kontrol Paneli",
+ "nav.products": "Ürünler",
+ "nav.plans": "Planlar",
+ "nav.releases": "Sürümler",
+ "nav.licenses": "Lisanslar",
+ "nav.apiKeys": "API Anahtarları",
+ "nav.webhooks": "Webhooks",
+ "nav.analytics": "Analitik",
+ "nav.audit": "Denetim Günlüğü",
+ "nav.catalog": "Katalog",
+ "nav.licensing": "Lisanslama",
+ "nav.developer": "Geliştirici",
+ "nav.insights": "İçgörüler",
+ "nav.customers": "Müşteriler",
+ "nav.settings": "Ayarlar",
+ "nav.sponsor": "Bize Destek Olun",
+ "nav.portal": "Kullanıcı Portalı",
+ "nav.logout": "Çıkış Yap",
+
+ // Kontrol Paneli (Dashboard)
+ "dashboard.title": "Kontrol Paneli",
+ "dashboard.totalLicenses": "Toplam Lisans",
+ "dashboard.activeLicenses": "Aktif Lisans",
+ "dashboard.totalActivations": "Toplam Etkinleştirme",
+ "dashboard.products": "Ürünler",
+ "dashboard.subtitle": "Lisans yönetim sisteminize genel bakış.",
+ "dashboard.statusDistribution": "Lisans Durumu Dağılımı",
+ "dashboard.recentLicenses": "Son Lisanslar",
+ "dashboard.updateAvailable": "Keygate {version} sürümü mevcut.",
+ "dashboard.viewUpdate": "Ayrıntıları Görüntüle",
+ "dashboard.releaseNotes": "Sürüm Notları",
+
+ // Ürünler (Products)
+ "products.title": "Ürünler",
+ "products.subtitle": "Yazılım ürünlerinizi yönetin.",
+ "products.new": "Yeni Ürün",
+ "products.slug": "Kısa Ad (Slug)",
+ "products.empty": "Henüz ürün yok",
+ "products.deleteConfirm": "Bu işlem geri alınamaz. Mevcut lisansları olan ürünler silinemez.",
+ "products.createTitle": "Ürün Oluştur",
+ "products.editTitle": "Ürünü Düzenle",
+ "products.formDesc": "Aşağıdaki ürün ayrıntılarını doldurun.",
+
+ // Planlar (Plans)
+ "plans.title": "Planlar",
+ "plans.subtitle": "Ürünleriniz için lisans planlarını yönetin.",
+ "plans.new": "Yeni Plan",
+ "plans.licenseType": "Lisans Türü",
+ "plans.maxActivations": "Maksimum Etkinleştirme",
+ "plans.maxSeats": "Maksimum Koltuk (Seats)",
+ "plans.licenseModel": "Lisans Modeli",
+ "plans.modelStandard": "Standart",
+ "plans.modelFloating": "Kayan / Eşzamanlı (Floating)",
+ "plans.floatingTimeout": "Kayan Lisans Zaman Aşımı (dakika)",
+ "plans.hintDesktop": "Masaüstü ürünü — etkinleştirmeleri + otomatik güncelleme akışlarını destekler. Koltuklar devre dışıdır.",
+ "plans.hintSaas": "SaaS ürünü — koltukları ve kullanımı destekler. Etkinleştirmeler ve sürüm akışları devre dışıdır.",
+ "plans.hintHybrid": "Hibrit ürün — etkinleştirmeler, koltuklar ve sürüm akışlarının tümü kullanılabilir.",
+ "plans.notApplicableForType": "Bu ürün türü için geçerli değil",
+ "plans.trialDays": "Deneme Süresi (Gün)",
+ "plans.graceDays": "Ek Süre (Grace Period - Gün)",
+ "plans.billingInterval": "Faturalandırma Aralığı",
+ "plans.empty": "Henüz plan yok",
+ "plans.copyCheckoutLink": "Ödeme bağlantısını kopyala",
+ "plans.linkCopied": "Ödeme bağlantısı kopyalandı!",
+ "plans.deleteConfirm": "Mevcut lisansları olan planlar silinemez.",
+ "plans.createTitle": "Plan Oluştur",
+ "plans.editTitle": "Planı Düzenle",
+ "plans.formDesc": "Plan ayrıntılarını yapılandırın.",
+ "plans.entitlements": "Haklar (Entitlements)",
+ "plans.addEntitlement": "Hak Ekle",
+ "plans.deleteEntitlementTitle": "{feature} silinsin mi?",
+ "plans.deleteEntitlementDesc":
+ "Bu işlem hakkı bu plandan anında kaldırır. Mevcut lisanslar bu özelliğe erişimini kaybeder.",
+ "plans.stripeMeterEventName": "Stripe sayaç etkinlik adı (isteğe bağlı)",
+ "plans.stripeMeterEventNameHelp":
+ "Stripe ölçümlü faturalandırmayı etkinleştirin. Stripe sayacınızdaki event_name ile eşleşmelidir; her kullanım çağrısı bu adla bir sayaç etkinliği gönderir.",
+ "plans.allProducts": "Tüm ürünler",
+
+ // Lisanslar (Licenses)
+ "licenses.title": "Lisanslar",
+ "licenses.subtitle": "Yazılım lisanslarını yönetin.",
+ "licenses.issue": "Lisans Oluştur",
+ "licenses.licenseKey": "Lisans Anahtarı",
+ "licenses.revealKey": "Anahtarı göster",
+ "licenses.validUntil": "Geçerlilik Tarihi",
+ "licenses.perpetual": "Süresiz",
+ "licenses.validUntilOptional": "Geçerlilik tarihi (isteğe bağlı)",
+ "licenses.validUntilHint": "Süresiz bir lisans için boş bırakın (deneme sürümleri yine de planın deneme günlerini kullanır).",
+ "licenses.validUntilEdit": "Bitiş tarihini düzenle",
+ "licenses.validUntilClear": "Lisansı süresiz yapmak için boş bırakın.",
+ "licenses.validUntilExpiredHint":
+ "Bu lisansın süresi dolmuş — tek başına yeni bir tarih lisansı yeniden etkinleştirmez. Yeniden Etkinleştir işlemini de kullanın.",
+ "licenses.empty": "Lisans bulunamadı",
+ "licenses.noProducts": "Henüz ürün yok",
+ "licenses.noProductsDesc": "Lisans oluşturmadan önce bir ürün ve plan tanımlayın.",
+ "licenses.detail": "Lisans Ayrıntısı",
+ "licenses.activations": "Etkinleştirmeler",
+ "licenses.revoke": "İptal Et (Revoke)",
+ "licenses.revokeConfirm":
+ "Bu işlem lisansı kalıcı olarak iptal eder. Müşteri erişimini hemen kaybeder. Bu işlem geri alınamaz.",
+ "licenses.suspend": "Askıya Al",
+ "licenses.suspendConfirm":
+ "Bu işlem lisansı askıya alır. Müşteri, lisans yeniden etkinleştirilene kadar yazılımı etkinleştiremez veya kullanamaz.",
+ "licenses.reinstate": "Yeniden Etkinleştir",
+ "licenses.refundConfirm":
+ "Bu işlem ödeme sağlayıcısındaki aboneliği iptal eder ve lisansı iptal eder. Müşteri erişimini hemen kaybeder.",
+ "licenses.changePlan": "Planı Değiştir",
+ "licenses.seats": "Koltuklar (Seats)",
+ "licenses.role": "Rol",
+ "licenses.active": "Aktif",
+ "licenses.pending": "Beklemede",
+ "licenses.removed": "Kaldırıldı",
+ "licenses.changePlanDesc": "Bu lisans için yeni bir plan seçin.",
+ "licenses.issueDesc": "Bir kullanıcı için manuel olarak lisans oluşturun.",
+ "licenses.info": "Bilgi",
+ "licenses.usage": "Kullanım",
+ "licenses.validFrom": "Başlangıç Tarihi",
+ "licenses.payment": "Ödeme",
+ "licenses.identifier": "Tanımlayıcı",
+ "licenses.type": "Tür",
+ "licenses.label": "Etiket",
+ "licenses.lastVerified": "Son Doğrulama",
+ "licenses.noActivations": "Henüz etkinleştirme yok.",
+ "licenses.usageCounters": "Kullanım Sayaçları",
+ "licenses.noUsageCounters": "Kullanım sayacı yok.",
+ "licenses.feature": "Özellik",
+ "licenses.period": "Dönem",
+ "licenses.periodKey": "Dönem Anahtarı",
+ "licenses.used": "Kullanılan",
+ "licenses.updated": "Güncellendi",
+ "licenses.resetCounter": "Sayacı sıfırla",
+ "licenses.resetCounterTitle": "{feature} için kullanım sıfırlansın mı?",
+ "licenses.resetCounterDesc":
+ "Mevcut dönem için sayaç sıfırlanır. Müşteri hemen tam kotayı tekrar kullanabilir. Bu işlem denetim günlüğüne kaydedilir.",
+ "licenses.deactivate": "Devre Dışı Bırak",
+ "licenses.invite": "Üye davet et",
+ "licenses.inviteTitle": "Ekip üyesi davet et",
+ "licenses.inviteDesc": "Bu adrese tek kullanımlık bir kabul bağlantısı göndereceğiz. Bağlantının süresi 7 gün içinde dolar.",
+ "licenses.sendInvite": "Davet gönder",
+ "licenses.seatInviteSent": "Davet gönderildi",
+ "licenses.seatCapReached": "Koltuk sınırına ulaşıldı. Daha fazla üye davet etmeden önce planı yükseltin veya pasif bir üyeyi kaldırın.",
+ "licenses.deactivateTitle": "{device} cihazı devre dışı bırakılsın mı?",
+ "licenses.deactivateDesc":
+ "Bu cihaz hemen erişimini kaybeder. Müşteri, lisans anahtarını bilen herhangi bir istemciden (bir etkinleştirme yuvası kullanarak) cihazı yeniden etkinleştirebilir.",
+ "licenses.recentEvents": "Son Olaylar",
+ "licenses.noUsageEvents": "Kullanım olayı yok.",
+ "licenses.quantity": "Miktar",
+ "licenses.ip": "IP",
+ "licenses.recorded": "Kaydedildi",
+ "licenses.noSeats": "Atanmış koltuk yok.",
+ "licenses.invited": "Davet Edildi",
+ "licenses.accepted": "Kabul Edildi",
+
+ // API Anahtarları (API Keys)
+ "apiKeys.title": "API Anahtarları",
+ "apiKeys.subtitle": "Yetki kapsamlarına sahip sunucular arası kimlik bilgileri.",
+ "apiKeys.new": "Yeni API Anahtarı",
+ "apiKeys.prefix": "Anahtar Öneki",
+ "apiKeys.lastUsed": "Son Kullanım",
+ "apiKeys.empty": "Henüz API anahtarı yok",
+ "apiKeys.created": "API Anahtarı Oluşturuldu",
+ "apiKeys.createdDesc": "Bu anahtarı şimdi kopyalayın. Tekrar görüntüleyemeyeceksiniz.",
+ "apiKeys.deleteConfirm": "Bu işlem bu anahtarı derhal geçersiz kılacaktır. Bu anahtarı kullanan tüm istemciler çalışmayı durduracaktır.",
+ "apiKeys.noProductsDesc": "API anahtarları oluşturmadan önce bir ürün oluşturun.",
+ "apiKeys.scopes": "Yetki Kapsamları (Scopes)",
+ "apiKeys.scopesNone": "Kapsam seçilmedi",
+ "apiKeys.lastUsedIP": "Kullanılan IP",
+ "apiKeys.rotate": "Döndür (Rotate)",
+ "apiKeys.rotateTitle": "API anahtarını döndür",
+ "apiKeys.rotateConfirm":
+ "Mevcut gizli anahtar derhal çalışmayı durduracaktır. Devam etmeden önce bu anahtarı kullanan tüm istemcileri güncelleyin.",
+ "apiKeys.scopeAdmin": "Yönetici (Admin - tam yetki)",
+ "apiKeys.scopeAdminDesc": "Tam /admin/* erişimi. Yönetim araçları için.",
+ "apiKeys.scopeLicensesWrite": "Lisanslar (yazma)",
+ "apiKeys.scopeLicensesWriteDesc": "Lisans oluşturun, güncelleyin ve yönetin. Ödeme ve tahsisat arka uçları için.",
+ "apiKeys.scopeReleasesWrite": "Sürümler (yazma)",
+ "apiKeys.scopeReleasesWriteDesc": "Sürümleri ve paketleri yükleyin. Derlemeleri yayınlayan CI/CD süreçleri için.",
+ "apiKeys.atLeastOneScope": "En az bir yetki kapsamı seçin.",
+
+ // Webhooks
+ "webhooks.title": "Webhooks",
+ "webhooks.subtitle": "Olay bildirimleri için webhook uç noktalarını yönetin.",
+ "webhooks.new": "Yeni Webhook",
+ "webhooks.events": "Olaylar",
+ "webhooks.empty": "Henüz webhook yok",
+ "webhooks.deliveries": "Teslimat Günlüğü",
+ "webhooks.test": "Test Et",
+ "webhooks.secretCreated": "Webhook Oluşturuldu",
+ "webhooks.secretDesc": "Bu imzalama gizli anahtarını şimdi kopyalayın. Tekrar görüntüleyemeyeceksiniz.",
+ "webhooks.newDesc": "Olay bildirimlerini almak için bir uç nokta yapılandırın.",
+ "webhooks.deleteConfirm": "Bu işlem webhook'u kalıcı olarak kaldıracaktır. Olaylar artık iletilmeyecektir.",
+ "webhooks.noProductsDesc": "Webhook'ları yapılandırmadan önce bir ürün oluşturun.",
+
+ // Analitik (Analytics)
+ "analytics.title": "Analitik",
+ "analytics.subtitle": "Zaman içindeki lisans metrikleri, dağılımları ve trendleri.",
+ "analytics.granularity": "Ayrıntı Düzeyi",
+ "analytics.daily": "Günlük",
+ "analytics.weekly": "Haftalık",
+ "analytics.monthly": "Aylık",
+ "analytics.trends": "Trendler",
+ "analytics.breakdowns": "Dağılımlar",
+ "analytics.usage": "Kullanım",
+ "analytics.activations": "Etkinleştirmeler",
+ "analytics.insights": "İçgörüler",
+ "analytics.totalLicenses": "Toplam Lisans",
+ "analytics.active": "Aktif",
+ "analytics.trialing": "Deneme Sürecinde",
+ "analytics.pastDue": "Vadesi Geçmiş",
+ "analytics.expired": "Süresi Dolmuş",
+ "analytics.canceled": "İptal Edilmiş",
+ "analytics.suspended": "Askıya Alınmış",
+ "analytics.revoked": "Hükümsüz Kılınmış (Revoked)",
+ "analytics.totalActivations": "Toplam Etkinleştirme",
+ "analytics.totalSeats": "Toplam Koltuk",
+ "analytics.avgActivations": "Ort. Etkinleştirme/Lisans",
+ "analytics.new": "Yeni",
+ "analytics.churned": "Kaybedilen (Churned)",
+ "analytics.netGrowth": "Net Büyüme",
+ "analytics.byStatus": "Duruma Göre",
+ "analytics.byPlan": "Plana Göre",
+ "analytics.byLicenseType": "Lisans Türüne Göre",
+ "analytics.topFeatures": "Kullanıma Göre Öne Çıkan Özellikler",
+ "analytics.rank": "Sıra",
+ "analytics.feature": "Özellik",
+ "analytics.totalUsage": "Toplam Kullanım",
+ "analytics.uniqueLicenses": "Benzersiz Lisanslar",
+ "analytics.activationTrend": "Etkinleştirme Trendi",
+ "analytics.noData": "Seçilen dönem için veri bulunamadı.",
+ "analytics.snapshots": "Anlık Görüntüler",
+ "analytics.growthRate": "Net Büyüme Oranı",
+ "analytics.trialConversion": "Deneme Sürümü Dönüşümü",
+ "analytics.newLast30d": "Yeni (Son 30 Gün)",
+ "analytics.churnedLast30d": "Kaybedilen (Son 30 Gün)",
+ "analytics.licenseAge": "Lisans Yaşı Dağılımı",
+ "analytics.monthlyRetention": "Aylık Elde Tutma (Retention)",
+ "analytics.topUsers": "En Çok Kullananlar",
+ "analytics.recentActivity": "Son Etkinlik",
+
+ // Denetim Günlüğü (Audit)
+ "audit.title": "Denetim Günlüğü",
+ "audit.subtitle": "Sistem genelindeki etkinlik geçmişi.",
+ "audit.entity": "Varlık",
+ "audit.action": "İşlem",
+ "audit.actor": "Gerçekleştiren",
+ "audit.changes": "Değişiklikler",
+ "audit.empty": "Denetim günlüğü kaydı yok",
+ "audit.filterEntity": "Varlık türü",
+ "audit.allEntities": "Tüm varlıklar",
+ "audit.filterEntityId": "Varlık kimliğine göre ara",
+ "audit.filterProduct": "Ürün",
+ "audit.allProducts": "Tüm ürünler",
+
+ // Müşteriler (Customers)
+ "customers.title": "Müşteriler",
+ "customers.subtitle": "{count} müşteri kayıtlı.",
+ "customers.customer": "Müşteri",
+ "customers.joined": "Katılım Tarihi",
+ "customers.lastUpdated": "Son Güncelleme",
+ "customers.empty": "Henüz müşteri yok",
+ "customers.detail": "Müşteri Ayrıntısı",
+ "customers.overview": "Genel Bakış",
+ "customers.licenses": "Lisanslar",
+ "customers.subscriptions": "Abonelikler",
+ "customers.activity": "Etkinlik",
+ "customers.totalUsage": "Toplam Kullanım",
+ "customers.activeSeats": "Aktif Koltuklar",
+
+ // Ekip (Team)
+ "team.title": "Ekip",
+ "team.subtitle": "Platform yöneticilerini yönetin.",
+ "team.invite": "Üye Davet Et",
+ "team.email": "E-posta adresi",
+ "team.role": "Rol",
+ "team.remove": "Kaldır",
+ "team.removeConfirm": "Bu ekip üyesi kaldırılsın mı? Yönetici erişimini kaybedecek.",
+ "team.empty": "Henüz ekip üyesi yok.",
+ "team.invited": "Üye başarıyla davet edildi.",
+ "team.removed": "Üye kaldırıldı.",
+ "team.ownerOnly": "Yalnızca ekip sahibi ekip üyelerini yönetebilir.",
+
+ // Ayarlar (Settings)
+ "settings.title": "Ayarlar",
+ "settings.subtitle": "Sistem yapılandırması ve tercihler.",
+ "settings.general": "Genel",
+ "settings.email": "E-posta",
+ "settings.security": "Güvenlik",
+ "settings.timezone": "Saat Dilimi",
+ "settings.timezoneDesc": "Tarih gösterimi ve analitik için varsayılan saat dilimi.",
+ "settings.siteName": "Site Adı",
+ "settings.siteNameDesc": "Yönetici panelinde görüntülenen ad.",
+ "settings.language": "Dil",
+ "settings.languageDesc": "Arayüz dili.",
+ "settings.brandColor": "Marka Rengi",
+ "settings.brandColorDesc": "Arayüz genelinde kullanılan birincil renk. Varsayılan için boş bırakın.",
+ "settings.logoUrl": "Özel Logo URL'si",
+ "settings.logoUrlDesc": "Logonuzun URL'si (SVG önerilir). Varsayılan Keygate logosu için boş bırakın.",
+ "settings.smtpHost": "SMTP Sunucusu",
+ "settings.smtpPort": "SMTP Bağlantı Noktası (Port)",
+ "settings.smtpUsername": "SMTP Kullanıcı Adı",
+ "settings.smtpPassword": "SMTP Parolası",
+ "settings.smtpPasswordStored": "Kaydedildi — değiştirmemek için boş bırakın",
+ "settings.smtpPasswordClear": "Kaldır",
+ "settings.smtpFrom": "Gönderen E-posta",
+ "settings.smtpFromDesc": "Giden e-postalar için gönderen e-posta adresi.",
+ "settings.testEmail": "Test E-postası Gönder",
+ "settings.testEmailDesc": "SMTP yapılandırmanızı doğrulamak için bir test e-postası gönderin.",
+ "settings.signup": "Kayıtlar",
+ "settings.signupMode": "Hesap oluşturabilecek kişiler",
+ "settings.signupOpen": "E-posta adresi olan herkes",
+ "settings.signupLicensedOnly": "Yalnızca lisansa sahip e-postalar",
+ "settings.signupModeDesc":
+ "Kayıtları sınırlandırmak, giriş kodu uç noktasının yabancılara e-posta göndermesini de durdurur. Zaten hesabı olan kullanıcılar her zaman giriş yapabilir.",
+ "settings.rateLimit": "İstek Sınırlandırma (Rate Limiting)",
+ "settings.rateLimitApi": "API İstek Sınırı",
+ "settings.rateLimitApiDesc": "API uç noktaları için dakika başına maksimum istek sayısı.",
+ "settings.rateLimitAdmin": "Yönetici İstek Sınırı",
+ "settings.rateLimitAdminDesc": "Yönetici uç noktaları için dakika başına maksimum istek sayısı.",
+ "settings.webhookConfig": "Webhook Yapılandırması",
+ "settings.webhookMaxAttempts": "Maksimum Yeniden Deneme Sayısı",
+ "settings.webhookTimeout": "HTTP Zaman Aşımı (saniye)",
+ "settings.quotaThreshold": "Kota Uyarı Eşiği",
+ "settings.quotaThresholdDesc": "Kullanım bu yüzdeye ulaştığında uyarı gönderilir (0-1 arası).",
+ "settings.emailTemplates": "E-posta Şablonları",
+ "settings.editTemplate": "Düzenle",
+ "settings.resetTemplate": "Varsayılana Sıfırla",
+ "settings.resetTemplateTitle": "{template} şablonu sıfırlansın mı?",
+ "settings.resetTemplateDesc":
+ "Özelleştirilmiş HTML içeriğiniz paketlenmiş varsayılan şablonla değiştirilecektir. Bu işlem geri alınamaz — gerekebilirse önce mevcut HTML kodunu kopyalayın.",
+ "settings.templateSaved": "Şablon kaydedildi.",
+ "settings.templateReset": "Şablon varsayılana sıfırlandı.",
+ "settings.templateDesc":
+ "E-posta şablonlarını özelleştirin. Go şablon sözdizimini kullanın: {{.Product}}, {{.Plan}}, {{.LicenseKey}}, vb.",
+ "settings.templatePreview": "Önizleme",
+ "settings.templateVariables": "Değişkenler",
+ "settings.system": "Sistem",
+ "settings.versionInfo": "Sürüm Bilgisi",
+ "settings.currentVersion": "Mevcut Sürüm",
+ "settings.commitHash": "Commit",
+ "settings.buildDate": "Derleme Tarihi",
+ "settings.checkUpdate": "Güncellemeleri Denetle",
+ "settings.updateAvailable": "Sürüm {version} mevcut!",
+ "settings.viewRelease": "Sürümü Görüntüle",
+ "settings.upToDate": "Sisteminiz güncel",
+ "settings.changelog": "Yenilikler",
+ "settings.migrations": "Veritabanı Geçişleri",
+ "settings.saved": "Ayarlar başarıyla kaydedildi.",
+ "settings.saveError": "Ayarlar kaydedilemedi.",
+ "settings.testEmailSent": "Test e-postası gönderildi.",
+ "settings.testEmailFailed": "Test e-postası gönderilemedi.",
+
+ // Sayfalama (Pagination)
+ "pagination.of": "/",
+ "pagination.rows": "Satır",
+
+ // Giriş Yap (Login)
+ "login.subtitle": "Hesabınıza giriş yapın",
+ "login.signInWith": "{provider} ile giriş yap",
+ "login.signingIn": "Giriş yapılıyor...",
+ "login.devLogin": "Geliştirici Girişi",
+ "login.devNote": "Yalnızca geliştirme ortamı içindir. Yönetici erişimi için e-postayı ADMIN_EMAILS listesine ekleyin.",
+ "login.noProviders": "Yapılandırılmış kimlik doğrulama sağlayıcısı yok.",
+ "login.failed": "Giriş başarısız oldu",
+ "login.sendCode": "Giriş kodu gönder",
+ "login.sendingCode": "Gönderiliyor...",
+ "login.codeSentTo": "Kod {email} adresine gönderildi",
+ "login.verify": "Doğrula",
+ "login.verifying": "Doğrulanıyor...",
+ "login.changeEmail": "Farklı bir e-posta kullan",
+ "login.resendCode": "Kodu tekrar gönder",
+ "login.resendIn": "{seconds} sn içinde tekrar gönder",
+ "login.or": "veya",
+
+ // Ödeme / Satın Alma (Checkout)
+ "checkout.verifying": "Ödemeniz doğrulanıyor...",
+ "checkout.success": "Ödeme başarılı!",
+ "checkout.licenseSentTo": "Lisans anahtarınız {email} adresine gönderildi",
+ "checkout.licenseCreated": "Lisans anahtarınız oluşturuldu",
+ "checkout.error": "Ödeme doğrulanamadı. Lisansınız kısa bir süre içinde e-posta ile gönderilecektir.",
+ "checkout.viewLicense": "Lisansımı görüntüle",
+
+ // Portal
+ "portal.myLicenses": "Lisanslarım",
+ "portal.myLicensesDesc": "Yazılım lisanslarınızı, kullanımınızı ve ekip üyelerinizi yönetin.",
+ "portal.noLicenses": "Henüz lisans yok",
+ "portal.noLicensesDesc": "Satın aldığınız lisanslar burada görüntülenecektir.",
+ "portal.validFrom": "Başlangıç Tarihi",
+ "portal.noDevices": "Aktif cihaz yok.",
+ "portal.activeDevices": "Aktif Cihazlar",
+ "portal.lastVerified": "Son doğrulama:",
+ "portal.noEntitlements": "Yapılandırılmış hak bulunmuyor.",
+ "portal.featuresIncluded": "Dahil Olan Özellikler",
+ "portal.teamMembers": "Ekip Üyeleri",
+ "portal.addMember": "Üye Ekle",
+ "portal.removeMember": "Üyeyi kaldır",
+ "portal.removeMemberTitle": "Ekip üyesi kaldırılsın mı?",
+ "portal.removeMemberDesc": "{email} hemen erişimini kaybeder. Daha sonra tekrar davet edebilirsiniz.",
+ "portal.removeDevice": "Cihazı kaldır",
+ "portal.removeDeviceTitle": "Bu cihaz kaldırılsın mı?",
+ "portal.removeDeviceDesc":
+ "{device} cihazının etkinleştirmesi derhal kaldırılır ve bir yuva boşa çıkar. Yuva mevcutsa cihaz daha sonra tekrar etkinleştirilebilir.",
+ "portal.noMembers": "Henüz ekip üyesi yok.",
+ "portal.seatLimit": "Koltuk sınırına ulaşıldı. Daha fazla üye eklemek için planınızı yükseltin.",
+ "portal.inviteDesc": "Bu lisansa yeni bir üye davet edin.",
+ "portal.account": "Hesap",
+ "portal.accountDesc": "Profiliniz ve lisans özetiniz.",
+ "portal.profile": "Profil",
+ "portal.editProfile": "Düzenle",
+ "portal.saveProfile": "Kaydet",
+ "portal.profileSaved": "Profil güncellendi.",
+ "portal.displayName": "Görünen Ad",
+ "portal.accountType": "Hesap Türü",
+ "portal.administrator": "Yönetici",
+ "portal.userRole": "Kullanıcı",
+ "portal.licenseOverview": "Lisans Özeti",
+ "portal.noLicensesAccount": "Hesabınızla ilişkili bir lisans bulunmuyor.",
+ "portal.roleMember": "Üye",
+ "portal.roleAdmin": "Yönetici",
+ "portal.viewInvoices": "Faturaları Görüntüle",
+ "portal.updatePayment": "Ödeme Yöntemini Güncelle",
+ "portal.changePlan": "Planı Değiştir",
+ "portal.cancelSubscription": "Aboneliği İptal Et",
+ "portal.cancelDesc": "{product} aboneliğinizi iptal etmek istediğinizden emin misiniz?",
+ "portal.cancelImmediately": "Hemen iptal et (erişimi şimdi sonlandır)",
+ "portal.cancelImmediateWarning": "Erişiminiz derhal sona erecektir. Bu işlem geri alınamaz.",
+ "portal.cancelEndWarning": "Mevcut faturalandırma döneminizin sonuna kadar erişiminizi korursunuz.",
+ "portal.confirmCancel": "İptali Onayla",
+ "portal.invoices": "Faturalar",
+ "portal.changePlanDesc": "Yeni bir plan seçin. Değişiklikler orantılı faturalandırma ile hemen yürürlüğe girer.",
+ "portal.noOtherPlans": "Başka plan bulunmuyor.",
+ "portal.switchTo": "Geçiş Yap",
+
+ // Filtreler (Filters)
+ "filter.allProducts": "Tüm ürünler",
+ "filter.allStatuses": "Tüm durumlar",
+ "filter.allPlans": "Tüm planlar",
+ "filter.allTypes": "Tüm türler",
+
+ // Durum değerleri (Status values)
+ "status.active": "Aktif",
+ "status.trialing": "Deneme",
+ "status.past_due": "Vadesi Geçmiş",
+ "status.canceled": "İptal Edildi",
+ "status.expired": "Süresi Doldu",
+ "status.suspended": "Askıya Alındı",
+ "status.revoked": "Hükümsüz (Revoked)",
+ "status.pending": "Beklemede",
+ "status.delivered": "Teslim Edildi",
+ "status.failed": "Başarısız",
+ "status.paused": "Duraklatıldı",
+
+ // Webhooks ekleri
+ "webhooks.inactive": "Pasif",
+ "webhooks.event": "Olay",
+ "webhooks.response": "Yanıt",
+ "webhooks.attempts": "Denemeler",
+ "webhooks.delivered": "Teslim Edildi",
+ "webhooks.noDeliveries": "Henüz teslimat yok.",
+ "webhooks.deliveriesCount": "toplam teslimat",
+ "webhooks.statusAll": "Tümü",
+ "webhooks.resend": "Tekrar Gönder",
+ "webhooks.resendQueued": "Tekrar gönderim sıraya alındı",
+ "webhooks.resendFailed": "Tekrar gönderim başarısız oldu",
+ "webhooks.testQueued": "Test olayı gönderildi",
+ "webhooks.testFailed": "Test gönderimi başarısız oldu",
+
+ // Planlar ekleri (Plans extras)
+ "plans.subscription": "Abonelik",
+ "plans.perpetual": "Süresiz (Perpetual)",
+ "plans.trial": "Deneme",
+ "plans.none": "Yok",
+ "plans.monthly": "Aylık",
+ "plans.yearly": "Yıllık",
+ "plans.stripePriceId": "Stripe Fiyat Kimliği (Price ID)",
+ "plans.valueType": "Değer Türü",
+ "plans.value": "Değer",
+ "plans.quotaPeriod": "Kota Dönemi",
+ "plans.quotaUnit": "Kota Birimi",
+ "plans.hourly": "Saatlik",
+ "plans.daily": "Günlük",
+ "plans.boolean": "Boolean",
+ "plans.integer": "Tamsayı (Integer)",
+ "plans.string": "Metin (String)",
+ "plans.quota": "Kota",
+ "plans.feature": "Özellik",
+
+ // Ürünler ekleri (Products extras)
+ "products.desktop": "Masaüstü (Desktop)",
+ "products.desktopDesc": "Yüklenebilir uygulama. Cihaz başına etkinleştirme + otomatik güncelleme akışları.",
+ "products.saas": "SaaS",
+ "products.saasDesc": "Barındırılan hizmet. Kullanıcı başına koltuk + kullanım ölçümü.",
+ "products.hybrid": "Hibrit",
+ "products.hybridDesc": "Bulut + yüklenebilir istemci. Tüm yetenekler kullanılabilir.",
+
+ // Lisanslar ekleri (Licenses extras)
+ "licenses.notes": "Notlar",
+ "licenses.notesOptional": "Notlar (isteğe bağlı)",
+ "licenses.externalCustomerID": "Harici Müşteri Kimliği",
+ "licenses.externalWorkspaceID": "Harici Çalışma Alanı Kimliği",
+ "licenses.externalIDPlaceholder": "İsteğe bağlı. Kendi kullanıcı veya kiracı kimliğiniz.",
+
+ // Analitik ekleri (Analytics extras)
+ "analytics.period": "Dönem",
+ "analytics.start": "Başlangıç",
+ "analytics.end": "Bitiş",
+ "analytics.retentionPct": "Elde Tutma %",
+ "analytics.churnPct": "Kayıp %",
+ "analytics.seats": "Koltuklar",
+ "analytics.weekOf": "Haftası",
+ "analytics.month": "Ay",
+ "analytics.date": "Tarih",
+ "analytics.noUsageData": "Kullanım verisi bulunmuyor.",
+ "analytics.noActivationData": "Etkinleştirme verisi bulunmuyor.",
+ "analytics.noUserData": "Kullanıcı verisi bulunmuyor.",
+ "analytics.noRecentActivity": "Son etkinlik yok.",
+ "analytics.loadingInsights": "İçgörüler yükleniyor...",
+ "analytics.converted": "dönüştürüldü",
+ "analytics.rawData": "Ham Veri",
+
+ // Kullanıcılar / Müşteriler ekleri (Users extras)
+ "customers.provider": "Sağlayıcı",
+ "customers.periodRange": "Dönem",
+ "customers.cancelAtEnd": "Dönem Sonunda İptal Et",
+
+ // Lisans iade (Licenses refund)
+ "licenses.refund": "İade Et",
+
+ // Eklentiler (Addons)
+ "nav.addons": "Eklentiler",
+ "addons.title": "Eklentiler",
+ "addons.subtitle": "Satın alınabilir özellik eklentilerini yönetin.",
+ "addons.new": "Yeni Eklenti",
+ "addons.edit": "Eklentiyi Düzenle",
+ "addons.empty": "Henüz eklenti yok",
+ "addons.deleteConfirm": "Bu işlem bu eklentiyi kalıcı olarak kaldıracaktır.",
+ "addons.formDesc": "Eklenti ayrıntılarını yapılandırın.",
+ "addons.description": "Açıklama",
+
+ // Bildirim mesajları (Toast messages)
+ "toast.productCreated": "Ürün başarıyla oluşturuldu.",
+ "toast.productDeleted": "Ürün silindi.",
+ "toast.planCreated": "Plan başarıyla oluşturuldu.",
+ "toast.planDeleted": "Plan silindi.",
+ "toast.licenseCreated": "Lisans başarıyla oluşturuldu.",
+ "toast.apiKeyCreated": "API anahtarı başarıyla oluşturuldu.",
+ "toast.addonCreated": "Eklenti başarıyla oluşturuldu.",
+ "toast.webhookCreated": "Webhook başarıyla oluşturuldu.",
+
+ // Hata ekranı (Error boundary)
+ "error.title": "Bir şeyler ters gitti",
+ "error.unexpected": "Beklenmeyen bir hata oluştu.",
+ "error.tryAgain": "Tekrar Dene",
+ "error.reload": "Sayfayı Yenile",
+
+ // Sürümler (Releases)
+ "releases.title": "Sürümler",
+ "releases.subtitle": "Sparkle, Velopack veya Tauri aracılığıyla müşterilerinize yazılım güncellemeleri dağıtın.",
+ "releases.signingKeys": "İmzalama anahtarları",
+ "releases.new": "Yeni sürüm",
+ "releases.noEligibleProducts": "Sürüm için uygun ürün bulunamadı",
+ "releases.noEligibleProductsDesc":
+ "Sürüm akışları yalnızca masaüstü ve hibrit ürünler için geçerlidir. Mevcut ürünlerinizin tümü SaaS türündedir — bir ürünün türünü değiştirin veya yeni bir masaüstü/hibrit ürün oluşturun.",
+ "releases.noProducts": "Henüz ürün yok",
+ "releases.noProductsDesc": "Sürüm yayınlamadan önce bir ürün oluşturun.",
+ "releases.manageProducts": "Ürünleri yönet",
+ "releases.createProduct": "Ürün oluştur",
+ "releases.allChannels": "Tüm kanallar",
+ "releases.allStatuses": "Tüm durumlar",
+ "releases.version": "Sürüm",
+ "releases.channel": "Kanal",
+ "releases.platforms": "Platformlar",
+ "releases.empty": 'Henüz sürüm yok. Başlamak için "Yeni sürüm" butonuna tıklayın.',
+ "releases.belowLatest": "en son sürümün altında",
+ "releases.platformsCount": "{count} platform",
+ "releases.platformsCountPlural": "{count} platform",
+ "releases.openDetail": "Sürüm ayrıntısını aç",
+ "releases.viewManageArtifacts": "Paketleri görüntüle / yönet",
+ "releases.publish": "Yayınla",
+ "releases.awaitingArtifacts": "Paketler bekleniyor ({ready}/{total} hazır)",
+ "releases.yank": "Geri Çek (Yank)",
+ "releases.unyank": "Geri Çekmeyi Kaldır (Unyank)",
+ "releases.deleteDraft": "Taslağı sil",
+ "releases.deleteYankInstead": "Sil (bunun yerine geri çekin)",
+ "releases.statusDraft": "taslak",
+ "releases.statusPublished": "yayınlandı",
+ "releases.statusYanked": "geri çekildi",
+ "releases.statusReady": "hazır",
+ "releases.statusPending": "beklemede",
+ "releases.unyankTitle": "v{version} geri çekilmesi kaldırılsın mı?",
+ "releases.unyankDesc":
+ "Bu işlem sürümü genel güncelleme akışına geri yükler. İlgili kanaldaki SDK istemcileri v{version} sürümünü yeniden geçerli bir güncelleme olarak almaya başlar.",
+ "releases.deleteDraftTitle": "Taslak sürüm silinsin mi?",
+ "releases.deleteDraftDesc":
+ "Taslağı ve yüklenen paketlerini kalıcı olarak siler. Yayınlanmış veya geri çekilmiş sürümler silinemez.",
+ "releases.publishBelowLatestTitle": "Mevcut en son sürümün altında bir sürüm yayınlansın mı?",
+ "releases.publishBelowLatestDesc":
+ "{latest} sürümünden daha eski olan {version} sürümü yayınlanıyor. Velopack ve Tauri sürüm düşürmelerini varsayılan olarak reddeder; Sparkle'ın standart karşılaştırıcısı SemVer duyarlı değildir. Bu sürüm geçmişinizde sıra dışı görünecektir — ara düzeltmeler (backports) için uygundur.",
+ "releases.publishAnyway": "Yine de yayınla",
+ "releases.createTitle": "Yeni sürüm",
+ "releases.createDesc":
+ "Sürüm kaydını oluşturun. Platforma özel ikili dosyaları (paketleri) bir sonraki adımda ekleyeceksiniz.",
+ "releases.displayNameOptional": "Görünen ad (isteğe bağlı)",
+ "releases.displayNamePlaceholder": "Uygulamam Pro",
+ "releases.releaseNotesOptional": "Sürüm notları (isteğe bağlı, markdown)",
+ "releases.releaseNotesPlaceholder": "Bu sürümdeki yenilikler...",
+ "releases.createDraftBtn": "Taslak oluştur",
+ "releases.creating": "Oluşturuluyor...",
+ "releases.draftCreatedToast": "Taslak {version} oluşturuldu. Yayınlamak için paketleri ekleyin.",
+ "releases.detailDraftDesc": "Aşağıdan platform ikili dosyalarını ekleyin. Hazır olduğunda yayınlayın.",
+ "releases.detailNonDraftDesc": "Bu sürüm {status} durumundadır. Paketler değiştirilemez.",
+ "releases.artifactsHeading": "Paketler ({count})",
+ "releases.noArtifacts": "Henüz paket yok — yayınlamadan önce en az bir platform ekleyin.",
+ "releases.addArtifactRemaining": "Paket ekle ({count} platform kaldı)",
+ "releases.notUploadedYet": "Henüz yüklenmedi",
+ "releases.addArtifactTitle": "Paket ekle",
+ "releases.addArtifactDesc":
+ "{version} sürümü için bir platform ikili dosyası yükleyin. Dosya doğrudan depolama alanına gider; yayınlama anında imzalanır ve tamamlanır.",
+ "releases.platform": "Platform",
+ "releases.artifactFile": "Paket dosyası",
+ "releases.platformAndFileRequired": "Platform ve dosya zorunludur",
+ "releases.uploadFailed": "Yükleme başarısız oldu: {status} {statusText}",
+ "releases.reservingSlot": "Paket yuvası ayrılıyor...",
+ "releases.uploadingToStorage": "Depolama alanına yükleniyor...",
+ "releases.computingSha": "SHA-256 hesaplanıyor ve tamamlanıyor...",
+ "releases.artifactUploadedToast": "{platform} için paket yüklendi",
+ "releases.upload": "Yükle",
+ "releases.working": "İşleniyor...",
+ "releases.yankTitle": "{version} geri çekilsin mi?",
+ "releases.yankDesc":
+ "Geri çekme işlemi bu sürümü (ve TÜM paketlerini) güncelleme akışlarından kaldırır. Mevcut kurulumlar çalışmaya devam eder. Denetim günlüğüne kaydedilmek üzere bir neden belirtin.",
+ "releases.reason": "Neden",
+ "releases.yankReasonPlaceholder": "v1.2.3 sürümünde Windows kullanıcılarını etkileyen kritik hata; geri alma önerilir.",
+ "releases.releaseYankedToast": "Sürüm geri çekildi",
+ "releases.releaseUnyankedToast": "Sürümün geri çekilmesi kaldırıldı",
+ "releases.releasePublishedToast": "Sürüm yayınlandı",
+ "releases.draftDeletedToast": "Taslak silindi",
+ "releases.signingKeysTitle": "Sürüm imzalama anahtarları",
+ "releases.signingKeysDesc":
+ "Ürün başına bir Ed25519 anahtar çifti oluşturun. Genel anahtar istemci uygulamanıza yerleştirilir; sunucu yayınlama sırasında her sürüm paketini özel anahtarla imzalar.",
+ "releases.noActiveKey": "Aktif imzalama anahtarı yok",
+ "releases.noActiveKeyDesc": "Bir anahtar oluşturana kadar bu ürün için sürümler imzasız olacaktır.",
+ "releases.generateKey": "İmzalama anahtarı oluştur",
+ "releases.generating": "Oluşturuluyor...",
+ "releases.keyGeneratedToast": "İmzalama anahtarı oluşturuldu",
+ "releases.activeSigningKey": "Aktif imzalama anahtarı",
+ "releases.createdDate": "Oluşturuldu: {date}",
+ "releases.publicKeyLabel": "Genel anahtar (Ed25519, base64)",
+ "releases.publicKeyEmbedHint":
+ "Bunu istemci uygulamanızın güncelleme doğrulayıcısına yerleştirin (Info.plist içindeki Sparkle SUPublicEDKey veya Tauri pubkey).",
+ "releases.downloadPem": ".pem İndir",
+ "releases.rotate": "Döndür",
+ "releases.deactivate": "Devre Dışı Bırak",
+ "releases.pastKeysHeading": "Geçmiş anahtarlar ({count})",
+ "releases.rotatedAt": "döndürüldü: {date}",
+ "releases.rotateTitle": "İmzalama anahtarı döndürülsün mü?",
+ "releases.rotateDesc":
+ "Yeni anahtar çifti oluşturuldu. Eski anahtar geçmişte saklanır ancak pasiftir. Yalnızca eski genel anahtara sahip mevcut kurulumlar yeni sürümleri doğrulayamaz.",
+ "releases.rotateReasonLabel": "Neden (denetim günlüğü)",
+ "releases.rotateReasonPlaceholder": "Rutin anahtar değişimi; güvenlik ihlali yok.",
+ "releases.keyRotatedToast": "İmzalama anahtarı döndürüldü",
+ "releases.rotating": "Döndürülüyor...",
+ "releases.deactivateTitle": "İmzalama anahtarı devre dışı bırakılsın mı?",
+ "releases.deactivateDesc":
+ "Yeni bir anahtar oluşturana kadar gelecekteki sürümler İMZASIZ olacaktır. Katı imza kontrolü yapan Sparkle / Tauri / Velopack istemcileri imzasız güncellemeleri reddedecektir.",
+ "releases.deactivateReasonPlaceholder": "Neden devre dışı bırakıyorsunuz?",
+ "releases.keyDeactivatedToast": "İmzalama anahtarı devre dışı bırakıldı",
+ "releases.deactivating": "Devre dışı bırakılıyor...",
+} as const
+
+export default tr
diff --git a/web/src/i18n/locales/zh.ts b/web/src/i18n/locales/zh.ts
index ff6d72d..c68275b 100644
--- a/web/src/i18n/locales/zh.ts
+++ b/web/src/i18n/locales/zh.ts
@@ -582,6 +582,120 @@ const zh = {
"error.unexpected": "发生了意外错误。",
"error.tryAgain": "重试",
"error.reload": "刷新页面",
+
+ // 发布管理 (Releases)
+ "releases.title": "发布管理",
+ "releases.subtitle": "通过 Sparkle、Velopack 或 Tauri 向客户分发软件更新。",
+ "releases.signingKeys": "签名密钥",
+ "releases.new": "新建发布",
+ "releases.noEligibleProducts": "暂无可发布的符合条件的产品",
+ "releases.noEligibleProductsDesc":
+ "发布源仅适用于桌面版和混合型产品。您现有的产品均为 SaaS — 请更改产品类型或创建新的桌面/混合产品。",
+ "releases.noProducts": "暂无产品",
+ "releases.noProductsDesc": "发布版本前请先创建产品。",
+ "releases.manageProducts": "管理产品",
+ "releases.createProduct": "创建产品",
+ "releases.allChannels": "所有通道",
+ "releases.allStatuses": "所有状态",
+ "releases.version": "版本",
+ "releases.channel": "通道",
+ "releases.platforms": "平台",
+ "releases.empty": "暂无发布版本。点击“新建发布”开始。",
+ "releases.belowLatest": "低于最新版本",
+ "releases.platformsCount": "{count} 个平台",
+ "releases.platformsCountPlural": "{count} 个平台",
+ "releases.openDetail": "打开发布详情",
+ "releases.viewManageArtifacts": "查看 / 管理发布包",
+ "releases.publish": "发布",
+ "releases.awaitingArtifacts": "等待发布包 ({ready}/{total} 就绪)",
+ "releases.yank": "撤回 (Yank)",
+ "releases.unyank": "取消撤回 (Unyank)",
+ "releases.deleteDraft": "删除草稿",
+ "releases.deleteYankInstead": "删除 (请使用撤回替代)",
+ "releases.statusDraft": "草稿",
+ "releases.statusPublished": "已发布",
+ "releases.statusYanked": "已撤回",
+ "releases.statusReady": "就绪",
+ "releases.statusPending": "等待中",
+ "releases.unyankTitle": "取消撤回 v{version}?",
+ "releases.unyankDesc":
+ "这将恢复该发布版本至公共更新源。受影响通道上的 SDK 客户端将重新将 v{version} 作为有效更新接收。",
+ "releases.deleteDraftTitle": "删除草稿版本?",
+ "releases.deleteDraftDesc":
+ "将永久删除该草稿及其上传的发布包。已发布或已撤回的版本无法删除。",
+ "releases.publishBelowLatestTitle": "发布低于当前最新版本的版本?",
+ "releases.publishBelowLatestDesc":
+ "正在发布 {version},该版本早于 {latest}。Velopack 和 Tauri 默认拒绝降级;Sparkle 标准比较器不感知 SemVer。此发布版本将乱序显示在您的历史记录中 — 适用于补丁回溯 (backports)。",
+ "releases.publishAnyway": "仍然发布",
+ "releases.createTitle": "新建发布",
+ "releases.createDesc":
+ "创建发布记录。您将在下一步添加特定平台的二进制文件(发布包)。",
+ "releases.displayNameOptional": "显示名称(可选)",
+ "releases.displayNamePlaceholder": "MyApp Pro",
+ "releases.releaseNotesOptional": "发布说明(可选,Markdown)",
+ "releases.releaseNotesPlaceholder": "此版本的新特性...",
+ "releases.createDraftBtn": "创建草稿",
+ "releases.creating": "创建中...",
+ "releases.draftCreatedToast": "草稿 {version} 已创建。添加发布包后即可发布。",
+ "releases.detailDraftDesc": "在下方添加平台二进制文件。就绪后即可发布。",
+ "releases.detailNonDraftDesc": "此发布版本处于 {status} 状态。发布包无法修改。",
+ "releases.artifactsHeading": "发布包 ({count})",
+ "releases.noArtifacts": "暂无发布包 — 发布前请至少添加一个平台。",
+ "releases.addArtifactRemaining": "添加发布包 (还剩 {count} 个平台)",
+ "releases.notUploadedYet": "尚未上传",
+ "releases.addArtifactTitle": "添加发布包",
+ "releases.addArtifactDesc":
+ "上传 {version} 的平台二进制文件。文件将直接传输至存储;我们在发布时签名并最终确认。",
+ "releases.platform": "平台",
+ "releases.artifactFile": "发布包文件",
+ "releases.platformAndFileRequired": "平台和文件为必填项",
+ "releases.uploadFailed": "上传失败:{status} {statusText}",
+ "releases.reservingSlot": "正在预留发布包位置...",
+ "releases.uploadingToStorage": "正在上传至存储...",
+ "releases.computingSha": "正在计算 SHA-256 并确认...",
+ "releases.artifactUploadedToast": "{platform} 的发布包已上传",
+ "releases.upload": "上传",
+ "releases.working": "处理中...",
+ "releases.yankTitle": "撤回 {version}?",
+ "releases.yankDesc":
+ "撤回将从更新源中移除此版本(及其所有发布包)。现有安装将继续运行。请提供原因 — 将记录在审计日志中。",
+ "releases.reason": "原因",
+ "releases.yankReasonPlaceholder": "v1.2.3 中影响 Windows 用户的严重缺陷;建议回滚。",
+ "releases.releaseYankedToast": "发布已撤回",
+ "releases.releaseUnyankedToast": "发布已取消撤回",
+ "releases.releasePublishedToast": "发布已上线",
+ "releases.draftDeletedToast": "草稿已删除",
+ "releases.signingKeysTitle": "发布签名密钥",
+ "releases.signingKeysDesc":
+ "为每个产品生成一个 Ed25519 密钥对。公钥嵌入在您的客户端应用中;服务器在发布时使用私钥对每个发布包进行签名。",
+ "releases.noActiveKey": "暂无活动签名密钥",
+ "releases.noActiveKeyDesc": "在生成密钥之前,此产品的发布将不包含签名。",
+ "releases.generateKey": "生成签名密钥",
+ "releases.generating": "生成中...",
+ "releases.keyGeneratedToast": "签名密钥已生成",
+ "releases.activeSigningKey": "活动签名密钥",
+ "releases.createdDate": "创建于 {date}",
+ "releases.publicKeyLabel": "公钥 (Ed25519, base64)",
+ "releases.publicKeyEmbedHint":
+ "将此嵌入到客户端应用的更新验证程序中(Info.plist 中的 Sparkle SUPublicEDKey,或 Tauri pubkey)。",
+ "releases.downloadPem": "下载 .pem",
+ "releases.rotate": "轮换",
+ "releases.deactivate": "停用",
+ "releases.pastKeysHeading": "历史密钥 ({count})",
+ "releases.rotatedAt": "轮换于 {date}",
+ "releases.rotateTitle": "轮换签名密钥?",
+ "releases.rotateDesc":
+ "已生成新密钥对。旧密钥保留在历史记录中但处于非活动状态。仅嵌入旧公钥的现有安装将无法验证新发布。",
+ "releases.rotateReasonLabel": "原因(审计日志)",
+ "releases.rotateReasonPlaceholder": "常规轮换;无密钥泄漏。",
+ "releases.keyRotatedToast": "签名密钥已轮换",
+ "releases.rotating": "轮换中...",
+ "releases.deactivateTitle": "停用签名密钥?",
+ "releases.deactivateDesc":
+ "在生成新密钥之前,未来的发布将为未签名状态。具有严格签名检查的 Sparkle / Tauri / Velopack 客户端将拒绝未签名的更新。",
+ "releases.deactivateReasonPlaceholder": "您停用的原因是什么?",
+ "releases.keyDeactivatedToast": "签名密钥已停用",
+ "releases.deactivating": "停用中...",
} as const
export default zh
diff --git a/web/src/pages/admin/releases.tsx b/web/src/pages/admin/releases.tsx
index f3647f9..8db2e5b 100644
--- a/web/src/pages/admin/releases.tsx
+++ b/web/src/pages/admin/releases.tsx
@@ -50,6 +50,7 @@ import {
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
+import { useI18n } from "@/i18n"
import {
admin,
RELEASE_CHANNELS,
@@ -63,6 +64,7 @@ import { formatDate } from "@/lib/utils"
const PAGE_SIZE = 20
export default function ReleasesPage() {
+ const { t } = useI18n()
const qc = useQueryClient()
const [productFilter, setProductFilter] = useState("")
const [channelFilter, setChannelFilter] = useState("")
@@ -105,7 +107,7 @@ export default function ReleasesPage() {
mutationFn: admin.publishRelease,
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["admin", "releases"] })
- showToast("Release published", "success")
+ showToast(t("releases.releasePublishedToast"), "success")
},
onError: (e: Error) => showToast(e.message, "error"),
})
@@ -113,7 +115,7 @@ export default function ReleasesPage() {
mutationFn: admin.unyankRelease,
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["admin", "releases"] })
- showToast("Release unyanked", "success")
+ showToast(t("releases.releaseUnyankedToast"), "success")
setUnyanking(null)
},
onError: (e: Error) => showToast(e.message, "error"),
@@ -123,7 +125,7 @@ export default function ReleasesPage() {
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["admin", "releases"] })
setDeleting(null)
- showToast("Draft deleted", "success")
+ showToast(t("releases.draftDeletedToast"), "success")
},
onError: (e: Error) => showToast(e.message, "error"),
})
@@ -135,31 +137,27 @@ export default function ReleasesPage() {
return (
-
Releases
-
- Distribute software updates to your customers via Sparkle, Velopack, or Tauri.
-
+
{t("releases.title")}
+
{t("releases.subtitle")}
{hasAnyProducts ? (
<>
-
No release-eligible products
-
- Release feeds are available for desktop and hybrid products only. Your existing products are all SaaS
- — change a product's type or create a new desktop/hybrid one.
-
@@ -414,23 +415,23 @@ export default function ReleasesPage() {
setConfirmPublish(null)}>
- Publish a version below current latest?
+ {t("releases.publishBelowLatestTitle")}
- Publishing {confirmPublish.rel.version}, older than{" "}
- {confirmPublish.latest}. Velopack and Tauri reject downgrades by default; Sparkle's
- standard comparator is not SemVer-aware. This release will appear in your history out of order —
- appropriate for backports.
+ {t("releases.publishBelowLatestDesc", {
+ version: confirmPublish.rel.version,
+ latest: confirmPublish.latest,
+ })}