Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Model Setup for String Catalog #14

Merged
merged 21 commits into from
Jan 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Uni-boy marked this conversation as resolved.
Show resolved Hide resolved

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 {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are dead codes too but I'll need to further look into it.

// 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())
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more thing: could you add some to(equal()) evaluation please?


// 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"))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eventually we want to use Decodable for all these JSON objects, but this PR is good as is.

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"
}
"""
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And here.

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")
}
}
}
}
}