Skip to content

Commit

Permalink
Merge pull request #14 from Uni-boy/feature/create-decodable-models
Browse files Browse the repository at this point in the history
Model Setup for String Catalog
  • Loading branch information
superarts authored Jan 5, 2024
2 parents 1566c3f + 159ef88 commit e359dd0
Show file tree
Hide file tree
Showing 5 changed files with 229 additions and 32 deletions.
8 changes: 2 additions & 6 deletions StringCatalogEnum/Sources/StringCatalogEnum/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
9 changes: 0 additions & 9 deletions StringCatalogEnum/Sources/StringCatalogEnum/model.swift

This file was deleted.

77 changes: 77 additions & 0 deletions StringCatalogEnum/Sources/StringCatalogEnumLibrary/Models.swift
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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")
}
}
}
}
}

0 comments on commit e359dd0

Please sign in to comment.