diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 1ecbde3..040aee1 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -17,13 +17,13 @@ "%lld" : { }, - "%lld Artikel in der Queue" : { + "%lld articles in the queue" : { }, "%lld min" : { }, - "%lld Minuten" : { + "%lld minutes" : { }, "%lld." : { @@ -47,6 +47,12 @@ }, "Abmelden" : { + }, + "About the App" : { + + }, + "Add a new link to your collection" : { + }, "Aktuelle Labels" : { @@ -61,99 +67,87 @@ } } } - }, - "Als Favorit markieren" : { - }, "Anmelden & speichern" : { }, - "Archivieren" : { + "Archive" : { }, - "Artikel automatisch als gelesen markieren" : { + "Archive bookmark" : { }, - "Artikel vorlesen" : { + "Automatic sync" : { }, - "Automatischer Sync" : { + "Automatically mark articles as read" : { }, - "Bookmark archivieren" : { + "Cancel" : { }, - "Bookmark speichern" : { + "Clear cache" : { }, - "Cache leeren" : { + "Clipboard" : { }, - "Datenmanagement" : { + "Close" : { }, - "Debug-Anmeldung" : { + "Data Management" : { }, - "Einfügen" : { + "Delete" : { }, - "Einstellungen" : { + "Developer: %@" : { }, - "Einstellungen speichern" : { + "Done" : { }, - "Einstellungen zurücksetzen" : { - - }, - "Entfernen" : { - - }, - "Entwickler: %@" : { + "e.g. work, important, later" : { }, "Erfolgreich angemeldet" : { - }, - "Erforderlich" : { - }, "Erneut anmelden & speichern" : { }, - "Es wurden noch keine Bookmarks in %@ gefunden." : { + "Error" : { }, - "Externe Links in In-App Safari öffnen" : { + "Error: %@" : { }, - "Favorit" : { + "Favorite" : { }, "Fehler" : { - }, - "Fehler: %@" : { - }, "Fertig" : { }, - "Fertig mit Lesen?" : { + "Finished reading?" : { }, - "Fortschritt: %lld%%" : { + "Font" : { }, - "Füge einen neuen Link zu deiner Sammlung hinzu" : { + "Font family" : { + + }, + "Font Settings" : { + + }, + "Font size" : { }, "Geben Sie Ihre Readeck-Server-Details ein, um zu beginnen." : { - }, - "Geschwindigkeit" : { - }, "https://example.com" : { @@ -163,12 +157,6 @@ }, "Ihre aktuelle Server-Verbindung und Anmeldedaten." : { - }, - "Keine Artikel in der Queue" : { - - }, - "Keine Bookmarks" : { - }, "Keine Bookmarks gefunden." : { @@ -191,68 +179,104 @@ "Labels verwalten" : { }, - "Lade %@..." : { + "Loading %@..." : { }, - "Lade Artikel..." : { + "Loading article..." : { }, - "Lese %lld/%lld: " : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Lese %1$lld/%2$lld: " - } - } - } - }, - "Leseeinstellungen" : { - - }, - "Löschen" : { - - }, - "Mehr" : { + "Mark as favorite" : { }, "Möchten Sie sich wirklich abmelden? Dies wird alle Ihre Anmeldedaten löschen und Sie zur Einrichtung zurückführen." : { }, - "Neues Bookmark" : { + "More" : { }, "Neues Label hinzufügen" : { + }, + "New Bookmark" : { + + }, + "No articles in the queue" : { + + }, + "No bookmarks" : { + + }, + "No bookmarks found in %@." : { + }, "OK" : { }, - "Optional: Eigener Titel" : { + "Open external links in in-app Safari" : { + + }, + "Optional: Custom title" : { }, "Password" : { + }, + "Paste" : { + + }, + "Preview" : { + + }, + "Progress: %lld%%" : { + + }, + "Read article aloud" : { + + }, + "Read-aloud Queue" : { + }, "readeck Bookmark Title" : { }, - "Safari Reader Modus" : { + "Reading %lld/%lld: " : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Reading %1$lld/%2$lld: " + } + } + } + }, + "Reading Settings" : { }, - "Schließen" : { + "Remove" : { }, - "Schrift" : { + "Required" : { }, - "Schrift-Einstellungen" : { + "Reset settings" : { }, - "Schriftart" : { + "Restore" : { }, - "Schriftgröße" : { + "Resume listening" : { + + }, + "Safari Reader Mode" : { + + }, + "Save bookmark" : { + + }, + "Save settings" : { + + }, + "Saving..." : { }, "Select a bookmark or tag" : { @@ -260,6 +284,12 @@ }, "Server-Endpunkt" : { + }, + "Settings" : { + + }, + "Speed" : { + }, "Speichern..." : { @@ -273,10 +303,10 @@ "Suche..." : { }, - "Sync-Einstellungen" : { + "Sync interval" : { }, - "Sync-Intervall" : { + "Sync Settings" : { }, "Theme" : { @@ -285,16 +315,13 @@ "This is how your bookmark descriptions and article text will appear in the app. The quick brown fox jumps over the lazy dog." : { }, - "Titel" : { - - }, - "Über die App" : { + "Title" : { }, "URL" : { }, - "URL gefunden:" : { + "URL found:" : { }, "Username" : { @@ -302,36 +329,15 @@ }, "Version %@" : { - }, - "Vorlese-Queue" : { - - }, - "Vorschau" : { - }, "Website" : { - }, - "Weiterhören" : { - - }, - "Wiederherstellen" : { - - }, - "Wird gespeichert..." : { - }, "Your Password" : { }, "Your Username" : { - }, - "z.B. arbeit, wichtig, später" : { - - }, - "Zwischenablage" : { - } }, "version" : "1.0" diff --git a/documentation/Architecture.md b/documentation/Architecture.md new file mode 100644 index 0000000..710241b --- /dev/null +++ b/documentation/Architecture.md @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: MIT + +# Architecture Overview: readeck client + +## 1. Introduction + +**readeck client** is an open-source iOS project for conveniently managing and reading bookmarks. The app uses the MVVM architecture pattern and follows a clear layer structure: **UI**, **Domain**, and **Data**. A key feature is its own dependency injection (DI) based on Swift protocols and the factory pattern—completely without external libraries. + +- **Architecture Pattern:** MVVM (Model-View-ViewModel) + Use Cases +- **Layers:** UI, Domain, Data +- **Technologies:** Swift, SwiftUI, CoreData, custom DI +- **DI:** Protocol-based, factory pattern, no external libraries + +## 2. Architecture Overview + +```mermaid +graph TD + UI["UI Layer\n(View, ViewModel)"] + Domain["Domain Layer\n(Use Cases, Models, Repository Protocols)"] + Data["Data Layer\n(Repository implementations, Database, Entities, API)"] + UI --> Domain + Domain --> Data +``` + +**Layer Overview:** + +| Layer | Responsibility | +|---------|----------------------| +| UI | Presentation, user interaction, ViewModels, bindings | +| Domain | Business logic, use cases, models, repository protocols | +| Data | Repository implementations, database, entities, API | + +## 3. Dependency Injection (DI) + +**Goal:** Loose coupling, better testability, exchangeability of implementations. + +**Approach:** +- Define protocols for dependencies (e.g., repository protocols) +- Implement the protocols in concrete classes +- Provide dependencies via a central factory +- Pass dependencies to ViewModels/use cases via initializers + +**Example:** + +```swift +// 1. Protocol definition +protocol PBookmarksRepository { + func getBookmarks() async throws -> [Bookmark] +} + +// 2. Implementation +class BookmarksRepository: PBookmarksRepository { + func getBookmarks() async throws -> [Bookmark] { + // ... + } +} + +// 3. Factory +class DefaultUseCaseFactory { + let bookmarksRepository: PBookmarksRepository = BookmarksRepository() + func makeGetBookmarksUseCase() -> GetBookmarksUseCase { + GetBookmarksUseCase(bookmarksRepository: bookmarksRepository) + } +} + +// 4. ViewModel +class BookmarksViewModel: ObservableObject { + private let getBookmarksUseCase: GetBookmarksUseCase + init(factory: DefaultUseCaseFactory) { + self.getBookmarksUseCase = factory.makeGetBookmarksUseCase() + } +} +``` + +**Advantages:** +- Exchangeability (e.g., for tests) +- No dependency on frameworks +- Central management of all dependencies + +## 4. Component Description + +| Component | Responsibility | +|---------------------|---------------| +| View | UI elements, presentation, user interaction | +| ViewModel | Bridge between View & Domain, state management | +| Use Case | Encapsulates a business logic (e.g., create bookmark) | +| Repository Protocol | Interface between Domain & Data layer | +| Repository Implementation | Concrete implementation of repository protocols, handles data access | +| Data Source / API | Access to external data sources (API, CoreData, Keychain) | +| Model/Entity | Represents core data structures | +| Dependency Factory | Creates and manages dependencies, central DI point | + +## 5. Data Flow + +1. **User interaction** in the view triggers an action in the ViewModel. +2. The **ViewModel** calls a **use case**. +3. The **use case** uses a **repository protocol** to load/save data. +4. The **repository implementation** accesses a **data source** (e.g., API, CoreData). +5. The response flows back up to the view and is displayed. + +## 6. Advantages of this Architecture + +- **Testability:** Protocols and DI allow components to be tested in isolation. +- **Maintainability:** Clear separation of concerns, easy extensibility. +- **Modularity:** Layers can be developed and adjusted independently. +- **Independence:** No dependency on external DI or architecture frameworks. + +## 7. Contributor Tips + +- **New dependencies:** Always define as a protocol and register in the factory. +- **Protocols:** Define in the domain layer, implement in the data layer. +- **Factory:** Extend the factory for new use cases or repositories. +- **No external frameworks:** Intentionally use custom solutions for better control and clarity. + +## 8. Glossary + +| Term | Definition | +|---------------------|------------| +| Dependency Injection| Technique for providing dependencies from the outside | +| Protocol | Swift interface that defines requirements for types | +| Factory Pattern | Design pattern for central object creation | +| MVVM | Architecture: Model-View-ViewModel | +| Use Case | Encapsulates a specific business logic | +| Repository Protocol | Interface in the domain layer for data access | +| Repository Implementation | Concrete class in the data layer that fulfills a repository protocol | +| Data Source | Implementation for data access (API, DB, etc.) | +| Model/Entity | Core data structure used in domain or data layer | + +## 9. Recommended Links + +- [Clean Architecture (Uncle Bob)](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) +- [Clean Architecture for Swift/iOS (adrian bilescu)](https://adrian-bilescu.medium.com/a-pragmatic-guide-to-clean-architecture-on-ios-e58d19d00559) +- [Swift.org: Protocols](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/protocols/) \ No newline at end of file diff --git a/readeck/Data/TokenProvider.swift b/readeck/Data/TokenProvider.swift index 33b8acb..1768054 100644 --- a/readeck/Data/TokenProvider.swift +++ b/readeck/Data/TokenProvider.swift @@ -14,7 +14,7 @@ class CoreDataTokenProvider: TokenProvider { private let keychainHelper = KeychainHelper.shared private func loadSettingsIfNeeded() async { - guard !isLoaded else { return } + guard isLoaded == false || cachedSettings == nil else { return } do { cachedSettings = try await settingsRepository.loadSettings() diff --git a/readeck/UI/AddBookmark/AddBookmarkView.swift b/readeck/UI/AddBookmark/AddBookmarkView.swift index 2ba5b15..fbde167 100644 --- a/readeck/UI/AddBookmark/AddBookmarkView.swift +++ b/readeck/UI/AddBookmark/AddBookmarkView.swift @@ -26,11 +26,11 @@ struct AddBookmarkView: View { .font(.system(size: 48)) .foregroundColor(.accentColor) - Text("Neues Bookmark") + Text("New Bookmark") .font(.title2) .fontWeight(.semibold) - Text("Füge einen neuen Link zu deiner Sammlung hinzu") + Text("Add a new link to your collection") .font(.subheadline) .foregroundColor(.secondary) .multilineTextAlignment(.center) @@ -48,7 +48,7 @@ struct AddBookmarkView: View { Spacer() - Text("Erforderlich") + Text("Required") .font(.caption) .foregroundColor(.red) } @@ -62,11 +62,11 @@ struct AddBookmarkView: View { // Title Field VStack(alignment: .leading, spacing: 8) { - Label("Titel", systemImage: "note.text") + Label("Title", systemImage: "note.text") .font(.headline) .foregroundColor(.primary) - TextField("Optional: Eigener Titel", text: $viewModel.title) + TextField("Optional: Custom title", text: $viewModel.title) .textFieldStyle(CustomTextFieldStyle()) } @@ -76,7 +76,7 @@ struct AddBookmarkView: View { .font(.headline) .foregroundColor(.primary) - TextField("z.B. arbeit, wichtig, später", text: $viewModel.labelsText) + TextField("e.g. work, important, later", text: $viewModel.labelsText) .textFieldStyle(CustomTextFieldStyle()) // Labels Preview @@ -102,13 +102,13 @@ struct AddBookmarkView: View { // Clipboard Section if viewModel.clipboardURL != nil { VStack(alignment: .leading, spacing: 12) { - Label("Zwischenablage", systemImage: "doc.on.clipboard") + Label("Clipboard", systemImage: "doc.on.clipboard") .font(.headline) .foregroundColor(.primary) HStack { VStack(alignment: .leading, spacing: 4) { - Text("URL gefunden:") + Text("URL found:") .font(.caption) .foregroundColor(.secondary) @@ -120,7 +120,7 @@ struct AddBookmarkView: View { Spacer() - Button("Einfügen") { + Button("Paste") { viewModel.pasteFromClipboard() } .buttonStyle(SecondaryButtonStyle()) @@ -133,7 +133,7 @@ struct AddBookmarkView: View { } .padding(.horizontal, 20) - Spacer(minLength: 100) // Platz für Button + Spacer(minLength: 100) // Space for button } } @@ -160,7 +160,7 @@ struct AddBookmarkView: View { Image(systemName: "bookmark.fill") } - Text(viewModel.isLoading ? "Wird gespeichert..." : "Bookmark speichern") + Text(viewModel.isLoading ? "Saving..." : "Save bookmark") .fontWeight(.semibold) } .frame(maxWidth: .infinity) @@ -172,7 +172,7 @@ struct AddBookmarkView: View { .disabled(!viewModel.isValid || viewModel.isLoading) // Cancel Button - Button("Abbrechen") { + Button("Cancel") { dismiss() viewModel.clearForm() } @@ -186,17 +186,17 @@ struct AddBookmarkView: View { .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { - Button("Schließen") { + Button("Close") { dismiss() viewModel.clearForm() } .foregroundColor(.secondary) } } - .alert("Fehler", isPresented: $viewModel.showErrorAlert) { + .alert("Error", isPresented: $viewModel.showErrorAlert) { Button("OK", role: .cancel) { } } message: { - Text(viewModel.errorMessage ?? "Unbekannter Fehler") + Text(viewModel.errorMessage ?? "Unknown error") } } .onAppear { diff --git a/readeck/UI/AddBookmark/AddBookmarkViewModel.swift b/readeck/UI/AddBookmark/AddBookmarkViewModel.swift index 98d9df8..46c1664 100644 --- a/readeck/UI/AddBookmark/AddBookmarkViewModel.swift +++ b/readeck/UI/AddBookmark/AddBookmarkViewModel.swift @@ -48,7 +48,7 @@ class AddBookmarkViewModel { let message = try await createBookmarkUseCase.execute(createRequest: request) - // Optional: Zeige die Server-Nachricht an + // Optional: Show the server message print("Server response: \(message)") clearForm() @@ -57,7 +57,7 @@ class AddBookmarkViewModel { errorMessage = error.localizedDescription showErrorAlert = true } catch { - errorMessage = "Fehler beim Erstellen des Bookmarks" + errorMessage = "Error creating bookmark" showErrorAlert = true } diff --git a/readeck/UI/BookmarkDetail/BookmarkDetailView.swift b/readeck/UI/BookmarkDetail/BookmarkDetailView.swift index 49e1fdd..196a0db 100644 --- a/readeck/UI/BookmarkDetail/BookmarkDetailView.swift +++ b/readeck/UI/BookmarkDetail/BookmarkDetailView.swift @@ -68,11 +68,11 @@ struct BookmarkDetailView: View { Spacer() } - .navigationTitle("Schrift-Einstellungen") + .navigationTitle("Font Settings") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { - Button("Fertig") { + Button("Done") { showingFontSettings = false } } @@ -173,7 +173,7 @@ struct BookmarkDetailView: View { .padding(.horizontal) .animation(.easeInOut, value: webViewHeight) } else if viewModel.isLoadingArticle { - ProgressView("Lade Artikel...") + ProgressView("Loading article...") .frame(maxWidth: .infinity, alignment: .center) .padding() } else { @@ -182,7 +182,7 @@ struct BookmarkDetailView: View { }) { HStack { Image(systemName: "safari") - Text((URLUtil.extractDomain(from: viewModel.bookmarkDetail.url) ?? "Original Seite") + " öffnen") + Text((URLUtil.extractDomain(from: viewModel.bookmarkDetail.url) ?? "Open original page") + " open") } .font(.title3.bold()) .frame(maxWidth: .infinity) @@ -196,10 +196,10 @@ struct BookmarkDetailView: View { private var metaInfoSection: some View { VStack(alignment: .leading, spacing: 8) { if !viewModel.bookmarkDetail.authors.isEmpty { - metaRow(icon: "person", text: (viewModel.bookmarkDetail.authors.count > 1 ? "Autor:innen: " : "Autor: ") + viewModel.bookmarkDetail.authors.joined(separator: ", ")) + metaRow(icon: "person", text: (viewModel.bookmarkDetail.authors.count > 1 ? "Authors: " : "Author: ") + viewModel.bookmarkDetail.authors.joined(separator: ", ")) } metaRow(icon: "calendar", text: formatDate(viewModel.bookmarkDetail.created)) - metaRow(icon: "textformat", text: "\(viewModel.bookmarkDetail.wordCount ?? 0) Wörter • \(viewModel.bookmarkDetail.readingTime ?? 0) min Lesezeit") + metaRow(icon: "textformat", text: "\(viewModel.bookmarkDetail.wordCount ?? 0) words • \(viewModel.bookmarkDetail.readingTime ?? 0) min read") // Labels section if !viewModel.bookmarkDetail.labels.isEmpty { @@ -236,7 +236,7 @@ struct BookmarkDetailView: View { Button(action: { SafariUtil.openInSafari(url: viewModel.bookmarkDetail.url) }) { - Text((URLUtil.extractDomain(from: viewModel.bookmarkDetail.url) ?? "Original Seite") + " öffnen") + Text((URLUtil.extractDomain(from: viewModel.bookmarkDetail.url) ?? "Open original page") + " open") .font(.subheadline) .foregroundColor(.secondary) } @@ -247,7 +247,7 @@ struct BookmarkDetailView: View { viewModel.addBookmarkToSpeechQueue() playerUIState.showPlayer() }) { - Text("Artikel vorlesen") + Text("Read article aloud") .font(.subheadline) .foregroundColor(.secondary) } @@ -296,7 +296,7 @@ struct BookmarkDetailView: View { private var archiveSection: some View { VStack(alignment: .center, spacing: 12) { - Text("Fertig mit Lesen?") + Text("Finished reading?") .font(.headline) .padding(.top, 24) VStack(alignment: .center, spacing: 16) { @@ -308,7 +308,7 @@ struct BookmarkDetailView: View { HStack { Image(systemName: viewModel.bookmarkDetail.isMarked ? "star.fill" : "star") .foregroundColor(viewModel.bookmarkDetail.isMarked ? .yellow : .gray) - Text(viewModel.bookmarkDetail.isMarked ? "Favorit" : "Als Favorit markieren") + Text(viewModel.bookmarkDetail.isMarked ? "Favorite" : "Mark as favorite") } .font(.title3.bold()) .frame(maxHeight: 60) @@ -325,7 +325,7 @@ struct BookmarkDetailView: View { }) { HStack { Image(systemName: "archivebox") - Text("Bookmark archivieren") + Text("Archive bookmark") } .font(.title3.bold()) .frame(maxHeight: 60) diff --git a/readeck/UI/BookmarkDetail/BookmarkDetailViewModel.swift b/readeck/UI/BookmarkDetail/BookmarkDetailViewModel.swift index d8474bb..caf29ef 100644 --- a/readeck/UI/BookmarkDetail/BookmarkDetailViewModel.swift +++ b/readeck/UI/BookmarkDetail/BookmarkDetailViewModel.swift @@ -34,7 +34,7 @@ class BookmarkDetailViewModel { settings = try await loadSettingsUseCase.execute() bookmarkDetail = try await getBookmarkUseCase.execute(id: id) } catch { - errorMessage = "Fehler beim Laden des Bookmarks" + errorMessage = "Error loading bookmark" } isLoading = false @@ -48,7 +48,7 @@ class BookmarkDetailViewModel { articleContent = try await getBookmarkArticleUseCase.execute(id: id) processArticleContent() } catch { - errorMessage = "Fehler beim Laden des Artikels" + errorMessage = "Error loading article" } isLoadingArticle = false @@ -70,7 +70,7 @@ class BookmarkDetailViewModel { try await updateBookmarkUseCase.toggleArchive(bookmarkId: id, isArchived: true) bookmarkDetail.isArchived = true } catch { - errorMessage = "Fehler beim Archivieren des Bookmarks" + errorMessage = "Error archiving bookmark" } isLoading = false } @@ -94,7 +94,7 @@ class BookmarkDetailViewModel { try await updateBookmarkUseCase.toggleFavorite(bookmarkId: id, isMarked: newValue) bookmarkDetail.isMarked = newValue } catch { - errorMessage = "Fehler beim Aktualisieren des Favoriten-Status" + errorMessage = "Error updating favorite status" } isLoading = false } diff --git a/readeck/UI/BookmarkDetail/BookmarkLabelsView.swift b/readeck/UI/BookmarkDetail/BookmarkLabelsView.swift index 3bba474..3c9556e 100644 --- a/readeck/UI/BookmarkDetail/BookmarkLabelsView.swift +++ b/readeck/UI/BookmarkDetail/BookmarkLabelsView.swift @@ -26,37 +26,37 @@ struct BookmarkLabelsView: View { } .padding() .background(Color(.systemGroupedBackground)) - .navigationTitle("Labels verwalten") + .navigationTitle("Manage Labels") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarLeading) { - Button("Abbrechen") { + Button("Cancel") { dismiss() } } ToolbarItem(placement: .navigationBarTrailing) { - Button("Fertig") { + Button("Done") { dismiss() } } } - .alert("Fehler", isPresented: $viewModel.showErrorAlert) { + .alert("Error", isPresented: $viewModel.showErrorAlert) { Button("OK") { } } message: { - Text(viewModel.errorMessage ?? "Unbekannter Fehler") + Text(viewModel.errorMessage ?? "Unknown error") } } } private var addLabelSection: some View { VStack(alignment: .leading, spacing: 12) { - Text("Neues Label hinzufügen") + Text("Add new label") .font(.headline) .foregroundColor(.primary) HStack(spacing: 12) { - TextField("Label eingeben...", text: $viewModel.newLabelText) + TextField("Enter label...", text: $viewModel.newLabelText) .textFieldStyle(RoundedBorderTextFieldStyle()) .onSubmit { Task { @@ -91,7 +91,7 @@ struct BookmarkLabelsView: View { private var currentLabelsSection: some View { VStack(alignment: .leading, spacing: 8) { HStack { - Text("Aktuelle Labels") + Text("Current labels") .font(.headline) .foregroundColor(.primary) @@ -108,7 +108,7 @@ struct BookmarkLabelsView: View { Image(systemName: "tag") .font(.title2) .foregroundColor(.secondary) - Text("Keine Labels vorhanden") + Text("No labels available") .font(.subheadline) .foregroundColor(.secondary) } diff --git a/readeck/UI/BookmarkDetail/BookmarkLabelsViewModel.swift b/readeck/UI/BookmarkDetail/BookmarkLabelsViewModel.swift index e43b01f..7b47811 100644 --- a/readeck/UI/BookmarkDetail/BookmarkLabelsViewModel.swift +++ b/readeck/UI/BookmarkDetail/BookmarkLabelsViewModel.swift @@ -29,7 +29,7 @@ class BookmarkLabelsViewModel { errorMessage = error.localizedDescription showErrorAlert = true } catch { - errorMessage = "Fehler beim Hinzufügen der Labels" + errorMessage = "Error adding labels" showErrorAlert = true } @@ -58,7 +58,7 @@ class BookmarkLabelsViewModel { errorMessage = error.localizedDescription showErrorAlert = true } catch { - errorMessage = "Fehler beim Entfernen der Labels" + errorMessage = "Error removing labels" showErrorAlert = true } diff --git a/readeck/UI/Bookmarks/BookmarkCardView.swift b/readeck/UI/Bookmarks/BookmarkCardView.swift index 468f31c..8e77f22 100644 --- a/readeck/UI/Bookmarks/BookmarkCardView.swift +++ b/readeck/UI/Bookmarks/BookmarkCardView.swift @@ -37,7 +37,7 @@ struct BookmarkCardView: View { VStack(alignment: .leading, spacing: 4) { HStack { - // Veröffentlichungsdatum + // Published date if let publishedDate = formattedPublishedDate { HStack { Label(publishedDate, systemImage: "calendar") @@ -59,7 +59,7 @@ struct BookmarkCardView: View { } HStack { - Label((URLUtil.extractDomain(from: bookmark.url) ?? "Original Seite") + " öffnen", systemImage: "safari") + Label((URLUtil.extractDomain(from: bookmark.url) ?? "Original Site") + " open", systemImage: "safari") .onTapGesture { SafariUtil.openInSafari(url: bookmark.url) } @@ -68,7 +68,7 @@ struct BookmarkCardView: View { .font(.caption) .foregroundColor(.secondary) - // Progress Bar für Lesefortschritt + // Progress Bar for reading progress if bookmark.readProgress > 0 { ProgressView(value: Double(bookmark.readProgress), total: 100) .progressViewStyle(LinearProgressViewStyle()) @@ -82,20 +82,20 @@ struct BookmarkCardView: View { .clipShape(RoundedRectangle(cornerRadius: 12)) .shadow(color: colorScheme == .light ? .black.opacity(0.1) : .white.opacity(0.1), radius: 2, x: 0, y: 1) .swipeActions(edge: .trailing, allowsFullSwipe: true) { - Button("Löschen", role: .destructive) { + Button("Delete", role: .destructive) { onDelete(bookmark) } .tint(.red) } .swipeActions(edge: .leading, allowsFullSwipe: true) { - // Archivieren (links) + // Archive (left) Button { onArchive(bookmark) } label: { if currentState == .archived { - Label("Wiederherstellen", systemImage: "tray.and.arrow.up") + Label("Restore", systemImage: "tray.and.arrow.up") } else { - Label("Archivieren", systemImage: "archivebox") + Label("Archive", systemImage: "archivebox") } } .tint(currentState == .archived ? .blue : .orange) @@ -103,7 +103,7 @@ struct BookmarkCardView: View { Button { onToggleFavorite(bookmark) } label: { - Label(bookmark.isMarked ? "Entfernen" : "Favorit", + Label(bookmark.isMarked ? "Remove" : "Favorite", systemImage: bookmark.isMarked ? "heart.slash" : "heart.fill") } .tint(bookmark.isMarked ? .gray : .pink) @@ -127,7 +127,7 @@ struct BookmarkCardView: View { formatter.locale = Locale(identifier: "en_US_POSIX") guard let date = formatter.date(from: published) else { - // Fallback ohne Millisekunden + // Fallback without milliseconds formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'" guard let fallbackDate = formatter.date(from: published) else { return nil @@ -142,42 +142,42 @@ struct BookmarkCardView: View { let now = Date() let calendar = Calendar.current - // Heute + // Today if calendar.isDateInToday(date) { let formatter = DateFormatter() formatter.timeStyle = .short - return "Heute, \(formatter.string(from: date))" + return "Today, \(formatter.string(from: date))" } - // Gestern + // Yesterday if calendar.isDateInYesterday(date) { let formatter = DateFormatter() formatter.timeStyle = .short - return "Gestern, \(formatter.string(from: date))" + return "Yesterday, \(formatter.string(from: date))" } - // Diese Woche + // This week if calendar.isDate(date, equalTo: now, toGranularity: .weekOfYear) { let formatter = DateFormatter() formatter.dateFormat = "EEEE, HH:mm" return formatter.string(from: date) } - // Dieses Jahr + // This year if calendar.isDate(date, equalTo: now, toGranularity: .year) { let formatter = DateFormatter() formatter.dateFormat = "d. MMM, HH:mm" return formatter.string(from: date) } - // Andere Jahre + // Other years let formatter = DateFormatter() formatter.dateFormat = "d. MMM yyyy" return formatter.string(from: date) } private var imageURL: URL? { - // Bevorzuge image, dann thumbnail, dann icon + // Prioritize image, then thumbnail, then icon if let imageUrl = bookmark.resources.image?.src { return URL(string: imageUrl) } else if let thumbnailUrl = bookmark.resources.thumbnail?.src { diff --git a/readeck/UI/Bookmarks/BookmarksView.swift b/readeck/UI/Bookmarks/BookmarksView.swift index ce07f90..db42e96 100644 --- a/readeck/UI/Bookmarks/BookmarksView.swift +++ b/readeck/UI/Bookmarks/BookmarksView.swift @@ -19,7 +19,14 @@ struct BookmarksView: View { @EnvironmentObject var playerUIState: PlayerUIState let tag: String? + + // MARK: Environments + + @Environment(\.horizontalSizeClass) var horizontalSizeClass + @Environment(\.verticalSizeClass) var verticalSizeClass + // MARK: Initializer + init(viewModel: BookmarksViewModel = .init(), state: BookmarkState, type: [BookmarkType], selectedBookmark: Binding, tag: String? = nil) { self.state = state self.type = type @@ -27,16 +34,11 @@ struct BookmarksView: View { self.tag = tag self.viewModel = viewModel } - - // MARK: Environments - - @Environment(\.horizontalSizeClass) var horizontalSizeClass - @Environment(\.verticalSizeClass) var verticalSizeClass var body: some View { ZStack { if viewModel.isLoading && viewModel.bookmarks?.bookmarks.isEmpty == true { - ProgressView("Lade \(state.displayName)...") + ProgressView("Loading \(state.displayName)...") } else { List { ForEach(viewModel.bookmarks?.bookmarks ?? [], id: \.id) { bookmark in @@ -96,17 +98,17 @@ struct BookmarksView: View { .overlay { if viewModel.bookmarks?.bookmarks.isEmpty == true && !viewModel.isLoading { ContentUnavailableView( - "Keine Bookmarks", + "No bookmarks", systemImage: "bookmark", description: Text( - "Es wurden noch keine Bookmarks in \(state.displayName.lowercased()) gefunden." + "No bookmarks found in \(state.displayName.lowercased())." ) ) } } } - // FAB Button - nur bei "Ungelesen" anzeigen + // FAB Button - only show for "Unread" if state == .unread || state == .all { VStack { Spacer() diff --git a/readeck/UI/Bookmarks/BookmarksViewModel.swift b/readeck/UI/Bookmarks/BookmarksViewModel.swift index ab0088a..d2b749c 100644 --- a/readeck/UI/Bookmarks/BookmarksViewModel.swift +++ b/readeck/UI/Bookmarks/BookmarksViewModel.swift @@ -61,8 +61,6 @@ class BookmarksViewModel { self.shareTitle = userInfo["title"] as? String ?? "" self.showingAddBookmarkFromShare = true } - - print("Received share notification - URL: \(url)") } private func throttleSearch() { @@ -87,8 +85,8 @@ class BookmarksViewModel { currentType = type currentTag = tag - offset = 0 // Offset zurücksetzen - hasMoreData = true // Pagination zurücksetzen + offset = 0 + hasMoreData = true do { let newBookmarks = try await getBooksmarksUseCase.execute( @@ -100,9 +98,9 @@ class BookmarksViewModel { tag: tag ) bookmarks = newBookmarks - hasMoreData = newBookmarks.currentPage != newBookmarks.totalPages // Prüfen, ob weitere Daten verfügbar sind + hasMoreData = newBookmarks.currentPage != newBookmarks.totalPages // check if more data is available } catch { - errorMessage = "Fehler beim Laden der Bookmarks" + errorMessage = "Error loading bookmarks" bookmarks = nil } @@ -111,13 +109,13 @@ class BookmarksViewModel { @MainActor func loadMoreBookmarks() async { - guard !isLoading && hasMoreData else { return } // Verhindern, dass mehrfach geladen wird + guard !isLoading && hasMoreData else { return } // prevent multiple loads isLoading = true errorMessage = nil do { - offset += limit // Offset erhöhen + offset += limit // inc. offset let newBookmarks = try await getBooksmarksUseCase.execute( state: currentState, limit: limit, @@ -128,7 +126,7 @@ class BookmarksViewModel { bookmarks?.bookmarks.append(contentsOf: newBookmarks.bookmarks) hasMoreData = newBookmarks.currentPage != newBookmarks.totalPages } catch { - errorMessage = "Fehler beim Nachladen der Bookmarks" + errorMessage = "Error loading more bookmarks" } isLoading = false @@ -147,11 +145,10 @@ class BookmarksViewModel { isArchived: !bookmark.isArchived ) - // Liste aktualisieren await loadBookmarks(state: currentState) } catch { - errorMessage = "Fehler beim Archivieren des Bookmarks" + errorMessage = "Error archiving bookmark" } } @@ -163,26 +160,21 @@ class BookmarksViewModel { isMarked: !bookmark.isMarked ) - // Liste aktualisieren await loadBookmarks(state: currentState) } catch { - errorMessage = "Fehler beim Markieren des Bookmarks" + errorMessage = "Error marking bookmark" } } @MainActor func deleteBookmark(bookmark: Bookmark) async { do { - // Echtes Löschen über API statt nur als gelöscht markieren try await deleteBookmarkUseCase.execute(bookmarkId: bookmark.id) - - // Lokal aus der Liste entfernen (optimistische Update) bookmarks?.bookmarks.removeAll { $0.id == bookmark.id } } catch { - errorMessage = "Fehler beim Löschen des Bookmarks" - // Bei Fehler die Liste neu laden, um konsistenten Zustand zu haben + errorMessage = "Error deleting bookmark" await loadBookmarks(state: currentState) } } diff --git a/readeck/UI/Labels/LabelsView.swift b/readeck/UI/Labels/LabelsView.swift index 41b8bf9..23d47db 100644 --- a/readeck/UI/Labels/LabelsView.swift +++ b/readeck/UI/Labels/LabelsView.swift @@ -10,7 +10,7 @@ struct LabelsView: View { if viewModel.isLoading { ProgressView() } else if let errorMessage = viewModel.errorMessage { - Text("Fehler: \(errorMessage)") + Text("Error: \(errorMessage)") .foregroundColor(.red) } else { List { diff --git a/readeck/UI/Labels/LabelsViewModel.swift b/readeck/UI/Labels/LabelsViewModel.swift index 1f2c188..2cce2d4 100644 --- a/readeck/UI/Labels/LabelsViewModel.swift +++ b/readeck/UI/Labels/LabelsViewModel.swift @@ -16,7 +16,7 @@ class LabelsViewModel { do { labels = try await getLabelsUseCase.execute() } catch { - errorMessage = "Fehler beim Laden der Labels" + errorMessage = "Error loading labels" } isLoading = false } diff --git a/readeck/UI/Menu/BookmarkState.swift b/readeck/UI/Menu/BookmarkState.swift index 85b7873..a26fa2d 100644 --- a/readeck/UI/Menu/BookmarkState.swift +++ b/readeck/UI/Menu/BookmarkState.swift @@ -14,13 +14,13 @@ enum BookmarkState: String, CaseIterable { var displayName: String { switch self { case .all: - return "Alle" + return "All" case .unread: - return "Ungelesen" + return "Unread" case .favorite: - return "Favoriten" + return "Favorites" case .archived: - return "Archiv" + return "Archive" } } diff --git a/readeck/UI/Menu/PhoneTabView.swift b/readeck/UI/Menu/PhoneTabView.swift index 6303f30..861a6d5 100644 --- a/readeck/UI/Menu/PhoneTabView.swift +++ b/readeck/UI/Menu/PhoneTabView.swift @@ -44,7 +44,7 @@ struct PhoneTabView: View { } .listRowBackground(Color(R.color.bookmark_list_bg)) } - .navigationTitle("Mehr") + .navigationTitle("More") .scrollContentBackground(.hidden) .background(Color(R.color.bookmark_list_bg)) @@ -52,7 +52,7 @@ struct PhoneTabView: View { .padding(.bottom, 16) } .tabItem { - Label("Mehr", systemImage: "ellipsis") + Label("More", systemImage: "ellipsis") } .tag(mainTabs.count) .onAppear { diff --git a/readeck/UI/Menu/PlayerQueueResumeButton.swift b/readeck/UI/Menu/PlayerQueueResumeButton.swift index 8153550..ff60e43 100644 --- a/readeck/UI/Menu/PlayerQueueResumeButton.swift +++ b/readeck/UI/Menu/PlayerQueueResumeButton.swift @@ -13,10 +13,10 @@ struct PlayerQueueResumeButton: View { }) { HStack(spacing: 12) { VStack(alignment: .leading, spacing: 2) { - Text("Vorlese-Queue") + Text("Read-aloud Queue") .font(.caption2) .foregroundColor(.secondary) - Text("\(queue.queueItems.count) Artikel in der Queue") + Text("\(queue.queueItems.count) articles in the queue") .font(.subheadline) .foregroundColor(.primary) } @@ -25,7 +25,7 @@ struct PlayerQueueResumeButton: View { playerViewModel.resume() playerUIState.showPlayer() }) { - Text("Weiterhören") + Text("Resume listening") .font(.subheadline) .fontWeight(.semibold) .padding(.horizontal, 14) @@ -48,4 +48,4 @@ struct PlayerQueueResumeButton: View { .animation(.spring(), value: queue.hasItems) } } -} \ No newline at end of file +} diff --git a/readeck/UI/Menu/SidebarTab.swift b/readeck/UI/Menu/SidebarTab.swift index edbfb84..245fec1 100644 --- a/readeck/UI/Menu/SidebarTab.swift +++ b/readeck/UI/Menu/SidebarTab.swift @@ -13,14 +13,14 @@ enum SidebarTab: Hashable, CaseIterable, Identifiable { var label: String { switch self { case .all: return "All" - case .unread: return "Ungelesen" - case .favorite: return "Favoriten" - case .archived: return "Archiv" - case .search: return "Suche" - case .settings: return "Einstellungen" - case .article: return "Artikel" + case .unread: return "Unread" + case .favorite: return "Favorites" + case .archived: return "Archive" + case .search: return "Search" + case .settings: return "Settings" + case .article: return "Articles" case .videos: return "Videos" - case .pictures: return "Bilder" + case .pictures: return "Pictures" case .tags: return "Tags" } } diff --git a/readeck/UI/Settings/FontSettingsView.swift b/readeck/UI/Settings/FontSettingsView.swift index df0cc30..87efb88 100644 --- a/readeck/UI/Settings/FontSettingsView.swift +++ b/readeck/UI/Settings/FontSettingsView.swift @@ -22,16 +22,16 @@ struct FontSettingsView: View { .font(.title2) .foregroundColor(.accentColor) - Text("Schrift") + Text("Font") .font(.title2) .fontWeight(.bold) } // Font Family Picker HStack(alignment: .firstTextBaseline, spacing: 16) { - Text("Schriftart") + Text("Font family") .font(.headline) - Picker("Schriftart", selection: $viewModel.selectedFontFamily) { + Picker("Font family", selection: $viewModel.selectedFontFamily) { ForEach(FontFamily.allCases, id: \.self) { family in Text(family.displayName).tag(family) } @@ -48,9 +48,9 @@ struct FontSettingsView: View { // Font Size Picker VStack(alignment: .leading, spacing: 8) { - Text("Schriftgröße") + Text("Font size") .font(.headline) - Picker("Schriftgröße", selection: $viewModel.selectedFontSize) { + Picker("Font size", selection: $viewModel.selectedFontSize) { ForEach(FontSize.allCases, id: \.self) { size in Text(size.displayName).tag(size) } @@ -65,7 +65,7 @@ struct FontSettingsView: View { // Font Preview VStack(alignment: .leading, spacing: 8) { - Text("Vorschau") + Text("Preview") .font(.caption) .foregroundColor(.secondary) diff --git a/readeck/UI/Settings/FontSettingsViewModel.swift b/readeck/UI/Settings/FontSettingsViewModel.swift index cf7f45e..374c1dc 100644 --- a/readeck/UI/Settings/FontSettingsViewModel.swift +++ b/readeck/UI/Settings/FontSettingsViewModel.swift @@ -76,7 +76,7 @@ class FontSettingsViewModel { selectedFontSize = settings.fontSize ?? .medium } } catch { - errorMessage = "Fehler beim Laden der Schrift-Einstellungen" + errorMessage = "Error loading font settings" } } @@ -87,9 +87,9 @@ class FontSettingsViewModel { selectedFontFamily: selectedFontFamily, selectedFontSize: selectedFontSize ) - successMessage = "Schrift-Einstellungen gespeichert" + successMessage = "Font settings saved" } catch { - errorMessage = "Fehler beim Speichern der Schrift-Einstellungen" + errorMessage = "Error saving font settings" } } diff --git a/readeck/UI/Settings/SettingsContainerView.swift b/readeck/UI/Settings/SettingsContainerView.swift index 4983fa2..dd9cf60 100644 --- a/readeck/UI/Settings/SettingsContainerView.swift +++ b/readeck/UI/Settings/SettingsContainerView.swift @@ -23,7 +23,7 @@ struct SettingsContainerView: View { .padding() .background(Color(.systemGroupedBackground)) } - .navigationTitle("Einstellungen") + .navigationTitle("Settings") .navigationBarTitleDisplayMode(.large) } } diff --git a/readeck/UI/Settings/SettingsGeneralView.swift b/readeck/UI/Settings/SettingsGeneralView.swift index ba96bac..8e9da56 100644 --- a/readeck/UI/Settings/SettingsGeneralView.swift +++ b/readeck/UI/Settings/SettingsGeneralView.swift @@ -6,7 +6,6 @@ // import SwiftUI -// SectionHeader wird jetzt zentral importiert struct SettingsGeneralView: View { @State private var viewModel: SettingsGeneralViewModel @@ -17,7 +16,7 @@ struct SettingsGeneralView: View { var body: some View { VStack(spacing: 20) { - SectionHeader(title: "Allgemeine Einstellungen", icon: "gear") + SectionHeader(title: "General Settings", icon: "gear") .padding(.bottom, 4) // Theme @@ -34,34 +33,34 @@ struct SettingsGeneralView: View { // Sync Settings VStack(alignment: .leading, spacing: 12) { - Text("Sync-Einstellungen") + Text("Sync Settings") .font(.headline) - Toggle("Automatischer Sync", isOn: $viewModel.autoSyncEnabled) + Toggle("Automatic sync", isOn: $viewModel.autoSyncEnabled) .toggleStyle(SwitchToggleStyle()) if viewModel.autoSyncEnabled { HStack { - Text("Sync-Intervall") + Text("Sync interval") Spacer() - Stepper("\(viewModel.syncInterval) Minuten", value: $viewModel.syncInterval, in: 1...60) + Stepper("\(viewModel.syncInterval) minutes", value: $viewModel.syncInterval, in: 1...60) } } } // Reading Settings VStack(alignment: .leading, spacing: 12) { - Text("Leseeinstellungen") + Text("Reading Settings") .font(.headline) - Toggle("Safari Reader Modus", isOn: $viewModel.enableReaderMode) + Toggle("Safari Reader Mode", isOn: $viewModel.enableReaderMode) .toggleStyle(SwitchToggleStyle()) - Toggle("Externe Links in In-App Safari öffnen", isOn: $viewModel.openExternalLinksInApp) + Toggle("Open external links in in-app Safari", isOn: $viewModel.openExternalLinksInApp) .toggleStyle(SwitchToggleStyle()) - Toggle("Artikel automatisch als gelesen markieren", isOn: $viewModel.autoMarkAsRead) + Toggle("Automatically mark articles as read", isOn: $viewModel.autoMarkAsRead) .toggleStyle(SwitchToggleStyle()) } // Data Management VStack(alignment: .leading, spacing: 12) { - Text("Datenmanagement") + Text("Data Management") .font(.headline) Button(role: .destructive) { Task { @@ -71,7 +70,7 @@ struct SettingsGeneralView: View { HStack { Image(systemName: "trash") .foregroundColor(.red) - Text("Cache leeren") + Text("Clear cache") .foregroundColor(.red) Spacer() } @@ -84,7 +83,7 @@ struct SettingsGeneralView: View { HStack { Image(systemName: "arrow.clockwise") .foregroundColor(.red) - Text("Einstellungen zurücksetzen") + Text("Reset settings") .foregroundColor(.red) Spacer() } @@ -93,7 +92,7 @@ struct SettingsGeneralView: View { // App Info VStack(alignment: .leading, spacing: 12) { - Text("Über die App") + Text("About the App") .font(.headline) HStack { Image(systemName: "info.circle") @@ -104,7 +103,7 @@ struct SettingsGeneralView: View { HStack { Image(systemName: "person.crop.circle") .foregroundColor(.secondary) - Text("Entwickler: \(viewModel.developerName)") + Text("Developer: \(viewModel.developerName)") Spacer() } HStack { @@ -122,7 +121,7 @@ struct SettingsGeneralView: View { } }) { HStack { - Text("Einstellungen speichern") + Text("Save settings") .fontWeight(.semibold) } .frame(maxWidth: .infinity) @@ -165,8 +164,8 @@ enum Theme: String, CaseIterable { var displayName: String { switch self { case .system: return "System" - case .light: return "Hell" - case .dark: return "Dunkel" + case .light: return "Light" + case .dark: return "Dark" } } } diff --git a/readeck/UI/Settings/SettingsGeneralViewModel.swift b/readeck/UI/Settings/SettingsGeneralViewModel.swift index 6aadead..ad9563b 100644 --- a/readeck/UI/Settings/SettingsGeneralViewModel.swift +++ b/readeck/UI/Settings/SettingsGeneralViewModel.swift @@ -22,7 +22,9 @@ class SettingsGeneralViewModel { // MARK: - Messages var errorMessage: String? var successMessage: String? - // MARK: - Data Management (Platzhalter) + + // MARK: - Data Management (Placeholder) + // func clearCache() async {} // func resetSettings() async {} @@ -45,7 +47,7 @@ class SettingsGeneralViewModel { developerName = "Ilyas Hallak" } } catch { - errorMessage = "Fehler beim Laden der Einstellungen" + errorMessage = "Error loading settings" } } @@ -63,9 +65,9 @@ class SettingsGeneralViewModel { openExternalLinksInApp: openExternalLinksInApp, autoMarkAsRead: autoMarkAsRead )*/ - successMessage = "Einstellungen gespeichert" + successMessage = "Settings saved" } catch { - errorMessage = "Fehler beim Speichern der Einstellungen" + errorMessage = "Error saving settings" } } diff --git a/readeck/UI/Settings/SettingsServerView.swift b/readeck/UI/Settings/SettingsServerView.swift index e867c23..9cef324 100644 --- a/readeck/UI/Settings/SettingsServerView.swift +++ b/readeck/UI/Settings/SettingsServerView.swift @@ -128,7 +128,7 @@ struct SettingsServerView: View { .foregroundColor(.white) .cornerRadius(10) } - .disabled(!viewModel.canLogin || viewModel.isLoading) + .disabled(!viewModel.canLogin || viewModel.isLoading) } } else { Button(action: { diff --git a/readeck/UI/Settings/SettingsServerViewModel.swift b/readeck/UI/Settings/SettingsServerViewModel.swift index ecdc552..b801818 100644 --- a/readeck/UI/Settings/SettingsServerViewModel.swift +++ b/readeck/UI/Settings/SettingsServerViewModel.swift @@ -48,14 +48,14 @@ class SettingsServerViewModel { isLoggedIn = settings.isLoggedIn } } catch { - errorMessage = "Fehler beim Laden der Einstellungen" + errorMessage = "Error loading settings" } } @MainActor func saveServerSettings() async { guard canLogin else { - errorMessage = "Bitte füllen Sie alle Felder aus." + errorMessage = "Please fill in all fields." return } clearMessages() @@ -65,11 +65,11 @@ class SettingsServerViewModel { let user = try await loginUseCase.execute(endpoint: endpoint, username: username.trimmingCharacters(in: .whitespacesAndNewlines), password: password) try await saveServerSettingsUseCase.execute(endpoint: endpoint, username: username, password: password, token: user.token) isLoggedIn = true - successMessage = "Server-Einstellungen gespeichert und erfolgreich angemeldet." + successMessage = "Server settings saved and successfully logged in." try await SettingsRepository().saveHasFinishedSetup(true) NotificationCenter.default.post(name: NSNotification.Name("SetupStatusChanged"), object: nil) } catch { - errorMessage = "Verbindung oder Anmeldung fehlgeschlagen: \(error.localizedDescription)" + errorMessage = "Connection or login failed: \(error.localizedDescription)" isLoggedIn = false } } @@ -79,10 +79,10 @@ class SettingsServerViewModel { do { try await logoutUseCase.execute() isLoggedIn = false - successMessage = "Abgemeldet" + successMessage = "Logged out" NotificationCenter.default.post(name: NSNotification.Name("SetupStatusChanged"), object: nil) } catch { - errorMessage = "Fehler beim Abmelden" + errorMessage = "Error logging out" } } diff --git a/readeck/UI/SpeechPlayer/SpeechPlayerView.swift b/readeck/UI/SpeechPlayer/SpeechPlayerView.swift index ef862bd..62d10c3 100644 --- a/readeck/UI/SpeechPlayer/SpeechPlayerView.swift +++ b/readeck/UI/SpeechPlayer/SpeechPlayerView.swift @@ -59,7 +59,7 @@ private struct CollapsedPlayerBar: View { .foregroundColor(.accentColor) } VStack(alignment: .leading, spacing: 2) { - Text(viewModel.currentText.isEmpty ? "Keine Wiedergabe" : viewModel.currentText) + Text(viewModel.currentText.isEmpty ? "No playback" : viewModel.currentText) .font(.subheadline) .fontWeight(.medium) .lineLimit(1) @@ -114,7 +114,7 @@ private struct ExpandedPlayerView: View { .foregroundColor(.secondary) } Spacer() - Text("Vorlese-Queue") + Text("Read-aloud Queue") .font(.headline) .fontWeight(.semibold) Spacer() @@ -126,13 +126,13 @@ private struct ExpandedPlayerView: View { } .padding(.horizontal, 16) .padding(.top, 16) - // Fortschrittsbalken für aktuellen Artikel + // progress bar for current article if viewModel.articleProgress > 0 && viewModel.articleProgress < 1 { VStack(spacing: 4) { ProgressView(value: viewModel.articleProgress) .progressViewStyle(LinearProgressViewStyle(tint: .accentColor)) HStack { - Text("Fortschritt: \(Int(viewModel.articleProgress * 100))%") + Text("Progress: \(Int(viewModel.articleProgress * 100))%") .font(.caption2) .foregroundColor(.secondary) Spacer() @@ -148,7 +148,7 @@ private struct ExpandedPlayerView: View { HStack(spacing: 8) { Image(systemName: "text.line.first.and.arrowtriangle.forward") .foregroundColor(.accentColor) - Text("Lese \(viewModel.currentUtteranceIndex + 1)/\(viewModel.queueCount): ") + Text("Reading \(viewModel.currentUtteranceIndex + 1)/\(viewModel.queueCount): ") .font(.caption) .foregroundColor(.secondary) Text(viewModel.queueItems[safe: viewModel.currentUtteranceIndex]?.title ?? "") @@ -195,7 +195,7 @@ private struct PlayerControls: View { } HStack { Spacer() - Picker("Geschwindigkeit", selection: Binding( + Picker("Speed", selection: Binding( get: { viewModel.rate }, set: { viewModel.setRate($0) } )) { @@ -240,7 +240,7 @@ private struct PlayerRate: View { HStack { Image(systemName: "speedometer") .foregroundColor(.accentColor) - Picker("Geschwindigkeit", selection: Binding( + Picker("Speed", selection: Binding( get: { viewModel.rate }, set: { viewModel.setRate($0) } )) { @@ -260,7 +260,7 @@ private struct PlayerQueueList: View { @ObservedObject var viewModel: SpeechPlayerViewModel var body: some View { if viewModel.queueCount == 0 { - Text("Keine Artikel in der Queue") + Text("No articles in the queue") .font(.subheadline) .foregroundColor(.secondary) .padding()