Skip to content

Commit

Permalink
Merge pull request #56 from transifex/stelios/various-improvements
Browse files Browse the repository at this point in the history
Various improvements
  • Loading branch information
Nikos Vasileiou authored Jul 7, 2023
2 parents e2e90b9 + 9f7b6e5 commit 07da18a
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 76 deletions.
152 changes: 82 additions & 70 deletions Sources/Transifex/CDSHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public enum TXCDSError: Error {
/// No locale codes were provided to the fetch operation
case noLocaleCodes
/// Translation strings to be pushed failed to be serialized
case failedSerialization
case failedSerialization(error: Error)
/// The CDS request failed with a specific underlying error
case requestFailed(error : Error)
/// The HTTP response received by CDS was invalid
Expand All @@ -33,11 +33,22 @@ public enum TXCDSError: Error {
case noData
/// The job status request failed
case failedJobRequest
/// There is no generated data to be sent to CDS
case noDataToBeSent
/// A specific job error was returned by CDS
case jobError(status: String, code: String, title: String,
detail: String, source: [String : String])
}

/// All possible warnings that may be produced when pushing strings to CDS.
public enum TXCDSWarning: Error {
/// A duplicate source string pair has been detected.
case duplicateSourceString(sourceString: String,
duplicate: String)
/// A source string with an empty key has been detected.
case emptyKey(SourceString: String)
}

/// Handles the logic of a pull HTTP request to CDS for a certain locale code
class CDSPullRequest {
let code : String
Expand Down Expand Up @@ -401,56 +412,63 @@ class CDSHandler {
/// - completionHandler: a callback function to call when the operation is complete
public func pushTranslations(_ translations: [TXSourceString],
purge: Bool = false,
completionHandler: @escaping (Bool, [Error]) -> Void) {
guard let cdsHostURL = URL(string: configuration.cdsHost) else {
Logger.error("Error: Invalid CDS host URL: \(configuration.cdsHost)")
completionHandler(false,
[TXCDSError.invalidCDSURL])
return
completionHandler: @escaping (Bool, [TXCDSError], [TXCDSWarning]) -> Void) {
let serializedResult = Self.serializeTranslations(translations,
purge: purge)
switch serializedResult.0 {
case .success(let jsonData):
guard jsonData.count > 0 else {
completionHandler(false, [.noDataToBeSent], serializedResult.1)
return
}
Logger.verbose("Pushing translations to CDS: \(translations)...")
pushData(jsonData,
warnings: serializedResult.1,
completionHandler: completionHandler)
case .failure(let error):
completionHandler(false, [.failedSerialization(error: error)], serializedResult.1)
}

guard let jsonData = serializeTranslations(translations,
purge: purge) else {
Logger.error("Error while serializing translations")
completionHandler(false,
[TXCDSError.failedSerialization])
}

/// Pushes the generated JSON data containing the source strings and propagates any generated
/// warnings to the final completion handler.
///
/// - Parameters:
/// - jsonData: The generated JSON data
/// - warnings: Any generated CDS warnings that have been generated
/// - completionHandler: Callback function to be called when the push operation completes.
private func pushData(_ jsonData: Data,
warnings: [TXCDSWarning],
completionHandler: @escaping (Bool, [TXCDSError], [TXCDSWarning]) -> Void) {
guard let cdsHostURL = URL(string: configuration.cdsHost) else {
completionHandler(false, [.invalidCDSURL], warnings)
return
}

Logger.verbose("Pushing translations to CDS: \(translations)...")

let baseURL = cdsHostURL.appendingPathComponent(CDSHandler.CONTENT_ENDPOINT)
var request = URLRequest(url: baseURL)
request.httpBody = jsonData
request.httpMethod = "POST"
request.allHTTPHeaderFields = getHeaders(withSecret: true)

session.dataTask(with: request) { (data, response, error) in
guard error == nil else {
Logger.error("Error pushing strings: \(error!)")
completionHandler(false,
[TXCDSError.requestFailed(error: error!)])
if let error = error {
completionHandler(false, [.requestFailed(error: error)], warnings)
return
}

guard let httpResponse = response as? HTTPURLResponse else {
Logger.error("Error pushing strings: Not a valid HTTP response")
completionHandler(false,
[TXCDSError.invalidHTTPResponse])
completionHandler(false, [.invalidHTTPResponse], warnings)
return
}

if httpResponse.statusCode != CDSHandler.HTTP_STATUS_CODE_ACCEPTED {
Logger.error("HTTP Status error while pushing strings: \(httpResponse.statusCode)")
completionHandler(false,
[TXCDSError.serverError(statusCode: httpResponse.statusCode)])
completionHandler(false, [.serverError(statusCode: httpResponse.statusCode)], warnings)
return
}

guard let data = data else {
Logger.error("Error: No data received while pushing strings")
completionHandler(false,
[TXCDSError.noData])
completionHandler(false, [.noData], warnings)
return
}

Expand All @@ -461,17 +479,15 @@ class CDSHandler {
response = try decoder.decode(PushResponseData.self,
from: data)
}
catch {
Logger.error("Error while decoding CDS push response: \(error)")
}
catch { }

guard let finalResponse = response else {
completionHandler(false,
[TXCDSError.nonParsableResponse])
completionHandler(false, [.nonParsableResponse], warnings)
return
}

self.pollJobStatus(jobURL: finalResponse.data.links.job,
warnings: warnings,
retryCount: 0,
completionHandler: completionHandler)

Expand All @@ -490,36 +506,29 @@ class CDSHandler {
/// - completionHandler: The completion handler that informs the caller whether the job was
/// successful or not.
private func pollJobStatus(jobURL: String,
warnings: [TXCDSWarning],
retryCount: Int,
completionHandler: @escaping (Bool, [Error]) -> Void) {
completionHandler: @escaping (Bool, [TXCDSError], [TXCDSWarning]) -> Void) {
// Delay the job status request by 1 second, so that the server can
// have enough time to process the job.
Thread.sleep(forTimeInterval: 1.0)

fetchJobStatus(jobURL: jobURL) {
jobStatus, jobErrors, jobDetails in
guard let finalJobStatus = jobStatus else {
Logger.error("Error: Fetch job status request failed")
completionHandler(false,
[TXCDSError.failedJobRequest])
completionHandler(false, [.failedJobRequest], warnings)
return
}

var finalErrors: [Error] = []
var finalErrors: [TXCDSError] = []

if let errors = jobErrors {
if let errors = jobErrors, errors.count > 0 {
for error in errors {
Logger.error("""
\(error.title) (\(error.status) - \(error.code)):
\(error.detail)
Source:
\(error.source)
""")
finalErrors.append(TXCDSError.jobError(status: error.status,
code: error.code,
title: error.title,
detail: error.detail,
source: error.source))
finalErrors.append(.jobError(status: error.status,
code: error.code,
title: error.title,
detail: error.detail,
source: error.source))
}
}

Expand All @@ -539,16 +548,17 @@ failed: \(details.failed)
case .processing:
if retryCount < CDSHandler.MAX_RETRIES {
self.pollJobStatus(jobURL: jobURL,
warnings: warnings,
retryCount: retryCount + 1,
completionHandler: completionHandler)
}
else {
completionHandler(false, [TXCDSError.maxRetriesReached])
completionHandler(false, [.maxRetriesReached], warnings)
}
case .failed:
completionHandler(false, finalErrors)
completionHandler(false, finalErrors, warnings)
case .completed:
completionHandler(true, finalErrors)
completionHandler(true, finalErrors, warnings)
}
}
}
Expand Down Expand Up @@ -627,28 +637,30 @@ failed: \(details.failed)
///
/// - Parameter translations: a list of `TXSourceString` objects
/// - Parameter purge: Whether the resulting data will replace the entire resource content or not
/// - Returns: a Data object ready to be used in the CDS request
private func serializeTranslations(_ translations: [TXSourceString],
purge: Bool = false) -> Data? {
/// - Returns: A tuple containing the Result object that either contains the Data object ready to be
/// used in the CDS request or an error and the list of warnings generated during processing.
private static func serializeTranslations(_ translations: [TXSourceString],
purge: Bool = false) -> (Result<Data, Error>, [TXCDSWarning]) {
var sourceStrings: [String:SourceString] = [:]

var warnings: [TXCDSWarning] = []

for translation in translations {
sourceStrings[translation.key] = translation.sourceStringRepresentation()
let key = translation.key
let sourceString = translation.sourceStringRepresentation()
if let duplicateSourceString = sourceStrings[key] {
warnings.append(.duplicateSourceString(sourceString: sourceString.debugDescription,
duplicate: duplicateSourceString.debugDescription))
}
if key.trimmingCharacters(in: .whitespacesAndNewlines).count == 0 {
warnings.append(.emptyKey(SourceString: sourceString.debugDescription))
}
sourceStrings[key] = translation.sourceStringRepresentation()
}

let data = PushData(data: sourceStrings,
meta: PushData.Meta(purge: purge))

var jsonData: Data?

do {
jsonData = try JSONEncoder().encode(data)
}
catch {
Logger.error("Error encoding source strings: \(error)")
}

return jsonData

return (Result { try JSONEncoder().encode(data) }, warnings)
}

/// Return the headers to use when making requests.
Expand Down
27 changes: 23 additions & 4 deletions Sources/Transifex/Core.swift
Original file line number Diff line number Diff line change
Expand Up @@ -181,10 +181,11 @@ class NativeCore : TranslationProvider {
/// - purge: Whether to replace the entire resource content (true) or not (false). Defaults to false.
/// - completionHandler: A callback to be called when the push operation is complete with a
/// boolean argument that informs the caller that the operation was successful (true) or not (false) and
/// an array that may or may not contain any errors produced during the push operation.
/// an array that may or may not contain any errors produced during the push operation and an array of
/// non-blocking errors (warnings) that may have been generated during the push procedure.
func pushTranslations(_ translations: [TXSourceString],
purge: Bool = false,
completionHandler: @escaping (Bool, [Error]) -> Void) {
completionHandler: @escaping (Bool, [Error], [Error]) -> Void) {
cdsHandler.pushTranslations(translations,
purge: purge,
completionHandler: completionHandler)
Expand Down Expand Up @@ -503,6 +504,23 @@ token: \(token)
)
}

/// Helper method used when translation is not possible (e.g. in SwiftUI views).
///
/// This method applies the translation using the currently selected locale. For pluralization use the
/// `localizedString(format:arguments:)` method.
///
/// Make sure that this method is called after the SDK has been initialized, otherwise
/// "<SDK NOT INITIALIZED>" string will be shown instead.
///
/// - Parameter sourceString: The source string to be translated
/// - Returns: The translated string
public static func t(_ sourceString: String) -> String {
return tx?.translate(sourceString: sourceString,
localeCode: nil,
params: [:],
context: nil) ?? "<SDK NOT INITIALIZED>"
}

/// Used by the Swift localizedString(format:arguments:) methods found in the
/// TXExtensions.swift file.
public static func localizedString(format: String,
Expand Down Expand Up @@ -538,11 +556,12 @@ token: \(token)
/// - purge: Whether to replace the entire resource content (true) or not (false). Defaults to false.
/// - completionHandler: A callback to be called when the push operation is complete with a
/// boolean argument that informs the caller that the operation was successful (true) or not (false) and
/// an array that may or may not contain any errors produced during the push operation.
/// an array that may or may not contain any errors produced during the push operation and an array of
/// non-blocking errors (warnings) that may have been generated during the push procedure.
@objc
public static func pushTranslations(_ translations: [TXSourceString],
purge: Bool = false,
completionHandler: @escaping (Bool, [Error]) -> Void) {
completionHandler: @escaping (Bool, [Error], [Error]) -> Void) {
tx?.pushTranslations(translations,
purge: purge,
completionHandler: completionHandler)
Expand Down
4 changes: 2 additions & 2 deletions Tests/TransifexTests/TransifexTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,7 @@ final class TransifexTests: XCTestCase {
session: urlSession)

var pushResult = false
cdsHandler.pushTranslations(translations) { (result, errors) in
cdsHandler.pushTranslations(translations) { (result, errors, warnings) in
pushResult = result
expectation.fulfill()
}
Expand Down Expand Up @@ -601,7 +601,7 @@ final class TransifexTests: XCTestCase {
session: urlSession)

var pushResult = false
cdsHandler.pushTranslations(translations) { (result, errors) in
cdsHandler.pushTranslations(translations) { (result, errors, warnings) in
pushResult = result
expectation.fulfill()
}
Expand Down

0 comments on commit 07da18a

Please sign in to comment.