@@ -12,10 +12,34 @@ import TransifexObjCRuntime
12
12
/// Swizzles all localizedString() calls made either by Storyboard files or by the use of NSLocalizedString()
13
13
/// function in code.
14
14
class SwizzledBundle : Bundle {
15
+ // NOTE:
16
+ // We can't override the `localizedAttributedString(forKey:value:table:)`
17
+ // method here, as it's not exposed in Swift.
18
+ // We can neither use swizzling in Swift for the same reason.
19
+ // In order to intercept this method (that SwiftUI uses for all its text
20
+ // components), we rely on the `TXNativeObjecSwizzler` to swizzle that
21
+ // method that is also going to work on runtime for Swift/SwiftUI.
15
22
override func localizedString( forKey key: String ,
16
23
value: String ? ,
17
24
table tableName: String ? ) -> String {
18
- if Swizzler . activated && value != Swizzler . SKIP_SWIZZLING_VALUE {
25
+ // Apply the swizzled method only if:
26
+ // * Swizzler has been activated.
27
+ // * Swizzling was not disabled temporarily using the
28
+ // `SKIP_SWIZZLING_VALUE` flag.
29
+ // * The key does not match the reserved Transifex StringsDict key that
30
+ // is used to extract the proper pluralization rule.
31
+ // NOTE: While running the Unit Test suite of the Transifex SDK, we
32
+ // noticed that certain unit tests (e.g. `testPlatformFormat`,
33
+ // `testPlatformFormatMultiple`) were triggering the Transifex module
34
+ // bundle to load to due to the call of the `extractPluralizationRule`
35
+ // method. Even though the loading of the module bundling was
36
+ // happening after the Swizzler was activated, subsequent unit tests
37
+ // had the bundle already loaded in the `Bundle.allBundles` array,
38
+ // causing the bundle to also be swizzled, thus preventing the
39
+ // `Localizable.stringsdict` to report the correct pluralization rule.
40
+ if Swizzler . activated
41
+ && value != Swizzler . SKIP_SWIZZLING_VALUE
42
+ && key != PlatformFormat . TRANSIFEX_STRINGSDICT_KEY {
19
43
return Swizzler . localizedString ( forKey: key,
20
44
value: value,
21
45
table: tableName)
@@ -55,20 +79,100 @@ class Swizzler {
55
79
}
56
80
57
81
self . translationProvider = translationProvider
58
-
82
+
83
+ // Swizzle `NSBundle.localizedString(String,String?,String?)` method
84
+ // for Swift.
59
85
activate ( bundles: Bundle . allBundles)
60
-
61
- TXNativeObjcSwizzler . activate {
86
+
87
+ // Swizzle `-[NSString localizedStringWithFormat:]` method for
88
+ // Objective-C.
89
+ TXNativeObjcSwizzler . swizzleLocalizedString {
62
90
return self . localizedString ( format: $0,
63
91
arguments: $1)
64
92
}
93
+
94
+ // Swizzle `-[NSBundle localizedAttributedStringForKey:value:table:]`
95
+ // method for Objective-C, Swift and SwiftUI.
96
+ TXNativeObjcSwizzler . swizzleLocalizedAttributedString ( self ,
97
+ selector: swizzledLocalizedAttributedStringSelector ( ) )
65
98
66
99
activated = true
67
100
}
68
-
101
+
102
+ /// Deactivates Swizzler
103
+ internal static func deactivate( ) {
104
+ guard activated else {
105
+ return
106
+ }
107
+
108
+ // Deactivate swizzled bundles
109
+ deactivate ( bundles: Bundle . allBundles)
110
+
111
+ // Deactivate swizzling in:
112
+ // * `-[NSString localizedStringWithFormat:]`
113
+ // * `-[NSBundle localizedAttributedStringForKey:value:table:]`
114
+ TXNativeObjcSwizzler . revertLocalizedString ( )
115
+ TXNativeObjcSwizzler . revertLocalizedAttributedString ( self ,
116
+ selector: swizzledLocalizedAttributedStringSelector ( ) )
117
+
118
+ translationProvider = nil
119
+
120
+ activated = false
121
+ }
122
+
123
+ private static func swizzledLocalizedAttributedStringSelector( ) -> Selector {
124
+ return #selector( swizzledLocalizedAttributedString ( forKey: value: table: ) )
125
+ }
126
+
127
+ @objc
128
+ private func swizzledLocalizedAttributedString( forKey key: String ,
129
+ value: String ? ,
130
+ table tableName: String ? ) -> NSAttributedString {
131
+ let swizzledString = Swizzler . localizedString ( forKey: key,
132
+ value: value,
133
+ table: tableName)
134
+ // On supported platforms, attempt to decode the attributed string as
135
+ // markdown in case it contains style decorators (e.g. *italic*,
136
+ // **bold** etc).
137
+ #if os(iOS)
138
+ if #available( iOS 15 , * ) {
139
+ return Self . attributedString ( with: swizzledString)
140
+ }
141
+ #elseif os(watchOS)
142
+ if #available( watchOS 8 , * ) {
143
+ return Self . attributedString ( with: swizzledString)
144
+ }
145
+ #elseif os(tvOS)
146
+ if #available( tvOS 15 , * ) {
147
+ return Self . attributedString ( with: swizzledString)
148
+ }
149
+ #elseif os(macOS)
150
+ if #available( macOS 12 , * ) {
151
+ return Self . attributedString ( with: swizzledString)
152
+ }
153
+ #endif
154
+ // Otherwise, return a simple attributed string
155
+ return NSAttributedString ( string: swizzledString)
156
+ }
157
+
158
+ @available ( macOS 12 , iOS 15 , tvOS 15 , watchOS 8 , * )
159
+ private static func attributedString( with swizzledString: String ) -> NSAttributedString {
160
+ var string : AttributedString
161
+ do {
162
+ string = try AttributedString ( markdown: swizzledString)
163
+ }
164
+ catch {
165
+ // Fallback to the non-Markdown version in case of an error
166
+ // during Markdown parsing.
167
+ return NSAttributedString ( string: swizzledString)
168
+ }
169
+ // If successful, return the Markdown-styled string
170
+ return NSAttributedString ( string)
171
+ }
172
+
69
173
/// Swizzles the passed bundles so that their localization methods are intercepted.
70
174
///
71
- /// - Parameter bundles: The Bundle that will be swizzled
175
+ /// - Parameter bundles: The bundles to be swizzled
72
176
internal static func activate( bundles: [ Bundle ] ) {
73
177
bundles. forEach ( { ( bundle) in
74
178
guard !bundle. isKind ( of: SwizzledBundle . self) else {
@@ -78,15 +182,27 @@ class Swizzler {
78
182
} )
79
183
}
80
184
185
+ /// Reverts the class of the passed swizzled bundles to original `Bundle` class.
186
+ ///
187
+ /// - Parameter bundles: The bundles to be reverted.
188
+ internal static func deactivate( bundles: [ Bundle ] ) {
189
+ bundles. forEach ( { ( bundle) in
190
+ guard bundle. isKind ( of: SwizzledBundle . self) else {
191
+ return
192
+ }
193
+ object_setClass ( bundle, Bundle . self)
194
+ } )
195
+ }
196
+
81
197
/// Centralized method that all swizzled or overriden localizedStringWithFormat: methods will call once
82
198
/// Swizzler is activated.
83
199
///
84
200
/// - Parameters:
85
201
/// - format: The format string
86
202
/// - arguments: An array of arguments of arbitrary type
87
203
/// - Returns: The final string
88
- static func localizedString( format: String ,
89
- arguments: [ Any ] ) -> String {
204
+ internal static func localizedString( format: String ,
205
+ arguments: [ Any ] ) -> String {
90
206
guard let translationProvider = translationProvider else {
91
207
return MISSING_PROVIDER
92
208
}
0 commit comments