diff --git a/StringCatalogEnum/Sources/StringCatalogEnum/main.swift b/StringCatalogEnum/Sources/StringCatalogEnum/main.swift index b6a2ea1..aa56ac3 100644 --- a/StringCatalogEnum/Sources/StringCatalogEnum/main.swift +++ b/StringCatalogEnum/Sources/StringCatalogEnum/main.swift @@ -36,13 +36,9 @@ struct StringCatalogEnum: ParsableCommand { let data = try Data(contentsOf: url) print(data) - guard let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { - throw Error.unexpectedJSON(message: "cannot parse first level object") - } + let decoder = JSONDecoder() + let strings = try decoder.decode(Localizations.self, from: data) - guard let strings = json["strings"] as? [String: Any] else { - throw Error.unexpectedJSON(message: "cannot parse `strings`") - } var output = """ // This file is generated by XcodeStringEnum. Please do *NOT* update it manually. diff --git a/StringCatalogEnum/Sources/StringCatalogEnum/model.swift b/StringCatalogEnum/Sources/StringCatalogEnum/model.swift deleted file mode 100644 index 5f8a01d..0000000 --- a/StringCatalogEnum/Sources/StringCatalogEnum/model.swift +++ /dev/null @@ -1,9 +0,0 @@ -// TODO: make XCStrings Decodable by listing all possible models -// Example: let obj = try JSONDecoder().decode(XCStrings.self, from: data) -/* - struct XCStrings: Decodable { - let sourceLanguage: String - let version: String - let strings: [Strsing: String] - } - */ diff --git a/StringCatalogEnum/Sources/StringCatalogEnumLibrary/Models.swift b/StringCatalogEnum/Sources/StringCatalogEnumLibrary/Models.swift new file mode 100644 index 0000000..9bd5895 --- /dev/null +++ b/StringCatalogEnum/Sources/StringCatalogEnumLibrary/Models.swift @@ -0,0 +1,77 @@ +/// Represents the root structure of the xcstrings JSON. +/// This struct is designed to handle various JSON formats, including those +/// generated from SwiftUI and manually added translations. +/// +/// Examples of supported JSON formats: +/// +/// 1. Generated from SwiftUI (no translation added): +/// ``` +/// "Home": {} +/// ``` +/// +/// 2. Generated from SwiftUI, with an English translation: +/// ``` +/// "Login": { +/// "localizations": { +/// "en": { +/// "stringUnit": { +/// "state": "translated", +/// "value": "Login" +/// } +/// } +/// } +/// } +/// ``` +/// +/// 3. Manually added, English only: +/// ``` +/// "welcomeBack": { +/// "extractionState": "manual", +/// "localizations": { +/// "en": { +/// "stringUnit": { +/// "state": "translated", +/// "value": "Welcome back" +/// } +/// } +/// } +/// } +/// ``` +public struct Localizations: Decodable { + public let sourceLanguage: String + public let version: String + public let strings: [String: StringInfo] +} + +public struct StringInfo: Decodable { + public let extractionState: String? + public let localizations: [String: Localization]? +} + +public struct Localization: Decodable { + public let stringUnit: StringUnit? + // let variations: Variations? +} + +// struct Variations: Decodable { +// let plural: PluralVariations? +// let device: DeviceVariations? +// } + +// struct PluralVariations: Decodable { +// let one: StringUnitWrapper? +// let other: StringUnitWrapper? +// } + +// struct DeviceVariations: Decodable { +// let variations: [String: StringUnitWrapper]? +// } + +// struct StringUnitWrapper: Decodable { +// let stringUnit: StringUnit +// } + +public struct StringUnit: Decodable { + public let state: String + public let value: String +} \ No newline at end of file diff --git a/StringCatalogEnum/Sources/StringCatalogEnumLibrary/StringEnumHelper.swift b/StringCatalogEnum/Sources/StringCatalogEnumLibrary/StringEnumHelper.swift index 91a5c8d..10dffd5 100644 --- a/StringCatalogEnum/Sources/StringCatalogEnumLibrary/StringEnumHelper.swift +++ b/StringCatalogEnum/Sources/StringCatalogEnumLibrary/StringEnumHelper.swift @@ -10,11 +10,12 @@ public struct StringEnumHelper { /// - stringData: A dictionary containing string data. /// - keyNameMatches: A boolean flag indicating whether the enum cases should match the keys exactly. /// - keywordEnum: An array of raw values from the Keyword enum in StringCatalogEnum struct - public func createEnumKeys(with stringData: [String: Any], keyNameMatches: Bool, keywordEnum: [String]) -> String { + public func createEnumKeys(with stringData: Localizations, keyNameMatches: Bool, keywordEnum: [String]) -> String { var partialOutput = "" var cases = [String]() var knownCases = [String]() - for (key, _) in stringData { + + for (key, data) in stringData.strings { guard let name = convertToVariableName(key: key) else { print("SKIPPING: \(key)") continue @@ -36,7 +37,22 @@ public struct StringEnumHelper { } knownCases.append(name) - // TODO: extract `localizations.en.stringUnit.value` and add in comments as inline documents + // Extract localization values and format them for comments + var localizationComments = [String]() + if let localizations = data.localizations { + for (languageCode, localization) in localizations { + if let stringUnit = localization.stringUnit { + let value = stringUnit.value.replacingOccurrences(of: "\n", with: " ") + localizationComments.append(" /// '\(languageCode)': \"\(value)\"") + } + } + } + + if localizationComments.isEmpty { + localizationComments.append(" /// No localizations available") + } + + let comment = localizationComments.joined(separator: "\n") let caseString: String = if keywordEnum.contains(name) { keyNameMatches @@ -48,7 +64,7 @@ public struct StringEnumHelper { : " case \(name) = \"\(key.replacingOccurrences(of: "\n", with: ""))\"\n" } - cases.append(caseString) + cases.append("\(comment)\n\(caseString)") } cases.sort() cases.forEach { string in @@ -58,7 +74,7 @@ public struct StringEnumHelper { return partialOutput } - /// Convert a Strint Catalog key to a Swift variable name. + /// Convert a String Catalog key to a Swift variable name. public func convertToVariableName(key: String) -> String? { var result = key // Check if the entire string is uppercase diff --git a/StringCatalogEnum/Tests/StringCatalogEnumTests/StringCatalogEnumTests.swift b/StringCatalogEnum/Tests/StringCatalogEnumTests/StringCatalogEnumTests.swift index 2db024f..77e27e7 100644 --- a/StringCatalogEnum/Tests/StringCatalogEnumTests/StringCatalogEnumTests.swift +++ b/StringCatalogEnum/Tests/StringCatalogEnumTests/StringCatalogEnumTests.swift @@ -1,18 +1,7 @@ import Nimble import Quick import StringCatalogEnumLibrary - -final class StringCatalogEnumSpec: QuickSpec { - override class func spec() { - context("StringCatalogEnum") { - describe("Example") { - it("should be replaced") { - expect(true).toNot(equal(false)) - } - } - } - } -} +import Foundation /// Tests the convertToVariableName() function in StringKeyModel final class StringKeyModelSpec: QuickSpec { @@ -50,5 +39,133 @@ final class StringKeyModelSpec: QuickSpec { } } } + + describe("a decodable model") { + it("can decode the json data with key only") { + let json = """ + { + "sourceLanguage" : "en", + "strings" : { + "Home" : { + }, + }, + "version" : "1.0" + } + """ + guard let jsonData = json.data(using: .utf8) else { + fatalError("Invalid JSON string") + } + let decoder = JSONDecoder() + expect{ + try decoder.decode(Localizations.self, from: jsonData) + }.toNot(throwError()) + + // Verify the decoded data + if let decodedData = try? decoder.decode(Localizations.self, from: jsonData) { + // Verify the sourceLanguage + expect(decodedData.sourceLanguage).to(equal("en")) + + // Verify the version + expect(decodedData.version).to(equal("1.0")) + + // Verify the contents of 'strings' dictionary + // Here we verify it with Not Be Nil because we design the model not to be optional + expect(decodedData.strings["Home"]).toNot(beNil()) + } else { + fail("Failed to decode Localizations") + } + } + + it("can decode the json data with English translation added") { + let json = """ + { + "sourceLanguage" : "en", + "strings" : { + "Login" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Login" + } + } + } + }, + }, + "version" : "1.0" + } + """ + guard let jsonData = json.data(using: .utf8) else { + fatalError("Invalid JSON string") + } + let decoder = JSONDecoder() + expect{ + try decoder.decode(Localizations.self, from: jsonData) + }.toNot(throwError()) + + // Verify the decoded data + if let decodedData = try? decoder.decode(Localizations.self, from: jsonData) { + // Verify the sourceLanguage + expect(decodedData.sourceLanguage).to(equal("en")) + + // Verify the version + expect(decodedData.version).to(equal("1.0")) + + // Verify the contents of 'strings' dictionary + expect(decodedData.strings["Login"]).toNot(beNil()) + expect(decodedData.strings["Login"]?.localizations?["en"]?.stringUnit?.state).to(equal("translated")) + expect(decodedData.strings["Login"]?.localizations?["en"]?.stringUnit?.value).to(equal("Login")) + } else { + fail("Failed to decode Localizations") + } + } + + it("can decode the json data with English translation manually added") { + let json = """ + { + "sourceLanguage" : "en", + "strings" : { + "welcomeBack" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Welcome back" + } + } + } + }, + }, + "version" : "1.0" + } + """ + guard let jsonData = json.data(using: .utf8) else { + fatalError("Invalid JSON string") + } + let decoder = JSONDecoder() + expect{ + try decoder.decode(Localizations.self, from: jsonData) + }.toNot(throwError()) + + // To complete this test, we should change all the structs and their attributes to be public + // Verify the decoded data + if let decodedData = try? decoder.decode(Localizations.self, from: jsonData) { + // Verify sourceLanguage + expect(decodedData.sourceLanguage).to(equal("en")) + + // Verify version + expect(decodedData.version).to(equal("1.0")) + + // Verify the contents of 'strings' dictionary + expect(decodedData.strings["welcomeBack"]).toNot(beNil()) + expect(decodedData.strings["welcomeBack"]?.extractionState).to(equal("manual")) + expect(decodedData.strings["welcomeBack"]?.localizations?["en"]?.stringUnit?.state).to(equal("translated")) + expect(decodedData.strings["welcomeBack"]?.localizations?["en"]?.stringUnit?.value).to(equal("Welcome back")) + } else { + fail("JSON data could not be decoded") + } + } + } } }