From 80f2178c166664c06539cb9727df44075ecb8c17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fatih=20Do=C4=9Fan?= Date: Tue, 7 Nov 2023 16:37:16 +0300 Subject: [PATCH] Add xcstrings support --- .../Resources/StringsTable+Parser.swift | 84 ++++++++++++++++++- .../RswiftParsers/Resources/XCString.swift | 37 ++++++++ 2 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 Sources/RswiftParsers/Resources/XCString.swift diff --git a/Sources/RswiftParsers/Resources/StringsTable+Parser.swift b/Sources/RswiftParsers/Resources/StringsTable+Parser.swift index 82360306..8c793dc0 100644 --- a/Sources/RswiftParsers/Resources/StringsTable+Parser.swift +++ b/Sources/RswiftParsers/Resources/StringsTable+Parser.swift @@ -9,7 +9,7 @@ import Foundation import RswiftResources extension StringsTable: SupportedExtensions { - static public let supportedExtensions: Set = ["strings", "stringsdict"] + static public let supportedExtensions: Set = ["strings", "stringsdict", "xcstrings"] static public func parse(url: URL) throws -> StringsTable { let warning: (String) -> Void = { print("warning: [R.swift]", $0) } @@ -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)") @@ -167,3 +174,78 @@ 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 { + throw ResourceParsingError("No value for source language \(xcString.sourceLanguage) on \(source): \(key)") + } + 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)") + } +} diff --git a/Sources/RswiftParsers/Resources/XCString.swift b/Sources/RswiftParsers/Resources/XCString.swift new file mode 100644 index 00000000..7dc6cd3f --- /dev/null +++ b/Sources/RswiftParsers/Resources/XCString.swift @@ -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 +}