diff --git a/Package.resolved b/Package.resolved
index 41da94a..3593e8e 100644
--- a/Package.resolved
+++ b/Package.resolved
@@ -27,15 +27,6 @@
"version" : "0.8.1"
}
},
- {
- "identity" : "plot",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/JohnSundell/Plot.git",
- "state" : {
- "revision" : "271926b4413fe868739d99f5eadcf2bd6cd62fb8",
- "version" : "0.14.0"
- }
- },
{
"identity" : "sqlite.swift",
"kind" : "remoteSourceControl",
diff --git a/Package.swift b/Package.swift
index dd93d82..cd8cea5 100644
--- a/Package.swift
+++ b/Package.swift
@@ -39,7 +39,6 @@ let package = Package(
.package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMajor(from: "1.5.1")),
.package(url: "https://github.com/apple/swift-syntax.git", .upToNextMajor(from: "509.0.0")),
.package(url: "https://github.com/CoreOffice/XMLCoder.git", .upToNextMajor(from: "0.17.1")),
- .package(url: "https://github.com/JohnSundell/Plot.git", .upToNextMajor(from: "0.14.0")),
.package(url: "https://github.com/alexisakers/HTMLString.git", .upToNextMajor(from: "6.0.0")),
.package(url: "https://github.com/stephencelis/SQLite.swift.git", .upToNextMajor(from: "0.15.4")),
],
@@ -66,7 +65,6 @@ let package = Package(
dependencies: [
"TranslationCatalog",
.product(name: "XMLCoder", package: "XMLCoder"),
- .product(name: "Plot", package: "Plot"),
.product(name: "HTMLString", package: "HTMLString"),
.product(name: "SwiftSyntax", package: "swift-syntax"),
.product(name: "SwiftSyntaxBuilder", package: "swift-syntax"),
diff --git a/Sources/TranslationCatalogIO/ExpressionEncoder.swift b/Sources/TranslationCatalogIO/ExpressionEncoder.swift
index 4c7ba87..d447c65 100644
--- a/Sources/TranslationCatalogIO/ExpressionEncoder.swift
+++ b/Sources/TranslationCatalogIO/ExpressionEncoder.swift
@@ -1,5 +1,4 @@
import Foundation
-import Plot
import TranslationCatalog
/// Utility for encoding catalog `Translation`s for export/transfer.
@@ -23,14 +22,19 @@ public struct ExpressionEncoder {
) throws -> Data {
switch format {
case .androidXML:
- let sorted = expressions.sorted(by: { $0.key < $1.key })
- let xml = XML.make(
- with: sorted,
- locale: locale,
- fallback: fallback
- )
- let raw = xml.render(indentedBy: .spaces(2))
- return raw.data(using: .utf8) ?? Data()
+ let sorted = expressions
+ .compactMap(locale: locale, fallback: fallback)
+ .sorted(by: { $0.key < $1.key })
+ let resources = sorted.map { exp -> Resource in
+ let multipleReplacements = exp.defaultValue.hasMultipleReplacements
+ return Resource(
+ name: exp.key,
+ value: exp.valueOrDefault(for: locale),
+ formatted: multipleReplacements ? false : nil
+ )
+ }
+ let strings = StringsXml(resources: resources)
+ return try strings.encoded()
case .appleStrings:
let sorted = expressions.sorted(by: { $0.key < $1.key })
var output: [String] = []
diff --git a/Sources/TranslationCatalogIO/ExpressionRenderer.swift b/Sources/TranslationCatalogIO/ExpressionRenderer.swift
deleted file mode 100644
index bb2cf12..0000000
--- a/Sources/TranslationCatalogIO/ExpressionRenderer.swift
+++ /dev/null
@@ -1,54 +0,0 @@
-import Plot
-import TranslationCatalog
-
-/// Utility for rendering expressions in multiple formats.
-public struct ExpressionRenderer {
-
- private init() {}
-
- /// Create a string representation of a rendered `Expression` document.
- ///
- /// - parameters:
- /// - expressions: The collection of `Expression` which should be rendered into the document.
- /// - renderFormat: The output format for the document.
- /// - returns: String representation of the requested text document.
- public static func render(
- expressions: [Expression],
- renderFormat: RenderFormat
- ) throws -> String {
- switch renderFormat {
- case .html:
- let html = HTML.make(with: expressions)
- return html.render(indentedBy: .spaces(2))
- case .markdown:
- var md: String = "# Strings"
-
- for expression in expressions {
- let table = try MarkdownTable<[Translation]>(
- paths: [\.id, \.locale.identifier, \.value],
- headers: ["ID", "Locale Identifier", "Value"]
- )
-
- let translations = expression.translations.sorted(by: { $0.language.identifier < $1.language.identifier })
- for translation in translations {
- table.addRow(translation, strong: translation.locale == expression.locale)
- }
-
- md += """
- \n
- ## \(expression.key)
- Id: \(expression.id)
- Value: \(expression.defaultValue)
- Language: \(expression.defaultLanguageCode.identifier)
- Name: \(expression.name)
- Context: \(expression.context ?? "")
- Feature: \(expression.feature ?? "")
-
- \(table)
- """
- }
-
- return md
- }
- }
-}
diff --git a/Sources/TranslationCatalogIO/Extensions/Expression+IO.swift b/Sources/TranslationCatalogIO/Extensions/Expression+IO.swift
index dcacbbf..0147097 100644
--- a/Sources/TranslationCatalogIO/Extensions/Expression+IO.swift
+++ b/Sources/TranslationCatalogIO/Extensions/Expression+IO.swift
@@ -1,5 +1,4 @@
import Foundation
-import Plot
import TranslationCatalog
extension TranslationCatalog.Expression {
diff --git a/Sources/TranslationCatalogIO/Extensions/Plot+IO.swift b/Sources/TranslationCatalogIO/Extensions/Plot+IO.swift
deleted file mode 100644
index 92c030a..0000000
--- a/Sources/TranslationCatalogIO/Extensions/Plot+IO.swift
+++ /dev/null
@@ -1,110 +0,0 @@
-import HTMLString
-import Plot
-import TranslationCatalog
-
-extension HTML {
- static func make(with expressions: [Expression]) -> Self {
- HTML(
- .head(
- .title("Localization Strings"),
- .style("""
- body {
- font-family: -apple-system, Helvetica, sans-serif;
- }
-
- h1 {
- color: purple;
- }
-
- h2 {
- color: royalblue;
- }
-
- table, th, td {
- border-collapse: collapse;
- border: 1px solid gray;
- }
-
- th {
- color: slategray;
- }
- """)
- ),
- .body(
- .div(
- .h1("Strings")
- ),
- .forEach(expressions) {
- .localization($0)
- }
- )
- )
- }
-}
-
-extension Node where Context == HTML.BodyContext {
- static func localization(_ expression: Expression) -> Self {
- let values = expression.translations.sorted(by: { $0.language.identifier < $1.language.identifier })
-
- return .div(
- .h2(
- .text(expression.name)
- ),
- .p(
- .text("ID: \(expression.id)"),
- .br(),
- .text("Key: \(expression.key)"),
- .br(),
- .text("Context: \(expression.context ?? "")"),
- .br(),
- .text("Feature: \(expression.feature ?? "")")
- ),
- .table(
- .tr(
- .th("ID"),
- .th("Locale Identifier"),
- .th("Value")
- ),
- .forEach(values) {
- .if($0.language == expression.defaultLanguageCode, .defaultValue($0), else: .value($0))
- }
- )
- )
- }
-}
-
-extension Node where Context == HTML.TableContext {
- static func value(_ translation: Translation) -> Self {
- .tr(
- .td(
- .text("\(translation.id)")
- ),
- .td(
- .text(translation.locale.identifier)
- ),
- .td(
- .raw(translation.value.addingASCIIEntities())
- )
- )
- }
-
- static func defaultValue(_ translation: Translation) -> Self {
- .tr(
- .td(
- .b(
- .text("\(translation.id)")
- )
- ),
- .td(
- .b(
- .text(translation.locale.identifier)
- )
- ),
- .td(
- .b(
- .raw(translation.value.addingASCIIEntities())
- )
- )
- )
- }
-}
diff --git a/Sources/TranslationCatalogIO/Extensions/XML+Expression.swift b/Sources/TranslationCatalogIO/Extensions/XML+Expression.swift
deleted file mode 100644
index f3afdbf..0000000
--- a/Sources/TranslationCatalogIO/Extensions/XML+Expression.swift
+++ /dev/null
@@ -1,29 +0,0 @@
-import Foundation
-import Plot
-import TranslationCatalog
-
-extension XML {
- static func make(
- with expressions: [TranslationCatalog.Expression],
- locale: Locale,
- fallback: Bool
- ) -> Self {
- let filtered = expressions.compactMap(locale: locale, fallback: fallback)
-
- return XML(
- .element(named: "resources", nodes: [
- .forEach(filtered) {
- .element(named: "string", nodes: [
- .attribute(named: "name", value: $0.key),
- .attribute(
- named: "formatted",
- value: $0.valueOrDefault(for: locale).hasMultipleReplacements ? "false" : "",
- ignoreIfValueIsEmpty: true
- ),
- .text($0.valueOrDefault(for: locale).simpleAndroidXMLEscaped()),
- ])
- },
- ])
- )
- }
-}
diff --git a/Sources/TranslationCatalogIO/Internal/Resource.swift b/Sources/TranslationCatalogIO/Internal/Resource.swift
index 84567d0..0be5baa 100644
--- a/Sources/TranslationCatalogIO/Internal/Resource.swift
+++ b/Sources/TranslationCatalogIO/Internal/Resource.swift
@@ -2,23 +2,32 @@ import Foundation
import TranslationCatalog
import XMLCoder
-struct Resource: Decodable, DynamicNodeDecoding {
+struct Resource: Codable, DynamicNodeDecoding, DynamicNodeEncoding {
enum CodingKeys: String, CodingKey {
case name
+ case formatted
case value = ""
}
- var name: String
- var value: String
+ let name: String
+ let value: String
+ let formatted: Bool?
static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding {
switch key {
- case CodingKeys.name:
+ case CodingKeys.name, CodingKeys.formatted:
.attribute
- case CodingKeys.value:
+ default:
.element
+ }
+ }
+
+ static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding {
+ switch key {
+ case CodingKeys.name, CodingKeys.formatted:
+ .attribute
default:
- .elementOrAttribute
+ .element
}
}
}
diff --git a/Sources/TranslationCatalogIO/Internal/StringsXml.swift b/Sources/TranslationCatalogIO/Internal/StringsXml.swift
index b738b4b..779f050 100644
--- a/Sources/TranslationCatalogIO/Internal/StringsXml.swift
+++ b/Sources/TranslationCatalogIO/Internal/StringsXml.swift
@@ -2,16 +2,20 @@ import Foundation
import TranslationCatalog
import XMLCoder
-struct StringsXml: Decodable, DynamicNodeDecoding {
+struct StringsXml: Codable, DynamicNodeDecoding, DynamicNodeEncoding {
enum CodingKeys: String, CodingKey {
case resources = "string"
}
- var resources: [Resource]
+ let resources: [Resource]
static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding {
.element
}
+
+ static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding {
+ .element
+ }
static func make(contentsOf url: URL) throws -> StringsXml {
let data = try Data(contentsOf: url)
@@ -21,6 +25,24 @@ struct StringsXml: Decodable, DynamicNodeDecoding {
static func make(with data: Data) throws -> StringsXml {
try XMLDecoder().decode(StringsXml.self, from: data)
}
+
+ func encoded() throws -> Data {
+ let encoder = XMLEncoder()
+ encoder.outputFormatting = [.sortedKeys]
+ let encoded = try encoder.encode(
+ self,
+ withRootKey: "resources",
+ header: XMLHeader(
+ version: 1.0,
+ encoding: "UTF-8"
+ )
+ )
+ var string = String(decoding: encoded, as: UTF8.self)
+ string = string.replacingOccurrences(of: ">", with: ">\n")
+ string = string.replacingOccurrences(of: ">\n ", with: ">\n")
+ return string.data(using: .utf8) ?? encoded
+ }
}
extension StringsXml {
diff --git a/Sources/TranslationCatalogIO/RenderFormat.swift b/Sources/TranslationCatalogIO/RenderFormat.swift
index 586781e..509459d 100644
--- a/Sources/TranslationCatalogIO/RenderFormat.swift
+++ b/Sources/TranslationCatalogIO/RenderFormat.swift
@@ -1,4 +1,5 @@
/// File types which can be generated containing `Expression`/`Translation` info.
+@available(*, deprecated)
public enum RenderFormat: String, CaseIterable {
case html
case markdown
diff --git a/Sources/localizer/Catalog+Generate.swift b/Sources/localizer/Catalog+Generate.swift
deleted file mode 100644
index 710327b..0000000
--- a/Sources/localizer/Catalog+Generate.swift
+++ /dev/null
@@ -1,46 +0,0 @@
-import ArgumentParser
-import Foundation
-import TranslationCatalog
-import TranslationCatalogIO
-
-extension Catalog {
- struct Generate: CatalogCommand {
-
- static let configuration = CommandConfiguration(
- commandName: "generate",
- abstract: "Generate a viewable document using the strings catalog.",
- discussion: """
- Available formats: \(RenderFormat.allCases.map(\.rawValue).joined(separator: " "))
- """,
- version: "1.0.0",
- helpNames: .shortAndLong
- )
-
- @Argument(help: "The export format")
- var format: RenderFormat
-
- @Option(help: "Storage mechanism used to persist the catalog. (*default) [core-data, filesystem, *sqlite]")
- var storage: Catalog.Storage = .default
-
- @Option(help: "Path to catalog to use in place of the application library.")
- var path: String?
-
- @Flag(help: "Additional execution details in the standard output.")
- var verbose: Bool = false
-
- func run() async throws {
- let catalog = try catalog()
- let expressions = try catalog.expressions()
- let sortedExpressions = expressions.sorted(by: { $0.key < $1.key })
- let expressionsWithTranslations = try sortedExpressions.map { expression in
- let translations = try catalog.translations(matching: GenericTranslationQuery.expressionId(expression.id))
- return TranslationCatalog.Expression(
- expression: expression,
- translations: translations
- )
- }
- let render = try ExpressionRenderer.render(expressions: expressionsWithTranslations, renderFormat: format)
- print(render)
- }
- }
-}
diff --git a/Sources/localizer/Catalog+Syntax.swift b/Sources/localizer/Catalog+Syntax.swift
index 59a8b93..8729f45 100644
--- a/Sources/localizer/Catalog+Syntax.swift
+++ b/Sources/localizer/Catalog+Syntax.swift
@@ -11,7 +11,7 @@ extension Catalog {
commandName: "syntax",
abstract: "Create a enumerated syntax tree.",
discussion: """
- Generate a enumerate reference to strings. For example:
+ Generate an enumerated reference to strings. For example:
enum LocalizedStrings: String, LocalizedStringConvertible {
/// Title for account screen.
diff --git a/Sources/localizer/Catalog.swift b/Sources/localizer/Catalog.swift
index 7be125c..61ced84 100644
--- a/Sources/localizer/Catalog.swift
+++ b/Sources/localizer/Catalog.swift
@@ -24,7 +24,6 @@ struct Catalog: AsyncParsableCommand {
subcommands: [
Export.self,
Delete.self,
- Generate.self,
Import.self,
Insert.self,
Query.self,
diff --git a/Sources/localizer/Extensions/RenderFormat+localizer.swift b/Sources/localizer/Extensions/RenderFormat+localizer.swift
index ffcb841..6a70a74 100644
--- a/Sources/localizer/Extensions/RenderFormat+localizer.swift
+++ b/Sources/localizer/Extensions/RenderFormat+localizer.swift
@@ -1,4 +1,5 @@
import ArgumentParser
import TranslationCatalogIO
+@available(*, deprecated)
extension RenderFormat: ExpressibleByArgument {}
diff --git a/Tests/LocalizerTests/CatalogGenerateTests.swift b/Tests/LocalizerTests/CatalogGenerateTests.swift
deleted file mode 100644
index 5d88bf7..0000000
--- a/Tests/LocalizerTests/CatalogGenerateTests.swift
+++ /dev/null
@@ -1,18 +0,0 @@
-import XCTest
-
-final class CatalogGenerateTests: XCTestCase {
-
- func testExecute() throws {
- let process = LocalizerProcess()
- let output = try process.runOutputting(with: [
- "catalog", "generate", "markdown", "--path", process.url.lastPathComponent,
- ])
-
- XCTAssertEqual(output, """
- # Strings
-
- """)
-
- try process.recycle()
- }
-}
diff --git a/Tests/LocalizerTests/CoreDataExportTests.swift b/Tests/LocalizerTests/CoreDataExportTests.swift
index 0ca32b6..f1bb72c 100644
--- a/Tests/LocalizerTests/CoreDataExportTests.swift
+++ b/Tests/LocalizerTests/CoreDataExportTests.swift
@@ -48,15 +48,14 @@ final class CoreDataExportTests: LocalizerTestCase {
XCTAssertEqual(output, """
-
-
- Lingua
- Hello World!
-
- Android
- Apple
- Web
-
+
+ Lingua
+ Hello World!
+
+ Android
+ Apple
+ Web
+
""")
}
@@ -68,11 +67,10 @@ final class CoreDataExportTests: LocalizerTestCase {
XCTAssertEqual(output, """
-
-
- Hola Mundo!
- solo en español
-
+
+ Hola Mundo!
+ solo en español
+
""")
}
@@ -84,15 +82,14 @@ final class CoreDataExportTests: LocalizerTestCase {
XCTAssertEqual(output, """
-
-
- Lingua
- Hola Mundo!
- solo en español
- Android
- Apple
- Web
-
+
+ Lingua
+ Hola Mundo!
+ solo en español
+ Android
+ Apple
+ Web
+
""")
}
diff --git a/Tests/LocalizerTests/FilesystemExportTests.swift b/Tests/LocalizerTests/FilesystemExportTests.swift
index 7be7baf..1dfec73 100644
--- a/Tests/LocalizerTests/FilesystemExportTests.swift
+++ b/Tests/LocalizerTests/FilesystemExportTests.swift
@@ -49,15 +49,14 @@ final class FilesystemExportTests: LocalizerTestCase {
XCTAssertEqual(output, """
-
-
- Lingua
- Hello World!
-
- Android
- Apple
- Web
-
+
+ Lingua
+ Hello World!
+
+ Android
+ Apple
+ Web
+
""")
}
@@ -69,11 +68,10 @@ final class FilesystemExportTests: LocalizerTestCase {
XCTAssertEqual(output, """
-
-
- Hola Mundo!
- solo en español
-
+
+ Hola Mundo!
+ solo en español
+
""")
}
@@ -85,15 +83,14 @@ final class FilesystemExportTests: LocalizerTestCase {
XCTAssertEqual(output, """
-
-
- Lingua
- Hola Mundo!
- solo en español
- Android
- Apple
- Web
-
+
+ Lingua
+ Hola Mundo!
+ solo en español
+ Android
+ Apple
+ Web
+
""")
}
diff --git a/Tests/LocalizerTests/SQLiteExportTests.swift b/Tests/LocalizerTests/SQLiteExportTests.swift
index 7f17f46..48bd9d4 100644
--- a/Tests/LocalizerTests/SQLiteExportTests.swift
+++ b/Tests/LocalizerTests/SQLiteExportTests.swift
@@ -47,15 +47,14 @@ final class SQLiteExportTests: LocalizerTestCase {
XCTAssertEqual(output, """
-
-
- Lingua
- Hello World!
-
- Android
- Apple
- Web
-
+
+ Lingua
+ Hello World!
+
+ Android
+ Apple
+ Web
+
""")
}
@@ -67,11 +66,10 @@ final class SQLiteExportTests: LocalizerTestCase {
XCTAssertEqual(output, """
-
-
- Hola Mundo!
- solo en español
-
+
+ Hola Mundo!
+ solo en español
+
""")
}
@@ -83,15 +81,14 @@ final class SQLiteExportTests: LocalizerTestCase {
XCTAssertEqual(output, """
-
-
- Lingua
- Hola Mundo!
- solo en español
- Android
- Apple
- Web
-
+
+ Lingua
+ Hola Mundo!
+ solo en español
+ Android
+ Apple
+ Web
+
""")
}
diff --git a/Tests/TranslationCatalogTests/ExpressionEncoderTests.swift b/Tests/TranslationCatalogTests/ExpressionEncoderTests.swift
new file mode 100644
index 0000000..b399369
--- /dev/null
+++ b/Tests/TranslationCatalogTests/ExpressionEncoderTests.swift
@@ -0,0 +1,78 @@
+import TranslationCatalog
+@testable import TranslationCatalogIO
+import XCTest
+
+final class ExpressionEncoderTests: XCTestCase {
+
+ private let locale: Locale = Locale(identifier: "en_US")
+ private let expressions: [TranslationCatalog.Expression] = [
+ Expression(
+ id: UUID(uuidString: "4D08CE7F-ED98-437E-B7CB-BB18147B9B1A")!,
+ key: "EXP_01",
+ value: "Hello World!",
+ languageCode: .english
+ ),
+ Expression(
+ id: UUID(uuidString: "5638B19C-2034-49F2-A163-54CC37269050")!,
+ key: "EXP_02",
+ value: "Hello %s!",
+ languageCode: .english
+ ),
+ Expression(
+ id: UUID(uuidString: "5870E853-35B9-4C71-B8D6-7EA922C72835")!,
+ key: "EXP_03",
+ value: "Hello %s, welcome to %s!",
+ languageCode: .english
+ ),
+ ]
+
+ func testAndroidXMLEncoding() throws {
+ let data = try ExpressionEncoder.encodeValues(
+ for: expressions,
+ locale: locale,
+ fallback: true,
+ format: .androidXML
+ )
+ let output = String(decoding: data, as: UTF8.self)
+ XCTAssertEqual(output, """
+
+
+ Hello World!
+ Hello %s!
+ Hello %s, welcome to %s!
+
+ """)
+ }
+
+ func testAppleStringsEncoding() throws {
+ let data = try ExpressionEncoder.encodeValues(
+ for: expressions,
+ locale: locale,
+ fallback: true,
+ format: .appleStrings
+ )
+ let output = String(decoding: data, as: UTF8.self)
+ XCTAssertEqual(output, """
+ "EXP_01" = "Hello World!";
+ "EXP_02" = "Hello %@!";
+ "EXP_03" = "Hello %@, welcome to %@!";
+ """)
+ }
+
+ func testJSONEncoding() throws {
+ let data = try ExpressionEncoder.encodeValues(
+ for: expressions,
+ locale: locale,
+ fallback: true,
+ format: .json
+ )
+ let output = String(decoding: data, as: UTF8.self)
+ XCTAssertEqual(output, """
+ {
+ "EXP_01" : "Hello World!",
+ "EXP_02" : "Hello %s!",
+ "EXP_03" : "Hello %s, welcome to %s!"
+ }
+ """)
+ }
+}