From a73f4ca7a12c40af30fede4304c39a4c7733d7e3 Mon Sep 17 00:00:00 2001 From: Stelios Petrakis Date: Tue, 12 Mar 2024 14:45:44 +0100 Subject: [PATCH 1/2] 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() { From 99969bfdb4c8a57b77be0ce6dc9852a0ab6cbaaf Mon Sep 17 00:00:00 2001 From: Stelios Petrakis Date: Mon, 1 Apr 2024 20:22:36 +0200 Subject: [PATCH 2/2] Bump version to 2.1.5 * Bumps CLI version to 2.1.5. * Updates the CHANGELOG. --- CHANGELOG.md | 13 +++++++++++++ Sources/TXCli/main.swift | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d95668..c343620 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -116,6 +116,12 @@ always be able to export the source locale from the Xcode project. - Fixes the issue where leading and trailing white space was being added around the extracted ICU pluralization rules. +## Transifex Command Line Tool 2.1.3 + +*October 30, 2023* + +- Adds base SDK option for push command. + ## Transifex Command Line Tool 2.1.4 *March 7, 2024* @@ -124,3 +130,10 @@ the extracted ICU pluralization rules. due to inversion. The option has been replaced by the `--delete-translations` option, in order to allow the underlying `keep_translations` meta flag to be set to `false`. + +## Transifex Command Line Tool 2.1.5 + +*April 1, 2024* + +- Parses and processes the new `.xcstrings` files. Only supports simple +"plural." rules for now. diff --git a/Sources/TXCli/main.swift b/Sources/TXCli/main.swift index db3b3ab..7656229 100644 --- a/Sources/TXCli/main.swift +++ b/Sources/TXCli/main.swift @@ -42,7 +42,7 @@ that can be bundled with the iOS application. The tool can be also used to force CDS cache invalidation so that the next pull command will fetch fresh translations from CDS. """, - version: "2.1.4", + version: "2.1.5", subcommands: [Push.self, Pull.self, Invalidate.self]) }