Skip to content

Commit

Permalink
Minor refactor of core functionality (appdecostudio#14)
Browse files Browse the repository at this point in the history
* Move arguments check to a computed property

* Pass process info property as initializer parameter

* Define functions as type methods

* Format

* Move language codes parsing into a failable initializer
  • Loading branch information
pereBohigas committed Apr 1, 2024
1 parent ca08f0e commit 573037a
Show file tree
Hide file tree
Showing 3 changed files with 169 additions and 102 deletions.
11 changes: 6 additions & 5 deletions Sources/SwiftPolyglot/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
240 changes: 151 additions & 89 deletions Sources/SwiftPolyglotCore/SwiftPolyglot.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
}
20 changes: 12 additions & 8 deletions Tests/SwiftPolyglotCoreTests/SwiftPolyglotCoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -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())
Expand All @@ -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())
Expand All @@ -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())
Expand Down

0 comments on commit 573037a

Please sign in to comment.