diff --git a/Sources/SwiftPolyglot/main.swift b/Sources/SwiftPolyglot/main.swift index fb06428..7bc4e28 100755 --- a/Sources/SwiftPolyglot/main.swift +++ b/Sources/SwiftPolyglot/main.swift @@ -8,12 +8,13 @@ else { exit(EXIT_FAILURE) } -let swiftPolyglot: SwiftPolyglot = .init( - arguments: Array(CommandLine.arguments.dropFirst()), - filePaths: filePaths -) - do { + let swiftPolyglot: SwiftPolyglot = try .init( + arguments: Array(CommandLine.arguments.dropFirst()), + filePaths: filePaths, + runningOnAGitHubAction: ProcessInfo.processInfo.environment["GITHUB_ACTIONS"] == "true" + ) + try swiftPolyglot.run() } catch { print(error.localizedDescription) diff --git a/Sources/SwiftPolyglotCore/SwiftPolyglot.swift b/Sources/SwiftPolyglotCore/SwiftPolyglot.swift index 8e993e6..dd2e9d4 100644 --- a/Sources/SwiftPolyglotCore/SwiftPolyglot.swift +++ b/Sources/SwiftPolyglotCore/SwiftPolyglot.swift @@ -1,130 +1,192 @@ import Foundation public struct SwiftPolyglot { + private static let errorOnMissingArgument = "--errorOnMissing" + private let arguments: [String] private let filePaths: [String] + private let languageCodes: [String] + private let runningOnAGitHubAction: Bool - public init(arguments: [String], filePaths: [String]) { - self.arguments = arguments - self.filePaths = filePaths + private var logErrorOnMissing: Bool { + arguments.contains(Self.errorOnMissingArgument) } - public func run() throws { - guard !arguments.isEmpty else { + public init(arguments: [String], filePaths: [String], runningOnAGitHubAction: Bool) throws { + let languageCodes = arguments[0].split(separator: ",").map(String.init) + + guard + !languageCodes.contains(Self.errorOnMissingArgument), + !languageCodes.isEmpty + else { throw SwiftPolyglotError.noLanguageCodes } - let isRunningFromGitHubActions = ProcessInfo.processInfo.environment["GITHUB_ACTIONS"] == "true" - let languages = arguments[0].split(separator: ",").map(String.init) - let errorOnMissing = arguments.contains("--errorOnMissing") + self.arguments = arguments + self.filePaths = filePaths + self.languageCodes = languageCodes + self.runningOnAGitHubAction = runningOnAGitHubAction + } + public func run() throws { var missingTranslations = false - func checkTranslations(in fileURL: URL, for languages: [String]) throws { - guard let data = try? Data(contentsOf: fileURL), - let jsonObject = try? JSONSerialization.jsonObject(with: data), - let jsonDict = jsonObject as? [String: Any], - let strings = jsonDict["strings"] as? [String: [String: Any]] + try searchDirectory(for: languageCodes, missingTranslations: &missingTranslations) + + if missingTranslations, logErrorOnMissing { + throw SwiftPolyglotError.missingTranslations + } else if missingTranslations { + print("Completed with missing translations.") + } else { + print("All translations are present.") + } + } + + private func checkDeviceVariations( + devices: [String: [String: Any]], + originalString: String, + lang: String, + fileURL: URL, + missingTranslations: inout Bool + ) { + for (device, value) in devices { + guard let stringUnit = value["stringUnit"] as? [String: Any], + let state = stringUnit["state"] as? String, state == "translated" else { - if isRunningFromGitHubActions { - print("::warning file=\(fileURL.path)::Could not process file at path: \(fileURL.path)") - } else { - print("Could not process file at path: \(fileURL.path)") - } - return + logWarning( + file: fileURL.path, + message: "'\(originalString)' device '\(device)' is missing or not translated in \(lang) in file: \(fileURL.path)" + ) + missingTranslations = true + continue } + } + } - for (originalString, translations) in strings { - guard let localizations = translations["localizations"] as? [String: [String: Any]] else { - logWarning(file: fileURL.path, message: "'\(originalString)' is not translated in any language in file: \(fileURL.path)") - missingTranslations = true - continue - } - - for lang in languages { - guard let languageDict = localizations[lang] else { - logWarning(file: fileURL.path, message: "'\(originalString)' is missing translations for language: \(lang) in file: \(fileURL.path)") - missingTranslations = true - continue - } - - if let variations = languageDict["variations"] as? [String: [String: [String: Any]]] { - try checkVariations(variations: variations, originalString: originalString, lang: lang, fileURL: fileURL) - } else if let stringUnit = languageDict["stringUnit"] as? [String: Any], - let state = stringUnit["state"] as? String, state != "translated" - { - logWarning(file: fileURL.path, message: "'\(originalString)' is missing or not translated in \(lang) in file: \(fileURL.path)") - missingTranslations = true - } - } + private func checkPluralizations( + pluralizations: [String: [String: Any]], + originalString: String, + lang: String, + fileURL: URL, + missingTranslations: inout Bool + ) { + for (pluralForm, value) in pluralizations { + guard let stringUnit = value["stringUnit"] as? [String: Any], + let state = stringUnit["state"] as? String, state == "translated" + else { + logWarning( + file: fileURL.path, + message: "'\(originalString)' plural form '\(pluralForm)' is missing or not translated in \(lang) in file: \(fileURL.path)" + ) + missingTranslations = true + continue } } + } - func checkVariations(variations: [String: [String: [String: Any]]], originalString: String, lang: String, fileURL: URL) throws { - for (variationKey, variationDict) in variations { - if variationKey == "plural" { - checkPluralizations(pluralizations: variationDict, originalString: originalString, lang: lang, fileURL: fileURL) - } else if variationKey == "device" { - checkDeviceVariations(devices: variationDict, originalString: originalString, lang: lang, fileURL: fileURL) - } else { - throw SwiftPolyglotError.unsupportedVariation(variation: variationKey) - } + private func checkTranslations(in fileURL: URL, for languages: [String], missingTranslations: inout Bool) throws { + guard let data = try? Data(contentsOf: fileURL), + let jsonObject = try? JSONSerialization.jsonObject(with: data), + let jsonDict = jsonObject as? [String: Any], + let strings = jsonDict["strings"] as? [String: [String: Any]] + else { + if runningOnAGitHubAction { + print("::warning file=\(fileURL.path)::Could not process file at path: \(fileURL.path)") + } else { + print("Could not process file at path: \(fileURL.path)") } + return } - func checkPluralizations(pluralizations: [String: [String: Any]], originalString: String, lang: String, fileURL: URL) { - for (pluralForm, value) in pluralizations { - guard let stringUnit = value["stringUnit"] as? [String: Any], - let state = stringUnit["state"] as? String, state == "translated" - else { - logWarning(file: fileURL.path, message: "'\(originalString)' plural form '\(pluralForm)' is missing or not translated in \(lang) in file: \(fileURL.path)") + for (originalString, translations) in strings { + guard let localizations = translations["localizations"] as? [String: [String: Any]] else { + logWarning( + file: fileURL.path, + message: "'\(originalString)' is not translated in any language in file: \(fileURL.path)" + ) + missingTranslations = true + continue + } + + for lang in languages { + guard let languageDict = localizations[lang] else { + logWarning( + file: fileURL.path, + message: "'\(originalString)' is missing translations for language: \(lang) in file: \(fileURL.path)" + ) missingTranslations = true continue } - } - } - func checkDeviceVariations(devices: [String: [String: Any]], originalString: String, lang: String, fileURL: URL) { - for (device, value) in devices { - guard let stringUnit = value["stringUnit"] as? [String: Any], - let state = stringUnit["state"] as? String, state == "translated" - else { - logWarning(file: fileURL.path, message: "'\(originalString)' device '\(device)' is missing or not translated in \(lang) in file: \(fileURL.path)") + if let variations = languageDict["variations"] as? [String: [String: [String: Any]]] { + try checkVariations( + variations: variations, + originalString: originalString, + lang: lang, + fileURL: fileURL, + missingTranslations: &missingTranslations + ) + } else if let stringUnit = languageDict["stringUnit"] as? [String: Any], + let state = stringUnit["state"] as? String, state != "translated" + { + logWarning( + file: fileURL.path, + message: "'\(originalString)' is missing or not translated in \(lang) in file: \(fileURL.path)" + ) missingTranslations = true - continue } } } + } - func searchDirectory() throws { - for filePath in filePaths { - if filePath.hasSuffix(".xcstrings") { - let fileURL = URL(fileURLWithPath: filePath) - try checkTranslations(in: fileURL, for: languages) - } + private func checkVariations( + variations: [String: [String: [String: Any]]], + originalString: String, + lang: String, + fileURL: URL, + missingTranslations: inout Bool + ) throws { + for (variationKey, variationDict) in variations { + if variationKey == "plural" { + checkPluralizations( + pluralizations: variationDict, + originalString: originalString, + lang: lang, + fileURL: fileURL, + missingTranslations: &missingTranslations + ) + } else if variationKey == "device" { + checkDeviceVariations( + devices: variationDict, + originalString: originalString, + lang: lang, + fileURL: fileURL, + missingTranslations: &missingTranslations + ) + } else { + throw SwiftPolyglotError.unsupportedVariation(variation: variationKey) } } - - func logWarning(file: String, message: String) { - if isRunningFromGitHubActions { - if errorOnMissing { - print("::error file=\(file)::\(message)") - } else { - print("::warning file=\(file)::\(message)") - } + } + + private func logWarning(file: String, message: String) { + if runningOnAGitHubAction { + if logErrorOnMissing { + print("::error file=\(file)::\(message)") } else { - print(message) + print("::warning file=\(file)::\(message)") } + } else { + print(message) } + } - try searchDirectory() - - if missingTranslations, errorOnMissing { - throw SwiftPolyglotError.missingTranslations - } else if missingTranslations { - print("Completed with missing translations.") - } else { - print("All translations are present.") + private func searchDirectory(for languages: [String], missingTranslations: inout Bool) throws { + for filePath in filePaths { + if filePath.hasSuffix(".xcstrings") { + let fileURL = URL(fileURLWithPath: filePath) + try checkTranslations(in: fileURL, for: languages, missingTranslations: &missingTranslations) + } } } } diff --git a/Tests/SwiftPolyglotCoreTests/SwiftPolyglotCoreTests.swift b/Tests/SwiftPolyglotCoreTests/SwiftPolyglotCoreTests.swift index dc2dbe3..5f7f21c 100644 --- a/Tests/SwiftPolyglotCoreTests/SwiftPolyglotCoreTests.swift +++ b/Tests/SwiftPolyglotCoreTests/SwiftPolyglotCoreTests.swift @@ -14,9 +14,10 @@ final class SwiftPolyglotCoreTests: XCTestCase { return } - let swiftPolyglot: SwiftPolyglot = .init( + let swiftPolyglot: SwiftPolyglot = try .init( arguments: ["ca,de,en,es"], - filePaths: [stringCatalogFilePath] + filePaths: [stringCatalogFilePath], + runningOnAGitHubAction: false ) XCTAssertNoThrow(try swiftPolyglot.run()) @@ -34,9 +35,10 @@ final class SwiftPolyglotCoreTests: XCTestCase { return } - let swiftPolyglot: SwiftPolyglot = .init( + let swiftPolyglot: SwiftPolyglot = try .init( arguments: ["ca,de,en,es"], - filePaths: [stringCatalogFilePath] + filePaths: [stringCatalogFilePath], + runningOnAGitHubAction: false ) XCTAssertNoThrow(try swiftPolyglot.run()) @@ -54,9 +56,10 @@ final class SwiftPolyglotCoreTests: XCTestCase { return } - let swiftPolyglot: SwiftPolyglot = .init( + let swiftPolyglot: SwiftPolyglot = try .init( arguments: ["ca,de,en,es", "--errorOnMissing"], - filePaths: [stringCatalogFilePath] + filePaths: [stringCatalogFilePath], + runningOnAGitHubAction: false ) XCTAssertThrowsError(try swiftPolyglot.run()) @@ -74,9 +77,10 @@ final class SwiftPolyglotCoreTests: XCTestCase { return } - let swiftPolyglot: SwiftPolyglot = .init( + let swiftPolyglot: SwiftPolyglot = try .init( arguments: ["de,en", "--errorOnMissing"], - filePaths: [stringCatalogFilePath] + filePaths: [stringCatalogFilePath], + runningOnAGitHubAction: false ) XCTAssertThrowsError(try swiftPolyglot.run())