Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release: 0.1.4 #32

Closed
wants to merge 8 commits into from
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,9 @@
*March 22, 2021*

- Fixes a minor version issue

## Transifex iOS SDK 0.1.4

*March 24, 2021*

- Exposes `TXStandardCache.getCache` method in Objective-C.
33 changes: 32 additions & 1 deletion Sources/Transifex/CDSHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ class CDSHandler {

private static let CONTENT_ENDPOINT = "content"
private static let INVALIDATE_ENDPOINT = "invalidate"
private static let FILTER_TAGS_PARAM = "filter[tags]"

fileprivate static let HTTP_STATUS_CODE_OK = 200
fileprivate static let HTTP_STATUS_CODE_ACCEPTED = 202
Expand Down Expand Up @@ -212,8 +213,10 @@ class CDSHandler {
/// - Parameters:
/// - localeCode: an optional locale to fetch translations from; if none provided it will fetch
/// translations for all locales defined in the configuration
/// - tags: An optional list of tags so that only strings that have all of the given tags are fetched.
/// - completionHandler: a callback function to call when the operation is complete
public func fetchTranslations(localeCode: String? = nil,
tags: [String]? = nil,
completionHandler: @escaping TXPullCompletionHandler) {
guard let cdsHostURL = URL(string: cdsHost) else {
Logger.error("Error: Invalid CDS host URL: \(cdsHost)")
Expand Down Expand Up @@ -244,7 +247,8 @@ class CDSHandler {

for code in fetchLocaleCodes {
let url = baseURL.appendingPathComponent(code)
var request = URLRequest(url: url)
var request = buildURLRequest(url: url,
tags: tags)
request.allHTTPHeaderFields = getHeaders(withSecret: false)
requestsByLocale[code] = request
}
Expand Down Expand Up @@ -482,4 +486,31 @@ Source:
}
return headers
}

/// Builds the URL request that is going to be used to query CDS using the optional tags list
///
/// - Parameters:
/// - url: The initial URL
/// - tags: The optional tag list
/// - Returns: The final URL request to be used to query CDS
private func buildURLRequest(url: URL,
tags: [String]?) -> URLRequest {
guard let tags = tags,
tags.count > 0,
var components = URLComponents(url: url,
resolvingAgainstBaseURL: false) else {
return URLRequest(url: url)
}

let tagList = tags.joined(separator: ",")
let queryItem = URLQueryItem(name: CDSHandler.FILTER_TAGS_PARAM,
value: tagList)
components.queryItems = [ queryItem ]

guard let tagRequestURL = components.url else {
return URLRequest(url: url)
}

return URLRequest(url: tagRequestURL)
}
}
1 change: 1 addition & 0 deletions Sources/Transifex/Cache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,7 @@ public final class TXStandardCache: NSObject {
/// 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 static func getCache(updatePolicy: TXCacheUpdatePolicy = .replaceAll,
groupIdentifier: String? = nil) -> TXCache {
var providers: [TXCacheProvider] = []
Expand Down
72 changes: 46 additions & 26 deletions Sources/Transifex/Core.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,8 @@ public enum TXRenderingStategy : Int {
/// If the string is not found the logic returns the value of the `params` dictionary and if this is
/// also not found, it returns the `sourceString`.
///
/// This logic is used when the app needs to access its source localization or when a string for the
/// requested localization is not found and the missing policy is about to be called. Due to the fact
/// that CDS doesn't provide a way to download the source localizations, the SDK needs to look into
/// the bundled translations for the source locale and use those translations instead.
/// This logic is used when a string for the requested localization is not found and the missing policy
/// is about to be called.
final class BypassLocalizer {
let bundle : Bundle?

Expand Down Expand Up @@ -123,7 +121,7 @@ class NativeCore : TranslationProvider {
) {
self.locales = locales
self.cdsHandler = CDSHandler(
localeCodes: self.locales.translatedLocales,
localeCodes: self.locales.appLocales,
token: token,
secret: secret,
cdsHost: cdsHost,
Expand All @@ -142,9 +140,11 @@ class NativeCore : TranslationProvider {
///
/// - Parameter localeCode: an optional locale to fetch translations from; if none provided, it
/// will fetch translations for all locales defined in the configuration
/// - Parameter tags: An optional list of tags so that only strings that have all of the given tags are fetched.
/// - Parameter completionHandler: The completion handler that informs the caller with the
/// new translations and a list of possible errors that might have occured
func fetchTranslations(_ localeCode: String? = nil,
tags: [String]? = nil,
completionHandler: TXPullCompletionHandler? = nil) {
cdsHandler.fetchTranslations(localeCode: localeCode) { (translations, errors) in
if errors.count > 0 {
Expand Down Expand Up @@ -224,36 +224,45 @@ class NativeCore : TranslationProvider {
params: [String: Any],
context: String?) -> String {
var translationTemplate: String?
let localeToRender = localeCode ?? self.locales.currentLocale
let isSource = self.locales.isSource(localeToRender)
let localeToRender = localeCode ?? locales.currentLocale
let key = txGenerateKey(sourceString: sourceString,
context: context)

translationTemplate = cache.get(key: key,
localeCode: localeToRender)

/// If the source locale is requested, or if the source string is missing from cache,
/// the bypass localizer is used, to look up on the application bundle and fetch the
/// localized content for the source locale (if found) by bypassing swizzling.
var applyMissingPolicy = false

if isSource {
translationTemplate = self.bypassLocalizer.get(sourceString: sourceString,
params: params)
}
else {
let key = txGenerateKey(sourceString: sourceString,
context: context)
translationTemplate = cache.get(key: key,
localeCode: localeToRender)
if !String.containsTranslation(translationTemplate) {
let bypassedString = self.bypassLocalizer.get(sourceString: sourceString,
params: params)
return missingPolicy.get(sourceString: bypassedString)
/// If the string is not found in the cache, use the bypass localizer to look it up on the
/// application bundle, which returns either the bundled translation if found, or the provided
/// source string.
if !String.containsTranslation(translationTemplate) {
translationTemplate = bypassLocalizer.get(sourceString: sourceString,
params: params)

/// For source locale, we treat the return value of the bypass localizer as the ground truth
/// and we use the value to render the final string.
///
/// For target locales, we do the same, with the exception that we pass the final rendered
/// string from the missing policy to inform the user that this string is missing.
if !locales.isSource(localeToRender) {
applyMissingPolicy = true
}
}

return render(
let renderedString = render(
sourceString: sourceString,
stringToRender: translationTemplate,
localeCode: localeToRender,
params: params
)

if applyMissingPolicy {
return missingPolicy.get(sourceString: renderedString)
}
else {
return renderedString
}
}

/// Renders the translation to the current format, taking into account any variable placeholders.
Expand Down Expand Up @@ -300,7 +309,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.3"
internal static let version = "0.1.4"

/// 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 All @@ -318,6 +327,9 @@ public final class TXNative : NSObject {

/// Designated initializer of the TXNative SDK.
///
/// Do not call initialize() twice without calling dispose() first to deconstruct the previous singleton
/// instance.
///
/// - Parameters:
/// - locales: keeps track of the available and current locales
/// - token: the Transifex token that can be used for retrieving translations from CDS
Expand Down Expand Up @@ -428,10 +440,12 @@ token: \(token)
///
/// - Parameter localeCode: if not provided, it will fetch translations for all locales defined in the
/// app configuration.
/// - Parameter tags: An optional list of tags so that only strings that have all of the given tags are fetched.
/// - Parameter completionHandler: The completion handler that informs the caller with the
/// new translations and a list of possible errors that might have occured
@objc
public static func fetchTranslations(_ localeCode: String? = nil,
tags: [String]? = nil,
completionHandler: TXPullCompletionHandler? = nil) {
tx?.fetchTranslations(localeCode,
completionHandler: completionHandler)
Expand Down Expand Up @@ -462,4 +476,10 @@ token: \(token)
public static func forceCacheInvalidation(completionHandler: @escaping (Bool) -> Void) {
tx?.forceCacheInvalidation(completionHandler: completionHandler)
}

/// Destructs the TXNative singleton instance so that another one can be used.
@objc
public static func dispose() {
tx = nil
}
}
14 changes: 7 additions & 7 deletions Sources/Transifex/SourceString.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,20 @@ import Foundation
/// properties of the struct while also being able to be used both by Swift and Objective-C applications.
public final class TXSourceString: NSObject {
/// The key that was generated by the `txGenerateKey()` method
let key: String
public let key: String
/// The source string
let sourceString: String
public let sourceString: String
/// An optional developer comment
let developerComment: String?
public let developerComment: String?
/// A list of relative file paths where this string is located.
let occurrences: [String]
public let occurrences: [String]
/// A list of tags accompanying this source string
let tags: [String]?
public let tags: [String]?
/// A limit provided by the developer that should be respected by translators when translating the
/// source string, 0 means no limit.
let characterLimit: Int
public let characterLimit: Int
/// A list of strings providing more context to the source string
let context: [String]?
public let context: [String]?

/// Public and designated constructor.
///
Expand Down
104 changes: 102 additions & 2 deletions Tests/TransifexTests/TransifexTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ struct MockResponse {
var data : Data?
var statusCode : Int?
var error : Error?
var url : URL?
}

class URLSessionMock: URLSession {
Expand Down Expand Up @@ -60,13 +61,24 @@ class URLSessionMock: URLSession {
else if let mockResponse = mockResponses?[mockResponseIndex] {
mockResponseIndex += 1

let requestURL = request.url!
let data = mockResponse.data
let error = mockResponse.error
let statusCode = mockResponse.statusCode
let url = mockResponse.url

var finalStatusCode = 200

if let statusCode = statusCode {
finalStatusCode = statusCode
}
else if let url = url {
finalStatusCode = (url == requestURL ? 200 : 403)
}

return URLSessionDataTaskMock {
let response = HTTPURLResponse(url: request.url!,
statusCode: statusCode ?? 200,
let response = HTTPURLResponse(url: requestURL,
statusCode: finalStatusCode,
httpVersion: nil,
headerFields: nil)
completionHandler(data, response, error)
Expand Down Expand Up @@ -114,6 +126,10 @@ class MockErrorPolicy : TXErrorPolicy {
}

final class TransifexTests: XCTestCase {
override func tearDown() {
TXNative.dispose()
}

func testDuplicateLocaleFiltering() {
let duplicateLocales = [ "en", "fr", "en" ]

Expand Down Expand Up @@ -198,6 +214,34 @@ final class TransifexTests: XCTestCase {
XCTAssertEqual("{something}".extractICUPlurals(), nil)
}

func testFetchTranslationsWithTags() {
let expectation = self.expectation(description: "Waiting for translations to be fetched")
var translationErrors : [Error]? = nil
let mockResponseData = "{\"data\":{\"testkey1\":{\"string\":\"test string 1\"},\"testkey2\":{\"string\":\"test string 2\"}}}".data(using: .utf8)

let expectedURL = URL(string: "https://cds.svc.transifex.net/content/en?filter%5Btags%5D=ios")
let mockResponse = MockResponse(data: mockResponseData,
url: expectedURL)

let urlSession = URLSessionMock()
urlSession.mockResponses = [mockResponse]

let cdsHandler = CDSHandler(localeCodes: [ "en" ],
token: "test_token",
session: urlSession)

cdsHandler.fetchTranslations(tags: ["ios"]) { translations, errors in
translationErrors = errors
expectation.fulfill()
}

waitForExpectations(timeout: 1.0) { (error) in
XCTAssertNil(error)
XCTAssertNotNil(translationErrors)
XCTAssertTrue(translationErrors?.count == 0)
}
}

func testFetchTranslations() {
let expectation = self.expectation(description: "Waiting for translations to be fetched")
var translationsResult : TXTranslations? = nil
Expand Down Expand Up @@ -475,6 +519,61 @@ final class TransifexTests: XCTestCase {
forKey: appleLanguagesKey)
}

func testTranslateWithSourceStringsInCache() {
let sourceLocale = "en"
let localeState = TXLocaleState(sourceLocale: sourceLocale,
appLocales: [
sourceLocale,
"el"])


let sourceStringTest = "tx_test_key"
let translatedStringTest = "test updated"

let sourceStringPlural = "tx_plural_test_key"
let translatedStringPluralOne = "car updated"
let translatedStringPluralOther = "cars updated"
let translatedStringPluralRule = "{cnt, plural, one {\(translatedStringPluralOne)} other {\(translatedStringPluralOther)}}"

let keyTest = txGenerateKey(sourceString: sourceStringTest, context: nil)
let keyPlural = txGenerateKey(sourceString: sourceStringPlural, context: nil)

let existingTranslations: TXTranslations = [
sourceLocale: [
keyTest: [ "string": translatedStringTest ],
keyPlural : [ "string": translatedStringPluralRule ]
]
]

let memoryCache = TXMemoryCache()
memoryCache.update(translations: existingTranslations)

TXNative.initialize(locales: localeState,
token: "<token>",
secret: "<secret>",
cache: memoryCache)

let result = TXNative.translate(sourceString: sourceStringTest,
params: [:],
context: nil)

XCTAssertEqual(result, translatedStringTest)

let pluralsResultOne = TXNative.translate(sourceString: sourceStringPlural,
params: [
Swizzler.PARAM_ARGUMENTS_KEY: [1 as CVarArg]],
context: nil)

XCTAssertEqual(pluralsResultOne, translatedStringPluralOne)

let pluralsResultOther = TXNative.translate(sourceString: sourceStringPlural,
params: [
Swizzler.PARAM_ARGUMENTS_KEY: [3 as CVarArg]],
context: nil)

XCTAssertEqual(pluralsResultOther, translatedStringPluralOther)
}

static var allTests = [
("testDuplicateLocaleFiltering", testDuplicateLocaleFiltering),
("testCurrentLocaleProvider", testCurrentLocaleProvider),
Expand All @@ -491,5 +590,6 @@ final class TransifexTests: XCTestCase {
("testPlatformStrategyWithInvalidSourceString", testPlatformStrategyWithInvalidSourceString),
("testErrorPolicy", testErrorPolicy),
("testCurrentLocale", testCurrentLocale),
("testTranslateWithSourceStringsInCache", testTranslateWithSourceStringsInCache),
]
}