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

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

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

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jack
2026-07-06 23:58:29 +02:00
co-authored by Claude Fable 5
parent b62f98cc62
commit 9cbfd131da
37 changed files with 258 additions and 258 deletions
+2 -2
View File
@@ -349,12 +349,12 @@ private extension AppRuntime {
TorManager.shared.isAutoStartAllowed() { TorManager.shared.isAutoStartAllowed() {
chatViewModel.torStatusAnnounced = true chatViewModel.torStatusAnnounced = true
chatViewModel.addGeohashOnlySystemMessage( 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 { } else if !TorManager.shared.torEnforced && !chatViewModel.torStatusAnnounced {
chatViewModel.torStatusAnnounced = true chatViewModel.torStatusAnnounced = true
chatViewModel.addGeohashOnlySystemMessage( chatViewModel.addGeohashOnlySystemMessage(
String(localized: "system.tor.dev_bypass", comment: "System message when Tor bypass is enabled in development") String(localized: "system.tor.dev_bypass", defaultValue: "development build: Tor bypass enabled.", comment: "System message when Tor bypass is enabled in development")
) )
} }
} }
+2 -2
View File
@@ -250,7 +250,7 @@ final class PrivateConversationModel: ObservableObject {
if let group = chatViewModel.groupStore.group(for: conversationPeerID) { if let group = chatViewModel.groupStore.group(for: conversationPeerID) {
displayName = "#\(group.name) (\(group.members.count))" displayName = "#\(group.name) (\(group.members.count))"
} else { } 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( return PrivateConversationHeaderState(
conversationPeerID: conversationPeerID, 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 { private func resolveAvailability(for headerPeerID: PeerID, peer: BitchatPeer?) -> PrivateConversationAvailability {
+1 -1
View File
@@ -186,6 +186,6 @@ final class VerificationModel: ObservableObject {
} }
} }
return String(localized: "common.unknown", comment: "Label for an unknown peer") return String(localized: "common.unknown", defaultValue: "unknown", comment: "Label for an unknown peer")
} }
} }
+17 -17
View File
@@ -36,11 +36,11 @@ enum CommandInfo: String, Identifiable {
var placeholder: String? { var placeholder: String? {
switch self { switch self {
case .block, .hug, .message, .slap, .unblock, .favorite, .unfavorite, .ping, .trace: 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: case .group:
return "<" + String(localized: "content.input.group_placeholder") + ">" return "<" + String(localized: "content.input.group_placeholder", defaultValue: "create|invite|leave|list") + ">"
case .pay: case .pay:
return "<" + String(localized: "content.input.token_placeholder") + ">" return "<" + String(localized: "content.input.token_placeholder", defaultValue: "token") + ">"
case .clear, .help, .who: case .clear, .help, .who:
return nil return nil
} }
@@ -48,20 +48,20 @@ enum CommandInfo: String, Identifiable {
var description: String { var description: String {
switch self { switch self {
case .block: String(localized: "content.commands.block") case .block: String(localized: "content.commands.block", defaultValue: "block or list blocked peers")
case .clear: String(localized: "content.commands.clear") case .clear: String(localized: "content.commands.clear", defaultValue: "clear chat messages")
case .group: String(localized: "content.commands.group") case .group: String(localized: "content.commands.group", defaultValue: "create or manage private groups")
case .help: String(localized: "content.commands.help") case .help: String(localized: "content.commands.help", defaultValue: "show available commands")
case .hug: String(localized: "content.commands.hug") case .hug: String(localized: "content.commands.hug", defaultValue: "send someone a warm hug")
case .message: String(localized: "content.commands.message") case .message: String(localized: "content.commands.message", defaultValue: "send private message")
case .pay: String(localized: "content.commands.pay") case .pay: String(localized: "content.commands.pay", defaultValue: "send a cashu ecash token in this chat")
case .slap: String(localized: "content.commands.slap") case .slap: String(localized: "content.commands.slap", defaultValue: "slap someone with a trout")
case .unblock: String(localized: "content.commands.unblock") case .unblock: String(localized: "content.commands.unblock", defaultValue: "unblock a peer")
case .who: String(localized: "content.commands.who") case .who: String(localized: "content.commands.who", defaultValue: "see who's online")
case .favorite: String(localized: "content.commands.favorite") case .favorite: String(localized: "content.commands.favorite", defaultValue: "add to favorites")
case .unfavorite: String(localized: "content.commands.unfavorite") case .unfavorite: String(localized: "content.commands.unfavorite", defaultValue: "remove from favorites")
case .ping: String(localized: "content.commands.ping") case .ping: String(localized: "content.commands.ping", defaultValue: "measure round-trip time to a mesh peer")
case .trace: String(localized: "content.commands.trace") case .trace: String(localized: "content.commands.trace", defaultValue: "estimate the mesh path to a peer")
} }
} }
+6 -6
View File
@@ -24,17 +24,17 @@ enum GeohashChannelLevel: CaseIterable, Codable, Equatable {
var displayName: String { var displayName: String {
switch self { switch self {
case .building: 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: 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: 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: 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: 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: case .region:
return String(localized: "location_levels.region", comment: "Name for region-level location channel") return String(localized: "location_levels.region", defaultValue: "region", comment: "Name for region-level location channel")
} }
} }
} }
+1 -1
View File
@@ -3719,7 +3719,7 @@ extension BLEService {
// Notify delegate of failure // Notify delegate of failure
notifyUI { [weak self] in notifyUI { [weak self] in
self?.deliverTransportEvent(.messageDeliveryStatusUpdated(messageID: message.messageID, status: .failed(reason: String(localized: "content.delivery.reason.encryption_failed", comment: "Failure reason shown when a message could not be encrypted for the peer")))) self?.deliverTransportEvent(.messageDeliveryStatusUpdated(messageID: message.messageID, status: .failed(reason: String(localized: "content.delivery.reason.encryption_failed", defaultValue: "encryption failed", comment: "Failure reason shown when a message could not be encrypted for the peer"))))
} }
} }
} }
+2 -2
View File
@@ -89,11 +89,11 @@ final class LocationNotesManager: ObservableObject {
private let maxNotesInMemory = 500 // Defensive cap (relay limit is 200) private let maxNotesInMemory = 500 // Defensive cap (relay limit is 200)
private enum Strings { 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 { static func failedToSend(_ detail: String) -> 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, locale: .current,
detail detail
) )
+10 -10
View File
@@ -116,30 +116,30 @@ enum EncryptionStatus: Equatable {
var description: String { var description: String {
switch self { switch self {
case .none: 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: 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: 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: 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: 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 { var accessibilityDescription: String {
switch self { switch self {
case .none: 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: 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: 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: 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: case .noiseVerified:
return String(localized: "encryption.accessibility.verified", comment: "Accessibility text when encryption is verified") return String(localized: "encryption.accessibility.verified", defaultValue: "encrypted and verified", comment: "Accessibility text when encryption is verified")
} }
} }
} }
@@ -12,17 +12,17 @@ enum ChatBluetoothAlertPolicy {
case .poweredOff: case .poweredOff:
ChatBluetoothAlertUpdate( ChatBluetoothAlertUpdate(
isPresented: true, 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: case .unauthorized:
ChatBluetoothAlertUpdate( ChatBluetoothAlertUpdate(
isPresented: true, 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: case .unsupported:
ChatBluetoothAlertUpdate( ChatBluetoothAlertUpdate(
isPresented: true, 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: case .poweredOn:
ChatBluetoothAlertUpdate(isPresented: false, message: "") ChatBluetoothAlertUpdate(isPresented: false, message: "")
+30 -30
View File
@@ -159,26 +159,26 @@ final class ChatGroupCoordinator {
func createGroup(named rawName: String) -> CommandResult { func createGroup(named rawName: String) -> CommandResult {
let name = rawName.trimmed let name = rawName.trimmed
guard !name.isEmpty else { guard !name.isEmpty else {
return .error(message: String(localized: "system.group.usage_create", comment: "Usage hint for /group create")) return .error(message: String(localized: "system.group.usage_create", defaultValue: "usage: /group create <name>", comment: "Usage hint for /group create"))
} }
guard name.count <= Self.maxGroupNameLength else { 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 myFingerprint = context.myNoiseFingerprint()
let mySigningKey = context.mySigningPublicKey() let mySigningKey = context.mySigningPublicKey()
guard !myFingerprint.isEmpty, mySigningKey.count == 32 else { 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) let creator = GroupMember(fingerprint: myFingerprint, signingKey: mySigningKey, nickname: context.nickname)
guard let group = context.groupStore.createGroup(named: name, creator: creator) else { 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) context.startPrivateChat(with: group.peerID)
return .success(message: String( 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, locale: .current,
name name
)) ))
@@ -187,45 +187,45 @@ final class ChatGroupCoordinator {
func inviteMember(nickname rawNickname: String) -> CommandResult { func inviteMember(nickname rawNickname: String) -> CommandResult {
let nickname = normalizedNickname(rawNickname) let nickname = normalizedNickname(rawNickname)
guard !nickname.isEmpty else { 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 { 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 { 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 { guard let peerID = context.getPeerIDForNickname(nickname) else {
return .error(message: String( 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, locale: .current,
nickname nickname
)) ))
} }
guard context.isPeerConnected(peerID) else { guard context.isPeerConnected(peerID) else {
return .error(message: String( 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, locale: .current,
nickname nickname
)) ))
} }
guard let identity = context.cryptoIdentity(for: peerID) else { guard let identity = context.cryptoIdentity(for: peerID) else {
return .error(message: String( 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, locale: .current,
nickname nickname
)) ))
} }
guard !group.isMember(fingerprint: identity.fingerprint) else { guard !group.isMember(fingerprint: identity.fingerprint) else {
return .error(message: String( 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, locale: .current,
nickname nickname
)) ))
} }
guard group.members.count < BitchatGroup.maxMembers else { guard group.members.count < BitchatGroup.maxMembers else {
return .error(message: String( 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, locale: .current,
"\(BitchatGroup.maxMembers)" "\(BitchatGroup.maxMembers)"
)) ))
@@ -243,14 +243,14 @@ final class ChatGroupCoordinator {
let members = group.members + [newMember] let members = group.members + [newMember]
guard let (updated, key) = context.groupStore.rotateKey(groupID: group.groupID, members: members), guard let (updated, key) = context.groupStore.rotateKey(groupID: group.groupID, members: members),
let payload = signedStatePayload(for: updated, key: key) else { 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) context.sendGroupInvitePayload(payload, to: peerID)
distributeState(payload, group: updated, excluding: [identity.fingerprint], type: .keyUpdate) distributeState(payload, group: updated, excluding: [identity.fingerprint], type: .keyUpdate)
return .success(message: String( 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, locale: .current,
nickname, nickname,
updated.name updated.name
@@ -263,36 +263,36 @@ final class ChatGroupCoordinator {
func removeMember(nickname rawNickname: String) -> CommandResult { func removeMember(nickname rawNickname: String) -> CommandResult {
let nickname = normalizedNickname(rawNickname) let nickname = normalizedNickname(rawNickname)
guard !nickname.isEmpty else { 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 { 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 { 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 { guard let member = group.members.first(where: { $0.nickname.caseInsensitiveCompare(nickname) == .orderedSame }) else {
return .error(message: String( 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, locale: .current,
nickname nickname
)) ))
} }
guard member.fingerprint != group.creatorFingerprint else { 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 } let remaining = group.members.filter { $0.fingerprint != member.fingerprint }
guard let (rotated, newKey) = context.groupStore.rotateKey(groupID: group.groupID, members: remaining), guard let (rotated, newKey) = context.groupStore.rotateKey(groupID: group.groupID, members: remaining),
let payload = signedStatePayload(for: rotated, key: newKey) else { 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) distributeState(payload, group: rotated, excluding: [], type: .keyUpdate)
notifyRemovedMember(member, rotated: rotated) notifyRemovedMember(member, rotated: rotated)
return .success(message: String( 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, locale: .current,
member.nickname member.nickname
)) ))
@@ -300,7 +300,7 @@ final class ChatGroupCoordinator {
func leaveGroup() -> CommandResult { func leaveGroup() -> CommandResult {
guard let group = selectedGroup() else { 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 // Close the chat window first so the confirmation message doesn't
// resurrect the conversation we're about to remove. // resurrect the conversation we're about to remove.
@@ -309,7 +309,7 @@ final class ChatGroupCoordinator {
context.groupStore.removeGroup(withID: group.groupID) context.groupStore.removeGroup(withID: group.groupID)
context.notifyUIChanged() context.notifyUIChanged()
return .success(message: String( 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, locale: .current,
group.name group.name
)) ))
@@ -318,14 +318,14 @@ final class ChatGroupCoordinator {
func listGroups() -> CommandResult { func listGroups() -> CommandResult {
let groups = context.groupStore.groups let groups = context.groupStore.groups
guard !groups.isEmpty else { guard !groups.isEmpty else {
return .success(message: String(localized: "system.group.none", comment: "System message when the user is in no groups")) return .success(message: String(localized: "system.group.none", defaultValue: "you are not in any groups — /group create <name> to start one", comment: "System message when the user is in no groups"))
} }
let myFingerprint = context.myNoiseFingerprint() let myFingerprint = context.myNoiseFingerprint()
let lines = groups.map { group -> String in let lines = groups.map { group -> String in
let role = group.creatorFingerprint == myFingerprint ? " (creator)" : "" let role = group.creatorFingerprint == myFingerprint ? " (creator)" : ""
return "#\(group.name)\(role)\(group.members.count)/\(BitchatGroup.maxMembers)" 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 // MARK: - Sending
@@ -336,7 +336,7 @@ final class ChatGroupCoordinator {
guard !content.isEmpty, content.count <= InputValidator.Limits.maxMessageLength else { return } guard !content.isEmpty, content.count <= InputValidator.Limits.maxMessageLength else { return }
guard let group = context.groupStore.group(for: groupPeerID), guard let group = context.groupStore.group(for: groupPeerID),
let key = context.groupStore.key(forGroupID: group.groupID) else { 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 return
} }
@@ -358,7 +358,7 @@ final class ChatGroupCoordinator {
} catch { } catch {
SecureLogger.error("Failed to seal group message: \(error)", category: .encryption) SecureLogger.error("Failed to seal group message: \(error)", category: .encryption)
context.addLocalPrivateSystemMessage( 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 to: groupPeerID
) )
return return
@@ -559,7 +559,7 @@ private extension ChatGroupCoordinator {
context.removePrivateChat(existing.peerID) context.removePrivateChat(existing.peerID)
context.groupStore.removeGroup(withID: existing.groupID) context.groupStore.removeGroup(withID: existing.groupID)
context.addSystemMessage(String( 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, locale: .current,
existing.name existing.name
)) ))
@@ -586,7 +586,7 @@ private extension ChatGroupCoordinator {
?? context.peerNickname(for: peerID) ?? context.peerNickname(for: peerID)
?? "?" ?? "?"
let notice = String( 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, locale: .current,
state.name, state.name,
inviter inviter
@@ -350,7 +350,7 @@ private extension ChatLifecycleCoordinator {
} catch { } catch {
SecureLogger.error("❌ Failed to send geohash screenshot message: \(error)", category: .session) SecureLogger.error("❌ Failed to send geohash screenshot message: \(error)", category: .session)
context.addSystemMessage( context.addSystemMessage(
String(localized: "system.location.send_failed", comment: "System message when a location channel send fails") String(localized: "system.location.send_failed", defaultValue: "failed to send to location channel", comment: "System message when a location channel send fails")
) )
} }
} }
@@ -117,13 +117,13 @@ final class ChatMediaTransferCoordinator {
try? FileManager.default.removeItem(at: url) try? FileManager.default.removeItem(at: url)
await MainActor.run { [weak self] in await MainActor.run { [weak self] in
guard let self else { return } 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 { } catch {
SecureLogger.error("Voice note send failed: \(error)", category: .session) SecureLogger.error("Voice note send failed: \(error)", category: .session)
await MainActor.run { [weak self] in await MainActor.run { [weak self] in
guard let self else { return } guard let self else { return }
self.handleMediaSendFailure(messageID: messageID, reason: String(localized: "content.delivery.reason.voice_send_failed", comment: "Failure reason shown when a voice note could not be sent")) self.handleMediaSendFailure(messageID: messageID, reason: String(localized: "content.delivery.reason.voice_send_failed", defaultValue: "voice note failed to send", comment: "Failure reason shown when a voice note could not be sent"))
} }
} }
} }
@@ -147,7 +147,7 @@ private extension ChatOutgoingCoordinator {
} catch { } catch {
SecureLogger.error("❌ Failed to prepare geohash message: \(error)", category: .session) SecureLogger.error("❌ Failed to prepare geohash message: \(error)", category: .session)
context.addSystemMessage( 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 return
} }
@@ -181,7 +181,7 @@ private extension ChatOutgoingCoordinator {
} catch { } catch {
SecureLogger.error("❌ Failed to prepare geohash message: \(error)", category: .session) SecureLogger.error("❌ Failed to prepare geohash message: \(error)", category: .session)
context?.addSystemMessage( 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 return
} }
@@ -238,7 +238,7 @@ final class ChatPrivateConversationCoordinator {
let nickname = context.peerNickname(for: peerID) ?? "user" let nickname = context.peerNickname(for: peerID) ?? "user"
context.addSystemMessage( context.addSystemMessage(
String( 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, locale: .current,
nickname nickname
) )
@@ -298,7 +298,7 @@ final class ChatPrivateConversationCoordinator {
} else { } else {
context.setPrivateDeliveryStatus( context.setPrivateDeliveryStatus(
.failed( .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, forMessageID: messageID,
peerID: peerID peerID: peerID
@@ -306,7 +306,7 @@ final class ChatPrivateConversationCoordinator {
let name = recipientNickname ?? "user" let name = recipientNickname ?? "user"
context.addSystemMessage( context.addSystemMessage(
String( 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, locale: .current,
name name
) )
@@ -317,7 +317,7 @@ final class ChatPrivateConversationCoordinator {
func sendGeohashDM(_ content: String, to peerID: PeerID) { func sendGeohashDM(_ content: String, to peerID: PeerID) {
guard case .location(let channel) = context.activeChannel else { guard case .location(let channel) = context.activeChannel else {
context.addSystemMessage( 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 return
} }
@@ -341,7 +341,7 @@ final class ChatPrivateConversationCoordinator {
guard let recipientHex = context.nostrKeyMapping[peerID] else { guard let recipientHex = context.nostrKeyMapping[peerID] else {
context.setPrivateDeliveryStatus( context.setPrivateDeliveryStatus(
.failed( .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, forMessageID: messageID,
peerID: peerID peerID: peerID
@@ -352,13 +352,13 @@ final class ChatPrivateConversationCoordinator {
if context.isNostrBlocked(pubkeyHexLowercased: recipientHex) { if context.isNostrBlocked(pubkeyHexLowercased: recipientHex) {
context.setPrivateDeliveryStatus( context.setPrivateDeliveryStatus(
.failed( .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, forMessageID: messageID,
peerID: peerID peerID: peerID
) )
context.addSystemMessage( 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 return
} }
@@ -368,7 +368,7 @@ final class ChatPrivateConversationCoordinator {
if recipientHex.lowercased() == identity.publicKeyHex.lowercased() { if recipientHex.lowercased() == identity.publicKeyHex.lowercased() {
context.setPrivateDeliveryStatus( context.setPrivateDeliveryStatus(
.failed( .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, forMessageID: messageID,
peerID: peerID peerID: peerID
@@ -390,7 +390,7 @@ final class ChatPrivateConversationCoordinator {
} catch { } catch {
context.setPrivateDeliveryStatus( context.setPrivateDeliveryStatus(
.failed( .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, forMessageID: messageID,
peerID: peerID peerID: peerID
@@ -107,7 +107,7 @@ private extension ChatViewModelBootstrapper {
category: .session category: .session
) )
viewModel.conversations.setDeliveryStatus( 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 forMessageID: messageID
) )
} }
@@ -19,7 +19,7 @@ extension ChatViewModel {
self.torStatusAnnounced = true self.torStatusAnnounced = true
// Post only in geohash channels (queue if not active) // Post only in geohash channels (queue if not active)
self.addGeohashOnlySystemMessage( 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 self.torRestartPending = true
// Post only in geohash channels (queue if not active) // Post only in geohash channels (queue if not active)
self.addGeohashOnlySystemMessage( 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 { if self.torRestartPending {
// Post only in geohash channels (queue if not active) // Post only in geohash channels (queue if not active)
self.addGeohashOnlySystemMessage( 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 self.torRestartPending = false
} else if TorManager.shared.torEnforced && !self.torInitialReadyAnnounced { } else if TorManager.shared.torEnforced && !self.torInitialReadyAnnounced {
// Initial start completed // Initial start completed
self.addGeohashOnlySystemMessage( 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 self.torInitialReadyAnnounced = true
} }
+2 -2
View File
@@ -144,7 +144,7 @@ struct AppInfoView: View {
// Custom header for macOS // Custom header for macOS
HStack { HStack {
Spacer() Spacer()
Button("app_info.done") { Button(String(localized: "app_info.done", defaultValue: "DONE")) {
dismiss() dismiss()
} }
.buttonStyle(.plain) .buttonStyle(.plain)
@@ -256,7 +256,7 @@ struct AppInfoView: View {
.contentShape(Rectangle()) .contentShape(Rectangle())
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.accessibilityHint(Text("app_info.network.topology.hint")) .accessibilityHint(Text(String(localized: "app_info.network.topology.hint", defaultValue: "opens the mesh topology map")))
} }
} }
@@ -16,32 +16,32 @@ extension DeliveryStatus {
var bitchatDescription: String { var bitchatDescription: String {
switch self { switch self {
case .sending: 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: 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: 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") 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, _): case .delivered(let nickname, _):
return String( 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, locale: .current,
nickname nickname
) )
case .read(let nickname, _): case .read(let nickname, _):
return String( 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, locale: .current,
nickname nickname
) )
case .failed(let reason): case .failed(let reason):
return String( 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, locale: .current,
reason reason
) )
case .partiallyDelivered(let reached, let total): case .partiallyDelivered(let reached, let total):
return String( 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, locale: .current,
reached, reached,
total total
@@ -70,9 +70,9 @@ struct PaymentChipView: View {
var label: String { var label: String {
switch self { switch self {
case .cashu: 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: 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 { Button {
copyToPasteboard(token) copyToPasteboard(token)
} label: { } 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 { Button {
redeemCashu() redeemCashu()
} label: { } 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 { if let webURL = paymentType.cashuWebRedeemURL {
Button { Button {
openExternalURL(webURL) openExternalURL(webURL)
} label: { } label: {
Label(String(localized: "content.payment.redeem_web", comment: "Context menu action opening a Cashu token in the web redemption page"), systemImage: "safari") Label(String(localized: "content.payment.redeem_web", defaultValue: "redeem on web", comment: "Context menu action opening a Cashu token in the web redemption page"), systemImage: "safari")
} }
} }
} }
@@ -24,6 +24,6 @@ struct SheetCloseButton: View {
.contentShape(Rectangle().inset(by: -6)) .contentShape(Rectangle().inset(by: -6))
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.accessibilityLabel(String(localized: "common.close", comment: "Accessibility label for close buttons")) .accessibilityLabel(String(localized: "common.close", defaultValue: "close", comment: "Accessibility label for close buttons"))
} }
} }
@@ -69,7 +69,7 @@ struct TextMessageView: View {
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.accessibilityHint( .accessibilityHint(
String(localized: "content.accessibility.delivery_detail_hint", comment: "Accessibility hint for the delivery status glyph explaining a tap reveals details") String(localized: "content.accessibility.delivery_detail_hint", defaultValue: "tap to show delivery details", comment: "Accessibility hint for the delivery status glyph explaining a tap reveals details")
) )
} }
} }
+13 -13
View File
@@ -118,17 +118,17 @@ private extension ContentComposerView {
let isGeoDM = privateConversationModel.selectedPeerID?.isGeoDM == true let isGeoDM = privateConversationModel.selectedPeerID?.isGeoDM == true
let target = isGeoDM ? header.displayName : "@\(header.displayName)" let target = isGeoDM ? header.displayName : "@\(header.displayName)"
return String( 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, locale: .current,
target target
) )
} }
switch locationChannelsModel.selectedChannel { switch locationChannelsModel.selectedChannel {
case .mesh: 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): case .location(let channel):
return String( 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, locale: .current,
channel.geohash channel.geohash
) )
@@ -184,15 +184,15 @@ private extension ContentComposerView {
showImagePicker = true showImagePicker = true
} }
.accessibilityLabel( .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( .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) .accessibilityAddTraits(.isButton)
// The long-press camera path is unreachable for VoiceOver users; // The long-press camera path is unreachable for VoiceOver users;
// mirror it as a named action. // 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 imagePickerSourceType = .camera
showImagePicker = true showImagePicker = true
} }
@@ -205,7 +205,7 @@ private extension ContentComposerView {
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.accessibilityLabel( .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 #endif
} }
@@ -249,15 +249,15 @@ private extension ContentComposerView {
) )
) )
.accessibilityLabel( .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( .accessibilityValue(
voiceRecordingVM.state.isActive 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( .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) .accessibilityAddTraits(.isButton)
// Press-and-hold drag gestures can't be activated by VoiceOver; // Press-and-hold drag gestures can't be activated by VoiceOver;
@@ -282,12 +282,12 @@ private extension ContentComposerView {
.buttonStyle(.plain) .buttonStyle(.plain)
.disabled(!enabled) .disabled(!enabled)
.accessibilityLabel( .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( .accessibilityHint(
enabled enabled
? String(localized: "content.accessibility.send_hint_ready", comment: "Hint prompting the user to send the 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", comment: "Hint prompting the user to enter a message") : String(localized: "content.accessibility.send_hint_empty", defaultValue: "enter a message to send", comment: "Hint prompting the user to enter a message")
) )
} }
} }
+15 -15
View File
@@ -44,7 +44,7 @@ struct ContentHeaderView: View {
// stays undiscoverable on purpose it's destructive.) // stays undiscoverable on purpose it's destructive.)
.accessibilityAddTraits(.isButton) .accessibilityAddTraits(.isButton)
.accessibilityHint( .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 { .accessibilityAction {
appChromeModel.presentAppInfo() appChromeModel.presentAppInfo()
@@ -56,7 +56,7 @@ struct ContentHeaderView: View {
.foregroundColor(palette.secondary) .foregroundColor(palette.secondary)
TextField( TextField(
"content.input.nickname_placeholder", String(localized: "content.input.nickname_placeholder", defaultValue: "nickname"),
text: Binding( text: Binding(
get: { appChromeModel.nickname }, get: { appChromeModel.nickname },
set: { appChromeModel.setNickname($0) } set: { appChromeModel.setNickname($0) }
@@ -133,7 +133,7 @@ struct ContentHeaderView: View {
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.accessibilityLabel( .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) .buttonStyle(.plain)
.accessibilityLabel( .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) .buttonStyle(.plain)
.accessibilityLabel( .accessibilityLabel(
String( 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, locale: .current,
channel.geohash channel.geohash
) )
@@ -211,7 +211,7 @@ struct ContentHeaderView: View {
.frame(maxHeight: .infinity) .frame(maxHeight: .infinity)
.contentShape(Rectangle()) .contentShape(Rectangle())
.accessibilityLabel( .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) .buttonStyle(.plain)
@@ -238,7 +238,7 @@ struct ContentHeaderView: View {
.buttonStyle(.plain) .buttonStyle(.plain)
.accessibilityLabel( .accessibilityLabel(
String( 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, locale: .current,
headerOtherPeersCount headerOtherPeersCount
) )
@@ -247,8 +247,8 @@ struct ContentHeaderView: View {
// color; say it. // color; say it.
.accessibilityValue( .accessibilityValue(
headerPeersReachable headerPeersReachable
? String(localized: "content.accessibility.peers_connected", comment: "Accessibility value when peers are reachable") ? String(localized: "content.accessibility.peers_connected", defaultValue: "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_none", defaultValue: "no one reachable", comment: "Accessibility value when no peers are reachable")
) )
} }
.layoutPriority(3) .layoutPriority(3)
@@ -325,10 +325,10 @@ struct ContentHeaderView: View {
.onChange(of: locationChannelsModel.permissionState) { _ in .onChange(of: locationChannelsModel.permissionState) { _ in
locationChannelsModel.refreshMeshChannelsIfNeeded() locationChannelsModel.refreshMeshChannelsIfNeeded()
} }
.alert("content.alert.screenshot.title", isPresented: $appChromeModel.showScreenshotPrivacyWarning) { .alert(String(localized: "content.alert.screenshot.title", defaultValue: "heads up"), isPresented: $appChromeModel.showScreenshotPrivacyWarning) {
Button("common.ok", role: .cancel) {} Button(String(localized: "common.ok", defaultValue: "OK"), role: .cancel) {}
} message: { } 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) .themedChromePanel(edge: .top)
} }
@@ -392,7 +392,7 @@ private struct ContentLocationNotesUnavailableView: View {
var body: some View { var body: some View {
VStack(spacing: 12) { VStack(spacing: 12) {
HStack { HStack {
Text("content.notes.title") Text(String(localized: "content.notes.title", defaultValue: "notes"))
.bitchatFont(size: 16, weight: .bold) .bitchatFont(size: 16, weight: .bold)
Spacer() Spacer()
SheetCloseButton { showLocationNotes = false } SheetCloseButton { showLocationNotes = false }
@@ -401,10 +401,10 @@ private struct ContentLocationNotesUnavailableView: View {
.frame(minHeight: headerHeight) .frame(minHeight: headerHeight)
.padding(.horizontal, 12) .padding(.horizontal, 12)
.themedChromePanel(edge: .top) .themedChromePanel(edge: .top)
Text("content.notes.location_unavailable") Text(String(localized: "content.notes.location_unavailable", defaultValue: "location unavailable"))
.bitchatFont(size: 14) .bitchatFont(size: 14)
.foregroundColor(palette.secondary) .foregroundColor(palette.secondary)
Button("content.location.enable") { Button(String(localized: "content.location.enable", defaultValue: "enable location")) {
locationChannelsModel.enableAndRefresh() locationChannelsModel.enableAndRefresh()
} }
.buttonStyle(.bordered) .buttonStyle(.bordered)
+17 -17
View File
@@ -163,10 +163,10 @@ private struct ContentPeopleListView: View {
// .help maps to the accessibility *hint* on iOS, so the // .help maps to the accessibility *hint* on iOS, so the
// button still needs a spoken name. // button still needs a spoken name.
.accessibilityLabel( .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( .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 { SheetCloseButton {
@@ -262,7 +262,7 @@ private struct ContentPeopleListView: View {
private extension ContentPeopleListView { private extension ContentPeopleListView {
var peopleSheetTitle: String { 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? { var peopleSheetSubtitle: String? {
@@ -329,7 +329,7 @@ private struct ContentPrivateChatSheetView: View {
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.accessibilityLabel( .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) Spacer(minLength: 0)
@@ -354,8 +354,8 @@ private struct ContentPrivateChatSheetView: View {
.buttonStyle(.plain) .buttonStyle(.plain)
.accessibilityLabel( .accessibilityLabel(
headerState.isFavorite headerState.isFavorite
? String(localized: "content.accessibility.remove_favorite", comment: "Accessibility label to remove 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", comment: "Accessibility label to add 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 { private var privacyCaptionText: String {
// Group chats are ChaCha20-Poly1305 sealed to the roster's shared key. // Group chats are ChaCha20-Poly1305 sealed to the roster's shared key.
if privateConversationModel.selectedPeerID?.isGroup == true { 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, // Geohash DMs are NIP-17 gift-wrapped always end-to-end encrypted,
// even though they carry no Noise session status. Mesh DMs earn the // even though they carry no Noise session status. Mesh DMs earn the
@@ -477,9 +477,9 @@ private struct ContentPrivateChatSheetView: View {
} }
}() }()
if isGeoDM || noiseSecured { 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") Image(systemName: "person.3.fill")
.font(.bitchatSystem(size: 14)) .font(.bitchatSystem(size: 14))
.foregroundColor(palette.primary) .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 { } else {
switch headerState.availability { switch headerState.availability {
case .bluetoothConnected: case .bluetoothConnected:
Image(systemName: "dot.radiowaves.left.and.right") Image(systemName: "dot.radiowaves.left.and.right")
.font(.bitchatSystem(size: 14)) .font(.bitchatSystem(size: 14))
.foregroundColor(palette.primary) .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: case .meshReachable:
Image(systemName: "point.3.filled.connected.trianglepath.dotted") Image(systemName: "point.3.filled.connected.trianglepath.dotted")
.font(.bitchatSystem(size: 14)) .font(.bitchatSystem(size: 14))
.foregroundColor(palette.primary) .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: case .nostrAvailable:
Image(systemName: "globe") Image(systemName: "globe")
.font(.bitchatSystem(size: 14)) .font(.bitchatSystem(size: 14))
.foregroundColor(.purple) .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: case .offline:
// Absence of a glyph was the only offline signal; say it. // 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) .bitchatFont(size: 11)
.foregroundColor(palette.secondary) .foregroundColor(palette.secondary)
} }
@@ -574,7 +574,7 @@ private struct ContentPrivateHeaderInfoButton: View {
) )
.accessibilityLabel( .accessibilityLabel(
String( 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, locale: .current,
encryptionStatus.accessibilityDescription encryptionStatus.accessibilityDescription
) )
@@ -585,7 +585,7 @@ private struct ContentPrivateHeaderInfoButton: View {
.buttonStyle(.plain) .buttonStyle(.plain)
.accessibilityLabel( .accessibilityLabel(
String( 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, locale: .current,
headerState.displayName headerState.displayName
) )
@@ -593,7 +593,7 @@ private struct ContentPrivateHeaderInfoButton: View {
.accessibilityHint( .accessibilityHint(
headerState.isGroupConversation 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) .frame(minHeight: headerHeight)
} }
+5 -5
View File
@@ -204,20 +204,20 @@ struct ContentView: View {
} }
} }
.alert("Recording Error", isPresented: $voiceRecordingVM.showAlert, actions: { .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 { if voiceRecordingVM.state == .permissionDenied {
Button("location_channels.action.open_settings") { Button(String(localized: "location_channels.action.open_settings", defaultValue: "open settings")) {
SystemSettings.microphone.open() SystemSettings.microphone.open()
} }
} }
}, message: { }, message: {
Text(voiceRecordingVM.state.alertMessage) Text(voiceRecordingVM.state.alertMessage)
}) })
.alert("content.alert.bluetooth_required.title", isPresented: $appChromeModel.showBluetoothAlert) { .alert(String(localized: "content.alert.bluetooth_required.title", defaultValue: "bluetooth required"), isPresented: $appChromeModel.showBluetoothAlert) {
Button("content.alert.bluetooth_required.settings") { Button(String(localized: "content.alert.bluetooth_required.settings", defaultValue: "settings")) {
SystemSettings.bluetooth.open() SystemSettings.bluetooth.open()
} }
Button("common.ok", role: .cancel) {} Button(String(localized: "common.ok", defaultValue: "OK"), role: .cancel) {}
} message: { } message: {
Text(appChromeModel.bluetoothAlertMessage) Text(appChromeModel.bluetoothAlertMessage)
} }
+3 -3
View File
@@ -30,7 +30,7 @@ struct FingerprintView: View {
static let verifiedMessage: LocalizedStringKey = "fingerprint.message.verified" static let verifiedMessage: LocalizedStringKey = "fingerprint.message.verified"
static func verifyHint(_ nickname: String) -> String { static func verifyHint(_ nickname: String) -> 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, locale: .current,
nickname nickname
) )
@@ -40,13 +40,13 @@ struct FingerprintView: View {
static let vouchedBadge: LocalizedStringKey = "fingerprint.badge.vouched" static let vouchedBadge: LocalizedStringKey = "fingerprint.badge.vouched"
static func vouchedBy(_ count: Int) -> String { static func vouchedBy(_ count: Int) -> String {
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, locale: .current,
count count
) )
} }
static func unknownPeer() -> String { static func unknownPeer() -> String {
String(localized: "common.unknown", comment: "Label for an unknown peer") String(localized: "common.unknown", defaultValue: "unknown", comment: "Label for an unknown peer")
} }
} }
+8 -8
View File
@@ -10,16 +10,16 @@ struct GeohashPeopleList: View {
private enum Strings { private enum Strings {
static let noneNearby: LocalizedStringKey = "geohash_people.none_nearby" static let noneNearby: LocalizedStringKey = "geohash_people.none_nearby"
static let youSuffix: LocalizedStringKey = "geohash_people.you_suffix" 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 unblock: LocalizedStringKey = "geohash_people.action.unblock"
static let block: LocalizedStringKey = "geohash_people.action.block" 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 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", comment: "Context menu action to block 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", comment: "State label for someone who joined the location channel from elsewhere") 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", comment: "State label for someone physically in the location channel's area") 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", comment: "State label for a blocked peer") 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", comment: "State label marking your own row in the people list") 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", comment: "Accessibility hint on a peer row explaining activation opens a private chat") 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 { var body: some View {
+6 -6
View File
@@ -10,12 +10,12 @@ struct GroupChatList: View {
let onTapGroup: (PeerID) -> Void let onTapGroup: (PeerID) -> Void
private enum Strings { private enum Strings {
static let header = String(localized: "groups.section.header", comment: "Section header above the private groups list") 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", comment: "State label for a group the user created") 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", comment: "State label for a peer with unread private messages") 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", comment: "Tooltip for the unread messages indicator") 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", comment: "Accessibility hint on a group row explaining activation opens the group chat") 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 %@", comment: "Member count shown next to a group name; placeholder is the count") 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 { var body: some View {
+8 -8
View File
@@ -32,13 +32,13 @@ struct LocationChannelsSheet: View {
static let toggleOn: LocalizedStringKey = "common.toggle.on" static let toggleOn: LocalizedStringKey = "common.toggle.on"
static let toggleOff: LocalizedStringKey = "common.toggle.off" 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 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", comment: "Accessibility hint on a channel row explaining activation switches to it") 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", comment: "Accessibility action name for bookmarking a channel") 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", comment: "Accessibility action name for removing a channel bookmark") 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 { 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) return rowTitle(label: label, count: count)
} }
@@ -73,7 +73,7 @@ struct LocationChannelsSheet: View {
static func subtitlePrefix(geohash: String, coverage: String) -> String { static func subtitlePrefix(geohash: String, coverage: String) -> 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, locale: .current,
geohash, coverage geohash, coverage
) )
@@ -82,7 +82,7 @@ struct LocationChannelsSheet: View {
static func subtitle(prefix: String, name: String?) -> String { static func subtitle(prefix: String, name: String?) -> String {
guard let name, !name.isEmpty else { return prefix } guard let name, !name.isEmpty else { return prefix }
return String( 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, locale: .current,
prefix, name prefix, name
) )
@@ -90,7 +90,7 @@ struct LocationChannelsSheet: View {
private static func rowTitle(label: String, count: Int) -> String { private static func rowTitle(label: String, count: Int) -> String {
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, locale: .current,
label, count label, count
) )
+1 -1
View File
@@ -135,7 +135,7 @@ struct LocationNotesView: View {
private func headerTitle(for count: Int) -> String { private func headerTitle(for count: Int) -> String {
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, locale: .current,
"\(geohash) ± 1", count "\(geohash) ± 1", count
) )
+15 -15
View File
@@ -25,20 +25,20 @@ struct BlockRevealImageView: View {
@State private var loadFailed = false @State private var loadFailed = false
private enum Strings { 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 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", comment: "Context menu action that opens an image full screen") 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", comment: "Context menu action that reveals a blurred image") 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", comment: "Context menu action that re-blurs a revealed 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", comment: "Context menu action that deletes a received 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", comment: "Title of the confirmation dialog before deleting 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", comment: "Body 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", comment: "Accessibility label for a blurred incoming 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", comment: "Accessibility label for a revealed 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", comment: "Accessibility hint for a blurred image; activating it reveals the 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", comment: "Accessibility hint for a revealed image; activating it opens the image full screen") 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", comment: "Accessibility label for an image that is still sending") 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", comment: "Accessibility label for an image whose file could not be loaded") 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", comment: "Accessibility label for the cancel button on an in-flight media send") 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( init(
@@ -154,7 +154,7 @@ struct BlockRevealImageView: View {
Button(Strings.delete, role: .destructive) { Button(Strings.delete, role: .destructive) {
onDelete?() onDelete?()
} }
Button("common.cancel", role: .cancel) {} Button(String(localized: "common.cancel", defaultValue: "cancel"), role: .cancel) {}
} message: { } message: {
Text(verbatim: Strings.deleteConfirmMessage) Text(verbatim: Strings.deleteConfirmMessage)
} }
+1 -1
View File
@@ -67,7 +67,7 @@ struct MediaMessageView: View {
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.accessibilityHint( .accessibilityHint(
String(localized: "content.accessibility.delivery_detail_hint", comment: "Accessibility hint for the delivery status glyph explaining a tap reveals details") String(localized: "content.accessibility.delivery_detail_hint", defaultValue: "tap to show delivery details", comment: "Accessibility hint for the delivery status glyph explaining a tap reveals details")
) )
} }
} }
+3 -3
View File
@@ -54,8 +54,8 @@ struct VoiceNoteView: View {
.buttonStyle(.plain) .buttonStyle(.plain)
.accessibilityLabel( .accessibilityLabel(
playback.isPlaying playback.isPlaying
? String(localized: "media.voice.accessibility.pause", comment: "Accessibility label for pausing voice note playback") ? String(localized: "media.voice.accessibility.pause", defaultValue: "pause voice note", 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.play", defaultValue: "play voice note", comment: "Accessibility label for playing a voice note")
) )
.accessibilityValue(playbackLabel) .accessibilityValue(playbackLabel)
@@ -83,7 +83,7 @@ struct VoiceNoteView: View {
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.accessibilityLabel( .accessibilityLabel(
String(localized: "media.accessibility.cancel_send", comment: "Accessibility label for the cancel button on an in-flight media send") String(localized: "media.accessibility.cancel_send", defaultValue: "cancel sending", comment: "Accessibility label for the cancel button on an in-flight media send")
) )
} }
} }
+18 -18
View File
@@ -16,24 +16,24 @@ struct MeshPeerList: View {
private enum Strings { private enum Strings {
static let noneNearby: LocalizedStringKey = "geohash_people.none_nearby" 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 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", comment: "Tooltip for the unread messages 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", comment: "Accessibility label for mesh-connected peer 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", comment: "Accessibility label for mesh-reachable 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", comment: "Accessibility label for Nostr-available 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", comment: "State label for a peer that is not currently reachable") 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", comment: "State label for a favorited peer") 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", comment: "State label for a peer with unread private messages") 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", comment: "State label for a blocked peer") 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", comment: "State label for a peer vouched for by someone the user verified") 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", comment: "Tooltip for the vouched (unfilled seal) badge next to a peer") 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", comment: "Accessibility label to add a favorite") 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", comment: "Accessibility label to remove 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", comment: "Context menu action that shows a peer's fingerprint/verification screen") 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", comment: "Accessibility hint on a peer row explaining activation opens a private chat") 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", comment: "Action that opens a private chat with the person") 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", comment: "Context menu action to block a 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", comment: "Context menu action to unblock 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 { var body: some View {
+6 -6
View File
@@ -40,12 +40,12 @@ struct MeshTopologyView: View {
#if os(macOS) #if os(macOS)
VStack(spacing: 0) { VStack(spacing: 0) {
HStack { HStack {
Text("topology.title") Text(String(localized: "topology.title", defaultValue: "mesh topology"))
.bitchatFont(size: 16, weight: .bold) .bitchatFont(size: 16, weight: .bold)
.foregroundColor(palette.primary) .foregroundColor(palette.primary)
Spacer() Spacer()
refreshButton refreshButton
Button("app_info.done") { Button(String(localized: "app_info.done", defaultValue: "DONE")) {
dismiss() dismiss()
} }
.buttonStyle(.plain) .buttonStyle(.plain)
@@ -62,7 +62,7 @@ struct MeshTopologyView: View {
NavigationView { NavigationView {
content content
.themedSheetBackground() .themedSheetBackground()
.navigationTitle(Text("topology.title")) .navigationTitle(Text(String(localized: "topology.title", defaultValue: "mesh topology")))
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {
ToolbarItem(placement: .navigationBarLeading) { ToolbarItem(placement: .navigationBarLeading) {
@@ -86,7 +86,7 @@ struct MeshTopologyView: View {
.foregroundColor(palette.primary) .foregroundColor(palette.primary)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.accessibilityLabel(Text("topology.refresh")) .accessibilityLabel(Text(String(localized: "topology.refresh", defaultValue: "refresh topology")))
} }
@ViewBuilder @ViewBuilder
@@ -94,7 +94,7 @@ struct MeshTopologyView: View {
VStack(spacing: 12) { VStack(spacing: 12) {
if model.nodes.count <= 1 { if model.nodes.count <= 1 {
Spacer() 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) .bitchatFont(size: 14)
.foregroundColor(palette.secondary) .foregroundColor(palette.secondary)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
@@ -109,7 +109,7 @@ struct MeshTopologyView: View {
Text(summaryText) Text(summaryText)
.bitchatFont(size: 13, weight: .semibold) .bitchatFont(size: 13, weight: .semibold)
.foregroundColor(palette.primary) .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) .bitchatFont(size: 11)
.foregroundColor(palette.secondary) .foregroundColor(palette.secondary)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
+25 -25
View File
@@ -109,11 +109,11 @@ struct MessageListView: View {
// mentioning the only other participant is noise, and "DM" // mentioning the only other participant is noise, and "DM"
// would just reopen the conversation that is already open. // would just reopen the conversation that is already open.
if privatePeer == nil { if privatePeer == nil {
Button("content.actions.mention") { Button(String(localized: "content.actions.mention", defaultValue: "mention")) {
insertMention(message.sender) insertMention(message.sender)
} }
if let peerID = message.senderPeerID { if let peerID = message.senderPeerID {
Button("content.actions.direct_message") { Button(String(localized: "content.actions.direct_message", defaultValue: "direct message")) {
privateConversationModel.openConversation(for: peerID) privateConversationModel.openConversation(for: peerID)
withAnimation(.easeInOut(duration: TransportConfig.uiAnimationMediumSeconds)) { withAnimation(.easeInOut(duration: TransportConfig.uiAnimationMediumSeconds)) {
showSidebar = true 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) conversationUIModel.sendHug(to: message.sender)
} }
Button("content.actions.slap") { Button(String(localized: "content.actions.slap", defaultValue: "slap")) {
conversationUIModel.sendSlap(to: message.sender) conversationUIModel.sendSlap(to: message.sender)
} }
} }
Button("content.message.copy") { Button(String(localized: "content.message.copy", defaultValue: "copy message")) {
#if os(iOS) #if os(iOS)
UIPasteboard.general.string = message.content UIPasteboard.general.string = message.content
#else #else
@@ -138,12 +138,12 @@ struct MessageListView: View {
#endif #endif
} }
if isResendableFailedMessage(message) { if isResendableFailedMessage(message) {
Button("content.actions.resend") { Button(String(localized: "content.actions.resend", defaultValue: "resend")) {
conversationUIModel.resendFailedPrivateMessage(message) conversationUIModel.resendFailedPrivateMessage(message)
} }
} }
if showsUserActions { 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) conversationUIModel.block(peerID: message.senderPeerID, displayName: message.sender)
} }
} }
@@ -165,14 +165,14 @@ struct MessageListView: View {
showClearConfirmation = true showClearConfirmation = true
} }
.confirmationDialog( .confirmationDialog(
"content.clear.confirm_title", String(localized: "content.clear.confirm_title", defaultValue: "clear this chat?"),
isPresented: $showClearConfirmation, isPresented: $showClearConfirmation,
titleVisibility: .visible titleVisibility: .visible
) { ) {
Button("content.clear.confirm_action", role: .destructive) { Button(String(localized: "content.clear.confirm_action", defaultValue: "clear chat"), role: .destructive) {
conversationUIModel.clearCurrentConversation() conversationUIModel.clearCurrentConversation()
} }
Button("common.cancel", role: .cancel) {} Button(String(localized: "common.cancel", defaultValue: "cancel"), role: .cancel) {}
} }
.onAppear { .onAppear {
scrollToBottom(on: proxy) scrollToBottom(on: proxy)
@@ -190,17 +190,17 @@ struct MessageListView: View {
onSelectedChannelChange(newChannel, proxy: proxy) onSelectedChannelChange(newChannel, proxy: proxy)
} }
.confirmationDialog( .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, isPresented: $showMessageActions,
titleVisibility: .visible titleVisibility: .visible
) { ) {
Button("content.actions.mention") { Button(String(localized: "content.actions.mention", defaultValue: "mention")) {
if let sender = selectedMessageSender { if let sender = selectedMessageSender {
insertMention(sender) insertMention(sender)
} }
} }
Button("content.actions.direct_message") { Button(String(localized: "content.actions.direct_message", defaultValue: "direct message")) {
if let peerID = selectedMessageSenderID { if let peerID = selectedMessageSenderID {
privateConversationModel.openConversation(for: peerID) privateConversationModel.openConversation(for: peerID)
withAnimation(.easeInOut(duration: TransportConfig.uiAnimationMediumSeconds)) { 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 { if let sender = selectedMessageSender {
conversationUIModel.sendHug(to: sender) conversationUIModel.sendHug(to: sender)
} }
} }
Button("content.actions.slap") { Button(String(localized: "content.actions.slap", defaultValue: "slap")) {
if let sender = selectedMessageSender { if let sender = selectedMessageSender {
conversationUIModel.sendSlap(to: sender) 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) conversationUIModel.block(peerID: selectedMessageSenderID, displayName: selectedMessageSender)
} }
Button("common.cancel", role: .cancel) {} Button(String(localized: "common.cancel", defaultValue: "cancel"), role: .cancel) {}
} }
.onAppear { .onAppear {
// Also check when view appears // Also check when view appears
@@ -277,18 +277,18 @@ private extension MessageListView {
VStack(alignment: .leading, spacing: 6) { VStack(alignment: .leading, spacing: 6) {
switch locationChannelsModel.selectedChannel { switch locationChannelsModel.selectedChannel {
case .mesh: 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_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", comment: "Second line of the empty mesh timeline saying no peers are in range yet")) 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", 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"))
case .location(let channel): case .location(let channel):
emptyStateLine( emptyStateLine(
String( 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, locale: .current,
channel.geohash 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) .padding(.horizontal, 12)
@@ -368,7 +368,7 @@ private extension MessageListView {
if unseenCount > 0 { if unseenCount > 0 {
Text( Text(
String( 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, locale: .current,
unseenCount unseenCount
) )
@@ -389,10 +389,10 @@ private extension MessageListView {
} }
var jumpToLatestAccessibilityLabel: String { 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 } guard unseenCount > 0 else { return base }
let count = String( 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, locale: .current,
unseenCount unseenCount
) )
+6 -6
View File
@@ -18,7 +18,7 @@ struct MyQRView: View {
private enum Strings { private enum Strings {
static let title: LocalizedStringKey = "verification.my_qr.title" 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 { var body: some View {
@@ -124,13 +124,13 @@ struct QRScanView: View {
static let validate: LocalizedStringKey = "verification.scan.validate" static let validate: LocalizedStringKey = "verification.scan.validate"
static func requested(_ nickname: String) -> String { static func requested(_ nickname: String) -> 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, locale: .current,
nickname 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 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", comment: "Status when a scanned QR payload is invalid") 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 { var body: some View {
@@ -296,7 +296,7 @@ struct VerificationSheetView: View {
VStack(spacing: 0) { VStack(spacing: 0) {
// Top header (always at top) // Top header (always at top)
HStack { HStack {
Text("verification.sheet.title") Text(String(localized: "verification.sheet.title", defaultValue: "VERIFY"))
.bitchatFont(size: 14, weight: .bold) .bitchatFont(size: 14, weight: .bold)
.foregroundColor(accentColor) .foregroundColor(accentColor)
Spacer() Spacer()
@@ -316,7 +316,7 @@ struct VerificationSheetView: View {
Group { Group {
if showingScanner { if showingScanner {
VStack(alignment: .leading, spacing: 12) { 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) .bitchatFont(size: 16, weight: .bold)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)