-
-
Notifications
You must be signed in to change notification settings - Fork 24
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support for legacy .strings and .stringsdict file formats (#20)
* Create StringSource enum with catalog and legacy cases for supporting loading strings from multiple sources * Add tests to assert expected behaviour with Legacy.strings file * Write initial parser for .strings file * Integrate .strings parser * Support decoding basic stringsdict entries * Update Generate command to support multiple input files * Add test for generating combined * Update the plugin to support locating .strings and .stringsdict files * Simplify plugin code * Update PluginTests to embed .strings/.stringsdict in en.lproj and match to defaultLocalization. Nest in Resources directory * Remove redundant isTruthy and extract merge method into extension * Update generator to filter input files based on the developmentLanguage * add debug logging * Add tests for InputParser
- Loading branch information
1 parent
5296dbd
commit 2158274
Showing
30 changed files
with
1,918 additions
and
72 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import Foundation | ||
import PackagePlugin | ||
|
||
private let stringResourceExtensions: Set<String> = ["xcstrings", "strings", "stringsdict"] | ||
|
||
extension FileList { | ||
var stringResources: [File] { | ||
self.filter { stringResourceExtensions.contains($0.path.extension ?? "") } | ||
} | ||
|
||
var stringTables: [(tableName: String, files: [File])] { | ||
Dictionary(grouping: stringResources, by: \.path.stem) | ||
.map { ($0.key, $0.value) } | ||
.sorted(by: { $0.tableName.localizedStandardCompare($1.tableName) == .orderedAscending }) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
import Foundation | ||
import StringCatalog | ||
import StringResource | ||
import SwiftIdentifier | ||
|
||
extension Resource { | ||
init(key: String, value: Any) throws { | ||
let extracted = try Self.extract(from: value, key: key) | ||
let identifier = SwiftIdentifier.variableIdentifier(for: key) | ||
|
||
self.init( | ||
key: key, | ||
comment: nil, // not supported with the legacy format | ||
identifier: identifier, | ||
arguments: extracted.arguments, | ||
sourceLocalization: extracted.sourceLocalization | ||
) | ||
} | ||
|
||
static func extract( | ||
from value: Any, | ||
key: String | ||
) throws -> (arguments: [Argument], sourceLocalization: String) { | ||
switch value { | ||
case let string as String: | ||
try extract(from: string, key: key) | ||
case let dictionary as [String: Any]: | ||
try extract(from: dictionary, key: key) | ||
default: | ||
throw ExtractionError.localizationCorrupt( | ||
ExtractionError.Context( | ||
key: key, | ||
debugDescription: """ | ||
Expected either a String or a plural dictionary ([String: Any]), \ | ||
but got ‘\(type(of: value))‘. | ||
""" | ||
) | ||
) | ||
} | ||
} | ||
|
||
static func extract( | ||
from value: String, | ||
key: String | ||
) throws -> (arguments: [Argument], sourceLocalization: String) { | ||
try extract( | ||
from: StringParser.parse(value, expandingSubstitutions: [:]), | ||
key: key | ||
) | ||
} | ||
|
||
static func extract( | ||
from values: [String: Any], | ||
key: String | ||
) throws -> (arguments: [Argument], sourceLocalization: String) { | ||
var values = values | ||
|
||
guard let value = values.removeValue(forKey: "NSStringLocalizedFormatKey") as? String else { | ||
throw ExtractionError.localizationCorrupt( | ||
ExtractionError.Context( | ||
key: key, | ||
debugDescription: "Legacy stringsdict entry is missing ‘NSStringLocalizedFormatKey‘." | ||
) | ||
) | ||
} | ||
|
||
var substitutions: [String: String] = [:] | ||
var labels: [String] = [] | ||
for (name, value) in values { | ||
guard var dict = value as? [String: String] else { | ||
throw ExtractionError.localizationCorrupt( | ||
ExtractionError.Context( | ||
key: key, | ||
debugDescription: "Nested object ‘\(name)‘ in ‘\(key)‘ is invalid type ‘\(type(of: value))‘." | ||
) | ||
) | ||
} | ||
|
||
guard let specType = dict.removeValue(forKey: "NSStringFormatSpecTypeKey") else { | ||
throw ExtractionError.localizationCorrupt( | ||
ExtractionError.Context( | ||
key: key, | ||
debugDescription: "Nested object ‘\(name)‘ in ‘\(key)‘ has not specified the ‘NSStringFormatSpecTypeKey‘ key." | ||
) | ||
) | ||
} | ||
|
||
guard specType == "NSStringPluralRuleType" else { | ||
throw ExtractionError.localizationCorrupt( | ||
ExtractionError.Context( | ||
key: key, | ||
debugDescription: "Nested object ‘\(name)‘ in ‘\(key)‘ is not a ‘NSStringPluralRuleType‘." | ||
) | ||
) | ||
} | ||
|
||
// TODO: Figure out if we actually need this ever? | ||
let _ = dict.removeValue(forKey: "NSStringFormatValueTypeKey") | ||
|
||
guard let value = dict["other"] ?? dict["one"] ?? dict.values.first else { | ||
throw ExtractionError.localizationCorrupt( | ||
ExtractionError.Context( | ||
key: key, | ||
debugDescription: "Plural substitution ‘\(name)‘ in ‘\(key)‘ does not define any variations." | ||
) | ||
) | ||
} | ||
|
||
substitutions[name] = value | ||
labels.append(name) | ||
} | ||
|
||
// Parse the raw segments | ||
let segments = StringParser.parse(value, expandingSubstitutions: substitutions) | ||
|
||
// Convert the parsed arguments and labels into the correct data | ||
return try extract(from: segments, labels: [:], key: key) | ||
} | ||
} |
14 changes: 14 additions & 0 deletions
14
Sources/StringExtractor/StringExtractor+LegacyFormat.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import StringResource | ||
import typealias StringSource.LegacyFormat | ||
|
||
extension StringExtractor { | ||
public static func extractResources( | ||
from legacyFormat: LegacyFormat | ||
) throws -> Result { | ||
try collect { resources, _ in | ||
for (key, value) in legacyFormat { | ||
resources.append(try Resource(key: key, value: value)) | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,17 @@ | ||
import StringResource | ||
import StringSource | ||
|
||
public struct StringExtractor { | ||
public typealias Result = (resources: [Resource], issues: [ExtractionIssue]) | ||
|
||
public static func extractResources( | ||
from source: StringSource | ||
) throws -> Result { | ||
switch source { | ||
case .catalog(let stringCatalog): | ||
try extractResources(from: stringCatalog) | ||
case .legacy(let legacyFormat): | ||
try extractResources(from: legacyFormat) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import Foundation | ||
import StringCatalog | ||
|
||
/// A representation of a raw .strings or .stringsdict file | ||
public typealias LegacyFormat = [String: Any] | ||
|
||
/// Various sources of localized strings | ||
public enum StringSource { | ||
case catalog(StringCatalog) | ||
case legacy(LegacyFormat) | ||
} | ||
|
||
// MARK: - Loading | ||
extension StringSource { | ||
public init(contentsOf fileURL: URL) throws { | ||
switch fileURL.pathExtension { | ||
case "xcstrings": | ||
self = .catalog(try StringCatalog(contentsOf: fileURL)) | ||
case "stringsdict", "strings": | ||
self = .legacy(try LegacyFormat(contentsOf: fileURL)) | ||
default: | ||
throw StringSourceError.invalidFileFormat(fileURL.pathExtension) | ||
} | ||
} | ||
} | ||
|
||
// MARK: - Legacy Format | ||
extension LegacyFormat { | ||
init(contentsOf fileURL: URL) throws { | ||
let data = try Data(contentsOf: fileURL) | ||
let plist = try PropertyListSerialization.propertyList(from: data, format: nil) | ||
|
||
if let dictionary = plist as? LegacyFormat { | ||
self = dictionary | ||
} else { | ||
throw StringSourceError.invalidPropertyListContents | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import Foundation | ||
|
||
public enum StringSourceError: LocalizedError { | ||
case invalidFileFormat(String) | ||
case invalidPropertyListContents | ||
|
||
public var errorDescription: String? { | ||
switch self { | ||
case .invalidFileFormat(let format): | ||
"The file format ‘\(format)‘ is not supported." | ||
case .invalidPropertyListContents: | ||
"The root object of the property list was not a dictionary." | ||
} | ||
} | ||
} |
Oops, something went wrong.