Translate UI and error messages from German to English

- BookmarkDetail: All user-facing texts and error messages in BookmarkDetailView, BookmarkDetailViewModel, BookmarkLabelsView, and BookmarkLabelsViewModel translated to English.
- Bookmarks: All UI strings, swipe actions, and error messages in BookmarkCardView, BookmarksView, BookmarksViewModel, and related enums translated to English.
- Labels: All UI and error messages in LabelsView and LabelsViewModel translated to English.
- Menu: All sidebar/tab names, navigation titles, and queue texts in BookmarkState, PhoneTabView, PlayerQueueResumeButton, SidebarTab updated to English.
- Settings: All section headers, toggle labels, button texts, and error/success messages in FontSettingsView, FontSettingsViewModel, SettingsContainerView, SettingsGeneralView, SettingsGeneralViewModel, SettingsServerView, SettingsServerViewModel translated to English.
- SpeechPlayer: All player UI texts, progress, and queue messages in SpeechPlayerView translated to English.

This commit unifies the app language to English for all user-facing areas.
This commit is contained in:
Ilyas Hallak 2025-07-18 14:57:45 +02:00
parent c52d974b05
commit 387a026e7d
26 changed files with 388 additions and 254 deletions

View File

@ -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"

View File

@ -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/)

View File

@ -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()

View File

@ -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 {

View File

@ -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
}

View File

@ -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)

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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<Bookmark?>, 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()

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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"
}
}

View File

@ -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 {

View File

@ -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)
}
}
}
}

View File

@ -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"
}
}

View File

@ -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)

View File

@ -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"
}
}

View File

@ -23,7 +23,7 @@ struct SettingsContainerView: View {
.padding()
.background(Color(.systemGroupedBackground))
}
.navigationTitle("Einstellungen")
.navigationTitle("Settings")
.navigationBarTitleDisplayMode(.large)
}
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}

View File

@ -128,7 +128,7 @@ struct SettingsServerView: View {
.foregroundColor(.white)
.cornerRadius(10)
}
.disabled(!viewModel.canLogin || viewModel.isLoading)
.disabled(!viewModel.canLogin || viewModel.isLoading)
}
} else {
Button(action: {

View File

@ -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"
}
}

View File

@ -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()