Skip to content

Commit 8d78687

Browse files
committed
feat: add completion handler for localization changes and update documentation
1 parent e2c07c1 commit 8d78687

File tree

7 files changed

+116
-15
lines changed

7 files changed

+116
-15
lines changed

Sources/CrowdinSDK/CrowdinSDK/CrowdinSDK.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ public typealias CrowdinSDKLocalizationUpdateDownload = () -> Void
1717
/// Closure type for localization update error handlers.
1818
public typealias CrowdinSDKLocalizationUpdateError = ([Error]) -> Void
1919

20+
/// Closure type for localization switch completion handlers.
21+
public typealias CrowdinSDKLocalizationChangeCompletion = (Error?) -> Void
22+
2023
/// Closure type for Log messages handlers.
2124
public typealias CrowdinSDKLogMessage = (String) -> Void
2225

@@ -92,6 +95,15 @@ public typealias CrowdinSDKLogMessage = (String) -> Void
9295
self.currentLocalization = localization
9396
}
9497

98+
/// Method for changing SDK localization and getting notified when localization refresh completes.
99+
///
100+
/// - Parameters:
101+
/// - localization: Localization code to use. If `nil`, localization will be auto-detected.
102+
/// - completion: Completion handler called when localization refresh finishes.
103+
public class func setCurrentLocalization(_ localization: String?, completion: @escaping CrowdinSDKLocalizationChangeCompletion) {
104+
Localization.setCurrentLocalization(localization, completion: completion)
105+
}
106+
95107
/// Utils method for extracting all localization strings and plurals to Documents folder.
96108
/// This method will extract all localization for all languages and store it in Extracted subfolder in Crowdin folder.
97109
public class func extractAllLocalization() {

Sources/CrowdinSDK/CrowdinSDK/Localization/Localization.swift

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,20 +31,28 @@ class Localization {
3131
/// Property for detecting and storing curent localization value depending on current SDK mode.
3232
static var currentLocalization: String? {
3333
set {
34-
self.customLocalization = newValue
35-
if let localization = newValue {
36-
Localization.current?.provider.localization = localization
37-
Localization.current?.extractor.localization = localization
38-
} else {
39-
Localization.current?.provider.localization = autoDetectedLocalization
40-
Localization.current?.extractor.localization = autoDetectedLocalization
41-
}
34+
setCurrentLocalization(newValue)
4235
}
4336
get {
4437
return customLocalization
4538
}
4639
}
4740

41+
static func setCurrentLocalization(_ localization: String?) {
42+
setCurrentLocalization(localization, completion: { _ in })
43+
}
44+
45+
static func setCurrentLocalization(_ localization: String?, completion: @escaping ((Error?) -> Void)) {
46+
self.customLocalization = localization
47+
let targetLocalization = localization ?? autoDetectedLocalization
48+
Localization.current?.extractor.localization = targetLocalization
49+
guard let provider = Localization.current?.provider else {
50+
completion(nil)
51+
return
52+
}
53+
provider.setLocalization(targetLocalization, completion: completion)
54+
}
55+
4856
/// Auto detects localization.
4957
/// For detection uses localizations from the bundle and from the current provider.
5058
/// Return "en" if SDK isn't initialized or there are no languages ether on crowdin and bundle.

Sources/CrowdinSDK/CrowdinSDK/Localization/Provider/LocalizationProvider.swift

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ protocol LocalizationProviderProtocol {
1515
var localStorage: LocalLocalizationStorageProtocol { get }
1616
var remoteStorage: RemoteLocalizationStorageProtocol { get }
1717

18-
var localization: String { get set }
18+
var localization: String { get }
1919
var localizations: [String] { get }
2020

2121
func refreshLocalization()
@@ -24,6 +24,7 @@ protocol LocalizationProviderProtocol {
2424
func prepare(with completion: @escaping () -> Void)
2525

2626
func deintegrate()
27+
func setLocalization(_ localization: String, completion: @escaping ((Error?) -> Void))
2728
func localizedString(for key: String) -> String?
2829
func key(for string: String) -> String?
2930
func values(for string: String, with format: String) -> [Any]?
@@ -36,11 +37,7 @@ class LocalizationProvider: NSObject, LocalizationProviderProtocol {
3637
case localizableStringsdict = "Localizable.stringsdict"
3738
}
3839
// Public
39-
var localization: String {
40-
didSet {
41-
self.refreshLocalization()
42-
}
43-
}
40+
var localization: String
4441
var localizations: [String] { return remoteStorage.localizations }
4542

4643
var localStorage: LocalLocalizationStorageProtocol
@@ -101,6 +98,11 @@ class LocalizationProvider: NSObject, LocalizationProviderProtocol {
10198
}
10299
}
103100

101+
func setLocalization(_ localization: String, completion: @escaping ((Error?) -> Void)) {
102+
self.localization = localization
103+
self.refreshLocalization(completion: completion)
104+
}
105+
104106
// Private method
105107
private func loadLocalLocalization(completion: @escaping ((Error?) -> Void)) {
106108
self.localStorage.localization = localization

Sources/Tests/Core/BundleSwizzleReentrancyTests.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ private class ReentrantProvider: LocalizationProviderProtocol {
6161

6262
func refreshLocalization() { }
6363
func refreshLocalization(completion: @escaping ((Error?) -> Void)) { completion(nil) }
64+
func setLocalization(_ localization: String, completion: @escaping ((Error?) -> Void)) {
65+
self.localization = localization
66+
completion(nil)
67+
}
6468
func prepare(with completion: @escaping () -> Void) { completion() }
6569
func deintegrate() { }
6670

@@ -89,6 +93,10 @@ private class NSErrorReentrantProvider: LocalizationProviderProtocol {
8993

9094
func refreshLocalization() { }
9195
func refreshLocalization(completion: @escaping ((Error?) -> Void)) { completion(nil) }
96+
func setLocalization(_ localization: String, completion: @escaping ((Error?) -> Void)) {
97+
self.localization = localization
98+
completion(nil)
99+
}
92100
func prepare(with completion: @escaping () -> Void) { completion() }
93101
func deintegrate() { }
94102

Tests/UnitTests/InitializationCompletionHandlerTests.swift

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,57 @@ class InitializationCompletionHandlerTests: XCTestCase {
3131
}
3232

3333
}
34+
35+
private class TrackingMockLocalizationProvider: MockLocalizationProvider {
36+
var setLocalizationCalls: [String] = []
37+
38+
override func setLocalization(_ localization: String, completion: @escaping ((Error?) -> Void)) {
39+
setLocalizationCalls.append(localization)
40+
super.setLocalization(localization, completion: completion)
41+
}
42+
}
43+
44+
class SetCurrentLocalizationCompletionTests: XCTestCase {
45+
override func setUp() {
46+
super.setUp()
47+
Localization.current = nil
48+
CrowdinSDK.currentLocalization = nil
49+
}
50+
51+
override func tearDown() {
52+
Localization.current = nil
53+
CrowdinSDK.currentLocalization = nil
54+
super.tearDown()
55+
}
56+
57+
func testSetCurrentLocalizationWithCompletionUpdatesProvider() {
58+
let provider = TrackingMockLocalizationProvider(
59+
localization: "en",
60+
localStorage: MockLocalStorage(),
61+
remoteStorage: MockRemoteStorage()
62+
)
63+
Localization.current = Localization(provider: provider)
64+
65+
let completionExpectation = expectation(description: "Localization completion called")
66+
CrowdinSDK.setCurrentLocalization("de") { error in
67+
XCTAssertNil(error)
68+
completionExpectation.fulfill()
69+
}
70+
wait(for: [completionExpectation], timeout: 1.0)
71+
72+
XCTAssertEqual(provider.setLocalizationCalls, ["de"])
73+
XCTAssertEqual(provider.localization, "de")
74+
XCTAssertEqual(CrowdinSDK.currentLocalization, "de")
75+
}
76+
77+
func testSetCurrentLocalizationWithCompletionWithoutInitializedProviderCallsCompletion() {
78+
let completionExpectation = expectation(description: "Localization completion called")
79+
CrowdinSDK.setCurrentLocalization("de") { error in
80+
XCTAssertNil(error)
81+
completionExpectation.fulfill()
82+
}
83+
wait(for: [completionExpectation], timeout: 1.0)
84+
85+
XCTAssertEqual(CrowdinSDK.currentLocalization, "de")
86+
}
87+
}

Tests/UnitTests/MockLocalizationProvider.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ class MockLocalizationProvider: LocalizationProviderProtocol {
2323

2424
func refreshLocalization() {}
2525
func refreshLocalization(completion: @escaping ((Error?) -> Void)) {}
26+
func setLocalization(_ localization: String, completion: @escaping ((Error?) -> Void)) {
27+
self.localization = localization
28+
completion(nil)
29+
}
2630
func prepare(with completion: @escaping () -> Void) {}
2731
func deintegrate() {}
2832

@@ -63,4 +67,4 @@ class MockRemoteStorage: RemoteLocalizationStorageProtocol {
6367
func fetchData(completion: ([String]?, String, [String : String]?, [AnyHashable : Any]?) -> Void, errorHandler: ((Error) -> Void)?) {}
6468
func prepare(with completion: @escaping () -> Void) { completion() }
6569
func deintegrate() {}
66-
}
70+
}

website/docs/setup.mdx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,19 @@ if let currentLocale = CrowdinSDK.currentLocalization {
174174

175175
This is the recommended way to change the language programmatically. The SDK will download the new localization if it's not already available. If set to `nil` - the localization will be detected automatically based on the languages available in Crowdin and the system's preferred languages.
176176

177+
If you need to update UI only after the localization refresh finishes, use the completion-based API:
178+
179+
```swift
180+
CrowdinSDK.setCurrentLocalization("<language_code>") { error in
181+
if let error = error {
182+
print("Localization switch failed: \(error.localizedDescription)")
183+
return
184+
}
185+
186+
// Reload your UI here.
187+
}
188+
```
189+
177190

178191
:::caution
179192
The UI doesn't update automatically. You must manually update the UI after changing the localization.

0 commit comments

Comments
 (0)