Skip to content

Commit d45ec1f

Browse files
authored
Merge pull request #6000 from vector-im/doug/5996_nse_strings
Make sure strings fall back to English if missing in the Notification extension
2 parents 740d50f + 607481b commit d45ec1f

File tree

6 files changed

+69
-41
lines changed

6 files changed

+69
-41
lines changed

Riot/Categories/Bundle.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import Foundation
1919
public extension Bundle {
2020
/// Returns the real app bundle.
2121
/// Can also be used in app extensions.
22-
static var app: Bundle {
22+
@objc static var app: Bundle {
2323
let bundle = main
2424
if bundle.bundleURL.pathExtension == "appex" {
2525
// Peel off two directory levels - MY_APP.app/PlugIns/MY_APP_EXTENSION.appex
@@ -31,6 +31,14 @@ public extension Bundle {
3131
return bundle
3232
}
3333

34+
/// Get an lproj language bundle from the main app bundle.
35+
/// - Parameter language: The language to try to load.
36+
/// - Returns: The lproj bundle if found otherwise `nil`.
37+
@objc static func lprojBundle(for language: String) -> Bundle? {
38+
guard let lprojURL = Bundle.app.url(forResource: language, withExtension: "lproj") else { return nil }
39+
return Bundle(url: lprojURL)
40+
}
41+
3442
/// Whether or not the bundle is the RiotShareExtension.
3543
var isShareExtension: Bool {
3644
bundleURL.lastPathComponent.contains("RiotShareExtension.appex")

Riot/Generated/Strings.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8006,7 +8006,7 @@ public class VectorL10n: NSObject {
80068006

80078007
extension VectorL10n {
80088008
static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String {
8009-
let format = NSLocalizedString(key, tableName: table, bundle: Bundle(for: BundleToken.self), comment: "")
8009+
let format = NSLocalizedString(key, tableName: table, bundle: Bundle.app, comment: "")
80108010
let locale: Locale
80118011
if let providedLocale = LocaleProvider.locale {
80128012
locale = providedLocale
@@ -8018,4 +8018,3 @@ extension VectorL10n {
80188018
}
80198019
}
80208020

8021-
private final class BundleToken {}

Riot/Modules/MatrixKit/Categories/NSBundle+MXKLanguage.m

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616

1717
#import "NSBundle+MXKLanguage.h"
18+
#import "GeneratedInterface-Swift.h"
1819

1920
#import <objc/runtime.h>
2021

@@ -55,37 +56,37 @@ + (void)mxk_setLanguage:(NSString *)language
5556
[self setupMXKLanguageBundle];
5657

5758
// [NSBundle localizedStringForKey] calls will be redirected to the bundle corresponding
58-
// to "language"
59-
objc_setAssociatedObject([NSBundle mainBundle],
60-
&_bundle, language ? [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:language ofType:@"lproj"]] : nil,
59+
// to "language". `lprojBundleFor` loads this from the main app bundle as we might be running in an extension.
60+
objc_setAssociatedObject(NSBundle.app,
61+
&_bundle, language ? [NSBundle lprojBundleFor:language] : nil,
6162
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
6263

63-
objc_setAssociatedObject([NSBundle mainBundle],
64+
objc_setAssociatedObject(NSBundle.app,
6465
&_language, language,
6566
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
6667
}
6768

6869
+ (NSString *)mxk_language
6970
{
70-
return objc_getAssociatedObject([NSBundle mainBundle], &_language);
71+
return objc_getAssociatedObject(NSBundle.app, &_language);
7172
}
7273

7374
+ (void)mxk_setFallbackLanguage:(NSString *)language
7475
{
7576
[self setupMXKLanguageBundle];
7677

77-
objc_setAssociatedObject([NSBundle mainBundle],
78-
&_fallbackBundle, language ? [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:language ofType:@"lproj"]] : nil,
78+
objc_setAssociatedObject(NSBundle.app,
79+
&_fallbackBundle, language ? [NSBundle lprojBundleFor:language] : nil,
7980
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
8081

81-
objc_setAssociatedObject([NSBundle mainBundle],
82+
objc_setAssociatedObject(NSBundle.app,
8283
&_fallbackLanguage, language,
8384
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
8485
}
8586

8687
+ (NSString *)mxk_fallbackLanguage
8788
{
88-
return objc_getAssociatedObject([NSBundle mainBundle], &_fallbackLanguage);
89+
return objc_getAssociatedObject(NSBundle.app, &_fallbackLanguage);
8990
}
9091

9192
#pragma mark - Private methods
@@ -96,7 +97,7 @@ + (void)setupMXKLanguageBundle
9697
dispatch_once(&onceToken, ^{
9798

9899
// Use MXKLanguageBundle as the [NSBundle mainBundle] class
99-
object_setClass([NSBundle mainBundle], [MXKLanguageBundle class]);
100+
object_setClass(NSBundle.app, MXKLanguageBundle.class);
100101
});
101102
}
102103

RiotNSE/NotificationService.swift

Lines changed: 46 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,17 @@ class NotificationService: UNNotificationServiceExtension {
7474
private static let backgroundServiceInitQueue = DispatchQueue(label: "io.element.NotificationService.backgroundServiceInitQueue")
7575
// MARK: - Method Overrides
7676

77+
override init() {
78+
super.init()
79+
80+
// Set up runtime language and fallback by considering the userDefaults object shared within the application group.
81+
let sharedUserDefaults = MXKAppSettings.standard().sharedUserDefaults
82+
if let language = sharedUserDefaults?.string(forKey: "appLanguage") {
83+
Bundle.mxk_setLanguage(language)
84+
}
85+
Bundle.mxk_setFallbackLanguage("en")
86+
}
87+
7788
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
7889
let userInfo = request.content.userInfo
7990

@@ -341,9 +352,9 @@ class NotificationService: UNNotificationServiceExtension {
341352
let isVideoCall = sdp?.contains("m=video") ?? false
342353

343354
if isVideoCall {
344-
notificationBody = NSString.localizedUserNotificationString(forKey: "VIDEO_CALL_FROM_USER", arguments: [eventSenderName as Any])
355+
notificationBody = NotificationService.localizedString(forKey: "VIDEO_CALL_FROM_USER", eventSenderName)
345356
} else {
346-
notificationBody = NSString.localizedUserNotificationString(forKey: "VOICE_CALL_FROM_USER", arguments: [eventSenderName as Any])
357+
notificationBody = NotificationService.localizedString(forKey: "VOICE_CALL_FROM_USER", eventSenderName)
347358
}
348359

349360
// call notifications should stand out from normal messages, so we don't stack them
@@ -405,7 +416,7 @@ class NotificationService: UNNotificationServiceExtension {
405416
}
406417

407418
let msgType = event.content[kMXMessageTypeKey] as? String
408-
let messageContent = event.content[kMXMessageBodyKey] as? String
419+
let messageContent = event.content[kMXMessageBodyKey] as? String ?? ""
409420
let isReply = event.isReply()
410421

411422
if isReply {
@@ -416,30 +427,30 @@ class NotificationService: UNNotificationServiceExtension {
416427

417428
if event.isEncrypted && !self.showDecryptedContentInNotifications {
418429
// Hide the content
419-
notificationBody = NSString.localizedUserNotificationString(forKey: "MESSAGE", arguments: [])
430+
notificationBody = NotificationService.localizedString(forKey: "MESSAGE")
420431
break
421432
}
422433

423434
if event.location != nil {
424-
notificationBody = NSString.localizedUserNotificationString(forKey: "LOCATION_FROM_USER", arguments: [eventSenderName])
435+
notificationBody = NotificationService.localizedString(forKey: "LOCATION_FROM_USER", eventSenderName)
425436
break
426437
}
427438

428439
switch msgType {
429440
case kMXMessageTypeEmote:
430-
notificationBody = NSString.localizedUserNotificationString(forKey: "ACTION_FROM_USER", arguments: [eventSenderName, messageContent as Any])
441+
notificationBody = NotificationService.localizedString(forKey: "ACTION_FROM_USER", eventSenderName, messageContent)
431442
case kMXMessageTypeImage:
432-
notificationBody = NSString.localizedUserNotificationString(forKey: "PICTURE_FROM_USER", arguments: [eventSenderName])
443+
notificationBody = NotificationService.localizedString(forKey: "PICTURE_FROM_USER", eventSenderName)
433444
case kMXMessageTypeVideo:
434-
notificationBody = NSString.localizedUserNotificationString(forKey: "VIDEO_FROM_USER", arguments: [eventSenderName])
445+
notificationBody = NotificationService.localizedString(forKey: "VIDEO_FROM_USER", eventSenderName)
435446
case kMXMessageTypeAudio:
436447
if event.isVoiceMessage() {
437-
notificationBody = NSString.localizedUserNotificationString(forKey: "VOICE_MESSAGE_FROM_USER", arguments: [eventSenderName])
448+
notificationBody = NotificationService.localizedString(forKey: "VOICE_MESSAGE_FROM_USER", eventSenderName)
438449
} else {
439-
notificationBody = NSString.localizedUserNotificationString(forKey: "AUDIO_FROM_USER", arguments: [eventSenderName, messageContent as Any])
450+
notificationBody = NotificationService.localizedString(forKey: "AUDIO_FROM_USER", eventSenderName, messageContent)
440451
}
441452
case kMXMessageTypeFile:
442-
notificationBody = NSString.localizedUserNotificationString(forKey: "FILE_FROM_USER", arguments: [eventSenderName, messageContent as Any])
453+
notificationBody = NotificationService.localizedString(forKey: "FILE_FROM_USER", eventSenderName, messageContent)
443454

444455
// All other message types such as text, notice, server notice etc
445456
default:
@@ -469,50 +480,50 @@ class NotificationService: UNNotificationServiceExtension {
469480
// If there was a change, use the sender's userID if one was blank and show the change.
470481
if let oldDisplayname = oldContent.displayname ?? event.sender,
471482
let displayname = newContent.displayname ?? event.sender {
472-
notificationBody = NSString.localizedUserNotificationString(forKey: "USER_UPDATED_DISPLAYNAME", arguments: [oldDisplayname, displayname])
483+
notificationBody = NotificationService.localizedString(forKey: "USER_UPDATED_DISPLAYNAME", oldDisplayname, displayname)
473484
} else {
474485
// Should never be reached as the event should always have a sender.
475-
notificationBody = NSString.localizedUserNotificationString(forKey: "GENERIC_USER_UPDATED_DISPLAYNAME", arguments: [eventSenderName])
486+
notificationBody = NotificationService.localizedString(forKey: "GENERIC_USER_UPDATED_DISPLAYNAME", eventSenderName)
476487
}
477488
} else {
478489
// If the display name hasn't changed, handle as an avatar change.
479-
notificationBody = NSString.localizedUserNotificationString(forKey: "USER_UPDATED_AVATAR", arguments: [eventSenderName])
490+
notificationBody = NotificationService.localizedString(forKey: "USER_UPDATED_AVATAR", eventSenderName)
480491
}
481492
} else {
482493
// No known reports of having reached this situation for a membership notification
483494
// So use a generic membership updated fallback.
484-
notificationBody = NSString.localizedUserNotificationString(forKey: "USER_MEMBERSHIP_UPDATED", arguments: [eventSenderName])
495+
notificationBody = NotificationService.localizedString(forKey: "USER_MEMBERSHIP_UPDATED", eventSenderName)
485496
}
486497
// Otherwise treat the notification as an invite.
487498
// This is the expected notification content for a membership event.
488499
} else {
489-
if roomDisplayName != nil && roomDisplayName != eventSenderName {
490-
notificationBody = NSString.localizedUserNotificationString(forKey: "USER_INVITE_TO_NAMED_ROOM", arguments: [eventSenderName, roomDisplayName as Any])
500+
if let roomDisplayName = roomDisplayName, roomDisplayName != eventSenderName {
501+
notificationBody = NotificationService.localizedString(forKey: "USER_INVITE_TO_NAMED_ROOM", eventSenderName, roomDisplayName)
491502
} else {
492-
notificationBody = NSString.localizedUserNotificationString(forKey: "USER_INVITE_TO_CHAT", arguments: [eventSenderName])
503+
notificationBody = NotificationService.localizedString(forKey: "USER_INVITE_TO_CHAT", eventSenderName)
493504
}
494505
}
495506

496507
case .sticker:
497508
notificationTitle = self.messageTitle(for: eventSenderName, in: roomDisplayName)
498-
notificationBody = NSString.localizedUserNotificationString(forKey: "STICKER_FROM_USER", arguments: [eventSenderName as Any])
509+
notificationBody = NotificationService.localizedString(forKey: "STICKER_FROM_USER", eventSenderName)
499510

500511
// Reactions are unexpected notification types, but have been seen in some circumstances.
501512
case .reaction:
502513
notificationTitle = self.messageTitle(for: eventSenderName, in: roomDisplayName)
503514
if let reactionKey = event.relatesTo?.key {
504515
// Try to show the reaction key in the notification.
505-
notificationBody = NSString.localizedUserNotificationString(forKey: "REACTION_FROM_USER", arguments: [eventSenderName, reactionKey])
516+
notificationBody = NotificationService.localizedString(forKey: "REACTION_FROM_USER", eventSenderName, reactionKey)
506517
} else {
507518
// Otherwise show a generic reaction.
508-
notificationBody = NSString.localizedUserNotificationString(forKey: "GENERIC_REACTION_FROM_USER", arguments: [eventSenderName])
519+
notificationBody = NotificationService.localizedString(forKey: "GENERIC_REACTION_FROM_USER", eventSenderName)
509520
}
510521

511522
case .custom:
512523
if (event.type == kWidgetMatrixEventTypeString || event.type == kWidgetModularEventTypeString),
513524
let type = event.content?["type"] as? String,
514525
(type == kWidgetTypeJitsiV1 || type == kWidgetTypeJitsiV2) {
515-
notificationBody = NSString.localizedUserNotificationString(forKey: "GROUP_CALL_STARTED", arguments: nil)
526+
notificationBody = NotificationService.localizedString(forKey: "GROUP_CALL_STARTED")
516527
notificationTitle = roomDisplayName
517528

518529
// call notifications should stand out from normal messages, so we don't stack them
@@ -566,7 +577,7 @@ class NotificationService: UNNotificationServiceExtension {
566577
var validatedNotificationTitle: String? = notificationTitle
567578
if self.localAuthenticationService.isProtectionSet {
568579
MXLog.debug("[NotificationService] validateNotificationContentAndComplete: Resetting title and body because app protection is set")
569-
validatedNotificationBody = NSString.localizedUserNotificationString(forKey: "MESSAGE_PROTECTED", arguments: [])
580+
validatedNotificationBody = NotificationService.localizedString(forKey: "MESSAGE_PROTECTED")
570581
validatedNotificationTitle = nil
571582
}
572583

@@ -596,7 +607,7 @@ class NotificationService: UNNotificationServiceExtension {
596607
private func messageTitle(for eventSenderName: String, in roomDisplayName: String?) -> String {
597608
// Display the room name only if it is different than the sender name
598609
if let roomDisplayName = roomDisplayName, roomDisplayName != eventSenderName {
599-
return NSString.localizedUserNotificationString(forKey: "MSG_FROM_USER_IN_ROOM_TITLE", arguments: [eventSenderName, roomDisplayName])
610+
return NotificationService.localizedString(forKey: "MSG_FROM_USER_IN_ROOM_TITLE", eventSenderName, roomDisplayName)
600611
} else {
601612
return eventSenderName
602613
}
@@ -605,9 +616,9 @@ class NotificationService: UNNotificationServiceExtension {
605616
private func replyTitle(for eventSenderName: String, in roomDisplayName: String?) -> String {
606617
// Display the room name only if it is different than the sender name
607618
if let roomDisplayName = roomDisplayName, roomDisplayName != eventSenderName {
608-
return NSString.localizedUserNotificationString(forKey: "REPLY_FROM_USER_IN_ROOM_TITLE", arguments: [eventSenderName, roomDisplayName])
619+
return NotificationService.localizedString(forKey: "REPLY_FROM_USER_IN_ROOM_TITLE", eventSenderName, roomDisplayName)
609620
} else {
610-
return NSString.localizedUserNotificationString(forKey: "REPLY_FROM_USER_TITLE", arguments: [eventSenderName])
621+
return NotificationService.localizedString(forKey: "REPLY_FROM_USER_TITLE", eventSenderName)
611622
}
612623
}
613624

@@ -816,4 +827,13 @@ class NotificationService: UNNotificationServiceExtension {
816827
}
817828
}
818829
}
830+
831+
private static func localizedString(forKey key: String, _ args: CVarArg...) -> String {
832+
// The bundle needs to be an MXKLanguageBundle and contain the lproj files.
833+
// MatrixKit now sets the app bundle as the MXKLanguageBundle
834+
let format = NSLocalizedString(key, bundle: Bundle.app, comment: "")
835+
let locale = LocaleProvider.locale ?? Locale.current
836+
837+
return String(format: format, locale: locale, arguments: args)
838+
}
819839
}

Tools/SwiftGen/Templates/Strings/flat-swift4-vector.stencil

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ import Foundation
6464

6565
extension {{className}} {
6666
static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String {
67-
let format = NSLocalizedString(key, tableName: table, bundle: Bundle(for: BundleToken.self), comment: "")
67+
let format = NSLocalizedString(key, tableName: table, bundle: Bundle.app, comment: "")
6868
let locale: Locale
6969

7070
if let providedLocale = LocaleProvider.locale {
@@ -77,7 +77,6 @@ extension {{className}} {
7777
}
7878
}
7979

80-
private final class BundleToken {}
8180
{% else %}
8281
// No string found
8382
{% endif %}

changelog.d/5996.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Notifications: Strings now fall back to English if they're missing for the current language.

0 commit comments

Comments
 (0)