From 7948b65eabaa2dde852d9ed4bceead8fd1a0238c Mon Sep 17 00:00:00 2001 From: Mattes Mohr Date: Fri, 8 Nov 2024 19:22:14 +0100 Subject: [PATCH 1/5] Change the string interpolation for the localization --- .../Abstraction/Elements/BodyElements.swift | 56 +++++------ .../Abstraction/Elements/FormElements.swift | 8 +- .../Abstraction/Elements/TableElements.swift | 4 +- .../Framework/Localization/Localizable.swift | 3 +- .../Framework/Localization/Localization.swift | 22 +++-- .../Localization/LocalizedString.swift | 23 +++++ .../Localization/LocalizedStringKey.swift | 94 +++++++++++++++---- .../Framework/Rendering/Renderer.swift | 18 ++-- .../HTMLKitComponents/Components/Text.swift | 4 +- .../Localization/en-GB/mobile.strings | 7 +- .../Localization/en-GB/web.strings | 28 ++++-- .../HTMLKitTests/Localization/fr/web.strings | 4 +- Tests/HTMLKitTests/LocalizationTests.swift | 33 ++++--- Tests/HTMLKitTests/PerformanceTests.swift | 2 +- .../Localization/en-GB/mobile.strings | 7 +- .../Localization/en-GB/web.strings | 7 +- .../Localization/fr/web.strings | 4 +- Tests/HTMLKitVaporTests/ProviderTests.swift | 2 +- 18 files changed, 212 insertions(+), 114 deletions(-) create mode 100644 Sources/HTMLKit/Framework/Localization/LocalizedString.swift diff --git a/Sources/HTMLKit/Abstraction/Elements/BodyElements.swift b/Sources/HTMLKit/Abstraction/Elements/BodyElements.swift index 4c954f0b..9dd61d1f 100644 --- a/Sources/HTMLKit/Abstraction/Elements/BodyElements.swift +++ b/Sources/HTMLKit/Abstraction/Elements/BodyElements.swift @@ -1720,8 +1720,8 @@ extension Heading1: GlobalAttributes, GlobalEventAttributes, GlobalAriaAttribute extension Heading1: Localizable { - public init(_ localizedKey: String, tableName: String? = nil, interpolation: Any...) { - self.content = [LocalizedStringKey(key: localizedKey, table: tableName, interpolation: interpolation)] + public init(_ localizedKey: LocalizedStringKey, tableName: String? = nil) { + self.content = [LocalizedString(key: localizedKey, table: tableName)] } } @@ -1997,8 +1997,8 @@ extension Heading2: GlobalAttributes, GlobalEventAttributes, GlobalAriaAttribute extension Heading2: Localizable { - public init(_ localizedKey: String, tableName: String? = nil, interpolation: Any...) { - self.content = [LocalizedStringKey(key: localizedKey, table: tableName, interpolation: interpolation)] + public init(_ localizedKey: LocalizedStringKey, tableName: String? = nil) { + self.content = [LocalizedString(key: localizedKey, table: tableName)] } } @@ -2274,8 +2274,8 @@ extension Heading3: GlobalAttributes, GlobalEventAttributes, GlobalAriaAttribute extension Heading3: Localizable { - public init(_ localizedKey: String, tableName: String? = nil, interpolation: Any...) { - self.content = [LocalizedStringKey(key: localizedKey, table: tableName, interpolation: interpolation)] + public init(_ localizedKey: LocalizedStringKey, tableName: String? = nil) { + self.content = [LocalizedString(key: localizedKey, table: tableName)] } } @@ -2551,8 +2551,8 @@ extension Heading4: GlobalAttributes, GlobalEventAttributes, GlobalAriaAttribute extension Heading4: Localizable { - public init(_ localizedKey: String, tableName: String? = nil, interpolation: Any...) { - self.content = [LocalizedStringKey(key: localizedKey, table: tableName, interpolation: interpolation)] + public init(_ localizedKey: LocalizedStringKey, tableName: String? = nil) { + self.content = [LocalizedString(key: localizedKey, table: tableName)] } } @@ -2828,8 +2828,8 @@ extension Heading5: GlobalAttributes, GlobalEventAttributes, GlobalAriaAttribute extension Heading5: Localizable { - public init(_ localizedKey: String, tableName: String? = nil, interpolation: Any...) { - self.content = [LocalizedStringKey(key: localizedKey, table: tableName, interpolation: interpolation)] + public init(_ localizedKey: LocalizedStringKey, tableName: String? = nil) { + self.content = [LocalizedString(key: localizedKey, table: tableName)] } } @@ -3105,8 +3105,8 @@ extension Heading6: GlobalAttributes, GlobalEventAttributes, GlobalAriaAttribute extension Heading6: Localizable { - public init(_ localizedKey: String, tableName: String? = nil, interpolation: Any...) { - self.content = [LocalizedStringKey(key: localizedKey, table: tableName, interpolation: interpolation)] + public init(_ localizedKey: LocalizedStringKey, tableName: String? = nil) { + self.content = [LocalizedString(key: localizedKey, table: tableName)] } } @@ -4462,8 +4462,8 @@ extension Paragraph: GlobalAttributes, GlobalEventAttributes, GlobalAriaAttribut extension Paragraph: Localizable { - public init(_ localizedKey: String, tableName: String? = nil, interpolation: Any...) { - self.content = [LocalizedStringKey(key: localizedKey, table: tableName, interpolation: interpolation)] + public init(_ localizedKey: LocalizedStringKey, tableName: String? = nil) { + self.content = [LocalizedString(key: localizedKey, table: tableName)] } } @@ -5278,8 +5278,8 @@ extension Blockquote: GlobalAttributes, GlobalEventAttributes, GlobalAriaAttribu extension Blockquote: Localizable { - public init(_ localizedKey: String, tableName: String? = nil, interpolation: Any...) { - self.content = [LocalizedStringKey(key: localizedKey, table: tableName, interpolation: interpolation)] + public init(_ localizedKey: LocalizedStringKey, tableName: String? = nil) { + self.content = [LocalizedString(key: localizedKey, table: tableName)] } } @@ -6857,8 +6857,8 @@ extension Anchor: GlobalAttributes, GlobalEventAttributes, GlobalAriaAttributes, extension Anchor: Localizable { - public init(_ localizedKey: String, tableName: String? = nil, interpolation: Any...) { - self.content = [LocalizedStringKey(key: localizedKey, table: tableName, interpolation: interpolation)] + public init(_ localizedKey: LocalizedStringKey, tableName: String? = nil) { + self.content = [LocalizedString(key: localizedKey, table: tableName)] } } @@ -7674,8 +7674,8 @@ extension Small: GlobalAttributes, GlobalEventAttributes, GlobalAriaAttributes { extension Small: Localizable { - public init(_ localizedKey: String, tableName: String? = nil, interpolation: Any...) { - self.content = [LocalizedStringKey(key: localizedKey, table: tableName, interpolation: interpolation)] + public init(_ localizedKey: LocalizedStringKey, tableName: String? = nil) { + self.content = [LocalizedString(key: localizedKey, table: tableName)] } } @@ -7875,8 +7875,8 @@ extension StrikeThrough: GlobalAttributes, GlobalEventAttributes { extension StrikeThrough: Localizable { - public init(_ localizedKey: String, tableName: String? = nil, interpolation: Any...) { - self.content = [LocalizedStringKey(key: localizedKey, table: tableName, interpolation: interpolation)] + public init(_ localizedKey: LocalizedStringKey, tableName: String? = nil) { + self.content = [LocalizedString(key: localizedKey, table: tableName)] } } @@ -12387,8 +12387,8 @@ extension Italic: GlobalAttributes, GlobalEventAttributes, GlobalAriaAttributes extension Italic: Localizable { - public init(_ localizedKey: String, tableName: String? = nil, interpolation: Any...) { - self.content = [LocalizedStringKey(key: localizedKey, table: tableName, interpolation: interpolation)] + public init(_ localizedKey: LocalizedStringKey, tableName: String? = nil) { + self.content = [LocalizedString(key: localizedKey, table: tableName)] } } @@ -12664,8 +12664,8 @@ extension Bold: GlobalAttributes, GlobalEventAttributes, GlobalAriaAttributes { extension Bold: Localizable { - public init(_ localizedKey: String, tableName: String? = nil, interpolation: Any...) { - self.content = [LocalizedStringKey(key: localizedKey, table: tableName, interpolation: interpolation)] + public init(_ localizedKey: LocalizedStringKey, tableName: String? = nil) { + self.content = [LocalizedString(key: localizedKey, table: tableName)] } } @@ -12941,8 +12941,8 @@ extension Underline: GlobalAttributes, GlobalEventAttributes, GlobalAriaAttribut extension Underline: Localizable { - public init(_ localizedKey: String, tableName: String? = nil, interpolation: Any...) { - self.content = [LocalizedStringKey(key: localizedKey, table: tableName, interpolation: interpolation)] + public init(_ localizedKey: LocalizedStringKey, tableName: String? = nil) { + self.content = [LocalizedString(key: localizedKey, table: tableName)] } } diff --git a/Sources/HTMLKit/Abstraction/Elements/FormElements.swift b/Sources/HTMLKit/Abstraction/Elements/FormElements.swift index f133dc36..e8c1efca 100644 --- a/Sources/HTMLKit/Abstraction/Elements/FormElements.swift +++ b/Sources/HTMLKit/Abstraction/Elements/FormElements.swift @@ -621,8 +621,8 @@ extension Label: GlobalAttributes, GlobalEventAttributes, GlobalAriaAttributes, extension Label: Localizable { - public init(_ localizedKey: String, tableName: String? = nil, interpolation: Any...) { - self.content = [LocalizedStringKey(key: localizedKey, table: tableName, interpolation: interpolation)] + public init(_ localizedKey: LocalizedStringKey, tableName: String? = nil) { + self.content = [LocalizedString(key: localizedKey, table: tableName)] } } @@ -1524,8 +1524,8 @@ extension Button: GlobalAttributes, GlobalEventAttributes, GlobalAriaAttributes, extension Button: Localizable { - public init(_ localizedKey: String, tableName: String? = nil, interpolation: Any...) { - self.content = [LocalizedStringKey(key: localizedKey, table: tableName, interpolation: interpolation)] + public init(_ localizedKey: LocalizedStringKey, tableName: String? = nil) { + self.content = [LocalizedString(key: localizedKey, table: tableName)] } } diff --git a/Sources/HTMLKit/Abstraction/Elements/TableElements.swift b/Sources/HTMLKit/Abstraction/Elements/TableElements.swift index cc0eef33..3e489a86 100644 --- a/Sources/HTMLKit/Abstraction/Elements/TableElements.swift +++ b/Sources/HTMLKit/Abstraction/Elements/TableElements.swift @@ -2428,7 +2428,7 @@ extension HeaderCell: GlobalAttributes, GlobalEventAttributes, GlobalAriaAttribu extension HeaderCell: Localizable { - public init(_ localizedKey: String, tableName: String? = nil, interpolation: Any...) { - self.content = [LocalizedStringKey(key: localizedKey, table: tableName, interpolation: interpolation)] + public init(_ localizedKey: LocalizedStringKey, tableName: String? = nil) { + self.content = [LocalizedString(key: localizedKey, table: tableName)] } } diff --git a/Sources/HTMLKit/Framework/Localization/Localizable.swift b/Sources/HTMLKit/Framework/Localization/Localizable.swift index c02329c1..f1e551d5 100644 --- a/Sources/HTMLKit/Framework/Localization/Localizable.swift +++ b/Sources/HTMLKit/Framework/Localization/Localizable.swift @@ -7,6 +7,5 @@ public protocol Localizable { /// - Parameters: /// - localizedKey: The string key to be translated /// - tableName: The name of the translation table - /// - interpolation: A variadic list of values used to replace placeholders within the translation string - init(_ localizedKey: String, tableName: String?, interpolation: Any...) + init(_ localizedKey: LocalizedStringKey, tableName: String?) } diff --git a/Sources/HTMLKit/Framework/Localization/Localization.swift b/Sources/HTMLKit/Framework/Localization/Localization.swift index f74727cd..632739fd 100644 --- a/Sources/HTMLKit/Framework/Localization/Localization.swift +++ b/Sources/HTMLKit/Framework/Localization/Localization.swift @@ -175,6 +175,12 @@ public class Localization { translation = translation.replacingCharacters(in: range, with: String(intValue)) } + case let floatValue as Float: + + if let range = translation.range(of: "%do") { + translation = translation.replacingCharacters(in: range, with: String(floatValue)) + } + default: break } @@ -188,7 +194,7 @@ public class Localization { /// - locale: The locale to use when retrieving the translation /// /// - Returns: The translation - public func localize(key: LocalizedStringKey, for locale: Locale? = nil) throws -> String { + public func localize(string: LocalizedString, for locale: Locale? = nil) throws -> String { guard let fallback = self.locale else { throw Errors.noFallback @@ -204,17 +210,17 @@ public class Localization { throw Errors.missingTable(currentLocale.tag) } - if let table = key.table { + if let table = string.table { guard let translationTable = translationTables.first(where: { $0.name == table }) else { throw Errors.unknownTable(table, currentLocale.tag) } - guard var translation = translationTable.retrieve(for: key.key) else { - throw Errors.missingKey(key.key, currentLocale.tag) + guard var translation = translationTable.retrieve(for: string.key.value) else { + throw Errors.missingKey(string.key.value, currentLocale.tag) } - if let interpolation = key.interpolation { + if let interpolation = string.key.interpolation { interpolate(arguments: interpolation, to: &translation, for: currentLocale) } @@ -224,9 +230,9 @@ public class Localization { for translationTable in translationTables { - if var translation = translationTable.retrieve(for: key.key) { + if var translation = translationTable.retrieve(for: string.key.value) { - if let interpolation = key.interpolation { + if let interpolation = string.key.interpolation { interpolate(arguments: interpolation, to: &translation, for: currentLocale) } @@ -234,7 +240,7 @@ public class Localization { } } - throw Errors.missingKey(key.key, currentLocale.tag) + throw Errors.missingKey(string.key.value, currentLocale.tag) } } diff --git a/Sources/HTMLKit/Framework/Localization/LocalizedString.swift b/Sources/HTMLKit/Framework/Localization/LocalizedString.swift new file mode 100644 index 00000000..306e2bb1 --- /dev/null +++ b/Sources/HTMLKit/Framework/Localization/LocalizedString.swift @@ -0,0 +1,23 @@ +import Foundation + +/// A type thats holds the information for the localization +@_documentation(visibility: internal) +public struct LocalizedString: Content { + + /// The key of the translation value + public let key: LocalizedStringKey + + /// The name of the translation table + internal let table: String? + + /// Initializes a localized string with context + /// + /// - Parameters: + /// - key: The string key to be translated + /// - table: The table where the string key should be looked up. Default is nil. + public init(key: LocalizedStringKey, table: String? = nil) { + + self.key = key + self.table = table + } +} diff --git a/Sources/HTMLKit/Framework/Localization/LocalizedStringKey.swift b/Sources/HTMLKit/Framework/Localization/LocalizedStringKey.swift index f9734271..c497b7a8 100644 --- a/Sources/HTMLKit/Framework/Localization/LocalizedStringKey.swift +++ b/Sources/HTMLKit/Framework/Localization/LocalizedStringKey.swift @@ -1,28 +1,86 @@ import Foundation -/// A type thats holds the information for the localization -@_documentation(visibility: internal) -public struct LocalizedStringKey: Content { - - /// The key of the translation value - internal let key: String - - /// The name of the translation table - internal let table: String? +/// A string key for the localization +public struct LocalizedStringKey { + + /// The actual key value + public let value: String - /// The interpolation for the translation string - internal var interpolation: [Any]? + /// The arguments for the interpolation + public var interpolation: [Any]? - /// Initializes a localized string key with a context + /// Initializes a string key for localization /// /// - Parameters: - /// - key: The string key to be translated - /// - table: The table where the string key should be looked up. Default is nil. - /// - interpolation: An array of values that will replace placeholders within the translation string. - public init(key: String, table: String? = nil, interpolation: [Any]? = nil) { + /// - value: The key value + /// - interpolation: An array of values that will replace placeholders within the translation string. + public init(value: String, interpolation: [Any]? = nil) { - self.key = key - self.table = table + self.value = value self.interpolation = interpolation } } + +extension LocalizedStringKey: ExpressibleByStringLiteral, ExpressibleByStringInterpolation { + + public init(stringLiteral: String) { + self.init(value: stringLiteral) + } + + public init(stringInterpolation: StringInterpolation) { + self.init(value: stringInterpolation.key, interpolation: stringInterpolation.arguments) + } + + public struct StringInterpolation: StringInterpolationProtocol { + + var key: String = "" + + var arguments: [Any] = [] + + public init(literalCapacity: Int, interpolationCount: Int) { + + key.reserveCapacity(literalCapacity + interpolationCount * 2) + + arguments.reserveCapacity(interpolationCount) + } + + public mutating func appendLiteral(_ literal: String) { + key.append(literal) + } + + public mutating func appendInterpolation(_ value: String) { + + key += "%st" + + arguments.append(value) + } + + public mutating func appendInterpolation(_ value: Int) { + + key += "%in" + + arguments.append(value) + } + + public mutating func appendInterpolation(_ value: Double) { + + key += "%do" + + arguments.append(value) + } + + public mutating func appendInterpolation(_ value: Float) { + + key += "%do" + + arguments.append(value) + } + + public mutating func appendInterpolation(_ value: Date) { + + key += "%dt" + + arguments.append(value) + } + } +} diff --git a/Sources/HTMLKit/Framework/Rendering/Renderer.swift b/Sources/HTMLKit/Framework/Rendering/Renderer.swift index 7e395bb8..7512419a 100644 --- a/Sources/HTMLKit/Framework/Rendering/Renderer.swift +++ b/Sources/HTMLKit/Framework/Rendering/Renderer.swift @@ -130,8 +130,8 @@ public final class Renderer { result += try render(element: element) } - if let stringkey = content as? LocalizedStringKey { - result += try render(stringkey: stringkey) + if let string = content as? LocalizedString { + result += try render(localized: string) } if let modifier = content as? EnvironmentModifier { @@ -205,8 +205,8 @@ public final class Renderer { result += try render(element: element) } - if let stringkey = content as? LocalizedStringKey { - result += try render(stringkey: stringkey) + if let string = content as? LocalizedString { + result += try render(localized: string) } if let modifier = content as? EnvironmentModifier { @@ -327,8 +327,8 @@ public final class Renderer { result += try render(element: element) } - if let stringkey = content as? LocalizedStringKey { - result += try render(stringkey: stringkey) + if let string = content as? LocalizedString { + result += try render(localized: string) } if let value = content as? EnvironmentValue { @@ -357,14 +357,14 @@ public final class Renderer { } /// Renders a localized string key. - private func render(stringkey: LocalizedStringKey) throws -> String { + private func render(localized string: LocalizedString) throws -> String { guard let localization = self.localization else { throw Errors.missingLocalization } do { - return try localization.localize(key: stringkey, for: environment.locale) + return try localization.localize(string: string, for: environment.locale) } catch Localization.Errors.missingKey(let key, let locale) { @@ -374,7 +374,7 @@ public final class Renderer { logger.warning("Unable to find translation key '\(key)' for the locale '\(locale)'.") // Seems not, let's try to recover by using the fallback - return try localization.localize(key: stringkey) + return try localization.localize(string: string) } else { // Recovery didn't work out. Let's face the truth diff --git a/Sources/HTMLKitComponents/Components/Text.swift b/Sources/HTMLKitComponents/Components/Text.swift index 700e526d..74b76b5e 100644 --- a/Sources/HTMLKitComponents/Components/Text.swift +++ b/Sources/HTMLKitComponents/Components/Text.swift @@ -25,9 +25,9 @@ public struct Text: View, Actionable, Modifiable { self.classes = ["text", "alignment:\(alignment.value)"] } - public init(_ localizedStringKey: String, alignment: Tokens.TextAlignment = .leading) { + public init(_ localizedStringKey: LocalizedStringKey, alignment: Tokens.TextAlignment = .leading) { - self.content = [LocalizedStringKey(key: localizedStringKey)] + self.content = [LocalizedString(key: localizedStringKey)] self.classes = ["text", "alignment:\(alignment.value)"] } diff --git a/Tests/HTMLKitTests/Localization/en-GB/mobile.strings b/Tests/HTMLKitTests/Localization/en-GB/mobile.strings index 9c0a22e6..2e66f69f 100644 --- a/Tests/HTMLKitTests/Localization/en-GB/mobile.strings +++ b/Tests/HTMLKitTests/Localization/en-GB/mobile.strings @@ -1,5 +1,2 @@ -/* A friendly greeting. */ -"greeting.world" = "Hello World"; - -/* A friendly greeting. */ -"greeting.person" = "Hello %st"; +/* A string key with a namespace pattern */ +"hello.world" = "Hello World"; diff --git a/Tests/HTMLKitTests/Localization/en-GB/web.strings b/Tests/HTMLKitTests/Localization/en-GB/web.strings index 929b84c6..5d5a8e27 100644 --- a/Tests/HTMLKitTests/Localization/en-GB/web.strings +++ b/Tests/HTMLKitTests/Localization/en-GB/web.strings @@ -1,8 +1,24 @@ -/* A friendly greeting. */ -"greeting.world" = "Hello World"; +/* String key with namespace pattern */ +"hello.world" = "Hello World"; -/* A friendly greeting. */ -"greeting.person" = "Hello %st"; +/* String key with namespace pattern and string interpolation */ +"cheers.person %st" = "Cheers %st"; -/* A personal indroduction. */ -"personal.intro" = "Hello, I am %st, and I am %in years old. I have a dog named %st. He is %in and %do inches tall."; +/* Interpolation with one value */ +"Hello %st" = "Hello %st"; + +/* Interpolation with multiple values */ +"Hello %st and %st" = "Hello %st and %st"; +"Do you %in have time at %dt?" = "Do you %in have time at %dt?"; + +/* Interpolation with a string value */ +"String: %st" = "String: %st"; + +/* Interpolation with a integer value */ +"Integer: %in" = "Integer: %in"; + +/* Interpolation with a double value */ +"Double: %do" = "Double: %do"; + +/* Interpolation with a date value */ +"Date: %dt" = "Date: %dt"; diff --git a/Tests/HTMLKitTests/Localization/fr/web.strings b/Tests/HTMLKitTests/Localization/fr/web.strings index 9aeb015b..53ac872d 100644 --- a/Tests/HTMLKitTests/Localization/fr/web.strings +++ b/Tests/HTMLKitTests/Localization/fr/web.strings @@ -1,2 +1,2 @@ -/* A friendly greeting. */ -"greeting.world" = "Bonjour le monde"; +/* String key with namespace pattern */ +"hello.world" = "Bonjour le monde"; diff --git a/Tests/HTMLKitTests/LocalizationTests.swift b/Tests/HTMLKitTests/LocalizationTests.swift index 5c269ebb..beb9edaf 100644 --- a/Tests/HTMLKitTests/LocalizationTests.swift +++ b/Tests/HTMLKitTests/LocalizationTests.swift @@ -1,8 +1,3 @@ -/* - Abstract: - The file tests the localization. - */ - import HTMLKit import XCTest @@ -24,7 +19,7 @@ final class LocalizationTests: XCTestCase { struct MainView: View { var body: Content { - Heading1("greeting.world") + Heading1("hello.world") } } @@ -44,18 +39,24 @@ final class LocalizationTests: XCTestCase { struct TestView: View { var body: Content { - Heading1("greeting.person", interpolation: "John Doe") + Paragraph("String: \("John Doe")") + Paragraph("Integer: \(31)") + Paragraph("Double: \(12.5)") + Paragraph("Date: \(Date.distantPast)") } } XCTAssertEqual(try renderer!.render(view: TestView()), """ -

Hello John Doe

+

String: John Doe

\ +

Integer: 31

\ +

Double: 12.5

\ +

Date: 01/01/0001

""" ) } - /// Tests the localization of string interpolation with multiple arguments + /// Tests the localization of string interpolation with multiple arguments and various data types /// /// The test expects the key to exist in the default translation table, to be correctly formatted /// with the arguments in the proper order, and to be rendered accurately. @@ -64,13 +65,17 @@ final class LocalizationTests: XCTestCase { struct TestView: View { var body: Content { - Paragraph("personal.intro", interpolation: "John Doe", 31, "Mozart", 5, 21.5) + Paragraph("Hello \("Jane") and \("John Doe")") + Paragraph("Do you \(2) have time at \(Date.distantPast)?") + Paragraph("cheers.person \("Jean")") } } XCTAssertEqual(try renderer!.render(view: TestView()), """ -

Hello, I am John Doe, and I am 31 years old. I have a dog named Mozart. He is 5 and 21.5 inches tall.

+

Hello Jane and John Doe

\ +

Do you 2 have time at 01/01/0001?

\ +

Cheers Jean

""" ) } @@ -83,7 +88,7 @@ final class LocalizationTests: XCTestCase { struct TestView: View { var body: Content { - Paragraph("greeting.world", tableName: "web") + Paragraph("hello.world", tableName: "web") } } @@ -120,7 +125,7 @@ final class LocalizationTests: XCTestCase { var body: Content { MainView { - Heading1("greeting.world") + Heading1("hello.world") .environment(key: \.locale) } } @@ -236,7 +241,7 @@ final class LocalizationTests: XCTestCase { var body: Content { MainView { - Heading1("greeting.person", interpolation: "John Doe") + Heading1("Hello \("John Doe")") .environment(key: \.locale) } } diff --git a/Tests/HTMLKitTests/PerformanceTests.swift b/Tests/HTMLKitTests/PerformanceTests.swift index ca8db25e..dba956bd 100644 --- a/Tests/HTMLKitTests/PerformanceTests.swift +++ b/Tests/HTMLKitTests/PerformanceTests.swift @@ -73,7 +73,7 @@ final class PerformanceTests: XCTestCase { struct TestView: View { var body: Content { - Paragraph("personal.intro", interpolation: "John Doe", 31, "Mozart", 5, 21.5) + Paragraph("Hello \("John Doe")") } } diff --git a/Tests/HTMLKitVaporTests/Localization/en-GB/mobile.strings b/Tests/HTMLKitVaporTests/Localization/en-GB/mobile.strings index 9c0a22e6..2e66f69f 100644 --- a/Tests/HTMLKitVaporTests/Localization/en-GB/mobile.strings +++ b/Tests/HTMLKitVaporTests/Localization/en-GB/mobile.strings @@ -1,5 +1,2 @@ -/* A friendly greeting. */ -"greeting.world" = "Hello World"; - -/* A friendly greeting. */ -"greeting.person" = "Hello %st"; +/* A string key with a namespace pattern */ +"hello.world" = "Hello World"; diff --git a/Tests/HTMLKitVaporTests/Localization/en-GB/web.strings b/Tests/HTMLKitVaporTests/Localization/en-GB/web.strings index 86fc468b..4bed9159 100644 --- a/Tests/HTMLKitVaporTests/Localization/en-GB/web.strings +++ b/Tests/HTMLKitVaporTests/Localization/en-GB/web.strings @@ -1,6 +1,3 @@ -/* A friendly greeting. */ -"greeting.world" = "Hello World"; - -/* A friendly greeting. */ -"greeting.person" = "Hello %st"; +/* A string key with a namespace pattern */ +"hello.world" = "Hello World"; diff --git a/Tests/HTMLKitVaporTests/Localization/fr/web.strings b/Tests/HTMLKitVaporTests/Localization/fr/web.strings index 9aeb015b..3b4758a0 100644 --- a/Tests/HTMLKitVaporTests/Localization/fr/web.strings +++ b/Tests/HTMLKitVaporTests/Localization/fr/web.strings @@ -1,2 +1,2 @@ -/* A friendly greeting. */ -"greeting.world" = "Bonjour le monde"; +/* A string key with a namespace pattern */ +"hello.world" = "Bonjour le monde"; diff --git a/Tests/HTMLKitVaporTests/ProviderTests.swift b/Tests/HTMLKitVaporTests/ProviderTests.swift index 135ec129..ba7bbb05 100644 --- a/Tests/HTMLKitVaporTests/ProviderTests.swift +++ b/Tests/HTMLKitVaporTests/ProviderTests.swift @@ -63,7 +63,7 @@ final class ProviderTests: XCTestCase { var body: HTMLKit.Content { MainView { - Paragraph("greeting.world") + Paragraph("hello.world") } } } From 9017033ee2cfbae86b5eb27e1394aff02033fe37 Mon Sep 17 00:00:00 2001 From: Mattes Mohr Date: Sat, 9 Nov 2024 16:23:08 +0100 Subject: [PATCH 2/5] Avoid type casting by using a type erasure --- .../Localization/InterpolationArgument.swift | 44 +++++++++++++ .../Framework/Localization/Localization.swift | 61 +++++++++---------- .../Localization/LocalizedString.swift | 2 +- .../Localization/LocalizedStringKey.swift | 38 +++++++----- 4 files changed, 99 insertions(+), 46 deletions(-) create mode 100644 Sources/HTMLKit/Framework/Localization/InterpolationArgument.swift diff --git a/Sources/HTMLKit/Framework/Localization/InterpolationArgument.swift b/Sources/HTMLKit/Framework/Localization/InterpolationArgument.swift new file mode 100644 index 00000000..9a538900 --- /dev/null +++ b/Sources/HTMLKit/Framework/Localization/InterpolationArgument.swift @@ -0,0 +1,44 @@ +import Foundation + +/// An enum that represents the data types of arguments used in interpolation. +/// +/// Each case corresponds to a specific data type and provides a placeholder +/// that can be used for replacing values in the localized string. +@_documentation(visibility: internal) +public enum InterpolationArgument { + + /// Holds an integer value + case int(Int) + + /// Holds a string value + case string(String) + + /// Holds a double value + case double(Double) + + /// Holds a float value + case float(Float) + + /// Holds a date value + case date(Date) + + /// The placeholder used for string interpolation + internal var placeholder: String { + switch self { + case .int(_): + return "%in" + + case .string(_): + return "%st" + + case .double(_): + return "%do" + + case .float(_): + return "%do" + + case .date(_): + return "%dt" + } + } +} diff --git a/Sources/HTMLKit/Framework/Localization/Localization.swift b/Sources/HTMLKit/Framework/Localization/Localization.swift index 632739fd..74db7d84 100644 --- a/Sources/HTMLKit/Framework/Localization/Localization.swift +++ b/Sources/HTMLKit/Framework/Localization/Localization.swift @@ -137,52 +137,52 @@ public class Localization { return localizationTables } + /// Replace the value with the placeholder + /// + /// - Parameters: + /// - placeholder: The placeholder to be replaced in + /// - value: The value to replace the placeholder with + /// - translation: The string in which the replacement will occur + private func replace(placeholder: String, with value: String, on translation: inout String) { + + if let range = translation.range(of: placeholder) { + translation = translation.replacingCharacters(in: range, with: value) + } + } + /// Apply interpolation values to the translation for the given locale /// /// - Parameters: - /// - arguments: An array of values used to replace placeholders within the translation string - /// - translation: The translation string - /// - locale: The locale - private func interpolate(arguments: [Any], to translation: inout String, for locale: Locale) { + /// - arguments: The arguments to replace the placeholders with + /// - translation: The string in which the interpolation will occur + /// - locale: The locale to respect during interpolation + private func interpolate(arguments: [InterpolationArgument], to translation: inout String, for locale: Locale) { for argument in arguments { - + switch argument { - case let stringValue as String: + case .int(let int): - if let range = translation.range(of: "%st") { - translation = translation.replacingCharacters(in: range, with: stringValue) - } + replace(placeholder: argument.placeholder, with: String(int), on: &translation) - case let dateValue as Date: + case .string(let string): - let formatter = DateFormatter() - formatter.dateFormat = locale.dateFormat + replace(placeholder: argument.placeholder, with: string, on: &translation) - if let range = translation.range(of: "%dt") { - translation = translation.replacingCharacters(in: range, with: formatter.string(from: dateValue)) - } + case .double(let double): - case let doubleValue as Double: + replace(placeholder: argument.placeholder, with: String(double), on: &translation) - if let range = translation.range(of: "%do") { - translation = translation.replacingCharacters(in: range, with: String(doubleValue)) - } + case .float(let float): - case let intValue as Int: - - if let range = translation.range(of: "%in") { - translation = translation.replacingCharacters(in: range, with: String(intValue)) - } + replace(placeholder: argument.placeholder, with: String(float), on: &translation) - case let floatValue as Float: + case .date(let date): - if let range = translation.range(of: "%do") { - translation = translation.replacingCharacters(in: range, with: String(floatValue)) - } + let formatter = DateFormatter() + formatter.dateFormat = locale.dateFormat - default: - break + replace(placeholder: argument.placeholder, with: formatter.string(from: date), on: &translation) } } } @@ -243,4 +243,3 @@ public class Localization { throw Errors.missingKey(string.key.value, currentLocale.tag) } } - diff --git a/Sources/HTMLKit/Framework/Localization/LocalizedString.swift b/Sources/HTMLKit/Framework/Localization/LocalizedString.swift index 306e2bb1..fa00f711 100644 --- a/Sources/HTMLKit/Framework/Localization/LocalizedString.swift +++ b/Sources/HTMLKit/Framework/Localization/LocalizedString.swift @@ -5,7 +5,7 @@ import Foundation public struct LocalizedString: Content { /// The key of the translation value - public let key: LocalizedStringKey + internal let key: LocalizedStringKey /// The name of the translation table internal let table: String? diff --git a/Sources/HTMLKit/Framework/Localization/LocalizedStringKey.swift b/Sources/HTMLKit/Framework/Localization/LocalizedStringKey.swift index c497b7a8..0cd4725a 100644 --- a/Sources/HTMLKit/Framework/Localization/LocalizedStringKey.swift +++ b/Sources/HTMLKit/Framework/Localization/LocalizedStringKey.swift @@ -4,17 +4,17 @@ import Foundation public struct LocalizedStringKey { /// The actual key value - public let value: String + internal let value: String /// The arguments for the interpolation - public var interpolation: [Any]? + internal var interpolation: [InterpolationArgument]? /// Initializes a string key for localization /// /// - Parameters: /// - value: The key value /// - interpolation: An array of values that will replace placeholders within the translation string. - public init(value: String, interpolation: [Any]? = nil) { + public init(value: String, interpolation: [InterpolationArgument]? = nil) { self.value = value self.interpolation = interpolation @@ -35,7 +35,7 @@ extension LocalizedStringKey: ExpressibleByStringLiteral, ExpressibleByStringInt var key: String = "" - var arguments: [Any] = [] + var arguments: [InterpolationArgument] = [] public init(literalCapacity: Int, interpolationCount: Int) { @@ -50,37 +50,47 @@ extension LocalizedStringKey: ExpressibleByStringLiteral, ExpressibleByStringInt public mutating func appendInterpolation(_ value: String) { - key += "%st" + let argument = InterpolationArgument.string(value) - arguments.append(value) + key += argument.placeholder + + arguments.append(argument) } public mutating func appendInterpolation(_ value: Int) { - key += "%in" + let argument = InterpolationArgument.int(value) + + key += argument.placeholder - arguments.append(value) + arguments.append(argument) } public mutating func appendInterpolation(_ value: Double) { - key += "%do" + let argument = InterpolationArgument.double(value) + + key += argument.placeholder - arguments.append(value) + arguments.append(argument) } public mutating func appendInterpolation(_ value: Float) { - key += "%do" + let argument = InterpolationArgument.float(value) - arguments.append(value) + key += argument.placeholder + + arguments.append(.float(value)) } public mutating func appendInterpolation(_ value: Date) { - key += "%dt" + let argument = InterpolationArgument.date(value) + + key += argument.placeholder - arguments.append(value) + arguments.append(argument) } } } From cd1df33b5698b2a6ac2c07ca653d8ec4f6c7b9ca Mon Sep 17 00:00:00 2001 From: Mattes Mohr Date: Fri, 29 Nov 2024 10:37:34 +0100 Subject: [PATCH 3/5] Return the fallback literal instead of throwing an error --- .../Localization/LocalizedStringKey.swift | 44 ++++++++++++++++--- .../Framework/Rendering/Renderer.swift | 19 +++----- Tests/HTMLKitTests/LocalizationTests.swift | 18 +++----- 3 files changed, 50 insertions(+), 31 deletions(-) diff --git a/Sources/HTMLKit/Framework/Localization/LocalizedStringKey.swift b/Sources/HTMLKit/Framework/Localization/LocalizedStringKey.swift index 0cd4725a..39050e60 100644 --- a/Sources/HTMLKit/Framework/Localization/LocalizedStringKey.swift +++ b/Sources/HTMLKit/Framework/Localization/LocalizedStringKey.swift @@ -3,20 +3,28 @@ import Foundation /// A string key for the localization public struct LocalizedStringKey { - /// The actual key value + /// The key value internal let value: String + /// A fallback literal string + /// + /// > Note: This literal is not intended for lookup in the translation table. Instead, it serves as + /// > a default value if localization is not set up or if the key is not found at all. + internal let literal: String + /// The arguments for the interpolation internal var interpolation: [InterpolationArgument]? /// Initializes a string key for localization - /// + /// /// - Parameters: /// - value: The key value - /// - interpolation: An array of values that will replace placeholders within the translation string. - public init(value: String, interpolation: [InterpolationArgument]? = nil) { + /// - literal: The default value + /// - interpolation: The arguments toreplace placeholders within the translation string + public init(value: String, literal: String, interpolation: [InterpolationArgument]? = nil) { self.value = value + self.literal = literal self.interpolation = interpolation } } @@ -24,19 +32,26 @@ public struct LocalizedStringKey { extension LocalizedStringKey: ExpressibleByStringLiteral, ExpressibleByStringInterpolation { public init(stringLiteral: String) { - self.init(value: stringLiteral) + self.init(value: stringLiteral, literal: stringLiteral) } public init(stringInterpolation: StringInterpolation) { - self.init(value: stringInterpolation.key, interpolation: stringInterpolation.arguments) + self.init(value: stringInterpolation.key, + literal: stringInterpolation.literal, + interpolation: stringInterpolation.arguments) } public struct StringInterpolation: StringInterpolationProtocol { - var key: String = "" + /// The key to be localized + var key = "" + /// The arguments for the interpolation var arguments: [InterpolationArgument] = [] + /// The string literal + var literal = "" + public init(literalCapacity: Int, interpolationCount: Int) { key.reserveCapacity(literalCapacity + interpolationCount * 2) @@ -45,11 +60,16 @@ extension LocalizedStringKey: ExpressibleByStringLiteral, ExpressibleByStringInt } public mutating func appendLiteral(_ literal: String) { + + self.literal += literal + key.append(literal) } public mutating func appendInterpolation(_ value: String) { + literal += value + let argument = InterpolationArgument.string(value) key += argument.placeholder @@ -59,6 +79,8 @@ extension LocalizedStringKey: ExpressibleByStringLiteral, ExpressibleByStringInt public mutating func appendInterpolation(_ value: Int) { + literal += String(value) + let argument = InterpolationArgument.int(value) key += argument.placeholder @@ -68,6 +90,8 @@ extension LocalizedStringKey: ExpressibleByStringLiteral, ExpressibleByStringInt public mutating func appendInterpolation(_ value: Double) { + literal += String(value) + let argument = InterpolationArgument.double(value) key += argument.placeholder @@ -77,6 +101,8 @@ extension LocalizedStringKey: ExpressibleByStringLiteral, ExpressibleByStringInt public mutating func appendInterpolation(_ value: Float) { + literal += String(value) + let argument = InterpolationArgument.float(value) key += argument.placeholder @@ -86,6 +112,10 @@ extension LocalizedStringKey: ExpressibleByStringLiteral, ExpressibleByStringInt public mutating func appendInterpolation(_ value: Date) { + let formatter = DateFormatter() + + literal += formatter.string(from: value) + let argument = InterpolationArgument.date(value) key += argument.placeholder diff --git a/Sources/HTMLKit/Framework/Rendering/Renderer.swift b/Sources/HTMLKit/Framework/Rendering/Renderer.swift index 7512419a..91f698f7 100644 --- a/Sources/HTMLKit/Framework/Rendering/Renderer.swift +++ b/Sources/HTMLKit/Framework/Rendering/Renderer.swift @@ -24,9 +24,6 @@ public final class Renderer { /// Indicates a missing environment value. case environmentValueNotFound - - /// Indicates a missing localization configuration. - case missingLocalization /// A brief error description. public var description: String { @@ -43,9 +40,6 @@ public final class Renderer { case .environmentObjectNotFound: return "Unable to retrieve environment object." - - case .missingLocalization: - return "The localization seem to missing." } } } @@ -360,7 +354,8 @@ public final class Renderer { private func render(localized string: LocalizedString) throws -> String { guard let localization = self.localization else { - throw Errors.missingLocalization + // Bail early with the fallback since the localization isn't set up + return string.key.literal } do { @@ -368,18 +363,16 @@ public final class Renderer { } catch Localization.Errors.missingKey(let key, let locale) { + logger.warning("Unable to find translation key '\(key)' for the locale '\(locale)'.") + // Check if the fallback was already in charge if environment.locale != nil { - logger.warning("Unable to find translation key '\(key)' for the locale '\(locale)'.") - // Seems not, let's try to recover by using the fallback return try localization.localize(string: string) - - } else { - // Recovery didn't work out. Let's face the truth - throw Localization.Errors.missingKey(key, locale) } + + return string.key.literal } } diff --git a/Tests/HTMLKitTests/LocalizationTests.swift b/Tests/HTMLKitTests/LocalizationTests.swift index beb9edaf..414f1a67 100644 --- a/Tests/HTMLKitTests/LocalizationTests.swift +++ b/Tests/HTMLKitTests/LocalizationTests.swift @@ -143,9 +143,9 @@ final class LocalizationTests: XCTestCase { /// Tests the behavior when a localization key is missing /// /// A key is considered as missing if it cannot be found in the translation table. In this case, - /// the renderer is expected to throw an error. + /// the renderer is expected to use the fallback literal string. func testMissingKey() throws { - + struct MainView: View { var body: Content { @@ -153,15 +153,11 @@ final class LocalizationTests: XCTestCase { } } - XCTAssertThrowsError(try renderer!.render(view: MainView())) { error in - - guard let localizationError = error as? Localization.Errors else { - return XCTFail("Unexpected error type: \(error)") - } - - XCTAssertEqual(localizationError, .missingKey("unknown.key", "en-GB")) - XCTAssertEqual(localizationError.description, "Unable to find translation key 'unknown.key' for the locale 'en-GB'.") - } + XCTAssertEqual(try renderer!.render(view: MainView()), + """ +

unknown.key

+ """ + ) } /// Tests the behavior when a translation table is missing From c92bd450e2e73ddf6bb6f5c57679ae11f06ffb55 Mon Sep 17 00:00:00 2001 From: Mattes Mohr Date: Tue, 3 Dec 2024 21:00:36 +0100 Subject: [PATCH 4/5] Try to solve the test errors --- Tests/HTMLKitTests/LocalizationTests.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/HTMLKitTests/LocalizationTests.swift b/Tests/HTMLKitTests/LocalizationTests.swift index 414f1a67..c7b5eb91 100644 --- a/Tests/HTMLKitTests/LocalizationTests.swift +++ b/Tests/HTMLKitTests/LocalizationTests.swift @@ -42,7 +42,7 @@ final class LocalizationTests: XCTestCase { Paragraph("String: \("John Doe")") Paragraph("Integer: \(31)") Paragraph("Double: \(12.5)") - Paragraph("Date: \(Date.distantPast)") + Paragraph("Date: \(Date(timeIntervalSince1970: 50000))") } } @@ -51,7 +51,7 @@ final class LocalizationTests: XCTestCase {

String: John Doe

\

Integer: 31

\

Double: 12.5

\ -

Date: 01/01/0001

+

Date: 01/01/1970

""" ) } @@ -66,7 +66,7 @@ final class LocalizationTests: XCTestCase { var body: Content { Paragraph("Hello \("Jane") and \("John Doe")") - Paragraph("Do you \(2) have time at \(Date.distantPast)?") + Paragraph("Do you \(2) have time at \(Date(timeIntervalSince1970: 50000))?") Paragraph("cheers.person \("Jean")") } } @@ -74,7 +74,7 @@ final class LocalizationTests: XCTestCase { XCTAssertEqual(try renderer!.render(view: TestView()), """

Hello Jane and John Doe

\ -

Do you 2 have time at 01/01/0001?

\ +

Do you 2 have time at 01/01/1970?

\

Cheers Jean

""" ) From 6a6cc14f30f487458f380314e1633fc56ba94124 Mon Sep 17 00:00:00 2001 From: Mattes Mohr Date: Wed, 4 Dec 2024 21:21:28 +0100 Subject: [PATCH 5/5] Replace the argument placeholders with the format specifiers used in swift --- .../Localization/InterpolationArgument.swift | 11 ++++++----- .../HTMLKitTests/Localization/en-GB/web.strings | 16 ++++++++-------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/Sources/HTMLKit/Framework/Localization/InterpolationArgument.swift b/Sources/HTMLKit/Framework/Localization/InterpolationArgument.swift index 9a538900..415c7513 100644 --- a/Sources/HTMLKit/Framework/Localization/InterpolationArgument.swift +++ b/Sources/HTMLKit/Framework/Localization/InterpolationArgument.swift @@ -24,21 +24,22 @@ public enum InterpolationArgument { /// The placeholder used for string interpolation internal var placeholder: String { + switch self { case .int(_): - return "%in" + return "%lld" case .string(_): - return "%st" + return "%@" case .double(_): - return "%do" + return "%f" case .float(_): - return "%do" + return "%f" case .date(_): - return "%dt" + return "%@" } } } diff --git a/Tests/HTMLKitTests/Localization/en-GB/web.strings b/Tests/HTMLKitTests/Localization/en-GB/web.strings index 5d5a8e27..a32cbf1d 100644 --- a/Tests/HTMLKitTests/Localization/en-GB/web.strings +++ b/Tests/HTMLKitTests/Localization/en-GB/web.strings @@ -2,23 +2,23 @@ "hello.world" = "Hello World"; /* String key with namespace pattern and string interpolation */ -"cheers.person %st" = "Cheers %st"; +"cheers.person %@" = "Cheers %@"; /* Interpolation with one value */ -"Hello %st" = "Hello %st"; +"Hello %@" = "Hello %@"; /* Interpolation with multiple values */ -"Hello %st and %st" = "Hello %st and %st"; -"Do you %in have time at %dt?" = "Do you %in have time at %dt?"; +"Hello %@ and %@" = "Hello %@ and %@"; +"Do you %lld have time at %@?" = "Do you %lld have time at %@?"; /* Interpolation with a string value */ -"String: %st" = "String: %st"; +"String: %@" = "String: %@"; /* Interpolation with a integer value */ -"Integer: %in" = "Integer: %in"; +"Integer: %lld" = "Integer: %lld"; /* Interpolation with a double value */ -"Double: %do" = "Double: %do"; +"Double: %f" = "Double: %f"; /* Interpolation with a date value */ -"Date: %dt" = "Date: %dt"; +"Date: %@" = "Date: %@";