diff --git a/bitchat/App/AppRuntime.swift b/bitchat/App/AppRuntime.swift index a706cfac..fd8f0077 100644 --- a/bitchat/App/AppRuntime.swift +++ b/bitchat/App/AppRuntime.swift @@ -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") ) } } diff --git a/bitchat/App/PrivateConversationModels.swift b/bitchat/App/PrivateConversationModels.swift index d9920646..e8232b32 100644 --- a/bitchat/App/PrivateConversationModels.swift +++ b/bitchat/App/PrivateConversationModels.swift @@ -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 { diff --git a/bitchat/App/VerificationModel.swift b/bitchat/App/VerificationModel.swift index 5916a958..a265d709 100644 --- a/bitchat/App/VerificationModel.swift +++ b/bitchat/App/VerificationModel.swift @@ -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") } } diff --git a/bitchat/Models/CommandInfo.swift b/bitchat/Models/CommandInfo.swift index bbe5b82b..b656bad3 100644 --- a/bitchat/Models/CommandInfo.swift +++ b/bitchat/Models/CommandInfo.swift @@ -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") } } diff --git a/bitchat/Protocols/LocationChannel.swift b/bitchat/Protocols/LocationChannel.swift index cbe1c55f..e2864b0d 100644 --- a/bitchat/Protocols/LocationChannel.swift +++ b/bitchat/Protocols/LocationChannel.swift @@ -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") } } } diff --git a/bitchat/Services/BLE/BLEService.swift b/bitchat/Services/BLE/BLEService.swift index cc342856..da46238c 100644 --- a/bitchat/Services/BLE/BLEService.swift +++ b/bitchat/Services/BLE/BLEService.swift @@ -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")))) } } } diff --git a/bitchat/Services/LocationNotesManager.swift b/bitchat/Services/LocationNotesManager.swift index 910ecced..a3437fb5 100644 --- a/bitchat/Services/LocationNotesManager.swift +++ b/bitchat/Services/LocationNotesManager.swift @@ -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 ) diff --git a/bitchat/Services/NoiseEncryptionService.swift b/bitchat/Services/NoiseEncryptionService.swift index 1642f618..3cbd1520 100644 --- a/bitchat/Services/NoiseEncryptionService.swift +++ b/bitchat/Services/NoiseEncryptionService.swift @@ -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") } } } diff --git a/bitchat/ViewModels/ChatBluetoothAlertPolicy.swift b/bitchat/ViewModels/ChatBluetoothAlertPolicy.swift index 0e7168e4..14e8bad0 100644 --- a/bitchat/ViewModels/ChatBluetoothAlertPolicy.swift +++ b/bitchat/ViewModels/ChatBluetoothAlertPolicy.swift @@ -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: "") diff --git a/bitchat/ViewModels/ChatGroupCoordinator.swift b/bitchat/ViewModels/ChatGroupCoordinator.swift index a83cb024..eb50b577 100644 --- a/bitchat/ViewModels/ChatGroupCoordinator.swift +++ b/bitchat/ViewModels/ChatGroupCoordinator.swift @@ -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 ", 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 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 diff --git a/bitchat/ViewModels/ChatLifecycleCoordinator.swift b/bitchat/ViewModels/ChatLifecycleCoordinator.swift index 1241f71b..0117bf60 100644 --- a/bitchat/ViewModels/ChatLifecycleCoordinator.swift +++ b/bitchat/ViewModels/ChatLifecycleCoordinator.swift @@ -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") ) } } diff --git a/bitchat/ViewModels/ChatMediaTransferCoordinator.swift b/bitchat/ViewModels/ChatMediaTransferCoordinator.swift index bf0009da..43e90291 100644 --- a/bitchat/ViewModels/ChatMediaTransferCoordinator.swift +++ b/bitchat/ViewModels/ChatMediaTransferCoordinator.swift @@ -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")) } } } diff --git a/bitchat/ViewModels/ChatOutgoingCoordinator.swift b/bitchat/ViewModels/ChatOutgoingCoordinator.swift index 8d8b6a9e..4d1b4c56 100644 --- a/bitchat/ViewModels/ChatOutgoingCoordinator.swift +++ b/bitchat/ViewModels/ChatOutgoingCoordinator.swift @@ -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 } diff --git a/bitchat/ViewModels/ChatPrivateConversationCoordinator.swift b/bitchat/ViewModels/ChatPrivateConversationCoordinator.swift index e577e406..0347f32e 100644 --- a/bitchat/ViewModels/ChatPrivateConversationCoordinator.swift +++ b/bitchat/ViewModels/ChatPrivateConversationCoordinator.swift @@ -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 diff --git a/bitchat/ViewModels/ChatViewModelBootstrapper.swift b/bitchat/ViewModels/ChatViewModelBootstrapper.swift index caca90ec..722cd177 100644 --- a/bitchat/ViewModels/ChatViewModelBootstrapper.swift +++ b/bitchat/ViewModels/ChatViewModelBootstrapper.swift @@ -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 ) } diff --git a/bitchat/ViewModels/Extensions/ChatViewModel+Tor.swift b/bitchat/ViewModels/Extensions/ChatViewModel+Tor.swift index 443849db..3221445f 100644 --- a/bitchat/ViewModels/Extensions/ChatViewModel+Tor.swift +++ b/bitchat/ViewModels/Extensions/ChatViewModel+Tor.swift @@ -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 } diff --git a/bitchat/Views/AppInfoView.swift b/bitchat/Views/AppInfoView.swift index a4b82104..496f0230 100644 --- a/bitchat/Views/AppInfoView.swift +++ b/bitchat/Views/AppInfoView.swift @@ -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"))) } } diff --git a/bitchat/Views/Components/DeliveryStatusView.swift b/bitchat/Views/Components/DeliveryStatusView.swift index e106d2c1..f54ee88b 100644 --- a/bitchat/Views/Components/DeliveryStatusView.swift +++ b/bitchat/Views/Components/DeliveryStatusView.swift @@ -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 diff --git a/bitchat/Views/Components/PaymentChipView.swift b/bitchat/Views/Components/PaymentChipView.swift index bdda8772..5e09e0b3 100644 --- a/bitchat/Views/Components/PaymentChipView.swift +++ b/bitchat/Views/Components/PaymentChipView.swift @@ -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") } } } diff --git a/bitchat/Views/Components/SheetCloseButton.swift b/bitchat/Views/Components/SheetCloseButton.swift index e2891e35..a1dee0bc 100644 --- a/bitchat/Views/Components/SheetCloseButton.swift +++ b/bitchat/Views/Components/SheetCloseButton.swift @@ -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")) } } diff --git a/bitchat/Views/Components/TextMessageView.swift b/bitchat/Views/Components/TextMessageView.swift index 4c96b201..36f5ab81 100644 --- a/bitchat/Views/Components/TextMessageView.swift +++ b/bitchat/Views/Components/TextMessageView.swift @@ -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") ) } } diff --git a/bitchat/Views/ContentComposerView.swift b/bitchat/Views/ContentComposerView.swift index a3ec8d0f..bc154792 100644 --- a/bitchat/Views/ContentComposerView.swift +++ b/bitchat/Views/ContentComposerView.swift @@ -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") ) } } diff --git a/bitchat/Views/ContentHeaderView.swift b/bitchat/Views/ContentHeaderView.swift index 6a260b07..e61f06af 100644 --- a/bitchat/Views/ContentHeaderView.swift +++ b/bitchat/Views/ContentHeaderView.swift @@ -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) diff --git a/bitchat/Views/ContentSheetViews.swift b/bitchat/Views/ContentSheetViews.swift index 4d9d5c5b..62a4c0ec 100644 --- a/bitchat/Views/ContentSheetViews.swift +++ b/bitchat/Views/ContentSheetViews.swift @@ -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) } diff --git a/bitchat/Views/ContentView.swift b/bitchat/Views/ContentView.swift index bd434ad1..ce0819de 100644 --- a/bitchat/Views/ContentView.swift +++ b/bitchat/Views/ContentView.swift @@ -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) } diff --git a/bitchat/Views/FingerprintView.swift b/bitchat/Views/FingerprintView.swift index fb4541f5..aff63985 100644 --- a/bitchat/Views/FingerprintView.swift +++ b/bitchat/Views/FingerprintView.swift @@ -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") } } diff --git a/bitchat/Views/GeohashPeopleList.swift b/bitchat/Views/GeohashPeopleList.swift index 9613f227..42034944 100644 --- a/bitchat/Views/GeohashPeopleList.swift +++ b/bitchat/Views/GeohashPeopleList.swift @@ -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 { diff --git a/bitchat/Views/GroupChatList.swift b/bitchat/Views/GroupChatList.swift index 8b62d8a2..c4aef7a1 100644 --- a/bitchat/Views/GroupChatList.swift +++ b/bitchat/Views/GroupChatList.swift @@ -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 { diff --git a/bitchat/Views/LocationChannelsSheet.swift b/bitchat/Views/LocationChannelsSheet.swift index 0a6e077a..6e9e5b95 100644 --- a/bitchat/Views/LocationChannelsSheet.swift +++ b/bitchat/Views/LocationChannelsSheet.swift @@ -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 ) diff --git a/bitchat/Views/LocationNotesView.swift b/bitchat/Views/LocationNotesView.swift index d2cd3f16..581cec8f 100644 --- a/bitchat/Views/LocationNotesView.swift +++ b/bitchat/Views/LocationNotesView.swift @@ -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 ) diff --git a/bitchat/Views/Media/BlockRevealImageView.swift b/bitchat/Views/Media/BlockRevealImageView.swift index 8e5d8bd2..0da74568 100644 --- a/bitchat/Views/Media/BlockRevealImageView.swift +++ b/bitchat/Views/Media/BlockRevealImageView.swift @@ -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) } diff --git a/bitchat/Views/Media/MediaMessageView.swift b/bitchat/Views/Media/MediaMessageView.swift index 90fd13c4..116826be 100644 --- a/bitchat/Views/Media/MediaMessageView.swift +++ b/bitchat/Views/Media/MediaMessageView.swift @@ -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") ) } } diff --git a/bitchat/Views/Media/VoiceNoteView.swift b/bitchat/Views/Media/VoiceNoteView.swift index d2a63e6e..2687ac07 100644 --- a/bitchat/Views/Media/VoiceNoteView.swift +++ b/bitchat/Views/Media/VoiceNoteView.swift @@ -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") ) } } diff --git a/bitchat/Views/MeshPeerList.swift b/bitchat/Views/MeshPeerList.swift index b09ef476..de29474b 100644 --- a/bitchat/Views/MeshPeerList.swift +++ b/bitchat/Views/MeshPeerList.swift @@ -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 { diff --git a/bitchat/Views/MeshTopologyView.swift b/bitchat/Views/MeshTopologyView.swift index a8bc4e3e..148efde2 100644 --- a/bitchat/Views/MeshTopologyView.swift +++ b/bitchat/Views/MeshTopologyView.swift @@ -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) diff --git a/bitchat/Views/MessageListView.swift b/bitchat/Views/MessageListView.swift index 1506c1da..b8a8cde4 100644 --- a/bitchat/Views/MessageListView.swift +++ b/bitchat/Views/MessageListView.swift @@ -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 ) diff --git a/bitchat/Views/VerificationViews.swift b/bitchat/Views/VerificationViews.swift index 1fba4f2d..87288122 100644 --- a/bitchat/Views/VerificationViews.swift +++ b/bitchat/Views/VerificationViews.swift @@ -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)