i18n: add English defaultValue fallback to key-only localized strings so no language shows raw keys

Non-en languages are each missing ~138 of 336 catalog keys, so a key
absent from that language's table renders as the raw dot-key on device
(no English fallback). This adds English defaultValues so every miss
resolves to English instead of a raw key.

- Class (a): 217 String(localized: "dot.key") calls missing defaultValue
  now carry defaultValue: "<en value>" pulled verbatim from the catalog
  (format specifiers preserved).
- Class (b): 41 SwiftUI LocalizedStringKey literals (Text/Button/alert/
  confirmationDialog/TextField) wrapped as
  Text(String(localized: "key", defaultValue: "en")) so they resolve to
  English too. Existing comments folded into the resolved String.

No catalog or en values changed; Swift source only. Both schemes build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jack
2026-07-06 23:58:29 +02:00
co-authored by Claude Fable 5
parent b62f98cc62
commit 9cbfd131da
37 changed files with 258 additions and 258 deletions
+2 -2
View File
@@ -349,12 +349,12 @@ private extension AppRuntime {
TorManager.shared.isAutoStartAllowed() {
chatViewModel.torStatusAnnounced = true
chatViewModel.addGeohashOnlySystemMessage(
String(localized: "system.tor.starting", comment: "System message when Tor is starting")
String(localized: "system.tor.starting", defaultValue: "starting tor...", comment: "System message when Tor is starting")
)
} else if !TorManager.shared.torEnforced && !chatViewModel.torStatusAnnounced {
chatViewModel.torStatusAnnounced = true
chatViewModel.addGeohashOnlySystemMessage(
String(localized: "system.tor.dev_bypass", comment: "System message when Tor bypass is enabled in development")
String(localized: "system.tor.dev_bypass", defaultValue: "development build: Tor bypass enabled.", comment: "System message when Tor bypass is enabled in development")
)
}
}
+2 -2
View File
@@ -250,7 +250,7 @@ final class PrivateConversationModel: ObservableObject {
if let group = chatViewModel.groupStore.group(for: conversationPeerID) {
displayName = "#\(group.name) (\(group.members.count))"
} else {
displayName = String(localized: "common.unknown", comment: "Fallback label for unknown peer")
displayName = String(localized: "common.unknown", defaultValue: "unknown", comment: "Fallback label for unknown peer")
}
return PrivateConversationHeaderState(
conversationPeerID: conversationPeerID,
@@ -328,7 +328,7 @@ final class PrivateConversationModel: ObservableObject {
}
}
return String(localized: "common.unknown", comment: "Fallback label for unknown peer")
return String(localized: "common.unknown", defaultValue: "unknown", comment: "Fallback label for unknown peer")
}
private func resolveAvailability(for headerPeerID: PeerID, peer: BitchatPeer?) -> PrivateConversationAvailability {
+1 -1
View File
@@ -186,6 +186,6 @@ final class VerificationModel: ObservableObject {
}
}
return String(localized: "common.unknown", comment: "Label for an unknown peer")
return String(localized: "common.unknown", defaultValue: "unknown", comment: "Label for an unknown peer")
}
}
+17 -17
View File
@@ -36,11 +36,11 @@ enum CommandInfo: String, Identifiable {
var placeholder: String? {
switch self {
case .block, .hug, .message, .slap, .unblock, .favorite, .unfavorite, .ping, .trace:
return "<" + String(localized: "content.input.nickname_placeholder") + ">"
return "<" + String(localized: "content.input.nickname_placeholder", defaultValue: "nickname") + ">"
case .group:
return "<" + String(localized: "content.input.group_placeholder") + ">"
return "<" + String(localized: "content.input.group_placeholder", defaultValue: "create|invite|leave|list") + ">"
case .pay:
return "<" + String(localized: "content.input.token_placeholder") + ">"
return "<" + String(localized: "content.input.token_placeholder", defaultValue: "token") + ">"
case .clear, .help, .who:
return nil
}
@@ -48,20 +48,20 @@ enum CommandInfo: String, Identifiable {
var description: String {
switch self {
case .block: String(localized: "content.commands.block")
case .clear: String(localized: "content.commands.clear")
case .group: String(localized: "content.commands.group")
case .help: String(localized: "content.commands.help")
case .hug: String(localized: "content.commands.hug")
case .message: String(localized: "content.commands.message")
case .pay: String(localized: "content.commands.pay")
case .slap: String(localized: "content.commands.slap")
case .unblock: String(localized: "content.commands.unblock")
case .who: String(localized: "content.commands.who")
case .favorite: String(localized: "content.commands.favorite")
case .unfavorite: String(localized: "content.commands.unfavorite")
case .ping: String(localized: "content.commands.ping")
case .trace: String(localized: "content.commands.trace")
case .block: String(localized: "content.commands.block", defaultValue: "block or list blocked peers")
case .clear: String(localized: "content.commands.clear", defaultValue: "clear chat messages")
case .group: String(localized: "content.commands.group", defaultValue: "create or manage private groups")
case .help: String(localized: "content.commands.help", defaultValue: "show available commands")
case .hug: String(localized: "content.commands.hug", defaultValue: "send someone a warm hug")
case .message: String(localized: "content.commands.message", defaultValue: "send private message")
case .pay: String(localized: "content.commands.pay", defaultValue: "send a cashu ecash token in this chat")
case .slap: String(localized: "content.commands.slap", defaultValue: "slap someone with a trout")
case .unblock: String(localized: "content.commands.unblock", defaultValue: "unblock a peer")
case .who: String(localized: "content.commands.who", defaultValue: "see who's online")
case .favorite: String(localized: "content.commands.favorite", defaultValue: "add to favorites")
case .unfavorite: String(localized: "content.commands.unfavorite", defaultValue: "remove from favorites")
case .ping: String(localized: "content.commands.ping", defaultValue: "measure round-trip time to a mesh peer")
case .trace: String(localized: "content.commands.trace", defaultValue: "estimate the mesh path to a peer")
}
}
+6 -6
View File
@@ -24,17 +24,17 @@ enum GeohashChannelLevel: CaseIterable, Codable, Equatable {
var displayName: String {
switch self {
case .building:
return String(localized: "location_levels.building", comment: "Name for building-level location channel")
return String(localized: "location_levels.building", defaultValue: "building", comment: "Name for building-level location channel")
case .block:
return String(localized: "location_levels.block", comment: "Name for block-level location channel")
return String(localized: "location_levels.block", defaultValue: "block", comment: "Name for block-level location channel")
case .neighborhood:
return String(localized: "location_levels.neighborhood", comment: "Name for neighborhood-level location channel")
return String(localized: "location_levels.neighborhood", defaultValue: "neighborhood", comment: "Name for neighborhood-level location channel")
case .city:
return String(localized: "location_levels.city", comment: "Name for city-level location channel")
return String(localized: "location_levels.city", defaultValue: "city", comment: "Name for city-level location channel")
case .province:
return String(localized: "location_levels.province", comment: "Name for province-level location channel")
return String(localized: "location_levels.province", defaultValue: "province", comment: "Name for province-level location channel")
case .region:
return String(localized: "location_levels.region", comment: "Name for region-level location channel")
return String(localized: "location_levels.region", defaultValue: "region", comment: "Name for region-level location channel")
}
}
}
+1 -1
View File
@@ -3719,7 +3719,7 @@ extension BLEService {
// Notify delegate of failure
notifyUI { [weak self] in
self?.deliverTransportEvent(.messageDeliveryStatusUpdated(messageID: message.messageID, status: .failed(reason: String(localized: "content.delivery.reason.encryption_failed", comment: "Failure reason shown when a message could not be encrypted for the peer"))))
self?.deliverTransportEvent(.messageDeliveryStatusUpdated(messageID: message.messageID, status: .failed(reason: String(localized: "content.delivery.reason.encryption_failed", defaultValue: "encryption failed", comment: "Failure reason shown when a message could not be encrypted for the peer"))))
}
}
}
+2 -2
View File
@@ -89,11 +89,11 @@ final class LocationNotesManager: ObservableObject {
private let maxNotesInMemory = 500 // Defensive cap (relay limit is 200)
private enum Strings {
static let noRelays = String(localized: "location_notes.error.no_relays", comment: "Shown when no geo relays are available near the selected location")
static let noRelays = String(localized: "location_notes.error.no_relays", defaultValue: "no geo relays available near this location. try again soon.", comment: "Shown when no geo relays are available near the selected location")
static func failedToSend(_ detail: String) -> String {
String(
format: String(localized: "location_notes.error.failed_to_send", comment: "Shown when a location note fails to send"),
format: String(localized: "location_notes.error.failed_to_send", defaultValue: "failed to send note. %@", comment: "Shown when a location note fails to send"),
locale: .current,
detail
)
+10 -10
View File
@@ -116,30 +116,30 @@ enum EncryptionStatus: Equatable {
var description: String {
switch self {
case .none:
return String(localized: "encryption.status.failed", comment: "Status text when encryption failed")
return String(localized: "encryption.status.failed", defaultValue: "encryption failed", comment: "Status text when encryption failed")
case .noHandshake:
return String(localized: "encryption.status.not_encrypted", comment: "Status text when no encryption handshake happened")
return String(localized: "encryption.status.not_encrypted", defaultValue: "not encrypted", comment: "Status text when no encryption handshake happened")
case .noiseHandshaking:
return String(localized: "encryption.status.establishing", comment: "Status text when encryption is being established")
return String(localized: "encryption.status.establishing", defaultValue: "establishing encryption...", comment: "Status text when encryption is being established")
case .noiseSecured:
return String(localized: "encryption.status.secured", comment: "Status text when encryption is secured but not verified")
return String(localized: "encryption.status.secured", defaultValue: "encrypted", comment: "Status text when encryption is secured but not verified")
case .noiseVerified:
return String(localized: "encryption.status.verified", comment: "Status text when encryption is verified")
return String(localized: "encryption.status.verified", defaultValue: "encrypted & verified", comment: "Status text when encryption is verified")
}
}
var accessibilityDescription: String {
switch self {
case .none:
return String(localized: "encryption.accessibility.failed", comment: "Accessibility text when encryption failed")
return String(localized: "encryption.accessibility.failed", defaultValue: "encryption failed", comment: "Accessibility text when encryption failed")
case .noHandshake:
return String(localized: "encryption.accessibility.not_encrypted", comment: "Accessibility text when encryption is not established")
return String(localized: "encryption.accessibility.not_encrypted", defaultValue: "not encrypted", comment: "Accessibility text when encryption is not established")
case .noiseHandshaking:
return String(localized: "encryption.accessibility.establishing", comment: "Accessibility text when encryption is being established")
return String(localized: "encryption.accessibility.establishing", defaultValue: "establishing encryption", comment: "Accessibility text when encryption is being established")
case .noiseSecured:
return String(localized: "encryption.accessibility.secured", comment: "Accessibility text when encryption is secured")
return String(localized: "encryption.accessibility.secured", defaultValue: "encrypted", comment: "Accessibility text when encryption is secured")
case .noiseVerified:
return String(localized: "encryption.accessibility.verified", comment: "Accessibility text when encryption is verified")
return String(localized: "encryption.accessibility.verified", defaultValue: "encrypted and verified", comment: "Accessibility text when encryption is verified")
}
}
}
@@ -12,17 +12,17 @@ enum ChatBluetoothAlertPolicy {
case .poweredOff:
ChatBluetoothAlertUpdate(
isPresented: true,
message: String(localized: "content.alert.bluetooth_required.off", comment: "Message shown when Bluetooth is turned off")
message: String(localized: "content.alert.bluetooth_required.off", defaultValue: "bluetooth is turned off. please turn on bluetooth in settings to use bitchat.", comment: "Message shown when Bluetooth is turned off")
)
case .unauthorized:
ChatBluetoothAlertUpdate(
isPresented: true,
message: String(localized: "content.alert.bluetooth_required.permission", comment: "Message shown when Bluetooth permission is missing")
message: String(localized: "content.alert.bluetooth_required.permission", defaultValue: "bitchat needs bluetooth permission to connect with nearby devices. please enable bluetooth access in settings.", comment: "Message shown when Bluetooth permission is missing")
)
case .unsupported:
ChatBluetoothAlertUpdate(
isPresented: true,
message: String(localized: "content.alert.bluetooth_required.unsupported", comment: "Message shown when the device lacks Bluetooth support")
message: String(localized: "content.alert.bluetooth_required.unsupported", defaultValue: "this device does not support bluetooth. bitchat requires bluetooth to function.", comment: "Message shown when the device lacks Bluetooth support")
)
case .poweredOn:
ChatBluetoothAlertUpdate(isPresented: false, message: "")
+30 -30
View File
@@ -159,26 +159,26 @@ final class ChatGroupCoordinator {
func createGroup(named rawName: String) -> CommandResult {
let name = rawName.trimmed
guard !name.isEmpty else {
return .error(message: String(localized: "system.group.usage_create", comment: "Usage hint for /group create"))
return .error(message: String(localized: "system.group.usage_create", defaultValue: "usage: /group create <name>", comment: "Usage hint for /group create"))
}
guard name.count <= Self.maxGroupNameLength else {
return .error(message: String(localized: "system.group.name_too_long", comment: "Error when a group name exceeds the length cap"))
return .error(message: String(localized: "system.group.name_too_long", defaultValue: "group names are limited to 40 characters", comment: "Error when a group name exceeds the length cap"))
}
let myFingerprint = context.myNoiseFingerprint()
let mySigningKey = context.mySigningPublicKey()
guard !myFingerprint.isEmpty, mySigningKey.count == 32 else {
return .error(message: String(localized: "system.group.identity_unavailable", comment: "Error when the local identity is not ready for group operations"))
return .error(message: String(localized: "system.group.identity_unavailable", defaultValue: "your identity keys are not ready yet", comment: "Error when the local identity is not ready for group operations"))
}
let creator = GroupMember(fingerprint: myFingerprint, signingKey: mySigningKey, nickname: context.nickname)
guard let group = context.groupStore.createGroup(named: name, creator: creator) else {
return .error(message: String(localized: "system.group.create_failed", comment: "Error when group creation fails"))
return .error(message: String(localized: "system.group.create_failed", defaultValue: "could not create the group", comment: "Error when group creation fails"))
}
context.startPrivateChat(with: group.peerID)
return .success(message: String(
format: String(localized: "system.group.created", comment: "System message after creating a group; placeholder is the group name"),
format: String(localized: "system.group.created", defaultValue: "created group '%@' — use /group invite @name to add people", comment: "System message after creating a group; placeholder is the group name"),
locale: .current,
name
))
@@ -187,45 +187,45 @@ final class ChatGroupCoordinator {
func inviteMember(nickname rawNickname: String) -> CommandResult {
let nickname = normalizedNickname(rawNickname)
guard !nickname.isEmpty else {
return .error(message: String(localized: "system.group.usage_invite", comment: "Usage hint for /group invite"))
return .error(message: String(localized: "system.group.usage_invite", defaultValue: "usage: /group invite @name", comment: "Usage hint for /group invite"))
}
guard let group = selectedGroup() else {
return .error(message: String(localized: "system.group.not_in_group", comment: "Error when a group command requires an open group chat"))
return .error(message: String(localized: "system.group.not_in_group", defaultValue: "open a group chat first", comment: "Error when a group command requires an open group chat"))
}
guard group.creatorFingerprint == context.myNoiseFingerprint() else {
return .error(message: String(localized: "system.group.creator_only", comment: "Error when a non-creator attempts a creator-only group action"))
return .error(message: String(localized: "system.group.creator_only", defaultValue: "only the group creator can do that", comment: "Error when a non-creator attempts a creator-only group action"))
}
guard let peerID = context.getPeerIDForNickname(nickname) else {
return .error(message: String(
format: String(localized: "system.group.peer_not_found", comment: "Error when the invitee nickname is unknown; placeholder is the nickname"),
format: String(localized: "system.group.peer_not_found", defaultValue: "'%@' not found", comment: "Error when the invitee nickname is unknown; placeholder is the nickname"),
locale: .current,
nickname
))
}
guard context.isPeerConnected(peerID) else {
return .error(message: String(
format: String(localized: "system.group.peer_not_connected", comment: "Error when the invitee is not connected over mesh; placeholder is the nickname"),
format: String(localized: "system.group.peer_not_connected", defaultValue: "%@ must be connected over mesh to be invited", comment: "Error when the invitee is not connected over mesh; placeholder is the nickname"),
locale: .current,
nickname
))
}
guard let identity = context.cryptoIdentity(for: peerID) else {
return .error(message: String(
format: String(localized: "system.group.peer_identity_unknown", comment: "Error when the invitee's verified identity is unavailable; placeholder is the nickname"),
format: String(localized: "system.group.peer_identity_unknown", defaultValue: "cannot verify %@'s identity yet — wait for their announce", comment: "Error when the invitee's verified identity is unavailable; placeholder is the nickname"),
locale: .current,
nickname
))
}
guard !group.isMember(fingerprint: identity.fingerprint) else {
return .error(message: String(
format: String(localized: "system.group.already_member", comment: "Error when the invitee is already a member; placeholder is the nickname"),
format: String(localized: "system.group.already_member", defaultValue: "%@ is already a member", comment: "Error when the invitee is already a member; placeholder is the nickname"),
locale: .current,
nickname
))
}
guard group.members.count < BitchatGroup.maxMembers else {
return .error(message: String(
format: String(localized: "system.group.full", comment: "Error when the group is at the member cap; placeholder is the cap"),
format: String(localized: "system.group.full", defaultValue: "group is full (max %@ members)", comment: "Error when the group is at the member cap; placeholder is the cap"),
locale: .current,
"\(BitchatGroup.maxMembers)"
))
@@ -243,14 +243,14 @@ final class ChatGroupCoordinator {
let members = group.members + [newMember]
guard let (updated, key) = context.groupStore.rotateKey(groupID: group.groupID, members: members),
let payload = signedStatePayload(for: updated, key: key) else {
return .error(message: String(localized: "system.group.invite_failed", comment: "Error when building or signing a group invite fails"))
return .error(message: String(localized: "system.group.invite_failed", defaultValue: "could not build the group invite", comment: "Error when building or signing a group invite fails"))
}
context.sendGroupInvitePayload(payload, to: peerID)
distributeState(payload, group: updated, excluding: [identity.fingerprint], type: .keyUpdate)
return .success(message: String(
format: String(localized: "system.group.invited", comment: "System message after inviting someone; placeholders are the nickname and the group name"),
format: String(localized: "system.group.invited", defaultValue: "invited %1$@ to '%2$@'", comment: "System message after inviting someone; placeholders are the nickname and the group name"),
locale: .current,
nickname,
updated.name
@@ -263,36 +263,36 @@ final class ChatGroupCoordinator {
func removeMember(nickname rawNickname: String) -> CommandResult {
let nickname = normalizedNickname(rawNickname)
guard !nickname.isEmpty else {
return .error(message: String(localized: "system.group.usage_remove", comment: "Usage hint for /group remove"))
return .error(message: String(localized: "system.group.usage_remove", defaultValue: "usage: /group remove @name", comment: "Usage hint for /group remove"))
}
guard let group = selectedGroup() else {
return .error(message: String(localized: "system.group.not_in_group", comment: "Error when a group command requires an open group chat"))
return .error(message: String(localized: "system.group.not_in_group", defaultValue: "open a group chat first", comment: "Error when a group command requires an open group chat"))
}
guard group.creatorFingerprint == context.myNoiseFingerprint() else {
return .error(message: String(localized: "system.group.creator_only", comment: "Error when a non-creator attempts a creator-only group action"))
return .error(message: String(localized: "system.group.creator_only", defaultValue: "only the group creator can do that", comment: "Error when a non-creator attempts a creator-only group action"))
}
guard let member = group.members.first(where: { $0.nickname.caseInsensitiveCompare(nickname) == .orderedSame }) else {
return .error(message: String(
format: String(localized: "system.group.member_not_found", comment: "Error when the member to remove is not in the roster; placeholder is the nickname"),
format: String(localized: "system.group.member_not_found", defaultValue: "'%@' is not a member of this group", comment: "Error when the member to remove is not in the roster; placeholder is the nickname"),
locale: .current,
nickname
))
}
guard member.fingerprint != group.creatorFingerprint else {
return .error(message: String(localized: "system.group.cannot_remove_creator", comment: "Error when the creator tries to remove themselves"))
return .error(message: String(localized: "system.group.cannot_remove_creator", defaultValue: "the creator cannot be removed", comment: "Error when the creator tries to remove themselves"))
}
let remaining = group.members.filter { $0.fingerprint != member.fingerprint }
guard let (rotated, newKey) = context.groupStore.rotateKey(groupID: group.groupID, members: remaining),
let payload = signedStatePayload(for: rotated, key: newKey) else {
return .error(message: String(localized: "system.group.rotate_failed", comment: "Error when rotating the group key fails"))
return .error(message: String(localized: "system.group.rotate_failed", defaultValue: "could not rotate the group key", comment: "Error when rotating the group key fails"))
}
distributeState(payload, group: rotated, excluding: [], type: .keyUpdate)
notifyRemovedMember(member, rotated: rotated)
return .success(message: String(
format: String(localized: "system.group.removed_member", comment: "System message after removing a member and rotating the key; placeholder is the nickname"),
format: String(localized: "system.group.removed_member", defaultValue: "removed %@ and rotated the group key", comment: "System message after removing a member and rotating the key; placeholder is the nickname"),
locale: .current,
member.nickname
))
@@ -300,7 +300,7 @@ final class ChatGroupCoordinator {
func leaveGroup() -> CommandResult {
guard let group = selectedGroup() else {
return .error(message: String(localized: "system.group.not_in_group", comment: "Error when a group command requires an open group chat"))
return .error(message: String(localized: "system.group.not_in_group", defaultValue: "open a group chat first", comment: "Error when a group command requires an open group chat"))
}
// Close the chat window first so the confirmation message doesn't
// resurrect the conversation we're about to remove.
@@ -309,7 +309,7 @@ final class ChatGroupCoordinator {
context.groupStore.removeGroup(withID: group.groupID)
context.notifyUIChanged()
return .success(message: String(
format: String(localized: "system.group.left", comment: "System message after leaving a group; placeholder is the group name"),
format: String(localized: "system.group.left", defaultValue: "left group '%@'", comment: "System message after leaving a group; placeholder is the group name"),
locale: .current,
group.name
))
@@ -318,14 +318,14 @@ final class ChatGroupCoordinator {
func listGroups() -> CommandResult {
let groups = context.groupStore.groups
guard !groups.isEmpty else {
return .success(message: String(localized: "system.group.none", comment: "System message when the user is in no groups"))
return .success(message: String(localized: "system.group.none", defaultValue: "you are not in any groups — /group create <name> to start one", comment: "System message when the user is in no groups"))
}
let myFingerprint = context.myNoiseFingerprint()
let lines = groups.map { group -> String in
let role = group.creatorFingerprint == myFingerprint ? " (creator)" : ""
return "#\(group.name)\(role)\(group.members.count)/\(BitchatGroup.maxMembers)"
}
return .success(message: String(localized: "system.group.list_header", comment: "Header line for the /group list output") + "\n" + lines.joined(separator: "\n"))
return .success(message: String(localized: "system.group.list_header", defaultValue: "your groups:", comment: "Header line for the /group list output") + "\n" + lines.joined(separator: "\n"))
}
// MARK: - Sending
@@ -336,7 +336,7 @@ final class ChatGroupCoordinator {
guard !content.isEmpty, content.count <= InputValidator.Limits.maxMessageLength else { return }
guard let group = context.groupStore.group(for: groupPeerID),
let key = context.groupStore.key(forGroupID: group.groupID) else {
context.addSystemMessage(String(localized: "system.group.unknown", comment: "System message when sending into an unknown group"))
context.addSystemMessage(String(localized: "system.group.unknown", defaultValue: "you are no longer in this group", comment: "System message when sending into an unknown group"))
return
}
@@ -358,7 +358,7 @@ final class ChatGroupCoordinator {
} catch {
SecureLogger.error("Failed to seal group message: \(error)", category: .encryption)
context.addLocalPrivateSystemMessage(
String(localized: "system.group.send_failed", comment: "System message when sealing a group message fails"),
String(localized: "system.group.send_failed", defaultValue: "could not encrypt the message", comment: "System message when sealing a group message fails"),
to: groupPeerID
)
return
@@ -559,7 +559,7 @@ private extension ChatGroupCoordinator {
context.removePrivateChat(existing.peerID)
context.groupStore.removeGroup(withID: existing.groupID)
context.addSystemMessage(String(
format: String(localized: "system.group.removed_from", comment: "System message when removed from a group; placeholder is the group name"),
format: String(localized: "system.group.removed_from", defaultValue: "you were removed from group '%@'", comment: "System message when removed from a group; placeholder is the group name"),
locale: .current,
existing.name
))
@@ -586,7 +586,7 @@ private extension ChatGroupCoordinator {
?? context.peerNickname(for: peerID)
?? "?"
let notice = String(
format: String(localized: "system.group.joined", comment: "System message when added to a group; placeholders are the group name and the inviter"),
format: String(localized: "system.group.joined", defaultValue: "you were added to group '%1$@' by %2$@ — it now appears in your people list", comment: "System message when added to a group; placeholders are the group name and the inviter"),
locale: .current,
state.name,
inviter
@@ -350,7 +350,7 @@ private extension ChatLifecycleCoordinator {
} catch {
SecureLogger.error("❌ Failed to send geohash screenshot message: \(error)", category: .session)
context.addSystemMessage(
String(localized: "system.location.send_failed", comment: "System message when a location channel send fails")
String(localized: "system.location.send_failed", defaultValue: "failed to send to location channel", comment: "System message when a location channel send fails")
)
}
}
@@ -117,13 +117,13 @@ final class ChatMediaTransferCoordinator {
try? FileManager.default.removeItem(at: url)
await MainActor.run { [weak self] in
guard let self else { return }
self.handleMediaSendFailure(messageID: messageID, reason: String(localized: "content.delivery.reason.voice_too_large", comment: "Failure reason shown when a voice note exceeds the size limit"))
self.handleMediaSendFailure(messageID: messageID, reason: String(localized: "content.delivery.reason.voice_too_large", defaultValue: "voice note too large", comment: "Failure reason shown when a voice note exceeds the size limit"))
}
} catch {
SecureLogger.error("Voice note send failed: \(error)", category: .session)
await MainActor.run { [weak self] in
guard let self else { return }
self.handleMediaSendFailure(messageID: messageID, reason: String(localized: "content.delivery.reason.voice_send_failed", comment: "Failure reason shown when a voice note could not be sent"))
self.handleMediaSendFailure(messageID: messageID, reason: String(localized: "content.delivery.reason.voice_send_failed", defaultValue: "voice note failed to send", comment: "Failure reason shown when a voice note could not be sent"))
}
}
}
@@ -147,7 +147,7 @@ private extension ChatOutgoingCoordinator {
} catch {
SecureLogger.error("❌ Failed to prepare geohash message: \(error)", category: .session)
context.addSystemMessage(
String(localized: "system.location.send_failed", comment: "System message when a location channel send fails")
String(localized: "system.location.send_failed", defaultValue: "failed to send to location channel", comment: "System message when a location channel send fails")
)
return
}
@@ -181,7 +181,7 @@ private extension ChatOutgoingCoordinator {
} catch {
SecureLogger.error("❌ Failed to prepare geohash message: \(error)", category: .session)
context?.addSystemMessage(
String(localized: "system.location.send_failed", comment: "System message when a location channel send fails")
String(localized: "system.location.send_failed", defaultValue: "failed to send to location channel", comment: "System message when a location channel send fails")
)
return
}
@@ -238,7 +238,7 @@ final class ChatPrivateConversationCoordinator {
let nickname = context.peerNickname(for: peerID) ?? "user"
context.addSystemMessage(
String(
format: String(localized: "system.dm.blocked_recipient", comment: "System message when attempting to message a blocked user"),
format: String(localized: "system.dm.blocked_recipient", defaultValue: "cannot send message to %@: person is blocked.", comment: "System message when attempting to message a blocked user"),
locale: .current,
nickname
)
@@ -298,7 +298,7 @@ final class ChatPrivateConversationCoordinator {
} else {
context.setPrivateDeliveryStatus(
.failed(
reason: String(localized: "content.delivery.reason.unreachable", comment: "Failure reason when a peer is unreachable")
reason: String(localized: "content.delivery.reason.unreachable", defaultValue: "peer not reachable", comment: "Failure reason when a peer is unreachable")
),
forMessageID: messageID,
peerID: peerID
@@ -306,7 +306,7 @@ final class ChatPrivateConversationCoordinator {
let name = recipientNickname ?? "user"
context.addSystemMessage(
String(
format: String(localized: "system.dm.unreachable", comment: "System message when a recipient is unreachable"),
format: String(localized: "system.dm.unreachable", defaultValue: "cannot send message to %@ - peer is not reachable via mesh or nostr.", comment: "System message when a recipient is unreachable"),
locale: .current,
name
)
@@ -317,7 +317,7 @@ final class ChatPrivateConversationCoordinator {
func sendGeohashDM(_ content: String, to peerID: PeerID) {
guard case .location(let channel) = context.activeChannel else {
context.addSystemMessage(
String(localized: "system.location.not_in_channel", comment: "System message when attempting to send without being in a location channel")
String(localized: "system.location.not_in_channel", defaultValue: "cannot send: not in a location channel", comment: "System message when attempting to send without being in a location channel")
)
return
}
@@ -341,7 +341,7 @@ final class ChatPrivateConversationCoordinator {
guard let recipientHex = context.nostrKeyMapping[peerID] else {
context.setPrivateDeliveryStatus(
.failed(
reason: String(localized: "content.delivery.reason.unknown_recipient", comment: "Failure reason when the recipient is unknown")
reason: String(localized: "content.delivery.reason.unknown_recipient", defaultValue: "unknown recipient", comment: "Failure reason when the recipient is unknown")
),
forMessageID: messageID,
peerID: peerID
@@ -352,13 +352,13 @@ final class ChatPrivateConversationCoordinator {
if context.isNostrBlocked(pubkeyHexLowercased: recipientHex) {
context.setPrivateDeliveryStatus(
.failed(
reason: String(localized: "content.delivery.reason.blocked", comment: "Failure reason when the user is blocked")
reason: String(localized: "content.delivery.reason.blocked", defaultValue: "user is blocked", comment: "Failure reason when the user is blocked")
),
forMessageID: messageID,
peerID: peerID
)
context.addSystemMessage(
String(localized: "system.dm.blocked_generic", comment: "System message when sending fails because user is blocked")
String(localized: "system.dm.blocked_generic", defaultValue: "cannot send message: person is blocked.", comment: "System message when sending fails because user is blocked")
)
return
}
@@ -368,7 +368,7 @@ final class ChatPrivateConversationCoordinator {
if recipientHex.lowercased() == identity.publicKeyHex.lowercased() {
context.setPrivateDeliveryStatus(
.failed(
reason: String(localized: "content.delivery.reason.self", comment: "Failure reason when attempting to message yourself")
reason: String(localized: "content.delivery.reason.self", defaultValue: "cannot message yourself", comment: "Failure reason when attempting to message yourself")
),
forMessageID: messageID,
peerID: peerID
@@ -390,7 +390,7 @@ final class ChatPrivateConversationCoordinator {
} catch {
context.setPrivateDeliveryStatus(
.failed(
reason: String(localized: "content.delivery.reason.send_error", comment: "Failure reason for a generic send error")
reason: String(localized: "content.delivery.reason.send_error", defaultValue: "send error", comment: "Failure reason for a generic send error")
),
forMessageID: messageID,
peerID: peerID
@@ -107,7 +107,7 @@ private extension ChatViewModelBootstrapper {
category: .session
)
viewModel.conversations.setDeliveryStatus(
.failed(reason: String(localized: "content.delivery.reason.not_delivered", comment: "Failure reason shown when the router gave up delivering a message")),
.failed(reason: String(localized: "content.delivery.reason.not_delivered", defaultValue: "not delivered", comment: "Failure reason shown when the router gave up delivering a message")),
forMessageID: messageID
)
}
@@ -19,7 +19,7 @@ extension ChatViewModel {
self.torStatusAnnounced = true
// Post only in geohash channels (queue if not active)
self.addGeohashOnlySystemMessage(
String(localized: "system.tor.starting", comment: "System message when Tor is starting")
String(localized: "system.tor.starting", defaultValue: "starting tor...", comment: "System message when Tor is starting")
)
}
}
@@ -30,7 +30,7 @@ extension ChatViewModel {
self.torRestartPending = true
// Post only in geohash channels (queue if not active)
self.addGeohashOnlySystemMessage(
String(localized: "system.tor.restarting", comment: "System message when Tor is restarting")
String(localized: "system.tor.restarting", defaultValue: "tor restarting to recover connectivity...", comment: "System message when Tor is restarting")
)
}
}
@@ -41,13 +41,13 @@ extension ChatViewModel {
if self.torRestartPending {
// Post only in geohash channels (queue if not active)
self.addGeohashOnlySystemMessage(
String(localized: "system.tor.restarted", comment: "System message when Tor has restarted")
String(localized: "system.tor.restarted", defaultValue: "tor restarted. network routing restored.", comment: "System message when Tor has restarted")
)
self.torRestartPending = false
} else if TorManager.shared.torEnforced && !self.torInitialReadyAnnounced {
// Initial start completed
self.addGeohashOnlySystemMessage(
String(localized: "system.tor.started", comment: "System message when Tor has started")
String(localized: "system.tor.started", defaultValue: "tor started. routing all chats via tor for IP privacy.", comment: "System message when Tor has started")
)
self.torInitialReadyAnnounced = true
}
+2 -2
View File
@@ -144,7 +144,7 @@ struct AppInfoView: View {
// Custom header for macOS
HStack {
Spacer()
Button("app_info.done") {
Button(String(localized: "app_info.done", defaultValue: "DONE")) {
dismiss()
}
.buttonStyle(.plain)
@@ -256,7 +256,7 @@ struct AppInfoView: View {
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.accessibilityHint(Text("app_info.network.topology.hint"))
.accessibilityHint(Text(String(localized: "app_info.network.topology.hint", defaultValue: "opens the mesh topology map")))
}
}
@@ -16,32 +16,32 @@ extension DeliveryStatus {
var bitchatDescription: String {
switch self {
case .sending:
return String(localized: "content.delivery.sending", comment: "Delivery status description while a private message is being sent")
return String(localized: "content.delivery.sending", defaultValue: "sending...", comment: "Delivery status description while a private message is being sent")
case .sent:
return String(localized: "content.delivery.sent", comment: "Delivery status description for a sent but not yet confirmed private message")
return String(localized: "content.delivery.sent", defaultValue: "sent — no delivery confirmation yet", comment: "Delivery status description for a sent but not yet confirmed private message")
case .carried:
return String(localized: "content.delivery.carried", defaultValue: "Carried by a friend who may meet them", comment: "Delivery status description for messages handed to a courier for physical delivery")
case .delivered(let nickname, _):
return String(
format: String(localized: "content.delivery.delivered_to", comment: "Tooltip for delivered private messages"),
format: String(localized: "content.delivery.delivered_to", defaultValue: "delivered to %@", comment: "Tooltip for delivered private messages"),
locale: .current,
nickname
)
case .read(let nickname, _):
return String(
format: String(localized: "content.delivery.read_by", comment: "Tooltip for read private messages"),
format: String(localized: "content.delivery.read_by", defaultValue: "read by %@", comment: "Tooltip for read private messages"),
locale: .current,
nickname
)
case .failed(let reason):
return String(
format: String(localized: "content.delivery.failed", comment: "Tooltip for failed message delivery"),
format: String(localized: "content.delivery.failed", defaultValue: "failed: %@", comment: "Tooltip for failed message delivery"),
locale: .current,
reason
)
case .partiallyDelivered(let reached, let total):
return String(
format: String(localized: "content.delivery.delivered_members", comment: "Tooltip for partially delivered messages"),
format: String(localized: "content.delivery.delivered_members", defaultValue: "delivered to %1$d of %2$d members", comment: "Tooltip for partially delivered messages"),
locale: .current,
reached,
total
@@ -70,9 +70,9 @@ struct PaymentChipView: View {
var label: String {
switch self {
case .cashu:
String(localized: "content.payment.cashu", comment: "Label for Cashu payment chip")
String(localized: "content.payment.cashu", defaultValue: "pay via cashu", comment: "Label for Cashu payment chip")
case .lightning:
String(localized: "content.payment.lightning", comment: "Label for Lightning payment chip")
String(localized: "content.payment.lightning", defaultValue: "pay via lightning", comment: "Label for Lightning payment chip")
}
}
}
@@ -145,18 +145,18 @@ struct PaymentChipView: View {
Button {
copyToPasteboard(token)
} label: {
Label(String(localized: "content.payment.copy_token", comment: "Context menu action copying a Cashu token to the pasteboard"), systemImage: "doc.on.doc")
Label(String(localized: "content.payment.copy_token", defaultValue: "copy token", comment: "Context menu action copying a Cashu token to the pasteboard"), systemImage: "doc.on.doc")
}
Button {
redeemCashu()
} label: {
Label(String(localized: "content.payment.redeem_wallet", comment: "Context menu action opening a Cashu token in an ecash wallet app"), systemImage: "wallet.pass")
Label(String(localized: "content.payment.redeem_wallet", defaultValue: "redeem in wallet", comment: "Context menu action opening a Cashu token in an ecash wallet app"), systemImage: "wallet.pass")
}
if let webURL = paymentType.cashuWebRedeemURL {
Button {
openExternalURL(webURL)
} label: {
Label(String(localized: "content.payment.redeem_web", comment: "Context menu action opening a Cashu token in the web redemption page"), systemImage: "safari")
Label(String(localized: "content.payment.redeem_web", defaultValue: "redeem on web", comment: "Context menu action opening a Cashu token in the web redemption page"), systemImage: "safari")
}
}
}
@@ -24,6 +24,6 @@ struct SheetCloseButton: View {
.contentShape(Rectangle().inset(by: -6))
}
.buttonStyle(.plain)
.accessibilityLabel(String(localized: "common.close", comment: "Accessibility label for close buttons"))
.accessibilityLabel(String(localized: "common.close", defaultValue: "close", comment: "Accessibility label for close buttons"))
}
}
@@ -69,7 +69,7 @@ struct TextMessageView: View {
}
.buttonStyle(.plain)
.accessibilityHint(
String(localized: "content.accessibility.delivery_detail_hint", comment: "Accessibility hint for the delivery status glyph explaining a tap reveals details")
String(localized: "content.accessibility.delivery_detail_hint", defaultValue: "tap to show delivery details", comment: "Accessibility hint for the delivery status glyph explaining a tap reveals details")
)
}
}
+13 -13
View File
@@ -118,17 +118,17 @@ private extension ContentComposerView {
let isGeoDM = privateConversationModel.selectedPeerID?.isGeoDM == true
let target = isGeoDM ? header.displayName : "@\(header.displayName)"
return String(
format: String(localized: "content.input.placeholder.private", comment: "Composer placeholder inside a private chat, naming the conversation partner"),
format: String(localized: "content.input.placeholder.private", defaultValue: "message %@ — private", comment: "Composer placeholder inside a private chat, naming the conversation partner"),
locale: .current,
target
)
}
switch locationChannelsModel.selectedChannel {
case .mesh:
return String(localized: "content.input.placeholder.mesh", comment: "Composer placeholder for the public mesh channel")
return String(localized: "content.input.placeholder.mesh", defaultValue: "message #mesh — public, nearby", comment: "Composer placeholder for the public mesh channel")
case .location(let channel):
return String(
format: String(localized: "content.input.placeholder.location", comment: "Composer placeholder for a public geohash channel, naming it"),
format: String(localized: "content.input.placeholder.location", defaultValue: "message #%@ — public", comment: "Composer placeholder for a public geohash channel, naming it"),
locale: .current,
channel.geohash
)
@@ -184,15 +184,15 @@ private extension ContentComposerView {
showImagePicker = true
}
.accessibilityLabel(
String(localized: "content.accessibility.attach_photo", comment: "Accessibility label for the photo attachment button")
String(localized: "content.accessibility.attach_photo", defaultValue: "attach photo", comment: "Accessibility label for the photo attachment button")
)
.accessibilityHint(
String(localized: "content.accessibility.attach_photo_hint", comment: "Accessibility hint explaining the attachment button opens the photo library")
String(localized: "content.accessibility.attach_photo_hint", defaultValue: "opens the photo library; use the take photo action for the camera", comment: "Accessibility hint explaining the attachment button opens the photo library")
)
.accessibilityAddTraits(.isButton)
// The long-press camera path is unreachable for VoiceOver users;
// mirror it as a named action.
.accessibilityAction(named: Text("content.accessibility.take_photo", comment: "Accessibility action name for taking a photo with the camera")) {
.accessibilityAction(named: Text(String(localized: "content.accessibility.take_photo", defaultValue: "take photo with camera", comment: "Accessibility action name for taking a photo with the camera"))) {
imagePickerSourceType = .camera
showImagePicker = true
}
@@ -205,7 +205,7 @@ private extension ContentComposerView {
}
.buttonStyle(.plain)
.accessibilityLabel(
String(localized: "content.accessibility.choose_photo", comment: "Accessibility label for the macOS photo picker button")
String(localized: "content.accessibility.choose_photo", defaultValue: "choose photo", comment: "Accessibility label for the macOS photo picker button")
)
#endif
}
@@ -249,15 +249,15 @@ private extension ContentComposerView {
)
)
.accessibilityLabel(
String(localized: "content.accessibility.record_voice_note", comment: "Accessibility label for the voice note button")
String(localized: "content.accessibility.record_voice_note", defaultValue: "record voice note", comment: "Accessibility label for the voice note button")
)
.accessibilityValue(
voiceRecordingVM.state.isActive
? String(localized: "content.accessibility.recording", comment: "Accessibility value announced while a voice note is recording")
? String(localized: "content.accessibility.recording", defaultValue: "recording", comment: "Accessibility value announced while a voice note is recording")
: ""
)
.accessibilityHint(
String(localized: "content.accessibility.record_voice_hint", comment: "Accessibility hint explaining double-tap toggles voice recording")
String(localized: "content.accessibility.record_voice_hint", defaultValue: "double-tap to start recording, double-tap again to send", comment: "Accessibility hint explaining double-tap toggles voice recording")
)
.accessibilityAddTraits(.isButton)
// Press-and-hold drag gestures can't be activated by VoiceOver;
@@ -282,12 +282,12 @@ private extension ContentComposerView {
.buttonStyle(.plain)
.disabled(!enabled)
.accessibilityLabel(
String(localized: "content.accessibility.send_message", comment: "Accessibility label for the send message button")
String(localized: "content.accessibility.send_message", defaultValue: "send message", comment: "Accessibility label for the send message button")
)
.accessibilityHint(
enabled
? String(localized: "content.accessibility.send_hint_ready", comment: "Hint prompting the user to send the message")
: String(localized: "content.accessibility.send_hint_empty", comment: "Hint prompting the user to enter a message")
? String(localized: "content.accessibility.send_hint_ready", defaultValue: "double tap to send", comment: "Hint prompting the user to send the message")
: String(localized: "content.accessibility.send_hint_empty", defaultValue: "enter a message to send", comment: "Hint prompting the user to enter a message")
)
}
}
+15 -15
View File
@@ -44,7 +44,7 @@ struct ContentHeaderView: View {
// stays undiscoverable on purpose it's destructive.)
.accessibilityAddTraits(.isButton)
.accessibilityHint(
String(localized: "content.accessibility.app_info_hint", comment: "Accessibility hint on the bitchat/ logo explaining a tap opens app info")
String(localized: "content.accessibility.app_info_hint", defaultValue: "shows app info", comment: "Accessibility hint on the bitchat/ logo explaining a tap opens app info")
)
.accessibilityAction {
appChromeModel.presentAppInfo()
@@ -56,7 +56,7 @@ struct ContentHeaderView: View {
.foregroundColor(palette.secondary)
TextField(
"content.input.nickname_placeholder",
String(localized: "content.input.nickname_placeholder", defaultValue: "nickname"),
text: Binding(
get: { appChromeModel.nickname },
set: { appChromeModel.setNickname($0) }
@@ -133,7 +133,7 @@ struct ContentHeaderView: View {
}
.buttonStyle(.plain)
.accessibilityLabel(
String(localized: "content.accessibility.open_unread_private_chat", comment: "Accessibility label for the unread private chat button")
String(localized: "content.accessibility.open_unread_private_chat", defaultValue: "open unread private chat", comment: "Accessibility label for the unread private chat button")
)
}
@@ -151,7 +151,7 @@ struct ContentHeaderView: View {
}
.buttonStyle(.plain)
.accessibilityLabel(
String(localized: "content.accessibility.location_notes", comment: "Accessibility label for location notes button")
String(localized: "content.accessibility.location_notes", defaultValue: "location notes for this place", comment: "Accessibility label for location notes button")
)
}
@@ -178,7 +178,7 @@ struct ContentHeaderView: View {
.buttonStyle(.plain)
.accessibilityLabel(
String(
format: String(localized: "content.accessibility.toggle_bookmark", comment: "Accessibility label for toggling a geohash bookmark"),
format: String(localized: "content.accessibility.toggle_bookmark", defaultValue: "toggle bookmark for #%@", comment: "Accessibility label for toggling a geohash bookmark"),
locale: .current,
channel.geohash
)
@@ -211,7 +211,7 @@ struct ContentHeaderView: View {
.frame(maxHeight: .infinity)
.contentShape(Rectangle())
.accessibilityLabel(
String(localized: "content.accessibility.location_channels", comment: "Accessibility label for the location channels button")
String(localized: "content.accessibility.location_channels", defaultValue: "location channels", comment: "Accessibility label for the location channels button")
)
}
.buttonStyle(.plain)
@@ -238,7 +238,7 @@ struct ContentHeaderView: View {
.buttonStyle(.plain)
.accessibilityLabel(
String(
format: String(localized: "content.accessibility.people_count", comment: "Accessibility label announcing number of people in header"),
format: String(localized: "content.accessibility.people_count", defaultValue: "%#@people@", comment: "Accessibility label announcing number of people in header"),
locale: .current,
headerOtherPeersCount
)
@@ -247,8 +247,8 @@ struct ContentHeaderView: View {
// color; say it.
.accessibilityValue(
headerPeersReachable
? String(localized: "content.accessibility.peers_connected", comment: "Accessibility value when peers are reachable")
: String(localized: "content.accessibility.peers_none", comment: "Accessibility value when no peers are reachable")
? String(localized: "content.accessibility.peers_connected", defaultValue: "connected", comment: "Accessibility value when peers are reachable")
: String(localized: "content.accessibility.peers_none", defaultValue: "no one reachable", comment: "Accessibility value when no peers are reachable")
)
}
.layoutPriority(3)
@@ -325,10 +325,10 @@ struct ContentHeaderView: View {
.onChange(of: locationChannelsModel.permissionState) { _ in
locationChannelsModel.refreshMeshChannelsIfNeeded()
}
.alert("content.alert.screenshot.title", isPresented: $appChromeModel.showScreenshotPrivacyWarning) {
Button("common.ok", role: .cancel) {}
.alert(String(localized: "content.alert.screenshot.title", defaultValue: "heads up"), isPresented: $appChromeModel.showScreenshotPrivacyWarning) {
Button(String(localized: "common.ok", defaultValue: "OK"), role: .cancel) {}
} message: {
Text("content.alert.screenshot.message")
Text(String(localized: "content.alert.screenshot.message", defaultValue: "screenshots of location channels will reveal your location. think before sharing publicly."))
}
.themedChromePanel(edge: .top)
}
@@ -392,7 +392,7 @@ private struct ContentLocationNotesUnavailableView: View {
var body: some View {
VStack(spacing: 12) {
HStack {
Text("content.notes.title")
Text(String(localized: "content.notes.title", defaultValue: "notes"))
.bitchatFont(size: 16, weight: .bold)
Spacer()
SheetCloseButton { showLocationNotes = false }
@@ -401,10 +401,10 @@ private struct ContentLocationNotesUnavailableView: View {
.frame(minHeight: headerHeight)
.padding(.horizontal, 12)
.themedChromePanel(edge: .top)
Text("content.notes.location_unavailable")
Text(String(localized: "content.notes.location_unavailable", defaultValue: "location unavailable"))
.bitchatFont(size: 14)
.foregroundColor(palette.secondary)
Button("content.location.enable") {
Button(String(localized: "content.location.enable", defaultValue: "enable location")) {
locationChannelsModel.enableAndRefresh()
}
.buttonStyle(.bordered)
+17 -17
View File
@@ -163,10 +163,10 @@ private struct ContentPeopleListView: View {
// .help maps to the accessibility *hint* on iOS, so the
// button still needs a spoken name.
.accessibilityLabel(
String(localized: "content.accessibility.verification", comment: "Accessibility label for the verification QR button")
String(localized: "content.accessibility.verification", defaultValue: "verify encryption", comment: "Accessibility label for the verification QR button")
)
.help(
String(localized: "content.help.verification", comment: "Help text for verification button")
String(localized: "content.help.verification", defaultValue: "verification: show my QR or scan a friend", comment: "Help text for verification button")
)
}
SheetCloseButton {
@@ -262,7 +262,7 @@ private struct ContentPeopleListView: View {
private extension ContentPeopleListView {
var peopleSheetTitle: String {
String(localized: "content.header.people", comment: "Title for the people list sheet").lowercased()
String(localized: "content.header.people", defaultValue: "PEOPLE", comment: "Title for the people list sheet").lowercased()
}
var peopleSheetSubtitle: String? {
@@ -329,7 +329,7 @@ private struct ContentPrivateChatSheetView: View {
}
.buttonStyle(.plain)
.accessibilityLabel(
String(localized: "content.accessibility.back_to_main_chat", comment: "Accessibility label for returning to main chat")
String(localized: "content.accessibility.back_to_main_chat", defaultValue: "back to main chat", comment: "Accessibility label for returning to main chat")
)
Spacer(minLength: 0)
@@ -354,8 +354,8 @@ private struct ContentPrivateChatSheetView: View {
.buttonStyle(.plain)
.accessibilityLabel(
headerState.isFavorite
? String(localized: "content.accessibility.remove_favorite", comment: "Accessibility label to remove a favorite")
: String(localized: "content.accessibility.add_favorite", comment: "Accessibility label to add a favorite")
? String(localized: "content.accessibility.remove_favorite", defaultValue: "remove from favorites", comment: "Accessibility label to remove a favorite")
: String(localized: "content.accessibility.add_favorite", defaultValue: "add to favorites", comment: "Accessibility label to add a favorite")
)
}
}
@@ -464,7 +464,7 @@ private struct ContentPrivateChatSheetView: View {
private var privacyCaptionText: String {
// Group chats are ChaCha20-Poly1305 sealed to the roster's shared key.
if privateConversationModel.selectedPeerID?.isGroup == true {
return String(localized: "content.private.caption_group", comment: "Caption above the group chat composer noting messages are encrypted to group members")
return String(localized: "content.private.caption_group", defaultValue: "encrypted group · members only", comment: "Caption above the group chat composer noting messages are encrypted to group members")
}
// Geohash DMs are NIP-17 gift-wrapped always end-to-end encrypted,
// even though they carry no Noise session status. Mesh DMs earn the
@@ -477,9 +477,9 @@ private struct ContentPrivateChatSheetView: View {
}
}()
if isGeoDM || noiseSecured {
return String(localized: "content.private.caption_encrypted", comment: "Caption above the private chat composer once the session is end-to-end encrypted")
return String(localized: "content.private.caption_encrypted", defaultValue: "private · end-to-end encrypted", comment: "Caption above the private chat composer once the session is end-to-end encrypted")
}
return String(localized: "content.private.caption", comment: "Caption above the private chat composer before encryption is established")
return String(localized: "content.private.caption", defaultValue: "private conversation", comment: "Caption above the private chat composer before encryption is established")
}
}
@@ -523,27 +523,27 @@ private struct ContentPrivateHeaderInfoButton: View {
Image(systemName: "person.3.fill")
.font(.bitchatSystem(size: 14))
.foregroundColor(palette.primary)
.accessibilityLabel(String(localized: "content.accessibility.group_chat", comment: "Accessibility label for the group chat indicator"))
.accessibilityLabel(String(localized: "content.accessibility.group_chat", defaultValue: "Group chat", comment: "Accessibility label for the group chat indicator"))
} else {
switch headerState.availability {
case .bluetoothConnected:
Image(systemName: "dot.radiowaves.left.and.right")
.font(.bitchatSystem(size: 14))
.foregroundColor(palette.primary)
.accessibilityLabel(String(localized: "content.accessibility.connected_mesh", comment: "Accessibility label for mesh-connected peer indicator"))
.accessibilityLabel(String(localized: "content.accessibility.connected_mesh", defaultValue: "connected via mesh", comment: "Accessibility label for mesh-connected peer indicator"))
case .meshReachable:
Image(systemName: "point.3.filled.connected.trianglepath.dotted")
.font(.bitchatSystem(size: 14))
.foregroundColor(palette.primary)
.accessibilityLabel(String(localized: "content.accessibility.reachable_mesh", comment: "Accessibility label for mesh-reachable peer indicator"))
.accessibilityLabel(String(localized: "content.accessibility.reachable_mesh", defaultValue: "reachable via mesh", comment: "Accessibility label for mesh-reachable peer indicator"))
case .nostrAvailable:
Image(systemName: "globe")
.font(.bitchatSystem(size: 14))
.foregroundColor(.purple)
.accessibilityLabel(String(localized: "content.accessibility.available_nostr", comment: "Accessibility label for Nostr-available peer indicator"))
.accessibilityLabel(String(localized: "content.accessibility.available_nostr", defaultValue: "available via Nostr", comment: "Accessibility label for Nostr-available peer indicator"))
case .offline:
// Absence of a glyph was the only offline signal; say it.
Text("mesh_peers.state.offline")
Text(String(localized: "mesh_peers.state.offline", defaultValue: "offline"))
.bitchatFont(size: 11)
.foregroundColor(palette.secondary)
}
@@ -574,7 +574,7 @@ private struct ContentPrivateHeaderInfoButton: View {
)
.accessibilityLabel(
String(
format: String(localized: "content.accessibility.encryption_status", comment: "Accessibility label announcing encryption status"),
format: String(localized: "content.accessibility.encryption_status", defaultValue: "encryption status: %@", comment: "Accessibility label announcing encryption status"),
locale: .current,
encryptionStatus.accessibilityDescription
)
@@ -585,7 +585,7 @@ private struct ContentPrivateHeaderInfoButton: View {
.buttonStyle(.plain)
.accessibilityLabel(
String(
format: String(localized: "content.accessibility.private_chat_header", comment: "Accessibility label describing the private chat header"),
format: String(localized: "content.accessibility.private_chat_header", defaultValue: "private chat with %@", comment: "Accessibility label describing the private chat header"),
locale: .current,
headerState.displayName
)
@@ -593,7 +593,7 @@ private struct ContentPrivateHeaderInfoButton: View {
.accessibilityHint(
headerState.isGroupConversation
? ""
: String(localized: "content.accessibility.view_fingerprint_hint", comment: "Accessibility hint for viewing encryption fingerprint")
: String(localized: "content.accessibility.view_fingerprint_hint", defaultValue: "tap to view encryption fingerprint", comment: "Accessibility hint for viewing encryption fingerprint")
)
.frame(minHeight: headerHeight)
}
+5 -5
View File
@@ -204,20 +204,20 @@ struct ContentView: View {
}
}
.alert("Recording Error", isPresented: $voiceRecordingVM.showAlert, actions: {
Button("common.ok", role: .cancel) {}
Button(String(localized: "common.ok", defaultValue: "OK"), role: .cancel) {}
if voiceRecordingVM.state == .permissionDenied {
Button("location_channels.action.open_settings") {
Button(String(localized: "location_channels.action.open_settings", defaultValue: "open settings")) {
SystemSettings.microphone.open()
}
}
}, message: {
Text(voiceRecordingVM.state.alertMessage)
})
.alert("content.alert.bluetooth_required.title", isPresented: $appChromeModel.showBluetoothAlert) {
Button("content.alert.bluetooth_required.settings") {
.alert(String(localized: "content.alert.bluetooth_required.title", defaultValue: "bluetooth required"), isPresented: $appChromeModel.showBluetoothAlert) {
Button(String(localized: "content.alert.bluetooth_required.settings", defaultValue: "settings")) {
SystemSettings.bluetooth.open()
}
Button("common.ok", role: .cancel) {}
Button(String(localized: "common.ok", defaultValue: "OK"), role: .cancel) {}
} message: {
Text(appChromeModel.bluetoothAlertMessage)
}
+3 -3
View File
@@ -30,7 +30,7 @@ struct FingerprintView: View {
static let verifiedMessage: LocalizedStringKey = "fingerprint.message.verified"
static func verifyHint(_ nickname: String) -> String {
String(
format: String(localized: "fingerprint.message.verify_hint", comment: "Instruction to compare fingerprints with a named peer"),
format: String(localized: "fingerprint.message.verify_hint", defaultValue: "compare these fingerprints with %@ using a secure channel.", comment: "Instruction to compare fingerprints with a named peer"),
locale: .current,
nickname
)
@@ -40,13 +40,13 @@ struct FingerprintView: View {
static let vouchedBadge: LocalizedStringKey = "fingerprint.badge.vouched"
static func vouchedBy(_ count: Int) -> String {
String(
format: String(localized: "fingerprint.message.vouched_by", comment: "How many people the user verified have vouched for this peer"),
format: String(localized: "fingerprint.message.vouched_by", defaultValue: "vouched for by %#@people@ you verified", comment: "How many people the user verified have vouched for this peer"),
locale: .current,
count
)
}
static func unknownPeer() -> String {
String(localized: "common.unknown", comment: "Label for an unknown peer")
String(localized: "common.unknown", defaultValue: "unknown", comment: "Label for an unknown peer")
}
}
+8 -8
View File
@@ -10,16 +10,16 @@ struct GeohashPeopleList: View {
private enum Strings {
static let noneNearby: LocalizedStringKey = "geohash_people.none_nearby"
static let youSuffix: LocalizedStringKey = "geohash_people.you_suffix"
static let blockedTooltip = String(localized: "geohash_people.tooltip.blocked", comment: "Tooltip shown next to users blocked in geohash channels")
static let blockedTooltip = String(localized: "geohash_people.tooltip.blocked", defaultValue: "blocked in geohash", comment: "Tooltip shown next to users blocked in geohash channels")
static let unblock: LocalizedStringKey = "geohash_people.action.unblock"
static let block: LocalizedStringKey = "geohash_people.action.block"
static let unblockText = String(localized: "geohash_people.action.unblock", comment: "Context menu action to unblock a person")
static let blockText = String(localized: "geohash_people.action.block", comment: "Context menu action to block a person")
static let teleported = String(localized: "geohash_people.state.teleported", comment: "State label for someone who joined the location channel from elsewhere")
static let nearby = String(localized: "geohash_people.state.nearby", comment: "State label for someone physically in the location channel's area")
static let blockedState = String(localized: "mesh_peers.state.blocked", comment: "State label for a blocked peer")
static let youState = String(localized: "geohash_people.state.you", comment: "State label marking your own row in the people list")
static let openDMHint = String(localized: "mesh_peers.accessibility.open_dm_hint", comment: "Accessibility hint on a peer row explaining activation opens a private chat")
static let unblockText = String(localized: "geohash_people.action.unblock", defaultValue: "unblock", comment: "Context menu action to unblock a person")
static let blockText = String(localized: "geohash_people.action.block", defaultValue: "block", comment: "Context menu action to block a person")
static let teleported = String(localized: "geohash_people.state.teleported", defaultValue: "teleported from elsewhere", comment: "State label for someone who joined the location channel from elsewhere")
static let nearby = String(localized: "geohash_people.state.nearby", defaultValue: "in this area", comment: "State label for someone physically in the location channel's area")
static let blockedState = String(localized: "mesh_peers.state.blocked", defaultValue: "blocked", comment: "State label for a blocked peer")
static let youState = String(localized: "geohash_people.state.you", defaultValue: "you", comment: "State label marking your own row in the people list")
static let openDMHint = String(localized: "mesh_peers.accessibility.open_dm_hint", defaultValue: "opens a private chat", comment: "Accessibility hint on a peer row explaining activation opens a private chat")
}
var body: some View {
+6 -6
View File
@@ -10,12 +10,12 @@ struct GroupChatList: View {
let onTapGroup: (PeerID) -> Void
private enum Strings {
static let header = String(localized: "groups.section.header", comment: "Section header above the private groups list")
static let creator = String(localized: "groups.state.creator", comment: "State label for a group the user created")
static let unread = String(localized: "mesh_peers.state.unread", comment: "State label for a peer with unread private messages")
static let newMessagesTooltip = String(localized: "mesh_peers.tooltip.new_messages", comment: "Tooltip for the unread messages indicator")
static let openGroupHint = String(localized: "groups.accessibility.open_group_hint", comment: "Accessibility hint on a group row explaining activation opens the group chat")
static let memberCountFormat = String(localized: "groups.member_count %@", comment: "Member count shown next to a group name; placeholder is the count")
static let header = String(localized: "groups.section.header", defaultValue: "groups", comment: "Section header above the private groups list")
static let creator = String(localized: "groups.state.creator", defaultValue: "Creator", comment: "State label for a group the user created")
static let unread = String(localized: "mesh_peers.state.unread", defaultValue: "new messages", comment: "State label for a peer with unread private messages")
static let newMessagesTooltip = String(localized: "mesh_peers.tooltip.new_messages", defaultValue: "new messages", comment: "Tooltip for the unread messages indicator")
static let openGroupHint = String(localized: "groups.accessibility.open_group_hint", defaultValue: "Opens the group chat", comment: "Accessibility hint on a group row explaining activation opens the group chat")
static let memberCountFormat = String(localized: "groups.member_count %@", defaultValue: "(%@)", comment: "Member count shown next to a group name; placeholder is the count")
}
var body: some View {
+8 -8
View File
@@ -32,13 +32,13 @@ struct LocationChannelsSheet: View {
static let toggleOn: LocalizedStringKey = "common.toggle.on"
static let toggleOff: LocalizedStringKey = "common.toggle.off"
static let invalidGeohash = String(localized: "location_channels.error.invalid_geohash", comment: "Error shown when a custom geohash is invalid")
static let switchChannelHint = String(localized: "location_channels.accessibility.switch_hint", comment: "Accessibility hint on a channel row explaining activation switches to it")
static let addBookmark = String(localized: "location_channels.accessibility.add_bookmark", comment: "Accessibility action name for bookmarking a channel")
static let removeBookmark = String(localized: "location_channels.accessibility.remove_bookmark", comment: "Accessibility action name for removing a channel bookmark")
static let invalidGeohash = String(localized: "location_channels.error.invalid_geohash", defaultValue: "invalid geohash", comment: "Error shown when a custom geohash is invalid")
static let switchChannelHint = String(localized: "location_channels.accessibility.switch_hint", defaultValue: "switches to this channel", comment: "Accessibility hint on a channel row explaining activation switches to it")
static let addBookmark = String(localized: "location_channels.accessibility.add_bookmark", defaultValue: "bookmark channel", comment: "Accessibility action name for bookmarking a channel")
static let removeBookmark = String(localized: "location_channels.accessibility.remove_bookmark", defaultValue: "remove bookmark", comment: "Accessibility action name for removing a channel bookmark")
static func meshTitle(_ count: Int) -> String {
let label = String(localized: "location_channels.mesh_label", comment: "Label for the mesh channel row")
let label = String(localized: "location_channels.mesh_label", defaultValue: "mesh", comment: "Label for the mesh channel row")
return rowTitle(label: label, count: count)
}
@@ -73,7 +73,7 @@ struct LocationChannelsSheet: View {
static func subtitlePrefix(geohash: String, coverage: String) -> String {
String(
format: String(localized: "location_channels.subtitle_prefix", comment: "Subtitle prefix showing geohash and coverage"),
format: String(localized: "location_channels.subtitle_prefix", defaultValue: "#%1$@ • %2$@", comment: "Subtitle prefix showing geohash and coverage"),
locale: .current,
geohash, coverage
)
@@ -82,7 +82,7 @@ struct LocationChannelsSheet: View {
static func subtitle(prefix: String, name: String?) -> String {
guard let name, !name.isEmpty else { return prefix }
return String(
format: String(localized: "location_channels.subtitle_with_name", comment: "Subtitle combining prefix and resolved location name"),
format: String(localized: "location_channels.subtitle_with_name", defaultValue: "%1$@ • %2$@", comment: "Subtitle combining prefix and resolved location name"),
locale: .current,
prefix, name
)
@@ -90,7 +90,7 @@ struct LocationChannelsSheet: View {
private static func rowTitle(label: String, count: Int) -> String {
String(
format: String(localized: "location_channels.row_title", comment: "List row title with participant count"),
format: String(localized: "location_channels.row_title", defaultValue: "%1$@ [%2$#@people_count@]", comment: "List row title with participant count"),
locale: .current,
label, count
)
+1 -1
View File
@@ -135,7 +135,7 @@ struct LocationNotesView: View {
private func headerTitle(for count: Int) -> String {
String(
format: String(localized: "location_notes.header", comment: "Header displaying the geohash and localized note count"),
format: String(localized: "location_notes.header", defaultValue: "#%1$@ • %2$#@note_count@", comment: "Header displaying the geohash and localized note count"),
locale: .current,
"\(geohash) ± 1", count
)
+15 -15
View File
@@ -25,20 +25,20 @@ struct BlockRevealImageView: View {
@State private var loadFailed = false
private enum Strings {
static let tapToReveal = String(localized: "media.image.tap_to_reveal", comment: "Caption on a blurred incoming image inviting a tap to reveal it")
static let open = String(localized: "media.image.action.open", comment: "Context menu action that opens an image full screen")
static let reveal = String(localized: "media.image.action.reveal", comment: "Context menu action that reveals a blurred image")
static let hide = String(localized: "media.image.action.hide", comment: "Context menu action that re-blurs a revealed image")
static let delete = String(localized: "media.image.action.delete", comment: "Context menu action that deletes a received image")
static let deleteConfirmTitle = String(localized: "media.image.delete_confirm_title", comment: "Title of the confirmation dialog before deleting a received image")
static let deleteConfirmMessage = String(localized: "media.image.delete_confirm_message", comment: "Body of the confirmation dialog before deleting a received image")
static let hiddenImage = String(localized: "media.image.accessibility.hidden", comment: "Accessibility label for a blurred incoming image")
static let revealedImage = String(localized: "media.image.accessibility.revealed", comment: "Accessibility label for a revealed image")
static let revealHint = String(localized: "media.image.accessibility.hint.reveal", comment: "Accessibility hint for a blurred image; activating it reveals the image")
static let openHint = String(localized: "media.image.accessibility.hint.open", comment: "Accessibility hint for a revealed image; activating it opens the image full screen")
static let sendingImage = String(localized: "media.image.accessibility.sending", comment: "Accessibility label for an image that is still sending")
static let unavailableImage = String(localized: "media.image.accessibility.unavailable", comment: "Accessibility label for an image whose file could not be loaded")
static let cancelSend = String(localized: "media.accessibility.cancel_send", comment: "Accessibility label for the cancel button on an in-flight media send")
static let tapToReveal = String(localized: "media.image.tap_to_reveal", defaultValue: "tap to reveal", comment: "Caption on a blurred incoming image inviting a tap to reveal it")
static let open = String(localized: "media.image.action.open", defaultValue: "open image", comment: "Context menu action that opens an image full screen")
static let reveal = String(localized: "media.image.action.reveal", defaultValue: "reveal image", comment: "Context menu action that reveals a blurred image")
static let hide = String(localized: "media.image.action.hide", defaultValue: "hide image", comment: "Context menu action that re-blurs a revealed image")
static let delete = String(localized: "media.image.action.delete", defaultValue: "delete image", comment: "Context menu action that deletes a received image")
static let deleteConfirmTitle = String(localized: "media.image.delete_confirm_title", defaultValue: "delete this image?", comment: "Title of the confirmation dialog before deleting a received image")
static let deleteConfirmMessage = String(localized: "media.image.delete_confirm_message", defaultValue: "this cannot be undone — the sender may not be in range to send it again.", comment: "Body of the confirmation dialog before deleting a received image")
static let hiddenImage = String(localized: "media.image.accessibility.hidden", defaultValue: "hidden image", comment: "Accessibility label for a blurred incoming image")
static let revealedImage = String(localized: "media.image.accessibility.revealed", defaultValue: "image", comment: "Accessibility label for a revealed image")
static let revealHint = String(localized: "media.image.accessibility.hint.reveal", defaultValue: "reveals the image", comment: "Accessibility hint for a blurred image; activating it reveals the image")
static let openHint = String(localized: "media.image.accessibility.hint.open", defaultValue: "opens the image full screen", comment: "Accessibility hint for a revealed image; activating it opens the image full screen")
static let sendingImage = String(localized: "media.image.accessibility.sending", defaultValue: "sending image", comment: "Accessibility label for an image that is still sending")
static let unavailableImage = String(localized: "media.image.accessibility.unavailable", defaultValue: "image unavailable", comment: "Accessibility label for an image whose file could not be loaded")
static let cancelSend = String(localized: "media.accessibility.cancel_send", defaultValue: "cancel sending", comment: "Accessibility label for the cancel button on an in-flight media send")
}
init(
@@ -154,7 +154,7 @@ struct BlockRevealImageView: View {
Button(Strings.delete, role: .destructive) {
onDelete?()
}
Button("common.cancel", role: .cancel) {}
Button(String(localized: "common.cancel", defaultValue: "cancel"), role: .cancel) {}
} message: {
Text(verbatim: Strings.deleteConfirmMessage)
}
+1 -1
View File
@@ -67,7 +67,7 @@ struct MediaMessageView: View {
}
.buttonStyle(.plain)
.accessibilityHint(
String(localized: "content.accessibility.delivery_detail_hint", comment: "Accessibility hint for the delivery status glyph explaining a tap reveals details")
String(localized: "content.accessibility.delivery_detail_hint", defaultValue: "tap to show delivery details", comment: "Accessibility hint for the delivery status glyph explaining a tap reveals details")
)
}
}
+3 -3
View File
@@ -54,8 +54,8 @@ struct VoiceNoteView: View {
.buttonStyle(.plain)
.accessibilityLabel(
playback.isPlaying
? String(localized: "media.voice.accessibility.pause", comment: "Accessibility label for pausing voice note playback")
: String(localized: "media.voice.accessibility.play", comment: "Accessibility label for playing a voice note")
? String(localized: "media.voice.accessibility.pause", defaultValue: "pause voice note", comment: "Accessibility label for pausing voice note playback")
: String(localized: "media.voice.accessibility.play", defaultValue: "play voice note", comment: "Accessibility label for playing a voice note")
)
.accessibilityValue(playbackLabel)
@@ -83,7 +83,7 @@ struct VoiceNoteView: View {
}
.buttonStyle(.plain)
.accessibilityLabel(
String(localized: "media.accessibility.cancel_send", comment: "Accessibility label for the cancel button on an in-flight media send")
String(localized: "media.accessibility.cancel_send", defaultValue: "cancel sending", comment: "Accessibility label for the cancel button on an in-flight media send")
)
}
}
+18 -18
View File
@@ -16,24 +16,24 @@ struct MeshPeerList: View {
private enum Strings {
static let noneNearby: LocalizedStringKey = "geohash_people.none_nearby"
static let blockedTooltip = String(localized: "geohash_people.tooltip.blocked", comment: "Tooltip shown next to a blocked peer indicator")
static let newMessagesTooltip = String(localized: "mesh_peers.tooltip.new_messages", comment: "Tooltip for the unread messages indicator")
static let connected = String(localized: "content.accessibility.connected_mesh", comment: "Accessibility label for mesh-connected peer indicator")
static let reachable = String(localized: "content.accessibility.reachable_mesh", comment: "Accessibility label for mesh-reachable peer indicator")
static let nostr = String(localized: "content.accessibility.available_nostr", comment: "Accessibility label for Nostr-available peer indicator")
static let offline = String(localized: "mesh_peers.state.offline", comment: "State label for a peer that is not currently reachable")
static let favorite = String(localized: "mesh_peers.state.favorite", comment: "State label for a favorited peer")
static let unread = String(localized: "mesh_peers.state.unread", comment: "State label for a peer with unread private messages")
static let blocked = String(localized: "mesh_peers.state.blocked", comment: "State label for a blocked peer")
static let vouched = String(localized: "mesh_peers.state.vouched", comment: "State label for a peer vouched for by someone the user verified")
static let vouchedTooltip = String(localized: "mesh_peers.tooltip.vouched", comment: "Tooltip for the vouched (unfilled seal) badge next to a peer")
static let addFavorite = String(localized: "content.accessibility.add_favorite", comment: "Accessibility label to add a favorite")
static let removeFavorite = String(localized: "content.accessibility.remove_favorite", comment: "Accessibility label to remove a favorite")
static let showFingerprint = String(localized: "mesh_peers.action.fingerprint", comment: "Context menu action that shows a peer's fingerprint/verification screen")
static let openDMHint = String(localized: "mesh_peers.accessibility.open_dm_hint", comment: "Accessibility hint on a peer row explaining activation opens a private chat")
static let directMessage = String(localized: "content.actions.direct_message", comment: "Action that opens a private chat with the person")
static let block = String(localized: "geohash_people.action.block", comment: "Context menu action to block a person")
static let unblock = String(localized: "geohash_people.action.unblock", comment: "Context menu action to unblock a person")
static let blockedTooltip = String(localized: "geohash_people.tooltip.blocked", defaultValue: "blocked in geohash", comment: "Tooltip shown next to a blocked peer indicator")
static let newMessagesTooltip = String(localized: "mesh_peers.tooltip.new_messages", defaultValue: "new messages", comment: "Tooltip for the unread messages indicator")
static let connected = String(localized: "content.accessibility.connected_mesh", defaultValue: "connected via mesh", comment: "Accessibility label for mesh-connected peer indicator")
static let reachable = String(localized: "content.accessibility.reachable_mesh", defaultValue: "reachable via mesh", comment: "Accessibility label for mesh-reachable peer indicator")
static let nostr = String(localized: "content.accessibility.available_nostr", defaultValue: "available via Nostr", comment: "Accessibility label for Nostr-available peer indicator")
static let offline = String(localized: "mesh_peers.state.offline", defaultValue: "offline", comment: "State label for a peer that is not currently reachable")
static let favorite = String(localized: "mesh_peers.state.favorite", defaultValue: "favorite", comment: "State label for a favorited peer")
static let unread = String(localized: "mesh_peers.state.unread", defaultValue: "new messages", comment: "State label for a peer with unread private messages")
static let blocked = String(localized: "mesh_peers.state.blocked", defaultValue: "blocked", comment: "State label for a blocked peer")
static let vouched = String(localized: "mesh_peers.state.vouched", defaultValue: "vouched", comment: "State label for a peer vouched for by someone the user verified")
static let vouchedTooltip = String(localized: "mesh_peers.tooltip.vouched", defaultValue: "vouched for by someone you verified", comment: "Tooltip for the vouched (unfilled seal) badge next to a peer")
static let addFavorite = String(localized: "content.accessibility.add_favorite", defaultValue: "add to favorites", comment: "Accessibility label to add a favorite")
static let removeFavorite = String(localized: "content.accessibility.remove_favorite", defaultValue: "remove from favorites", comment: "Accessibility label to remove a favorite")
static let showFingerprint = String(localized: "mesh_peers.action.fingerprint", defaultValue: "show fingerprint", comment: "Context menu action that shows a peer's fingerprint/verification screen")
static let openDMHint = String(localized: "mesh_peers.accessibility.open_dm_hint", defaultValue: "opens a private chat", comment: "Accessibility hint on a peer row explaining activation opens a private chat")
static let directMessage = String(localized: "content.actions.direct_message", defaultValue: "direct message", comment: "Action that opens a private chat with the person")
static let block = String(localized: "geohash_people.action.block", defaultValue: "block", comment: "Context menu action to block a person")
static let unblock = String(localized: "geohash_people.action.unblock", defaultValue: "unblock", comment: "Context menu action to unblock a person")
}
var body: some View {
+6 -6
View File
@@ -40,12 +40,12 @@ struct MeshTopologyView: View {
#if os(macOS)
VStack(spacing: 0) {
HStack {
Text("topology.title")
Text(String(localized: "topology.title", defaultValue: "mesh topology"))
.bitchatFont(size: 16, weight: .bold)
.foregroundColor(palette.primary)
Spacer()
refreshButton
Button("app_info.done") {
Button(String(localized: "app_info.done", defaultValue: "DONE")) {
dismiss()
}
.buttonStyle(.plain)
@@ -62,7 +62,7 @@ struct MeshTopologyView: View {
NavigationView {
content
.themedSheetBackground()
.navigationTitle(Text("topology.title"))
.navigationTitle(Text(String(localized: "topology.title", defaultValue: "mesh topology")))
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
@@ -86,7 +86,7 @@ struct MeshTopologyView: View {
.foregroundColor(palette.primary)
}
.buttonStyle(.plain)
.accessibilityLabel(Text("topology.refresh"))
.accessibilityLabel(Text(String(localized: "topology.refresh", defaultValue: "refresh topology")))
}
@ViewBuilder
@@ -94,7 +94,7 @@ struct MeshTopologyView: View {
VStack(spacing: 12) {
if model.nodes.count <= 1 {
Spacer()
Text("topology.empty")
Text(String(localized: "topology.empty", defaultValue: "no mesh links yet — the map fills in as peer announces arrive"))
.bitchatFont(size: 14)
.foregroundColor(palette.secondary)
.multilineTextAlignment(.center)
@@ -109,7 +109,7 @@ struct MeshTopologyView: View {
Text(summaryText)
.bitchatFont(size: 13, weight: .semibold)
.foregroundColor(palette.primary)
Text("topology.caption")
Text(String(localized: "topology.caption", defaultValue: "estimated from gossiped neighbor lists (up to 10 per peer) — your device is highlighted"))
.bitchatFont(size: 11)
.foregroundColor(palette.secondary)
.multilineTextAlignment(.center)
+25 -25
View File
@@ -109,11 +109,11 @@ struct MessageListView: View {
// mentioning the only other participant is noise, and "DM"
// would just reopen the conversation that is already open.
if privatePeer == nil {
Button("content.actions.mention") {
Button(String(localized: "content.actions.mention", defaultValue: "mention")) {
insertMention(message.sender)
}
if let peerID = message.senderPeerID {
Button("content.actions.direct_message") {
Button(String(localized: "content.actions.direct_message", defaultValue: "direct message")) {
privateConversationModel.openConversation(for: peerID)
withAnimation(.easeInOut(duration: TransportConfig.uiAnimationMediumSeconds)) {
showSidebar = true
@@ -121,14 +121,14 @@ struct MessageListView: View {
}
}
}
Button("content.actions.hug") {
Button(String(localized: "content.actions.hug", defaultValue: "hug")) {
conversationUIModel.sendHug(to: message.sender)
}
Button("content.actions.slap") {
Button(String(localized: "content.actions.slap", defaultValue: "slap")) {
conversationUIModel.sendSlap(to: message.sender)
}
}
Button("content.message.copy") {
Button(String(localized: "content.message.copy", defaultValue: "copy message")) {
#if os(iOS)
UIPasteboard.general.string = message.content
#else
@@ -138,12 +138,12 @@ struct MessageListView: View {
#endif
}
if isResendableFailedMessage(message) {
Button("content.actions.resend") {
Button(String(localized: "content.actions.resend", defaultValue: "resend")) {
conversationUIModel.resendFailedPrivateMessage(message)
}
}
if showsUserActions {
Button("content.actions.block", role: .destructive) {
Button(String(localized: "content.actions.block", defaultValue: "block"), role: .destructive) {
conversationUIModel.block(peerID: message.senderPeerID, displayName: message.sender)
}
}
@@ -165,14 +165,14 @@ struct MessageListView: View {
showClearConfirmation = true
}
.confirmationDialog(
"content.clear.confirm_title",
String(localized: "content.clear.confirm_title", defaultValue: "clear this chat?"),
isPresented: $showClearConfirmation,
titleVisibility: .visible
) {
Button("content.clear.confirm_action", role: .destructive) {
Button(String(localized: "content.clear.confirm_action", defaultValue: "clear chat"), role: .destructive) {
conversationUIModel.clearCurrentConversation()
}
Button("common.cancel", role: .cancel) {}
Button(String(localized: "common.cancel", defaultValue: "cancel"), role: .cancel) {}
}
.onAppear {
scrollToBottom(on: proxy)
@@ -190,17 +190,17 @@ struct MessageListView: View {
onSelectedChannelChange(newChannel, proxy: proxy)
}
.confirmationDialog(
selectedMessageSender.map { "@\($0)" } ?? String(localized: "content.actions.title", comment: "Fallback title for the message action sheet"),
selectedMessageSender.map { "@\($0)" } ?? String(localized: "content.actions.title", defaultValue: "actions", comment: "Fallback title for the message action sheet"),
isPresented: $showMessageActions,
titleVisibility: .visible
) {
Button("content.actions.mention") {
Button(String(localized: "content.actions.mention", defaultValue: "mention")) {
if let sender = selectedMessageSender {
insertMention(sender)
}
}
Button("content.actions.direct_message") {
Button(String(localized: "content.actions.direct_message", defaultValue: "direct message")) {
if let peerID = selectedMessageSenderID {
privateConversationModel.openConversation(for: peerID)
withAnimation(.easeInOut(duration: TransportConfig.uiAnimationMediumSeconds)) {
@@ -209,23 +209,23 @@ struct MessageListView: View {
}
}
Button("content.actions.hug") {
Button(String(localized: "content.actions.hug", defaultValue: "hug")) {
if let sender = selectedMessageSender {
conversationUIModel.sendHug(to: sender)
}
}
Button("content.actions.slap") {
Button(String(localized: "content.actions.slap", defaultValue: "slap")) {
if let sender = selectedMessageSender {
conversationUIModel.sendSlap(to: sender)
}
}
Button("content.actions.block", role: .destructive) {
Button(String(localized: "content.actions.block", defaultValue: "block"), role: .destructive) {
conversationUIModel.block(peerID: selectedMessageSenderID, displayName: selectedMessageSender)
}
Button("common.cancel", role: .cancel) {}
Button(String(localized: "common.cancel", defaultValue: "cancel"), role: .cancel) {}
}
.onAppear {
// Also check when view appears
@@ -277,18 +277,18 @@ private extension MessageListView {
VStack(alignment: .leading, spacing: 6) {
switch locationChannelsModel.selectedChannel {
case .mesh:
emptyStateLine(String(localized: "content.empty.mesh_intro", comment: "First line of the empty mesh timeline explaining what the mesh channel is"))
emptyStateLine(String(localized: "content.empty.mesh_waiting", comment: "Second line of the empty mesh timeline saying no peers are in range yet"))
emptyStateLine(String(localized: "content.empty.switch_hint", comment: "Empty timeline hint pointing at the channel switcher and the help screen"))
emptyStateLine(String(localized: "content.empty.mesh_intro", defaultValue: "you're on #mesh — reaches people within bluetooth range", comment: "First line of the empty mesh timeline explaining what the mesh channel is"))
emptyStateLine(String(localized: "content.empty.mesh_waiting", defaultValue: "nobody in range yet... messages appear here", comment: "Second line of the empty mesh timeline saying no peers are in range yet"))
emptyStateLine(String(localized: "content.empty.switch_hint", defaultValue: "tap the channel name above to switch · tap bitchat/ for help", comment: "Empty timeline hint pointing at the channel switcher and the help screen"))
case .location(let channel):
emptyStateLine(
String(
format: String(localized: "content.empty.location_intro", comment: "First line of an empty geohash timeline naming the channel"),
format: String(localized: "content.empty.location_intro", defaultValue: "you're in #%@ — a public location channel over the internet", comment: "First line of an empty geohash timeline naming the channel"),
locale: .current,
channel.geohash
)
)
emptyStateLine(String(localized: "content.empty.switch_hint", comment: "Empty timeline hint pointing at the channel switcher and the help screen"))
emptyStateLine(String(localized: "content.empty.switch_hint", defaultValue: "tap the channel name above to switch · tap bitchat/ for help", comment: "Empty timeline hint pointing at the channel switcher and the help screen"))
}
}
.padding(.horizontal, 12)
@@ -368,7 +368,7 @@ private extension MessageListView {
if unseenCount > 0 {
Text(
String(
format: String(localized: "content.jump.new_count", comment: "Count of messages that arrived while scrolled up, shown in the jump-to-latest pill"),
format: String(localized: "content.jump.new_count", defaultValue: "%lld new", comment: "Count of messages that arrived while scrolled up, shown in the jump-to-latest pill"),
locale: .current,
unseenCount
)
@@ -389,10 +389,10 @@ private extension MessageListView {
}
var jumpToLatestAccessibilityLabel: String {
let base = String(localized: "content.accessibility.jump_to_latest", comment: "Accessibility label for the jump to latest messages button")
let base = String(localized: "content.accessibility.jump_to_latest", defaultValue: "jump to latest messages", comment: "Accessibility label for the jump to latest messages button")
guard unseenCount > 0 else { return base }
let count = String(
format: String(localized: "content.jump.new_count", comment: "Count of messages that arrived while scrolled up, shown in the jump-to-latest pill"),
format: String(localized: "content.jump.new_count", defaultValue: "%lld new", comment: "Count of messages that arrived while scrolled up, shown in the jump-to-latest pill"),
locale: .current,
unseenCount
)
+6 -6
View File
@@ -18,7 +18,7 @@ struct MyQRView: View {
private enum Strings {
static let title: LocalizedStringKey = "verification.my_qr.title"
static let accessibilityLabel = String(localized: "verification.my_qr.accessibility_label", comment: "Accessibility label describing the verification QR code")
static let accessibilityLabel = String(localized: "verification.my_qr.accessibility_label", defaultValue: "verification QR code", comment: "Accessibility label describing the verification QR code")
}
var body: some View {
@@ -124,13 +124,13 @@ struct QRScanView: View {
static let validate: LocalizedStringKey = "verification.scan.validate"
static func requested(_ nickname: String) -> String {
String(
format: String(localized: "verification.scan.status.requested", comment: "Status text when verification is requested for a nickname"),
format: String(localized: "verification.scan.status.requested", defaultValue: "verification requested for %@", comment: "Status text when verification is requested for a nickname"),
locale: .current,
nickname
)
}
static let notFound = String(localized: "verification.scan.status.no_peer", comment: "Status when no matching peer is found for a verification request")
static let invalid = String(localized: "verification.scan.status.invalid", comment: "Status when a scanned QR payload is invalid")
static let notFound = String(localized: "verification.scan.status.no_peer", defaultValue: "could not find matching peer", comment: "Status when no matching peer is found for a verification request")
static let invalid = String(localized: "verification.scan.status.invalid", defaultValue: "invalid or expired QR payload", comment: "Status when a scanned QR payload is invalid")
}
var body: some View {
@@ -296,7 +296,7 @@ struct VerificationSheetView: View {
VStack(spacing: 0) {
// Top header (always at top)
HStack {
Text("verification.sheet.title")
Text(String(localized: "verification.sheet.title", defaultValue: "VERIFY"))
.bitchatFont(size: 14, weight: .bold)
.foregroundColor(accentColor)
Spacer()
@@ -316,7 +316,7 @@ struct VerificationSheetView: View {
Group {
if showingScanner {
VStack(alignment: .leading, spacing: 12) {
Text("verification.scan.prompt_friend")
Text(String(localized: "verification.scan.prompt_friend", defaultValue: "scan a friend's QR"))
.bitchatFont(size: 16, weight: .bold)
.frame(maxWidth: .infinity)
.multilineTextAlignment(.center)