From 5f718d92bd61cecbf34484cebe6057db7b383e56 Mon Sep 17 00:00:00 2001 From: Kaley Nguyen Date: Fri, 21 Nov 2025 16:13:07 +0700 Subject: [PATCH 1/9] MT-8247 New W3WRfcLanguage --- Package.swift | 1 + .../W3WTranslationsProtocol.swift | 18 +- .../W3WRfcLanguage+W3WLanguage.swift | 26 +++ .../W3WRfcLanguage+W3WSdkLanguage.swift | 29 +++ .../RfcLanguage/W3WRfcLanguage.md | 29 +++ .../RfcLanguage/W3WRfcLanguage.swift | 132 ++++++++++++ .../RfcLanguage/W3WRfcLanguageProtocol.swift | 19 ++ .../Types/Util/LanguageUtils.swift | 35 ++++ .../Types/Values/W3WLanguage.swift | 51 ++--- .../W3wRfcLanguage_w3wLanguage_tests.swift | 198 ++++++++++++++++++ .../W3wRfcLanguage_w3wSdkLanguage_tests.swift | 79 +++++++ 11 files changed, 587 insertions(+), 30 deletions(-) create mode 100644 Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage+W3WLanguage.swift create mode 100644 Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage+W3WSdkLanguage.swift create mode 100644 Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage.md create mode 100644 Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage.swift create mode 100644 Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguageProtocol.swift create mode 100644 Sources/W3WSwiftCore/Types/Util/LanguageUtils.swift create mode 100644 Tests/languages-Tests/W3wRfcLanguage_w3wLanguage_tests.swift create mode 100644 Tests/languages-Tests/W3wRfcLanguage_w3wSdkLanguage_tests.swift diff --git a/Package.swift b/Package.swift index 4d907e4..afd910f 100644 --- a/Package.swift +++ b/Package.swift @@ -16,5 +16,6 @@ let package = Package( .target(name: "W3WSwiftCore", dependencies: []), .testTarget(name: "w3w-swift-typesTests", dependencies: ["W3WSwiftCore"]), .testTarget(name: "w3w-swift-Tests", dependencies: ["W3WSwiftCore"]), + .testTarget(name: "languages-Tests", dependencies: ["W3WSwiftCore"]) ] ) diff --git a/Sources/W3WSwiftCore/Localization/W3WTranslationsProtocol.swift b/Sources/W3WSwiftCore/Localization/W3WTranslationsProtocol.swift index 259726a..8e45cd1 100644 --- a/Sources/W3WSwiftCore/Localization/W3WTranslationsProtocol.swift +++ b/Sources/W3WSwiftCore/Localization/W3WTranslationsProtocol.swift @@ -6,7 +6,7 @@ // -public protocol W3WTranslationsProtocol { +public protocol W3WTranslationsProtocol: W3WAvailableLanguageProtocol { /// given a translation id return the translation for the given device locale /// - Parameters: @@ -21,6 +21,15 @@ public protocol W3WTranslationsProtocol { } +public protocol W3WLanguageSelectionProtocol { + /// to set RFCLanguage + func set(language: W3WRfcLanguage) +} + +/// list out all available RFCLanguages +public protocol W3WAvailableLanguageProtocol { + func availableLanguages() -> [W3WRfcLanguage] +} public extension W3WTranslationsProtocol { @@ -42,3 +51,10 @@ public extension W3WTranslationsProtocol { return String(format: localized, arguments) } } + +public extension W3WAvailableLanguageProtocol { + /// default convenience func for available languages, should override when in need + func availableLanguages() -> [W3WRfcLanguage] { + return [] + } +} diff --git a/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage+W3WLanguage.swift b/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage+W3WLanguage.swift new file mode 100644 index 0000000..c5aab5c --- /dev/null +++ b/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage+W3WLanguage.swift @@ -0,0 +1,26 @@ +// +// File.swift +// w3w-swift-core +// +// Created by Kaley Nguyen on 16/9/25. +// + +import Foundation + +public extension W3WRfcLanguageProtocol { + /// convert W3WRfcLanguage to W3WLanguage + func toW3wLanguage() -> W3WLanguage? { + guard let code, !code.isEmpty else { + return nil + } + return W3WBaseLanguage(locale: self.shortIdentifier) + } +} + +extension W3WBaseLanguage: W3WRfcLanguageConvertable { + /// convert W3WBaseLanguage to any W3WRfcLanguageProtocol + public func toRfcLanguage() -> any W3WRfcLanguageProtocol { + let parsedLocale = Locale(identifier: locale) + return W3WRfcLanguage(locale: parsedLocale) + } +} diff --git a/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage+W3WSdkLanguage.swift b/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage+W3WSdkLanguage.swift new file mode 100644 index 0000000..70c7246 --- /dev/null +++ b/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage+W3WSdkLanguage.swift @@ -0,0 +1,29 @@ +// +// File.swift +// w3w-swift-core +// +// Created by Kaley Nguyen on 21/11/25. +// + +import Foundation +#if canImport(w3w) +import w3w + +public extension W3WRfcLanguageProtocol { + /// convert W3WRfcLanguage to W3WSdkLanguage + func toW3wSdkLanguage() throws -> W3WSdkLanguage? { + guard let code, !code.isEmpty else { + return nil + } + return try W3WSdkLanguage(shortIdentifier) + } +} + +extension W3WSdkLanguage: W3WRfcLanguageConvertable { + /// convert W3WSdkLanguage to any W3WRfcLanguageProtocol + public func toRfcLanguage() -> any W3WRfcLanguageProtocol { + return W3WRfcLanguage(locale: Locale(identifier: locale)) + } +} +#endif + diff --git a/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage.md b/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage.md new file mode 100644 index 0000000..da2e337 --- /dev/null +++ b/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage.md @@ -0,0 +1,29 @@ +# W3WRfcLanguage + +A lightweight representation of an RFC 5646 language tag used in w3w-swift-core. It models: + +- language code (ISO 639; prefer 2-letter when available) +- optional script code (4 letters, Titlecase) +- optional region code (2 letters or 3 digits) + +It provides: +- A protocol `W3WRfcLanguageProtocol` that any type can conform to +- A concrete struct `W3WRfcLanguage` +- An extension that makes `Locale.Language` conform to the protocol on iOS 16+/watchOS 9+ +- Utilities to build identifiers and parse from strings + +## Availability + +- `Locale.Language` conformance requires iOS 16+ or watchOS 9+. +- `W3WRfcLanguage` itself is available on all supported platforms; when running on iOS < 16/watchOS < 9, script extraction from `Locale` may be unavailable and remain `nil`. + +## Types + +### W3WRfcLanguageProtocol +```swift +public protocol W3WRfcLanguageProtocol: Equatable { + var code: String? { get } // ISO 639 language code + var scriptCode: String? { get } // 4-letter Titlecase script code (e.g. "Latn", "Hans") + var regionCode: String? { get } // 2-letter or 3-digit region (e.g. "US", "419") +} + diff --git a/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage.swift b/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage.swift new file mode 100644 index 0000000..3a6bca6 --- /dev/null +++ b/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage.swift @@ -0,0 +1,132 @@ +// +// File.swift +// w3w-swift-core +// +// Created by Kaley Nguyen on 15/8/25. +// + +import Foundation + +/// language[-script][-region] +/// - language = ISO 639 +/// Prefer ISO 639-1 (2-letter) whenever it exists +/// Only fall back to ISO 639-2 (3-letter) if a language has no 2-letter code +/// - script (optional, 4 letters, titlecased) +/// - region (optional, 2 letters or 3-digit number) +public struct W3WRfcLanguage: W3WRfcLanguageProtocol { + public var code: String? + public var scriptCode: String? + public var regionCode: String? + + public init(locale: Locale) { + if #available(iOS 16, *), #available(watchOS 9, *) { + // from iOS 16 supports getting script + self.init(from: locale.language) + } else { + // no script for iOS < 16 + self.init( + languageCode: locale.languageCode, + script: locale.scriptCode, + region: locale.regionCode + ) + } + } + + public init( + languageCode: String? = nil, + script: String? = nil, + region: String? = nil + ) { + self.code = languageCode + self.scriptCode = script + self.regionCode = region + } + + @available(iOS 16, *) + @available(watchOS 9, *) + public init(from language: Locale.Language) { + self.code = language.code + self.scriptCode = language.scriptCode + self.regionCode = language.regionCode + } +} + +public extension W3WRfcLanguage { + /// Initialize from a string + init(from string: String) { + // Normalize separators + let normalized = string + .trimmingCharacters(in: .whitespacesAndNewlines) + .replacingOccurrences(of: "_", with: "-") + + // Handle “Base” or empty strings, check if string is a valid locale string + guard !normalized.isEmpty, normalized.lowercased() != "base", string.isValidLocale else { + self.init(languageCode: nil, script: nil, region: nil) + return + } + + // Split into parts (e.g. zh-Hans-CN → ["zh", "Hans", "CN"]) + let parts = normalized.split(separator: "-").map(String.init) + + var langCode: String? + var script: String? + var region: String? + + if parts.count > 0 { + langCode = parts[0] + } + + if parts.count > 1 { + let part = parts[1] + // Detect if this is a script code (Titlecase and 4 letters) + if part.count == 4, part.first?.isUppercase == true { + script = part + if parts.count > 2 { + region = parts[2] + } + } else { + region = part + } + } + + // Create the RFC language struct + if #available(iOS 16, *), #available(watchOS 9, *) { + self.init(from: Locale.Language(identifier: normalized)) + } else { + // no script for iOS < 16 + self.init(languageCode: langCode, script: script, region: region) + } + } +} + +public extension W3WRfcLanguageProtocol { + /// full version: code - script - region + var identifier: String { + return [code, scriptCode, regionCode] + .compactMap { $0 } + .joined(separator: "-") + } + /// short : code - region + var shortIdentifier: String { + return [code, regionCode] + .compactMap { $0 } + .joined(separator: "-") + } +} + +@available(watchOS 9, *) +@available(iOS 16, *) +extension Locale.Language : W3WRfcLanguageProtocol { + public var code: String? { + return languageCode?.identifier + } + + public var scriptCode: String? { + return self.script?.identifier + } + + public var regionCode: String? { + return region?.identifier + } +} + diff --git a/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguageProtocol.swift b/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguageProtocol.swift new file mode 100644 index 0000000..c9f9700 --- /dev/null +++ b/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguageProtocol.swift @@ -0,0 +1,19 @@ +// +// File.swift +// w3w-swift-core +// +// Created by Kaley Nguyen on 21/11/25. +// + +import Foundation + +public protocol W3WRfcLanguageProtocol: Equatable { + var code: String? { get } + var scriptCode: String? { get } + var regionCode: String? { get } +} + +/// protocol to convert any language to W3WRfcLanguage +public protocol W3WRfcLanguageConvertable { + func toRfcLanguage() -> any W3WRfcLanguageProtocol +} diff --git a/Sources/W3WSwiftCore/Types/Util/LanguageUtils.swift b/Sources/W3WSwiftCore/Types/Util/LanguageUtils.swift new file mode 100644 index 0000000..cd6be1d --- /dev/null +++ b/Sources/W3WSwiftCore/Types/Util/LanguageUtils.swift @@ -0,0 +1,35 @@ +// +// LanguageUtils.swift +// w3w-swift-core +// +// Created by Kaley Nguyen on 14/11/25. +// +import Foundation + +public class LanguageUtils { + /// gets the langauge name for a locale in the language specified by 'inLocale' + /// Parameters + /// - forLocale: locale to find the language of + /// - inLocale: locale to translate the langauge name into + public static func getLanguageName(forLocale: String, inLocale: String) -> String? { + let inLocaleObj = NSLocale(localeIdentifier: inLocale) + let forLocaleObj = NSLocale(localeIdentifier: forLocale) + + var languageName = inLocaleObj.localizedString(forLanguageCode: forLocale) ?? "" + + if forLocale.count > 2 { + if let countryCode = forLocaleObj.countryCode { + languageName += " (" + (inLocaleObj.localizedString(forCountryCode: countryCode) ?? "") + ")" + } + } + + return languageName + } +} + +extension String { + var isValidLocale: Bool { + let normalized = self.replacingOccurrences(of: "-", with: "_") + return Locale.availableIdentifiers.contains(normalized) + } +} diff --git a/Sources/W3WSwiftCore/Types/Values/W3WLanguage.swift b/Sources/W3WSwiftCore/Types/Values/W3WLanguage.swift index acbf9e5..a0cc7cc 100644 --- a/Sources/W3WSwiftCore/Types/Values/W3WLanguage.swift +++ b/Sources/W3WSwiftCore/Types/Values/W3WLanguage.swift @@ -32,12 +32,10 @@ public protocol W3WLanguage { extension W3WLanguage { - public static func getLanguageCode(from: String) -> String { return String(from.prefix(2)) } - public static func getLanguageRegion(from: String) -> String { if from.count == 5 { if String(Array(from)[2]) == "_" { @@ -51,35 +49,14 @@ extension W3WLanguage { return "" } - - /// gets the langauge name for a locale in the language specified by 'inLocale' - /// Parameters - /// - forLocale: locale to find the language of - /// - inLocale: locale to translate the langauge name into - public static func getLanguageName(forLocale: String, inLocale: String) -> String? { - let inLocaleObj = NSLocale(localeIdentifier: inLocale) - let forLocaleObj = NSLocale(localeIdentifier: forLocale) - - var langaugeName = inLocaleObj.localizedString(forLanguageCode: forLocale) ?? "" - - if forLocale.count > 2 { - if let countryCode = forLocaleObj.countryCode { - langaugeName += " (" + (inLocaleObj.localizedString(forCountryCode: countryCode) ?? "") + ")" - } - } - - return langaugeName - } - - public func getDeviceLanguages() -> [W3WLanguage] { var langauges = [W3WLanguage]() for locale_code in NSLocale.availableLocaleIdentifiers.sorted() { let language = W3WBaseLanguage( locale: locale_code, - name: Self.getLanguageName(forLocale: locale_code, inLocale: self.locale) ?? "", - nativeName: Self.getLanguageName(forLocale: locale_code, inLocale: locale_code) ?? "" + name: LanguageUtils.getLanguageName(forLocale: locale_code, inLocale: self.locale) ?? "", + nativeName: LanguageUtils.getLanguageName(forLocale: locale_code, inLocale: locale_code) ?? "" ) langauges.append(language) } @@ -133,13 +110,13 @@ public struct W3WBaseLanguage: W3WLanguage, ExpressibleByStringLiteral { /// - nativeName: Name of the langiage in that language public init(code: String, name: String = "", nativeName: String = "") { if name == "" { - self.name = Self.getLanguageName(forLocale: code, inLocale: "en") + self.name = LanguageUtils.getLanguageName(forLocale: code, inLocale: "en") } else { self.name = name } if nativeName == "" { - self.nativeName = Self.getLanguageName(forLocale: code, inLocale: code) + self.nativeName = LanguageUtils.getLanguageName(forLocale: code, inLocale: code) } else { self.nativeName = nativeName } @@ -156,13 +133,13 @@ public struct W3WBaseLanguage: W3WLanguage, ExpressibleByStringLiteral { /// - nativeName: Name of the langiage in that language public init(locale: String, name: String = "", nativeName: String = "") { if name == "" { - self.name = Self.getLanguageName(forLocale: locale, inLocale: "en") + self.name = LanguageUtils.getLanguageName(forLocale: locale, inLocale: "en") } else { self.name = name } if nativeName == "" { - self.nativeName = Self.getLanguageName(forLocale: locale, inLocale: locale) + self.nativeName = LanguageUtils.getLanguageName(forLocale: locale, inLocale: locale) } else { self.nativeName = nativeName } @@ -171,6 +148,22 @@ public struct W3WBaseLanguage: W3WLanguage, ExpressibleByStringLiteral { self.locale = locale } + public init(code: String, locale: String, name: String = "", nativeName: String = "") { + self.code = String(code.prefix(2)) + self.locale = String(locale.prefix(2)) + if name == "" { + self.name = LanguageUtils.getLanguageName(forLocale: locale, inLocale: "en") + } else { + self.name = name + } + + if nativeName == "" { + self.nativeName = LanguageUtils.getLanguageName(forLocale: locale, inLocale: locale) + } else { + self.nativeName = nativeName + } + } + public init(stringLiteral: String) { self.init(locale: stringLiteral) diff --git a/Tests/languages-Tests/W3wRfcLanguage_w3wLanguage_tests.swift b/Tests/languages-Tests/W3wRfcLanguage_w3wLanguage_tests.swift new file mode 100644 index 0000000..bb593f2 --- /dev/null +++ b/Tests/languages-Tests/W3wRfcLanguage_w3wLanguage_tests.swift @@ -0,0 +1,198 @@ +// +// w3wRfcLanguage_w3wLanguage_tests.swift +// w3w-swift-core +// +// Created by Kaley Nguyen on 21/11/25. +// + +import XCTest +@testable import W3WSwiftCore +import w3w + +final class W3wRfcLanguage_w3wLanguage_tests: XCTestCase { + + func testConvertLanguageToRfcLanguage_LanguageOnly_Success() { + let list = [ + "fr", "en", "ja", "ko", "pt", "de" + ] + for lang in list { + let testLang = W3WBaseLanguage(locale: lang) + let rfcLang = testLang.toRfcLanguage() + XCTAssertNil(rfcLang.regionCode) + XCTAssertEqual(rfcLang.code, testLang.code) + + let rfcLang2 = W3WRfcLanguage(from: lang) + XCTAssertEqual(rfcLang2.code, rfcLang.code) + XCTAssertEqual(rfcLang2.regionCode, rfcLang.regionCode) + XCTAssertEqual(rfcLang2.scriptCode, rfcLang.scriptCode) + } + } + + func testConvertLanguageToRfcLangage_LanguageRegion_Success() { + let list = [ + "en-US", "en-GB", "fr-CA", "pt-BR", "pt-PT", + "es-MX", "es-ES", "vi-VN", "ja-JP", "ko-KR", + ] + for lang in list { + let testLang = W3WBaseLanguage(locale: lang) + let rfcLang = testLang.toRfcLanguage() + XCTAssertNotNil(rfcLang.regionCode) + XCTAssertEqual(rfcLang.code, testLang.code) + + let rfcLang2 = W3WRfcLanguage(from: lang) + XCTAssertEqual(rfcLang2.code, rfcLang.code) + XCTAssertEqual(rfcLang2.regionCode, rfcLang.regionCode) + XCTAssertEqual(rfcLang2.scriptCode, rfcLang.scriptCode) + } + } + + func testConvertLanguageToRfcLangage_LanguageScript_Success() { + let list = [ + "zh-Hans", "zh-Hant", "sr-Cyrl", "sr-Latn", + "kk-Cyrl", "kk-Cyrl", "uz-Cyrl", "uz-Latn", + ] + for lang in list { + let testLang = W3WBaseLanguage(locale: lang) + let rfcLang = testLang.toRfcLanguage() + XCTAssertNil(rfcLang.regionCode) + if #available(iOS 16, *) { + XCTAssertNotNil(rfcLang.scriptCode) + } + + let rfcLang2 = W3WRfcLanguage(from: lang) + XCTAssertEqual(rfcLang2.code, rfcLang.code) + XCTAssertEqual(rfcLang2.regionCode, rfcLang.regionCode) + if #available(iOS 16, *) { + XCTAssertEqual(rfcLang2.scriptCode, rfcLang.scriptCode) + } + } + } + + func testConvertLanguageToRfcLangage_LanguageScriptRegion_Success() { + let list = [ + "zh-Hans-CN", "zh-Hant-TW", "sr-Cyrl-RS", "sr-Latn-BA", + "kk-Cyrl-KZ", "uz-Latn-UZ", + ] + for lang in list { + let testLang = W3WBaseLanguage(locale: lang) + let rfcLang = testLang.toRfcLanguage() + XCTAssertNotNil(rfcLang.regionCode) + if #available(iOS 16, *) { + XCTAssertNotNil(rfcLang.scriptCode) + } + XCTAssertNotNil(rfcLang.code) + XCTAssertEqual(rfcLang.code, testLang.code) + + let rfcLang2 = W3WRfcLanguage(from: lang) + XCTAssertEqual(rfcLang2.code, rfcLang.code) + XCTAssertEqual(rfcLang2.regionCode, rfcLang.regionCode) + if #available(iOS 16, *) { + XCTAssertEqual(rfcLang2.scriptCode, rfcLang.scriptCode) + } + } + } + + func testConvertLanguageToRfcLangage_CurrentLanguageList_Success() { + let list: [String] = [ + "vi", + "af", + "id", + "ms", + "de", + "en-AU", + "en-GB", + "en-US", + "en-IN", + "es-ES", + "es-MX", + "sw", + "nl", + "pt-BR", + "pt-PT", + "ro", + "fi", + "sv", + "xh", + "zu", + "bg", + "mn", + "ru", + "kk", + "ko", + "ja", + "ar" + ] + + for lang in list { + let rfcLang = W3WRfcLanguage(from: lang) + XCTAssertNotNil(rfcLang.code) + if #available(iOS 16, *) { + XCTAssertNotNil(rfcLang.scriptCode) + } + if lang.count > 3 { + XCTAssertNotNil(rfcLang.regionCode) + } + } + } + + func testConvertLanguageToRfcLangage_UnderscoreFormats_AppleLegacy_Success() { + let list = [ + "en_US", "fr_FR", "pt_BR", "zh_CN", "zh_TW", + "sr_Cyrl_RS", + ] + for lang in list { + let rfcLang = W3WRfcLanguage(from: lang) + let w3wLang = rfcLang.toW3wLanguage() + XCTAssertEqual(w3wLang?.code, rfcLang.code) + } + } + + func testConvertLanguageToRfcLanguage_3CharactersCode_Success() { + let list = [ + "haw-US", // Hawaiian + "yue-Hant", // Cantonese + "gsw-CH", // Swiss German ] + ] + for lang in list { + let rfcLang = W3WRfcLanguage(from: lang) + XCTAssertEqual(rfcLang.code?.count, 3) + } + } + + func testConvertLanguageToRfcLanguage_InvalidLanguage_Failure() { + let list = [ + // invalid code + "xx", + "xxx", + "123", + // invalid script + "en-ABCDE", + "sr-OOO", + "hr-12ab", + // invalid region + "en-USS", + "fr-1234", + "es-1X", + "zh-Hans-CNN", + // wrong order + "CN-zh", + "Cyrl-sr", + "US-en", + // too many components + "en-US-extra", + "sr-Cyrl-RS-foo", + // empty component + "-en", + "en--US", + "en-", + "en- -US" + ] + for lang in list { + let rfcLang = W3WRfcLanguage(from: lang) + XCTAssertNil(rfcLang.code) + XCTAssertNil(rfcLang.scriptCode) + XCTAssertNil(rfcLang.regionCode) + } + } + +} diff --git a/Tests/languages-Tests/W3wRfcLanguage_w3wSdkLanguage_tests.swift b/Tests/languages-Tests/W3wRfcLanguage_w3wSdkLanguage_tests.swift new file mode 100644 index 0000000..a90261c --- /dev/null +++ b/Tests/languages-Tests/W3wRfcLanguage_w3wSdkLanguage_tests.swift @@ -0,0 +1,79 @@ +// +// w3wRfcLanguage_w3wSdkLanguage_tests.swift +// w3w-swift-core +// +// Created by Kaley Nguyen on 21/11/25. +// + +import XCTest +@testable import W3WSwiftCore +import w3w + +final class W3wRfcLanguage_w3wSdkLanguage_tests: XCTestCase { + func testConvertSdkLanguageToRfcLanguage_Success() throws { + //1 + let sdkLanguage = try W3WSdkLanguage("fr_fr") + let convertedRfcLanguage = sdkLanguage.toRfcLanguage() as! W3WRfcLanguage + let rfcLanguage = W3WRfcLanguage(from: "fr_FR") + + XCTAssertTrue(convertedRfcLanguage == rfcLanguage ) + XCTAssertEqual(convertedRfcLanguage.code, "fr") + XCTAssertEqual(convertedRfcLanguage.regionCode, "FR") + if #available(iOS 16, *) { + XCTAssertNotNil(convertedRfcLanguage.scriptCode) + } + } + + func testConvertSdkLanguageToRfcLanguage_CodeOnly_Success() throws { + //1 + let sdkLanguage = try W3WSdkLanguage("ja") + let convertedRfcLanguage = sdkLanguage.toRfcLanguage() as! W3WRfcLanguage + let rfcLanguage = W3WRfcLanguage(from: "ja") + + XCTAssertTrue(convertedRfcLanguage == rfcLanguage ) + XCTAssertEqual(convertedRfcLanguage.code, "ja") + XCTAssertNil(convertedRfcLanguage.regionCode) + if #available(iOS 16, *) { + XCTAssertNotNil(convertedRfcLanguage.scriptCode) + } + } + + func testConvertRfcLanguageToSdkLanguage_Success() throws { + let rfcLanguage = W3WRfcLanguage(from: "zh-Hans") + let sdkLanguage = try rfcLanguage.toW3wSdkLanguage() + + XCTAssertNotNil(sdkLanguage) + XCTAssertEqual(sdkLanguage?.code, "zh") + } + + func testConvertRfcLanguageToSdkLanguage_Code3Characters_Failure() throws { + let rfcLanguage = W3WRfcLanguage(from: "yue-Hans") + + XCTAssertThrowsError(try rfcLanguage.toW3wSdkLanguage()) { error in + XCTAssertNotNil(error) + } + } + + func testConvertSdkLanguageToRfcLanguage_CodeScriptRegion_failure() throws { + let rfcLanguage = W3WRfcLanguage(from: "kk_Cyrl_KZ") + + XCTAssertThrowsError(try rfcLanguage.toW3wSdkLanguage()) { error in + XCTAssertNotNil(error) + } + } + + // comparing 2 languages (W3WBaseLanguage & W3WSdkLanguage) by converting them to W3WRfcLanguage + func testMatchingRfcLanguage() throws { + let string = "sv-SE" + let sdkString = "sv_se" + let w3wBaseLanguage = W3WBaseLanguage(locale: string) + let w3wSdkLanguage = try W3WSdkLanguage(sdkString) + + let firstLang = w3wBaseLanguage.toRfcLanguage() as! W3WRfcLanguage + let secondLang = w3wSdkLanguage.toRfcLanguage() as! W3WRfcLanguage + XCTAssertTrue(firstLang == secondLang) + XCTAssertEqual(firstLang.code, secondLang.code) + XCTAssertEqual(firstLang.regionCode, secondLang.regionCode) + } + +} From 2c5aa7ed34eda3e81675540acb66da7a40990ae7 Mon Sep 17 00:00:00 2001 From: Kaley Nguyen Date: Mon, 24 Nov 2025 15:32:56 +0700 Subject: [PATCH 2/9] update --- Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage.swift b/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage.swift index 3a6bca6..7d17c23 100644 --- a/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage.swift +++ b/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage.swift @@ -60,7 +60,7 @@ public extension W3WRfcLanguage { .replacingOccurrences(of: "_", with: "-") // Handle “Base” or empty strings, check if string is a valid locale string - guard !normalized.isEmpty, normalized.lowercased() != "base", string.isValidLocale else { + guard !normalized.isEmpty, string.isValidLocale else { self.init(languageCode: nil, script: nil, region: nil) return } From cc9f5ced4f4021e0c261d334b84afeee7632fff5 Mon Sep 17 00:00:00 2001 From: Kaley Nguyen Date: Tue, 25 Nov 2025 18:54:57 +0700 Subject: [PATCH 3/9] update codes --- .../W3WRfcLanguage+W3WLanguage.swift | 5 +- .../W3WRfcLanguage+W3WSdkLanguage.swift | 4 +- .../RfcLanguage/W3WRfcLanguage.swift | 50 +++--- .../RfcLanguage/W3WRfcLanguageProtocol.swift | 5 +- .../W3wRfcLanguage_w3wLanguage_tests.swift | 156 +++++++++--------- .../W3wRfcLanguage_w3wSdkLanguage_tests.swift | 74 +++++---- 6 files changed, 153 insertions(+), 141 deletions(-) diff --git a/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage+W3WLanguage.swift b/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage+W3WLanguage.swift index c5aab5c..7b379e8 100644 --- a/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage+W3WLanguage.swift +++ b/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage+W3WLanguage.swift @@ -17,10 +17,13 @@ public extension W3WRfcLanguageProtocol { } } +@available(iOS 13.0.0, *) +@available(watchOS 6.0.0, *) extension W3WBaseLanguage: W3WRfcLanguageConvertable { /// convert W3WBaseLanguage to any W3WRfcLanguageProtocol - public func toRfcLanguage() -> any W3WRfcLanguageProtocol { + public func toRfcLanguage() -> some W3WRfcLanguageProtocol { let parsedLocale = Locale(identifier: locale) return W3WRfcLanguage(locale: parsedLocale) } } + diff --git a/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage+W3WSdkLanguage.swift b/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage+W3WSdkLanguage.swift index 70c7246..f1937ac 100644 --- a/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage+W3WSdkLanguage.swift +++ b/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage+W3WSdkLanguage.swift @@ -19,9 +19,11 @@ public extension W3WRfcLanguageProtocol { } } +@available(iOS 13.0.0, *) +@available(watchOS 6.0.0, *) extension W3WSdkLanguage: W3WRfcLanguageConvertable { /// convert W3WSdkLanguage to any W3WRfcLanguageProtocol - public func toRfcLanguage() -> any W3WRfcLanguageProtocol { + public func toRfcLanguage() -> some W3WRfcLanguageProtocol { return W3WRfcLanguage(locale: Locale(identifier: locale)) } } diff --git a/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage.swift b/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage.swift index 7d17c23..9ffd8a2 100644 --- a/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage.swift +++ b/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage.swift @@ -65,35 +65,33 @@ public extension W3WRfcLanguage { return } - // Split into parts (e.g. zh-Hans-CN → ["zh", "Hans", "CN"]) - let parts = normalized.split(separator: "-").map(String.init) - - var langCode: String? - var script: String? - var region: String? - - if parts.count > 0 { - langCode = parts[0] - } - - if parts.count > 1 { - let part = parts[1] - // Detect if this is a script code (Titlecase and 4 letters) - if part.count == 4, part.first?.isUppercase == true { - script = part - if parts.count > 2 { - region = parts[2] - } - } else { - region = part - } - } - - // Create the RFC language struct if #available(iOS 16, *), #available(watchOS 9, *) { self.init(from: Locale.Language(identifier: normalized)) } else { - // no script for iOS < 16 + // Split into parts (e.g. zh-Hans-CN → ["zh", "Hans", "CN"]) + let parts = normalized.split(separator: "-").map(String.init) + + var langCode: String? + var script: String? + var region: String? + + if parts.count > 0 { + langCode = parts[0] + } + + if parts.count > 1 { + let part = parts[1] + // Detect if this is a script code (Titlecase and 4 letters) + if part.count == 4, part.first?.isUppercase == true { + script = part + if parts.count > 2 { + region = parts[2] + } + } else { + region = part + } + } + self.init(languageCode: langCode, script: script, region: region) } } diff --git a/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguageProtocol.swift b/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguageProtocol.swift index c9f9700..41ea5bb 100644 --- a/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguageProtocol.swift +++ b/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguageProtocol.swift @@ -14,6 +14,7 @@ public protocol W3WRfcLanguageProtocol: Equatable { } /// protocol to convert any language to W3WRfcLanguage -public protocol W3WRfcLanguageConvertable { - func toRfcLanguage() -> any W3WRfcLanguageProtocol +public protocol W3WRfcLanguageConvertable { + associatedtype Language: W3WRfcLanguageProtocol + func toRfcLanguage() -> Language } diff --git a/Tests/languages-Tests/W3wRfcLanguage_w3wLanguage_tests.swift b/Tests/languages-Tests/W3wRfcLanguage_w3wLanguage_tests.swift index bb593f2..8143545 100644 --- a/Tests/languages-Tests/W3wRfcLanguage_w3wLanguage_tests.swift +++ b/Tests/languages-Tests/W3wRfcLanguage_w3wLanguage_tests.swift @@ -1,165 +1,162 @@ // -// w3wRfcLanguage_w3wLanguage_tests.swift +// W3wRfcLanguage_w3wLanguage_tests.swift // w3w-swift-core // // Created by Kaley Nguyen on 21/11/25. // -import XCTest +import Testing @testable import W3WSwiftCore import w3w -final class W3wRfcLanguage_w3wLanguage_tests: XCTestCase { +@Suite +struct W3wRfcLanguage_W3wLanguage_Tests { - func testConvertLanguageToRfcLanguage_LanguageOnly_Success() { - let list = [ - "fr", "en", "ja", "ko", "pt", "de" - ] + @Test + func convertLanguageToRfcLanguage_LanguageOnly_Success() { + let list = ["fr", "en", "ja", "ko", "pt", "de"] + for lang in list { let testLang = W3WBaseLanguage(locale: lang) let rfcLang = testLang.toRfcLanguage() - XCTAssertNil(rfcLang.regionCode) - XCTAssertEqual(rfcLang.code, testLang.code) + + #expect(rfcLang.regionCode == nil) + #expect(rfcLang.code == testLang.code) let rfcLang2 = W3WRfcLanguage(from: lang) - XCTAssertEqual(rfcLang2.code, rfcLang.code) - XCTAssertEqual(rfcLang2.regionCode, rfcLang.regionCode) - XCTAssertEqual(rfcLang2.scriptCode, rfcLang.scriptCode) + #expect(rfcLang2.code == rfcLang.code) + #expect(rfcLang2.regionCode == rfcLang.regionCode) + #expect(rfcLang2.scriptCode == rfcLang.scriptCode) } } - func testConvertLanguageToRfcLangage_LanguageRegion_Success() { + @Test + func convertLanguageToRfcLangage_LanguageRegion_Success() { let list = [ "en-US", "en-GB", "fr-CA", "pt-BR", "pt-PT", - "es-MX", "es-ES", "vi-VN", "ja-JP", "ko-KR", + "es-MX", "es-ES", "vi-VN", "ja-JP", "ko-KR" ] + for lang in list { let testLang = W3WBaseLanguage(locale: lang) let rfcLang = testLang.toRfcLanguage() - XCTAssertNotNil(rfcLang.regionCode) - XCTAssertEqual(rfcLang.code, testLang.code) + + #expect(rfcLang.regionCode != nil) + #expect(rfcLang.code == testLang.code) let rfcLang2 = W3WRfcLanguage(from: lang) - XCTAssertEqual(rfcLang2.code, rfcLang.code) - XCTAssertEqual(rfcLang2.regionCode, rfcLang.regionCode) - XCTAssertEqual(rfcLang2.scriptCode, rfcLang.scriptCode) + #expect(rfcLang2.code == rfcLang.code) + #expect(rfcLang2.regionCode == rfcLang.regionCode) + #expect(rfcLang2.scriptCode == rfcLang.scriptCode) } } - func testConvertLanguageToRfcLangage_LanguageScript_Success() { + @Test + func convertLanguageToRfcLangage_LanguageScript_Success() { let list = [ "zh-Hans", "zh-Hant", "sr-Cyrl", "sr-Latn", - "kk-Cyrl", "kk-Cyrl", "uz-Cyrl", "uz-Latn", + "uz-Cyrl", "uz-Latn" ] + for lang in list { let testLang = W3WBaseLanguage(locale: lang) let rfcLang = testLang.toRfcLanguage() - XCTAssertNil(rfcLang.regionCode) + + #expect(rfcLang.regionCode == nil) if #available(iOS 16, *) { - XCTAssertNotNil(rfcLang.scriptCode) + #expect(rfcLang.scriptCode != nil) } let rfcLang2 = W3WRfcLanguage(from: lang) - XCTAssertEqual(rfcLang2.code, rfcLang.code) - XCTAssertEqual(rfcLang2.regionCode, rfcLang.regionCode) + #expect(rfcLang2.code == rfcLang.code) + #expect(rfcLang2.regionCode == rfcLang.regionCode) if #available(iOS 16, *) { - XCTAssertEqual(rfcLang2.scriptCode, rfcLang.scriptCode) + #expect(rfcLang2.scriptCode == rfcLang.scriptCode) } } } - func testConvertLanguageToRfcLangage_LanguageScriptRegion_Success() { + @Test + func convertLanguageToRfcLangage_LanguageScriptRegion_Success() { let list = [ "zh-Hans-CN", "zh-Hant-TW", "sr-Cyrl-RS", "sr-Latn-BA", - "kk-Cyrl-KZ", "uz-Latn-UZ", + "uz-Latn-UZ" ] + for lang in list { let testLang = W3WBaseLanguage(locale: lang) let rfcLang = testLang.toRfcLanguage() - XCTAssertNotNil(rfcLang.regionCode) + + #expect(rfcLang.regionCode != nil) if #available(iOS 16, *) { - XCTAssertNotNil(rfcLang.scriptCode) + #expect(rfcLang.scriptCode != nil) } - XCTAssertNotNil(rfcLang.code) - XCTAssertEqual(rfcLang.code, testLang.code) + #expect(rfcLang.code == testLang.code) let rfcLang2 = W3WRfcLanguage(from: lang) - XCTAssertEqual(rfcLang2.code, rfcLang.code) - XCTAssertEqual(rfcLang2.regionCode, rfcLang.regionCode) + #expect(rfcLang2.code == rfcLang.code) + #expect(rfcLang2.regionCode == rfcLang.regionCode) if #available(iOS 16, *) { - XCTAssertEqual(rfcLang2.scriptCode, rfcLang.scriptCode) + #expect(rfcLang2.scriptCode == rfcLang.scriptCode) } } } - func testConvertLanguageToRfcLangage_CurrentLanguageList_Success() { + @Test + func convertLanguageToRfcLangage_CurrentLanguageList_Success() { let list: [String] = [ - "vi", - "af", - "id", - "ms", - "de", - "en-AU", - "en-GB", - "en-US", - "en-IN", - "es-ES", - "es-MX", - "sw", - "nl", - "pt-BR", - "pt-PT", - "ro", - "fi", - "sv", - "xh", - "zu", - "bg", - "mn", - "ru", - "kk", - "ko", - "ja", - "ar" + "vi", "af", "id", "ms", "de", + "en-AU", "en-GB", "en-US", "en-IN", + "es-ES", "es-MX", "sw", "nl", + "pt-BR", "pt-PT", "ro", "fi", + "sv", "xh", "zu", "bg", "mn", + "ru", "kk", "ko", "ja", "ar" ] for lang in list { let rfcLang = W3WRfcLanguage(from: lang) - XCTAssertNotNil(rfcLang.code) + + #expect(rfcLang.code != nil) if #available(iOS 16, *) { - XCTAssertNotNil(rfcLang.scriptCode) + #expect(rfcLang.scriptCode != nil) } if lang.count > 3 { - XCTAssertNotNil(rfcLang.regionCode) + #expect(rfcLang.regionCode != nil) } } } - func testConvertLanguageToRfcLangage_UnderscoreFormats_AppleLegacy_Success() { + @Test + func convertLanguageToRfcLangage_UnderscoreFormats_AppleLegacy_Success() { let list = [ "en_US", "fr_FR", "pt_BR", "zh_CN", "zh_TW", - "sr_Cyrl_RS", + "sr_Cyrl_RS" ] + for lang in list { let rfcLang = W3WRfcLanguage(from: lang) let w3wLang = rfcLang.toW3wLanguage() - XCTAssertEqual(w3wLang?.code, rfcLang.code) + + #expect(w3wLang?.code == rfcLang.code) } } - func testConvertLanguageToRfcLanguage_3CharactersCode_Success() { + @Test + func convertLanguageToRfcLanguage_3CharactersCode_Success() { let list = [ - "haw-US", // Hawaiian - "yue-Hant", // Cantonese - "gsw-CH", // Swiss German ] + "haw-US", + "yue-Hant", + "gsw-CH" ] + for lang in list { let rfcLang = W3WRfcLanguage(from: lang) - XCTAssertEqual(rfcLang.code?.count, 3) + #expect(rfcLang.code?.count == 3) } } - func testConvertLanguageToRfcLanguage_InvalidLanguage_Failure() { + @Test + func convertLanguageToRfcLanguage_InvalidLanguage_Failure() { let list = [ // invalid code "xx", @@ -186,13 +183,14 @@ final class W3wRfcLanguage_w3wLanguage_tests: XCTestCase { "en--US", "en-", "en- -US" + ] + for lang in list { let rfcLang = W3WRfcLanguage(from: lang) - XCTAssertNil(rfcLang.code) - XCTAssertNil(rfcLang.scriptCode) - XCTAssertNil(rfcLang.regionCode) + #expect(rfcLang.code == nil) + #expect(rfcLang.scriptCode == nil) + #expect(rfcLang.regionCode == nil) } } - } diff --git a/Tests/languages-Tests/W3wRfcLanguage_w3wSdkLanguage_tests.swift b/Tests/languages-Tests/W3wRfcLanguage_w3wSdkLanguage_tests.swift index a90261c..c8aa679 100644 --- a/Tests/languages-Tests/W3wRfcLanguage_w3wSdkLanguage_tests.swift +++ b/Tests/languages-Tests/W3wRfcLanguage_w3wSdkLanguage_tests.swift @@ -5,75 +5,85 @@ // Created by Kaley Nguyen on 21/11/25. // -import XCTest +import Testing @testable import W3WSwiftCore import w3w -final class W3wRfcLanguage_w3wSdkLanguage_tests: XCTestCase { - func testConvertSdkLanguageToRfcLanguage_Success() throws { - //1 +@Suite +struct W3wRfcLanguage_W3wSdkLanguage_Tests { + + @Test + func convertSdkLanguageToRfcLanguage_Success() throws { let sdkLanguage = try W3WSdkLanguage("fr_fr") let convertedRfcLanguage = sdkLanguage.toRfcLanguage() as! W3WRfcLanguage let rfcLanguage = W3WRfcLanguage(from: "fr_FR") - XCTAssertTrue(convertedRfcLanguage == rfcLanguage ) - XCTAssertEqual(convertedRfcLanguage.code, "fr") - XCTAssertEqual(convertedRfcLanguage.regionCode, "FR") + #expect(convertedRfcLanguage == rfcLanguage) + #expect(convertedRfcLanguage.code == "fr") + #expect(convertedRfcLanguage.regionCode == "FR") + if #available(iOS 16, *) { - XCTAssertNotNil(convertedRfcLanguage.scriptCode) + #expect(convertedRfcLanguage.scriptCode != nil) } } - func testConvertSdkLanguageToRfcLanguage_CodeOnly_Success() throws { - //1 + @Test + func convertSdkLanguageToRfcLanguage_CodeOnly_Success() throws { let sdkLanguage = try W3WSdkLanguage("ja") let convertedRfcLanguage = sdkLanguage.toRfcLanguage() as! W3WRfcLanguage let rfcLanguage = W3WRfcLanguage(from: "ja") - XCTAssertTrue(convertedRfcLanguage == rfcLanguage ) - XCTAssertEqual(convertedRfcLanguage.code, "ja") - XCTAssertNil(convertedRfcLanguage.regionCode) + #expect(convertedRfcLanguage == rfcLanguage) + #expect(convertedRfcLanguage.code == "ja") + #expect(convertedRfcLanguage.regionCode == nil) + if #available(iOS 16, *) { - XCTAssertNotNil(convertedRfcLanguage.scriptCode) + #expect(convertedRfcLanguage.scriptCode != nil) } } - func testConvertRfcLanguageToSdkLanguage_Success() throws { + @Test + func convertRfcLanguageToSdkLanguage_Success() throws { let rfcLanguage = W3WRfcLanguage(from: "zh-Hans") let sdkLanguage = try rfcLanguage.toW3wSdkLanguage() - XCTAssertNotNil(sdkLanguage) - XCTAssertEqual(sdkLanguage?.code, "zh") + #expect(sdkLanguage != nil) + #expect(sdkLanguage?.code == "zh") } - func testConvertRfcLanguageToSdkLanguage_Code3Characters_Failure() throws { + @Test + func convertRfcLanguageToSdkLanguage_Code3Characters_Failure() { let rfcLanguage = W3WRfcLanguage(from: "yue-Hans") - - XCTAssertThrowsError(try rfcLanguage.toW3wSdkLanguage()) { error in - XCTAssertNotNil(error) + + #expect(throws: Error.self) { + _ = try rfcLanguage.toW3wSdkLanguage() } } - func testConvertSdkLanguageToRfcLanguage_CodeScriptRegion_failure() throws { - let rfcLanguage = W3WRfcLanguage(from: "kk_Cyrl_KZ") - - XCTAssertThrowsError(try rfcLanguage.toW3wSdkLanguage()) { error in - XCTAssertNotNil(error) + @Test + func convertSdkLanguageToRfcLanguage_CodeScriptRegion_Failure() { + let rfcLanguage = W3WRfcLanguage(from: "zh-Hans-CN") + + #expect(throws: Error.self) { + _ = try rfcLanguage.toW3wSdkLanguage() } } - // comparing 2 languages (W3WBaseLanguage & W3WSdkLanguage) by converting them to W3WRfcLanguage - func testMatchingRfcLanguage() throws { + /// Compare W3WBaseLanguage & W3WSdkLanguage by converting to W3WRfcLanguage + @Test + func matchingRfcLanguage() throws { let string = "sv-SE" let sdkString = "sv_se" + let w3wBaseLanguage = W3WBaseLanguage(locale: string) let w3wSdkLanguage = try W3WSdkLanguage(sdkString) let firstLang = w3wBaseLanguage.toRfcLanguage() as! W3WRfcLanguage let secondLang = w3wSdkLanguage.toRfcLanguage() as! W3WRfcLanguage - XCTAssertTrue(firstLang == secondLang) - XCTAssertEqual(firstLang.code, secondLang.code) - XCTAssertEqual(firstLang.regionCode, secondLang.regionCode) + + #expect(firstLang == secondLang) + #expect(firstLang.code == secondLang.code) + #expect(firstLang.regionCode == secondLang.regionCode) } - } + From ae6336bee77f408874b97c878295d56beb6b9960 Mon Sep 17 00:00:00 2001 From: Kaley Nguyen Date: Tue, 25 Nov 2025 19:15:43 +0700 Subject: [PATCH 4/9] update logic inside W3WLanguage --- .../Types/Values/W3WLanguage.swift | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/Sources/W3WSwiftCore/Types/Values/W3WLanguage.swift b/Sources/W3WSwiftCore/Types/Values/W3WLanguage.swift index a0cc7cc..cbd2b4d 100644 --- a/Sources/W3WSwiftCore/Types/Values/W3WLanguage.swift +++ b/Sources/W3WSwiftCore/Types/Values/W3WLanguage.swift @@ -32,8 +32,17 @@ public protocol W3WLanguage { extension W3WLanguage { + /// To get the first 2 (or 3 max) in a locale string separated by "-", i.e: en-US / en + static func extractCode(from string: String) -> String { + let parts = string.split(separator: "-").map(String.init) + if !parts.isEmpty { + return parts[0] + } + return "" + } + public static func getLanguageCode(from: String) -> String { - return String(from.prefix(2)) + return extractCode(from: from) } public static func getLanguageRegion(from: String) -> String { @@ -121,8 +130,8 @@ public struct W3WBaseLanguage: W3WLanguage, ExpressibleByStringLiteral { self.nativeName = nativeName } - self.code = String(code.prefix(2)) - self.locale = String(code.prefix(2)) + self.code = Self.extractCode(from: code) + self.locale = Self.extractCode(from: code) } @@ -144,13 +153,13 @@ public struct W3WBaseLanguage: W3WLanguage, ExpressibleByStringLiteral { self.nativeName = nativeName } - self.code = String(locale.prefix(2)) + self.code = Self.extractCode(from: locale) self.locale = locale } public init(code: String, locale: String, name: String = "", nativeName: String = "") { - self.code = String(code.prefix(2)) - self.locale = String(locale.prefix(2)) + self.code = Self.extractCode(from: code) + self.locale = Self.extractCode(from: locale) if name == "" { self.name = LanguageUtils.getLanguageName(forLocale: locale, inLocale: "en") } else { @@ -169,6 +178,5 @@ public struct W3WBaseLanguage: W3WLanguage, ExpressibleByStringLiteral { self.init(locale: stringLiteral) } - static public let english = W3WBaseLanguage(locale: "en_gb") } From 5be3883b3d88690a6490a0b659a8b8994b5642b3 Mon Sep 17 00:00:00 2001 From: Kaley Nguyen Date: Thu, 27 Nov 2025 17:46:04 +0700 Subject: [PATCH 5/9] update nativeName and name --- .../RfcLanguage/W3WRfcLanguageProtocol.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguageProtocol.swift b/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguageProtocol.swift index 41ea5bb..4271dca 100644 --- a/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguageProtocol.swift +++ b/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguageProtocol.swift @@ -13,6 +13,16 @@ public protocol W3WRfcLanguageProtocol: Equatable { var regionCode: String? { get } } +extension W3WRfcLanguageProtocol { + public var nativeName: String? { + return LanguageUtils.getLanguageName(forLocale: identifier, inLocale: identifier) + } + + public var name: String? { + return LanguageUtils.getLanguageName(forLocale: identifier, inLocale: "en") + } +} + /// protocol to convert any language to W3WRfcLanguage public protocol W3WRfcLanguageConvertable { associatedtype Language: W3WRfcLanguageProtocol From 52713ee1c95711564f4621d7fced01bc7bb7ae01 Mon Sep 17 00:00:00 2001 From: Kaley Nguyen Date: Fri, 28 Nov 2025 18:12:20 +0700 Subject: [PATCH 6/9] minor --- Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage.swift | 9 +++++++++ .../RfcLanguage/W3WRfcLanguageProtocol.swift | 10 ---------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage.swift b/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage.swift index 9ffd8a2..da62b32 100644 --- a/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage.swift +++ b/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage.swift @@ -128,3 +128,12 @@ extension Locale.Language : W3WRfcLanguageProtocol { } } +extension W3WRfcLanguage { + public var nativeName: String? { + return LanguageUtils.getLanguageName(forLocale: identifier, inLocale: identifier) + } + + public var name: String? { + return LanguageUtils.getLanguageName(forLocale: identifier, inLocale: "en") + } +} diff --git a/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguageProtocol.swift b/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguageProtocol.swift index 4271dca..41ea5bb 100644 --- a/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguageProtocol.swift +++ b/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguageProtocol.swift @@ -13,16 +13,6 @@ public protocol W3WRfcLanguageProtocol: Equatable { var regionCode: String? { get } } -extension W3WRfcLanguageProtocol { - public var nativeName: String? { - return LanguageUtils.getLanguageName(forLocale: identifier, inLocale: identifier) - } - - public var name: String? { - return LanguageUtils.getLanguageName(forLocale: identifier, inLocale: "en") - } -} - /// protocol to convert any language to W3WRfcLanguage public protocol W3WRfcLanguageConvertable { associatedtype Language: W3WRfcLanguageProtocol From ce14e0df58afabfc43c8a698e3d8ad6f90682ec0 Mon Sep 17 00:00:00 2001 From: Kaley Nguyen Date: Mon, 1 Dec 2025 13:06:22 +0700 Subject: [PATCH 7/9] update tests --- .../W3wRfcLanguage_w3wLanguage_tests.swift | 263 +++++++----------- .../W3wRfcLanguage_w3wSdkLanguage_tests.swift | 8 +- 2 files changed, 110 insertions(+), 161 deletions(-) diff --git a/Tests/languages-Tests/W3wRfcLanguage_w3wLanguage_tests.swift b/Tests/languages-Tests/W3wRfcLanguage_w3wLanguage_tests.swift index 8143545..62c4c6d 100644 --- a/Tests/languages-Tests/W3wRfcLanguage_w3wLanguage_tests.swift +++ b/Tests/languages-Tests/W3wRfcLanguage_w3wLanguage_tests.swift @@ -12,185 +12,134 @@ import w3w @Suite struct W3wRfcLanguage_W3wLanguage_Tests { - @Test - func convertLanguageToRfcLanguage_LanguageOnly_Success() { - let list = ["fr", "en", "ja", "ko", "pt", "de"] + @Test(arguments: ["fr", "en", "ja", "ko", "pt", "de"]) + func convertLanguageToRfcLanguage_LanguageOnly_Success(lang: String) { + let testLang = W3WBaseLanguage(locale: lang) + let rfcLang = testLang.toRfcLanguage() - for lang in list { - let testLang = W3WBaseLanguage(locale: lang) - let rfcLang = testLang.toRfcLanguage() - - #expect(rfcLang.regionCode == nil) - #expect(rfcLang.code == testLang.code) - - let rfcLang2 = W3WRfcLanguage(from: lang) - #expect(rfcLang2.code == rfcLang.code) - #expect(rfcLang2.regionCode == rfcLang.regionCode) - #expect(rfcLang2.scriptCode == rfcLang.scriptCode) - } + #expect(rfcLang.regionCode == nil) + #expect(rfcLang.code == testLang.code) + + let rfcLang2 = W3WRfcLanguage(from: lang) + #expect(rfcLang2.code == rfcLang.code) + #expect(rfcLang2.regionCode == rfcLang.regionCode) + #expect(rfcLang2.scriptCode == rfcLang.scriptCode) } - @Test - func convertLanguageToRfcLangage_LanguageRegion_Success() { - let list = [ - "en-US", "en-GB", "fr-CA", "pt-BR", "pt-PT", - "es-MX", "es-ES", "vi-VN", "ja-JP", "ko-KR" - ] + @Test(arguments: [ + "en-US", "en-GB", "fr-CA", "pt-BR", "pt-PT", + "es-MX", "es-ES", "vi-VN", "ja-JP", "ko-KR" + ]) + func convertLanguageToRfcLangage_LanguageRegion_Success(lang: String) { + let testLang = W3WBaseLanguage(locale: lang) + let rfcLang = testLang.toRfcLanguage() + + #expect(rfcLang.regionCode != nil) + #expect(rfcLang.code == testLang.code) + + let rfcLang2 = W3WRfcLanguage(from: lang) + #expect(rfcLang2.code == rfcLang.code) + #expect(rfcLang2.regionCode == rfcLang.regionCode) + #expect(rfcLang2.scriptCode == rfcLang.scriptCode) - for lang in list { - let testLang = W3WBaseLanguage(locale: lang) - let rfcLang = testLang.toRfcLanguage() - - #expect(rfcLang.regionCode != nil) - #expect(rfcLang.code == testLang.code) - - let rfcLang2 = W3WRfcLanguage(from: lang) - #expect(rfcLang2.code == rfcLang.code) - #expect(rfcLang2.regionCode == rfcLang.regionCode) - #expect(rfcLang2.scriptCode == rfcLang.scriptCode) - } } - @Test - func convertLanguageToRfcLangage_LanguageScript_Success() { - let list = [ - "zh-Hans", "zh-Hant", "sr-Cyrl", "sr-Latn", - "uz-Cyrl", "uz-Latn" - ] + @Test(arguments: [ + "zh-Hans", "zh-Hant", "sr-Cyrl", "sr-Latn", + "uz-Cyrl", "uz-Latn" + ]) + func convertLanguageToRfcLangage_LanguageScript_Success(lang: String) { + let testLang = W3WBaseLanguage(locale: lang) + let rfcLang = testLang.toRfcLanguage() - for lang in list { - let testLang = W3WBaseLanguage(locale: lang) - let rfcLang = testLang.toRfcLanguage() - - #expect(rfcLang.regionCode == nil) - if #available(iOS 16, *) { - #expect(rfcLang.scriptCode != nil) - } - - let rfcLang2 = W3WRfcLanguage(from: lang) - #expect(rfcLang2.code == rfcLang.code) - #expect(rfcLang2.regionCode == rfcLang.regionCode) - if #available(iOS 16, *) { - #expect(rfcLang2.scriptCode == rfcLang.scriptCode) - } + #expect(rfcLang.regionCode == nil) + if #available(iOS 16, *) { + #expect(rfcLang.scriptCode != nil) } - } - - @Test - func convertLanguageToRfcLangage_LanguageScriptRegion_Success() { - let list = [ - "zh-Hans-CN", "zh-Hant-TW", "sr-Cyrl-RS", "sr-Latn-BA", - "uz-Latn-UZ" - ] - for lang in list { - let testLang = W3WBaseLanguage(locale: lang) - let rfcLang = testLang.toRfcLanguage() - - #expect(rfcLang.regionCode != nil) - if #available(iOS 16, *) { - #expect(rfcLang.scriptCode != nil) - } - #expect(rfcLang.code == testLang.code) - - let rfcLang2 = W3WRfcLanguage(from: lang) - #expect(rfcLang2.code == rfcLang.code) - #expect(rfcLang2.regionCode == rfcLang.regionCode) - if #available(iOS 16, *) { - #expect(rfcLang2.scriptCode == rfcLang.scriptCode) - } + let rfcLang2 = W3WRfcLanguage(from: lang) + #expect(rfcLang2.code == rfcLang.code) + #expect(rfcLang2.regionCode == rfcLang.regionCode) + if #available(iOS 16, *) { + #expect(rfcLang2.scriptCode == rfcLang.scriptCode) } } - @Test - func convertLanguageToRfcLangage_CurrentLanguageList_Success() { - let list: [String] = [ - "vi", "af", "id", "ms", "de", - "en-AU", "en-GB", "en-US", "en-IN", - "es-ES", "es-MX", "sw", "nl", - "pt-BR", "pt-PT", "ro", "fi", - "sv", "xh", "zu", "bg", "mn", - "ru", "kk", "ko", "ja", "ar" - ] + @Test(arguments: [ + "zh-Hans-CN", "zh-Hant-TW", "sr-Cyrl-RS", + "sr-Latn-BA", "uz-Latn-UZ" + ]) + func convertLanguageToRfcLangage_LanguageScriptRegion_Success(lang: String) { + let testLang = W3WBaseLanguage(locale: lang) + let rfcLang = testLang.toRfcLanguage() - for lang in list { - let rfcLang = W3WRfcLanguage(from: lang) - - #expect(rfcLang.code != nil) - if #available(iOS 16, *) { - #expect(rfcLang.scriptCode != nil) - } - if lang.count > 3 { - #expect(rfcLang.regionCode != nil) - } + #expect(rfcLang.regionCode != nil) + if #available(iOS 16, *) { + #expect(rfcLang.scriptCode != nil) } - } - - @Test - func convertLanguageToRfcLangage_UnderscoreFormats_AppleLegacy_Success() { - let list = [ - "en_US", "fr_FR", "pt_BR", "zh_CN", "zh_TW", - "sr_Cyrl_RS" - ] + #expect(rfcLang.code == testLang.code) - for lang in list { - let rfcLang = W3WRfcLanguage(from: lang) - let w3wLang = rfcLang.toW3wLanguage() - - #expect(w3wLang?.code == rfcLang.code) + let rfcLang2 = W3WRfcLanguage(from: lang) + #expect(rfcLang2.code == rfcLang.code) + #expect(rfcLang2.regionCode == rfcLang.regionCode) + if #available(iOS 16, *) { + #expect(rfcLang2.scriptCode == rfcLang.scriptCode) } } - @Test - func convertLanguageToRfcLanguage_3CharactersCode_Success() { - let list = [ - "haw-US", - "yue-Hant", - "gsw-CH" - ] + @Test(arguments: [ + "vi", "af", "id", "ms", "de", + "en-AU", "en-GB", "en-US", "en-IN", + "es-ES", "es-MX", "sw", "nl", + "pt-BR", "pt-PT", "ro", "fi", + "sv", "xh", "zu", "bg", "mn", + "ru", "kk", "ko", "ja", "ar" + ]) + func convertLanguageToRfcLangage_CurrentLanguageList_Success(lang: String) { + let rfcLang = W3WRfcLanguage(from: lang) - for lang in list { - let rfcLang = W3WRfcLanguage(from: lang) - #expect(rfcLang.code?.count == 3) + #expect(rfcLang.code != nil) + if #available(iOS 16, *) { + #expect(rfcLang.scriptCode != nil) + } + if lang.count > 3 { + #expect(rfcLang.regionCode != nil) } } - @Test - func convertLanguageToRfcLanguage_InvalidLanguage_Failure() { - let list = [ - // invalid code - "xx", - "xxx", - "123", - // invalid script - "en-ABCDE", - "sr-OOO", - "hr-12ab", - // invalid region - "en-USS", - "fr-1234", - "es-1X", - "zh-Hans-CNN", - // wrong order - "CN-zh", - "Cyrl-sr", - "US-en", - // too many components - "en-US-extra", - "sr-Cyrl-RS-foo", - // empty component - "-en", - "en--US", - "en-", - "en- -US" - - ] + @Test(arguments: [ + "en_US", "fr_FR", "pt_BR", "zh_CN", "zh_TW", + "sr_Cyrl_RS" + ]) + func convertLanguageToRfcLangage_UnderscoreFormats_AppleLegacy_Success(lang: String) { + let rfcLang = W3WRfcLanguage(from: lang) + let w3wLang = rfcLang.toW3wLanguage() - for lang in list { - let rfcLang = W3WRfcLanguage(from: lang) - #expect(rfcLang.code == nil) - #expect(rfcLang.scriptCode == nil) - #expect(rfcLang.regionCode == nil) - } + #expect(w3wLang?.code == rfcLang.code) + } + + @Test(arguments: [ + "haw-US", + "yue-Hant", + "gsw-CH" + ]) + func convertLanguageToRfcLanguage_3CharactersCode_Success(lang: String) { + let rfcLang = W3WRfcLanguage(from: lang) + #expect(rfcLang.code?.count == 3) + } + + @Test(arguments: [ + "xx", "xxx", "123", + "en-ABCDE", "sr-OOO", "hr-12ab", + "en-USS", "fr-1234", "es-1X", "zh-Hans-CNN", + "CN-zh", "Cyrl-sr", "US-en", + "en-US-extra", "sr-Cyrl-RS-foo", + "-en", "en--US", "en-", "en- -US" + ]) + func convertLanguageToRfcLanguage_InvalidLanguage_Failure(lang: String) { + let rfcLang = W3WRfcLanguage(from: lang) + #expect(rfcLang.code == nil) + #expect(rfcLang.scriptCode == nil) + #expect(rfcLang.regionCode == nil) } } diff --git a/Tests/languages-Tests/W3wRfcLanguage_w3wSdkLanguage_tests.swift b/Tests/languages-Tests/W3wRfcLanguage_w3wSdkLanguage_tests.swift index c8aa679..fcafa0b 100644 --- a/Tests/languages-Tests/W3wRfcLanguage_w3wSdkLanguage_tests.swift +++ b/Tests/languages-Tests/W3wRfcLanguage_w3wSdkLanguage_tests.swift @@ -15,7 +15,7 @@ struct W3wRfcLanguage_W3wSdkLanguage_Tests { @Test func convertSdkLanguageToRfcLanguage_Success() throws { let sdkLanguage = try W3WSdkLanguage("fr_fr") - let convertedRfcLanguage = sdkLanguage.toRfcLanguage() as! W3WRfcLanguage + let convertedRfcLanguage = try #require(sdkLanguage.toRfcLanguage() as? W3WRfcLanguage) let rfcLanguage = W3WRfcLanguage(from: "fr_FR") #expect(convertedRfcLanguage == rfcLanguage) @@ -30,7 +30,7 @@ struct W3wRfcLanguage_W3wSdkLanguage_Tests { @Test func convertSdkLanguageToRfcLanguage_CodeOnly_Success() throws { let sdkLanguage = try W3WSdkLanguage("ja") - let convertedRfcLanguage = sdkLanguage.toRfcLanguage() as! W3WRfcLanguage + let convertedRfcLanguage = try #require(sdkLanguage.toRfcLanguage() as? W3WRfcLanguage) let rfcLanguage = W3WRfcLanguage(from: "ja") #expect(convertedRfcLanguage == rfcLanguage) @@ -78,8 +78,8 @@ struct W3wRfcLanguage_W3wSdkLanguage_Tests { let w3wBaseLanguage = W3WBaseLanguage(locale: string) let w3wSdkLanguage = try W3WSdkLanguage(sdkString) - let firstLang = w3wBaseLanguage.toRfcLanguage() as! W3WRfcLanguage - let secondLang = w3wSdkLanguage.toRfcLanguage() as! W3WRfcLanguage + let firstLang = try #require(w3wBaseLanguage.toRfcLanguage() as? W3WRfcLanguage) + let secondLang = try #require(w3wSdkLanguage.toRfcLanguage() as? W3WRfcLanguage) #expect(firstLang == secondLang) #expect(firstLang.code == secondLang.code) From 4b3eda10715d154859251da912f7c47f840ebe3c Mon Sep 17 00:00:00 2001 From: Kaley Nguyen Date: Wed, 3 Dec 2025 14:02:23 +0700 Subject: [PATCH 8/9] update some logics and funcs --- .../RfcLanguage/W3WRfcLanguage.swift | 36 +++++++++++-------- .../w3wRfcLanguage_tests.swift | 29 +++++++++++++++ 2 files changed, 50 insertions(+), 15 deletions(-) create mode 100644 Tests/languages-Tests/w3wRfcLanguage_tests.swift diff --git a/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage.swift b/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage.swift index da62b32..09803ce 100644 --- a/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage.swift +++ b/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage.swift @@ -14,6 +14,9 @@ import Foundation /// - script (optional, 4 letters, titlecased) /// - region (optional, 2 letters or 3-digit number) public struct W3WRfcLanguage: W3WRfcLanguageProtocol { + /// default language is "en". name's default can be changed externally to use a different language by an app if nessesary + public static var `default` = W3WRfcLanguage(code: "en") + public var code: String? public var scriptCode: String? public var regionCode: String? @@ -24,22 +27,20 @@ public struct W3WRfcLanguage: W3WRfcLanguageProtocol { self.init(from: locale.language) } else { // no script for iOS < 16 - self.init( - languageCode: locale.languageCode, - script: locale.scriptCode, - region: locale.regionCode - ) + self.init(code: locale.languageCode, + scriptCode: locale.scriptCode, + regionCode: locale.regionCode) } } public init( - languageCode: String? = nil, - script: String? = nil, - region: String? = nil + code: String? = nil, + scriptCode: String? = nil, + regionCode: String? = nil ) { - self.code = languageCode - self.scriptCode = script - self.regionCode = region + self.code = code + self.scriptCode = scriptCode + self.regionCode = regionCode } @available(iOS 16, *) @@ -61,7 +62,7 @@ public extension W3WRfcLanguage { // Handle “Base” or empty strings, check if string is a valid locale string guard !normalized.isEmpty, string.isValidLocale else { - self.init(languageCode: nil, script: nil, region: nil) + self.init(code: nil, scriptCode: nil, regionCode: nil) return } @@ -92,7 +93,7 @@ public extension W3WRfcLanguage { } } - self.init(languageCode: langCode, script: script, region: region) + self.init(code: langCode, scriptCode: script, regionCode: region) } } } @@ -103,7 +104,7 @@ public extension W3WRfcLanguageProtocol { return [code, scriptCode, regionCode] .compactMap { $0 } .joined(separator: "-") - } + } /// short : code - region var shortIdentifier: String { return [code, regionCode] @@ -134,6 +135,11 @@ extension W3WRfcLanguage { } public var name: String? { - return LanguageUtils.getLanguageName(forLocale: identifier, inLocale: "en") + return LanguageUtils.getLanguageName(forLocale: identifier, inLocale: Self.default.identifier) + } + + /// get name of the language in any particular locale + func name(in locale: String) -> String? { + return LanguageUtils.getLanguageName(forLocale: identifier, inLocale: locale) } } diff --git a/Tests/languages-Tests/w3wRfcLanguage_tests.swift b/Tests/languages-Tests/w3wRfcLanguage_tests.swift new file mode 100644 index 0000000..940f2aa --- /dev/null +++ b/Tests/languages-Tests/w3wRfcLanguage_tests.swift @@ -0,0 +1,29 @@ +// +// w3wRfcLanguage_tests.swift +// w3w-swift-core +// +// Created by Kaley Nguyen on 3/12/25. +// + +import Testing +@testable import W3WSwiftCore +import w3w + +@Suite +struct w3wRfcLanguage_tests { + + @Test func initRfcLanguage() async throws { + let lang = W3WRfcLanguage(locale: Locale(identifier: "fr-FR")) + #expect(lang.code == "fr") + #expect(lang.nativeName == "français (France)") + #expect(lang.name == "French (France)") + #expect(lang.name(in: "ja") == "フランス語 (フランス)") + } + + @Test func initRfcLanguage_changeDefault() async throws { + let lang = W3WRfcLanguage(locale: Locale(identifier: "fr-FR")) + W3WRfcLanguage.default = W3WRfcLanguage(from: "de") + #expect(lang.code == "fr") + #expect(lang.name == "Französisch (Frankreich)") + } +} From ab09d8c860fd3c220f42a0a5a24672cb1e9ca646 Mon Sep 17 00:00:00 2001 From: Kaley Nguyen Date: Wed, 3 Dec 2025 20:49:19 +0700 Subject: [PATCH 9/9] update 1 more extension --- Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage.swift b/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage.swift index 09803ce..2a14558 100644 --- a/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage.swift +++ b/Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage.swift @@ -142,4 +142,9 @@ extension W3WRfcLanguage { func name(in locale: String) -> String? { return LanguageUtils.getLanguageName(forLocale: identifier, inLocale: locale) } + + /// get name of the language in any other language + func name(in language: W3WRfcLanguage) -> String? { + return name(in: language.identifier) + } }