diff --git a/Sources/HTMLKit/Framework/Localization/Localization.swift b/Sources/HTMLKit/Framework/Localization/Localization.swift index 74db7d84..45ea3cf9 100644 --- a/Sources/HTMLKit/Framework/Localization/Localization.swift +++ b/Sources/HTMLKit/Framework/Localization/Localization.swift @@ -56,6 +56,16 @@ public class Localization { } } + /// Indicates whether the localization is properly configured + internal var isConfigured: Bool { + + if self.tables != nil && self.locale != nil { + return true + } + + return false + } + /// The translations tables internal var tables: [Locale: [TranslationTable]]? diff --git a/Sources/HTMLKit/Framework/Rendering/Renderer.swift b/Sources/HTMLKit/Framework/Rendering/Renderer.swift index 91f698f7..56ba9c6a 100644 --- a/Sources/HTMLKit/Framework/Rendering/Renderer.swift +++ b/Sources/HTMLKit/Framework/Rendering/Renderer.swift @@ -354,7 +354,12 @@ public final class Renderer { private func render(localized string: LocalizedString) throws -> String { guard let localization = self.localization else { - // Bail early with the fallback since the localization isn't set up + // Bail early with the fallback since the localization is not in use + return string.key.literal + } + + if !localization.isConfigured { + // Bail early, since the localization is not properly configured return string.key.literal } diff --git a/Sources/HTMLKitVapor/Extensions/Vapor+HTMLKit.swift b/Sources/HTMLKitVapor/Extensions/Vapor+HTMLKit.swift index 8d6af0e1..fd8f5bfa 100644 --- a/Sources/HTMLKitVapor/Extensions/Vapor+HTMLKit.swift +++ b/Sources/HTMLKitVapor/Extensions/Vapor+HTMLKit.swift @@ -112,7 +112,7 @@ extension Request { public var htmlkit: ViewRenderer { if let acceptLanguage = self.acceptLanguage { - self.application.htmlkit.localization.set(locale: acceptLanguage) + self.application.htmlkit.environment.locale = HTMLKit.Locale(tag: acceptLanguage) } return .init(eventLoop: self.eventLoop, configuration: self.application.htmlkit.configuration, logger: self.logger) diff --git a/Tests/HTMLKitVaporTests/ProviderTests.swift b/Tests/HTMLKitVaporTests/ProviderTests.swift index ba7bbb05..517f46a8 100644 --- a/Tests/HTMLKitVaporTests/ProviderTests.swift +++ b/Tests/HTMLKitVaporTests/ProviderTests.swift @@ -155,22 +155,162 @@ final class ProviderTests: XCTestCase { ) } } - + + /// Tests the setup of localization through Vapor func testLocalizationIntegration() throws { - let currentFile = URL(fileURLWithPath: #file).deletingLastPathComponent() + guard let source = Bundle.module.url(forResource: "Localization", withExtension: nil) else { + return + } + + let app = Application(.testing) + + defer { app.shutdown() } + + app.htmlkit.localization.set(source: source) + app.htmlkit.localization.set(locale: "fr") + + app.get("test") { request async throws -> Vapor.View in + + return try await request.htmlkit.render(TestPage.ChildView()) + } + + try app.test(.GET, "test") { response in + XCTAssertEqual(response.status, .ok) + XCTAssertEqual(response.body.string, + """ + \ + \ +
\ +Bonjour le monde
\ + \ + + """ + ) + } + } + + /// Tests the behavior when localization is not properly configured + /// + /// Localization is considered improperly configured when one or both of the essential factors are missing. + /// In such case the renderer is expected to skip the localization and directly return the fallback string literal. + func testLocalizationFallback() throws { + + let app = Application(.testing) + + defer { app.shutdown() } + + app.get("test") { request async throws -> Vapor.View in + + return try await request.htmlkit.render(TestPage.ChildView()) + } + + try app.test(.GET, "test") { response in + XCTAssertEqual(response.status, .ok) + XCTAssertEqual(response.body.string, + """ + \ + \ + \ +hello.world
\ + \ + + """ + ) + } + } + + /// Tests the error reporting to Vapor for issues that may occur during localization + /// + /// The error is expected to be classified as an internal server error and includes a error message. + func testLocalizationErrorReporting() throws { + + struct UnknownTableView: HTMLKit.View { + + var body: HTMLKit.Content { + Paragraph("hello.world", tableName: "unknown.table") + } + } + + struct UnknownTagView: HTMLKit.View { + + var body: HTMLKit.Content { + Division { + Heading1("greeting.world") + .environment(key: \.locale) + } + .environment(key: \.locale, value: Locale(tag: "unknown.tag")) + } + } - let currentDirectory = currentFile.appendingPathComponent("Localization") + guard let source = Bundle.module.url(forResource: "Localization", withExtension: nil) else { + return + } let app = Application(.testing) defer { app.shutdown() } - app.htmlkit.localization.set(source: currentDirectory) + app.htmlkit.localization.set(source: source) app.htmlkit.localization.set(locale: "fr") + app.get("unknowntable") { request async throws -> Vapor.View in + + return try await request.htmlkit.render(UnknownTableView()) + } + + app.get("unknowntag") { request async throws -> Vapor.View in + + return try await request.htmlkit.render(UnknownTagView()) + } + + try app.test(.GET, "unknowntable") { response in + + XCTAssertEqual(response.status, .internalServerError) + + let abort = try response.content.decode(AbortResponse.self) + + XCTAssertEqual(abort.reason, "Unable to find translation table 'unknown.table' for the locale 'fr'.") + } + + try app.test(.GET, "unknowntag") { response in + + XCTAssertEqual(response.status, .internalServerError) + + let abort = try response.content.decode(AbortResponse.self) + + XCTAssertEqual(abort.reason, "Unable to find a translation table for the locale 'unknown.tag'.") + } + } + + /// Tests the localization behavior based on the accept language of the client + /// + /// The environment locale is expected to be changed according to the language given by the provider. + /// The renderer is expected to localize correctly the content based on the updated environment locale. + func testLocalizationByAcceptingHeaders() throws { + + guard let source = Bundle.module.url(forResource: "Localization", withExtension: nil) else { + return + } + + let app = Application(.testing) + + defer { app.shutdown() } + + app.htmlkit.localization.set(source: source) + app.htmlkit.localization.set(locale: "en-GB") + app.get("test") { request async throws -> Vapor.View in + // Overwrite the accept language header to simulate a different language + request.headers.replaceOrAdd(name: "accept-language", value: "fr") + return try await request.htmlkit.render(TestPage.ChildView()) } diff --git a/Tests/HTMLKitVaporTests/Utilities/AbortResponse.swift b/Tests/HTMLKitVaporTests/Utilities/AbortResponse.swift new file mode 100644 index 00000000..fa0afae8 --- /dev/null +++ b/Tests/HTMLKitVaporTests/Utilities/AbortResponse.swift @@ -0,0 +1,6 @@ +import Vapor + +struct AbortResponse: Vapor.Content { + + var reason: String +}