diff --git a/Modules/Package.swift b/Modules/Package.swift index 6fa743000e7a..92e8a8aba708 100644 --- a/Modules/Package.swift +++ b/Modules/Package.swift @@ -215,6 +215,7 @@ let package = Package( "WordPressKitModels", "WordPressKitObjCUtils", "NSObject-SafeExpectations", + "WordPressShared", "wpxmlrpc", ], swiftSettings: [.swiftLanguageMode(.v5)] diff --git a/Modules/Sources/WordPressKit/WordPressComLanguageDatabase.swift b/Modules/Sources/WordPressKit/WordPressComLanguageDatabase.swift deleted file mode 100644 index d52e4b911c71..000000000000 --- a/Modules/Sources/WordPressKit/WordPressComLanguageDatabase.swift +++ /dev/null @@ -1,362 +0,0 @@ -import Foundation - -/// This helper class allows us to map WordPress.com LanguageID's into human readable language strings. -/// -class WordPressComLanguageDatabase: NSObject { - // MARK: - Properties - - /// Languages considered 'popular' - /// - let popular: [Language] - - /// Every supported language - /// - let all: [Language] - - /// Returns both, Popular and All languages, grouped - /// - let grouped: [[Language]] - - // MARK: - Methods - - /// Designated Initializer: will load the languages contained within the `Languages.json` file. - /// - override init() { - // Parse the json file - let raw = languagesJSON.data(using: .utf8)! - let parsed = try! JSONSerialization.jsonObject(with: raw, options: [.mutableContainers, .mutableLeaves]) as? NSDictionary - - // Parse All + Popular: All doesn't contain Popular. Otherwise the json would have dupe data. Right? - let parsedAll = Language.fromArray(parsed![Keys.all] as! [[String: Any]]) - let parsedPopular = Language.fromArray(parsed![Keys.popular] as! [[String: Any]]) - let merged = parsedAll + parsedPopular - - // Done! - popular = parsedPopular - all = merged.sorted { $0.name < $1.name } - grouped = [popular] + [all] - } - - /// Returns the Human Readable name for a given Language Identifier - /// - /// - Parameter languageId: The Identifier of the language. - /// - /// - Returns: A string containing the language name, or an empty string, in case it wasn't found. - /// - @objc func nameForLanguageWithId(_ languageId: Int) -> String { - return find(id: languageId)?.name ?? "" - } - - /// Returns the Language with a given Language Identifier - /// - /// - Parameter id: The Identifier of the language. - /// - /// - Returns: The language with the matching Identifier, or nil, in case it wasn't found. - /// - func find(id: Int) -> Language? { - return all.first(where: { $0.id == id }) - } - - /// Returns the current device language as the corresponding WordPress.com language ID. - /// If the language is not supported, it returns 1 (English). - /// - /// This is a wrapper for Objective-C, Swift code should use deviceLanguage directly. - /// - @objc(deviceLanguageId) - func deviceLanguageIdNumber() -> NSNumber { - return NSNumber(value: deviceLanguage.id) - } - - /// Returns the slug string for the current device language. - /// If the language is not supported, it returns "en" (English). - /// - /// This is a wrapper for Objective-C, Swift code should use deviceLanguage directly. - /// - @objc(deviceLanguageSlug) - func deviceLanguageSlugString() -> String { - return deviceLanguage.slug - } - - /// Returns the current device language as the corresponding WordPress.com language. - /// If the language is not supported, it returns English. - /// - var deviceLanguage: Language { - let variants = LanguageTagVariants(string: deviceLanguageCode) - for variant in variants { - if let match = self.languageWithSlug(variant) { - return match - } - } - return languageWithSlug("en")! - } - - /// Searches for a WordPress.com language that matches a language tag. - /// - fileprivate func languageWithSlug(_ slug: String) -> Language? { - let search = languageCodeReplacements[slug] ?? slug - - // Use lazy evaluation so we stop filtering as soon as we got the first match - return all.lazy.filter({ $0.slug == search }).first - } - - /// Overrides the device language. For testing purposes only. - /// - @objc func _overrideDeviceLanguageCode(_ code: String) { - deviceLanguageCode = code.lowercased() - } - - // MARK: - Nested Classes - - /// Represents a Language supported by WordPress.com - /// - class Language: Equatable { - /// Language Unique Identifier - /// - let id: Int - - /// Human readable Language name - /// - let name: String - - /// Language's Slug String - /// - let slug: String - - /// Localized description for the current language - /// - var description: String { - return (Locale.current as NSLocale).displayName(forKey: NSLocale.Key.identifier, value: slug) ?? name - } - - /// Designated initializer. Will fail if any of the required properties is missing - /// - init?(dict: [String: Any]) { - guard let unwrappedId = (dict[Keys.identifier] as? NSNumber)?.intValue, - let unwrappedSlug = dict[Keys.slug] as? String, - let unwrappedName = dict[Keys.name] as? String else { - id = Int.min - name = String() - slug = String() - return nil - } - - id = unwrappedId - name = unwrappedName - slug = unwrappedSlug - } - - /// Given an array of raw languages, will return a parsed array. - /// - static func fromArray(_ array: [[String: Any]] ) -> [Language] { - return array.compactMap { - return Language(dict: $0) - } - } - - static func == (lhs: Language, rhs: Language) -> Bool { - return lhs.id == rhs.id - } - } - - // MARK: - Private Variables - - /// The device's current preferred language, or English if there's no preferred language. - /// - fileprivate lazy var deviceLanguageCode: String = { - return NSLocale.preferredLanguages.first?.lowercased() ?? "en" - }() - - // MARK: - Private Constants - fileprivate let filename = "Languages" - - // (@koke 2016-04-29) I'm not sure how correct this mapping is, but it matches - // what we do for the app translations, so they will at least be consistent - fileprivate let languageCodeReplacements: [String: String] = [ - "zh-hans": "zh-cn", - "zh-hant": "zh-tw" - ] - - // MARK: - Private Nested Structures - - /// Keys used to parse the raw languages. - /// - fileprivate struct Keys { - // swiftlint:disable operator_usage_whitespace - static let popular = "popular" - static let all = "all" - static let identifier = "i" - static let slug = "s" - static let name = "n" - // swiftlint:enable operator_usage_whitespace - } -} - -/// Provides a sequence of language tags from the specified string, from more to less specific -/// For instance, "zh-Hans-HK" will yield `["zh-Hans-HK", "zh-Hans", "zh"]` -/// -private struct LanguageTagVariants: Sequence { - let string: String - - func makeIterator() -> AnyIterator { - var components = string.components(separatedBy: "-") - return AnyIterator { - guard !components.isEmpty else { - return nil - } - - let current = components.joined(separator: "-") - components.removeLast() - - return current - } - } -} - -private let languagesJSON = """ -{ - "popular" : [ - { "i": 1, "s": "en", "n": "English" }, - { "i": 19, "s": "es", "n": "Español" }, - { "i": 438, "s": "pt-br", "n": "Português do Brasil" }, - { "i": 15, "s": "de", "n": "Deutsch" }, - { "i": 24, "s": "fr", "n": "Français" }, - { "i": 29, "s": "he", "n": "עברית" }, - { "i": 36, "s": "ja", "n": "日本語" }, - { "i": 35, "s": "it", "n": "Italiano" }, - { "i": 49, "s": "nl", "n": "Nederlands" }, - { "i": 62, "s": "ru", "n": "Русский" }, - { "i": 78, "s": "tr", "n": "Türkçe" }, - { "i": 33, "s": "id", "n": "Bahasa Indonesia" }, - { "i": 449, "s": "zh-cn", "n": "中文(简体)" }, - { "i": 452, "s": "zh-tw", "n": "中文(繁體)" }, - { "i": 40, "s": "ko", "n": "한국어" } - ], - "all" : [ - { "i": 2, "s": "af", "n": "Afrikaans" }, - { "i": 418, "s": "als", "n": "Alemannisch" }, - { "i": 481, "s": "am", "n": "Amharic" }, - { "i": 3, "s": "ar", "n": "العربية" }, - { "i": 419, "s": "arc", "n": "ܕܥܒܪܸܝܛ" }, - { "i": 4, "s": "as", "n": "অসমীয়া" }, - { "i": 420, "s": "ast", "n": "Asturianu" }, - { "i": 421, "s": "av", "n": "Авар" }, - { "i": 422, "s": "ay", "n": "Aymar" }, - { "i": 79, "s": "az", "n": "Azərbaycan" }, - { "i": 423, "s": "ba", "n": "Башҡорт" }, - { "i": 5, "s": "be", "n": "Беларуская" }, - { "i": 6, "s": "bg", "n": "Български" }, - { "i": 7, "s": "bm", "n": "Bamanankan" }, - { "i": 8, "s": "bn", "n": "বাংলা" }, - { "i": 9, "s": "bo", "n": "བོད་ཡིག" }, - { "i": 424, "s": "br", "n": "Brezhoneg" }, - { "i": 454, "s": "bs", "n": "Bosanski" }, - { "i": 10, "s": "ca", "n": "Català" }, - { "i": 425, "s": "ce", "n": "Нохчийн" }, - { "i": 11, "s": "cs", "n": "Česky" }, - { "i": 12, "s": "csb", "n": "Kaszëbsczi" }, - { "i": 426, "s": "cv", "n": "Чӑваш" }, - { "i": 13, "s": "cy", "n": "Cymraeg" }, - { "i": 14, "s": "da", "n": "Dansk" }, - { "i": 427, "s": "dv", "n": "Divehi" }, - { "i": 16, "s": "dz", "n": "ཇོང་ཁ" }, - { "i": 17, "s": "el", "n": "Ελληνικά" }, - { "i": 468, "s": "el-po", "n": "Greek-polytonic" }, - { "i": 18, "s": "eo", "n": "Esperanto" }, - { "i": 20, "s": "et", "n": "Eesti" }, - { "i": 429, "s": "eu", "n": "Euskara" }, - { "i": 21, "s": "fa", "n": "فارسی" }, - { "i": 22, "s": "fi", "n": "Suomi" }, - { "i": 473, "s": "fil", "n": "Filipino" }, - { "i": 23, "s": "fo", "n": "Føroyskt" }, - { "i": 478, "s": "fr-be", "n": "Français de Belgique" }, - { "i": 475, "s": "fr-ca", "n": "Français (Canada)" }, - { "i": 474, "s": "fr-ch", "n": "Français de Suisse" }, - { "i": 25, "s": "fur", "n": "Furlan" }, - { "i": 26, "s": "fy", "n": "Frysk" }, - { "i": 27, "s": "ga", "n": "Gaeilge" }, - { "i": 476, "s": "gd", "n": "Gàidhlig" }, - { "i": 457, "s": "gl", "n": "Galego" }, - { "i": 430, "s": "gn", "n": "Avañeẽ" }, - { "i": 28, "s": "gu", "n": "ગુજરાતી" }, - { "i": 30, "s": "hi", "n": "हिन्दी" }, - { "i": 431, "s": "hr", "n": "Hrvatski" }, - { "i": 31, "s": "hu", "n": "Magyar" }, - { "i": 467, "s": "hy", "n": "Armenian" }, - { "i": 32, "s": "ia", "n": "Interlingua" }, - { "i": 432, "s": "ii", "n": "ꆇꉙ" }, - { "i": 469, "s": "ilo", "n": "Ilokano" }, - { "i": 34, "s": "is", "n": "Íslenska" }, - { "i": 37, "s": "ka", "n": "ქართული" }, - { "i": 462, "s": "kk", "n": "Қазақ тілі" }, - { "i": 38, "s": "km", "n": "ភាសាខ្មែរ" }, - { "i": 39, "s": "kn", "n": "ಕನ್ನಡ" }, - { "i": 433, "s": "ks", "n": "कश्मीरी - (كشميري)" }, - { "i": 41, "s": "ku", "n": "Kurdî / كوردي" }, - { "i": 434, "s": "kv", "n": "Коми" }, - { "i": 479, "s": "ky", "n": "кыргыз тили" }, - { "i": 42, "s": "la", "n": "Latina" }, - { "i": 43, "s": "li", "n": "Limburgs" }, - { "i": 44, "s": "lo", "n": "ລາວ" }, - { "i": 45, "s": "lt", "n": "Lietuvių" }, - { "i": 453, "s": "lv", "n": "Latviešu valoda" }, - { "i": 435, "s": "mk", "n": "Македонски" }, - { "i": 46, "s": "ml", "n": "മലയാളം" }, - { "i": 472, "s": "mn", "n": "монгол хэл" }, - { "i": 461, "s": "mr", "n": "मराठी Marāṭhī" }, - { "i": 47, "s": "ms", "n": "Bahasa Melayu" }, - { "i": 465, "s": "mt", "n": "Malti" }, - { "i": 464, "s": "mwl", "n": "Mirandés" }, - { "i": 436, "s": "nah", "n": "Nahuatl" }, - { "i": 437, "s": "nap", "n": "Nnapulitano" }, - { "i": 48, "s": "nds", "n": "Plattdüütsch" }, - { "i": 456, "s": "ne", "n": "Nepali" }, - { "i": 50, "s": "nn", "n": "Norsk (nynorsk)" }, - { "i": 51, "s": "no", "n": "Norsk (bokmål)" }, - { "i": 52, "s": "non", "n": "Norrǿna" }, - { "i": 53, "s": "nv", "n": "Diné bizaad" }, - { "i": 54, "s": "oc", "n": "Occitan" }, - { "i": 55, "s": "or", "n": "ଓଡ଼ିଆ" }, - { "i": 56, "s": "os", "n": "Иронау" }, - { "i": 57, "s": "pa", "n": "ਪੰਜਾਬੀ" }, - { "i": 58, "s": "pl", "n": "Polski" }, - { "i": 59, "s": "ps", "n": "پښتو" }, - { "i": 60, "s": "pt", "n": "Português" }, - { "i": 439, "s": "qu", "n": "Runa Simi" }, - { "i": 61, "s": "ro", "n": "Română" }, - { "i": 483, "s": "rup", "n": "Armãneashce" }, - { "i": 63, "s": "sc", "n": "Sardu" }, - { "i": 440, "s": "sd", "n": "سنڌي" }, - { "i": 471, "s": "si", "n": "Sinhala" }, - { "i": 64, "s": "sk", "n": "Slovenčina" }, - { "i": 65, "s": "sl", "n": "Slovenščina" }, - { "i": 459, "s": "so", "n": "Somali" }, - { "i": 66, "s": "sq", "n": "Shqip" }, - { "i": 67, "s": "sr", "n": "Српски / Srpski" }, - { "i": 441, "s": "su", "n": "Basa Sunda" }, - { "i": 68, "s": "sv", "n": "Svenska" }, - { "i": 69, "s": "ta", "n": "தமிழ்" }, - { "i": 70, "s": "te", "n": "తెలుగు" }, - { "i": 71, "s": "th", "n": "ไทย" }, - { "i": 480, "s": "tir", "n": "Tigrigna" }, - { "i": 455, "s": "tl", "n": "Tagalog" }, - { "i": 72, "s": "tt", "n": "Tatarça" }, - { "i": 442, "s": "ty", "n": "Reo Mā`ohi" }, - { "i": 443, "s": "udm", "n": "Удмурт" }, - { "i": 444, "s": "ug", "n": "Uyghur"}, - { "i": 73, "s": "uk", "n": "Українська" }, - { "i": 74, "s": "ur", "n": "اردو" }, - { "i": 458, "s": "uz", "n": "Uzbek" }, - { "i": 463, "s": "va", "n": "valencià" }, - { "i": 445, "s": "vec", "n": "Vèneto" }, - { "i": 446, "s": "vi", "n": "Tiếng Việt" }, - { "i": 75, "s": "wa", "n": "Walon" }, - { "i": 447, "s": "xal", "n": "Хальмг" }, - { "i": 76, "s": "yi", "n": "ייִדיש" }, - { "i": 477, "s": "yo", "n": "èdè Yorùbá" }, - { "i": 448, "s": "za", "n": "Zhuang (Cuengh)" }, - { "i": 77, "s": "zh", "n": "中文" }, - { "i": 450, "s": "zh-hk", "n": "中文(繁體)" }, - { "i": 451, "s": "zh-sg", "n": "中文(简体)" } - ] -} -""" diff --git a/Modules/Sources/WordPressKit/WordPressComRestApi.swift b/Modules/Sources/WordPressKit/WordPressComRestApi.swift index d3e37042282d..b9e5a891a7cd 100644 --- a/Modules/Sources/WordPressKit/WordPressComRestApi.swift +++ b/Modules/Sources/WordPressKit/WordPressComRestApi.swift @@ -1,4 +1,5 @@ import Foundation +import WordPressShared // MARK: - WordPressComRestApiError @@ -320,7 +321,7 @@ open class WordPressComRestApi: NSObject { var builder = HTTPRequestBuilder(url: url) if appendsPreferredLanguageLocale { - let preferredLanguageIdentifier = WordPressComLanguageDatabase().deviceLanguage.slug + let preferredLanguageIdentifier = WordPressComLanguageDatabase.shared.deviceLanguage.slug builder = builder.query(defaults: [URLQueryItem(name: localeKey, value: preferredLanguageIdentifier)]) } diff --git a/Modules/Sources/WordPressKit/WordPressComServiceRemote+SiteCreation.swift b/Modules/Sources/WordPressKit/WordPressComServiceRemote+SiteCreation.swift index ef9dc01f01b7..dd56d6571bfe 100644 --- a/Modules/Sources/WordPressKit/WordPressComServiceRemote+SiteCreation.swift +++ b/Modules/Sources/WordPressKit/WordPressComServiceRemote+SiteCreation.swift @@ -11,7 +11,7 @@ public struct SiteCreationRequest: Encodable { public let tagline: String? public let siteURLString: String public let isPublic: Bool - public let languageIdentifier: String + public let languageIdentifier: Int public let shouldValidate: Bool public let clientIdentifier: String public let clientSecret: String @@ -27,7 +27,7 @@ public struct SiteCreationRequest: Encodable { tagline: String?, siteURLString: String, isPublic: Bool, - languageIdentifier: String, + languageIdentifier: Int, shouldValidate: Bool, clientIdentifier: String, clientSecret: String, diff --git a/Modules/Sources/WordPressShared/Utility/Languages.swift b/Modules/Sources/WordPressShared/Utility/Languages.swift index 2bc535a9c377..85fe2b9f5056 100644 --- a/Modules/Sources/WordPressShared/Utility/Languages.swift +++ b/Modules/Sources/WordPressShared/Utility/Languages.swift @@ -2,40 +2,48 @@ import Foundation /// This helper class allows us to map WordPress.com LanguageID's into human readable language strings. /// -public class WordPressComLanguageDatabase: NSObject { +public struct WordPressComLanguageDatabase { + + public static let shared = WordPressComLanguageDatabase() + // MARK: - Public Properties /// Languages considered 'popular' /// - public let popular: [Language] + public let popular: [WPComLanguage] /// Every supported language /// - public let all: [Language] + public let all: [WPComLanguage] - /// Returns both, Popular and All languages, grouped - /// - public let grouped: [[Language]] + /// Allow mocking the device language code for testing purposes + private let _deviceLanguageCode: String? // MARK: - Public Methods /// Designated Initializer: will load the languages contained within the `Languages.json` file. /// - public override init() { + private init() { + // Parse the json file + let path = Bundle.wordPressSharedBundle.path(forResource: "Languages", ofType: "json") + let data = try! Data(contentsOf: URL(fileURLWithPath: path!)) + let bundle = try! JSONDecoder().decode(WPComLanguageBundle.self, from: data) + + self.popular = bundle.popular + self.all = bundle.all + self._deviceLanguageCode = nil + } + + /// Specifically marked internal for used by test code + internal init(deviceLanguageCode: String) { // Parse the json file - let path = Bundle.wordPressSharedBundle.path(forResource: filename, ofType: "json") - let raw = try! Data(contentsOf: URL(fileURLWithPath: path!)) - let parsed = try! JSONSerialization.jsonObject(with: raw, options: [.mutableContainers, .mutableLeaves]) as? NSDictionary - - // Parse All + Popular: All doesn't contain Popular. Otherwise the json would have dupe data. Right? - let parsedAll = Language.fromArray(parsed![Keys.all] as! [[String: Any]]) - let parsedPopular = Language.fromArray(parsed![Keys.popular] as! [[String: Any]]) - let merged = parsedAll + parsedPopular - - // Done! - popular = parsedPopular - all = merged.sorted { $0.name < $1.name } - grouped = [popular] + [all] + let path = Bundle.wordPressSharedBundle.path(forResource: "Languages", ofType: "json") + let data = try! Data(contentsOf: URL(fileURLWithPath: path!)) + let bundle = try! JSONDecoder().decode(WPComLanguageBundle.self, from: data) + + self.popular = bundle.popular + self.all = bundle.all + self._deviceLanguageCode = deviceLanguageCode.lowercased() } /// Returns the Human Readable name for a given Language Identifier @@ -44,7 +52,7 @@ public class WordPressComLanguageDatabase: NSObject { /// /// - Returns: A string containing the language name, or an empty string, in case it wasn't found. /// - @objc public func nameForLanguageWithId(_ languageId: Int) -> String { + public func nameForLanguageWithId(_ languageId: Int) -> String { return find(id: languageId)?.name ?? "" } @@ -54,34 +62,14 @@ public class WordPressComLanguageDatabase: NSObject { /// /// - Returns: The language with the matching Identifier, or nil, in case it wasn't found. /// - public func find(id: Int) -> Language? { + public func find(id: Int) -> WPComLanguage? { return all.first(where: { $0.id == id }) } - /// Returns the current device language as the corresponding WordPress.com language ID. - /// If the language is not supported, it returns 1 (English). - /// - /// This is a wrapper for Objective-C, Swift code should use deviceLanguage directly. - /// - @objc(deviceLanguageId) - public func deviceLanguageIdNumber() -> NSNumber { - return NSNumber(value: deviceLanguage.id) - } - - /// Returns the slug string for the current device language. - /// If the language is not supported, it returns "en" (English). - /// - /// This is a wrapper for Objective-C, Swift code should use deviceLanguage directly. - /// - @objc(deviceLanguageSlug) - public func deviceLanguageSlugString() -> String { - return deviceLanguage.slug - } - /// Returns the current device language as the corresponding WordPress.com language. /// If the language is not supported, it returns English. /// - public var deviceLanguage: Language { + public var deviceLanguage: WPComLanguage { let variants = LanguageTagVariants(string: deviceLanguageCode) for variant in variants { if let match = self.languageWithSlug(variant) { @@ -93,101 +81,36 @@ public class WordPressComLanguageDatabase: NSObject { /// Searches for a WordPress.com language that matches a language tag. /// - fileprivate func languageWithSlug(_ slug: String) -> Language? { + fileprivate func languageWithSlug(_ slug: String) -> WPComLanguage? { let search = languageCodeReplacements[slug] ?? slug - - // Use lazy evaluation so we stop filtering as soon as we got the first match - return all.lazy.filter({ $0.slug == search }).first - } - - /// Overrides the device language. For testing purposes only. - /// - @objc func _overrideDeviceLanguageCode(_ code: String) { - deviceLanguageCode = code.lowercased() + return all.first { $0.slug == search } } - // MARK: - Public Nested Classes + // MARK: - Private Variables - /// Represents a Language supported by WordPress.com + /// The device's current preferred language, or English if there's no preferred language. /// - public class Language: Equatable { - /// Language Unique Identifier - /// - public let id: Int - - /// Human readable Language name - /// - public let name: String - - /// Language's Slug String - /// - public let slug: String + /// Specifically marked internal for used by test code + internal var deviceLanguageCode: String { - /// Localized description for the current language - /// - public var description: String { - return (Locale.current as NSLocale).displayName(forKey: NSLocale.Key.identifier, value: slug) ?? name + // Return the mocked language code, if set + if let _deviceLanguageCode { + return _deviceLanguageCode } - /// Designated initializer. Will fail if any of the required properties is missing - /// - init?(dict: [String: Any]) { - guard let unwrappedId = (dict[Keys.identifier] as? NSNumber)?.intValue, - let unwrappedSlug = dict[Keys.slug] as? String, - let unwrappedName = dict[Keys.name] as? String else { - id = Int.min - name = String() - slug = String() - return nil - } - - id = unwrappedId - name = unwrappedName - slug = unwrappedSlug + guard let preferredLanguage = Locale.preferredLanguages.first else { + return "en" } - /// Given an array of raw languages, will return a parsed array. - /// - public static func fromArray(_ array: [[String: Any]] ) -> [Language] { - return array.compactMap { - return Language(dict: $0) - } - } - - public static func == (lhs: Language, rhs: Language) -> Bool { - return lhs.id == rhs.id - } + return preferredLanguage.lowercased() } - // MARK: - Private Variables - - /// The device's current preferred language, or English if there's no preferred language. - /// - fileprivate lazy var deviceLanguageCode: String = { - return NSLocale.preferredLanguages.first?.lowercased() ?? "en" - }() - - // MARK: - Private Constants - fileprivate let filename = "Languages" - // (@koke 2016-04-29) I'm not sure how correct this mapping is, but it matches // what we do for the app translations, so they will at least be consistent fileprivate let languageCodeReplacements: [String: String] = [ "zh-hans": "zh-cn", "zh-hant": "zh-tw" ] - - // MARK: - Private Nested Structures - - /// Keys used to parse the raw languages. - /// - fileprivate struct Keys { - static let popular = "popular" - static let all = "all" - static let identifier = "i" - static let slug = "s" - static let name = "n" - } } /// Provides a sequence of language tags from the specified string, from more to less specific @@ -210,3 +133,48 @@ private struct LanguageTagVariants: Sequence { } } } + +// MARK: - Public Nested Classes + +public struct WPComLanguageBundle: Codable { + let popular: [WPComLanguage] + let others: [WPComLanguage] + + enum CodingKeys: String, CodingKey { + case popular = "popular" + case others = "all" + } + + var all: [WPComLanguage] { + (popular + others).sorted { $0.name < $1.name } + } +} + +/// Represents a Language supported by WordPress.com +/// +public struct WPComLanguage: Codable, Equatable { + + enum CodingKeys: String, CodingKey { + case id = "i" + case name = "n" + case slug = "s" + } + + /// Language Unique Identifier + /// + public let id: Int + + /// Human readable Language name + /// + public let name: String + + /// Language's Slug String + /// + public let slug: String + + /// Localized description for the current language + /// + public var description: String { + return (Locale.current as NSLocale).displayName(forKey: NSLocale.Key.identifier, value: slug) ?? name + } +} diff --git a/Modules/Tests/WordPressSharedTests/LanguagesTests.swift b/Modules/Tests/WordPressSharedTests/LanguagesTests.swift index 323ff2046dc7..2af2c91101a4 100644 --- a/Modules/Tests/WordPressSharedTests/LanguagesTests.swift +++ b/Modules/Tests/WordPressSharedTests/LanguagesTests.swift @@ -1,16 +1,23 @@ import XCTest +import Testing @testable import WordPressShared -class LanguagesTests: XCTestCase { - func testLanguagesEffectivelyLoadJsonFile() { - let languages = WordPressComLanguageDatabase() +@Suite("Languages Tests") +class LanguagesTests { + let en = 1 + let es = 19 + let zhCN = 449 + let zhTW = 452 + + @Test func testLanguagesEffectivelyLoadJsonFile() { + let languages = WordPressComLanguageDatabase.shared XCTAssert(languages.all.count != 0) XCTAssert(languages.popular.count != 0) } - func testAllLanguagesHaveValidFields() { - let languages = WordPressComLanguageDatabase() + @Test func testAllLanguagesHaveValidFields() { + let languages = WordPressComLanguageDatabase.shared let sum = languages.all + languages.popular for language in sum { @@ -19,8 +26,8 @@ class LanguagesTests: XCTestCase { } } - func testAllLanguagesContainPopularLanguages() { - let languages = WordPressComLanguageDatabase() + @Test func testAllLanguagesContainPopularLanguages() { + let languages = WordPressComLanguageDatabase.shared for language in languages.popular { let filtered = languages.all.filter { $0.id == language.id } @@ -28,8 +35,8 @@ class LanguagesTests: XCTestCase { } } - func testNameForLanguageWithIdentifierReturnsTheRightName() { - let languages = WordPressComLanguageDatabase() + @Test func testNameForLanguageWithIdentifierReturnsTheRightName() { + let languages = WordPressComLanguageDatabase.shared let english = languages.nameForLanguageWithId(en) let spanish = languages.nameForLanguageWithId(es) @@ -38,79 +45,53 @@ class LanguagesTests: XCTestCase { XCTAssert(spanish == "Español") } - func testDeviceLanguageReturnsValueForSpanish() { - let languages = WordPressComLanguageDatabase() - languages._overrideDeviceLanguageCode("es") - + @Test func testDeviceLanguageReturnsValueForSpanish() { + let languages = WordPressComLanguageDatabase(deviceLanguageCode: "es") XCTAssertEqual(languages.deviceLanguage.id, es) } - func testDeviceLanguageReturnsValueForSpanishSpainLowercase() { - let languages = WordPressComLanguageDatabase() - languages._overrideDeviceLanguageCode("es-es") - + @Test func testDeviceLanguageReturnsValueForSpanishSpainLowercase() { + let languages = WordPressComLanguageDatabase(deviceLanguageCode: "es-es") XCTAssertEqual(languages.deviceLanguage.id, es) } - func testDeviceLanguageReturnsValueForSpanishSpain() { - let languages = WordPressComLanguageDatabase() - languages._overrideDeviceLanguageCode("es-ES") - + @Test func testDeviceLanguageReturnsValueForSpanishSpain() { + let languages = WordPressComLanguageDatabase(deviceLanguageCode: "es-ES") XCTAssertEqual(languages.deviceLanguage.id, es) } - func testDeviceLanguageReturnsEnglishForUnknownLanguage() { - let languages = WordPressComLanguageDatabase() - languages._overrideDeviceLanguageCode("not-a-language") - + @Test func testDeviceLanguageReturnsEnglishForUnknownLanguage() { + let languages = WordPressComLanguageDatabase(deviceLanguageCode: "not-a-language") XCTAssertEqual(languages.deviceLanguage.id, en) } - func testDeviceLanguageReturnsValueForSpanishSpainExtra() { - let languages = WordPressComLanguageDatabase() - languages._overrideDeviceLanguageCode("es-ES-extra") - + @Test func testDeviceLanguageReturnsValueForSpanishSpainExtra() { + let languages = WordPressComLanguageDatabase(deviceLanguageCode: "es-ES-extra") XCTAssertEqual(languages.deviceLanguage.id, es) } - func testDeviceLanguageReturnsValueForSpanishNO() { - let languages = WordPressComLanguageDatabase() - languages._overrideDeviceLanguageCode("es-NO") - + @Test func testDeviceLanguageReturnsValueForSpanishNO() { + let languages = WordPressComLanguageDatabase(deviceLanguageCode: "es-NO") XCTAssertEqual(languages.deviceLanguage.id, es) } - func testDeviceLanguageReturnsZhCNForZhHans() { - let languages = WordPressComLanguageDatabase() - languages._overrideDeviceLanguageCode("zh-Hans") - + @Test func testDeviceLanguageReturnsZhCNForZhHans() { + let languages = WordPressComLanguageDatabase(deviceLanguageCode: "zh-Hans") XCTAssertEqual(languages.deviceLanguage.id, zhCN) } - func testDeviceLanguageReturnsZhTWForZhHant() { - let languages = WordPressComLanguageDatabase() - languages._overrideDeviceLanguageCode("zh-Hant") - + @Test func testDeviceLanguageReturnsZhTWForZhHant() { + let languages = WordPressComLanguageDatabase(deviceLanguageCode: "zh-Hant") XCTAssertEqual(languages.deviceLanguage.id, zhTW) } - func testDeviceLanguageReturnsZhCNForZhHansES() { - let languages = WordPressComLanguageDatabase() - languages._overrideDeviceLanguageCode("zh-Hans-ES") - + @Test func testDeviceLanguageReturnsZhCNForZhHansES() { + let languages = WordPressComLanguageDatabase(deviceLanguageCode: "zh-Hans-ES") XCTAssertEqual(languages.deviceLanguage.id, zhCN) } - func testDeviceLanguageReturnsZhTWForZhHantES() { - let languages = WordPressComLanguageDatabase() - languages._overrideDeviceLanguageCode("zh-Hant-ES") - + @Test func testDeviceLanguageReturnsZhTWForZhHantES() { + let languages = WordPressComLanguageDatabase(deviceLanguageCode: "zh-Hant-ES") XCTAssertEqual(languages.deviceLanguage.id, zhTW) } - - fileprivate let en = 1 - fileprivate let es = 19 - fileprivate let zhCN = 449 - fileprivate let zhTW = 452 - } diff --git a/Modules/Tests/WordPressSharedTests/WordPressShared.xctestplan b/Modules/Tests/WordPressSharedTests/WordPressShared.xctestplan new file mode 100644 index 000000000000..bb65c9976d92 --- /dev/null +++ b/Modules/Tests/WordPressSharedTests/WordPressShared.xctestplan @@ -0,0 +1,25 @@ +{ + "configurations" : [ + { + "id" : "D6F49D5C-3B89-4ED4-BE33-0A53AD094239", + "name" : "Test Scheme Action", + "options" : { + + } + } + ], + "defaultOptions" : { + "codeCoverage" : false, + "performanceAntipatternCheckerEnabled" : true + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:", + "identifier" : "WordPressSharedTests", + "name" : "WordPressSharedTests" + } + } + ], + "version" : 1 +} diff --git a/Tests/WordPressKitTests/CoreAPITests/WordPressComRestApiTests+Locale.swift b/Tests/WordPressKitTests/CoreAPITests/WordPressComRestApiTests+Locale.swift index 77a12077aa62..effbdd1dbb29 100644 --- a/Tests/WordPressKitTests/CoreAPITests/WordPressComRestApiTests+Locale.swift +++ b/Tests/WordPressKitTests/CoreAPITests/WordPressComRestApiTests+Locale.swift @@ -1,7 +1,7 @@ import XCTest import OHHTTPStubs import OHHTTPStubsSwift - +import WordPressShared @testable import WordPressKit extension WordPressComRestApiTests { @@ -16,7 +16,7 @@ extension WordPressComRestApiTests { let api = WordPressComRestApi() let _ = await api.perform(.get, URLString: "/path/path") - let preferredLanguageIdentifier = WordPressComLanguageDatabase().deviceLanguage.slug + let preferredLanguageIdentifier = WordPressComLanguageDatabase.shared.deviceLanguage.slug XCTAssertEqual(request?.url?.query, "locale=\(preferredLanguageIdentifier)") } @@ -35,7 +35,7 @@ extension WordPressComRestApiTests { let api = WordPressComRestApi() let _ = await api.perform(.get, URLString: path, parameters: params) - let preferredLanguageIdentifier = WordPressComLanguageDatabase().deviceLanguage.slug + let preferredLanguageIdentifier = WordPressComLanguageDatabase.shared.deviceLanguage.slug let query = try XCTUnwrap(request?.url?.query?.split(separator: "&")) XCTAssertEqual(Set(query), Set(["locale=\(preferredLanguageIdentifier)", "someKey=value"])) } @@ -90,7 +90,7 @@ extension WordPressComRestApiTests { let api = WordPressComRestApi(localeKey: "foo") - let preferredLanguageIdentifier = WordPressComLanguageDatabase().deviceLanguage.slug + let preferredLanguageIdentifier = WordPressComLanguageDatabase.shared.deviceLanguage.slug let _ = await api.perform(.get, URLString: "/path/path") XCTAssertEqual(request?.url?.query, "foo=\(preferredLanguageIdentifier)") } diff --git a/Tests/WordPressKitTests/WordPressKitTests/Tests/SiteCreationRequestEncodingTests.swift b/Tests/WordPressKitTests/WordPressKitTests/Tests/SiteCreationRequestEncodingTests.swift index d8edc7fee7a3..e0dbc9441887 100644 --- a/Tests/WordPressKitTests/WordPressKitTests/Tests/SiteCreationRequestEncodingTests.swift +++ b/Tests/WordPressKitTests/WordPressKitTests/Tests/SiteCreationRequestEncodingTests.swift @@ -9,7 +9,7 @@ final class SiteCreationRequestEncodingTests: XCTestCase { static let expectedTagline = "This is a site I like" static let expectedBlogName = "Cool Restaurant" static let expectedPublicValue = true - static let expectedLanguageId = "TEST-ENGLISH" + static let expectedLanguageId = 1 static let expectedValidateValue = true static let expectedClientId = "TEST-ID" static let expectedClientSecret = "TEST-SECRET" @@ -246,7 +246,7 @@ extension SiteCreationRequestEncodingTests { XCTAssertNotNil(actualPublicValue) XCTAssertEqual(expectedPublicValue, actualPublicValue!) - let actualLanguageId = jsonDictionary["lang_id"] as? String + let actualLanguageId = jsonDictionary["lang_id"] as? Int XCTAssertNotNil(actualLanguageId) let actualValidateValue = jsonDictionary["validate"] as? Bool diff --git a/Tests/WordPressKitTests/WordPressKitTests/Tests/WordPressComServiceRemoteTests+SiteCreation.swift b/Tests/WordPressKitTests/WordPressKitTests/Tests/WordPressComServiceRemoteTests+SiteCreation.swift index 67f094b07792..94a481127b75 100644 --- a/Tests/WordPressKitTests/WordPressKitTests/Tests/WordPressComServiceRemoteTests+SiteCreation.swift +++ b/Tests/WordPressKitTests/WordPressKitTests/Tests/WordPressComServiceRemoteTests+SiteCreation.swift @@ -20,7 +20,7 @@ final class SiteCreationServiceTests: RemoteTestCase, RESTTestable { tagline: "This is a site I like", siteURLString: expectedUrlString, isPublic: true, - languageIdentifier: "TEST-ENGLISH", + languageIdentifier: 1, shouldValidate: true, clientIdentifier: "TEST-ID", clientSecret: "TEST-SECRET", diff --git a/WordPress.xcworkspace/contents.xcworkspacedata b/WordPress.xcworkspace/contents.xcworkspacedata index e1bc1296b185..36e89c89701f 100644 --- a/WordPress.xcworkspace/contents.xcworkspacedata +++ b/WordPress.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/WordPress/Classes/Utility/Editor/EditorConfiguration+Blog.swift b/WordPress/Classes/Utility/Editor/EditorConfiguration+Blog.swift index 8637422df7b7..e226119311c7 100644 --- a/WordPress/Classes/Utility/Editor/EditorConfiguration+Blog.swift +++ b/WordPress/Classes/Utility/Editor/EditorConfiguration+Blog.swift @@ -45,7 +45,7 @@ extension EditorConfiguration { .setShouldUseThemeStyles(GutenbergSettings().isThemeStylesEnabled(for: blog)) // Limited to Jetpack-connected sites until editor assets endpoint is available in WordPress core .setShouldUsePlugins(Self.shouldEnablePlugins(for: blog, appPassword: applicationPassword)) - .setLocale(WordPressComLanguageDatabase().deviceLanguage.slug) + .setLocale(WordPressComLanguageDatabase.shared.deviceLanguage.slug) if let blogUrl = blog.url { builder = builder.setSiteUrl(blogUrl) diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/SoTW 2023/SOTWCardView.swift b/WordPress/Classes/ViewRelated/Blog/Blog Details/SoTW 2023/SOTWCardView.swift index b4ba2ec7fccd..af05d5e37153 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Details/SoTW 2023/SOTWCardView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Details/SoTW 2023/SOTWCardView.swift @@ -161,7 +161,7 @@ extension BlogDetailsViewController { let cardIsHidden = repository.bool(forKey: SotWConstants.hideCardPreferenceKey) // ensure that the device language is in English. - let usesEnglish = WordPressComLanguageDatabase().deviceLanguageSlugString() == "en" + let usesEnglish = WordPressComLanguageDatabase.shared.deviceLanguage.slug == "en" // ensure that the card is not displayed before Dec. 11, 2023 where the event takes place. let dateComponents = Date().dateAndTimeComponents() diff --git a/WordPress/Classes/ViewRelated/Blog/Site Settings/LanguageSelectorViewController.swift b/WordPress/Classes/ViewRelated/Blog/Site Settings/LanguageSelectorViewController.swift index 164365bcea99..7f8ef8564163 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Settings/LanguageSelectorViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Settings/LanguageSelectorViewController.swift @@ -117,7 +117,7 @@ class LanguageSelectorViewController: UITableViewController, UISearchResultsUpda // MARK: - Private properties - fileprivate typealias Language = WordPressComLanguageDatabase.Language + fileprivate typealias Language = WPComLanguage private let selectedLanguage: Language? @@ -127,7 +127,7 @@ class LanguageSelectorViewController: UITableViewController, UISearchResultsUpda return controller }() - private let database = WordPressComLanguageDatabase() + private let database = WordPressComLanguageDatabase.shared } private struct LanguageSelectorRow: ImmuTableRow { diff --git a/WordPress/Classes/ViewRelated/Blog/Site Settings/LanguageViewController.swift b/WordPress/Classes/ViewRelated/Blog/Site Settings/LanguageViewController.swift index e2913596b941..c765c996bada 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Settings/LanguageViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Settings/LanguageViewController.swift @@ -102,5 +102,5 @@ open class LanguageViewController: UITableViewController, LanguageSelectorDelega // MARK: - Private Properties fileprivate var blog: Blog! - fileprivate let languageDatabase = WordPressComLanguageDatabase() + fileprivate let languageDatabase = WordPressComLanguageDatabase.shared } diff --git a/WordPress/Classes/ViewRelated/Blog/Site Settings/SiteSettingsViewController+Swift.swift b/WordPress/Classes/ViewRelated/Blog/Site Settings/SiteSettingsViewController+Swift.swift index 06039e8865b6..60a1fc770978 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Settings/SiteSettingsViewController+Swift.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Settings/SiteSettingsViewController+Swift.swift @@ -351,7 +351,7 @@ extension SiteSettingsViewController { let name: String if let languageId = blog.settings?.languageID.intValue { - name = WordPressComLanguageDatabase().nameForLanguageWithId(languageId) + name = WordPressComLanguageDatabase.shared.nameForLanguageWithId(languageId) } else { // Since the settings can be nil, we need to handle the scenario... but it // really should not be possible to reach this line. diff --git a/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift b/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift index 822e214e85b0..82963883c31f 100644 --- a/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift @@ -1081,7 +1081,7 @@ extension GutenbergViewController: GutenbergBridgeDataSource { } func gutenbergLocale() -> String? { - return WordPressComLanguageDatabase().deviceLanguage.slug + return WordPressComLanguageDatabase.shared.deviceLanguage.slug } func gutenbergTranslations() -> [String: [String]]? { diff --git a/WordPress/Classes/ViewRelated/Site Creation/Final Assembly/SiteCreationRequest+Validation.swift b/WordPress/Classes/ViewRelated/Site Creation/Final Assembly/SiteCreationRequest+Validation.swift index 1d3c13bc962a..030f8272ae78 100644 --- a/WordPress/Classes/ViewRelated/Site Creation/Final Assembly/SiteCreationRequest+Validation.swift +++ b/WordPress/Classes/ViewRelated/Site Creation/Final Assembly/SiteCreationRequest+Validation.swift @@ -43,7 +43,7 @@ extension SiteCreationRequest { tagline: tagline, siteURLString: siteURLString, isPublic: true, - languageIdentifier: WordPressComLanguageDatabase().deviceLanguageIdNumber().stringValue, + languageIdentifier: WordPressComLanguageDatabase.shared.deviceLanguage.id, shouldValidate: true, clientIdentifier: clientIdentifier, clientSecret: clientSecret,