diff --git a/WordPress/Classes/ViewRelated/Stats/Extensions/Double+Stats.swift b/WordPress/Classes/ViewRelated/Stats/Extensions/Double+Stats.swift index 5b1ea82ed58e..b66ab7fb9f83 100644 --- a/WordPress/Classes/ViewRelated/Stats/Extensions/Double+Stats.swift +++ b/WordPress/Classes/ViewRelated/Stats/Extensions/Double+Stats.swift @@ -143,6 +143,10 @@ extension Double { return formattedString } + func percentageString() -> String { + return NumberFormatter.statsPercentage.string(from: .init(value: self))! + } + private func formatWithCommas() -> String { return numberFormatter.string(for: self) ?? "" } @@ -157,16 +161,28 @@ extension NSNumber { func abbreviatedString(forHeroNumber: Bool = false) -> String { return self.doubleValue.abbreviatedString(forHeroNumber: forHeroNumber) } + + func percentageString() -> String { + return self.doubleValue.percentageString() + } } extension Float { func abbreviatedString(forHeroNumber: Bool = false) -> String { return Double(self).abbreviatedString(forHeroNumber: forHeroNumber) } + + func percentageString(forHeroNumber: Bool = false) -> String { + return Double(self).percentageString() + } } extension Int { func abbreviatedString(forHeroNumber: Bool = false) -> String { return Double(self).abbreviatedString(forHeroNumber: forHeroNumber) } + + func percentageString(forHeroNumber: Bool = false) -> String { + return Double(self).percentageString() + } } diff --git a/WordPress/Classes/ViewRelated/Stats/Extensions/NumberFormatter+Stats.swift b/WordPress/Classes/ViewRelated/Stats/Extensions/NumberFormatter+Stats.swift new file mode 100644 index 000000000000..956eba6bb627 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Stats/Extensions/NumberFormatter+Stats.swift @@ -0,0 +1,18 @@ +import Foundation + +extension NumberFormatter { + static let statsPercentage: NumberFormatter = { + let formatter = NumberFormatter() + formatter.numberStyle = .percent + formatter.multiplier = 1 + formatter.minimumFractionDigits = 0 + formatter.maximumFractionDigits = 2 + if let preferredLocaleIdentifier = Bundle.main.preferredLocalizations.first { + formatter.locale = Locale(identifier: preferredLocaleIdentifier) + } else { + formatter.locale = Locale.current + } + + return formatter + }() +} diff --git a/WordPress/Classes/ViewRelated/Stats/Insights/SiteStatsInsightsViewModel.swift b/WordPress/Classes/ViewRelated/Stats/Insights/SiteStatsInsightsViewModel.swift index 717439c0e4b7..144dfdb4435d 100644 --- a/WordPress/Classes/ViewRelated/Stats/Insights/SiteStatsInsightsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Stats/Insights/SiteStatsInsightsViewModel.swift @@ -452,8 +452,8 @@ private extension SiteStatsInsightsViewModel { static let bestDay = NSLocalizedString("Best Day", comment: "'Best Day' label for Most Popular stat.") static let bestHour = NSLocalizedString("Best Hour", comment: "'Best Hour' label for Most Popular stat.") static let viewPercentage = NSLocalizedString( - "stats.insights.mostPopularCard.viewPercentage", - value: "%d%% of views", + "stats.insights.mostPopularCard.viewsNumber", + value: "%1$@ of views", comment: "Label showing the percentage of views to a user's site which fall on a particular day." ) } @@ -523,8 +523,8 @@ private extension SiteStatsInsightsViewModel { return nil } - let dayPercentage = String(format: MostPopularStats.viewPercentage, mostPopularStats.mostPopularDayOfWeekPercentage) - let hourPercentage = String(format: MostPopularStats.viewPercentage, mostPopularStats.mostPopularHourPercentage) + let dayPercentage = String(format: MostPopularStats.viewPercentage, mostPopularStats.mostPopularDayOfWeekPercentage.percentageString()) + let hourPercentage = String(format: MostPopularStats.viewPercentage, mostPopularStats.mostPopularHourPercentage.percentageString()) return StatsMostPopularTimeData(mostPopularDayTitle: MostPopularStats.bestDay, mostPopularTimeTitle: MostPopularStats.bestHour, mostPopularDay: dayString, mostPopularTime: timeString.uppercased(), dayPercentage: dayPercentage, timePercentage: hourPercentage) } diff --git a/WordPress/Classes/ViewRelated/Stats/Insights/StatsTotalInsightsCell.swift b/WordPress/Classes/ViewRelated/Stats/Insights/StatsTotalInsightsCell.swift index 7b3ffb3f3bc7..299386b84797 100644 --- a/WordPress/Classes/ViewRelated/Stats/Insights/StatsTotalInsightsCell.swift +++ b/WordPress/Classes/ViewRelated/Stats/Insights/StatsTotalInsightsCell.swift @@ -304,9 +304,9 @@ class StatsTotalInsightsCell: StatsBaseCell { let differenceText: String = { if difference > 0 { - return String(format: TextContent.differenceHigher, differencePrefix, difference.abbreviatedString(), percentage.abbreviatedString()) + return String(format: TextContent.differenceHigher, differencePrefix, difference.abbreviatedString(), percentage.percentageString()) } else if difference < 0 { - return String(format: TextContent.differenceLower, differencePrefix, difference.abbreviatedString(), percentage.abbreviatedString()) + return String(format: TextContent.differenceLower, differencePrefix, difference.abbreviatedString(), percentage.percentageString()) } else { return TextContent.differenceSame } @@ -397,11 +397,11 @@ class StatsTotalInsightsCell: StatsBaseCell { private enum TextContent { static let differenceDelimiter = Character("*") - static let differenceHigher = NSLocalizedString("stats.insights.label.totalLikes.higher", - value: "*%@%@ (%@%%)* higher than the previous 7-days", + static let differenceHigher = NSLocalizedString("stats.insights.label.totalLikes.higherNumber", + value: "*%1$@%2$@ (%3$@)* higher than the previous 7-days", comment: "Label shown on some metrics in the Stats Insights section, such as Comments count. The placeholders will be populated with a change and a percentage – e.g. '+17 (40%) higher than the previous 7-days'. The *s mark the numerical values, which will be highlighted differently from the rest of the text.") - static let differenceLower = NSLocalizedString("stats.insights.label.totalLikes.lower", - value: "*%@%@ (%@%%)* lower than the previous 7-days", + static let differenceLower = NSLocalizedString("stats.insights.label.totalLikes.lowerNumber", + value: "*%1$@%2$@ (%3$@)* lower than the previous 7-days", comment: "Label shown on some metrics in the Stats Insights section, such as Comments count. The placeholders will be populated with a change and a percentage – e.g. '-17 (40%) lower than the previous 7-days'. The *s mark the numerical values, which will be highlighted differently from the rest of the text.") static let differenceSame = NSLocalizedString("stats.insights.label.totalLikes.same", value: "The same as the previous 7-days", diff --git a/WordPress/Classes/ViewRelated/Stats/Insights/ViewsVisitors/ViewsVisitorsChartMarker.swift b/WordPress/Classes/ViewRelated/Stats/Insights/ViewsVisitors/ViewsVisitorsChartMarker.swift index e588c5a1af96..54f265c35786 100644 --- a/WordPress/Classes/ViewRelated/Stats/Insights/ViewsVisitors/ViewsVisitorsChartMarker.swift +++ b/WordPress/Classes/ViewRelated/Stats/Insights/ViewsVisitors/ViewsVisitorsChartMarker.swift @@ -265,7 +265,7 @@ final class ViewsVisitorsChartMarker: MarkerView { .paragraphStyle: paragraphStyle, .foregroundColor: UIColor.white] - let topRowStr = NSMutableAttributedString(string: "\(differenceStr) (\(roundedPercentage)%)\n", attributes: topRowAttributes) + let topRowStr = NSMutableAttributedString(string: "\(differenceStr) (\(roundedPercentage.percentageString()))\n", attributes: topRowAttributes) let bottomRowStr = NSAttributedString(string: "\(yValue) \(name)", attributes: bottomRowAttributes) topRowStr.append(bottomRowStr) diff --git a/WordPress/Classes/ViewRelated/Stats/Insights/ViewsVisitors/ViewsVisitorsLineChartCell.swift b/WordPress/Classes/ViewRelated/Stats/Insights/ViewsVisitors/ViewsVisitorsLineChartCell.swift index 1b87cfb7e0d0..a345b42db4fd 100644 --- a/WordPress/Classes/ViewRelated/Stats/Insights/ViewsVisitors/ViewsVisitorsLineChartCell.swift +++ b/WordPress/Classes/ViewRelated/Stats/Insights/ViewsVisitors/ViewsVisitorsLineChartCell.swift @@ -69,15 +69,15 @@ struct StatsSegmentedControlData { if differencePercent != 0 { let stringFormat = NSLocalizedString( - "insights.visitorsLineChartCell.differenceLabelWithPercentage", - value: "%1$@%2$@ (%3$@%%)", - comment: "Text for the Insights Overview stat difference label. Shows the change from the previous period, including the percentage value. E.g.: +12.3K (5%). %1$@ is the placeholder for the change sign ('-', '+', or none). %2$@ is the placeholder for the change numerical value. %3$@ is the placeholder for the change percentage value, excluding the % sign." + "insights.visitorsLineChartCell.differenceLabelWithNumber", + value: "%1$@%2$@ (%3$@)", + comment: "Text for the Insights Overview stat difference label. Shows the change from the previous period, including the percentage value. E.g.: +12.3K (5%). %1$@ is the placeholder for the change sign ('-', '+', or none). %2$@ is the placeholder for the change numerical value. %3$@ is the placeholder for the change percentage value." ) return String.localizedStringWithFormat( stringFormat, plusSign, difference.abbreviatedString(), - differencePercent.abbreviatedString() + differencePercent.percentageString() ) } else { let stringFormat = NSLocalizedString( diff --git a/WordPress/Classes/ViewRelated/Stats/Period Stats/Overview/OverviewCell.swift b/WordPress/Classes/ViewRelated/Stats/Period Stats/Overview/OverviewCell.swift index 403a9079b358..6cea18a78e6a 100644 --- a/WordPress/Classes/ViewRelated/Stats/Period Stats/Overview/OverviewCell.swift +++ b/WordPress/Classes/ViewRelated/Stats/Period Stats/Overview/OverviewCell.swift @@ -56,11 +56,16 @@ struct OverviewTabData: FilterTabBarItem, Hashable { } var differenceLabel: String { - let stringFormat = NSLocalizedString("%@%@ (%@%%)", comment: "Difference label for Period Overview stat, indicating change from previous period. Ex: +99.9K (5%)") + let stringFormat = NSLocalizedString( + "stats.overview.differenceLabelWithNumber", + value: "%1$@%2$@ (%3$@)", + comment: "Text for the Stats Traffic Overview stat difference label. Shows the change from the previous period, including the percentage value. E.g.: +12.3K (5%). %1$@ is the placeholder for the change sign ('-', '+', or none). %2$@ is the placeholder for the change numerical value. %3$@ is the placeholder for the change percentage value." + ) + return String.localizedStringWithFormat(stringFormat, difference < 0 ? "" : "+", difference.abbreviatedString(), - differencePercent.abbreviatedString()) + differencePercent.percentageString()) } var differenceTextColor: UIColor { diff --git a/WordPress/Classes/ViewRelated/Stats/Traffic/Chart/StatsTrafficBarChartCell.swift b/WordPress/Classes/ViewRelated/Stats/Traffic/Chart/StatsTrafficBarChartCell.swift index d403870d3ac7..57b65064c4c6 100644 --- a/WordPress/Classes/ViewRelated/Stats/Traffic/Chart/StatsTrafficBarChartCell.swift +++ b/WordPress/Classes/ViewRelated/Stats/Traffic/Chart/StatsTrafficBarChartCell.swift @@ -53,7 +53,6 @@ final class StatsTrafficBarChartCell: UITableViewCell { self.unit = unit self.siteStatsPeriodDelegate = siteStatsPeriodDelegate - updateLabels() updateButtons() updateChartView() } @@ -61,16 +60,10 @@ final class StatsTrafficBarChartCell: UITableViewCell { private extension StatsTrafficBarChartCell { @objc func selectedFilterDidChange(_ filterBar: FilterTabBar) { - updateLabels() updateChartView() siteStatsPeriodDelegate?.barChartTabSelected?(filterBar.selectedIndex) } - func updateLabels() { - let tabData = tabsData[filterTabBar.selectedIndex] - differenceLabel.attributedText = differenceAttributedString(tabData) - } - func updateButtons() { let font = tabsFont(for: tabsData) filterTabBar.tabsFont = font @@ -165,121 +158,6 @@ private extension StatsTrafficBarChartCell { } } -// MARK: - Difference - -private extension StatsTrafficBarChartCell { - enum DifferenceStrings { - static let weekHigher = NSLocalizedString("stats.traffic.label.weekDifference.higher", - value: "%@ higher than the previous 7-days\n", - comment: "Stats views higher than previous 7 days") - static let weekLower = NSLocalizedString("stats.traffic.label.weekDifference.lower", - value: "%@ lower than the previous 7-days\n", - comment: "Stats views lower than previous 7 days") - - static let monthHigher = NSLocalizedString("stats.traffic.label.monthDifference.higher", - value: "%@ higher than the previous month\n", - comment: "Stats views higher than previous month") - static let monthLower = NSLocalizedString("stats.traffic.label.monthDifference.lower", - value: "%@ lower than the previous month\n", - comment: "Stats views lower than previous month") - - static let yearHigher = NSLocalizedString("stats.traffic.label.yearDifference.higher", - value: "%@ higher than the previous year\n", - comment: "Stats views higher than previous year") - static let yearLower = NSLocalizedString("stats.traffic.label.yearDifference.lower", - value: "%@ lower than the previous year\n", - comment: "Stats views lower than previous year") - } - - func differenceAttributedString(_ data: StatsTrafficBarChartTabData) -> NSAttributedString? { - guard let differenceText = differenceText(data) else { - return nil - } - - let defaultAttributes = [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .footnote), NSAttributedString.Key.foregroundColor: UIColor.DS.Foreground.secondary] - let differenceColor = data.difference > 0 ? UIColor.DS.Foreground.success : UIColor.DS.Foreground.error - let differenceLabel = differenceLabel(data) - let attributedString = NSMutableAttributedString( - string: String(format: differenceText, differenceLabel), - attributes: defaultAttributes - ) - - let str = attributedString.string as NSString - let range = str.range(of: differenceLabel) - - attributedString.addAttributes( - [.foregroundColor: differenceColor, - .font: UIFont.preferredFont(forTextStyle: .footnote) - ], - range: NSRange(location: range.location, length: differenceLabel.count) - ) - - return attributedString - } - - func differenceText(_ data: StatsTrafficBarChartTabData) -> String? { - switch data.period { - case .week: - if data.difference > 0 { - return DifferenceStrings.weekHigher - } else if data.difference < 0 { - return DifferenceStrings.weekLower - } - case .month: - if data.difference > 0 { - return DifferenceStrings.monthHigher - } else if data.difference < 0 { - return DifferenceStrings.monthLower - } - case .year: - if data.difference > 0 { - return DifferenceStrings.yearHigher - } else if data.difference < 0 { - return DifferenceStrings.yearLower - } - default: - return nil - } - - return nil - } - - func differenceLabel(_ data: StatsTrafficBarChartTabData) -> String { - // We want to show something like "+10.2K (+5%)" if we have a percentage difference and "1.2K" if we don't. - // - // Negative cases automatically appear with a negative sign "-10.2K (-5%)" by using `abbreviatedString()`. - // `abbreviatedString()` also handles formatting big numbers, i.e. 10,200 will become 10.2K. - let formatter = NumberFormatter() - formatter.locale = .current - let plusSign = data.difference <= 0 ? "" : "\(formatter.plusSign ?? "")" - - if data.differencePercent != 0 { - let stringFormat = NSLocalizedString( - "stats.traffic.differenceLabelWithPercentage", - value: "%1$@%2$@ (%3$@%%)", - comment: "Text for the Stats Traffic Overview stat difference label. Shows the change from the previous period, including the percentage value. E.g.: +12.3K (5%). %1$@ is the placeholder for the change sign ('-', '+', or none). %2$@ is the placeholder for the change numerical value. %3$@ is the placeholder for the change percentage value, excluding the % sign." - ) - return String.localizedStringWithFormat( - stringFormat, - plusSign, - data.difference.abbreviatedString(), - data.differencePercent.abbreviatedString() - ) - } else { - let stringFormat = NSLocalizedString( - "stats.traffic.differenceLabelWithoutPercentage", - value: "%1$@%2$@", - comment: "Text for the Stats Traffic Overview stat difference label. Shows the change from the previous period. E.g.: +12.3K. %1$@ is the placeholder for the change sign ('-', '+', or none). %2$@ is the placeholder for the change numerical value." - ) - return String.localizedStringWithFormat( - stringFormat, - plusSign, - data.difference.abbreviatedString() - ) - } - } -} - struct StatsTrafficBarChartTabData: FilterTabBarItem, Equatable { var tabTitle: String var tabData: Int diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index 23af6c31bf32..8445a624b5bd 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -250,6 +250,9 @@ 01E2580B2ACDC72C00F09666 /* PlanWizardContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01E2580A2ACDC72C00F09666 /* PlanWizardContentViewModel.swift */; }; 01E2580C2ACDC72C00F09666 /* PlanWizardContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01E2580A2ACDC72C00F09666 /* PlanWizardContentViewModel.swift */; }; 01E2580E2ACDC88100F09666 /* PlanWizardContentViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01E2580D2ACDC88100F09666 /* PlanWizardContentViewModelTests.swift */; }; + 01E70EBB2BB5CCCF000BFE45 /* NumberFormatter+Stats.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01E70EBA2BB5CCCF000BFE45 /* NumberFormatter+Stats.swift */; }; + 01E70EBC2BB5CCCF000BFE45 /* NumberFormatter+Stats.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01E70EBA2BB5CCCF000BFE45 /* NumberFormatter+Stats.swift */; }; + 01E70EBD2BB5D035000BFE45 /* NumberFormatter+Stats.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01E70EBA2BB5CCCF000BFE45 /* NumberFormatter+Stats.swift */; }; 01E78D1D296EA54F00FB6863 /* StatsPeriodHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01E78D1C296EA54F00FB6863 /* StatsPeriodHelperTests.swift */; }; 02761EC02270072F009BAF0F /* BlogDetailsViewController+SectionHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02761EBF2270072F009BAF0F /* BlogDetailsViewController+SectionHelpers.swift */; }; 02761EC222700A9C009BAF0F /* BlogDetailsSubsectionToSectionCategoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02761EC122700A9C009BAF0F /* BlogDetailsSubsectionToSectionCategoryTests.swift */; }; @@ -6017,6 +6020,7 @@ 01E258082ACC3AA000F09666 /* iOS17WidgetAPIs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOS17WidgetAPIs.swift; sourceTree = ""; }; 01E2580A2ACDC72C00F09666 /* PlanWizardContentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlanWizardContentViewModel.swift; sourceTree = ""; }; 01E2580D2ACDC88100F09666 /* PlanWizardContentViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlanWizardContentViewModelTests.swift; sourceTree = ""; }; + 01E70EBA2BB5CCCF000BFE45 /* NumberFormatter+Stats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NumberFormatter+Stats.swift"; sourceTree = ""; }; 01E78D1C296EA54F00FB6863 /* StatsPeriodHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsPeriodHelperTests.swift; sourceTree = ""; }; 02761EBF2270072F009BAF0F /* BlogDetailsViewController+SectionHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BlogDetailsViewController+SectionHelpers.swift"; sourceTree = ""; }; 02761EC122700A9C009BAF0F /* BlogDetailsSubsectionToSectionCategoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogDetailsSubsectionToSectionCategoryTests.swift; sourceTree = ""; }; @@ -14709,6 +14713,7 @@ 98487E3921EE8FB500352B4E /* UITableViewCell+Stats.swift */, FAB9826D2697038700B172A3 /* StatsViewController+JetpackSettings.swift */, FAB985C02697550C00B172A3 /* NoResultsViewController+StatsModule.swift */, + 01E70EBA2BB5CCCF000BFE45 /* NumberFormatter+Stats.swift */, ); path = Extensions; sourceTree = ""; @@ -21325,6 +21330,7 @@ 0107E0D028F97D5000DE87DB /* HomeWidgetThisWeek.swift in Sources */, C9FE384729C2A3D200D39841 /* LockScreenStatsWidgetConfig.swift in Sources */, C9B477BA29CD2FEF008CBF49 /* LockScreenUnconfiguredViewModel.swift in Sources */, + 01E70EBD2BB5D035000BFE45 /* NumberFormatter+Stats.swift in Sources */, 0107E0D128F97D5000DE87DB /* SingleStatView.swift in Sources */, 0188FE4C2AA62F800093EDA5 /* LockScreenTodayLikesCommentsStatWidgetConfig.swift in Sources */, 0107E0D228F97D5000DE87DB /* UnconfiguredView.swift in Sources */, @@ -22044,6 +22050,7 @@ 98B52AE121F7AF4A006FF6B4 /* StatsDataHelper.swift in Sources */, 8BAD272C241FEF3300E9D105 /* PrepublishingViewController.swift in Sources */, 4A1E77C92988997C006281CC /* PublicizeConnection+Creation.swift in Sources */, + 01E70EBB2BB5CCCF000BFE45 /* NumberFormatter+Stats.swift in Sources */, F10D634F26F0B78E00E46CC7 /* Blog+Organization.swift in Sources */, DCCDF75B283BEFEA00AA347E /* SiteStatsInsightsDetailsTableViewController.swift in Sources */, 80A2154029CA68D5002FE8EB /* RemoteFeatureFlag.swift in Sources */, @@ -25417,6 +25424,7 @@ FABB24182602FC2C00C8785C /* WKWebView+UserAgent.swift in Sources */, FABB24192602FC2C00C8785C /* JetpackCapabilitiesService.swift in Sources */, FABB241A2602FC2C00C8785C /* PostToPost30To31.m in Sources */, + 01E70EBC2BB5CCCF000BFE45 /* NumberFormatter+Stats.swift in Sources */, FABB241B2602FC2C00C8785C /* GutenGhostView.swift in Sources */, FABB241C2602FC2C00C8785C /* ModelSettableCell.swift in Sources */, FABB241D2602FC2C00C8785C /* FollowCommentsService.swift in Sources */,