diff --git a/CHANGELOG.md b/CHANGELOG.md index 147b312d..06f8b22c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ If needed, pluralize to `Tasks`, `PRs` or `Authors` and list multiple entries se ## [Unreleased] ### Added -- None. +- Adds support for Intent Definition files. ### Changed - None. ### Deprecated diff --git a/Package.swift b/Package.swift index 1bca6e89..0ffcecd9 100644 --- a/Package.swift +++ b/Package.swift @@ -15,8 +15,9 @@ let package = Package( .package(name: "MungoHealer", url: "https://github.com/Flinesoft/MungoHealer.git", from: "0.3.4"), .package(name: "Rainbow", url: "https://github.com/onevcat/Rainbow.git", from: "3.1.5"), .package(name: "SwiftCLI", url: "https://github.com/jakeheis/SwiftCLI.git", from: "6.0.3"), - .package(name: "Toml", url: "https://github.com/jdfergason/swift-toml.git", .branch("master")), .package(name: "SwiftSyntax", url: "https://github.com/apple/swift-syntax.git", from: "0.50500.0"), + .package(name: "SwiftyXML", url: "https://github.com/chenyunguiMilook/SwiftyXML.git", from: "3.1.0"), + .package(name: "Toml", url: "https://github.com/jdfergason/swift-toml.git", .branch("master")), ], targets: [ .executableTarget( @@ -32,6 +33,7 @@ let package = Package( "Rainbow", "SwiftCLI", "SwiftSyntax", + "SwiftyXML", "Toml", ] ), diff --git a/Sources/BartyCrouchKit/FileHandling/StringsFilesSearch.swift b/Sources/BartyCrouchKit/FileHandling/StringsFilesSearch.swift index 6c924fb2..bf17cf61 100644 --- a/Sources/BartyCrouchKit/FileHandling/StringsFilesSearch.swift +++ b/Sources/BartyCrouchKit/FileHandling/StringsFilesSearch.swift @@ -18,6 +18,12 @@ public final class StringsFilesSearch: FilesSearchable { return self.findAllFilePaths(inDirectoryPath: baseDirectoryPath, matching: ibFileRegex) } + public func findAllIntentDefinitionFiles(within baseDirectoryPath: String, withLocale locale: String = "Base") -> [String] { + // swiftlint:disable:next force_try + let intentsFileRegex = try! NSRegularExpression(pattern: "^(.*\\/)?\(locale).lproj.*\\.intentdefinition\\z", options: .caseInsensitive) + return self.findAllFilePaths(inDirectoryPath: baseDirectoryPath, matching: intentsFileRegex) + } + public func findAllStringsFiles(within baseDirectoryPath: String, withLocale locale: String) -> [String] { // swiftlint:disable:next force_try let stringsFileRegex = try! NSRegularExpression(pattern: "^(.*\\/)?\(locale).lproj.*\\.strings\\z", options: .caseInsensitive) diff --git a/Sources/BartyCrouchKit/OldCommandLine/CommandLineActor.swift b/Sources/BartyCrouchKit/OldCommandLine/CommandLineActor.swift index 1f632845..b93e01ec 100644 --- a/Sources/BartyCrouchKit/OldCommandLine/CommandLineActor.swift +++ b/Sources/BartyCrouchKit/OldCommandLine/CommandLineActor.swift @@ -1,6 +1,7 @@ -// swiftlint:disable function_parameter_count type_body_length cyclomatic_complexity +// swiftlint:disable function_parameter_count type_body_length cyclomatic_complexity file_length import Foundation +import SwiftyXML // NOTE: // This file was not refactored as port of the work/big-refactoring branch for version 4.0 to prevent unexpected behavior changes. @@ -76,6 +77,29 @@ public class CommandLineActor { } } + func actOnIntentDefinitions(paths: [String], override: Bool, verbose: Bool, defaultToBase: Bool, unstripped: Bool, ignoreEmptyStrings: Bool) { + let inputFilePaths = paths.flatMap { StringsFilesSearch.shared.findAllIntentDefinitionFiles(within: $0, withLocale: "Base") }.withoutDuplicates() + + guard !inputFilePaths.isEmpty else { print("No input files found.", level: .warning); return } + + for inputFilePath in inputFilePaths { + guard FileManager.default.fileExists(atPath: inputFilePath) else { + print("No file exists at input path '\(inputFilePath)'", level: .error); return + } + + let outputStringsFilePaths = StringsFilesSearch.shared.findAllLocalesForStringsFile(sourceFilePath: inputFilePath).filter { $0 != inputFilePath } + self.incrementalIntentDefinitionUpdate( + inputFilePath, + outputStringsFilePaths, + override: override, + verbose: verbose, + defaultToBase: defaultToBase, + unstripped: unstripped, + ignoreEmptyStrings: ignoreEmptyStrings + ) + } + } + func actOnTranslate(paths: [String], override: Bool, verbose: Bool, secret: Secret, locale: String) { let inputFilePaths = paths.flatMap { StringsFilesSearch.shared.findAllStringsFiles(within: $0, withLocale: locale) }.withoutDuplicates() @@ -320,6 +344,92 @@ public class CommandLineActor { print("Successfully updated strings file(s) of Storyboard or XIB file.", level: .success, file: inputFilePath) } + // swiftlint:disable:next function_body_length + private func incrementalIntentDefinitionUpdate( + _ inputFilePath: String, + _ outputStringsFilePaths: [String], + override: Bool, + verbose: Bool, + defaultToBase: Bool, + unstripped: Bool, + ignoreEmptyStrings: Bool + ) { + let extractedStringsFilePath = inputFilePath + ".tmpstrings" + + // Extract translations + var xmlString = "" + do { + xmlString = try String(contentsOfFile: inputFilePath) + } catch { + print("Could not extract string for file at path '\(inputFilePath)'.", level: .error) + return + } + + let xml = XML(string: xmlString) + var translationDict = [String: String]() + + // Traverse the xml structure and search for translatable values + // To check if a value is translatable, it is sufficient to check if the parent dictionary + // contains a key with the same name and the suffix "ID". + func traverse(xml: XML) { + for child in xml.xmlChildren { + traverse(xml: child) + } + + guard xml.xmlName == "dict" else { return } + + var xmlDict = [String: String]() + var currentKey = "" + for child in xml.xmlChildren { + if child.xmlName == "key" { + currentKey = child.stringValue + } else if child.xmlName == "string" { + xmlDict[currentKey] = child.stringValue + } + } + + for (key, value) in xmlDict { + guard key.hasSuffix("ID") else { continue } + guard let baseTranslation = xmlDict[String(key.prefix(key.count - 2))] else { continue } + + // Convert linebreaks + translationDict[value] = baseTranslation.replacingOccurrences(of: "/\\\n", with: "\\n") + } + } + + traverse(xml: xml) + + let translationString = translationDict.map { key, value in "\"\(key)\" = \"\(value)\";" } + .joined(separator: "\n\n") + + try? translationString.write(toFile: extractedStringsFilePath, atomically: true, encoding: .utf8) + + for outputStringsFilePath in outputStringsFilePaths { + guard let stringsFileUpdater = StringsFileUpdater(path: outputStringsFilePath) else { continue } + + stringsFileUpdater.incrementallyUpdateKeys( + withStringsFileAtPath: extractedStringsFilePath, + addNewValuesAsEmpty: !defaultToBase, + override: override, + keepWhitespaceSurroundings: unstripped, + ignoreEmptyStrings: ignoreEmptyStrings + ) + + if verbose { + print("Incrementally updated keys of file '\(outputStringsFilePath)'.", level: .info) + } + } + + do { + try FileManager.default.removeItem(atPath: extractedStringsFilePath) + } catch { + print("Temporary strings file couldn't be deleted at path '\(extractedStringsFilePath)'", level: .error) + return + } + + print("Successfully updated strings file(s) of Intent definition file.", level: .success, file: inputFilePath) + } + private func translate(secret: Secret, _ inputFilePath: String, _ outputStringsFilePaths: [String], override: Bool, verbose: Bool) { var overallTranslatedValuesCount = 0 var filesWithTranslatedValuesCount = 0 diff --git a/Sources/BartyCrouchKit/TaskHandlers/InterfacesTaskHandler.swift b/Sources/BartyCrouchKit/TaskHandlers/InterfacesTaskHandler.swift index d509695f..f168bda0 100644 --- a/Sources/BartyCrouchKit/TaskHandlers/InterfacesTaskHandler.swift +++ b/Sources/BartyCrouchKit/TaskHandlers/InterfacesTaskHandler.swift @@ -20,6 +20,15 @@ extension InterfacesTaskHandler: TaskHandler { unstripped: options.unstripped, ignoreEmptyStrings: options.ignoreEmptyStrings ) + + CommandLineActor().actOnIntentDefinitions( + paths: options.paths, + override: false, + verbose: GlobalOptions.verbose.value, + defaultToBase: options.defaultToBase, + unstripped: options.unstripped, + ignoreEmptyStrings: options.ignoreEmptyStrings + ) } } }