diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index c4be241..5754633 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -9,10 +9,10 @@ on: jobs: build: - runs-on: macos-latest + runs-on: macos-12 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Build run: swift build -v - name: Run tests diff --git a/CHANGELOG.md b/CHANGELOG.md index e188d91..c1124ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,3 +68,11 @@ key if the entry is not found. - Adds method to activate SDK from a Swift Package. - Adds reference to SwiftUI limitation in README. + +## Transifex iOS SDK 1.0.3 + +*December 27, 2022* + +- Fixes TXPreferredLocaleProvider so that it uses the correct language candidate +based on user's preference and supported languages by the app developer. +- Fixes deprecation warnings on Github action. diff --git a/Sources/Transifex/Core.swift b/Sources/Transifex/Core.swift index 43ad9d5..7a03e49 100644 --- a/Sources/Transifex/Core.swift +++ b/Sources/Transifex/Core.swift @@ -334,7 +334,7 @@ render '\(stringToRender)' locale code: \(localeCode) params: \(params). Error: /// A static class that is the main point of entry for all the functionality of Transifex Native throughout the SDK. public final class TXNative : NSObject { /// The SDK version - internal static let version = "1.0.2" + internal static let version = "1.0.3" /// The filename of the file that holds the translated strings and it's bundled inside the app. public static let STRINGS_FILENAME = "txstrings.json" diff --git a/Sources/Transifex/Locales.swift b/Sources/Transifex/Locales.swift index 493433d..4b6af53 100644 --- a/Sources/Transifex/Locales.swift +++ b/Sources/Transifex/Locales.swift @@ -17,15 +17,23 @@ public protocol TXCurrentLocaleProvider { func currentLocale() -> String } -/// Class that returns the language code of the current user's locale and falls back to "en" if the language -/// code cannot be found. +/// Class that returns the active language code that can be used for localization, based on the current user's +/// language preferences and the locales that the application supports. The class falls back to "en" if the +/// language code cannot be found. public final class TXPreferredLocaleProvider : NSObject { + private static let FALLBACK_LOCALE = "en" + private var _appLocales : [String] private var _currentLocale : String - override init() { + /// Designated initializer. + /// + /// - Parameter appLocales: The locales the the application supports. + init(_ appLocales: [String]) { + _appLocales = appLocales + // Fetch the current locale on initialization and return it when it's // requested by the `currentLocale()` method. - _currentLocale = TXPreferredLocaleProvider.getCurrentLocale() + _currentLocale = Self.getCurrentLocale(_appLocales) super.init() @@ -38,18 +46,33 @@ public final class TXPreferredLocaleProvider : NSObject { @objc private func currentLocaleDidChange() { - _currentLocale = TXPreferredLocaleProvider.getCurrentLocale() + _currentLocale = Self.getCurrentLocale(_appLocales) } private static func getPreferredLocale() -> Locale { - guard let preferredIdentifier = Locale.preferredLanguages.first else { + if let preferredLanguage = Locale.preferredLanguages.first { + return Locale(identifier: preferredLanguage) + } + else { return Locale.autoupdatingCurrent } - return Locale(identifier: preferredIdentifier) } - private static func getCurrentLocale() -> String { - return getPreferredLocale().languageCode ?? "en" + private static func getCurrentLocale(_ appLocales: [String]) -> String { + // Attempt to get the most preferred locale based on: + // * the supported app locales provided by the developer when + // initializing the TXNative instance. + // * the preferred languages set by the user in the device Settings. + if let preferredLocalization = Bundle.preferredLocalizations(from: appLocales, + forPreferences: Locale.preferredLanguages).first { + return preferredLocalization + } + // Although it's highly unlikely that a preferred localization will not + // be extracted using the preferredLocalizations() call, we have a + // number of fallbacks in place to locate the best candidate. + else { + return getPreferredLocale().languageCode ?? FALLBACK_LOCALE + } } } @@ -113,13 +136,19 @@ public final class TXLocaleState : NSObject { // Make sure we filter all duplicate values // by converting the array to a Set. var distinctAppLocales = Set(appLocales) - // Insert the source locale in case it hasn't been added. - distinctAppLocales.insert(sourceLocale) - self.appLocales = Array(distinctAppLocales) + // Remove the source locale (if it has been already added by the + // developer). + distinctAppLocales.remove(sourceLocale) - self.translatedLocales = self.appLocales.filter { $0 != sourceLocale } + self.translatedLocales = Array(distinctAppLocales) + + self.appLocales = Array(distinctAppLocales) + // Add the source locale as the first element of the array, as the + // `TXPreferredLocaleProvider` (and `Bundle.preferredLocalizations` more + // specifically) uses the first element as a fallback. + self.appLocales.insert(sourceLocale, at: 0) - self.currentLocaleProvider = currentLocaleProvider ?? TXPreferredLocaleProvider() + self.currentLocaleProvider = currentLocaleProvider ?? TXPreferredLocaleProvider(self.appLocales) } /// Returns true if the given locale is the source locale, false otherwise. diff --git a/Tests/TransifexTests/TransifexTests.swift b/Tests/TransifexTests/TransifexTests.swift index cd46d73..b21c9ca 100644 --- a/Tests/TransifexTests/TransifexTests.swift +++ b/Tests/TransifexTests/TransifexTests.swift @@ -572,22 +572,47 @@ final class TransifexTests: XCTestCase { XCTAssertEqual(result, "ERROR") } - func testCurrentLocale() { + func testCurrentLocaleNotFirstPreference() { let appleLanguagesKey = "AppleLanguages" let storedLanguages = UserDefaults.standard.value(forKey: appleLanguagesKey) - UserDefaults.standard.set([ "el" ], + UserDefaults.standard.set([ "nl", "fr" ], forKey: appleLanguagesKey) - let locale = TXLocaleState(appLocales: []) + let locale = TXLocaleState(sourceLocale: "en", + appLocales: [ "fr", "de", "es", "it"]) XCTAssertEqual(locale.currentLocale, - "el") + "fr") UserDefaults.standard.set(storedLanguages, forKey: appleLanguagesKey) } + func testCurrentLocaleNotAnyPreference() { + let appleLanguagesKey = "AppleLanguages" + let storedLanguages = UserDefaults.standard.value(forKey: appleLanguagesKey) + + UserDefaults.standard.set([ "nl", "fr" ], + forKey: appleLanguagesKey) + + let locale = TXLocaleState(sourceLocale: "en", + appLocales: [ "de", "es", "it"]) + + XCTAssertEqual(locale.currentLocale, + "en") + + UserDefaults.standard.set(storedLanguages, + forKey: appleLanguagesKey) + } + + func testSourceLocalePosition() { + let locale = TXLocaleState(sourceLocale: "en", + appLocales: [ "fr", "el" ]) + + XCTAssertTrue(locale.appLocales.first == "en") + } + func testTranslateWithSourceStringsInCache() { let sourceLocale = "en" let localeState = TXLocaleState(sourceLocale: sourceLocale, @@ -661,7 +686,9 @@ final class TransifexTests: XCTestCase { ("testPlatformStrategyWithInvalidSourceString", testPlatformStrategyWithInvalidSourceString), ("testErrorPolicy", testErrorPolicy), ("testErrorPolicyException", testErrorPolicyException), - ("testCurrentLocale", testCurrentLocale), + ("testCurrentLocaleNotFirstPreference", testCurrentLocaleNotFirstPreference), + ("testCurrentLocaleNotAnyPreference", testCurrentLocaleNotAnyPreference), + ("testSourceLocalePosition", testSourceLocalePosition), ("testTranslateWithSourceStringsInCache", testTranslateWithSourceStringsInCache), ] }