diff --git a/CHANGELOG.md b/CHANGELOG.md index d365069..402d49c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,3 +11,10 @@ *February 24, 2021* - Fixed preferred locale provider + +## Transifex iOS SDK 0.1.2 + +*March 17, 2021* + +- Applies minor refactors in cache logic +- Renames override policy to update policy and improves documentation. diff --git a/README.md b/README.md index 6b60ac4..c567cd8 100644 --- a/README.md +++ b/README.md @@ -180,9 +180,9 @@ get notified asynchronously whether the request was successful or not. ### Standard Cache The default cache strategy used by the SDK, if no other cache is provided by the -developer, is the `TXStandardCache`. The standard cache operates by making use of the -publicly exposed classes and protocols from the Cache.swift file of the SDK, so it's easy -to construct another cache strategy if that's desired. +developer, is the `TXStandardCache.getCache()`. The standard cache operates +by making use of the publicly exposed classes and protocols from the Cache.swift file of the +SDK, so it's easy to construct another cache strategy if that's desired. The standard cache is initialized with a memory cache (`TXMemoryCache`) that manages all cached entries in memory. After the memory cache gets initialized, it tries to look up if @@ -196,9 +196,9 @@ by the developer. (using the optional app group identifier argument if provided), in case the app had already downloaded the translations from the server from a previous launch. -Those two providers are used to initialize the memory cache using an override policy -(`TXCacheOverridePolicy`) which is optionally provided by the developer and defaults to -the `overrideAll` value. +Those two providers are used to initialize the memory cache using an update policy +(`TXCacheUpdatePolicy`) which is optionally provided by the developer and defaults to +the `replaceAll` value. After the cached entries have updated the memory cache, the cache is ready to be used. @@ -210,28 +210,21 @@ they are available on the next app launch. #### Alternative cache strategy You might want to update the internal memory cache as soon as the newly downloaded -translations are available and always override all entries, so that the override policy can +translations are available and always update all entries, so that the update policy can also be ommited. -In order to achieve that, you can create a new `TXDecoratorCache` subclass that has a -similar initializer as the `TXStandardCache` one, with the exception of the -`TXReadonlyCacheDecorator` and the `TXStringOverrideFilterCache` initializers: - - ```swift - public init(groupIdentifier: String? = nil) { - // ...Same as TXStandardCache... - - let cache = TXFileOutputCacheDecorator( - fileURL: downloadURL, - internalCache: TXProviderBasedCache( - providers: providers, - internalCache: TXMemoryCache() - ) - ) - - super.init(internalCache: cache) - } - ``` +In order to achieve that, you can create a new `TXDecoratorCache` subclass or create a +method that returns a `TXCache` instance, just like in the `TXStandardCache.getCache()` +case. + +```swift +func getCustomCache() -> TXCache { + return TXFileOutputCacheDecorator( + fileURL: ..., + internalCache: ... + ) +} +``` This way, whenever the cache is updated with the new translations from the `fetchTranslations()` method, the `update()` call will propagate to the internal diff --git a/Sources/Transifex/Cache.swift b/Sources/Transifex/Cache.swift index 0696c12..e2a08a4 100644 --- a/Sources/Transifex/Cache.swift +++ b/Sources/Transifex/Cache.swift @@ -23,23 +23,64 @@ public typealias TXLocaleStrings = [String: TXStringInfo] /// Format: {locale code} : {TXLocaleStrings} public typealias TXTranslations = [String: TXLocaleStrings] -/// Overriding policy `TXStringOverrideFilterCache` decorator class so that any translations fed by -/// the `update()` method of the `TXCache` protocol are updated using one of the following policies. +extension String { + /// Given an optional translated string, returns whether that string contains a translation or not. + /// + /// In order for a string to be considered a translation, it has to be not nil and not being an empty string. + /// + /// - Parameter string: The string to be checked + /// - Returns: True if the string contains a translation, False otherwise. + static func containsTranslation(_ string: String?) -> Bool { + guard let string = string else { + return false + } + + return string.count > 0 + } +} + +/// Update policy that specifies the way that the internal cache is updated with new translations. +/// +/// You can find an easy to understand table containing a number of cases and how each policy updates the +/// cache below: +/// +///``` +/// | Key || Cache | New || Replace All | Update using Translated | +/// |-----||-------|------||---------------|--------------------------------| +/// | a || "a" | - || - | "a" | +/// | b || "b" | "B" || "B" | "B" | +/// | c || "c" | "" || "" | "c" | +/// | d || "" | - || - | "" | +/// | e || "" | "E" || "E" | "E" | +/// | f || - | "F" || "F" | "F" | +/// | g || - | "" || "" | - | +///``` +/// +/// Here's an example on how to read the table above: +/// +/// * Given a string with `key="c"` +/// * and a cache that has `"c"` as the stored value for this key (`"c" -> "c"`) +/// * if an empty translation arrives for this string (`""`) +/// * if policy is `.replaceAll`, then the cache will be updated so that (`"c" -> "")` +/// * in contrast to that, if policy is `.updateUsingTranslated`, then the cache will stay as is +/// (`"c" -> "c"`), because the new translation is empty. +/// +/// A `"-"` value means that the respective key does not exist. For example: +/// +/// * Given a string with `key="f"` +/// * and a cache that has no entry with `"f"` as a key +/// * if a translation arrives for this string (`"f" -> "F"`) +/// * if policy is `.replaceAll`, then the cache will be updated by adding a new entry so that +/// (`"f" -> "F"`) +/// * if policy is `.updateUsingTranslated`, then the same will happen, since the new translation +/// is not empty @objc -public enum TXCacheOverridePolicy : Int { - /// All of the cache entries are replaced with the new translations. - /// Empty cache entries from the new translations are filtered out. - case overrideAll - /// All new translations are added to the cache, either updating existing translations or adding - /// new ones. - /// If a translation is not found in cache but exists in the new translations, it's not added. - /// If a translation is found in cache but doesn't exist in the new translations, it's left untouched. - /// Empty cache entries from the new translations are filtered out. - case overrideUsingTranslatedOnly - /// Only the translations not existing in the cache are updated. - /// If a translation is found in the cache but not in the new translations, it's left untouched. - /// Empty cache entries from the new translations are filtered out. - case overrideUntranslatedOnly +public enum TXCacheUpdatePolicy : Int { + /// Discards the existing cache entries completely and populates the cache with the new entries, + /// even if they contain empty translations. + case replaceAll + /// Updates the existing cache with the new entries that have a non-empty translation, ignoring the rest. + case updateUsingTranslated } /// A protocol for classes that act as cache for translations. @@ -90,7 +131,7 @@ public final class TXDiskCacheProvider: NSObject, TXCacheProvider { /// The translations extracted from disk after initialization. public let translations: TXTranslations? - /// Initializes the disk cache provider with a file URL from disk. + /// Initializes the disk cache provider with a file URL from disk synchronously. /// /// The disk cache provider expects the file to be encoded in JSON format using the `TXTranslations` /// data structure. @@ -297,97 +338,56 @@ public final class TXProviderBasedCache: TXDecoratorCache { } } -/// Class responsible for updating the passed internalCache using a certain override policy defined -/// in the `TXCacheOverridePolicy` enum. This is done by filtering any translations that are passed -/// via the `update(translations:)` call using an override policy that checks both the passed +/// Class responsible for updating the passed internalCache using a certain update policy defined +/// in the `TXCacheUpdatePolicy` enum. This is done by filtering any translations that are passed +/// via the `update(translations:)` call using an update policy that checks both the passed /// translations and the internal cache state to decide whether a translation should update the internal cache /// or not. -public final class TXStringOverrideFilterCache: TXDecoratorCache { - let policy: TXCacheOverridePolicy +public final class TXStringUpdateFilterCache: TXDecoratorCache { + let policy: TXCacheUpdatePolicy - /// Initializes the cache with a certain override policy and an internal cache that will be updated + /// Initializes the cache with a certain update policy and an internal cache that will be updated /// according to that policy. /// /// - Parameters: - /// - policy: The override policy to be used - /// - internalCache: The internal cache to be updated with the specified override policy + /// - policy: The update policy to be used + /// - internalCache: The internal cache to be updated with the specified update policy @objc - public init(policy: TXCacheOverridePolicy, + public init(policy: TXCacheUpdatePolicy, internalCache: TXCache) { self.policy = policy super.init(internalCache: internalCache) } - /// Updates the internal cache with the provided translations using the override policy specified during + /// Updates the internal cache with the provided translations using the update policy specified during /// initialization. /// /// - Parameter translations: The provided translations override public func update(translations: TXTranslations) { - let filteredTranslations = filterEmptyTranslations(translations) - - if policy == .overrideAll { - super.update(translations: filteredTranslations) + if policy == .replaceAll { + super.update(translations: translations) return } var updatedTranslations = self.get() - for (localeCode, localeTranslations) in filteredTranslations { + for (localeCode, localeTranslations) in translations { for (stringKey, translation) in localeTranslations { - // Make sure that the new translation has a value and it's not - // an empty string. - guard let translatedString = translation[TXDecoratorCache.STRING_KEY], - translatedString.count > 0 else { + /// Make sure that the new entry contains a translation, otherwise don't process it. + guard String.containsTranslation(translation[TXDecoratorCache.STRING_KEY]) == true else { continue } - - // If the policy is set to override untranslated only, then - // update the cache only if there's no existing translation - // for that stringKey. - if (policy == .overrideUntranslatedOnly - && self.get(key: stringKey, - localeCode: localeCode) == nil) - || - // If the policy is set to override using translated only, - // then always update the cache. - policy == .overrideUsingTranslatedOnly { - if updatedTranslations[localeCode] == nil { - updatedTranslations[localeCode] = [:] - } - updatedTranslations[localeCode]?[stringKey] = translation + if updatedTranslations[localeCode] == nil { + updatedTranslations[localeCode] = [:] } + + updatedTranslations[localeCode]?[stringKey] = translation } } super.update(translations: updatedTranslations) } - - /// Filters out any empty translations from the provided TXTranslations structure. - /// - /// - Parameter translations: The provided TXTranslations structure that may include empty - /// translations - /// - Returns: The filtered TXTranslations structured that contains no empty translations - private func filterEmptyTranslations(_ translations: TXTranslations) -> TXTranslations { - /// Copy the provided translations to the final structure - var filteredTranslations = translations - - /// Remove the entries from the final structure that contain empty translations - for (localeCode, localeTranslations) in translations { - for (stringKey, translation) in localeTranslations { - // Make sure that the new translation has a value and it's not - // an empty string. - if let translatedString = translation[TXDecoratorCache.STRING_KEY], - translatedString.count > 0 { - continue - } - - filteredTranslations[localeCode]?.removeValue(forKey: stringKey) - } - } - - return filteredTranslations - } } /// The standard cache that the TXNative SDK is initialized with, if no other cache is provided. @@ -396,18 +396,17 @@ public final class TXStringOverrideFilterCache: TXDecoratorCache { /// reads from any existing translation files either from the app bundle or the app sandbox. The cache is also /// responsible for creating or updating the sandbox file with new translations when they will become available /// and it offers a memory cache for retrieving such translations so that they can be displayed in the UI. -public final class TXStandardCache: TXDecoratorCache { - /// Initializes the cache using a specific override policy and an optional group identifier based on the - /// architecture of the application using the SDK. +public final class TXStandardCache: NSObject { + /// Initializes and returns the cache using a specific update policy and an optional group identifier based + /// on the architecture of the application that uses the SDK. /// /// - Parameters: - /// - overridePolicy: The specific override policy to be used when updating the internal - /// memory cache with the stored contents from disk. Defaults to .overrideAll. + /// - updatePolicy: The specific update policy to be used when updating the internal + /// memory cache with the stored contents from disk. Defaults to .replaceAll. /// - groupIdentifier: The group identifier of the app, if the app makes use of the app groups /// entitlement. Defaults to nil. - @objc - public init(overridePolicy: TXCacheOverridePolicy = .overrideAll, - groupIdentifier: String? = nil) { + public static func getCache(updatePolicy: TXCacheUpdatePolicy = .replaceAll, + groupIdentifier: String? = nil) -> TXCache { var providers: [TXCacheProvider] = [] if let bundleURL = TXStandardCache.bundleURL() { @@ -422,20 +421,18 @@ public final class TXStandardCache: TXDecoratorCache { providers.append(TXDiskCacheProvider(fileURL: downloadURL)) } - let cache = TXFileOutputCacheDecorator( + return TXFileOutputCacheDecorator( fileURL: downloadURL, internalCache: TXReadonlyCacheDecorator( internalCache: TXProviderBasedCache( providers: providers, - internalCache: TXStringOverrideFilterCache( - policy: overridePolicy, + internalCache: TXStringUpdateFilterCache( + policy: updatePolicy, internalCache: TXMemoryCache() ) ) ) ) - - super.init(internalCache: cache) } /// Constructs the file URL of the translations file found in the main bundle of the app. The method diff --git a/Sources/Transifex/Core.swift b/Sources/Transifex/Core.swift index adc3f94..70ea719 100644 --- a/Sources/Transifex/Core.swift +++ b/Sources/Transifex/Core.swift @@ -129,7 +129,7 @@ class NativeCore : TranslationProvider { cdsHost: cdsHost, session: session ) - self.cache = cache ?? TXStandardCache() + self.cache = cache ?? TXStandardCache.getCache() self.missingPolicy = missingPolicy ?? TXSourceStringPolicy() self.errorPolicy = errorPolicy ?? TXRenderedSourceErrorPolicy() self.renderingStrategy = renderingStrategy @@ -194,7 +194,7 @@ class NativeCore : TranslationProvider { /// If this call call originates from a `localizedStringWithFormat` swizzled method, it will /// contain the extra arguments. In that case the first argument of those methods (format) would /// have already been resolved by a `NSLocalizedString()` call, so we should not perform - /// a second lookup on the cache, we can proceed by directly rendering the strict and let the + /// a second lookup on the cache, we can proceed by directly rendering the string and let the /// `render()` method extract the ICU plurals. if params[Swizzler.PARAM_ARGUMENTS_KEY] != nil { return render(sourceString: sourceString, @@ -240,7 +240,7 @@ class NativeCore : TranslationProvider { context: context) translationTemplate = cache.get(key: key, localeCode: localeToRender) - if translationTemplate == nil { + if !String.containsTranslation(translationTemplate) { let bypassedString = self.bypassLocalizer.get(sourceString: sourceString, params: params) @@ -300,7 +300,7 @@ Error rendering source string '\(sourceString)' with string to render '\(stringT /// 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 = "0.1.1" + internal static let version = "0.1.2" /// 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/Tests/TransifexTests/TransifexTests.swift b/Tests/TransifexTests/TransifexTests.swift index d60e0e9..89d1af7 100644 --- a/Tests/TransifexTests/TransifexTests.swift +++ b/Tests/TransifexTests/TransifexTests.swift @@ -301,114 +301,123 @@ final class TransifexTests: XCTestCase { } } - func testOverrideFilterCacheAll() { - let firstProviderTranslations: TXTranslations = [ + func testReplaceAllPolicy() { + let existingTranslations: TXTranslations = [ "en": [ - "key1": [ "string": "localized string 1" ] + "a": [ "string": "a" ], + "b": [ "string": "b" ], + "c": [ "string": "c" ], + "d": [ "string": "" ], + "e": [ "string": "" ] ] ] - let secondProviderTranslations: TXTranslations = [ + + let memoryCache = TXMemoryCache() + memoryCache.update(translations: existingTranslations) + + let newTranslations: TXTranslations = [ "en": [ - "key2": [ "string": "localized string 2" ], - "key3": [ "string": "" ] + "b": [ "string": "B" ], + "c": [ "string": "" ], + "e": [ "string": "E" ], + "f": [ "string": "F" ], + "g": [ "string": "" ] ] ] - - let firstProvider = MockCacheProvider(translations: firstProviderTranslations) - let secondProvider = MockCacheProvider(translations: secondProviderTranslations) - + let provider = MockCacheProvider(translations: newTranslations) + let cache = TXProviderBasedCache( - providers: [ - firstProvider, - secondProvider - ], - internalCache: TXStringOverrideFilterCache( - policy: .overrideAll, - internalCache: TXMemoryCache() + providers: [ provider ], + internalCache: TXStringUpdateFilterCache( + policy: .replaceAll, + internalCache: memoryCache ) ) - XCTAssertNil(cache.get(key: "key1", localeCode: "en")) - XCTAssertNotNil(cache.get(key: "key2", localeCode: "en")) - XCTAssertNil(cache.get(key: "key3", localeCode: "en")) + XCTAssertNil(cache.get(key: "a", localeCode: "en")) + XCTAssertEqual(cache.get(key: "b", localeCode: "en"), "B") + XCTAssertEqual(cache.get(key: "c", localeCode: "en"), "") + XCTAssertNil(cache.get(key: "d", localeCode: "en")) + XCTAssertEqual(cache.get(key: "e", localeCode: "en"), "E") + XCTAssertEqual(cache.get(key: "f", localeCode: "en"), "F") + XCTAssertEqual(cache.get(key: "g", localeCode: "en"), "") } - func testOverrideFilterCacheUntranslated() { - let firstProviderTranslations: TXTranslations = [ + func testUpdateUsingTranslatePolicy() { + let existingTranslations: TXTranslations = [ "en": [ - "key1": [ "string": "localized string 1" ] + "a": [ "string": "a" ], + "b": [ "string": "b" ], + "c": [ "string": "c" ], + "d": [ "string": "" ], + "e": [ "string": "" ] ] ] - let secondProviderTranslations: TXTranslations = [ - "en": [ - "key2": [ "string": "localized string 2" ], - "key3": [ "string": "" ] - ] - ] - - let firstProvider = MockCacheProvider(translations: firstProviderTranslations) - let secondProvider = MockCacheProvider(translations: secondProviderTranslations) let memoryCache = TXMemoryCache() - memoryCache.update(translations: [ + memoryCache.update(translations: existingTranslations) + + let newTranslations: TXTranslations = [ "en": [ - "key1": [ "string": "old localized string 1"] + "b": [ "string": "B" ], + "c": [ "string": "" ], + "e": [ "string": "E" ], + "f": [ "string": "F" ], + "g": [ "string": "" ] ] - ]) - + ] + let provider = MockCacheProvider(translations: newTranslations) + let cache = TXProviderBasedCache( - providers: [ - firstProvider, - secondProvider - ], - internalCache: TXStringOverrideFilterCache( - policy: .overrideUntranslatedOnly, + providers: [ provider ], + internalCache: TXStringUpdateFilterCache( + policy: .updateUsingTranslated, internalCache: memoryCache ) ) - XCTAssertNotNil(cache.get(key: "key1", localeCode: "en")) - XCTAssertTrue(cache.get(key: "key1", localeCode: "en") == "old localized string 1") - XCTAssertNotNil(cache.get(key: "key2", localeCode: "en")) - XCTAssertNil(cache.get(key: "key3", localeCode: "en")) + XCTAssertEqual(cache.get(key: "a", localeCode: "en"), "a") + XCTAssertEqual(cache.get(key: "b", localeCode: "en"), "B") + XCTAssertEqual(cache.get(key: "c", localeCode: "en"), "c") + XCTAssertEqual(cache.get(key: "d", localeCode: "en"), "") + XCTAssertEqual(cache.get(key: "e", localeCode: "en"), "E") + XCTAssertEqual(cache.get(key: "f", localeCode: "en"), "F") + XCTAssertNil(cache.get(key: "g", localeCode: "en")) } - func testOverrideFilterCacheTranslated() { - let firstProviderTranslations: TXTranslations = [ + func testReadOnlyCache() { + let existingTranslations: TXTranslations = [ "en": [ - "key1": [ "string": "localized string 1" ] + "a": [ "string": "a" ], + "b": [ "string": "b" ], + "c": [ "string": "c" ], + "d": [ "string": "" ], + "e": [ "string": "" ] ] ] - let secondProviderTranslations: TXTranslations = [ - "en": [ - "key2": [ "string": "localized string 2" ] - ] - ] - - let firstProvider = MockCacheProvider(translations: firstProviderTranslations) - let secondProvider = MockCacheProvider(translations: secondProviderTranslations) let memoryCache = TXMemoryCache() - memoryCache.update(translations: [ + memoryCache.update(translations: existingTranslations) + + let newTranslations: TXTranslations = [ "en": [ - "key1": [ "string": "old localized string 1"] + "b": [ "string": "B" ], + "c": [ "string": "" ], + "e": [ "string": "E" ], + "f": [ "string": "F" ] ] - ]) - + ] + let provider = MockCacheProvider(translations: newTranslations) + let cache = TXProviderBasedCache( - providers: [ - firstProvider, - secondProvider - ], - internalCache: TXStringOverrideFilterCache( - policy: .overrideUsingTranslatedOnly, - internalCache: memoryCache + providers: [ provider ], + internalCache: TXStringUpdateFilterCache( + policy: .updateUsingTranslated, + internalCache: TXReadonlyCacheDecorator(internalCache: memoryCache) ) ) - XCTAssertNotNil(cache.get(key: "key1", localeCode: "en")) - XCTAssertTrue(cache.get(key: "key1", localeCode: "en") == "localized string 1") - XCTAssertNotNil(cache.get(key: "key2", localeCode: "en")) + XCTAssertEqual(cache.get(), existingTranslations) } func testPlatformStrategyWithInvalidSourceString() { @@ -472,13 +481,13 @@ final class TransifexTests: XCTestCase { ("testEncodingSourceStringMeta", testEncodingSourceStringMeta), ("testEncodingSourceString", testEncodingSourceString), ("testEncodingSourceStringWithMeta", testEncodingSourceStringWithMeta), + ("testExtractICUPlurals", testExtractICUPlurals), ("testFetchTranslations", testFetchTranslations), ("testFetchTranslationsNotReady", testFetchTranslationsNotReady), - ("testExtractICUPlurals", testExtractICUPlurals), ("testPushTranslations", testPushTranslations), - ("testOverrideFilterCacheAll", testOverrideFilterCacheAll), - ("testOverrideFilterCacheUntranslated", testOverrideFilterCacheUntranslated), - ("testOverrideFilterCacheTranslated", testOverrideFilterCacheTranslated), + ("testReplaceAllPolicy", testReplaceAllPolicy), + ("testUpdateUsingTranslatePolicy", testUpdateUsingTranslatePolicy), + ("testReadOnlyCache", testReadOnlyCache), ("testPlatformStrategyWithInvalidSourceString", testPlatformStrategyWithInvalidSourceString), ("testErrorPolicy", testErrorPolicy), ("testCurrentLocale", testCurrentLocale),