mirror of
https://github.com/yattee/yattee.git
synced 2026-10-23 05:02:53 +00:00
Let the words fit the things they describe
tvOS UX campaign polish (TV1, TV3, TV5, TV6):
- Invidious sign-in asks for a Username, which is what it verifies;
the wire field is still called email, noted where it is sent
- Video counts pluralize ("1 video"), via catalog plural variations;
the two-argument count-and-duration key uses fully positional
substitutions, since a mixed format made CFRetain fault on the
integer argument (TV10)
- Character counters render their limits verbatim ("0/1000"), here
and in the preset-name and bookmark-note counters with the same bug
- Deleting a playlist, local or remote, names the playlist the way
removing an account already named the account
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user