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

Add xcstrings support #886

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
85 changes: 84 additions & 1 deletion Sources/RswiftParsers/Resources/StringsTable+Parser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Foundation
import RswiftResources

extension StringsTable: SupportedExtensions {
static public let supportedExtensions: Set<String> = ["strings", "stringsdict"]
static public let supportedExtensions: Set<String> = ["strings", "stringsdict", "xcstrings"]

static public func parse(url: URL) throws -> StringsTable {
let warning: (String) -> Void = { print("warning: [R.swift]", $0) }
Expand All @@ -21,6 +21,13 @@ extension StringsTable: SupportedExtensions {
// Get locale from url (second to last component)
let locale = LocaleReference(url: url)

if url.pathExtension == "xcstrings" {
let dictionary: [StringsTable.Key: StringsTable.Value]
let xcstring = try JSONDecoder().decode(XCString.self, from: .init(contentsOf: url))
dictionary = try parseXcstrings(xcstring, source: locale.debugDescription(filename: "\(basename).xcstrings"))
return StringsTable(filename: basename, locale: locale, dictionary: dictionary)
}

// Check to make sure url can be parsed as a dictionary
guard let nsDictionary = NSDictionary(contentsOf: url) else {
throw ResourceParsingError("File could not be parsed as a strings file: \(url.absoluteString)")
Expand Down Expand Up @@ -167,3 +174,79 @@ private func lookup(key: String, in dict: [String: AnyObject], processedReferenc

return results
}

private func parseXcstrings(_ xcString: XCString, source: String) throws -> [StringsTable.Key: StringsTable.Value] {
var dictionary: [StringsTable.Key: StringsTable.Value] = [:]
for item in xcString.strings {
let key = item.key
guard let val = item.value.localizations?[xcString.sourceLanguage] else {
dictionary[key] = .init(params: [], originalValue: "")
continue
}
let params: [StringParam] = try parse(localization: val, source: source, key: key)
dictionary[key] = .init(params: params, originalValue: val.stringUnit?.value ?? "")
}
return dictionary
}

private func parse(localization: XCLocalization, source: String, key: String) throws -> [StringParam] {
let val = parse(stringUnit: localization.stringUnit, orVariations: localization.variations, withSubstitutions: localization.substitutions)
let parts = FormatPart.formatParts(formatString: val)
var params: [StringParam] = []
for part in parts {
switch part {
case let .reference(reference):
throw ResourceParsingError("No value for reference \(reference) on \(source): \(key)")
case let .spec(formatSpecifier):
params.append(StringParam(name: nil, spec: formatSpecifier))
}
}
return params
}

private func parse(
stringUnit: XCStringUnit?,
orVariations variations: XCVariations?,
withSubstitutions substitutions: [String: XCSubstitution]?
) -> String {
if let stringUnit = stringUnit {
return parse(stringUnit: stringUnit, withSubstitutions: substitutions)
} else if let deviceVariations = variations?.device {
return parse(variations: deviceVariations, withSubstitutions: substitutions)
} else if let pluralVariations = variations?.plural {
return parse(variations: pluralVariations, withSubstitutions: substitutions)
} else {
return ""
}
}

private func parse(stringUnit: XCStringUnit, withSubstitutions substitutions: [String: XCSubstitution]?) -> String {
var val = stringUnit.value
for (key, substitution) in substitutions ?? [:] {
val = val.replacingOccurrences(of: "%#@\(key)@", with: parse(substitution: substitution))
}
return val
}

private func parse(variations: [String: XCPluralVariationsValue], withSubstitutions substitutions: [String: XCSubstitution]?) -> String {
var longestVal = ""
var longestValArgCount = -1
for variation in variations.values {
let val = parse(stringUnit: variation.stringUnit, orVariations: variation.variations, withSubstitutions: substitutions)
let count = FormatPart.formatParts(formatString: val).count
if count > longestValArgCount {
longestVal = val
longestValArgCount = count
}
}
return longestVal
}

private func parse(substitution: XCSubstitution) -> String {
let val = parse(stringUnit: nil, orVariations: substitution.variations, withSubstitutions: nil)
if let argNum = substitution.argNum {
return val.replacingOccurrences(of: "%arg", with: "%\(argNum)$\(substitution.formatSpecifier)")
} else {
return val.replacingOccurrences(of: "%arg", with: "%\(substitution.formatSpecifier)")
}
}
37 changes: 37 additions & 0 deletions Sources/RswiftParsers/Resources/XCString.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Foundation

struct XCString: Decodable {
let sourceLanguage: String
let strings: [String: XCStringString]
let version: String
}

struct XCStringString: Decodable {
let localizations: [String: XCLocalization]?
}

struct XCLocalization: Decodable {
let stringUnit: XCStringUnit?
let variations: XCVariations?
let substitutions: [String: XCSubstitution]?
}

struct XCVariations: Decodable {
let plural: [String: XCPluralVariationsValue]?
let device: [String: XCPluralVariationsValue]?
}

struct XCPluralVariationsValue: Decodable {
let stringUnit: XCStringUnit?
let variations: XCVariations?
}

struct XCStringUnit: Decodable {
let value: String
}

struct XCSubstitution: Decodable {
let argNum: Int?
let formatSpecifier: String
let variations: XCVariations
}