Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Sources/CrowdinSDK/CrowdinSDK/CrowdinSDK.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ public typealias CrowdinSDKLocalizationUpdateDownload = () -> Void
/// Closure type for localization update error handlers.
public typealias CrowdinSDKLocalizationUpdateError = ([Error]) -> Void

/// Closure type for localization switch completion handlers.
public typealias CrowdinSDKLocalizationChangeCompletion = (Error?) -> Void

/// Closure type for Log messages handlers.
public typealias CrowdinSDKLogMessage = (String) -> Void

Expand Down Expand Up @@ -92,6 +95,15 @@ public typealias CrowdinSDKLogMessage = (String) -> Void
self.currentLocalization = localization
}

/// Method for changing SDK localization and getting notified when localization refresh completes.
///
/// - Parameters:
/// - localization: Localization code to use. If `nil`, localization will be auto-detected.
/// - completion: Completion handler called when localization refresh finishes.
public class func setCurrentLocalization(_ localization: String?, completion: @escaping CrowdinSDKLocalizationChangeCompletion) {
Localization.setCurrentLocalization(localization, completion: completion)
}

/// Utils method for extracting all localization strings and plurals to Documents folder.
/// This method will extract all localization for all languages and store it in Extracted subfolder in Crowdin folder.
public class func extractAllLocalization() {
Expand Down
24 changes: 16 additions & 8 deletions Sources/CrowdinSDK/CrowdinSDK/Localization/Localization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,28 @@ class Localization {
/// Property for detecting and storing curent localization value depending on current SDK mode.
static var currentLocalization: String? {
set {
self.customLocalization = newValue
if let localization = newValue {
Localization.current?.provider.localization = localization
Localization.current?.extractor.localization = localization
} else {
Localization.current?.provider.localization = autoDetectedLocalization
Localization.current?.extractor.localization = autoDetectedLocalization
}
setCurrentLocalization(newValue)
}
get {
return customLocalization
}
}

static func setCurrentLocalization(_ localization: String?) {
setCurrentLocalization(localization, completion: { _ in })
}

static func setCurrentLocalization(_ localization: String?, completion: @escaping ((Error?) -> Void)) {
self.customLocalization = localization
let targetLocalization = localization ?? autoDetectedLocalization
Localization.current?.extractor.localization = targetLocalization
guard let provider = Localization.current?.provider else {
completion(nil)
return
}
provider.setLocalization(targetLocalization, completion: completion)
}

/// Auto detects localization.
/// For detection uses localizations from the bundle and from the current provider.
/// Return "en" if SDK isn't initialized or there are no languages ether on crowdin and bundle.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ protocol LocalizationProviderProtocol {
var localStorage: LocalLocalizationStorageProtocol { get }
var remoteStorage: RemoteLocalizationStorageProtocol { get }

var localization: String { get set }
var localization: String { get }
var localizations: [String] { get }

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

func deintegrate()
func setLocalization(_ localization: String, completion: @escaping ((Error?) -> Void))
func localizedString(for key: String) -> String?
func key(for string: String) -> String?
func values(for string: String, with format: String) -> [Any]?
Expand All @@ -36,11 +37,7 @@ class LocalizationProvider: NSObject, LocalizationProviderProtocol {
case localizableStringsdict = "Localizable.stringsdict"
}
// Public
var localization: String {
didSet {
self.refreshLocalization()
}
}
var localization: String
var localizations: [String] { return remoteStorage.localizations }

var localStorage: LocalLocalizationStorageProtocol
Expand Down Expand Up @@ -101,6 +98,11 @@ class LocalizationProvider: NSObject, LocalizationProviderProtocol {
}
}

func setLocalization(_ localization: String, completion: @escaping ((Error?) -> Void)) {
self.localization = localization
self.refreshLocalization(completion: completion)
}

// Private method
private func loadLocalLocalization(completion: @escaping ((Error?) -> Void)) {
self.localStorage.localization = localization
Expand Down
8 changes: 8 additions & 0 deletions Sources/Tests/Core/BundleSwizzleReentrancyTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ private class ReentrantProvider: LocalizationProviderProtocol {

func refreshLocalization() { }
func refreshLocalization(completion: @escaping ((Error?) -> Void)) { completion(nil) }
func setLocalization(_ localization: String, completion: @escaping ((Error?) -> Void)) {
self.localization = localization
completion(nil)
}
func prepare(with completion: @escaping () -> Void) { completion() }
func deintegrate() { }

Expand Down Expand Up @@ -89,6 +93,10 @@ private class NSErrorReentrantProvider: LocalizationProviderProtocol {

func refreshLocalization() { }
func refreshLocalization(completion: @escaping ((Error?) -> Void)) { completion(nil) }
func setLocalization(_ localization: String, completion: @escaping ((Error?) -> Void)) {
self.localization = localization
completion(nil)
}
func prepare(with completion: @escaping () -> Void) { completion() }
func deintegrate() { }

Expand Down
54 changes: 54 additions & 0 deletions Tests/UnitTests/InitializationCompletionHandlerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,57 @@ class InitializationCompletionHandlerTests: XCTestCase {
}

}

private class TrackingMockLocalizationProvider: MockLocalizationProvider {
var setLocalizationCalls: [String] = []

override func setLocalization(_ localization: String, completion: @escaping ((Error?) -> Void)) {
setLocalizationCalls.append(localization)
super.setLocalization(localization, completion: completion)
}
}

class SetCurrentLocalizationCompletionTests: XCTestCase {
override func setUp() {
super.setUp()
Localization.current = nil
CrowdinSDK.currentLocalization = nil
}

override func tearDown() {
Localization.current = nil
CrowdinSDK.currentLocalization = nil
super.tearDown()
}

func testSetCurrentLocalizationWithCompletionUpdatesProvider() {
let provider = TrackingMockLocalizationProvider(
localization: "en",
localStorage: MockLocalStorage(),
remoteStorage: MockRemoteStorage()
)
Localization.current = Localization(provider: provider)

let completionExpectation = expectation(description: "Localization completion called")
CrowdinSDK.setCurrentLocalization("de") { error in
XCTAssertNil(error)
completionExpectation.fulfill()
}
wait(for: [completionExpectation], timeout: 1.0)

XCTAssertEqual(provider.setLocalizationCalls, ["de"])
XCTAssertEqual(provider.localization, "de")
XCTAssertEqual(CrowdinSDK.currentLocalization, "de")
}

func testSetCurrentLocalizationWithCompletionWithoutInitializedProviderCallsCompletion() {
let completionExpectation = expectation(description: "Localization completion called")
CrowdinSDK.setCurrentLocalization("de") { error in
XCTAssertNil(error)
completionExpectation.fulfill()
}
wait(for: [completionExpectation], timeout: 1.0)

XCTAssertEqual(CrowdinSDK.currentLocalization, "de")
}
}
6 changes: 5 additions & 1 deletion Tests/UnitTests/MockLocalizationProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ class MockLocalizationProvider: LocalizationProviderProtocol {

func refreshLocalization() {}
func refreshLocalization(completion: @escaping ((Error?) -> Void)) {}
func setLocalization(_ localization: String, completion: @escaping ((Error?) -> Void)) {
self.localization = localization
completion(nil)
}
func prepare(with completion: @escaping () -> Void) {}
func deintegrate() {}

Expand Down Expand Up @@ -63,4 +67,4 @@ class MockRemoteStorage: RemoteLocalizationStorageProtocol {
func fetchData(completion: ([String]?, String, [String : String]?, [AnyHashable : Any]?) -> Void, errorHandler: ((Error) -> Void)?) {}
func prepare(with completion: @escaping () -> Void) { completion() }
func deintegrate() {}
}
}
13 changes: 13 additions & 0 deletions website/docs/setup.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,19 @@ if let currentLocale = CrowdinSDK.currentLocalization {

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.

If you need to update UI only after the localization refresh finishes, use the completion-based API:

```swift
CrowdinSDK.setCurrentLocalization("<language_code>") { error in
if let error = error {
print("Localization switch failed: \(error.localizedDescription)")
return
}

// Reload your UI here.
}
```


:::caution
The UI doesn't update automatically. You must manually update the UI after changing the localization.
Expand Down
Loading