From a73f4ca7a12c40af30fede4304c39a4c7733d7e3 Mon Sep 17 00:00:00 2001 From: Stelios Petrakis Date: Tue, 12 Mar 2024 14:45:44 +0100 Subject: [PATCH] String Catalog (.xcstrings) support * Total refactor of the XLIFF parser logic. * More readable and robust code that can handle out-of-order translation units and special cases. * XLIFF parser can now also parse `.xcstrings` files on top of the existing `.strings` and `.stringsdict`. * Only simple "plural." rules of `.xcstrings` files are parsed. The rest of the rule types (device variations, substitutions) produce and log an error and are not pushed to CDS. * Improves XLIFF parsing logic. * Adds unit test for simple `.xcstrings` plurals. * Documents limitations. --- README.md | 25 +- Sources/TXCli/main.swift | 17 +- Sources/TXCliLib/XLIFFParser.swift | 545 ++++++++++++++++-------- Tests/TXCliTests/XLIFFParserTests.swift | 130 +++++- 4 files changed, 498 insertions(+), 219 deletions(-) diff --git a/README.md b/README.md index c69e710..b7e26e8 100644 --- a/README.md +++ b/README.md @@ -139,19 +139,19 @@ command. For example, for iOS applications the option can be set to `--base-sdk ##### Pushing pluralizations limitations -Currently (version 0.1.0) pluralization is supported but only for cases where one variable is -used per pluralization rule. More advanced cases such as nested pluralization rules (for -example: "%d out of %d values entered") will be supported in future releases. +Generally, pluralization is supported but only for cases where one variable is used per pluralization rule. -Also, at the moment of writing (version 0.1.0), the `.stringsdict` specification only supports -plural types (`NSStringPluralRuleType`) which is the only possible value of the -`NSStringFormatSpecTypeKey` key ([Ref](https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPInternational/StringsdictFileFormat/StringsdictFileFormat.html#//apple_ref/doc/uid/10000171i-CH16-SW4)). +Both the existing `.stringsdict` and the newly introduced string catalog (`.xcstrings`) files are supported with some limitations mentioned below. -If more rule types are added in the `.stringsdict` specification, the XLIFF parser must be -updated in order to be able to extract them properly and to construct the ICU format out -of them. +We are actively working on adding support for more variations in future releases. -Width Variants in `.stringsdict` files are also not currently supported ([Ref](https://help.apple.com/xcode/mac/current/#/devaf8b4090a)). +###### String Catalogs (`.xcstrings`) + +Only plural rules are supported for string catalogs. Device variation [^1] and substitution rules are not currently supported. + +###### Strings Dictionary Files (`.stringsdict`) + +Only the plural type is supported (`NSStringPluralRuleType`) which is the only possible value of the `NSStringFormatSpecTypeKey` key [^2]. Width Variants are not currently supported [^3] [^4]. #### Pulling @@ -186,3 +186,8 @@ command can be simplified to: ## License Licensed under Apache License 2.0, see [LICENSE](LICENSE) file. + +[^1]: https://developer.apple.com/documentation/xcode/localizing-and-varying-text-with-a-string-catalog#Vary-strings-by-device +[^2]: https://developer.apple.com/documentation/xcode/localizing-strings-that-contain-plurals +[^3]: https://help.apple.com/xcode/mac/current/#/devaf8b4090a +[^4]: https://developer.apple.com/documentation/xcode/creating-width-and-device-variants-of-strings diff --git a/Sources/TXCli/main.swift b/Sources/TXCli/main.swift index 98dbfda..db3b3ab 100644 --- a/Sources/TXCli/main.swift +++ b/Sources/TXCli/main.swift @@ -245,8 +245,21 @@ Emulate a content push, without doing actual changes. // If the result contains string dict elements, convert them to // ICU format and use that as a source string - if let icuRule = result.generateICURuleIfPossible() { - sourceString = icuRule + switch result.generateICURuleIfPossible() { + case .success((let icuRule, let icuRuleType)): + // Only support plural rule type for now + if icuRuleType == .Plural { + sourceString = icuRule + } + case .failure(let error): + switch error { + case .noRules: + break + default: + logHandler.error("Error: \(error)") + // Do not add a translation unit in case of an error. + continue + } } let translationUnit = TXSourceString(key: key, diff --git a/Sources/TXCliLib/XLIFFParser.swift b/Sources/TXCliLib/XLIFFParser.swift index 692975d..421cd76 100644 --- a/Sources/TXCliLib/XLIFFParser.swift +++ b/Sources/TXCliLib/XLIFFParser.swift @@ -12,9 +12,21 @@ import Transifex /// Structure that holds all the information about a pluralization rule exported from the XLIFF file being /// parsed. public struct PluralizationRule { + private static let STRINGSDICT_SEPARATOR = "/" + private static let STRINGSDICT_DICT_TYPE = ":dict" + private static let STRINGSDICT_STRING_TYPE = ":string" + private static let STRINGSDICT_LOCALIZED_FORMAT_KEY = "NSStringLocalizedFormatKey" + + private static let XCSTRINGS_SEPARATOR = "|==|" + + public enum StringsSourceType { + case StringsDict + case XCStrings + } + private var components: [String] - var sourceString: String! + var sourceString: String var pluralKey: String? var pluralRule: String? var containsLocalizedFormatKey: Bool @@ -22,42 +34,78 @@ public struct PluralizationRule { var source: String? var target: String? var note: String? - + + var stringsSourceType : StringsSourceType + /// Initializes the structure with the id attribute found in the `trans-unit` XML tag of the XLIFF /// file. /// - /// From this id, the components are extracted (via the `extractComponents` static method) and - /// the properties are initialized: + /// From this id, the components are extracted (via the `extractComponentsXCStrings` or + /// `extractComponentsStringsDict` static methods) and the properties are initialized: + /// + /// For example, if the id is the following (from a `.xcstrings` file): + /// ``` + /// "unit-time.%d-minute(s)|==|plural.one" + /// ``` /// - /// For example, if the id is the following: + /// Then the properties have the following values: + /// sourceString: "unit-time.%d-minute(s)" + /// pluralKey: nil + /// pluralRule: "plural.one" + /// containsLocalizedFormatKey: false + /// + /// On the other hand, if the id is the following (from a `.stringsdict` file): + /// ``` /// "/unit-time.%d-minute(s):dict/d_unit_time:dict/one:dict/:string" + /// ``` /// /// Then the properties have the following values: - /// sourceString: "/unit-time.%d-minute(s)" + /// sourceString: "unit-time.%d-minute(s)" /// pluralKey: "d_unit_time" /// pluralRule: "one" /// containsLocalizedFormatKey: false /// /// - Parameter id: The id attribute init?(with id: String) { - let components = PluralizationRule.extractComponents(from: id) - - if components.count < 2 { - return nil - } - - self.components = components - self.sourceString = components.first! - self.containsLocalizedFormatKey = id.contains("NSStringLocalizedFormatKey") - - if self.containsLocalizedFormatKey { - return + if Self.isXCStringsID(id) { + self.stringsSourceType = .XCStrings + + // Modern .xcstrings format + let components = Self.extractComponentsXCStrings(from: id) + + if components.count != 2 { + return nil + } + + self.components = components + self.sourceString = components[0] + self.containsLocalizedFormatKey = false + self.pluralKey = nil + self.pluralRule = components[1] } - - self.pluralKey = components[1] - - if self.components.count > 2 { - self.pluralRule = self.components[2] + else { + self.stringsSourceType = .StringsDict + + // Legacy .stringsdict format + let components = Self.extractComponentsStringsDict(from: id) + + if components.count < 2 { + return nil + } + + self.components = components + self.sourceString = components[0] + self.containsLocalizedFormatKey = id.contains(Self.STRINGSDICT_LOCALIZED_FORMAT_KEY) + + if self.containsLocalizedFormatKey { + return + } + + self.pluralKey = components[1] + + if self.components.count > 2 { + self.pluralRule = self.components[2] + } } } @@ -85,14 +133,35 @@ public struct PluralizationRule { /// /// - Parameter id: The id attribute /// - Returns: The components that define this pluralization rule - static private func extractComponents(from id: String) -> [String] { + static private func extractComponentsStringsDict(from id: String) -> [String] { return id - .replacingOccurrences(of: ":dict", with: "") - .replacingOccurrences(of: ":string", with: "") - .trimmingCharacters(in: CharacterSet(charactersIn: "/")) - .components(separatedBy: "/") + .replacingOccurrences(of: Self.STRINGSDICT_DICT_TYPE, with: "") + .replacingOccurrences(of: Self.STRINGSDICT_STRING_TYPE, with: "") + .trimmingCharacters(in: CharacterSet(charactersIn: Self.STRINGSDICT_SEPARATOR)) + .components(separatedBy: Self.STRINGSDICT_SEPARATOR) } - + + /// Parses the id attribute of the `trans-unit` XML tag and splits the string into components that + /// each represents a certain property to be used during the initialization of the `PluralizationRule` + /// + /// e.g: + /// For id: + /// "unit-time.%d-minute(s)|==|plural.one" + /// The components returned are: + /// [ "unit-time.%d-minute(s)", "plural.one"] + /// + /// - Parameter id: The id attribute + /// - Returns: The components that define this pluralization rule + static private func extractComponentsXCStrings(from id: String) -> [String] { + return id.components(separatedBy: XCSTRINGS_SEPARATOR) + } + + /// - Parameter id: The id attribute + /// - Returns: True if the id contains a XCString identifier, False otherwise + static private func isXCStringsID(_ id: String) -> Bool { + return id.contains(XCSTRINGS_SEPARATOR) + } + /// Checks whether two pluralization rules have the same source string. /// /// Important when deciding whether a pluralization rule is part of the same `TranslationUnit`. @@ -123,7 +192,7 @@ public struct TranslationUnit { public var target: String public var files: [String] = [] public var note: String? - public var pluralizationRules: [PluralizationRule]? + public var pluralizationRules: [PluralizationRule] } extension TranslationUnit: Equatable { @@ -138,39 +207,163 @@ extension TranslationUnit: Equatable { } extension TranslationUnit { + private static let LOCALIZED_FORMAT_KEY_PREFIX = "%#@" + private static let LOCALIZED_FORMAT_KEY_SUFFIX:Character = "@" + + /// Tags used by Apple's .xcstrings format + private static let XCSTRINGS_PLURAL_RULE_PREFIX = "plural" + private static let XCSTRINGS_DEVICE_RULE_PREFIX = "device" + private static let XCSTRINGS_SUBSTITUTIONS_RULE_PREFIX = "substitutions" + + /// The types of the generated ICU rule by the `generateICURuleIfPossible` method. + public enum ICURuleType { + // Pluralization + case Plural + // Vary by device + case Device + // Substitution (multiple variables) + case Substitutions + // Something unexpected / not yet supported + case Other + } + + /// All possible errors emitted by the `generateICURuleIfPossible` method. + public enum ICUError: Error, CustomDebugStringConvertible { + // Not supported + case notSupported(ICURuleType) + // No pluralization rules found to process. Not really an error. + case noRules + // The legacy pluralization rules (.stringsdict) contain a localized + // format key that is not on the format that Apple recommends. + case malformedPluralizedFormat(String) + // The method could not generate an ICU rule based on the provided + // pluralization rules. + case emptyRule + + public var debugDescription: String { + switch (self) { + case .notSupported(let type): + return "Pluralization rule not supported: \(type)" + case .noRules: + return "No pluralization rules found" + case .malformedPluralizedFormat(let error): + return "Malformed pluralized format detected: \(error)" + case .emptyRule: + return "Unable to generate ICU rule" + } + } + } + /// If the current `TranslationUnit` contains a number of `PluralizationRule` objects in its /// property, then the method attempts to generate an ICU rule out of them that can be pushed to CDS. /// /// - Returns: The ICU pluralization rule if its generation is possible, nil otherwise. - public func generateICURuleIfPossible() -> String? { - guard let pluralizationRules = pluralizationRules, - pluralizationRules.count > 0 else { - return nil + public func generateICURuleIfPossible() -> Result<(String, ICURuleType), ICUError> { + guard pluralizationRules.count > 0 else { + return .failure(.noRules) } var icuRules : [String] = [] - - for pluralizationRule in pluralizationRules { - if pluralizationRule.containsLocalizedFormatKey { - continue - } - - guard let pluralRule = pluralizationRule.pluralRule else { - continue + + let activeStringsSourceType = pluralizationRules.map { $0.stringsSourceType }.first + + // For the legacy .stringsdict format, require the localized format key + // to have the %#@[KEY]@ format. Otherwise do not process it. + // As per documentation: + // > If the formatted string contains multiple variables, enter a separate subdictionary for each variable. + // Ref: https://developer.apple.com/documentation/xcode/localizing-strings-that-contain-plurals + // So for example, the following is correct: + // + // + // %#@lu_devices@ + // %#@lu_devices@ + // + // + // + // Message is sent to %lu device. + // Message is sent to %lu device. + // + // + // + // Message is sent to %lu devices. + // Message is sent to %lu devices. + // + // + // + // while this is wrong: + // + // + // Message is sent to %#@lu_devices@. + // Message is sent to %#@lu_devices@. + // + // + // + // %lu device + // %lu device + // + // + // + // %lu devices + // %lu devices + // + // + if activeStringsSourceType == .StringsDict, + let target = pluralizationRules.filter({ $0.containsLocalizedFormatKey}).first?.target, + !(target.starts(with: Self.LOCALIZED_FORMAT_KEY_PREFIX) && target.last == Self.LOCALIZED_FORMAT_KEY_SUFFIX) { + return .failure(.malformedPluralizedFormat(target)) + } + + var isICUFriendly = false + + if activeStringsSourceType == .StringsDict { + isICUFriendly = true + } + else if let pluralRule = pluralizationRules.first?.pluralRule, + pluralRule.starts(with: "\(Self.XCSTRINGS_PLURAL_RULE_PREFIX).") { + isICUFriendly = true + } + + if isICUFriendly { + for pluralizationRule in pluralizationRules { + if pluralizationRule.containsLocalizedFormatKey { + continue + } + + guard let pluralRule = pluralizationRule.pluralRule else { + continue + } + + guard let target = pluralizationRule.target else { + continue + } + + let normalizedRule = pluralRule.replacingOccurrences(of: "\(Self.XCSTRINGS_PLURAL_RULE_PREFIX).", + with: "") + icuRules.append("\(normalizedRule) {\(target)}") } - - guard let target = pluralizationRule.target else { - continue + + guard icuRules.count > 0 else { + return .failure(.emptyRule) } - - icuRules.append("\(pluralRule) {\(target)}") + + return .success(("{cnt, plural, \(icuRules.joined(separator: " "))}", .Plural)) } - - guard icuRules.count > 0 else { - return nil + else { + var icuRuleType: ICURuleType = .Other + + if let rule = pluralizationRules.first?.pluralRule?.components(separatedBy: ".").first { + switch rule { + case Self.XCSTRINGS_DEVICE_RULE_PREFIX: + icuRuleType = .Device + case Self.XCSTRINGS_SUBSTITUTIONS_RULE_PREFIX: + icuRuleType = .Substitutions + default: + icuRuleType = .Other + } + } + + return .failure(.notSupported(icuRuleType)) } - - return "{cnt, plural, \(icuRules.joined(separator: " "))}" } } @@ -188,13 +381,15 @@ public class XLIFFParser: NSObject { private static let XML_FILE_NAME = "file" private static let XML_ID_ATTRIBUTE = "id" private static let XML_ORIGINAL_ATTRIBUTE = "original" - + + private var pendingTranslationUnits: [PendingTranslationUnit] = [] + private var pendingPluralizationRules: [PluralizationRule] = [] + private var activeTranslationUnit: PendingTranslationUnit? - private var activeElement: String? - private var activeFile: String? + private var activeElementName: String? + private var activeFileName: String? private var parseError: Error? - private var parsesStringDict = false private var activePluralizationRule: PluralizationRule? /// Internal struct that's used as a temporary data structure by the XML parser to store optional fields @@ -229,38 +424,6 @@ public class XLIFFParser: NSObject { /// /// If the file cannot be found, the constructor returns a nil object. /// - /// You can find a sample of an XLIFF file below: - /// - /// ``` - /// - /// - /// - ///
- /// - ///
- /// - /// - /// Label - /// A localized label - /// Class = "UILabel"; text = "Label"; ObjectID = "7pN-ag-DRB"; Note = "The main label of the app"; - /// - /// - ///
- /// - ///
- /// - ///
- /// - /// - /// This is a subtitle - /// This is a subtitle - /// The subtitle label set programatically - /// - /// - ///
- ///
- /// ``` - /// /// - Parameters: /// - fileURL: The url of the XLIFF file /// - logHandler: Optional log handler @@ -393,9 +556,7 @@ public class XLIFFParser: NSObject { note = result.note } - if pluralizationRules == nil && result.pluralizationRules != nil { - pluralizationRules = result.pluralizationRules - } + pluralizationRules = result.pluralizationRules } // If for some reason those units don't have the same id, source @@ -421,37 +582,73 @@ public class XLIFFParser: NSObject { return consolidatedResults } - - private func appendActivePluralizationRuleToTranslationUnit() { - guard let activePluralizationRule = activePluralizationRule else { + + /// Adds pending translation units and pluralization rules to the results array. + private func processPendingStructures() { + guard let fileName = activeFileName else { return } - - activeTranslationUnit?.pluralizationRules.append(activePluralizationRule) - } - - private func appendTranslationUnitToResults() { - guard let activeTranslationUnit = activeTranslationUnit, - let file = activeFile, - let source = activeTranslationUnit.source, - let target = activeTranslationUnit.target else { - return + + // Process the translation units first + for pendingTranslationUnit in pendingTranslationUnits { + guard let source = pendingTranslationUnit.source, + let target = pendingTranslationUnit.target else { + continue + } + + let id = pendingTranslationUnit.id + + // Find the associated pluralization rules for this translation + // unit. + let pluralizationRules = pendingPluralizationRules.filter { + $0.sourceString == id + } + + let translationUnit = TranslationUnit(id: pendingTranslationUnit.id, + source: source, + target: target, + files: [fileName], + note: pendingTranslationUnit.note, + pluralizationRules: pluralizationRules) + results.append(translationUnit) + + // Remove the found rules from the pending array. + pendingPluralizationRules.removeAll { $0.sourceString == id } } - - var pluralizationRules : [PluralizationRule]? = nil - - if activeTranslationUnit.pluralizationRules.count > 0 { - pluralizationRules = activeTranslationUnit.pluralizationRules + + pendingTranslationUnits.removeAll() + + // If there are leftover pending pluralization rules, it means that they + // do not have an associated translation unit. + if pendingPluralizationRules.count > 0 { + // Group them based on their source string (use Set to use only the + // unique source strings). + let sourceStrings = Set(pendingPluralizationRules.map { + $0.sourceString + }).sorted() + + // Process each group. + for sourceString in sourceStrings { + let pluralizationRules = pendingPluralizationRules.filter { + $0.sourceString == sourceString + } + + if pluralizationRules.count == 0 { + continue + } + + // Create a translation unit that hosts those rules. + let translationUnit = TranslationUnit(id: sourceString, + source: sourceString, + target: sourceString, + files: [fileName], + note: nil, + pluralizationRules: pluralizationRules) + results.append(translationUnit) + } } - - let translationUnit = TranslationUnit(id: activeTranslationUnit.id, - source: source, - target: target, - files: [file], - note: activeTranslationUnit.note, - pluralizationRules: pluralizationRules) - - results.append(translationUnit) + + pendingPluralizationRules.removeAll() } } @@ -459,93 +656,61 @@ extension XLIFFParser : XMLParserDelegate { public func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String] = [:]) { - if elementName == XLIFFParser.XML_TRANSUNIT_NAME, - let id = attributeDict[XLIFFParser.XML_ID_ATTRIBUTE] { - - if parsesStringDict, - let pluralizationRule = PluralizationRule(with: id) { - - var shouldCreateTranslationUnit = false - - if let activePluralizationRule = activePluralizationRule, - !activePluralizationRule.hasSameSourceString(with: pluralizationRule) { - shouldCreateTranslationUnit = true - } - else if activeTranslationUnit == nil { - shouldCreateTranslationUnit = true - } - - if activePluralizationRule != nil - && activeTranslationUnit != nil { - appendActivePluralizationRuleToTranslationUnit() - } - - if activeTranslationUnit != nil - && shouldCreateTranslationUnit { - appendTranslationUnitToResults() - } - + // + if elementName == Self.XML_TRANSUNIT_NAME, + let id = attributeDict[Self.XML_ID_ATTRIBUTE] { + + if let pluralizationRule = PluralizationRule(with: id) { activePluralizationRule = pluralizationRule - - if shouldCreateTranslationUnit { - activeTranslationUnit = PendingTranslationUnit(id: pluralizationRule.sourceString, - source: pluralizationRule.sourceString, - target: pluralizationRule.sourceString) - } } else { activeTranslationUnit = PendingTranslationUnit(id: id) } } - else if elementName == XLIFFParser.XML_SOURCE_NAME - || elementName == XLIFFParser.XML_TARGET_NAME - || elementName == XLIFFParser.XML_NOTE_NAME { - if activeTranslationUnit != nil { - activeElement = elementName - } + // , , + else if elementName == Self.XML_SOURCE_NAME + || elementName == Self.XML_TARGET_NAME + || elementName == Self.XML_NOTE_NAME { + activeElementName = elementName } - else if elementName == XLIFFParser.XML_FILE_NAME, - let original = attributeDict[XLIFFParser.XML_ORIGINAL_ATTRIBUTE]{ - activeFile = original - - let fileURL = URL(fileURLWithPath: original) - - if fileURL.pathExtension == "stringsdict" { - parsesStringDict = true - } + // + else if elementName == Self.XML_FILE_NAME, + let original = attributeDict[Self.XML_ORIGINAL_ATTRIBUTE]{ + activeFileName = original } } public func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) { - if elementName == XLIFFParser.XML_TRANSUNIT_NAME - && !parsesStringDict { - appendTranslationUnitToResults() - activeTranslationUnit = nil - } - else if elementName == XLIFFParser.XML_SOURCE_NAME - || elementName == XLIFFParser.XML_TARGET_NAME - || elementName == XLIFFParser.XML_NOTE_NAME { - if activeTranslationUnit != nil { - activeElement = nil + // + if elementName == Self.XML_TRANSUNIT_NAME { + // If the translation unit contained a pluralization rule, append + // it to the active translation unit. + if let activePluralizationRule = activePluralizationRule { + pendingPluralizationRules.append(activePluralizationRule) + self.activePluralizationRule = nil } - } - else if elementName == XLIFFParser.XML_FILE_NAME { - if parsesStringDict { - appendActivePluralizationRuleToTranslationUnit() - appendTranslationUnitToResults() - - activeTranslationUnit = nil - activePluralizationRule = nil - parsesStringDict = false + else if let activeTranslationUnit = activeTranslationUnit { + pendingTranslationUnits.append(activeTranslationUnit) + self.activeTranslationUnit = nil } - - activeFile = nil + } + // , , + else if elementName == Self.XML_SOURCE_NAME + || elementName == Self.XML_TARGET_NAME + || elementName == Self.XML_NOTE_NAME { + activeElementName = nil + } + // + else if elementName == Self.XML_FILE_NAME { + processPendingStructures() + activeFileName = nil } } public func parser(_ parser: XMLParser, foundCharacters string: String) { - if activeElement == XLIFFParser.XML_SOURCE_NAME { + // {SOMETHING} + if activeElementName == Self.XML_SOURCE_NAME { if activePluralizationRule != nil { activePluralizationRule?.updateSource(string) } @@ -553,7 +718,8 @@ extension XLIFFParser : XMLParserDelegate { activeTranslationUnit?.updateSource(string) } } - else if activeElement == XLIFFParser.XML_TARGET_NAME { + // {SOMETHING} + else if activeElementName == Self.XML_TARGET_NAME { if activePluralizationRule != nil { activePluralizationRule?.updateTarget(string) } @@ -561,7 +727,8 @@ extension XLIFFParser : XMLParserDelegate { activeTranslationUnit?.updateTarget(string) } } - else if activeElement == XLIFFParser.XML_NOTE_NAME { + // {SOMETHING} + else if activeElementName == Self.XML_NOTE_NAME { if activePluralizationRule != nil { activePluralizationRule?.updateNote(string) } diff --git a/Tests/TXCliTests/XLIFFParserTests.swift b/Tests/TXCliTests/XLIFFParserTests.swift index a573880..aea8063 100644 --- a/Tests/TXCliTests/XLIFFParserTests.swift +++ b/Tests/TXCliTests/XLIFFParserTests.swift @@ -74,7 +74,8 @@ final class XLIFFParserTests: XCTestCase { source: "Label", target: "A localized label", files: ["project/Base.lproj/Main.storyboard"], - note: "Class = \"UILabel\"; text = \"Label\"; ObjectID = \"7pN-ag-DRB\"; Note = \"The main label of the app\";") + note: "Class = \"UILabel\"; text = \"Label\"; ObjectID = \"7pN-ag-DRB\"; Note = \"The main label of the app\";", + pluralizationRules: []) XCTAssertEqual(results[0], translationOne) @@ -82,12 +83,105 @@ final class XLIFFParserTests: XCTestCase { source: "This is a subtitle", target: "This is a subtitle", files: ["project/en.lproj/Localizable.strings"], - note: "The subtitle label set programatically") + note: "The subtitle label set programatically", + pluralizationRules: []) XCTAssertEqual(results[1], translationTwo) } - func testXLIFFParserWithStringsDict() { + func testXLIFFParserWithXCStrings() throws { + let fileURL = tempXLIFFFileURL() + let sampleXLIFF = """ + + + +
+ +
+ + + I find your lack of faith disturbing. + I find your lack of faith disturbing. + + + + Powerful you have become, the dark side I sense in you. + Powerful you have become, the dark side I sense in you. + + + + test string + test string + Test comment + + + %d minute + %d minute + dminutes + + + %d minutes + %d minutes + dminutes + + + %u minute + %u minute + uminutes + + + %u minutes + %u minutes + uminutes + + +
+
+""" + do { + try sampleXLIFF.write(to: fileURL, atomically: true, encoding: .utf8) + } + catch { } + + let xliffParser = XLIFFParser(fileURL: fileURL) + XCTAssertNotNil(xliffParser, "Failed to initialize parser") + + let parsed = xliffParser!.parse() + + XCTAssertTrue(parsed) + + let results = xliffParser!.results + + XCTAssertTrue(results.count == 5) + + do { + let pluralizedResult = results[3] + + XCTAssertTrue(pluralizedResult.pluralizationRules.count == 2) + + let icuRule = try pluralizedResult.generateICURuleIfPossible().get() + + let expectedIcuRule = "{cnt, plural, one {%d minute} other {%d minutes}}" + + XCTAssertEqual(icuRule.0, expectedIcuRule) + XCTAssertEqual(icuRule.1, .Plural) + } + + do { + let pluralizedResult = results[4] + + XCTAssertTrue(pluralizedResult.pluralizationRules.count == 2) + + let icuRule = try pluralizedResult.generateICURuleIfPossible().get() + + let expectedIcuRule = "{cnt, plural, one {%u minute} other {%u minutes}}" + + XCTAssertEqual(icuRule.0, expectedIcuRule) + XCTAssertEqual(icuRule.1, .Plural) + } + } + + func testXLIFFParserWithStringsDict() throws { let fileURL = tempXLIFFFileURL() let sampleXLIFF = """ @@ -132,17 +226,17 @@ final class XLIFFParserTests: XCTestCase { XCTAssertTrue(results.count == 1) - let pluralizationRules = results.first!.pluralizationRules + XCTAssertTrue(results.first!.pluralizationRules.count == 3) - XCTAssertTrue(pluralizationRules?.count == 3) - - let icuRule = results.first!.generateICURuleIfPossible() + let icuRule = try results.first!.generateICURuleIfPossible().get() + let expectedIcuRule = "{cnt, plural, one {%d minute} other {%d minutes}}" - - XCTAssertEqual(icuRule, expectedIcuRule) + + XCTAssertEqual(icuRule.0, expectedIcuRule) + XCTAssertEqual(icuRule.1, .Plural) } - - func testXLIFFResultConsolidation() { + + func testXLIFFResultConsolidation() throws { let fileURL = tempXLIFFFileURL() let sampleXLIFF = """ @@ -206,15 +300,15 @@ final class XLIFFParserTests: XCTestCase { let result = consolidatedResults.first! XCTAssertNotNil(result.note) - - XCTAssertNotNil(result.pluralizationRules) - - XCTAssertTrue(result.pluralizationRules!.count == 3) - let icuRule = result.generateICURuleIfPossible() + XCTAssertTrue(result.pluralizationRules.count == 3) + + let icuRule = try result.generateICURuleIfPossible().get() + let expectedIcuRule = "{cnt, plural, one {%d minute} other {%d minutes}}" - - XCTAssertEqual(icuRule, expectedIcuRule) + + XCTAssertEqual(icuRule.0, expectedIcuRule) + XCTAssertEqual(icuRule.1, .Plural) } func testXLIFFParserWithQuotes() {