mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-24 22:45:19 +00:00
Merge pull request #960 from permissionlesstech/cleanup/dead-code-and-helpers
Remove dead code and extract helper methods
This commit is contained in:
@@ -279,8 +279,3 @@ final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
var nilIfEmpty: String? {
|
||||
self.isEmpty ? nil : self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,23 +59,12 @@ final class CommandProcessor {
|
||||
weak var meshService: Transport?
|
||||
private let identityManager: SecureIdentityStateManagerProtocol
|
||||
|
||||
/// Backward-compatible property for existing code
|
||||
weak var chatViewModel: CommandContextProvider? {
|
||||
get { contextProvider }
|
||||
set { contextProvider = newValue }
|
||||
}
|
||||
|
||||
init(contextProvider: CommandContextProvider? = nil, meshService: Transport? = nil, identityManager: SecureIdentityStateManagerProtocol) {
|
||||
self.contextProvider = contextProvider
|
||||
self.meshService = meshService
|
||||
self.identityManager = identityManager
|
||||
}
|
||||
|
||||
/// Backward-compatible initializer
|
||||
convenience init(chatViewModel: ChatViewModel? = nil, meshService: Transport? = nil, identityManager: SecureIdentityStateManagerProtocol) {
|
||||
self.init(contextProvider: chatViewModel, meshService: meshService, identityManager: identityManager)
|
||||
}
|
||||
|
||||
/// Process a command string
|
||||
@MainActor
|
||||
func process(_ command: String) -> CommandResult {
|
||||
|
||||
@@ -34,9 +34,20 @@ final class MessageRouter {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Transport Selection
|
||||
|
||||
private func reachableTransport(for peerID: PeerID) -> Transport? {
|
||||
transports.first { $0.isPeerReachable(peerID) }
|
||||
}
|
||||
|
||||
private func connectedTransport(for peerID: PeerID) -> Transport? {
|
||||
transports.first { $0.isPeerConnected(peerID) }
|
||||
}
|
||||
|
||||
// MARK: - Message Sending
|
||||
|
||||
func sendPrivate(_ content: String, to peerID: PeerID, recipientNickname: String, messageID: String) {
|
||||
// Try to find a reachable transport
|
||||
if let transport = transports.first(where: { $0.isPeerReachable(peerID) }) {
|
||||
if let transport = reachableTransport(for: peerID) {
|
||||
SecureLogger.debug("Routing PM via \(type(of: transport)) to \(peerID.id.prefix(8))… id=\(messageID.prefix(8))…", category: .session)
|
||||
transport.sendPrivateMessage(content, to: peerID, recipientNickname: recipientNickname, messageID: messageID)
|
||||
} else {
|
||||
@@ -48,38 +59,26 @@ final class MessageRouter {
|
||||
}
|
||||
|
||||
func sendReadReceipt(_ receipt: ReadReceipt, to peerID: PeerID) {
|
||||
if let transport = transports.first(where: { $0.isPeerReachable(peerID) }) {
|
||||
if let transport = reachableTransport(for: peerID) {
|
||||
SecureLogger.debug("Routing READ ack via \(type(of: transport)) to \(peerID.id.prefix(8))… id=\(receipt.originalMessageID.prefix(8))…", category: .session)
|
||||
transport.sendReadReceipt(receipt, to: peerID)
|
||||
} else if !transports.isEmpty {
|
||||
// Fallback to last transport (usually Nostr) if neither is explicitly reachable?
|
||||
// Or better: just try the first one that supports it?
|
||||
// Existing logic preferred mesh, then nostr.
|
||||
// If neither reachable, existing logic queued it (via mesh usually) or sent via nostr.
|
||||
// Let's stick to "try reachable". If none, maybe pick the first one to queue?
|
||||
// Actually, for READ receipts, we might want to just fire-and-forget on the "best effort" transport.
|
||||
// But let's stick to the reachable check.
|
||||
SecureLogger.debug("No reachable transport for READ ack to \(peerID.id.prefix(8))…", category: .session)
|
||||
}
|
||||
}
|
||||
|
||||
func sendDeliveryAck(_ messageID: String, to peerID: PeerID) {
|
||||
if let transport = transports.first(where: { $0.isPeerReachable(peerID) }) {
|
||||
if let transport = reachableTransport(for: peerID) {
|
||||
SecureLogger.debug("Routing DELIVERED ack via \(type(of: transport)) to \(peerID.id.prefix(8))… id=\(messageID.prefix(8))…", category: .session)
|
||||
transport.sendDeliveryAck(for: messageID, to: peerID)
|
||||
}
|
||||
}
|
||||
|
||||
func sendFavoriteNotification(to peerID: PeerID, isFavorite: Bool) {
|
||||
if let transport = transports.first(where: { $0.isPeerConnected(peerID) }) {
|
||||
if let transport = connectedTransport(for: peerID) {
|
||||
transport.sendFavoriteNotification(to: peerID, isFavorite: isFavorite)
|
||||
} else if let transport = transports.first(where: { $0.isPeerReachable(peerID) }) {
|
||||
} else if let transport = reachableTransport(for: peerID) {
|
||||
transport.sendFavoriteNotification(to: peerID, isFavorite: isFavorite)
|
||||
} else {
|
||||
// Fallback: try all? or just the last one?
|
||||
// Old logic: if mesh connected, mesh. Else nostr.
|
||||
// Note: NostrTransport.isPeerReachable now returns true if mapped.
|
||||
// If not mapped, we can't send via Nostr anyway.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +90,7 @@ final class MessageRouter {
|
||||
var remaining: [(content: String, nickname: String, messageID: String)] = []
|
||||
|
||||
for (content, nickname, messageID) in queued {
|
||||
if let transport = transports.first(where: { $0.isPeerReachable(peerID) }) {
|
||||
if let transport = reachableTransport(for: peerID) {
|
||||
SecureLogger.debug("Outbox -> \(type(of: transport)) for \(peerID.id.prefix(8))… id=\(messageID.prefix(8))…", category: .session)
|
||||
transport.sendPrivateMessage(content, to: peerID, recipientNickname: nickname, messageID: messageID)
|
||||
} else {
|
||||
|
||||
@@ -118,32 +118,15 @@ final class NostrTransport: Transport, @unchecked Sendable {
|
||||
|
||||
func sendPrivateMessage(_ content: String, to peerID: PeerID, recipientNickname: String, messageID: String) {
|
||||
Task { @MainActor in
|
||||
guard let recipientNpub = resolveRecipientNpub(for: peerID) else { return }
|
||||
guard let senderIdentity = try? idBridge.getCurrentNostrIdentity() else { return }
|
||||
SecureLogger.debug("NostrTransport: preparing PM to \(recipientNpub.prefix(16))… for peerID \(peerID.id.prefix(8))… id=\(messageID.prefix(8))…", category: .session)
|
||||
// Convert recipient npub -> hex (x-only)
|
||||
let recipientHex: String
|
||||
do {
|
||||
let (hrp, data) = try Bech32.decode(recipientNpub)
|
||||
guard hrp == "npub" else {
|
||||
SecureLogger.error("NostrTransport: recipient key not npub (hrp=\(hrp))", category: .session)
|
||||
return
|
||||
}
|
||||
recipientHex = data.hexEncodedString()
|
||||
} catch {
|
||||
SecureLogger.error("NostrTransport: failed to decode npub -> hex: \(error)", category: .session)
|
||||
return
|
||||
}
|
||||
guard let recipientNpub = resolveRecipientNpub(for: peerID),
|
||||
let recipientHex = npubToHex(recipientNpub),
|
||||
let senderIdentity = try? idBridge.getCurrentNostrIdentity() else { return }
|
||||
SecureLogger.debug("NostrTransport: preparing PM to \(recipientNpub.prefix(16))… id=\(messageID.prefix(8))…", category: .session)
|
||||
guard let embedded = NostrEmbeddedBitChat.encodePMForNostr(content: content, messageID: messageID, recipientPeerID: peerID, senderPeerID: senderPeerID) else {
|
||||
SecureLogger.error("NostrTransport: failed to embed PM packet", category: .session)
|
||||
return
|
||||
}
|
||||
guard let event = try? NostrProtocol.createPrivateMessage(content: embedded, recipientPubkey: recipientHex, senderIdentity: senderIdentity) else {
|
||||
SecureLogger.error("NostrTransport: failed to build Nostr event for PM", category: .session)
|
||||
return
|
||||
}
|
||||
SecureLogger.debug("NostrTransport: sending PM giftWrap id=\(event.id.prefix(16))…", category: .session)
|
||||
NostrRelayManager.shared.sendEvent(event)
|
||||
sendWrappedMessage(content: embedded, recipientHex: recipientHex, senderIdentity: senderIdentity)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,58 +141,31 @@ final class NostrTransport: Transport, @unchecked Sendable {
|
||||
|
||||
func sendFavoriteNotification(to peerID: PeerID, isFavorite: Bool) {
|
||||
Task { @MainActor in
|
||||
guard let recipientNpub = resolveRecipientNpub(for: peerID) else { return }
|
||||
guard let senderIdentity = try? idBridge.getCurrentNostrIdentity() else { return }
|
||||
guard let recipientNpub = resolveRecipientNpub(for: peerID),
|
||||
let recipientHex = npubToHex(recipientNpub),
|
||||
let senderIdentity = try? idBridge.getCurrentNostrIdentity() else { return }
|
||||
let content = isFavorite ? "[FAVORITED]:\(senderIdentity.npub)" : "[UNFAVORITED]:\(senderIdentity.npub)"
|
||||
SecureLogger.debug("NostrTransport: preparing FAVORITE(\(isFavorite)) to \(recipientNpub.prefix(16))…", category: .session)
|
||||
// Convert recipient npub -> hex
|
||||
let recipientHex: String
|
||||
do {
|
||||
let (hrp, data) = try Bech32.decode(recipientNpub)
|
||||
guard hrp == "npub" else { return }
|
||||
recipientHex = data.hexEncodedString()
|
||||
} catch {
|
||||
SecureLogger.error("NostrTransport: failed to decode recipient npub for favorite notification: \(error.localizedDescription)", category: .session)
|
||||
return
|
||||
}
|
||||
guard let embedded = NostrEmbeddedBitChat.encodePMForNostr(content: content, messageID: UUID().uuidString, recipientPeerID: peerID, senderPeerID: senderPeerID) else {
|
||||
SecureLogger.error("NostrTransport: failed to embed favorite notification", category: .session)
|
||||
return
|
||||
}
|
||||
guard let event = try? NostrProtocol.createPrivateMessage(content: embedded, recipientPubkey: recipientHex, senderIdentity: senderIdentity) else {
|
||||
SecureLogger.error("NostrTransport: failed to build Nostr event for favorite notification", category: .session)
|
||||
return
|
||||
}
|
||||
SecureLogger.debug("NostrTransport: sending favorite giftWrap id=\(event.id.prefix(16))…", category: .session)
|
||||
NostrRelayManager.shared.sendEvent(event)
|
||||
sendWrappedMessage(content: embedded, recipientHex: recipientHex, senderIdentity: senderIdentity)
|
||||
}
|
||||
}
|
||||
|
||||
func sendBroadcastAnnounce() { /* no-op for Nostr */ }
|
||||
func sendDeliveryAck(for messageID: String, to peerID: PeerID) {
|
||||
Task { @MainActor in
|
||||
guard let recipientNpub = resolveRecipientNpub(for: peerID) else { return }
|
||||
guard let senderIdentity = try? idBridge.getCurrentNostrIdentity() else { return }
|
||||
SecureLogger.debug("NostrTransport: preparing DELIVERED ack for id=\(messageID.prefix(8))… to \(recipientNpub.prefix(16))…", category: .session)
|
||||
let recipientHex: String
|
||||
do {
|
||||
let (hrp, data) = try Bech32.decode(recipientNpub)
|
||||
guard hrp == "npub" else { return }
|
||||
recipientHex = data.hexEncodedString()
|
||||
} catch {
|
||||
SecureLogger.error("NostrTransport: failed to decode recipient npub for delivery ack: \(error.localizedDescription)", category: .session)
|
||||
return
|
||||
}
|
||||
guard let recipientNpub = resolveRecipientNpub(for: peerID),
|
||||
let recipientHex = npubToHex(recipientNpub),
|
||||
let senderIdentity = try? idBridge.getCurrentNostrIdentity() else { return }
|
||||
SecureLogger.debug("NostrTransport: preparing DELIVERED ack id=\(messageID.prefix(8))…", category: .session)
|
||||
guard let ack = NostrEmbeddedBitChat.encodeAckForNostr(type: .delivered, messageID: messageID, recipientPeerID: peerID, senderPeerID: senderPeerID) else {
|
||||
SecureLogger.error("NostrTransport: failed to embed DELIVERED ack", category: .session)
|
||||
return
|
||||
}
|
||||
guard let event = try? NostrProtocol.createPrivateMessage(content: ack, recipientPubkey: recipientHex, senderIdentity: senderIdentity) else {
|
||||
SecureLogger.error("NostrTransport: failed to build Nostr event for DELIVERED ack", category: .session)
|
||||
return
|
||||
}
|
||||
SecureLogger.debug("NostrTransport: sending DELIVERED ack giftWrap id=\(event.id.prefix(16))…", category: .session)
|
||||
NostrRelayManager.shared.sendEvent(event)
|
||||
sendWrappedMessage(content: ack, recipientHex: recipientHex, senderIdentity: senderIdentity)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -221,21 +177,17 @@ extension NostrTransport {
|
||||
// MARK: Geohash ACK helpers
|
||||
func sendDeliveryAckGeohash(for messageID: String, toRecipientHex recipientHex: String, from identity: NostrIdentity) {
|
||||
Task { @MainActor in
|
||||
SecureLogger.debug("GeoDM: send DELIVERED -> recip=\(recipientHex.prefix(8))… mid=\(messageID.prefix(8))… from=\(identity.publicKeyHex.prefix(8))…", category: .session)
|
||||
SecureLogger.debug("GeoDM: send DELIVERED mid=\(messageID.prefix(8))…", category: .session)
|
||||
guard let embedded = NostrEmbeddedBitChat.encodeAckForNostrNoRecipient(type: .delivered, messageID: messageID, senderPeerID: senderPeerID) else { return }
|
||||
guard let event = try? NostrProtocol.createPrivateMessage(content: embedded, recipientPubkey: recipientHex, senderIdentity: identity) else { return }
|
||||
NostrRelayManager.registerPendingGiftWrap(id: event.id)
|
||||
NostrRelayManager.shared.sendEvent(event)
|
||||
sendWrappedMessage(content: embedded, recipientHex: recipientHex, senderIdentity: identity, registerPending: true)
|
||||
}
|
||||
}
|
||||
|
||||
func sendReadReceiptGeohash(_ messageID: String, toRecipientHex recipientHex: String, from identity: NostrIdentity) {
|
||||
Task { @MainActor in
|
||||
SecureLogger.debug("GeoDM: send READ -> recip=\(recipientHex.prefix(8))… mid=\(messageID.prefix(8))… from=\(identity.publicKeyHex.prefix(8))…", category: .session)
|
||||
SecureLogger.debug("GeoDM: send READ mid=\(messageID.prefix(8))…", category: .session)
|
||||
guard let embedded = NostrEmbeddedBitChat.encodeAckForNostrNoRecipient(type: .readReceipt, messageID: messageID, senderPeerID: senderPeerID) else { return }
|
||||
guard let event = try? NostrProtocol.createPrivateMessage(content: embedded, recipientPubkey: recipientHex, senderIdentity: identity) else { return }
|
||||
NostrRelayManager.registerPendingGiftWrap(id: event.id)
|
||||
NostrRelayManager.shared.sendEvent(event)
|
||||
sendWrappedMessage(content: embedded, recipientHex: recipientHex, senderIdentity: identity, registerPending: true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,19 +195,12 @@ extension NostrTransport {
|
||||
func sendPrivateMessageGeohash(content: String, toRecipientHex recipientHex: String, from identity: NostrIdentity, messageID: String) {
|
||||
Task { @MainActor in
|
||||
guard !recipientHex.isEmpty else { return }
|
||||
SecureLogger.debug("GeoDM: send PM -> recip=\(recipientHex.prefix(8))… mid=\(messageID.prefix(8))… from=\(identity.publicKeyHex.prefix(8))…", category: .session)
|
||||
// Build embedded BitChat packet without recipient peer ID
|
||||
SecureLogger.debug("GeoDM: send PM mid=\(messageID.prefix(8))…", category: .session)
|
||||
guard let embedded = NostrEmbeddedBitChat.encodePMForNostrNoRecipient(content: content, messageID: messageID, senderPeerID: senderPeerID) else {
|
||||
SecureLogger.error("NostrTransport: failed to embed geohash PM packet", category: .session)
|
||||
return
|
||||
}
|
||||
guard let event = try? NostrProtocol.createPrivateMessage(content: embedded, recipientPubkey: recipientHex, senderIdentity: identity) else {
|
||||
SecureLogger.error("NostrTransport: failed to build Nostr event for geohash PM", category: .session)
|
||||
return
|
||||
}
|
||||
SecureLogger.debug("NostrTransport: sending geohash PM giftWrap id=\(event.id.prefix(16))…", category: .session)
|
||||
NostrRelayManager.registerPendingGiftWrap(id: event.id)
|
||||
NostrRelayManager.shared.sendEvent(event)
|
||||
sendWrappedMessage(content: embedded, recipientHex: recipientHex, senderIdentity: identity, registerPending: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -263,6 +208,32 @@ extension NostrTransport {
|
||||
// MARK: - Private Helpers
|
||||
|
||||
extension NostrTransport {
|
||||
/// Converts npub bech32 string to hex pubkey
|
||||
@MainActor
|
||||
private func npubToHex(_ npub: String) -> String? {
|
||||
do {
|
||||
let (hrp, data) = try Bech32.decode(npub)
|
||||
guard hrp == "npub" else { return nil }
|
||||
return data.hexEncodedString()
|
||||
} catch {
|
||||
SecureLogger.error("NostrTransport: failed to decode npub -> hex: \(error)", category: .session)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates and sends a gift-wrapped private message event
|
||||
@MainActor
|
||||
private func sendWrappedMessage(content: String, recipientHex: String, senderIdentity: NostrIdentity, registerPending: Bool = false) {
|
||||
guard let event = try? NostrProtocol.createPrivateMessage(content: content, recipientPubkey: recipientHex, senderIdentity: senderIdentity) else {
|
||||
SecureLogger.error("NostrTransport: failed to build Nostr event", category: .session)
|
||||
return
|
||||
}
|
||||
if registerPending {
|
||||
NostrRelayManager.registerPendingGiftWrap(id: event.id)
|
||||
}
|
||||
NostrRelayManager.shared.sendEvent(event)
|
||||
}
|
||||
|
||||
/// Must be called within a barrier on `queue`
|
||||
private func processReadQueueIfNeeded() {
|
||||
guard !isSendingReadAcks else { return }
|
||||
@@ -275,31 +246,16 @@ extension NostrTransport {
|
||||
/// Sends a single read ack item (called after extraction from queue within barrier)
|
||||
private func sendReadAckItem(_ item: QueuedRead) {
|
||||
Task { @MainActor in
|
||||
guard let recipientNpub = resolveRecipientNpub(for: item.peerID) else { scheduleNextReadAck(); return }
|
||||
guard let senderIdentity = try? idBridge.getCurrentNostrIdentity() else { scheduleNextReadAck(); return }
|
||||
SecureLogger.debug("NostrTransport: preparing READ ack for id=\(item.receipt.originalMessageID.prefix(8))… to \(recipientNpub.prefix(16))…", category: .session)
|
||||
// Convert recipient npub -> hex
|
||||
let recipientHex: String
|
||||
do {
|
||||
let (hrp, data) = try Bech32.decode(recipientNpub)
|
||||
guard hrp == "npub" else { scheduleNextReadAck(); return }
|
||||
recipientHex = data.hexEncodedString()
|
||||
} catch {
|
||||
SecureLogger.error("NostrTransport: failed to decode recipient npub for read ack: \(error.localizedDescription)", category: .session)
|
||||
scheduleNextReadAck()
|
||||
return
|
||||
}
|
||||
defer { scheduleNextReadAck() }
|
||||
guard let recipientNpub = resolveRecipientNpub(for: item.peerID),
|
||||
let recipientHex = npubToHex(recipientNpub),
|
||||
let senderIdentity = try? idBridge.getCurrentNostrIdentity() else { return }
|
||||
SecureLogger.debug("NostrTransport: preparing READ ack id=\(item.receipt.originalMessageID.prefix(8))…", category: .session)
|
||||
guard let ack = NostrEmbeddedBitChat.encodeAckForNostr(type: .readReceipt, messageID: item.receipt.originalMessageID, recipientPeerID: item.peerID, senderPeerID: senderPeerID) else {
|
||||
SecureLogger.error("NostrTransport: failed to embed READ ack", category: .session)
|
||||
scheduleNextReadAck(); return
|
||||
return
|
||||
}
|
||||
guard let event = try? NostrProtocol.createPrivateMessage(content: ack, recipientPubkey: recipientHex, senderIdentity: senderIdentity) else {
|
||||
SecureLogger.error("NostrTransport: failed to build Nostr event for READ ack", category: .session)
|
||||
scheduleNextReadAck(); return
|
||||
}
|
||||
SecureLogger.debug("NostrTransport: sending READ ack giftWrap id=\(event.id.prefix(16))…", category: .session)
|
||||
NostrRelayManager.shared.sendEvent(event)
|
||||
scheduleNextReadAck()
|
||||
sendWrappedMessage(content: ack, recipientHex: recipientHex, senderIdentity: senderIdentity)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -448,7 +448,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, CommandContextProv
|
||||
self.deduplicationService = MessageDeduplicationService()
|
||||
|
||||
// Wire up dependencies
|
||||
self.commandProcessor.chatViewModel = self
|
||||
self.commandProcessor.contextProvider = self
|
||||
self.participantTracker.configure(context: self)
|
||||
|
||||
// Subscribe to privateChatManager changes to trigger UI updates
|
||||
|
||||
@@ -6,7 +6,7 @@ struct CommandProcessorTests {
|
||||
|
||||
@MainActor
|
||||
@Test func slapNotFoundGrammar() {
|
||||
let processor = CommandProcessor(chatViewModel: nil, meshService: nil, identityManager: identityManager)
|
||||
let processor = CommandProcessor(contextProvider: nil, meshService: nil, identityManager: identityManager)
|
||||
let result = processor.process("/slap @system")
|
||||
switch result {
|
||||
case .error(let message):
|
||||
@@ -18,7 +18,7 @@ struct CommandProcessorTests {
|
||||
|
||||
@MainActor
|
||||
@Test func hugNotFoundGrammar() {
|
||||
let processor = CommandProcessor(chatViewModel: nil, meshService: nil, identityManager: identityManager)
|
||||
let processor = CommandProcessor(contextProvider: nil, meshService: nil, identityManager: identityManager)
|
||||
let result = processor.process("/hug @system")
|
||||
switch result {
|
||||
case .error(let message):
|
||||
@@ -30,7 +30,7 @@ struct CommandProcessorTests {
|
||||
|
||||
@MainActor
|
||||
@Test func slapUsageMessage() {
|
||||
let processor = CommandProcessor(chatViewModel: nil, meshService: nil, identityManager: identityManager)
|
||||
let processor = CommandProcessor(contextProvider: nil, meshService: nil, identityManager: identityManager)
|
||||
let result = processor.process("/slap")
|
||||
switch result {
|
||||
case .error(let message):
|
||||
|
||||
@@ -1,530 +0,0 @@
|
||||
//import Foundation
|
||||
//import XCTest
|
||||
//@testable import bitchat
|
||||
//
|
||||
//private let localizationTestsDirectoryURL = URL(fileURLWithPath: #filePath).deletingLastPathComponent()
|
||||
//private let testsRootURL = localizationTestsDirectoryURL.deletingLastPathComponent()
|
||||
//private let repoRootURL = testsRootURL.deletingLastPathComponent()
|
||||
//
|
||||
//final class LocalizationCatalogTests: XCTestCase {
|
||||
// // Ensures every app locale includes exactly the same keys as Base.
|
||||
// func testAppCatalogLocaleParity() throws {
|
||||
// let context = try loadContext(relativePath: "bitchat/Localizable.xcstrings")
|
||||
// assertLocaleParity(context: context, catalogName: "App")
|
||||
// }
|
||||
//
|
||||
// // Verifies format placeholders stay consistent across app locales.
|
||||
// func testAppCatalogPlaceholderConsistency() throws {
|
||||
// let context = try loadContext(relativePath: "bitchat/Localizable.xcstrings")
|
||||
// assertPlaceholderConsistency(context: context, catalogName: "App")
|
||||
// }
|
||||
//
|
||||
// // Guards a core set of app strings from going empty per locale.
|
||||
// func testAppPrimaryKeysNonEmpty() throws {
|
||||
// let context = try loadContext(relativePath: "bitchat/Localizable.xcstrings")
|
||||
// let primaryKeys = try loadPrimaryKeys().app
|
||||
// assertPrimaryKeysPresent(context: context, keys: primaryKeys, catalogName: "App")
|
||||
// }
|
||||
//
|
||||
// // Ensures every share extension locale matches Base key coverage.
|
||||
// func testShareExtensionCatalogLocaleParity() throws {
|
||||
// let context = try loadContext(relativePath: "bitchatShareExtension/Localization/Localizable.xcstrings")
|
||||
// assertLocaleParity(context: context, catalogName: "ShareExtension")
|
||||
// }
|
||||
//
|
||||
// // Verifies share extension placeholders align across locales.
|
||||
// func testShareExtensionCatalogPlaceholderConsistency() throws {
|
||||
// let context = try loadContext(relativePath: "bitchatShareExtension/Localization/Localizable.xcstrings")
|
||||
// assertPlaceholderConsistency(context: context, catalogName: "ShareExtension")
|
||||
// }
|
||||
//
|
||||
// // Confirms critical share extension strings remain non-empty per locale.
|
||||
// func testShareExtensionPrimaryKeysNonEmpty() throws {
|
||||
// let context = try loadContext(relativePath: "bitchatShareExtension/Localization/Localizable.xcstrings")
|
||||
// let primaryKeys = try loadPrimaryKeys().shareExtension
|
||||
// assertPrimaryKeysPresent(context: context, keys: primaryKeys, catalogName: "ShareExtension")
|
||||
// }
|
||||
//
|
||||
// // Validates that configured locales contain expected string values.
|
||||
// func testLocalizationExpectedValues() throws {
|
||||
// let appContext = try loadContext(relativePath: "bitchat/Localizable.xcstrings")
|
||||
// let shareContext = try loadContext(relativePath: "bitchatShareExtension/Localization/Localizable.xcstrings")
|
||||
// let config = try loadPrimaryKeys()
|
||||
//
|
||||
// guard let testLocales = config.testLocales else {
|
||||
// // If no testLocales specified, skip this test
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// guard let expectedValues = config.expectedValues else {
|
||||
// XCTFail("No expectedValues configured in PrimaryLocalizationKeys.json")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// // Loop through each locale to test
|
||||
// for locale in testLocales {
|
||||
// guard let localeExpectedValues = expectedValues[locale] else {
|
||||
// XCTFail("No expected values configured for locale '\(locale)' in PrimaryLocalizationKeys.json")
|
||||
// continue
|
||||
// }
|
||||
//
|
||||
// // Test each expected key/value pair for this locale
|
||||
// for (key, expectedValue) in localeExpectedValues {
|
||||
// if config.app.contains(key) {
|
||||
// assertLocaleStringValue(context: appContext, locale: locale, key: key, expectedValue: expectedValue, catalogName: "App")
|
||||
// } else if config.shareExtension.contains(key) {
|
||||
// assertLocaleStringValue(context: shareContext, locale: locale, key: key, expectedValue: expectedValue, catalogName: "ShareExtension")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Ensures configured test locales are present and complete.
|
||||
// func testConfiguredLocalesCompleteness() throws {
|
||||
// let appContext = try loadContext(relativePath: "bitchat/Localizable.xcstrings")
|
||||
// let shareContext = try loadContext(relativePath: "bitchatShareExtension/Localization/Localizable.xcstrings")
|
||||
// let config = try loadPrimaryKeys()
|
||||
//
|
||||
// guard let testLocales = config.testLocales else {
|
||||
// // If no testLocales specified, skip this test
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// let baseLocale = appContext.baseLocale
|
||||
// let baseAppKeys = appContext.keysByLocale[baseLocale] ?? Set()
|
||||
// let baseShareKeys = shareContext.keysByLocale[baseLocale] ?? Set()
|
||||
//
|
||||
// for locale in testLocales {
|
||||
// // Skip base locale comparison with itself
|
||||
// if locale == baseLocale { continue }
|
||||
//
|
||||
// // Verify locale is present in both catalogs
|
||||
// XCTAssertTrue(appContext.locales.contains(locale), "Locale '\(locale)' missing from app catalog")
|
||||
// XCTAssertTrue(shareContext.locales.contains(locale), "Locale '\(locale)' missing from share extension catalog")
|
||||
//
|
||||
// // Verify locale has same number of keys as base locale
|
||||
// let appLocaleKeys = appContext.keysByLocale[locale] ?? Set()
|
||||
// XCTAssertEqual(appLocaleKeys.count, baseAppKeys.count, "Locale '\(locale)' app catalog missing keys compared to \(baseLocale)")
|
||||
//
|
||||
// let shareLocaleKeys = shareContext.keysByLocale[locale] ?? Set()
|
||||
// XCTAssertEqual(shareLocaleKeys.count, baseShareKeys.count, "Locale '\(locale)' share extension catalog missing keys compared to \(baseLocale)")
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // MARK: - Assertions
|
||||
//
|
||||
// private func assertLocaleParity(context: CatalogContext, catalogName: String, file: StaticString = #filePath, line: UInt = #line) {
|
||||
// let baseLocale = context.baseLocale
|
||||
// guard let baseKeys = context.keysByLocale[baseLocale] else {
|
||||
// return XCTFail("Missing base locale \(baseLocale) in \(catalogName) catalog", file: file, line: line)
|
||||
// }
|
||||
//
|
||||
// for (locale, keys) in context.keysByLocale.sorted(by: { $0.key < $1.key }) {
|
||||
// XCTAssertEqual(keys, baseKeys, "Locale \(locale) has key mismatch in \(catalogName) catalog", file: file, line: line)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func assertPlaceholderConsistency(context: CatalogContext, catalogName: String, file: StaticString = #filePath, line: UInt = #line) {
|
||||
// let baseLocale = context.baseLocale
|
||||
// guard let baseSignatures = context.placeholderSignature[baseLocale] else {
|
||||
// return XCTFail("Missing base placeholder signature for \(catalogName)", file: file, line: line)
|
||||
// }
|
||||
//
|
||||
// for (locale, localeSignatures) in context.placeholderSignature.sorted(by: { $0.key < $1.key }) {
|
||||
// guard locale != baseLocale else { continue }
|
||||
// for key in baseSignatures.keys.sorted() {
|
||||
// guard let baseMap = baseSignatures[key] else {
|
||||
// continue
|
||||
// }
|
||||
// guard let localeMap = localeSignatures[key] else {
|
||||
// return XCTFail("Key \(key) missing for locale \(locale) in \(catalogName) catalog", file: file, line: line)
|
||||
// }
|
||||
// for path in baseMap.keys.sorted() {
|
||||
// let expected = normalizedPlaceholders(baseMap[path, default: []])
|
||||
// let actual = normalizedPlaceholders(localeMap[path, default: []])
|
||||
// XCTAssertEqual(actual, expected, "Placeholder mismatch for key \(key) at \(path) in locale \(locale) (\(catalogName))", file: file, line: line)
|
||||
// }
|
||||
// for (localePath, localeTokens) in localeMap {
|
||||
// guard baseMap[localePath] == nil else { continue }
|
||||
// guard let fallback = fallbackPath(for: localePath, baseMap: baseMap) else {
|
||||
// XCTFail("Unexpected variation \(localePath) for key \(key) in locale \(locale) (\(catalogName))", file: file, line: line)
|
||||
// continue
|
||||
// }
|
||||
// let expected = normalizedPlaceholders(baseMap[fallback, default: []])
|
||||
// let actual = normalizedPlaceholders(localeTokens)
|
||||
// XCTAssertEqual(actual, expected, "Placeholder mismatch for key \(key) at \(localePath) (fallback \(fallback)) in locale \(locale) (\(catalogName))", file: file, line: line)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func assertPrimaryKeysPresent(context: CatalogContext, keys: [String], catalogName: String, file: StaticString = #filePath, line: UInt = #line) {
|
||||
// for key in keys {
|
||||
// guard let entry = context.catalog.strings[key] else {
|
||||
// XCTFail("Missing primary key \(key) in \(catalogName) catalog", file: file, line: line)
|
||||
// continue
|
||||
// }
|
||||
// for locale in context.locales.sorted() {
|
||||
// guard let localization = entry.localizations[locale], let unit = localization.stringUnit else {
|
||||
// XCTFail("Missing localization for key \(key) in locale \(locale) (\(catalogName))", file: file, line: line)
|
||||
// continue
|
||||
// }
|
||||
// let segments = gatherSegments(from: unit)
|
||||
// XCTAssertFalse(segments.isEmpty, "No content for key \(key) in locale \(locale) (\(catalogName))", file: file, line: line)
|
||||
// for segment in segments {
|
||||
// let trimmed = segment.value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
// XCTAssertFalse(trimmed.isEmpty, "Empty translation for key \(key) at \(segment.path) in locale \(locale) (\(catalogName))", file: file, line: line)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func assertLocaleStringValue(context: CatalogContext, locale: String, key: String, expectedValue: String, catalogName: String, file: StaticString = #filePath, line: UInt = #line) {
|
||||
// guard let entry = context.catalog.strings[key] else {
|
||||
// XCTFail("Missing key \(key) in \(catalogName) catalog", file: file, line: line)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// guard let localization = entry.localizations[locale], let unit = localization.stringUnit else {
|
||||
// XCTFail("Missing \(locale) localization for key \(key) in \(catalogName) catalog", file: file, line: line)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// // For simple strings (non-pluralized)
|
||||
// if let actualValue = unit.value {
|
||||
// XCTAssertEqual(actualValue, expectedValue, "\(locale) translation mismatch for key \(key) in \(catalogName) catalog. Expected: '\(expectedValue)', Actual: '\(actualValue)'", file: file, line: line)
|
||||
// } else {
|
||||
// XCTFail("Key \(key) has no value in \(locale) localization for \(catalogName) catalog", file: file, line: line)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // MARK: - Loading
|
||||
//
|
||||
// private func loadContext(relativePath: String) throws -> CatalogContext {
|
||||
// let catalog = try loadCatalog(relativePath: relativePath)
|
||||
// let locales = catalog.locales
|
||||
// let baseLocale = catalog.sourceLanguage
|
||||
// var keysByLocale: [String: Set<String>] = [:]
|
||||
// var placeholderSignature: [String: [String: [String: [String]]]] = [:]
|
||||
//
|
||||
// for locale in locales {
|
||||
// var localeKeys: Set<String> = []
|
||||
// var localePlaceholders: [String: [String: [String]]] = [:]
|
||||
// for (key, entry) in catalog.strings {
|
||||
// guard let localization = entry.localizations[locale], let unit = localization.stringUnit else {
|
||||
// continue
|
||||
// }
|
||||
// localeKeys.insert(key)
|
||||
// let segments = gatherSegments(from: unit)
|
||||
// var pathMap: [String: [String]] = [:]
|
||||
// for segment in segments {
|
||||
// pathMap[segment.path] = placeholders(in: segment.value)
|
||||
// }
|
||||
// localePlaceholders[key] = pathMap
|
||||
// }
|
||||
// keysByLocale[locale] = localeKeys
|
||||
// placeholderSignature[locale] = localePlaceholders
|
||||
// }
|
||||
//
|
||||
// return CatalogContext(catalog: catalog, locales: locales, baseLocale: baseLocale, keysByLocale: keysByLocale, placeholderSignature: placeholderSignature)
|
||||
// }
|
||||
//
|
||||
// private func loadCatalog(relativePath: String) throws -> StringCatalog {
|
||||
// let url = repoRootURL.appendingPathComponent(relativePath)
|
||||
// let data = try Data(contentsOf: url)
|
||||
// return try JSONDecoder().decode(StringCatalog.self, from: data)
|
||||
// }
|
||||
//
|
||||
// private func loadPrimaryKeys() throws -> PrimaryKeyConfig {
|
||||
// let url = localizationTestsDirectoryURL.appendingPathComponent("PrimaryLocalizationKeys.json")
|
||||
// let data = try Data(contentsOf: url)
|
||||
// return try JSONDecoder().decode(PrimaryKeyConfig.self, from: data)
|
||||
// }
|
||||
//
|
||||
// // MARK: - Regression Tests
|
||||
//
|
||||
// /// Helper method to access the app bundle for localization testing.
|
||||
// /// This ensures tests use the actual app's Localizable.xcstrings instead of
|
||||
// /// the test bundle, preventing false positives where localization keys are
|
||||
// /// returned instead of translated strings.
|
||||
// private func appBundle() -> Bundle {
|
||||
// Bundle(for: ChatViewModel.self)
|
||||
// }
|
||||
//
|
||||
// /// Tests to prevent regression of the pluralization format string issue
|
||||
// /// These tests ensure that String(localized:) properly handles
|
||||
// /// pluralized format strings with %#@variable@ syntax
|
||||
// func testPluralizedFormatStringsDoNotCrash() {
|
||||
// // These are the exact calls that were causing runtime errors
|
||||
//
|
||||
// // Test 1: content.accessibility.people_count with Int argument
|
||||
// XCTAssertNoThrow({
|
||||
// let result = String(
|
||||
// localized: "content.accessibility.people_count",
|
||||
// bundle: appBundle(),
|
||||
// comment: "Accessibility label announcing number of people in header"
|
||||
// )
|
||||
// XCTAssertFalse(result.isEmpty, "Should return formatted string")
|
||||
// // Verify we're getting actual localized content, not just the key
|
||||
// XCTAssertNotEqual(result, "content.accessibility.people_count", "Should return localized string, not key")
|
||||
// }(), "People count localization should not throw")
|
||||
//
|
||||
// // Test 2: location_notes.header with String and Int arguments
|
||||
// XCTAssertNoThrow({
|
||||
// let result = String(
|
||||
// localized: "location_notes.header",
|
||||
// bundle: appBundle(),
|
||||
// comment: "Header displaying the geohash and localized note count"
|
||||
// )
|
||||
// XCTAssertFalse(result.isEmpty, "Should return formatted string")
|
||||
// // Verify we're getting actual localized content, not just the key
|
||||
// XCTAssertNotEqual(result, "location_notes.header", "Should return localized string, not key")
|
||||
// }(), "Location notes header localization should not throw")
|
||||
// }
|
||||
//
|
||||
// func testFormatStringArgumentMatching() {
|
||||
// // Verify that the format strings properly handle their expected arguments
|
||||
//
|
||||
// // People count expects: %#@people@ format with Int
|
||||
// let peopleResult = String(
|
||||
// localized: "content.accessibility.people_count",
|
||||
// bundle: appBundle(),
|
||||
// comment: "Test"
|
||||
// )
|
||||
//
|
||||
// // Should not show format specifiers in the base string
|
||||
// XCTAssertFalse(peopleResult.isEmpty, "Should return non-empty string")
|
||||
//
|
||||
// // Location notes expects: #%@ • %#@note_count@ format with String, Int
|
||||
// let notesResult = String(
|
||||
// localized: "location_notes.header",
|
||||
// bundle: appBundle(),
|
||||
// comment: "Test"
|
||||
// )
|
||||
//
|
||||
// XCTAssertFalse(notesResult.isEmpty, "Should return non-empty string")
|
||||
// }
|
||||
//
|
||||
// func testPluralizedStringsWithArguments() {
|
||||
// // Test the pluralized strings with actual format arguments
|
||||
//
|
||||
// // Test people count with different values
|
||||
// let testCases = [0, 1, 2, 5]
|
||||
//
|
||||
// for count in testCases {
|
||||
// let result = String(
|
||||
// format: String(localized: "content.accessibility.people_count", bundle: appBundle(), comment: "Test"),
|
||||
// locale: .current,
|
||||
// count
|
||||
// )
|
||||
//
|
||||
// // Just verify it doesn't crash and returns a reasonable string
|
||||
// XCTAssertFalse(result.isEmpty,
|
||||
// "People count should return non-empty string for count \(count)")
|
||||
// XCTAssertTrue(result.contains("\(count)"),
|
||||
// "Result should contain the count number for count \(count)")
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func testLocationNotesHeaderWithArguments() {
|
||||
// let geohash = "abc123"
|
||||
// let testCases = [0, 1, 2, 10]
|
||||
//
|
||||
// for count in testCases {
|
||||
// let result = String(
|
||||
// format: String(localized: "location_notes.header", bundle: appBundle(), comment: "Test"),
|
||||
// locale: .current,
|
||||
// geohash, count
|
||||
// )
|
||||
//
|
||||
// // Verify basic structure is maintained
|
||||
// XCTAssertTrue(result.contains(geohash),
|
||||
// "Result should contain geohash for count \(count)")
|
||||
// XCTAssertTrue(result.contains("\(count)"),
|
||||
// "Result should contain count for count \(count)")
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func testLocationChannelsRowTitleWithArguments() {
|
||||
// let label = "test-channel"
|
||||
// let testCases = [0, 1, 2, 5]
|
||||
//
|
||||
// for count in testCases {
|
||||
// let result = String(
|
||||
// format: String(localized: "location_channels.row_title", bundle: appBundle(), comment: "Test"),
|
||||
// locale: .current,
|
||||
// label, count
|
||||
// )
|
||||
//
|
||||
// // Verify basic structure is maintained
|
||||
// XCTAssertTrue(result.contains(label),
|
||||
// "Result should contain label for count \(count)")
|
||||
// XCTAssertTrue(result.contains("\(count)"),
|
||||
// "Result should contain count for count \(count)")
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func testNoArgumentsLocalization() {
|
||||
// // Test that strings without arguments still work
|
||||
// let result = String(
|
||||
// localized: "common.ok",
|
||||
// bundle: appBundle(),
|
||||
// comment: "OK button text"
|
||||
// )
|
||||
//
|
||||
// XCTAssertFalse(result.isEmpty, "Simple localization should work")
|
||||
// }
|
||||
//
|
||||
// func testLocalizationEdgeCases() {
|
||||
// // Test edge cases that might cause issues
|
||||
//
|
||||
// // Large numbers
|
||||
// let largeNumberResult = String(
|
||||
// format: String(localized: "content.accessibility.people_count", bundle: appBundle(), comment: "Test"),
|
||||
// locale: .current,
|
||||
// 1000000
|
||||
// )
|
||||
// XCTAssertTrue(largeNumberResult.contains("1000000"), "Should handle large numbers")
|
||||
//
|
||||
// // Zero
|
||||
// let zeroResult = String(
|
||||
// format: String(localized: "content.accessibility.people_count", bundle: appBundle(), comment: "Test"),
|
||||
// locale: .current,
|
||||
// 0
|
||||
// )
|
||||
// XCTAssertTrue(zeroResult.contains("0"), "Should handle zero")
|
||||
//
|
||||
// // Empty geohash (edge case)
|
||||
// let emptyGeohashResult = String(
|
||||
// format: String(localized: "location_notes.header", bundle: appBundle(), comment: "Test"),
|
||||
// locale: .current,
|
||||
// "", 1
|
||||
// )
|
||||
// XCTAssertFalse(emptyGeohashResult.isEmpty, "Should handle empty geohash")
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//// MARK: - Helpers
|
||||
//
|
||||
//private struct CatalogContext {
|
||||
// let catalog: StringCatalog
|
||||
// let locales: [String]
|
||||
// let baseLocale: String
|
||||
// let keysByLocale: [String: Set<String>]
|
||||
// let placeholderSignature: [String: [String: [String: [String]]]]
|
||||
//}
|
||||
//
|
||||
//private struct StringCatalog: Decodable {
|
||||
// let sourceLanguage: String
|
||||
// let strings: [String: CatalogEntry]
|
||||
//
|
||||
// var locales: [String] {
|
||||
// var localeSet: Set<String> = []
|
||||
// for entry in strings.values {
|
||||
// localeSet.formUnion(entry.localizations.keys)
|
||||
// }
|
||||
// return localeSet.sorted()
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//private struct CatalogEntry: Decodable {
|
||||
// let localizations: [String: CatalogLocalization]
|
||||
//}
|
||||
//
|
||||
//private struct CatalogLocalization: Decodable {
|
||||
// let stringUnit: CatalogStringUnit?
|
||||
//}
|
||||
//
|
||||
//private struct CatalogStringUnit: Decodable {
|
||||
// let state: String
|
||||
// let value: String?
|
||||
// let variations: CatalogVariations?
|
||||
// let comment: String?
|
||||
//}
|
||||
//
|
||||
//private struct CatalogVariations: Decodable {
|
||||
// let plural: [String: [String: CatalogVariationValue]]?
|
||||
//}
|
||||
//
|
||||
//private struct CatalogVariationValue: Decodable {
|
||||
// let stringUnit: CatalogStringUnit?
|
||||
//}
|
||||
//
|
||||
//private struct Segment {
|
||||
// let components: [String]
|
||||
// let value: String
|
||||
//
|
||||
// var path: String {
|
||||
// components.isEmpty ? "base" : components.joined(separator: ".")
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//private func gatherSegments(from unit: CatalogStringUnit, prefix: [String] = []) -> [Segment] {
|
||||
// var segments: [Segment] = []
|
||||
// if let value = unit.value {
|
||||
// segments.append(Segment(components: prefix, value: value))
|
||||
// } else if prefix.isEmpty {
|
||||
// segments.append(Segment(components: [], value: ""))
|
||||
// }
|
||||
// if let plural = unit.variations?.plural {
|
||||
// for (variable, categories) in plural.sorted(by: { $0.key < $1.key }) {
|
||||
// for (category, variation) in categories.sorted(by: { $0.key < $1.key }) {
|
||||
// if let nested = variation.stringUnit {
|
||||
// var nextPrefix = prefix
|
||||
// nextPrefix.append("plural")
|
||||
// nextPrefix.append(variable)
|
||||
// nextPrefix.append(category)
|
||||
// segments.append(contentsOf: gatherSegments(from: nested, prefix: nextPrefix))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return segments
|
||||
//}
|
||||
//
|
||||
//private func normalizedPlaceholders(_ tokens: [String]) -> [String] {
|
||||
// tokens.sorted()
|
||||
//}
|
||||
//
|
||||
//private func fallbackPath(for localePath: String, baseMap: [String: [String]]) -> String? {
|
||||
// let parts = localePath.split(separator: ".")
|
||||
// guard parts.count == 3, parts.first == "plural" else {
|
||||
// return nil
|
||||
// }
|
||||
// let variable = parts[1]
|
||||
// let otherKey = "plural.\(variable).other"
|
||||
// if baseMap[otherKey] != nil {
|
||||
// return otherKey
|
||||
// }
|
||||
// let oneKey = "plural.\(variable).one"
|
||||
// if baseMap[oneKey] != nil {
|
||||
// return oneKey
|
||||
// }
|
||||
// return nil
|
||||
//}
|
||||
//
|
||||
//private let placeholderRegex: NSRegularExpression = {
|
||||
// let pattern = "%(?:\\d+\\$)?#@[A-Za-z0-9_]+@|%(?:\\d+\\$)?[#0\\- +'\"]*(?:\\d+|\\*)?(?:\\.\\d+)?(?:hh|h|ll|l|z|t|L)?[a-zA-Z@]"
|
||||
// return try! NSRegularExpression(pattern: pattern, options: [])
|
||||
//}()
|
||||
//
|
||||
//private func placeholders(in string: String) -> [String] {
|
||||
// let range = NSRange(location: 0, length: (string as NSString).length)
|
||||
// let matches = placeholderRegex.matches(in: string, options: [], range: range)
|
||||
// var tokens: [String] = []
|
||||
// for match in matches {
|
||||
// if let range = Range(match.range, in: string) {
|
||||
// let token = String(string[range])
|
||||
// if token == "%%" { continue }
|
||||
// tokens.append(token)
|
||||
// }
|
||||
// }
|
||||
// return tokens
|
||||
//}
|
||||
//
|
||||
//private struct PrimaryKeyConfig: Decodable {
|
||||
// let app: [String]
|
||||
// let shareExtension: [String]
|
||||
// let expectedValues: [String: [String: String]]?
|
||||
// let testLocales: [String]?
|
||||
//}
|
||||
Reference in New Issue
Block a user