diff --git a/.gitignore b/.gitignore index 32f60fb..43b9cec 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ CodexSwitcher.app/ # Swift Package Manager Package.resolved Package.repos +.swiftpm/ diff --git a/Info.plist b/Info.plist index 1376e9e..c6da28e 100644 --- a/Info.plist +++ b/Info.plist @@ -29,6 +29,6 @@ SUPublicEDKey 7lqmueyvl3O06tlzS0bafDBXiWwB0fkyW0/xScLU/Fo= SUEnableAutomaticChecks - + diff --git a/Package.swift b/Package.swift index 498f46f..c598374 100644 --- a/Package.swift +++ b/Package.swift @@ -3,7 +3,8 @@ import PackageDescription let package = Package( name: "CodexSwitcher", - platforms: [.macOS(.v26)], + defaultLocalization: "en", + platforms: [.macOS(.v15)], dependencies: [ .package(url: "https://github.com/sparkle-project/Sparkle", from: "2.6.0") ], @@ -15,7 +16,7 @@ let package = Package( ], path: "Sources/CodexSwitcher", resources: [ - .copy("Resources/codex.icns") + .process("Resources") ] ) ] diff --git a/Sources/CodexSwitcher/AddAccountInlineView.swift b/Sources/CodexSwitcher/AddAccountInlineView.swift index c683e6e..8a9b032 100644 --- a/Sources/CodexSwitcher/AddAccountInlineView.swift +++ b/Sources/CodexSwitcher/AddAccountInlineView.swift @@ -84,7 +84,7 @@ struct AddAccountInlineView: View { Circle() .stroke(gw.opacity(0.06), lineWidth: 2) .frame(width: 52 + pulsePhase * 20, height: 52 + pulsePhase * 20) - .opacity(1 - pulsePhase * 0.8) + .opacity(1 - Double(pulsePhase) * 0.8) // Inner glass circle Circle() @@ -224,7 +224,11 @@ struct AddAccountInlineView: View { .font(.system(size: 14, weight: .semibold)) .foregroundStyle(gw.opacity(0.9)) - glassButton(Str.close, icon: "xmark") { store.closeAddAccountWindow() } + glassButton(Str.close, icon: "xmark") { + store.closeAddAccountWindow() + store.addingStep = .idle + store.isAddingAccount = false + } } } diff --git a/Sources/CodexSwitcher/AppStore.swift b/Sources/CodexSwitcher/AppStore.swift index 9b6d88f..86c500a 100644 --- a/Sources/CodexSwitcher/AppStore.swift +++ b/Sources/CodexSwitcher/AppStore.swift @@ -179,12 +179,13 @@ final class AppStore: ObservableObject { } } - // Consecutive failure gating + // Consecutive failure gating — back off for up to 3 skipped polls, then retry if successCount == 0 && !credPairs.isEmpty { consecutiveFetchFailures += 1 - if consecutiveFetchFailures >= 3 { - // Back off: skip next few polls + if consecutiveFetchFailures < 6 && consecutiveFetchFailures >= 3 { return + } else if consecutiveFetchFailures >= 6 { + consecutiveFetchFailures = 0 } } else { consecutiveFetchFailures = 0 @@ -605,7 +606,7 @@ final class AppStore: ObservableObject { stopAuthWatcher() closeAddAccountWindow() notifyProfileChanged() - sendNotification(title: "Hesap eklendi", body: profile.displayName) + sendNotification(title: L("Hesap eklendi", "Account added"), body: profile.displayName) Task { await fetchAllRateLimits() } } @@ -733,7 +734,7 @@ final class AppStore: ObservableObject { let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let tokens = dict["tokens"] as? [String: Any], let access = tokens["access_token"] as? String else { return } - pendingProfileEmail = profileManager.extractEmail(from: access) ?? "bilinmeyen" + pendingProfileEmail = profileManager.extractEmail(from: access) ?? "unknown" addingStep = .confirmProfile } else { // External modification detected — verify diff --git a/Sources/CodexSwitcher/L10n.swift b/Sources/CodexSwitcher/L10n.swift index 89666cf..5ec52da 100644 --- a/Sources/CodexSwitcher/L10n.swift +++ b/Sources/CodexSwitcher/L10n.swift @@ -1,17 +1,20 @@ import Foundation -/// Basit TR/EN yerelleştirme. -/// Önce UserDefaults'taki "appLanguage" key'ine bakar ("tr" / "en" / "system"). -/// "system" veya ayarlanmamışsa sistem dilini kullanır. +/// TR/EN localisation. English is the base language; Turkish ships as a standard +/// tr.lproj/Localizable.strings bundle loaded via NSLocalizedString. +/// UserDefaults key "appLanguage": "en" (default) | "tr" | "system" func L(_ tr: String, _ en: String) -> String { - let stored = UserDefaults.standard.string(forKey: "appLanguage") ?? "system" + let stored = UserDefaults.standard.string(forKey: "appLanguage") ?? "en" let code: String if stored == "system" { code = Locale.current.language.languageCode?.identifier ?? "en" } else { code = stored } - return code == "tr" ? tr : en + guard code == "tr" else { return en } + // Look up Turkish from the bundle; fall back to the hardcoded tr string + // for interpolated strings that can't be static .strings keys. + return NSLocalizedString(en, tableName: "Localizable", bundle: .module, value: tr, comment: "") } enum Str { diff --git a/Sources/CodexSwitcher/ProfileManager.swift b/Sources/CodexSwitcher/ProfileManager.swift index f3df6b9..25f74f6 100644 --- a/Sources/CodexSwitcher/ProfileManager.swift +++ b/Sources/CodexSwitcher/ProfileManager.swift @@ -155,7 +155,7 @@ final class ProfileManager: @unchecked Sendable { return nil } - let email = extractEmail(from: accessToken) ?? "bilinmeyen@hesap.com" + let email = extractEmail(from: accessToken) ?? "unknown@account.com" let profile = Profile( id: UUID(), alias: alias, @@ -270,13 +270,13 @@ enum SwitcherError: LocalizedError { var errorDescription: String? { switch self { case .missingAuthFile(let email): - return "Auth dosyası bulunamadı: \(email)" + return L("Auth dosyası bulunamadı: \(email)", "Auth file not found: \(email)") case .noProfilesAvailable: - return "Henüz hesap eklenmedi." + return L("Henüz hesap eklenmedi.", "No accounts added yet.") case .allProfilesExhausted: - return "Tüm hesapların limiti doldu!" + return L("Tüm hesapların limiti doldu!", "All accounts have reached their limit!") case .activationFailed(let email): - return "Aktivasyon başarısız: \(email)" + return L("Aktivasyon başarısız: \(email)", "Activation failed: \(email)") } } } diff --git a/Sources/CodexSwitcher/RateLimitFetcher.swift b/Sources/CodexSwitcher/RateLimitFetcher.swift index 5a54de7..68ca317 100644 --- a/Sources/CodexSwitcher/RateLimitFetcher.swift +++ b/Sources/CodexSwitcher/RateLimitFetcher.swift @@ -32,7 +32,7 @@ struct RateLimitInfo: Sendable { var weeklyResetLabel: String { guard let date = weeklyResetAt else { return "" } let fmt = DateFormatter() - fmt.locale = Locale(identifier: "tr_TR") + fmt.locale = Locale(identifier: "en_US_POSIX") fmt.dateFormat = "d MMM" return fmt.string(from: date) } @@ -40,7 +40,7 @@ struct RateLimitInfo: Sendable { var fiveHourResetLabel: String { guard let date = fiveHourResetAt else { return "" } let fmt = DateFormatter() - fmt.locale = Locale(identifier: "tr_TR") + fmt.locale = Locale(identifier: "en_US_POSIX") fmt.dateFormat = "HH:mm" return fmt.string(from: date) } diff --git a/Sources/CodexSwitcher/Resources/tr.lproj/Localizable.strings b/Sources/CodexSwitcher/Resources/tr.lproj/Localizable.strings new file mode 100644 index 0000000..298cd9b --- /dev/null +++ b/Sources/CodexSwitcher/Resources/tr.lproj/Localizable.strings @@ -0,0 +1,70 @@ +/* Str enum */ +"Add Account" = "Hesap Ekle"; +"Switch Now" = "Şimdi Geç"; +"Quit" = "Çıkış"; +"Weekly" = "Haftalık"; +"5 Hour" = "5 Saat"; +"Active Account" = "Aktif Hesap"; +"No accounts added" = "Hesap eklenmedi"; +"Rename" = "Yeniden Adlandır"; +"Delete" = "Sil"; +"Cancel" = "İptal"; +"Save" = "Kaydet"; +"Close" = "Kapat"; +"Start" = "Başlat"; +"Add New Account" = "Yeni Hesap Ekle"; +"Waiting for Login" = "Login Bekleniyor"; +"Account Detected!" = "Hesap Algılandı!"; +"Account Added!" = "Hesap Eklendi!"; +"Alias (optional)" = "Takma ad (opsiyonel)"; +"e.g. Work account" = "örn: İş hesabım"; +"Terminal will run `codex login`.\nSign in with your new account in the browser." = "Terminal'de `codex login` çalıştırılacak.\nBrowser'da yeni hesabınla giriş yap."; +"Sign in with your account in the browser.\nWill continue automatically when done." = "Browser'da hesabınla giriş yap.\nTamamlanınca otomatik devam eder."; +"Rename Account" = "Hesap Adını Değiştir"; +"All accounts exhausted!" = "Tüm hesaplar doldu!"; +"Switch anyway" = "Yine de geç"; +"last" = "son"; +"usage unknown" = "kullanım bilinmiyor"; +"Resets" = "Sıfırlanma"; +"History" = "Geçmiş"; +"No switches yet" = "Henüz geçiş yok"; +"Dark" = "Koyu"; +"Light" = "Açık"; +"Hide" = "Gizle"; +"Show" = "Göster"; +"Auto" = "Oto"; +"Back" = "Geri"; +"Limit reset" = "Limit sıfırlandı"; +"is ready to use again" = "kullanıma hazır"; + +/* MenuContentView */ +"Manual override" = "Manuel override"; +"Chart" = "Grafik"; +"Re-login" = "Girişi Yenile"; +"Update" = "Güncelle"; +"Reset" = "Sıfırla"; +"No usage data in the last 7 days" = "Son 7 günde kullanım verisi yok"; +"Last 7 days — Token usage" = "Son 7 gün — Token kullanımı"; + +/* AppStore */ +"Account" = "Hesap"; +"Limit warning" = "Limit uyarısı"; +"Manual switch" = "Manuel geçiş"; +"Will resume when limits reset." = "Limitler sıfırlanınca devam eder."; +"Manual selection" = "Manuel seçim"; +"Switch failed" = "Geçiş başarısız"; +"Account verification failed. Please try again." = "Hesap doğrulanamadı. Lütfen tekrar deneyin."; +"Account switched" = "Hesap değiştirildi"; +"Account Switched" = "Hesap değiştirildi"; +"Codex is restarting. New account is now active." = "Codex yeniden başlatılıyor. Yeni hesap aktif."; +"Limit reached" = "Limit doldu"; +"Account added" = "Hesap eklendi"; +"Re-login successful" = "Giriş yenilendi"; +"Wrong account" = "Hatalı hesap"; +"A different account was detected. Please try again." = "Farklı bir hesaba giriş yapıldı. Tekrar deneyin."; +"Auth issue" = "Auth sorunu"; +"Auth file corrupted. You may need to re-login to your accounts." = "Auth dosyası bozuldu. Hesapları yeniden giriş yapmanız gerekebilir."; + +/* ProfileManager errors */ +"No accounts added yet." = "Henüz hesap eklenmedi."; +"All accounts have reached their limit!" = "Tüm hesapların limiti doldu!"; diff --git a/build.sh b/build.sh index 8926032..4e2949b 100755 --- a/build.sh +++ b/build.sh @@ -17,6 +17,13 @@ cp "${BUILD_DIR}/${APP_NAME}" "${APP_BUNDLE}/Contents/MacOS/" cp "Info.plist" "${APP_BUNDLE}/Contents/" cp "Sources/CodexSwitcher/Resources/AppIcon.icns" "${APP_BUNDLE}/Contents/Resources/" +# Bundle Sparkle framework +mkdir -p "${APP_BUNDLE}/Contents/Frameworks" +cp -R "${BUILD_DIR}/Sparkle.framework" "${APP_BUNDLE}/Contents/Frameworks/" + +# Fix rpath so the binary finds Sparkle in Contents/Frameworks +install_name_tool -add_rpath "@executable_path/../Frameworks" "${APP_BUNDLE}/Contents/MacOS/${APP_NAME}" 2>/dev/null || true + # Ad-hoc sign — Gatekeeper "damaged" hatasını önler codesign --force --deep --sign - "${APP_BUNDLE}"