diff --git a/ios/brave-ios/App/iOS/Delegates/AppDelegate.swift b/ios/brave-ios/App/iOS/Delegates/AppDelegate.swift index 04ed23e80980..6e0bde05aa58 100644 --- a/ios/brave-ios/App/iOS/Delegates/AppDelegate.swift +++ b/ios/brave-ios/App/iOS/Delegates/AppDelegate.swift @@ -271,6 +271,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { ) } + LanguageMetrics.recordPrimaryLanguageP3A() + Task(priority: .low) { await self.cleanUpLargeTemporaryDirectory() } diff --git a/ios/brave-ios/Package.swift b/ios/brave-ios/Package.swift index 08ff22c1f4c8..808c6e89857b 100644 --- a/ios/brave-ios/Package.swift +++ b/ios/brave-ios/Package.swift @@ -107,6 +107,7 @@ var package = Package( name: "Growth", dependencies: [ "BraveVPN", "Shared", "BraveShared", "Strings", "SnapKit", "CertificateUtilities", + .product(name: "OrderedCollections", package: "swift-collections"), ], plugins: ["LoggerPlugin"] ), diff --git a/ios/brave-ios/Sources/Growth/LanguageMetrics.swift b/ios/brave-ios/Sources/Growth/LanguageMetrics.swift new file mode 100644 index 000000000000..127c9f45155a --- /dev/null +++ b/ios/brave-ios/Sources/Growth/LanguageMetrics.swift @@ -0,0 +1,225 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +import BraveCore +import Foundation +import OrderedCollections + +public class LanguageMetrics { + + // This list is copied from //brave/components/misc_metrics/language_metrics.cc + static let acceptedLanguages: OrderedSet = [ + "aa", // Afar + "ab", // Abkhazian + "ae", // Avestan + "af", // Afrikaans + "ak", // Akan + "am", // Amharic + "an", // Aragonese + "ar", // Arabic + "as", // Assamese + "av", // Avaric + "ay", // Aymara + "az", // Azerbaijani + "ba", // Bashkir + "be", // Belarusian + "bg", // Bulgarian + "bh", // Bihari languages + "bi", // Bislama + "bm", // Bambara + "bn", // Bengali + "bo", // Tibetan + "br", // Breton + "bs", // Bosnian + "ca", // Catalan; Valencian + "ce", // Chechen + "ch", // Chamorro + "co", // Corsican + "cr", // Cree + "cs", // Czech + "cu", // Church Slavic; Old Slavonic; Church Slavonic; Old Bulgarian; // Old Church Slavonic + "cv", // Chuvash + "cy", // Welsh + "da", // Danish + "de", // German + "dv", // Divehi; Dhivehi; Maldivian + "dz", // Dzongkha + "ee", // Ewe + "el", // "Greek, Modern (1453-)" + "en", // English + "eo", // Esperanto + "es", // Spanish; Castilian + "et", // Estonian + "eu", // Basque + "fa", // Persian + "ff", // Fulah + "fi", // Finnish + "fj", // Fijian + "fo", // Faroese + "fr", // French + "fy", // Western Frisian + "ga", // Irish + "gd", // Gaelic; Scottish Gaelic + "gl", // Galician + "gn", // Guarani + "gu", // Gujarati + "gv", // Manx + "ha", // Hausa + "he", // Hebrew + "hi", // Hindi + "ho", // Hiri Motu + "hr", // Croatian + "ht", // Haitian; Haitian Creole + "hu", // Hungarian + "hy", // Armenian + "hz", // Herero + "ia", // Interlingua (International Auxiliary Language Association) + "id", // Indonesian + "ie", // Interlingue; Occidental + "ig", // Igbo + "ii", // Sichuan Yi; Nuosu + "ik", // Inupiaq + "io", // Ido + "is", // Icelandic + "it", // Italian + "iu", // Inuktitut + "ja", // Japanese + "jv", // Javanese + "ka", // Georgian + "kg", // Kongo + "ki", // Kikuyu; Gikuyu + "kj", // Kuanyama; Kwanyama + "kk", // Kazakh + "kl", // Kalaallisut; Greenlandic + "km", // Central Khmer + "kn", // Kannada + "ko", // Korean + "kr", // Kanuri + "ks", // Kashmiri + "ku", // Kurdish + "kv", // Komi + "kw", // Cornish + "ky", // Kirghiz; Kyrgyz + "la", // Latin + "lb", // Luxembourgish; Letzeburgesch + "lg", // Ganda + "li", // Limburgan; Limburger; Limburgish + "ln", // Lingala + "lo", // Lao + "lt", // Lithuanian + "lu", // Luba-Katanga + "lv", // Latvian + "mg", // Malagasy + "mh", // Marshallese + "mi", // Maori + "mk", // Macedonian + "ml", // Malayalam + "mn", // Mongolian + "mr", // Marathi + "ms", // Malay + "mt", // Maltese + "my", // Burmese + "na", // Nauru + "nb", // "Bokmål, Norwegian; Norwegian Bokmål" + "nd", // "Ndebele, North; North Ndebele" + "ne", // Nepali + "ng", // Ndonga + "nl", // Dutch; Flemish + "nn", // "Norwegian Nynorsk; Nynorsk, Norwegian" + "no", // Norwegian + "nr", // "Ndebele, South; South Ndebele" + "nv", // Navajo; Navaho + "ny", // Chichewa; Chewa; Nyanja + "oc", // Occitan (post 1500) + "oj", // Ojibwa + "om", // Oromo + "or", // Oriya + "os", // Ossetian; Ossetic + "pa", // Panjabi; Punjabi + "pi", // Pali + "pl", // Polish + "ps", // Pushto; Pashto + "pt", // Portuguese + "qu", // Quechua + "rm", // Romansh + "rn", // Rundi + "ro", // Romanian; Moldavian; Moldovan + "ru", // Russian + "rw", // Kinyarwanda + "sa", // Sanskrit + "sc", // Sardinian + "sd", // Sindhi + "se", // Northern Sami + "sg", // Sango + "si", // Sinhala; Sinhalese + "sk", // Slovak + "sl", // Slovenian + "sm", // Samoan + "sn", // Shona + "so", // Somali + "sq", // Albanian + "sr", // Serbian + "ss", // Swati + "st", // "Sotho, Southern" + "su", // Sundanese + "sv", // Swedish + "sw", // Swahili + "ta", // Tamil + "te", // Telugu + "tg", // Tajik + "th", // Thai + "ti", // Tigrinya + "tk", // Turkmen + "tl", // Tagalog + "tn", // Tswana + "to", // Tonga (Tonga Islands) + "tr", // Turkish + "ts", // Tsonga + "tt", // Tatar + "tw", // Twi + "ty", // Tahitian + "ug", // Uighur; Uyghur + "uk", // Ukrainian + "ur", // Urdu + "uz", // Uzbek + "ve", // Venda + "vi", // Vietnamese + "vo", // Volapük + "wa", // Walloon + "wo", // Wolof + "xh", // Xhosa + "yi", // Yiddish + "yo", // Yoruba + "za", // Zhuang; Chuang + "zh", // Chinese + "zu", // Zulu + ] + + static let languageSynonyms: [String: String] = [ + "in": "id", + "iw": "he", + "ji": "yi", + "jw": "jv", + "mo": "ro", + ] + + static func answerForLangaugeCode(_ languageCode: String?) -> Int { + let suspendedMetricValue = Int.max - 1 + guard let languageCode = languageCode, + case let primaryLanguage = languageSynonyms[languageCode, default: languageCode], + let answer = acceptedLanguages.firstIndex(of: primaryLanguage) + else { + // Suspend metric if primary language not detected or language isn't accepted + return suspendedMetricValue + } + return answer + } + + public static func recordPrimaryLanguageP3A() { + let primaryLanguageHistogramName = "Brave.Core.PrimaryLang" + let answer = answerForLangaugeCode(Locale.current.languageCode) + UmaHistogramExactLinear(primaryLanguageHistogramName, answer, acceptedLanguages.count) + } +} diff --git a/ios/brave-ios/Tests/GrowthTests/LangaugeMetricsTests.swift b/ios/brave-ios/Tests/GrowthTests/LangaugeMetricsTests.swift new file mode 100644 index 000000000000..58826f9b8b04 --- /dev/null +++ b/ios/brave-ios/Tests/GrowthTests/LangaugeMetricsTests.swift @@ -0,0 +1,29 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +import Foundation +import XCTest + +@testable import Growth + +class LanguageMetricsTests: XCTestCase { + func testInvalidLanguageCodes() { + let expectedValue = Int.max - 1 + XCTAssertEqual(LanguageMetrics.answerForLangaugeCode(nil), expectedValue) + XCTAssertEqual(LanguageMetrics.answerForLangaugeCode("notalangugage"), expectedValue) + } + + func testValidLanguageCodes() { + for (index, languageCode) in LanguageMetrics.acceptedLanguages.enumerated() { + XCTAssertEqual(LanguageMetrics.answerForLangaugeCode(languageCode), index) + } + } + + func testLanguageSynonyms() { + let (deprecated, active) = ("in", "id") + let expectedAnswer = LanguageMetrics.answerForLangaugeCode(active) + XCTAssertEqual(LanguageMetrics.answerForLangaugeCode(deprecated), expectedAnswer) + } +}