diff --git a/Sources/CrowdinSDK/CrowdinAPI/ContentDeliveryAPI/CrowdinContentDeliveryAPI.swift b/Sources/CrowdinSDK/CrowdinAPI/ContentDeliveryAPI/CrowdinContentDeliveryAPI.swift index 3d05d42e..374a1f15 100644 --- a/Sources/CrowdinSDK/CrowdinAPI/ContentDeliveryAPI/CrowdinContentDeliveryAPI.swift +++ b/Sources/CrowdinSDK/CrowdinAPI/ContentDeliveryAPI/CrowdinContentDeliveryAPI.swift @@ -8,6 +8,7 @@ import Foundation import BaseAPI +typealias CrowdinAPIFileDataCompletion = ((Data?, String?, Error?) -> Void) typealias CrowdinAPIStringsCompletion = (([String: String]?, String?, Error?) -> Void) typealias CrowdinAPIPluralsCompletion = (([AnyHashable: Any]?, String?, Error?) -> Void) typealias CrowdinAPIXliffCompletion = (([AnyHashable: Any]?, String?, Error?) -> Void) @@ -87,6 +88,18 @@ class CrowdinContentDeliveryAPI: BaseAPI { } } + // MARK - Localization download methods: + func getFileData(filePath: String, etag: String?, timestamp: TimeInterval?, completion: @escaping CrowdinAPIFileDataCompletion) { + self.getFile(filePath: filePath, etag: etag, timestamp: timestamp) { (data, response, error) in + let etag = (response as? HTTPURLResponse)?.allHeaderFields[Strings.etag.rawValue] as? String + if let data { + completion(data, etag, nil) + } else { + completion(nil, etag, error) + } + } + } + func getPlurals(filePath: String, etag: String?, timestamp: TimeInterval?, completion: @escaping CrowdinAPIPluralsCompletion) { self.getFile(filePath: filePath, etag: etag, timestamp: timestamp) { (data, response, error) in let etag = (response as? HTTPURLResponse)?.allHeaderFields[Strings.etag.rawValue] as? String diff --git a/Sources/CrowdinSDK/CrowdinFileSystem/ReadWriteProtocol.swift b/Sources/CrowdinSDK/CrowdinFileSystem/ReadWriteProtocol.swift index 0b6025bc..e6e998c9 100644 --- a/Sources/CrowdinSDK/CrowdinFileSystem/ReadWriteProtocol.swift +++ b/Sources/CrowdinSDK/CrowdinFileSystem/ReadWriteProtocol.swift @@ -68,3 +68,19 @@ extension CodableWrapper: ReadWriteProtocol { return self.init(object: object) } } + +extension Data: ReadWriteProtocol { + public func write(to path: String) { + do { + let url = URL(fileURLWithPath: path) + try Folder(path: url.deletingLastPathComponent().relativePath).create() + try self.write(to: url) + } catch { + print(error) + } + } + + public static func read(from path: String) -> Data? { + try? Data(contentsOf: URL(fileURLWithPath: path)) + } +} diff --git a/Sources/CrowdinSDK/Features/RealtimeUpdateFeature/LocalizationProvider/RURemoteLocalizationStorage.swift b/Sources/CrowdinSDK/Features/RealtimeUpdateFeature/LocalizationProvider/RURemoteLocalizationStorage.swift index 1404d4d5..f6ad89e2 100644 --- a/Sources/CrowdinSDK/Features/RealtimeUpdateFeature/LocalizationProvider/RURemoteLocalizationStorage.swift +++ b/Sources/CrowdinSDK/Features/RealtimeUpdateFeature/LocalizationProvider/RURemoteLocalizationStorage.swift @@ -20,10 +20,10 @@ class RURemoteLocalizationStorage: RemoteLocalizationStorageProtocol { let fileDownloader: RUFilesDownloader let manifestManager: ManifestManager - init(localization: String, hash: String, projectId: String, organizationName: String?) { + init(localization: String, sourceLanguage: String, hash: String, projectId: String, organizationName: String?) { self.localization = localization self.hash = hash - manifestManager = ManifestManager.manifest(for: hash, organizationName: organizationName) + manifestManager = ManifestManager.manifest(for: hash, sourceLanguage: sourceLanguage, organizationName: organizationName) self.fileDownloader = RUFilesDownloader(projectId: projectId, manifestManager: manifestManager, organizationName: organizationName) } diff --git a/Sources/CrowdinSDK/Features/RealtimeUpdateFeature/RealtimeUpdateFeature.swift b/Sources/CrowdinSDK/Features/RealtimeUpdateFeature/RealtimeUpdateFeature.swift index a3effba3..d565139a 100644 --- a/Sources/CrowdinSDK/Features/RealtimeUpdateFeature/RealtimeUpdateFeature.swift +++ b/Sources/CrowdinSDK/Features/RealtimeUpdateFeature/RealtimeUpdateFeature.swift @@ -40,6 +40,7 @@ class RealtimeUpdateFeature: RealtimeUpdateFeatureProtocol { return CrowdinSDK.currentLocalization ?? Bundle.main.preferredLanguage(with: localizations) } var hashString: String + let sourceLanguage: String let organizationName: String? var distributionResponse: DistributionsResponse? = nil @@ -60,6 +61,7 @@ class RealtimeUpdateFeature: RealtimeUpdateFeatureProtocol { required init(hash: String, sourceLanguage: String, organizationName: String?) { self.hashString = hash + self.sourceLanguage = sourceLanguage self.organizationName = organizationName self.mappingManager = CrowdinMappingManager(hash: hash, sourceLanguage: sourceLanguage, organizationName: organizationName) } @@ -134,7 +136,7 @@ class RealtimeUpdateFeature: RealtimeUpdateFeatureProtocol { func setupRealtimeUpdatesLocalizationProvider(with projectId: String, completion: @escaping () -> Void) { oldProvider = Localization.current.provider - Localization.current.provider = LocalizationProvider(localization: self.localization, localStorage: RULocalLocalizationStorage(localization: self.localization), remoteStorage: RURemoteLocalizationStorage(localization: self.localization, hash: self.hashString, projectId: projectId, organizationName: self.organizationName)) + Localization.current.provider = LocalizationProvider(localization: self.localization, localStorage: RULocalLocalizationStorage(localization: self.localization), remoteStorage: RURemoteLocalizationStorage(localization: self.localization, sourceLanguage: sourceLanguage, hash: self.hashString, projectId: projectId, organizationName: self.organizationName)) Localization.current.provider.refreshLocalization { [weak self] error in guard let self = self else { return } @@ -163,7 +165,7 @@ class RealtimeUpdateFeature: RealtimeUpdateFeatureProtocol { func setupSocketManager(with projectId: String, projectWsHash: String, userId: String, wsUrl: String) { // Download manifest if it is not initialized. - let manifestManager = ManifestManager.manifest(for: hashString, organizationName: organizationName) + let manifestManager = ManifestManager.manifest(for: hashString, sourceLanguage: sourceLanguage, organizationName: organizationName) guard manifestManager.downloaded else { manifestManager.download { [weak self] in guard let self = self else { return } diff --git a/Sources/CrowdinSDK/Providers/Crowdin/CrowdinRemoteLocalizationStorage.swift b/Sources/CrowdinSDK/Providers/Crowdin/CrowdinRemoteLocalizationStorage.swift index efcb54a7..2da3a294 100644 --- a/Sources/CrowdinSDK/Providers/Crowdin/CrowdinRemoteLocalizationStorage.swift +++ b/Sources/CrowdinSDK/Providers/Crowdin/CrowdinRemoteLocalizationStorage.swift @@ -23,7 +23,7 @@ class CrowdinRemoteLocalizationStorage: RemoteLocalizationStorageProtocol { self.localization = localization self.hashString = config.hashString self.organizationName = config.organizationName - self.manifestManager = ManifestManager.manifest(for: config.hashString, organizationName: config.organizationName) + self.manifestManager = ManifestManager.manifest(for: config.hashString, sourceLanguage: config.sourceLanguage, organizationName: config.organizationName) self.crowdinDownloader = CrowdinLocalizationDownloader(manifestManager: manifestManager) self.localizations = self.manifestManager.iOSLanguages self.crowdinSupportedLanguages = CrowdinSupportedLanguages(organizationName: config.organizationName) @@ -58,13 +58,13 @@ class CrowdinRemoteLocalizationStorage: RemoteLocalizationStorageProtocol { }) } - required init(localization: String, enterprise: Bool, organizationName: String?) { + required init(localization: String, sourceLanguage: String, organizationName: String?) { self.localization = localization guard let hashString = Bundle.main.crowdinDistributionHash else { fatalError("Please add CrowdinDistributionHash key to your Info.plist file") } self.hashString = hashString - self.manifestManager = ManifestManager.manifest(for: hashString, organizationName: organizationName) + self.manifestManager = ManifestManager.manifest(for: hashString, sourceLanguage: sourceLanguage, organizationName: organizationName) self.crowdinDownloader = CrowdinLocalizationDownloader(manifestManager: self.manifestManager) self.localizations = [] self.crowdinSupportedLanguages = CrowdinSupportedLanguages(organizationName: organizationName) diff --git a/Sources/CrowdinSDK/Providers/Crowdin/Extensions/String+Extensions.swift b/Sources/CrowdinSDK/Providers/Crowdin/Extensions/String+Extensions.swift index 9acddfef..2d53a25c 100644 --- a/Sources/CrowdinSDK/Providers/Crowdin/Extensions/String+Extensions.swift +++ b/Sources/CrowdinSDK/Providers/Crowdin/Extensions/String+Extensions.swift @@ -12,22 +12,31 @@ enum FileExtensions: String { case stringsdict case xliff case json + case xcstrings + + var `extension`: String { + return ".\(self.rawValue)" + } } extension String { var isStrings: Bool { - return self.hasSuffix(FileExtensions.strings.rawValue) + hasSuffix(FileExtensions.strings.extension) } var isStringsDict: Bool { - return self.hasSuffix(FileExtensions.stringsdict.rawValue) + hasSuffix(FileExtensions.stringsdict.extension) } var isXliff: Bool { - return self.hasSuffix(FileExtensions.xliff.rawValue) + hasSuffix(FileExtensions.xliff.extension) } var isJson: Bool { - return self.hasSuffix(FileExtensions.json.rawValue) + hasSuffix(FileExtensions.json.extension) + } + + var isXcstrings: Bool { + hasSuffix(FileExtensions.xcstrings.extension) } } diff --git a/Sources/CrowdinSDK/Providers/Crowdin/LocalizationDownloader/CrowdinLocalizationDownloader.swift b/Sources/CrowdinSDK/Providers/Crowdin/LocalizationDownloader/CrowdinLocalizationDownloader.swift index e04831e5..0119cb95 100644 --- a/Sources/CrowdinSDK/Providers/Crowdin/LocalizationDownloader/CrowdinLocalizationDownloader.swift +++ b/Sources/CrowdinSDK/Providers/Crowdin/LocalizationDownloader/CrowdinLocalizationDownloader.swift @@ -31,7 +31,8 @@ class CrowdinLocalizationDownloader: CrowdinDownloaderProtocol { let plurals = files.filter({ $0.isStringsDict }) let xliffs = files.filter({ $0.isXliff }) let jsons = files.filter({ $0.isJson }) - self.download(strings: strings, plurals: plurals, xliffs:xliffs, jsons: jsons, with: hash, timestamp: timestamp, for: localization) + let xcstrings = files.filter({ $0.isXcstrings }) + self.download(strings: strings, plurals: plurals, xliffs:xliffs, jsons: jsons, xcstrings: xcstrings, with: hash, timestamp: timestamp, for: localization) } else if let error = error { self.errors = [error] self.completion?(nil, nil, self.errors) @@ -39,7 +40,7 @@ class CrowdinLocalizationDownloader: CrowdinDownloaderProtocol { } } - func download(strings: [String], plurals: [String], xliffs: [String], jsons: [String], with hash: String, timestamp: TimeInterval?, for localization: String) { + func download(strings: [String], plurals: [String], xliffs: [String], jsons: [String], xcstrings: [String], with hash: String, timestamp: TimeInterval?, for localization: String) { self.operationQueue.cancelAllOperations() self.contentDeliveryAPI = CrowdinContentDeliveryAPI(hash: hash, session: URLSession.shared) @@ -96,6 +97,23 @@ class CrowdinLocalizationDownloader: CrowdinDownloaderProtocol { completionBlock.addDependency(download) operationQueue.addOperation(download) } + + xcstrings.forEach { filePath in + let download = CrowdinXcstringsDownloadOperation(filePath: filePath, + localization: localization, + language: manifestManager.xcstringsLanguage, + timestamp: timestamp, + contentDeliveryAPI: contentDeliveryAPI) + download.completion = { [weak self] (strings, plurals, error) in + guard let self = self else { return } + self.add(strings: strings) + self.add(plurals: plurals) + self.add(error: error) + } + completionBlock.addDependency(download) + operationQueue.addOperation(download) + } + operationQueue.operations.forEach({ $0.qualityOfService = .userInitiated }) operationQueue.addOperation(completionBlock) } diff --git a/Sources/CrowdinSDK/Providers/Crowdin/LocalizationDownloader/Operations/CrowdinXcstringsDownloadOperation.swift b/Sources/CrowdinSDK/Providers/Crowdin/LocalizationDownloader/Operations/CrowdinXcstringsDownloadOperation.swift new file mode 100644 index 00000000..35377493 --- /dev/null +++ b/Sources/CrowdinSDK/Providers/Crowdin/LocalizationDownloader/Operations/CrowdinXcstringsDownloadOperation.swift @@ -0,0 +1,210 @@ +// +// CrowdinXcstringsDownloadOperation.swift +// CrowdinSDK +// +// Created by Serhii Londar on 16.03.2024. +// + +import Foundation + +public struct Localizations: Codable { + public let sourceLanguage: String + public let version: String + public let strings: [String: StringInfo] +} + +public struct StringInfo: Codable { + public let extractionState: String? + public let localizations: [String: StringLocalization]? +} + +public struct StringLocalization: Codable { + public let stringUnit: StringUnit? + public let variations: Variations? + public let substitutions: [String: Substitution]? +} + +public struct Substitution: Codable { + let argNum: Int + let formatSpecifier: String + let variations: Variations +} + +public struct Variations: Codable { + let plural: [String: StringUnitWrapper]? + // Skip for v 1.0 + // let device: DeviceVariations? +} + +// Skip for v 1.0 +//public struct DeviceVariations: Codable { +// let variations: [String: StringUnitWrapper]? +//} + +public struct StringUnitWrapper: Codable { + let stringUnit: StringUnit +} + +public struct StringUnit: Codable { + public let state: String + public let value: String +} + + +class XcstringsParser { + enum Keys: String { + case NSStringLocalizedFormatKey + case NSStringFormatSpecTypeKey + case NSStringFormatValueTypeKey + } + + enum Strings: String { + case NSStringPluralRuleType + } + + static func parse(localizations: Localizations, localization: String) -> ([String: String]?, [AnyHashable: Any]?, Error?) { + var strings: [String: String] = [:] + var plurals: [String: Any] = [:] + + let localizationStrings = localizations.strings + + for (key, value) in localizationStrings { + if let value = value.localizations?[localization] { + if let stringUnit = value.stringUnit, let substitutions = value.substitutions { + var dict = [String: Any]() + dict[Keys.NSStringLocalizedFormatKey.rawValue] = stringUnit.value + + for (key, substitution) in substitutions { + var pluralDict = Self.dictFor(substitution: substitution, with: substitutions) + pluralDict?[Keys.NSStringFormatSpecTypeKey.rawValue] = Strings.NSStringPluralRuleType.rawValue + pluralDict?[Keys.NSStringFormatValueTypeKey.rawValue] = substitution.formatSpecifier + dict[key] = pluralDict + } + + plurals[key] = dict + } else if let pluralVariation = value.variations?.plural { + var pluralDict = pluralVariation.mapValues({ $0.stringUnit.value }) + pluralDict[Keys.NSStringFormatSpecTypeKey.rawValue] = Strings.NSStringPluralRuleType.rawValue + pluralDict[Keys.NSStringFormatValueTypeKey.rawValue] = pluralDict.values.map({ Self.formats(from: $0) }).filter({ $0.count > 0 }).first?.first ?? "u" + + var dict = [String: Any]() + dict[Keys.NSStringLocalizedFormatKey.rawValue] = "%#@\(key)@" + dict[key] = pluralDict + + plurals[key] = dict + } else if let stringUnit = value.stringUnit { + strings[key] = stringUnit.value + } + } + } + return (strings, plurals, nil) + } + + static func parse(data: Data, localization: String) -> (strings: [String: String]?, plurals: [AnyHashable: Any]?, error: Error?) { + do { + let localizations = try JSONDecoder().decode(Localizations.self, from: data) + + return parse(localizations: localizations, localization: localization) + } catch { + return (nil, nil, error) + } + } + + static func dictFor(substitution: Substitution, with substitutions: [String: Substitution]) -> [String: Any]? { + var dict = substitution.variations.plural?.mapValues({ $0.stringUnit.value }) ?? [:] + + for (key, value) in dict { + for (key1, substitution) in substitutions { + let refKey = "%#@\(key1)@" + if value.contains(refKey) { + let parameteredKey = "%\(substitution.argNum)$#@\(key1)@" + dict[key] = value.replacingOccurrences(of: refKey, with: parameteredKey) + } + } + } + + return dict + } + + static func formats(from value: String) -> [String] { + var specifiers: [String] = [] + var isSpecifier = false + var currentSpecifier = "" + + for char in value { + if char == "%" { + isSpecifier = true + } else if isSpecifier { + if char.isLetter || char == "@" { + currentSpecifier.append(char) + } else { + isSpecifier = false + if !currentSpecifier.isEmpty { + specifiers.append(currentSpecifier) + currentSpecifier = "" + } + } + } + } + + return specifiers + } +} + +class XCStringsStorage { + private enum Strings: String { + case XCStrings + } + + static let folder = try! CrowdinFolder.shared.createFolder(with: Strings.XCStrings.rawValue) + + static func getFile(path: String) -> Data? { + Data.read(from: folder.path + path) + } + + static func saveFile(_ data: Data, path: String) { + data.write(to: folder.path + path) + } +} + +class CrowdinXcstringsDownloadOperation: CrowdinDownloadOperation { + var timestamp: TimeInterval? + let eTagStorage: AnyEtagStorage + var completion: CrowdinJsonDownloadOperationCompletion? = nil + let localization: String + + init(filePath: String, localization: String, language: String, timestamp: TimeInterval?, contentDeliveryAPI: CrowdinContentDeliveryAPI, completion: CrowdinJsonDownloadOperationCompletion?) { + self.localization = localization + self.timestamp = timestamp + self.eTagStorage = FileEtagStorage(localization: language) + super.init(filePath: filePath, contentDeliveryAPI: contentDeliveryAPI) + self.completion = completion + } + + required init(filePath: String, localization: String, language: String, timestamp: TimeInterval?, contentDeliveryAPI: CrowdinContentDeliveryAPI) { + self.localization = localization + self.timestamp = timestamp + self.eTagStorage = FileEtagStorage(localization: language) + super.init(filePath: filePath, contentDeliveryAPI: contentDeliveryAPI) + } + + override func main() { + let etag = eTagStorage.etag(for: filePath) + contentDeliveryAPI.getFileData(filePath: filePath, etag: etag, timestamp: timestamp) { [weak self] data, etag, error in + guard let self = self else { return } + self.eTagStorage.save(etag: etag, for: self.filePath) + if let data, data.count > 0 { + XCStringsStorage.saveFile(data, path: self.filePath) + let parsed = XcstringsParser.parse(data: data, localization: self.localization) + self.completion?(parsed.strings, parsed.plurals, parsed.error) + self.finish(with: parsed.error != nil) + } else if let data = XCStringsStorage.getFile(path: self.filePath) { + let parsed = XcstringsParser.parse(data: data, localization: self.localization) + self.completion?(parsed.strings, parsed.plurals, parsed.error) + self.finish(with: error != nil) + } else { + self.finish(with: error != nil) + } + } + } +} diff --git a/Sources/CrowdinSDK/Providers/Crowdin/ManifestManager/ManifestManager.swift b/Sources/CrowdinSDK/Providers/Crowdin/ManifestManager/ManifestManager.swift index 9a5485fe..a62dfaab 100644 --- a/Sources/CrowdinSDK/Providers/Crowdin/ManifestManager/ManifestManager.swift +++ b/Sources/CrowdinSDK/Providers/Crowdin/ManifestManager/ManifestManager.swift @@ -51,6 +51,7 @@ class ManifestManager { } let hash: String + let sourceLanguage: String let organizationName: String? var manifest: ManifestResponse? @@ -58,8 +59,9 @@ class ManifestManager { var contentDeliveryAPI: CrowdinContentDeliveryAPI var crowdinSupportedLanguages: CrowdinSupportedLanguages - fileprivate init(hash: String, organizationName: String?) { + fileprivate init(hash: String, sourceLanguage: String, organizationName: String?) { self.hash = hash + self.sourceLanguage = sourceLanguage self.organizationName = organizationName self.contentDeliveryAPI = CrowdinContentDeliveryAPI(hash: hash) self.crowdinSupportedLanguages = CrowdinSupportedLanguages(organizationName: organizationName) @@ -67,8 +69,8 @@ class ManifestManager { ManifestManager.manifestMap[self.hash] = self } - class func manifest(for hash: String, organizationName: String?) -> ManifestManager { - manifestMap[hash] ?? ManifestManager(hash: hash, organizationName: organizationName) + class func manifest(for hash: String, sourceLanguage: String, organizationName: String?) -> ManifestManager { + manifestMap[hash] ?? ManifestManager(hash: hash, sourceLanguage: sourceLanguage, organizationName: organizationName) } var languages: [String]? { manifest?.languages } @@ -76,6 +78,7 @@ class ManifestManager { var timestamp: TimeInterval? { manifest?.timestamp } var customLanguages: [CustomLangugage] { manifest?.customLanguages ?? [] } var mappingFiles: [String] { manifest?.mapping ?? [] } + var xcstringsLanguage: String { languages?.sorted().first ?? sourceLanguage } var iOSLanguages: [String] { return self.languages?.compactMap({ self.iOSLanguageCode(for: $0) }) ?? [] @@ -83,7 +86,13 @@ class ManifestManager { func contentFiles(for language: String) -> [String] { guard let crowdinLanguage = crowdinLanguageCode(for: language) else { return [] } - return manifest?.content[crowdinLanguage] ?? [] + var files = manifest?.content[crowdinLanguage] ?? [] + // Add xcstrings files from source language if language != firstLanguage + if language != xcstringsLanguage { // Avoid duplications for first language in languages array. + let xcstrings = manifest?.content[xcstringsLanguage]?.filter({ $0.isXcstrings }) ?? [] + files.append(contentsOf: xcstrings) + } + return files } func download(completion: @escaping () -> Void) { diff --git a/Sources/CrowdinSDK/Providers/Crowdin/MappingDownloader/CrowdinMappingManager.swift b/Sources/CrowdinSDK/Providers/Crowdin/MappingDownloader/CrowdinMappingManager.swift index 5d24b2e9..6446cb83 100644 --- a/Sources/CrowdinSDK/Providers/Crowdin/MappingDownloader/CrowdinMappingManager.swift +++ b/Sources/CrowdinSDK/Providers/Crowdin/MappingDownloader/CrowdinMappingManager.swift @@ -26,7 +26,7 @@ public class CrowdinMappingManager: CrowdinMappingManagerProtocol { var plurals: [AnyHashable: Any] = [:] init(hash: String, sourceLanguage: String, organizationName: String?) { - self.manifestManager = ManifestManager.manifest(for: hash, organizationName: organizationName) + self.manifestManager = ManifestManager.manifest(for: hash, sourceLanguage: sourceLanguage, organizationName: organizationName) self.downloader = CrowdinMappingDownloader(manifestManager: self.manifestManager) self.download(hash: hash, sourceLanguage: sourceLanguage) } @@ -35,7 +35,7 @@ public class CrowdinMappingManager: CrowdinMappingManagerProtocol { if manifestManager.downloaded == false { manifestManager.download { [weak self] in guard let self = self else { return } - self.downloadMapping(hash: hash, sourceLanguage: sourceLanguage) + self.downloadMapping(hash: hash, sourceLanguage: sourceLanguage) } } else { downloadMapping(hash: hash, sourceLanguage: sourceLanguage) diff --git a/Sources/Tests/CrowdinProvider/ManifestManagerTests.swift b/Sources/Tests/CrowdinProvider/ManifestManagerTests.swift index df2b0de7..1f0ffd05 100644 --- a/Sources/Tests/CrowdinProvider/ManifestManagerTests.swift +++ b/Sources/Tests/CrowdinProvider/ManifestManagerTests.swift @@ -3,7 +3,7 @@ import XCTest class ManifestManagerTests: XCTestCase { let crowdinTestHash = "5290b1cfa1eb44bf2581e78106i" - + let sourceLanguage = "en" override func setUp() { } @@ -18,7 +18,7 @@ class ManifestManagerTests: XCTestCase { func testDownloadManifest() { let expectation = XCTestExpectation(description: "Manifest download expectation") - let manifest = ManifestManager.manifest(for: crowdinTestHash, organizationName: nil) + let manifest = ManifestManager.manifest(for: crowdinTestHash, sourceLanguage: sourceLanguage, organizationName: nil) XCTAssertFalse(manifest.loaded) XCTAssertFalse(manifest.downloaded) diff --git a/website/docs/setup.mdx b/website/docs/setup.mdx index f7050ec3..cae71728 100644 --- a/website/docs/setup.mdx +++ b/website/docs/setup.mdx @@ -143,3 +143,11 @@ Text("key".cw_localized) ``` After we add SwiftUI support, you will just need to remove the `cw_localized` method call. + +## Apple Strings Catalog support + +The Crowdin SDK supports the [Apple Strings Catalog](https://store.crowdin.com/string_catalog) (`.xcstrings`) format out of the box. It doesn't require any additional setup. Just upload your localization files to Crowdin, set up the distribution and start using the SDK. + +:::caution +Only the CDN Content Delivery feature is available for the Apple Strings Catalog format. The Screenshots and Real-Time Preview features are not yet supported. +:::