diff --git a/Riot/Categories/Bundle.swift b/Riot/Categories/Bundle.swift index 5b3430154..458469742 100644 --- a/Riot/Categories/Bundle.swift +++ b/Riot/Categories/Bundle.swift @@ -19,7 +19,7 @@ import Foundation public extension Bundle { /// Returns the real app bundle. /// Can also be used in app extensions. - static var app: Bundle { + @objc static var app: Bundle { let bundle = main if bundle.bundleURL.pathExtension == "appex" { // Peel off two directory levels - MY_APP.app/PlugIns/MY_APP_EXTENSION.appex @@ -31,6 +31,14 @@ public extension Bundle { return bundle } + /// Get an lproj language bundle from the main app bundle. + /// - Parameter language: The language to try to load. + /// - Returns: The lproj bundle if found otherwise `nil`. + @objc static func lprojBundle(for language: String) -> Bundle? { + guard let lprojURL = Bundle.app.url(forResource: language, withExtension: "lproj") else { return nil } + return Bundle(url: lprojURL) + } + /// Whether or not the bundle is the RiotShareExtension. var isShareExtension: Bool { bundleURL.lastPathComponent.contains("RiotShareExtension.appex") diff --git a/Riot/Modules/MatrixKit/Categories/NSBundle+MXKLanguage.m b/Riot/Modules/MatrixKit/Categories/NSBundle+MXKLanguage.m index a0112bf99..9615420cd 100644 --- a/Riot/Modules/MatrixKit/Categories/NSBundle+MXKLanguage.m +++ b/Riot/Modules/MatrixKit/Categories/NSBundle+MXKLanguage.m @@ -15,6 +15,7 @@ */ #import "NSBundle+MXKLanguage.h" +#import "GeneratedInterface-Swift.h" #import @@ -55,37 +56,37 @@ static const char _fallbackLanguage = 0; [self setupMXKLanguageBundle]; // [NSBundle localizedStringForKey] calls will be redirected to the bundle corresponding - // to "language" - objc_setAssociatedObject([NSBundle mainBundle], - &_bundle, language ? [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:language ofType:@"lproj"]] : nil, + // to "language". `lprojBundleFor` loads this from the main app bundle as we might be running in an extension. + objc_setAssociatedObject(NSBundle.app, + &_bundle, language ? [NSBundle lprojBundleFor:language] : nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC); - objc_setAssociatedObject([NSBundle mainBundle], + objc_setAssociatedObject(NSBundle.app, &_language, language, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } + (NSString *)mxk_language { - return objc_getAssociatedObject([NSBundle mainBundle], &_language); + return objc_getAssociatedObject(NSBundle.app, &_language); } + (void)mxk_setFallbackLanguage:(NSString *)language { [self setupMXKLanguageBundle]; - objc_setAssociatedObject([NSBundle mainBundle], - &_fallbackBundle, language ? [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:language ofType:@"lproj"]] : nil, + objc_setAssociatedObject(NSBundle.app, + &_fallbackBundle, language ? [NSBundle lprojBundleFor:language] : nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC); - objc_setAssociatedObject([NSBundle mainBundle], + objc_setAssociatedObject(NSBundle.app, &_fallbackLanguage, language, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } + (NSString *)mxk_fallbackLanguage { - return objc_getAssociatedObject([NSBundle mainBundle], &_fallbackLanguage); + return objc_getAssociatedObject(NSBundle.app, &_fallbackLanguage); } #pragma mark - Private methods @@ -96,7 +97,7 @@ static const char _fallbackLanguage = 0; dispatch_once(&onceToken, ^{ // Use MXKLanguageBundle as the [NSBundle mainBundle] class - object_setClass([NSBundle mainBundle], [MXKLanguageBundle class]); + object_setClass(NSBundle.app, MXKLanguageBundle.class); }); } diff --git a/RiotNSE/NotificationService.swift b/RiotNSE/NotificationService.swift index e72804449..5e141c574 100644 --- a/RiotNSE/NotificationService.swift +++ b/RiotNSE/NotificationService.swift @@ -74,6 +74,17 @@ class NotificationService: UNNotificationServiceExtension { private static let backgroundServiceInitQueue = DispatchQueue(label: "io.element.NotificationService.backgroundServiceInitQueue") // MARK: - Method Overrides + override init() { + super.init() + + // Set up runtime language and fallback by considering the userDefaults object shared within the application group. + let sharedUserDefaults = MXKAppSettings.standard().sharedUserDefaults + if let language = sharedUserDefaults?.string(forKey: "appLanguage") { + Bundle.mxk_setLanguage(language) + } + Bundle.mxk_setFallbackLanguage("en") + } + override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { let userInfo = request.content.userInfo @@ -341,9 +352,9 @@ class NotificationService: UNNotificationServiceExtension { let isVideoCall = sdp?.contains("m=video") ?? false if isVideoCall { - notificationBody = NSString.localizedUserNotificationString(forKey: "VIDEO_CALL_FROM_USER", arguments: [eventSenderName as Any]) + notificationBody = NotificationService.localizedString(forKey: "VIDEO_CALL_FROM_USER", eventSenderName) } else { - notificationBody = NSString.localizedUserNotificationString(forKey: "VOICE_CALL_FROM_USER", arguments: [eventSenderName as Any]) + notificationBody = NotificationService.localizedString(forKey: "VOICE_CALL_FROM_USER", eventSenderName) } // call notifications should stand out from normal messages, so we don't stack them @@ -405,7 +416,7 @@ class NotificationService: UNNotificationServiceExtension { } let msgType = event.content[kMXMessageTypeKey] as? String - let messageContent = event.content[kMXMessageBodyKey] as? String + let messageContent = event.content[kMXMessageBodyKey] as? String ?? "" let isReply = event.isReply() if isReply { @@ -416,30 +427,30 @@ class NotificationService: UNNotificationServiceExtension { if event.isEncrypted && !self.showDecryptedContentInNotifications { // Hide the content - notificationBody = NSString.localizedUserNotificationString(forKey: "MESSAGE", arguments: []) + notificationBody = NotificationService.localizedString(forKey: "MESSAGE") break } if event.location != nil { - notificationBody = NSString.localizedUserNotificationString(forKey: "LOCATION_FROM_USER", arguments: [eventSenderName]) + notificationBody = NotificationService.localizedString(forKey: "LOCATION_FROM_USER", eventSenderName) break } switch msgType { case kMXMessageTypeEmote: - notificationBody = NSString.localizedUserNotificationString(forKey: "ACTION_FROM_USER", arguments: [eventSenderName, messageContent as Any]) + notificationBody = NotificationService.localizedString(forKey: "ACTION_FROM_USER", eventSenderName, messageContent) case kMXMessageTypeImage: - notificationBody = NSString.localizedUserNotificationString(forKey: "PICTURE_FROM_USER", arguments: [eventSenderName]) + notificationBody = NotificationService.localizedString(forKey: "PICTURE_FROM_USER", eventSenderName) case kMXMessageTypeVideo: - notificationBody = NSString.localizedUserNotificationString(forKey: "VIDEO_FROM_USER", arguments: [eventSenderName]) + notificationBody = NotificationService.localizedString(forKey: "VIDEO_FROM_USER", eventSenderName) case kMXMessageTypeAudio: if event.isVoiceMessage() { - notificationBody = NSString.localizedUserNotificationString(forKey: "VOICE_MESSAGE_FROM_USER", arguments: [eventSenderName]) + notificationBody = NotificationService.localizedString(forKey: "VOICE_MESSAGE_FROM_USER", eventSenderName) } else { - notificationBody = NSString.localizedUserNotificationString(forKey: "AUDIO_FROM_USER", arguments: [eventSenderName, messageContent as Any]) + notificationBody = NotificationService.localizedString(forKey: "AUDIO_FROM_USER", eventSenderName, messageContent) } case kMXMessageTypeFile: - notificationBody = NSString.localizedUserNotificationString(forKey: "FILE_FROM_USER", arguments: [eventSenderName, messageContent as Any]) + notificationBody = NotificationService.localizedString(forKey: "FILE_FROM_USER", eventSenderName, messageContent) // All other message types such as text, notice, server notice etc default: @@ -469,50 +480,50 @@ class NotificationService: UNNotificationServiceExtension { // If there was a change, use the sender's userID if one was blank and show the change. if let oldDisplayname = oldContent.displayname ?? event.sender, let displayname = newContent.displayname ?? event.sender { - notificationBody = NSString.localizedUserNotificationString(forKey: "USER_UPDATED_DISPLAYNAME", arguments: [oldDisplayname, displayname]) + notificationBody = NotificationService.localizedString(forKey: "USER_UPDATED_DISPLAYNAME", oldDisplayname, displayname) } else { // Should never be reached as the event should always have a sender. - notificationBody = NSString.localizedUserNotificationString(forKey: "GENERIC_USER_UPDATED_DISPLAYNAME", arguments: [eventSenderName]) + notificationBody = NotificationService.localizedString(forKey: "GENERIC_USER_UPDATED_DISPLAYNAME", eventSenderName) } } else { // If the display name hasn't changed, handle as an avatar change. - notificationBody = NSString.localizedUserNotificationString(forKey: "USER_UPDATED_AVATAR", arguments: [eventSenderName]) + notificationBody = NotificationService.localizedString(forKey: "USER_UPDATED_AVATAR", eventSenderName) } } else { // No known reports of having reached this situation for a membership notification // So use a generic membership updated fallback. - notificationBody = NSString.localizedUserNotificationString(forKey: "USER_MEMBERSHIP_UPDATED", arguments: [eventSenderName]) + notificationBody = NotificationService.localizedString(forKey: "USER_MEMBERSHIP_UPDATED", eventSenderName) } // Otherwise treat the notification as an invite. // This is the expected notification content for a membership event. } else { - if roomDisplayName != nil && roomDisplayName != eventSenderName { - notificationBody = NSString.localizedUserNotificationString(forKey: "USER_INVITE_TO_NAMED_ROOM", arguments: [eventSenderName, roomDisplayName as Any]) + if let roomDisplayName = roomDisplayName, roomDisplayName != eventSenderName { + notificationBody = NotificationService.localizedString(forKey: "USER_INVITE_TO_NAMED_ROOM", eventSenderName, roomDisplayName) } else { - notificationBody = NSString.localizedUserNotificationString(forKey: "USER_INVITE_TO_CHAT", arguments: [eventSenderName]) + notificationBody = NotificationService.localizedString(forKey: "USER_INVITE_TO_CHAT", eventSenderName) } } case .sticker: notificationTitle = self.messageTitle(for: eventSenderName, in: roomDisplayName) - notificationBody = NSString.localizedUserNotificationString(forKey: "STICKER_FROM_USER", arguments: [eventSenderName as Any]) + notificationBody = NotificationService.localizedString(forKey: "STICKER_FROM_USER", eventSenderName) // Reactions are unexpected notification types, but have been seen in some circumstances. case .reaction: notificationTitle = self.messageTitle(for: eventSenderName, in: roomDisplayName) if let reactionKey = event.relatesTo?.key { // Try to show the reaction key in the notification. - notificationBody = NSString.localizedUserNotificationString(forKey: "REACTION_FROM_USER", arguments: [eventSenderName, reactionKey]) + notificationBody = NotificationService.localizedString(forKey: "REACTION_FROM_USER", eventSenderName, reactionKey) } else { // Otherwise show a generic reaction. - notificationBody = NSString.localizedUserNotificationString(forKey: "GENERIC_REACTION_FROM_USER", arguments: [eventSenderName]) + notificationBody = NotificationService.localizedString(forKey: "GENERIC_REACTION_FROM_USER", eventSenderName) } case .custom: if (event.type == kWidgetMatrixEventTypeString || event.type == kWidgetModularEventTypeString), let type = event.content?["type"] as? String, (type == kWidgetTypeJitsiV1 || type == kWidgetTypeJitsiV2) { - notificationBody = NSString.localizedUserNotificationString(forKey: "GROUP_CALL_STARTED", arguments: nil) + notificationBody = NotificationService.localizedString(forKey: "GROUP_CALL_STARTED") notificationTitle = roomDisplayName // call notifications should stand out from normal messages, so we don't stack them @@ -566,7 +577,7 @@ class NotificationService: UNNotificationServiceExtension { var validatedNotificationTitle: String? = notificationTitle if self.localAuthenticationService.isProtectionSet { MXLog.debug("[NotificationService] validateNotificationContentAndComplete: Resetting title and body because app protection is set") - validatedNotificationBody = NSString.localizedUserNotificationString(forKey: "MESSAGE_PROTECTED", arguments: []) + validatedNotificationBody = NotificationService.localizedString(forKey: "MESSAGE_PROTECTED") validatedNotificationTitle = nil } @@ -596,7 +607,7 @@ class NotificationService: UNNotificationServiceExtension { private func messageTitle(for eventSenderName: String, in roomDisplayName: String?) -> String { // Display the room name only if it is different than the sender name if let roomDisplayName = roomDisplayName, roomDisplayName != eventSenderName { - return NSString.localizedUserNotificationString(forKey: "MSG_FROM_USER_IN_ROOM_TITLE", arguments: [eventSenderName, roomDisplayName]) + return NotificationService.localizedString(forKey: "MSG_FROM_USER_IN_ROOM_TITLE", eventSenderName, roomDisplayName) } else { return eventSenderName } @@ -605,9 +616,9 @@ class NotificationService: UNNotificationServiceExtension { private func replyTitle(for eventSenderName: String, in roomDisplayName: String?) -> String { // Display the room name only if it is different than the sender name if let roomDisplayName = roomDisplayName, roomDisplayName != eventSenderName { - return NSString.localizedUserNotificationString(forKey: "REPLY_FROM_USER_IN_ROOM_TITLE", arguments: [eventSenderName, roomDisplayName]) + return NotificationService.localizedString(forKey: "REPLY_FROM_USER_IN_ROOM_TITLE", eventSenderName, roomDisplayName) } else { - return NSString.localizedUserNotificationString(forKey: "REPLY_FROM_USER_TITLE", arguments: [eventSenderName]) + return NotificationService.localizedString(forKey: "REPLY_FROM_USER_TITLE", eventSenderName) } } @@ -816,4 +827,13 @@ class NotificationService: UNNotificationServiceExtension { } } } + + private static func localizedString(forKey key: String, _ args: CVarArg...) -> String { + // The bundle needs to be an MXKLanguageBundle and contain the lproj files. + // MatrixKit now sets the app bundle as the MXKLanguageBundle + let format = NSLocalizedString(key, bundle: Bundle.app, comment: "") + let locale = LocaleProvider.locale ?? Locale.current + + return String(format: format, locale: locale, arguments: args) + } } diff --git a/changelog.d/5996.bugfix b/changelog.d/5996.bugfix new file mode 100644 index 000000000..362696b64 --- /dev/null +++ b/changelog.d/5996.bugfix @@ -0,0 +1 @@ +Notifications: Strings now fall back to English if they're missing for the current language. \ No newline at end of file