diff --git a/Makefile b/Makefile index fe8c794..2144d34 100644 --- a/Makefile +++ b/Makefile @@ -14,6 +14,14 @@ override platform?=${DOCKER_IMAGE_PLATFORM} override config?=${SWIFT_BUILD_CONFIGURATION} override clean?=${DOCKER_IMAGE_CLEAN} +# --- IDE --- + +xcode: ## Open this package in Xcode. + @open -a Xcode Package.swift + +vscode: ## Open this package with Visual Studio Code. + @code . + # --- DEPENDENCIES --- outdated: ## List the package dependencies that can be updated. @@ -70,11 +78,6 @@ flush-images: ## Flush all outstanding Swift docker images. @docker images \ --all | grep ${DOCKER_IMAGE_NAME} | awk '{print $$3}' | xargs docker rmi --force -# --- MISCELLANEOUS --- - -xcode: ## Open this package in Xcode. - @open -a Xcode Package.swift - # --- HELP --- # Outputs the documentation for each of the defined tasks when `help` is called. diff --git a/Package.swift b/Package.swift index f7c463c..26716a6 100644 --- a/Package.swift +++ b/Package.swift @@ -91,6 +91,7 @@ targetsPackage.append(contentsOf: [ let package = Package( name: .Package.name, + defaultLocalization: "en", platforms: [ .iOS(.v15), .macOS(.v12), diff --git a/Sources/Core/Errors/BundleError.swift b/Sources/Core/Errors/BundleError.swift new file mode 100644 index 0000000..ee4dc76 --- /dev/null +++ b/Sources/Core/Errors/BundleError.swift @@ -0,0 +1,15 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftLibs open source project +// +// Copyright (c) 2023 Röck+Cöde VoF. and the SwiftLibs project authors +// Licensed under the EUPL 1.2 or later. +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftLibs project authors +// +//===----------------------------------------------------------------------===// + +public enum BundleError: Error { + case bundleNotFound +} diff --git a/Sources/Core/Extensions/Bundle+LocalisationBundle.swift b/Sources/Core/Extensions/Bundle+LocalisationBundle.swift new file mode 100644 index 0000000..ee2205a --- /dev/null +++ b/Sources/Core/Extensions/Bundle+LocalisationBundle.swift @@ -0,0 +1,42 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftLibs open source project +// +// Copyright (c) 2023 Röck+Cöde VoF. and the SwiftLibs project authors +// Licensed under the EUPL 1.2 or later. +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftLibs project authors +// +//===----------------------------------------------------------------------===// + +import Foundation + +public extension Bundle { + + // MARK: Functions + + /// Retrieve a localisation bundle for a given language code or identifier, if exist inside a certain bundle. + /// - Parameter languageCode: A string that represent a language code or identifier. + /// - Returns: A `Bundle` instance that contains localised resources based on a given language code or identifier. + /// - Throws: A `BundleError` error in case the localisation bundle for a given language code or identifier is not found inside a certain bundle. + func localisation(for languageCode: String) throws -> Bundle { + guard + let path = path(forResource: languageCode, ofType: .ResourceType.localisationBundle), + let bundle = Bundle(path: path) + else { + throw BundleError.bundleNotFound + } + + return bundle + } + +} + +// MARK: - String+Constants + +private extension String { + enum ResourceType { + static let localisationBundle = "lproj" + } +} diff --git a/Sources/Core/Extensions/String+Localisation.swift b/Sources/Core/Extensions/String+Localisation.swift new file mode 100644 index 0000000..2a79450 --- /dev/null +++ b/Sources/Core/Extensions/String+Localisation.swift @@ -0,0 +1,45 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftLibs open source project +// +// Copyright (c) 2023 Röck+Cöde VoF. and the SwiftLibs project authors +// Licensed under the EUPL 1.2 or later. +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftLibs project authors +// +//===----------------------------------------------------------------------===// + +import Foundation + +public extension String { + + // MARK: Functions + + /// Localise a string based on a given language code or identifier in an specific bundle. + /// - Parameters: + /// - languageCode: A string that represent a language code or identifier. + /// - bundle: A bundle in which to retrieve a localisation bundle. + /// - value: A default value to return if key is nil or if a localized string for key can't be found in the table. + /// - table: The receiver's string table to search. In case of nil or an empty string, the method attempts to use the table in `Localizable.strings`. + /// - Returns: A localized version of the string in case it is found. Otherwise, it returns the original string or a default string, if provided. + func localise( + for languageCode: String, + in bundle: Bundle, + value: String? = nil, + table: String? = nil + ) -> String { + do { + return try bundle + .localisation(for: languageCode) + .localizedString( + forKey: self, + value: value, + table: table + ) + } catch { + return value ?? self + } + } + +} diff --git a/Tests/Core/Cases/Extensions/Bundle+LocalisationBundleTests.swift b/Tests/Core/Cases/Extensions/Bundle+LocalisationBundleTests.swift new file mode 100644 index 0000000..dc9b8f5 --- /dev/null +++ b/Tests/Core/Cases/Extensions/Bundle+LocalisationBundleTests.swift @@ -0,0 +1,49 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftLibs open source project +// +// Copyright (c) 2023 Röck+Cöde VoF. and the SwiftLibs project authors +// Licensed under the EUPL 1.2 or later. +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftLibs project authors +// +//===----------------------------------------------------------------------===// + +import Core +import Foundation +import XCTest + +final class Bundle_LocalisationBundleTests: XCTestCase { + + // MARK: Properties + + private let bundle = Bundle.module + + private var languageCode: String! + + // MARK: Tests + + func test_localisation_withExistingLocalisationBundle() throws { + // GIVEN + languageCode = "en" + + // WHEN + let localisationBundle = try bundle.localisation(for: languageCode) + + // THEN + XCTAssertNotNil(localisationBundle) + XCTAssertEqual(localisationBundle.bundleURL.lastPathComponent, "en.lproj") + } + + func test_localisation_withNonExistingLocalisationBundle() throws { + // GIVEN + languageCode = "nl" + + // WHEN & THEN + XCTAssertThrowsError(try bundle.localisation(for: languageCode)) { error in + XCTAssertEqual(error as? BundleError, .bundleNotFound) + } + } + +} diff --git a/Tests/Core/Cases/Extensions/String+LocalisationTests.swift b/Tests/Core/Cases/Extensions/String+LocalisationTests.swift new file mode 100644 index 0000000..dd6e809 --- /dev/null +++ b/Tests/Core/Cases/Extensions/String+LocalisationTests.swift @@ -0,0 +1,196 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftLibs open source project +// +// Copyright (c) 2023 Röck+Cöde VoF. and the SwiftLibs project authors +// Licensed under the EUPL 1.2 or later. +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftLibs project authors +// +//===----------------------------------------------------------------------===// + +import Core +import Foundation +import XCTest + +final class String_LocalisationTests: XCTestCase { + + // MARK: Properties + + private var languageCode: String! + private var stringToLocalise: String! + private var localisedString: String! + + private var defaultValue: String? + + // MARK: Tests + + func test_localise_definedKey_inDefinedLocalisationBundle() { + // GIVEN + languageCode = "en" + stringToLocalise = .Seed.Localisation.someLocalisableString + + // WHEN + localisedString = stringToLocalise.localise(for: languageCode, in: .module) + + // THEN + XCTAssertEqual(localisedString, .Result.Localisation.someLocalisableString) + } + + func test_localise_definedKey_inDefinedLocalisationBundle_withDefaultValue() { + // GIVEN + languageCode = "en" + stringToLocalise = .Seed.Localisation.otherLocalisableString + defaultValue = "Some default value goes here..." + + // WHEN + localisedString = stringToLocalise.localise( + for: languageCode, + in: .module, + value: defaultValue + ) + + // THEN + XCTAssertEqual(localisedString, .Result.Localisation.otherLocalisableString) + } + + func test_localise_definedKey_inDefinedLocalisationBundle_withDefinedTable() { + // GIVEN + languageCode = "en" + stringToLocalise = .Seed.Localisation.someLocalisableString + + // WHEN + localisedString = stringToLocalise.localise( + for: languageCode, + in: .module, + table: "Some table name goes in here..." + ) + + // THEN + XCTAssertEqual(localisedString, stringToLocalise) + } + + func test_localise_definedKey_inDefinedLocalisationBundle_withDefauledValue_andDefinedTable() { + // GIVEN + languageCode = "en" + stringToLocalise = .Seed.Localisation.otherLocalisableString + defaultValue = "Some default value goes in here..." + + // WHEN + localisedString = stringToLocalise.localise( + for: languageCode, + in: .module, + value: defaultValue, + table: "Some table name goes in here..." + ) + + // THEN + XCTAssertEqual(localisedString, defaultValue) + } + + func test_localise_definedKey_inNotDefinedLocalisationBundle() { + // GIVEN + languageCode = "nl" + stringToLocalise = .Seed.Localisation.someLocalisableString + + // WHEN + localisedString = stringToLocalise.localise(for: languageCode, in: .module) + + // THEN + XCTAssertEqual(localisedString, stringToLocalise) + } + + func test_localise_definedKey_inNotDefinedLocalisationBundle_withDefaultValue() { + // GIVEN + languageCode = "nl" + stringToLocalise = .Seed.Localisation.otherLocalisableString + defaultValue = "Some default value goes in here..." + + // WHEN + localisedString = stringToLocalise.localise( + for: languageCode, + in: .module, + value: defaultValue + ) + + // THEN + XCTAssertEqual(localisedString, defaultValue) + } + + func test_localise_notDefinedKey_inDefinedLocalisationBundle() { + // GIVEN + languageCode = "en" + stringToLocalise = .Seed.Localisation.notLocalisableString + + // WHEN + localisedString = stringToLocalise.localise(for: languageCode, in: .module) + + // THEN + XCTAssertEqual(localisedString, stringToLocalise) + } + + func test_localise_notDefinedKey_inDefinedLocalisationBundle_withDefaultValue() { + languageCode = "en" + stringToLocalise = .Seed.Localisation.notLocalisableString + defaultValue = "Some default value goes here..." + + // WHEN + localisedString = stringToLocalise.localise( + for: languageCode, + in: .module, + value: defaultValue + ) + + // THEN + XCTAssertEqual(localisedString, defaultValue) + } + + func test_localise_inDifferentBundle() { + // GIVEN + languageCode = "en" + stringToLocalise = .Seed.Localisation.someLocalisableString + + // WHEN + localisedString = stringToLocalise.localise(for: languageCode, in: .main) + + // THEN + XCTAssertEqual(localisedString, stringToLocalise) + } + + func test_localise_inDifferentBundle_withDefaultValue() { + // GIVEN + languageCode = "en" + stringToLocalise = .Seed.Localisation.otherLocalisableString + defaultValue = "Some default value goes here..." + + // WHEN + localisedString = stringToLocalise.localise( + for: languageCode, + in: .main, + value: defaultValue + ) + + // THEN + XCTAssertEqual(localisedString, defaultValue) + } + +} + // MARK: - String+Seed + +private extension String.Seed { + enum Localisation { + static let someLocalisableString = "test.core.bundle.some-localisable-string" + static let otherLocalisableString = "test.core.bundle.other-localisable-string" + static let notLocalisableString = "test.core.bundle.non-localisable-string" + } +} + +// MARK: - String+Result + +private extension String.Result { + enum Localisation { + static let someLocalisableString = "Some localisable string to use for testing purposes." + static let otherLocalisableString = "Other localisable string to use for testing purposes." + } +} diff --git a/Tests/Core/Resources/en.lproj/Localizable.strings b/Tests/Core/Resources/en.lproj/Localizable.strings new file mode 100644 index 0000000..66ea830 --- /dev/null +++ b/Tests/Core/Resources/en.lproj/Localizable.strings @@ -0,0 +1,14 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftLibs open source project +// +// Copyright (c) 2023 Röck+Cöde VoF. and the SwiftLibs project authors +// Licensed under the EUPL 1.2 or later. +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftLibs project authors +// +//===----------------------------------------------------------------------===// + +"test.core.bundle.some-localisable-string" = "Some localisable string to use for testing purposes."; +"test.core.bundle.other-localisable-string" = "Other localisable string to use for testing purposes.";