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

Various improvements #56

Merged
merged 2 commits into from
Jul 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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