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