diff --git a/Yattee/Localizable.xcstrings b/Yattee/Localizable.xcstrings index 8326f85d..bcadea0b 100644 --- a/Yattee/Localizable.xcstrings +++ b/Yattee/Localizable.xcstrings @@ -5309,9 +5309,21 @@ "comment" : "Video count label for playlist row", "localizations" : { "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld videos" + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 video" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld videos" + } + } + } } } } @@ -5594,24 +5606,13 @@ } } }, - "login.email" : { - "comment" : "Email field label in login form", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Email or Username" - } - } - } - }, "login.error.invalidCredentials" : { "comment" : "Error message for invalid login credentials", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Invalid email or password" + "value" : "Invalid username or password" } } } @@ -8430,6 +8431,17 @@ } } }, + "playlist.delete.confirm.named %@" : { + "comment" : "Delete playlist confirmation with the playlist name", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Are you sure you want to delete \"%@\"?" + } + } + } + }, "playlist.description" : { "comment" : "Description field label", "localizations" : { @@ -8690,6 +8702,17 @@ } } }, + "playlist.remote.delete.confirm.named %@" : { + "comment" : "Delete server-side playlist confirmation with the playlist name", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delete \"%@\" from your account?" + } + } + } + }, "playlist.remote.delete.error" : { "comment" : "Deleting a playlist from the server failed", "localizations" : { @@ -8814,9 +8837,21 @@ "playlist.videoCount %lld" : { "localizations" : { "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld videos" + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 video" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld videos" + } + } + } } } } @@ -8826,7 +8861,29 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "%lld videos • %@" + "value" : "%1$#@videoCount@ • %2$@" + }, + "substitutions" : { + "videoCount" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 video" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%arg videos" + } + } + } + } + } } } } diff --git a/Yattee/Views/Home/PlaylistsListView.swift b/Yattee/Views/Home/PlaylistsListView.swift index 95649869..630b2ac0 100644 --- a/Yattee/Views/Home/PlaylistsListView.swift +++ b/Yattee/Views/Home/PlaylistsListView.swift @@ -58,6 +58,7 @@ struct PlaylistsListView: View { private struct RemotePlaylistDeletion { let account: Account let playlistID: String + let title: String } // View options (persisted) @@ -90,6 +91,17 @@ struct PlaylistsListView: View { ) } + /// The delete confirmation, naming the playlist it is about. + /// + /// A playlist the server gave no title for falls back to the unnamed + /// wording rather than asking about `""`. + private var remoteDeletionTitle: String { + guard let title = remoteDeletion?.title, !title.isEmpty else { + return String(localized: "playlist.remote.delete.confirm") + } + return String(localized: "playlist.remote.delete.confirm.named \(title)") + } + /// View options button lives on the leading edge on macOS, trailing elsewhere. private var viewOptionsPlacement: ToolbarItemPlacement { #if os(macOS) @@ -300,7 +312,7 @@ struct PlaylistsListView: View { // Taking a playlist off a server is a very different sentence from // deleting a local one, which stays unconfirmed as it always was. .confirmationDialog( - String(localized: "playlist.remote.delete.confirm"), + remoteDeletionTitle, isPresented: Binding( get: { remoteDeletion != nil }, set: { if !$0 { remoteDeletion = nil } } @@ -683,7 +695,8 @@ struct PlaylistsListView: View { Button(role: .destructive) { remoteDeletion = RemotePlaylistDeletion( account: account, - playlistID: playlist.id.playlistID + playlistID: playlist.id.playlistID, + title: playlist.title ) } label: { Label(String(localized: "playlist.delete"), systemImage: "trash") @@ -737,7 +750,8 @@ struct PlaylistsListView: View { reset() remoteDeletion = RemotePlaylistDeletion( account: account, - playlistID: playlist.id.playlistID + playlistID: playlist.id.playlistID, + title: playlist.title ) } ] diff --git a/Yattee/Views/Playlist/PlaylistFormSheet.swift b/Yattee/Views/Playlist/PlaylistFormSheet.swift index dccb2a6d..d65a7118 100644 --- a/Yattee/Views/Playlist/PlaylistFormSheet.swift +++ b/Yattee/Views/Playlist/PlaylistFormSheet.swift @@ -192,7 +192,7 @@ struct PlaylistFormSheet: View { HStack { Text(String(localized: "playlist.description.optional")) Spacer() - Text("\(descriptionText.count)/\(maxDescriptionLength)") + Text(verbatim: "\(descriptionText.count)/\(maxDescriptionLength)") .monospacedDigit() } .foregroundStyle(.secondary) @@ -249,7 +249,7 @@ struct PlaylistFormSheet: View { HStack { Text(String(localized: "playlist.description.optional")) Spacer() - Text("\(descriptionText.count)/\(maxDescriptionLength)") + Text(verbatim: "\(descriptionText.count)/\(maxDescriptionLength)") .monospacedDigit() } .foregroundStyle(.secondary) diff --git a/Yattee/Views/Playlist/UnifiedPlaylistDetailView.swift b/Yattee/Views/Playlist/UnifiedPlaylistDetailView.swift index a5f37450..27ae4074 100644 --- a/Yattee/Views/Playlist/UnifiedPlaylistDetailView.swift +++ b/Yattee/Views/Playlist/UnifiedPlaylistDetailView.swift @@ -181,10 +181,21 @@ struct UnifiedPlaylistDetailView: View { /// Deleting a server-side playlist takes it off the server, which is a very /// different sentence from deleting a local one. + /// + /// Both name the playlist, and both fall back to the unnamed wording while + /// there is no name yet to say. private var deleteConfirmationTitle: String { - isLocal - ? String(localized: "playlist.delete.confirm") - : String(localized: "playlist.remote.delete.confirm") + let name = title.isEmpty ? cachedHeader?.title ?? "" : title + + guard !name.isEmpty else { + return isLocal + ? String(localized: "playlist.delete.confirm") + : String(localized: "playlist.remote.delete.confirm") + } + + return isLocal + ? String(localized: "playlist.delete.confirm.named \(name)") + : String(localized: "playlist.remote.delete.confirm.named \(name)") } private var navigationTitleText: String { diff --git a/Yattee/Views/Settings/InstanceLoginView.swift b/Yattee/Views/Settings/InstanceLoginView.swift index d3cf1322..a6c702b4 100644 --- a/Yattee/Views/Settings/InstanceLoginView.swift +++ b/Yattee/Views/Settings/InstanceLoginView.swift @@ -165,9 +165,10 @@ struct InstanceLoginView: View { /// What the first field is called on this backend. /// - /// Named per backend rather than genericised: Invidious signs in by email, - /// and calling that "Username" is how a user ends up typing the wrong thing. - /// Every case is spelled out so a backend that gains accounts — + /// Named per backend rather than genericised: every backend that has + /// accounts signs in by username — Invidious posts one in a form field it + /// still calls `email`, but an email is never what it wants. Every case is + /// spelled out so a backend that gains accounts — /// `AccountCapabilities.supportsAccounts` is what decides that — arrives /// here rather than falling through to somebody else's wording. private var usernameFieldLabel: String { @@ -175,7 +176,7 @@ struct InstanceLoginView: View { case .piped: return String(localized: "login.username") case .invidious: - return String(localized: "login.email") + return String(localized: "login.username") case .yatteeServer: return String(localized: "login.username") case .peertube: diff --git a/Yattee/Views/Settings/LegacyDataImportView.swift b/Yattee/Views/Settings/LegacyDataImportView.swift index 6d8014cc..e813c4f5 100644 --- a/Yattee/Views/Settings/LegacyDataImportView.swift +++ b/Yattee/Views/Settings/LegacyDataImportView.swift @@ -366,10 +366,10 @@ private struct LegacyAccountImportRow: View { @ViewBuilder private var credentialsFields: some View { #if os(tvOS) - TVSettingsTextField(title: usernameLabel, text: $state.username) + TVSettingsTextField(title: String(localized: "login.username"), text: $state.username) TVSettingsTextField(title: String(localized: "login.password"), text: $state.password, isSecure: true) #else - TextField(usernameLabel, text: $state.username) + TextField(String(localized: "login.username"), text: $state.username) .textContentType(.username) #if os(iOS) .textInputAutocapitalization(.never) @@ -380,15 +380,6 @@ private struct LegacyAccountImportRow: View { .textContentType(.password) #endif } - - private var usernameLabel: String { - switch item.instanceType { - case .invidious: - return String(localized: "login.email") - default: - return String(localized: "login.username") - } - } } // MARK: - Source Row diff --git a/Yattee/Views/Settings/PlayerControls/PresetEditorView.swift b/Yattee/Views/Settings/PlayerControls/PresetEditorView.swift index ce487592..76459e0c 100644 --- a/Yattee/Views/Settings/PlayerControls/PresetEditorView.swift +++ b/Yattee/Views/Settings/PlayerControls/PresetEditorView.swift @@ -158,7 +158,7 @@ struct PresetEditorView: View { .foregroundStyle(.red) } Spacer() - Text("\(trimmedName.count)/\(LayoutPreset.maxNameLength)") + Text(verbatim: "\(trimmedName.count)/\(LayoutPreset.maxNameLength)") .foregroundStyle( trimmedName.count > LayoutPreset.maxNameLength ? .red : .secondary ) diff --git a/Yattee/Views/Video/VideoInfoView.swift b/Yattee/Views/Video/VideoInfoView.swift index 8053abe7..6cdf221e 100644 --- a/Yattee/Views/Video/VideoInfoView.swift +++ b/Yattee/Views/Video/VideoInfoView.swift @@ -1708,7 +1708,7 @@ struct VideoInfoView: View { if bookmarkNote.count > 900 { HStack { Spacer() - Text("\(bookmarkNote.count)/1000") + Text(verbatim: "\(bookmarkNote.count)/1000") .font(.caption) .foregroundStyle(bookmarkNote.count > 1000 ? .red : .secondary) .monospacedDigit()