Skip to content

Commit

Permalink
Merge pull request #20 from transifex/stelios/fix/cache-policy
Browse files Browse the repository at this point in the history
Refactor cache policies
  • Loading branch information
Dimitrios Bendilas authored Mar 19, 2021
2 parents adb327d + a3e162a commit db39894
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 195 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
45 changes: 19 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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
Expand Down
177 changes: 87 additions & 90 deletions Sources/Transifex/Cache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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() {
Expand All @@ -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
Expand Down
8 changes: 4 additions & 4 deletions Sources/Transifex/Core.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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"
Expand Down
Loading

0 comments on commit db39894

Please sign in to comment.