From 624ee6359d8ee45f7dc26c707fde06f237af32f5 Mon Sep 17 00:00:00 2001 From: Max Radermacher Date: Fri, 3 Apr 2026 16:20:31 -0500 Subject: [PATCH] Add memory safe localized string method --- Signal.xcodeproj/project.pbxproj | 4 ++ Signal/AppLaunch/AppDelegate.swift | 4 +- Signal/AppLaunch/LoadingViewController.swift | 4 +- Signal/Backups/BackupEnablingManager.swift | 2 +- .../BackupSettingsViewController.swift | 50 +++++++++---------- .../ChooseBackupPlanViewController.swift | 12 ++--- .../Calls/UserInterface/CallDrawerSheet.swift | 6 +-- Signal/Calls/UserInterface/CallHeader.swift | 12 ++--- .../CallMemberWaitingAndErrorView.swift | 8 +-- .../CallsListViewController.swift | 8 +-- .../GroupCallNotificationView.swift | 4 +- ...oupCallVideoContextMenuConfiguration.swift | 4 +- .../IndividualCallViewController.swift | 4 +- .../UserInterface/RaisedHandsToast.swift | 10 ++-- .../Calls/UserInterface/RemoteMuteToast.swift | 16 +++--- .../BlockingAnnouncementOnlyView.swift | 2 +- .../CellViews/CVMediaAlbumView.swift | 4 +- .../CellViews/CVQuotedMessageView.swift | 4 +- .../CellViews/GiftBadgeView.swift | 2 +- .../CVComponentArchivedPayment.swift | 8 +-- .../Components/CVComponentBodyText.swift | 2 +- .../Components/CVComponentContactShare.swift | 2 +- .../Components/CVComponentMessage.swift | 2 +- .../CVComponentPaymentAttachment.swift | 8 +-- .../Components/CVComponentPoll.swift | 4 +- .../Components/CVComponentQuotedReply.swift | 6 +-- .../Components/CVComponentSticker.swift | 4 +- .../Components/CVComponentSystemMessage.swift | 4 +- .../Components/CVComponentThreadDetails.swift | 35 +++++++------ ...ationInputToolbar+QuotedReplyPreview.swift | 4 +- .../ConversationViewController+Banners.swift | 2 +- ...ConversationViewController+BottomBar.swift | 8 +-- ...onViewController+CVComponentDelegate.swift | 36 ++++++------- ...rsationViewController+MessageRequest.swift | 14 +++--- .../ConversationViewController+OWS.swift | 2 +- .../Reactions/EmojiCountsCollectionView.swift | 4 +- .../BackupsEnabledNotificationMegaphone.swift | 4 +- ...NewLinkedDeviceNotificationMegaphone.swift | 4 +- ...ncProvisioningProgressViewController.swift | 8 +-- ...iceRestoreBackupPromptViewController.swift | 4 +- ...honeNumberConfirmationViewController.swift | 4 +- ...ationChangePhoneNumberViewController.swift | 2 +- .../RegistrationLoadingViewController.swift | 2 +- ...eNumberDiscoverabilityViewController.swift | 2 +- .../RegistrationPhoneNumberViewState.swift | 2 +- ...strationReglockTimeoutViewController.swift | 4 +- ...FromBackupConfirmationViewController.swift | 4 +- ...gistrationVerificationViewController.swift | 6 +-- .../UserInterface/RegistrationViewUtil.swift | 2 +- ...eteAccountConfirmationViewController.swift | 2 +- .../ComposeSupportEmailOperation.swift | 24 ++++----- ...dgeGiftingConfirmationViewController.swift | 2 +- ...tionSettingsViewController+MySupport.swift | 12 ++--- .../DonationSettingsViewController.swift | 4 +- .../BackupRestoreProgressModal.swift | 8 +-- .../LinkedDevicesEducationSheet.swift | 4 +- .../Linked Devices/LinkedDevicesView.swift | 8 +-- ...ificationSettingsSoundViewController.swift | 4 +- .../PaymentsDetailViewController.swift | 8 +-- ...mentsRestoreWalletWordViewController.swift | 6 +-- .../PaymentsSettingsViewController.swift | 2 +- ...sViewPassphraseConfirmViewController.swift | 6 +-- .../Profile/ProfileBioViewController.swift | 2 +- .../ProfileSettingsViewController.swift | 4 +- .../DatabaseRecoveryViewController.swift | 2 +- .../Donations/BadgeDetailsSheet.swift | 2 +- .../BadgeGiftingAlreadyRedeemedSheet.swift | 4 +- .../Donations/BadgeGiftingThanksSheet.swift | 2 +- .../Donations/BadgeIssueSheet.swift | 10 ++-- .../Donations/BadgeThanksSheet.swift | 6 +-- .../DonateChoosePaymentMethodSheet.swift | 14 +++--- ...troller+MonthlySubscriptionLevelView.swift | 6 +-- .../Donations/DonateViewController.swift | 10 ++-- ...DonationPaymentDetailsViewController.swift | 4 +- .../Donations/DonationViewsUtil.swift | 8 +-- .../HomeView/Chat List/ChatListCell.swift | 2 +- ...ontroller+BackupDownloadProgressView.swift | 16 +++--- ...wController+BackupExportProgressView.swift | 4 +- .../ChatListViewController+Reminders.swift | 4 +- .../Chat List/ChatListViewController.swift | 8 +-- .../ConversationSearchViewController.swift | 2 +- .../StoryContextMenuGenerator.swift | 4 +- .../StoryReplyInputToolbar.swift | 2 +- .../NewGroupConfirmViewController.swift | 2 +- .../SendPaymentCompletionActionSheet.swift | 6 +-- .../Payments/SendPaymentHelper.swift | 8 +-- .../SendPaymentMemoViewController.swift | 4 +- .../Payments/SendPaymentViewController.swift | 6 +-- .../Photos/MediaControls.swift | 2 +- .../Photos/PhotoCaptureViewController.swift | 8 +-- .../Polls/NewPollViewController2.swift | 4 +- .../AddToGroupViewController.swift | 4 +- .../ThreadSettings/ContactAboutSheet.swift | 4 +- ...nSettingsViewController+LegacyGroups.swift | 2 +- .../ConversationSettingsViewController.swift | 6 +-- .../GroupDescriptionViewController.swift | 2 +- ...mberRequestsAndInvitesViewController.swift | 12 ++--- .../GroupViewHelper+MemberActionSheet.swift | 2 +- ...dNotificationsSettingsViewController.swift | 4 +- .../PreviewWallpaperViewController.swift | 2 +- Signal/src/views/ExpirationNagView.swift | 12 ++--- .../src/views/NameCollisionReviewCell.swift | 4 +- SignalNSE/NotificationService.swift | 2 +- .../Calls/Group/OWSGroupCallMessage.swift | 8 +-- ...earingConfigurationUpdateInfoMessage.swift | 8 +-- .../TSInfoMessage+PinnedMessage.swift | 2 +- .../Polls/TSInfoMessage+Polls.swift | 4 +- .../Quotes/DraftQuotedReplyModel.swift | 4 +- .../Quotes/QuotedReplyManager.swift | 2 +- .../Interactions/TSErrorMessage.swift | 2 +- .../TSInfoMessage+LearnedProfileName.swift | 4 +- .../Interactions/TSInfoMessage+Payments.swift | 4 +- .../TSInfoMessage+ProfileChanges.swift | 4 +- .../TSInfoMessage+SessionSwitchover.swift | 2 +- .../TSInfoMessage+ThreadMerge.swift | 4 +- .../Messages/Interactions/TSInfoMessage.swift | 8 +-- .../Messages/Interactions/TSMessage.swift | 16 +++--- .../Messages/MessageSender+Errors.swift | 4 +- .../Messages/Stories/StoryMessage.swift | 6 +-- .../NotificationPresenterImpl.swift | 44 ++++++++-------- .../Security/OWSFingerprint.swift | 4 +- SignalServiceKit/Util/DateUtil.swift | 4 +- .../Util/OWSLocalizedString.swift | 31 ++++++++++++ SignalServiceKit/Util/ThreadUtil.swift | 2 +- .../tests/Util/OWSLocalizedStringTest.swift | 21 ++++++++ .../ShareViewController.swift | 2 +- .../SharingThreadPickerProgressSheet.swift | 4 +- .../SharingThreadPickerViewController.swift | 2 +- .../ColorOrGradientSwatchView.swift | 4 +- .../ImageEditorCropViewController.swift | 8 +-- .../Payments/PaymentsFormat+MobileCoin.swift | 4 +- .../RecipientPickers/ConversationPicker.swift | 2 +- .../DeleteSystemContactViewController.swift | 4 +- SignalUI/RecipientPickers/InviteFlow.swift | 2 +- .../RecipientContextMenuHelper.swift | 16 +++--- .../RecipientPickerViewController.swift | 8 +-- .../FingerprintScanViewController.swift | 2 +- .../FingerprintViewController.swift | 10 ++-- SignalUI/Sending/QuotedReplyModel.swift | 2 +- SignalUI/Usernames/UsernameQuerier.swift | 8 +-- SignalUI/Utils/BlockListUIUtils.swift | 22 ++++---- .../DisappearingTimerConfigurationView.swift | 2 +- SignalUI/Views/MediaMessageView.swift | 8 +-- SignalUI/Views/ProfileDetailLabel.swift | 14 +++--- SignalUI/Views/TextFieldFormatting.swift | 2 +- 145 files changed, 512 insertions(+), 457 deletions(-) create mode 100644 SignalServiceKit/tests/Util/OWSLocalizedStringTest.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 76269fc352..33646e0db8 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -861,6 +861,7 @@ 50EA40912E3A899F009CB839 /* RegistrationWebSocketManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50EA40902E3A899F009CB839 /* RegistrationWebSocketManager.swift */; }; 50EA76E52EEA1C6000DA8CD0 /* PasteboardAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50EA76E42EEA1C6000DA8CD0 /* PasteboardAttachment.swift */; }; 50ED28022F0EDB0B00E57C54 /* ImageQualityTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50ED28012F0EDB0B00E57C54 /* ImageQualityTest.swift */; }; + 50EED2DC2F61E69100D76131 /* OWSLocalizedStringTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50EED2DB2F61E69100D76131 /* OWSLocalizedStringTest.swift */; }; 50EF8DCA2A1885C000A00935 /* AppIconBadgeUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50EF8DC92A1885C000A00935 /* AppIconBadgeUpdater.swift */; }; 50EF8DCC2A189B3000A00935 /* ConversationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50EF8DCB2A189B3000A00935 /* ConversationViewModel.swift */; }; 50EF8DD52A1FE55D00A00935 /* SignalAccountMergeObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50EF8DD42A1FE55D00A00935 /* SignalAccountMergeObserver.swift */; }; @@ -5124,6 +5125,7 @@ 50EA40902E3A899F009CB839 /* RegistrationWebSocketManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationWebSocketManager.swift; sourceTree = ""; }; 50EA76E42EEA1C6000DA8CD0 /* PasteboardAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasteboardAttachment.swift; sourceTree = ""; }; 50ED28012F0EDB0B00E57C54 /* ImageQualityTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageQualityTest.swift; sourceTree = ""; }; + 50EED2DB2F61E69100D76131 /* OWSLocalizedStringTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSLocalizedStringTest.swift; sourceTree = ""; }; 50EF8DC42A1860EF00A00935 /* BadgeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeManager.swift; sourceTree = ""; }; 50EF8DC92A1885C000A00935 /* AppIconBadgeUpdater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconBadgeUpdater.swift; sourceTree = ""; }; 50EF8DCB2A189B3000A00935 /* ConversationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationViewModel.swift; sourceTree = ""; }; @@ -14362,6 +14364,7 @@ F94261E6289B1B5400460798 /* OWSErrorTest.swift */, F97217F528DC9A5000113D9F /* OWSFileSystemTest.swift */, F94261EE289B1B5400460798 /* OWSFormatTest.swift */, + 50EED2DB2F61E69100D76131 /* OWSLocalizedStringTest.swift */, 663F94072CED62E5002C9063 /* OWSProgressTest.swift */, 4C3EF7FC2107DDEE0007EBF7 /* ParamParserTest.swift */, F9CAC7842919B5A400EEC1DE /* PhoneNumberRegionsTest.swift */, @@ -20251,6 +20254,7 @@ F9426248289B1B5500460798 /* OWSIdentityManagerTests.swift in Sources */, 669FAE1B2B7AC919009EE2FE /* OWSLinkPreviewSerializationTest.swift in Sources */, F9426290289B1B5600460798 /* OWSLinkPreviewTest.swift in Sources */, + 50EED2DC2F61E69100D76131 /* OWSLocalizedStringTest.swift in Sources */, 508C72242C2DFCB2000811F3 /* OWSOutgoingResendResponseTest.swift in Sources */, 0436E4B02E5E2DC20011E125 /* OWSPollTest.swift in Sources */, 663F94082CED62EC002C9063 /* OWSProgressTest.swift in Sources */, diff --git a/Signal/AppLaunch/AppDelegate.swift b/Signal/AppLaunch/AppDelegate.swift index 8b9cfc3494..c01e5cbbe1 100644 --- a/Signal/AppLaunch/AppDelegate.swift +++ b/Signal/AppLaunch/AppDelegate.swift @@ -862,8 +862,8 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { Logger.warn("Exiting because we are in the background and the database password is not accessible.") let notificationContent = UNMutableNotificationContent() - notificationContent.body = String( - format: OWSLocalizedString( + notificationContent.body = String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "NOTIFICATION_BODY_PHONE_LOCKED_FORMAT", comment: "Lock screen notification text presented after user powers on their device without unlocking. Embeds {{device model}} (either 'iPad' or 'iPhone')", ), diff --git a/Signal/AppLaunch/LoadingViewController.swift b/Signal/AppLaunch/LoadingViewController.swift index 73862517b7..5fda9c6880 100644 --- a/Signal/AppLaunch/LoadingViewController.swift +++ b/Signal/AppLaunch/LoadingViewController.swift @@ -223,8 +223,8 @@ class LoadingViewController: UIViewController { let unitCountCompleted = Int(Float(unitCountToComplete) * progress.percentComplete) progressView.setProgress(percentComplete, animated: true) - percentCompleteLabel.text = String( - format: OWSLocalizedString( + percentCompleteLabel.text = String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "LINK_NEW_DEVICE_SYNC_PROGRESS_PERCENT", comment: "On a progress modal indicating the percent complete the sync process is. Embeds {{ formatted percentage }}", ), diff --git a/Signal/Backups/BackupEnablingManager.swift b/Signal/Backups/BackupEnablingManager.swift index 3022b0c993..e61a69d2fb 100644 --- a/Signal/Backups/BackupEnablingManager.swift +++ b/Signal/Backups/BackupEnablingManager.swift @@ -134,7 +134,7 @@ final class BackupEnablingManager { ) throw ActionSheetDisplayableError( localizedTitle: title, - localizedMessage: String(format: message, nextRetryString), + localizedMessage: String.nonPluralLocalizedStringWithFormat(message, nextRetryString), ) } else { throw .genericError diff --git a/Signal/Backups/BackupSettingsViewController.swift b/Signal/Backups/BackupSettingsViewController.swift index 9827d6e8c7..901c8f1df4 100644 --- a/Signal/Backups/BackupSettingsViewController.swift +++ b/Signal/Backups/BackupSettingsViewController.swift @@ -1173,8 +1173,8 @@ class BackupSettingsViewController: let limits = try? await backupIdService.fetchBackupIDLimits(auth: .implicit(), logger: PrefixedLogger(prefix: "[Settings]")), !limits.hasPermitsRemaining { - let bodyText = String( - format: OWSLocalizedString( + let bodyText = String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "BACKUP_SETTINGS_CREATE_NEW_KEY_LIMIT_REACHED_WARNING_SHEET_BODY", comment: "Explanation text for a sheet warning users they've reached a rate limit for creating Recovery Key. {{ Embeds 1: the preformatted time they must wait before enabling backups, such as \"1 week\" or \"6 hours\". }}", ), @@ -1785,8 +1785,8 @@ struct BackupSettingsView: View { VStack(alignment: .leading) { Label { Text( - String( - format: OWSLocalizedString( + String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "BACKUP_SETTINGS_UPLOAD_PROGRESS_SUBTITLE_PAUSED_OUT_OF_STORAGE_SPACE_FORMAT", comment: "Subtitle for a progress bar tracking uploads that are paused because the user is out of remote storage space. Embeds 1:{{ total storage space provided, e.g. 100 GB }}; 2:{{ space the user needs to free up by deleting media, e.g. 1 GB }}.", ), @@ -2079,8 +2079,8 @@ private struct BackupExportProgressView: View { let percentComplete = (0.95 * percentExportCompleted) + (0.05 * percentUploadCompleted) return ProgressBarState( style: .determinate(percentComplete: percentComplete), - label: String( - format: OWSLocalizedString( + label: String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "BACKUP_SETTINGS_BACKUP_EXPORT_PROGRESS_DESCRIPTION_PREPARING_BACKUP", comment: "Description for a progress bar tracking the preparation of a Backup. Embeds 1:{{ the percentage completed preformatted as a percent, e.g. 10% }}.", ), @@ -2353,16 +2353,16 @@ private struct BackupAttachmentDownloadProgressView: View { case .suspended: switch backupPlan { case .disabled, .free, .paid, .paidAsTester: - String( - format: OWSLocalizedString( + String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "BACKUP_SETTINGS_DOWNLOAD_PROGRESS_SUBTITLE_SUSPENDED", comment: "Subtitle for a view explaining that downloads are available but not running. Embeds {{ the amount available to download as a file size, e.g. 100 MB }}.", ), latestDownloadUpdate.bytesRemaining.formatted(.owsByteCount()), ) case .disabling, .paidExpiringSoon: - String( - format: OWSLocalizedString( + String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "BACKUP_SETTINGS_DOWNLOAD_PROGRESS_SUBTITLE_SUSPENDED_PAID_SUBSCRIPTION_EXPIRING", comment: "Subtitle for a view explaining that downloads are available but not running, and the user's paid subscription is expiring. Embeds {{ the amount available to download as a file size, e.g. 100 MB }}.", ), @@ -2370,8 +2370,8 @@ private struct BackupAttachmentDownloadProgressView: View { ) } case .running: - String( - format: OWSLocalizedString( + String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "BACKUP_SETTINGS_DOWNLOAD_PROGRESS_SUBTITLE_RUNNING", comment: "Subtitle for a progress bar tracking active downloading. Embeds 1:{{ the amount downloaded as a file size, e.g. 100 MB }}; 2:{{ the total amount to download as a file size, e.g. 1 GB }}; 3:{{ the amount downloaded as a percentage, e.g. 10% }}.", ), @@ -2400,8 +2400,8 @@ private struct BackupAttachmentDownloadProgressView: View { comment: "Subtitle for a progress bar tracking downloads that are paused because they need internet.", ) case .outOfDiskSpace(let bytesRequired): - String( - format: OWSLocalizedString( + String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "BACKUP_SETTINGS_DOWNLOAD_PROGRESS_SUBTITLE_PAUSED_NEEDS_DISK_SPACE", comment: "Subtitle for a progress bar tracking downloads that are paused because they need more disk space available. Embeds {{ the amount of space needed as a file size, e.g. 100 MB }}.", ), @@ -2525,8 +2525,8 @@ private struct BackupAttachmentUploadProgressView: View { let totalBytesToUpload = uploadUpdate.totalBytesToUpload let percentageUploaded = uploadUpdate.percentageUploaded - return String( - format: OWSLocalizedString( + return String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "BACKUP_SETTINGS_UPLOAD_PROGRESS_SUBTITLE_RUNNING", comment: "Subtitle for a progress bar tracking active uploading. Embeds 1:{{ the amount uploaded as a file size, e.g. 100 MB }}; 2:{{ the total amount to upload as a file size, e.g. 1 GB }}; 3:{{ the percentage uploaded as a percent, e.g. 40% }}.", ), @@ -2733,8 +2733,8 @@ private struct BackupSubscriptionLoadedView: View { "BACKUP_SETTINGS_BACKUP_PLAN_PAID_PRICE_FORMAT", comment: "Text explaining the price of the user's paid backup plan. Embeds {{ the formatted price }}.", ) - Text(String( - format: priceStringFormat, + Text(String.nonPluralLocalizedStringWithFormat( + priceStringFormat, CurrencyFormatter.format(money: price), )) @@ -2742,8 +2742,8 @@ private struct BackupSubscriptionLoadedView: View { "BACKUP_SETTINGS_BACKUP_PLAN_PAID_RENEWAL_FORMAT", comment: "Text explaining when the user's paid backup plan renews. Embeds {{ the formatted renewal date }}.", ) - Text(String( - format: renewalStringFormat, + Text(String.nonPluralLocalizedStringWithFormat( + renewalStringFormat, DateFormatter.localizedString(from: renewalDate, dateStyle: .medium, timeStyle: .none), )) case .paidButExpiring(let expirationDate), .paidButExpired(let expirationDate): @@ -2770,8 +2770,8 @@ private struct BackupSubscriptionLoadedView: View { .fontWeight(.semibold) .foregroundStyle(Color.Signal.red) - Text(String( - format: expirationDateFormatString, + Text(String.nonPluralLocalizedStringWithFormat( + expirationDateFormatString, DateFormatter.localizedString(from: expirationDate, dateStyle: .medium, timeStyle: .none), )) case .paidButFailedToRenew: @@ -2930,21 +2930,21 @@ private struct BackupDetailsView: View { comment: "Text explaining that the user's last backup was today. Embeds {{ the time of the backup }}.", ) - return String(format: todayFormatString, lastBackupTimeString) + return String.nonPluralLocalizedStringWithFormat(todayFormatString, lastBackupTimeString) } else if Calendar.current.isDateInYesterday(lastBackupDate) { let yesterdayFormatString = OWSLocalizedString( "BACKUP_SETTINGS_ENABLED_LAST_BACKUP_YESTERDAY_FORMAT", comment: "Text explaining that the user's last backup was yesterday. Embeds {{ the time of the backup }}.", ) - return String(format: yesterdayFormatString, lastBackupTimeString) + return String.nonPluralLocalizedStringWithFormat(yesterdayFormatString, lastBackupTimeString) } else { let pastFormatString = OWSLocalizedString( "BACKUP_SETTINGS_ENABLED_LAST_BACKUP_PAST_FORMAT", comment: "Text explaining that the user's last backup was in the past. Embeds 1:{{ the date of the backup }} and 2:{{ the time of the backup }}.", ) - return String(format: pastFormatString, lastBackupDateString, lastBackupTimeString) + return String.nonPluralLocalizedStringWithFormat(pastFormatString, lastBackupDateString, lastBackupTimeString) } }() diff --git a/Signal/Backups/ChooseBackupPlanViewController.swift b/Signal/Backups/ChooseBackupPlanViewController.swift index 79faea7f36..b2c0d1514c 100644 --- a/Signal/Backups/ChooseBackupPlanViewController.swift +++ b/Signal/Backups/ChooseBackupPlanViewController.swift @@ -273,8 +273,8 @@ struct ChooseBackupPlanView: View { title: { switch viewModel.storeKitAvailability { case .available(let paidPlanDisplayPrice): - String( - format: OWSLocalizedString( + String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "CHOOSE_BACKUP_PLAN_PAID_PLAN_TITLE", comment: "Title for the paid plan option, when choosing a Backup plan. Embeds {{ the formatted monthly cost, as currency, of the paid plan }}.", ), @@ -300,8 +300,8 @@ struct ChooseBackupPlanView: View { "CHOOSE_BACKUP_PLAN_BULLET_FULL_MEDIA_BACKUP", comment: "Text for a bullet point in a list of Backup features, describing that all media is included.", )), - PlanOptionView.BulletPoint(iconKey: "data", text: String( - format: OWSLocalizedString( + PlanOptionView.BulletPoint(iconKey: "data", text: String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "CHOOSE_BACKUP_PLAN_BULLET_STORAGE_AMOUNT", comment: "Text for a bullet point in a list of Backup features, describing the amount of included storage. Embeds {{ the amount of storage preformatted as a localized byte count, e.g. '100 GB' }}.", ), @@ -339,8 +339,8 @@ struct ChooseBackupPlanView: View { case .paid: switch viewModel.storeKitAvailability { case .available(let paidPlanDisplayPrice): - String( - format: OWSLocalizedString( + String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "CHOOSE_BACKUP_PLAN_SUBSCRIBE_PAID_BUTTON_TEXT", comment: "Text for a button that will subscribe the user to the paid Backup plan. Embeds {{ the formatted monthly cost, as currency, of the paid plan }}.", ), diff --git a/Signal/Calls/UserInterface/CallDrawerSheet.swift b/Signal/Calls/UserInterface/CallDrawerSheet.swift index 129f5fb739..b1cb796da4 100644 --- a/Signal/Calls/UserInterface/CallDrawerSheet.swift +++ b/Signal/Calls/UserInterface/CallDrawerSheet.swift @@ -273,12 +273,12 @@ class CallDrawerSheet: InteractiveSheetViewController, UITableViewDelegate, Call label.attributedText = .composed(of: [ titleText.styled(with: .font(.dynamicTypeHeadline)), " ", - String( - format: OWSLocalizedString( + String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "GROUP_CALL_MEMBER_LIST_SECTION_HEADER_MEMBER_COUNT", comment: "A count of members in a given group call member list section, displayed after the header.", ), - self.memberCount, + String(self.memberCount), ), ]).styled( with: .font(.dynamicTypeBody), diff --git a/Signal/Calls/UserInterface/CallHeader.swift b/Signal/Calls/UserInterface/CallHeader.swift index dda5c24ba8..8595fde5ef 100644 --- a/Signal/Calls/UserInterface/CallHeader.swift +++ b/Signal/Calls/UserInterface/CallHeader.swift @@ -194,9 +194,9 @@ class CallHeader: UIView { case 0: return zeroMemberString() case 1: - return String(format: oneMemberFormat(), names[0]) + return String.nonPluralLocalizedStringWithFormat(oneMemberFormat(), names[0]) case 2: - return String(format: twoMemberFormat(), names[0], names[1]) + return String.nonPluralLocalizedStringWithFormat(twoMemberFormat(), names[0], names[1]) default: return String.localizedStringWithFormat(manyMemberFormat(), count - 2, names[0], names[1]) } @@ -289,7 +289,7 @@ class CallHeader: UIView { "GROUP_CALL_INCOMING_RING_FORMAT", comment: "Text explaining that someone has sent a ring to the group. Embeds {ring sender name}", ) - return String(format: formatString, callerName) + return String.nonPluralLocalizedStringWithFormat(formatString, callerName) } private func whoIsHereText(joinedMembers: [UUID?]) -> String { @@ -316,14 +316,14 @@ class CallHeader: UIView { "GROUP_CALL_ONE_PERSON_HERE_FORMAT", comment: "Text explaining that there is one person in the group call. Embeds {member name}", ) - return String(format: format, someMember) + return String.nonPluralLocalizedStringWithFormat(format, someMember) case ((let someMember, let someOtherMember?)?, 0): // exactly two members, both known let format = OWSLocalizedString( "GROUP_CALL_TWO_PEOPLE_HERE_FORMAT", comment: "Text explaining that there are two people in the group call. Embeds {{ %1$@ participant1, %2$@ participant2 }}", ) - return String(format: format, someMember, someOtherMember) + return String.nonPluralLocalizedStringWithFormat(format, someMember, someOtherMember) case ((let someMember, let someOtherMember?)?, let otherCount): // two or more members, at least two known let format = OWSLocalizedString( @@ -459,7 +459,7 @@ class CallHeader: UIView { "GROUP_CALL_PRESENTING_FORMAT", comment: "Text explaining that a member is presenting. Embeds {member name}", ) - return String(format: formatString, presentingName) + return String.nonPluralLocalizedStringWithFormat(formatString, presentingName) } switch groupCall.concreteType { case .groupThread(let groupThreadCall): diff --git a/Signal/Calls/UserInterface/CallMemberWaitingAndErrorView.swift b/Signal/Calls/UserInterface/CallMemberWaitingAndErrorView.swift index 160647d86e..60d1ea6bd9 100644 --- a/Signal/Calls/UserInterface/CallMemberWaitingAndErrorView.swift +++ b/Signal/Calls/UserInterface/CallMemberWaitingAndErrorView.swift @@ -136,7 +136,7 @@ class CallMemberWaitingAndErrorView: UIView, CallMemberComposableView { comment: "String displayed in group call grid cell when a user is blocked. Embeds {user's name}", ) let displayName = displayName(address: addr) - label = String(format: blockFormat, arguments: [displayName]) + label = String.nonPluralLocalizedStringWithFormat(blockFormat, displayName) image = UIImage(named: "block") case .noMediaKeys(let addr): let missingKeyFormat = OWSLocalizedString( @@ -144,7 +144,7 @@ class CallMemberWaitingAndErrorView: UIView, CallMemberComposableView { comment: "String displayed in cell when media from a user can't be displayed in group call grid. Embeds {user's name}", ) let displayName = displayName(address: addr) - label = String(format: missingKeyFormat, arguments: [displayName]) + label = String.nonPluralLocalizedStringWithFormat(missingKeyFormat, displayName) image = UIImage(named: "error-circle-fill") } @@ -177,7 +177,7 @@ class CallMemberWaitingAndErrorView: UIView, CallMemberComposableView { comment: "Title for alert explaining that a group call participant is blocked. Embeds {{ user's name }}", ) let displayName = SSKEnvironment.shared.databaseStorageRef.read { tx in SSKEnvironment.shared.contactManagerRef.displayName(for: address, tx: tx).resolvedValue() } - title = String(format: titleFormat, displayName) + title = String.nonPluralLocalizedStringWithFormat(titleFormat, displayName) case let .noMediaKeys(address): message = OWSLocalizedString( @@ -190,7 +190,7 @@ class CallMemberWaitingAndErrorView: UIView, CallMemberComposableView { comment: "Title for alert explaining that a group call participant cannot be displayed because of missing keys. Embeds {{ user's name }}", ) let displayName = SSKEnvironment.shared.databaseStorageRef.read { tx in SSKEnvironment.shared.contactManagerRef.displayName(for: address, tx: tx).resolvedValue() } - title = String(format: titleFormat, displayName) + title = String.nonPluralLocalizedStringWithFormat(titleFormat, displayName) } return (title, message) diff --git a/Signal/Calls/UserInterface/CallsListViewController.swift b/Signal/Calls/UserInterface/CallsListViewController.swift index 3663d3205d..150b70051c 100644 --- a/Signal/Calls/UserInterface/CallsListViewController.swift +++ b/Signal/Calls/UserInterface/CallsListViewController.swift @@ -1630,9 +1630,9 @@ class CallsListViewController: OWSViewController, HomeTabViewController, CallSer private func updateEmptyStateMessage() { switch (viewModelLoader.isEmpty, searchTerm) { case (true, .some(let searchTerm)) where !searchTerm.isEmpty: - noSearchResultsView.text = String( - format: Strings.searchNoResultsFoundLabelFormat, - arguments: [searchTerm], + noSearchResultsView.text = String.nonPluralLocalizedStringWithFormat( + Strings.searchNoResultsFoundLabelFormat, + searchTerm, ) noSearchResultsView.alpha = 1 emptyStateMessageView.alpha = 0 @@ -2498,7 +2498,7 @@ private extension CallsListViewController { if viewModel.callRecords.count <= 1 { return viewModel.title } else { - return String(format: Strings.coalescedCallsTitleFormat, viewModel.title, "\(viewModel.callRecords.count)") + return String.nonPluralLocalizedStringWithFormat(Strings.coalescedCallsTitleFormat, viewModel.title, "\(viewModel.callRecords.count)") } }() titleLabel.text = titleText diff --git a/Signal/Calls/UserInterface/GroupCallNotificationView.swift b/Signal/Calls/UserInterface/GroupCallNotificationView.swift index 8232f4dd9e..d9ed385cda 100644 --- a/Signal/Calls/UserInterface/GroupCallNotificationView.swift +++ b/Signal/Calls/UserInterface/GroupCallNotificationView.swift @@ -200,7 +200,7 @@ private class BannerView: UIView { "GROUP_CALL_NOTIFICATION_TWO_LEFT_FORMAT", comment: "Copy explaining that two users have left the group call. Embeds {first member name}, {second member name}", ) - actionText = String(format: formatText, displayNames[0], displayNames[1]) + actionText = String.nonPluralLocalizedStringWithFormat(formatText, displayNames[0], displayNames[1]) } else { let formatText = action == .join ? OWSLocalizedString( @@ -211,7 +211,7 @@ private class BannerView: UIView { "GROUP_CALL_NOTIFICATION_ONE_LEFT_FORMAT", comment: "Copy explaining that a user has left the group call. Embeds {member name}", ) - actionText = String(format: formatText, displayNames[0]) + actionText = String.nonPluralLocalizedStringWithFormat(formatText, displayNames[0]) } let hStack = UIStackView() diff --git a/Signal/Calls/UserInterface/GroupCallVideoContextMenuConfiguration.swift b/Signal/Calls/UserInterface/GroupCallVideoContextMenuConfiguration.swift index a3ffc41bc7..bccbc6079c 100644 --- a/Signal/Calls/UserInterface/GroupCallVideoContextMenuConfiguration.swift +++ b/Signal/Calls/UserInterface/GroupCallVideoContextMenuConfiguration.swift @@ -182,8 +182,8 @@ enum GroupCallVideoContextMenuConfiguration { ringRtcGroupCall: SignalRingRTC.GroupCall, ) { let actionSheet = ActionSheetController( - title: String( - format: OWSLocalizedString( + title: String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "GROUP_CALL_REMOVE_MEMBER_CONFIRMATION_ACTION_SHEET_TITLE", comment: "Title for action sheet confirming removal of a member from a group call. embeds {{ name }}", ), diff --git a/Signal/Calls/UserInterface/IndividualCallViewController.swift b/Signal/Calls/UserInterface/IndividualCallViewController.swift index 804dd3035e..726e06907d 100644 --- a/Signal/Calls/UserInterface/IndividualCallViewController.swift +++ b/Signal/Calls/UserInterface/IndividualCallViewController.swift @@ -1378,8 +1378,8 @@ private class PermissionErrorView: UIView { } let needPermissionLabel = UILabel() - needPermissionLabel.text = String( - format: OWSLocalizedString( + needPermissionLabel.text = String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "CALL_VIEW_NEED_PERMISSION_ERROR_FORMAT", comment: "Error displayed on the 'call' view when the callee needs to grant permission before we can call them. Embeds {callee short name}.", ), diff --git a/Signal/Calls/UserInterface/RaisedHandsToast.swift b/Signal/Calls/UserInterface/RaisedHandsToast.swift index e22d30e6a1..21f132508a 100644 --- a/Signal/Calls/UserInterface/RaisedHandsToast.swift +++ b/Signal/Calls/UserInterface/RaisedHandsToast.swift @@ -239,12 +239,12 @@ class RaisedHandsToast: UIView { } self.collapsedText = if self.yourHandIsRaised, raisedHands.count > 1 { - String( - format: OWSLocalizedString( + String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "RAISED_HANDS_TOAST_YOU_PLUS_OTHERS_COUNT", comment: "A compact member count on the call view's raised hands toast indicating that you and a number of other users raised a hand. Embeds {{number of other users}}", ), - raisedHands.count - 1, + String(raisedHands.count - 1), ) } else if self.yourHandIsRaised { CommonStrings.you @@ -285,8 +285,8 @@ class RaisedHandsToast: UIView { return String.localizedStringWithFormat(format, otherMembersCount, firstRaisedHandMemberName) } - return String( - format: OWSLocalizedString( + return String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "RAISED_HANDS_TOAST_SINGLE_HAND_MESSAGE", comment: "A message appearing on the call view's raised hands toast indicating that another named member has raised their hand.", ), diff --git a/Signal/Calls/UserInterface/RemoteMuteToast.swift b/Signal/Calls/UserInterface/RemoteMuteToast.swift index 5d71766815..9794954d5a 100644 --- a/Signal/Calls/UserInterface/RemoteMuteToast.swift +++ b/Signal/Calls/UserInterface/RemoteMuteToast.swift @@ -55,15 +55,15 @@ class RemoteMuteToast: UIView { let toastText: String let localAci = self.deps.tsAccountManager.localIdentifiersWithMaybeSneakyTransaction!.aci if muteSource == localAci { - toastText = String( - format: OWSLocalizedString( + toastText = String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "REMOTE_MUTE_TOAST_YOU_MUTED_YOURSELF", comment: "A message that displays when you joined a call on two devices and mute one from the other.", ), ) } else { - toastText = String( - format: OWSLocalizedString( + toastText = String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "REMOTE_MUTE_TOAST_SOMEONE_MUTED_YOU", comment: "A message that displays when your microphone is remotely muted by another call participant. Embeds {{name}}", @@ -91,8 +91,8 @@ class RemoteMuteToast: UIView { ).resolvedValue(useShortNameIfAvailable: true) } - toastText = String( - format: OWSLocalizedString( + toastText = String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "REMOTE_MUTE_TOAST_YOU_MUTED_SOMEONE", comment: "A message that displays when you remotely muted another call participant's microphone. Embeds {{name}}", @@ -117,8 +117,8 @@ class RemoteMuteToast: UIView { ) } - toastText = String( - format: OWSLocalizedString( + toastText = String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "REMOTE_MUTE_TOAST_A_MUTED_B", comment: "A message that displays when one person in a call remotely muted another participant. Embeds {{name}} {{name}}", diff --git a/Signal/ConversationView/BlockingAnnouncementOnlyView.swift b/Signal/ConversationView/BlockingAnnouncementOnlyView.swift index 37491089a4..6539cc9777 100644 --- a/Signal/ConversationView/BlockingAnnouncementOnlyView.swift +++ b/Signal/ConversationView/BlockingAnnouncementOnlyView.swift @@ -37,7 +37,7 @@ class BlockingAnnouncementOnlyView: ConversationBottomPanelView { "GROUPS_ANNOUNCEMENT_ONLY_ADMINISTRATORS", comment: "Label for group administrators in the 'announcement-only' group UI.", ) - let text = String(format: format, adminsText) + let text = String.nonPluralLocalizedStringWithFormat(format, adminsText) let attributedString = NSMutableAttributedString(string: text) attributedString.setAttributes([.foregroundColor: UIColor.Signal.link], forSubstring: adminsText) diff --git a/Signal/ConversationView/CellViews/CVMediaAlbumView.swift b/Signal/ConversationView/CellViews/CVMediaAlbumView.swift index dbb8a9161f..cce2116830 100644 --- a/Signal/ConversationView/CellViews/CVMediaAlbumView.swift +++ b/Signal/ConversationView/CellViews/CVMediaAlbumView.swift @@ -129,8 +129,8 @@ class CVMediaAlbumView: ManualStackViewWithLayer { let moreCount = max(1, items.count - Constants.maxItemCount) let moreCountText = OWSFormat.formatInt(moreCount) - let moreText = String( - format: OWSLocalizedString( + let moreText = String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "MEDIA_GALLERY_MORE_ITEMS_FORMAT", comment: "Format for the 'more items' indicator for media galleries. Embeds {{the number of additional items}}.", ), diff --git a/Signal/ConversationView/CellViews/CVQuotedMessageView.swift b/Signal/ConversationView/CellViews/CVQuotedMessageView.swift index 3aa72f60d7..f808b606f7 100644 --- a/Signal/ConversationView/CellViews/CVQuotedMessageView.swift +++ b/Signal/ConversationView/CellViews/CVQuotedMessageView.swift @@ -262,7 +262,7 @@ public class CVQuotedMessageView: ManualStackViewWithLayer { "QUOTED_REPLY_STORY_AUTHOR_INDICATOR_FORMAT", comment: "Message header when you are quoting a story. Embeds {{ story author name }}", ) - text = String(format: format, authorName) + text = String.nonPluralLocalizedStringWithFormat(format, authorName) } else { text = authorName } @@ -415,7 +415,7 @@ public class CVQuotedMessageView: ManualStackViewWithLayer { "QUOTED_REPLY_REACTION_TO_STORY_FORMAT_SECOND_PERSON", comment: "Label explaining that the content of a quoted message includes you reacting to someone's story. Embeds {{ %1$@ the story author }}.", ) - text = String(format: formatText, quotedAuthorName.string) + text = String.nonPluralLocalizedStringWithFormat(formatText, quotedAuthorName.string) } return CVLabelConfig.unstyledText( diff --git a/Signal/ConversationView/CellViews/GiftBadgeView.swift b/Signal/ConversationView/CellViews/GiftBadgeView.swift index cb3f7f5d6e..54076688b3 100644 --- a/Signal/ConversationView/CellViews/GiftBadgeView.swift +++ b/Signal/ConversationView/CellViews/GiftBadgeView.swift @@ -75,7 +75,7 @@ class GiftBadgeView: ManualStackView { comment: "You sent a donation to a friend. This is the title of that message in the chat. Embeds {{short contact name}}.", ) } - let text = String(format: textFormat, state.otherUserShortName) + let text = String.nonPluralLocalizedStringWithFormat(textFormat, state.otherUserShortName) let textColor = state.conversationStyle.bubbleTextColor(isIncoming: state.isIncoming) diff --git a/Signal/ConversationView/Components/CVComponentArchivedPayment.swift b/Signal/ConversationView/Components/CVComponentArchivedPayment.swift index 48b6c8c6d7..40afe5a5a3 100644 --- a/Signal/ConversationView/Components/CVComponentArchivedPayment.swift +++ b/Signal/ConversationView/Components/CVComponentArchivedPayment.swift @@ -200,26 +200,26 @@ public class CVComponentArchivedPayment: CVComponentBase, CVComponent { "PAYMENTS_PAYMENT_STATUS_IN_CHAT_SENT_YOU", comment: "Payment status context with contact name, incoming. Embeds {{ Name of sending contact }}", ) - text = String(format: format, archivedPaymentAttachment.otherUserShortName) + text = String.nonPluralLocalizedStringWithFormat(format, archivedPaymentAttachment.otherUserShortName) case (.outgoingMessage, .failed): let format = OWSLocalizedString( "PAYMENTS_PAYMENT_STATUS_IN_CHAT_PAYMENT_TO", comment: "Payment status context with contact name, failed. Embeds {{ Name of receiving contact }}", ) - text = String(format: format, archivedPaymentAttachment.otherUserShortName) + text = String.nonPluralLocalizedStringWithFormat(format, archivedPaymentAttachment.otherUserShortName) case (.outgoingMessage, _): let format = OWSLocalizedString( "PAYMENTS_PAYMENT_STATUS_IN_CHAT_YOU_SENT", comment: "Payment status context with contact name, sent. Embeds {{ Name of receiving contact }}", ) - text = String(format: format, archivedPaymentAttachment.otherUserShortName) + text = String.nonPluralLocalizedStringWithFormat(format, archivedPaymentAttachment.otherUserShortName) default: // default to failed text because it doesn't imply success let format = OWSLocalizedString( "PAYMENTS_PAYMENT_STATUS_IN_CHAT_PAYMENT_TO", comment: "Payment status context with contact name, failed. Embeds {{ Name of receiving contact }}", ) - text = String(format: format, archivedPaymentAttachment.otherUserShortName) + text = String.nonPluralLocalizedStringWithFormat(format, archivedPaymentAttachment.otherUserShortName) } return CVLabelConfig( diff --git a/Signal/ConversationView/Components/CVComponentBodyText.swift b/Signal/ConversationView/Components/CVComponentBodyText.swift index 192a00a897..b434cd5b13 100644 --- a/Signal/ConversationView/Components/CVComponentBodyText.swift +++ b/Signal/ConversationView/Components/CVComponentBodyText.swift @@ -566,7 +566,7 @@ public class CVComponentBodyText: CVComponentBase, CVComponent { ) text.append( NSAttributedString( - string: String(format: format, displayName), + string: String.nonPluralLocalizedStringWithFormat(format, displayName), ), ) diff --git a/Signal/ConversationView/Components/CVComponentContactShare.swift b/Signal/ConversationView/Components/CVComponentContactShare.swift index 3e2ffded47..a5fe3c9ae8 100644 --- a/Signal/ConversationView/Components/CVComponentContactShare.swift +++ b/Signal/ConversationView/Components/CVComponentContactShare.swift @@ -97,7 +97,7 @@ extension CVComponentContactShare: CVAccessibilityComponent { "ACCESSIBILITY_LABEL_CONTACT_FORMAT", comment: "Accessibility label for contact. Embeds: {{ the contact name }}.", ) - return String(format: format, contactName) + return String.nonPluralLocalizedStringWithFormat(format, contactName) } else { return OWSLocalizedString( "ACCESSIBILITY_LABEL_CONTACT", diff --git a/Signal/ConversationView/Components/CVComponentMessage.swift b/Signal/ConversationView/Components/CVComponentMessage.swift index 8e00aa9ece..72743641e1 100644 --- a/Signal/ConversationView/Components/CVComponentMessage.swift +++ b/Signal/ConversationView/Components/CVComponentMessage.swift @@ -1420,7 +1420,7 @@ public class CVComponentMessage: CVComponentBase, CVRootComponent { "CONVERSATION_VIEW_CELL_ACCESSIBILITY_SENDER_FORMAT", comment: "Format for sender info for accessibility label for message. Embeds {{ the sender name }}.", ) - elements.append(String(format: format, accessibilityAuthorName)) + elements.append(String.nonPluralLocalizedStringWithFormat(format, accessibilityAuthorName)) } else { owsFailDebug("Missing accessibilityAuthorName.") } diff --git a/Signal/ConversationView/Components/CVComponentPaymentAttachment.swift b/Signal/ConversationView/Components/CVComponentPaymentAttachment.swift index f6c141c19f..03b750a335 100644 --- a/Signal/ConversationView/Components/CVComponentPaymentAttachment.swift +++ b/Signal/ConversationView/Components/CVComponentPaymentAttachment.swift @@ -222,26 +222,26 @@ public class CVComponentPaymentAttachment: CVComponentBase, CVComponent { "PAYMENTS_PAYMENT_STATUS_IN_CHAT_SENT_YOU", comment: "Payment status context with contact name, incoming. Embeds {{ Name of sending contact }}", ) - text = String(format: format, contactName) + text = String.nonPluralLocalizedStringWithFormat(format, contactName) case (_, .outgoingMessage, .failed): let format = OWSLocalizedString( "PAYMENTS_PAYMENT_STATUS_IN_CHAT_PAYMENT_TO", comment: "Payment status context with contact name, failed. Embeds {{ Name of receiving contact }}", ) - text = String(format: format, contactName) + text = String.nonPluralLocalizedStringWithFormat(format, contactName) case (_, .outgoingMessage, _): let format = OWSLocalizedString( "PAYMENTS_PAYMENT_STATUS_IN_CHAT_YOU_SENT", comment: "Payment status context with contact name, sent. Embeds {{ Name of receiving contact }}", ) - text = String(format: format, contactName) + text = String.nonPluralLocalizedStringWithFormat(format, contactName) default: // default to failed text because it doesn't imply success let format = OWSLocalizedString( "PAYMENTS_PAYMENT_STATUS_IN_CHAT_PAYMENT_TO", comment: "Payment status context with contact name, failed. Embeds {{ Name of receiving contact }}", ) - text = String(format: format, contactName) + text = String.nonPluralLocalizedStringWithFormat(format, contactName) } return CVLabelConfig( diff --git a/Signal/ConversationView/Components/CVComponentPoll.swift b/Signal/ConversationView/Components/CVComponentPoll.swift index 6298625f51..0cd1366049 100644 --- a/Signal/ConversationView/Components/CVComponentPoll.swift +++ b/Signal/ConversationView/Components/CVComponentPoll.swift @@ -64,7 +64,7 @@ public class CVComponentPoll: CVComponentBase, CVComponent { "CONVERSATION_VIEW_CELL_ACCESSIBILITY_SENDER_FORMAT", comment: "Format for sender info for accessibility label for message. Embeds {{ the sender name }}.", ) - elements.append(String(format: senderFormat, accessibilityAuthorName)) + elements.append(String.nonPluralLocalizedStringWithFormat(senderFormat, accessibilityAuthorName)) } else { owsFailDebug("Missing accessibilityAuthorName.") } @@ -79,7 +79,7 @@ public class CVComponentPoll: CVComponentBase, CVComponent { "POLL_ACCESSIBILITY_LABEL", comment: "Accessibility label for poll message. Embeds {{ poll question }}.", ) - elements.append(String(format: formatQuestion, poll.state.poll.question)) + elements.append(String.nonPluralLocalizedStringWithFormat(formatQuestion, poll.state.poll.question)) let result = elements.joined(separator: " ") return result diff --git a/Signal/ConversationView/Components/CVComponentQuotedReply.swift b/Signal/ConversationView/Components/CVComponentQuotedReply.swift index c3a432a78b..8caf8a7456 100644 --- a/Signal/ConversationView/Components/CVComponentQuotedReply.swift +++ b/Signal/ConversationView/Components/CVComponentQuotedReply.swift @@ -121,10 +121,10 @@ extension CVComponentQuotedReply: CVAccessibilityComponent { let originalMessage = self.quotedReply.viewState.quotedReplyModel.originalMessageAccessibilityLabel ?? "" if quotedReply.quotedReplyModel.isOriginalMessageAuthorLocalUser { - return String(format: format, CommonStrings.you, originalMessage) + return String.nonPluralLocalizedStringWithFormat(format, CommonStrings.you, originalMessage) } else { - return String( - format: format, + return String.nonPluralLocalizedStringWithFormat( + format, self.quotedReply.viewState.quotedAuthorName, originalMessage, ) diff --git a/Signal/ConversationView/Components/CVComponentSticker.swift b/Signal/ConversationView/Components/CVComponentSticker.swift index 8a8ce586d1..9e0b3cb62f 100644 --- a/Signal/ConversationView/Components/CVComponentSticker.swift +++ b/Signal/ConversationView/Components/CVComponentSticker.swift @@ -238,8 +238,8 @@ public class CVComponentSticker: CVComponentBase, CVComponent { extension CVComponentSticker: CVAccessibilityComponent { public var accessibilityDescription: String { if let approximateEmoji = stickerMetadata?.firstEmoji { - return String( - format: OWSLocalizedString( + return String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "ACCESSIBILITY_LABEL_STICKER_FORMAT", comment: "Accessibility label for stickers. Embeds {{ name of top emoji the sticker resembles }}", ), diff --git a/Signal/ConversationView/Components/CVComponentSystemMessage.swift b/Signal/ConversationView/Components/CVComponentSystemMessage.swift index 85f1b759a2..c51d675de6 100644 --- a/Signal/ConversationView/Components/CVComponentSystemMessage.swift +++ b/Signal/ConversationView/Components/CVComponentSystemMessage.swift @@ -827,7 +827,7 @@ extension CVComponentSystemMessage { } let displayName = SSKEnvironment.shared.contactManagerRef.displayName(for: verificationMessage.recipientAddress, tx: transaction).resolvedValue() - return String(format: format, displayName) + return String.nonPluralLocalizedStringWithFormat(format, displayName) } if let infoMessage = interaction as? TSInfoMessage { return infoMessage.conversationSystemMessageComponentText(with: transaction) @@ -1147,7 +1147,7 @@ extension CVComponentSystemMessage { "SYSTEM_MESSAGE_DEFAULT_DISAPPEARING_MESSAGE_TIMER_FORMAT", comment: "Indicator that the default disappearing message timer will be applied when you send a message. Embeds {default disappearing message time}", ) - labelText.append(String(format: titleFormat, configuration.durationString())) + labelText.append(String.nonPluralLocalizedStringWithFormat(titleFormat, configuration.durationString())) return buildComponentState(title: labelText, action: nil, expiration: nil) } diff --git a/Signal/ConversationView/Components/CVComponentThreadDetails.swift b/Signal/ConversationView/Components/CVComponentThreadDetails.swift index 06719eb0c5..40ca670a73 100644 --- a/Signal/ConversationView/Components/CVComponentThreadDetails.swift +++ b/Signal/ConversationView/Components/CVComponentThreadDetails.swift @@ -463,7 +463,7 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent { ) } - let formattedString = String(format: predicate, subject) + let formattedString = String.nonPluralLocalizedStringWithFormat(predicate, subject) let subjectRange = NSString(string: formattedString).range(of: subject) let attributedString = NSMutableAttributedString(string: formattedString) attributedString.addAttributes( @@ -987,7 +987,7 @@ extension CVComponentThreadDetails { let formatString: String var underlinedPortion: String? - var arguments: [CVarArg] = sortedMemberNames + var arguments: [String] = sortedMemberNames switch (sortedMemberNames.count, localUserIsAMember) { case (0, _): formatString = OWSLocalizedString( @@ -1050,8 +1050,8 @@ extension CVComponentThreadDetails { arguments = firstThreeMembers + [otherMembersString] } - let membersString = String( - format: formatString, + let membersString = String.nonPluralLocalizedStringWithFormat( + formatString, arguments: arguments, ) let membersAttributedString: NSAttributedString @@ -1136,41 +1136,40 @@ extension CVComponentThreadDetails { tx: tx, ) - // We need these to be CVarArgs for them to format appropriately. - let groupNamesFormatArg: [CVarArg] = mutualGroupNames + let groupNamesFormatArg: [String] = mutualGroupNames let formattedString: String switch mutualGroupNames.count { case 0: - formattedString = String( - format: OWSLocalizedString( + formattedString = String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "THREAD_DETAILS_ZERO_MUTUAL_GROUPS", comment: "A string indicating there are no mutual groups the user shares with this contact", ), - groupNamesFormatArg, + arguments: groupNamesFormatArg, ) case 1: - formattedString = String( - format: OWSLocalizedString( + formattedString = String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "THREAD_DETAILS_ONE_MUTUAL_GROUP", comment: "A string indicating a mutual group the user shares with this contact. Embeds {{mutual group name}}", ), - groupNamesFormatArg, + arguments: groupNamesFormatArg, ) case 2: - formattedString = String( - format: OWSLocalizedString( + formattedString = String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "THREAD_DETAILS_TWO_MUTUAL_GROUP", comment: "A string indicating two mutual groups the user shares with this contact. Embeds {{mutual group name}}", ), - groupNamesFormatArg, + arguments: groupNamesFormatArg, ) case 3: - formattedString = String( - format: OWSLocalizedString( + formattedString = String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "THREAD_DETAILS_THREE_MUTUAL_GROUP", comment: "A string indicating three mutual groups the user shares with this contact. Embeds {{mutual group name}}", ), - groupNamesFormatArg, + arguments: groupNamesFormatArg, ) default: // For this string, we want to use the first two groups' names diff --git a/Signal/ConversationView/ConversationInputToolbar+QuotedReplyPreview.swift b/Signal/ConversationView/ConversationInputToolbar+QuotedReplyPreview.swift index 7756ac24d6..0f2b957bae 100644 --- a/Signal/ConversationView/ConversationInputToolbar+QuotedReplyPreview.swift +++ b/Signal/ConversationView/ConversationInputToolbar+QuotedReplyPreview.swift @@ -191,8 +191,8 @@ private class QuotedMessageSnippetView: UIView { tx: tx, ).resolvedValue() } - quotedAuthor = String( - format: NSLocalizedString( + quotedAuthor = String.nonPluralLocalizedStringWithFormat( + NSLocalizedString( "QUOTED_REPLY_AUTHOR_INDICATOR_FORMAT", comment: "Indicates the author of a quoted message. Embeds {{the author's name or phone number}}.", ), diff --git a/Signal/ConversationView/ConversationViewController+Banners.swift b/Signal/ConversationView/ConversationViewController+Banners.swift index dd60122d75..afede4ea87 100644 --- a/Signal/ConversationView/ConversationViewController+Banners.swift +++ b/Signal/ConversationView/ConversationViewController+Banners.swift @@ -1115,7 +1115,7 @@ private extension ConversationViewController { "MESSAGES_VIEW_CONTACT_NO_LONGER_VERIFIED_FORMAT", comment: "Indicates that this 1:1 conversation is no longer verified. Embeds {{user's name or phone number}}.", ) - title = String(format: format, displayName) + title = String.nonPluralLocalizedStringWithFormat(format, displayName) default: title = OWSLocalizedString( diff --git a/Signal/ConversationView/ConversationViewController+BottomBar.swift b/Signal/ConversationView/ConversationViewController+BottomBar.swift index c7d86f8b9b..5c289608ec 100644 --- a/Signal/ConversationView/ConversationViewController+BottomBar.swift +++ b/Signal/ConversationView/ConversationViewController+BottomBar.swift @@ -246,7 +246,7 @@ public extension ConversationViewController { ) let learnMoreText = CommonStrings.learnMore - let attributedString = NSMutableAttributedString(string: String(format: format, learnMoreText)) + let attributedString = NSMutableAttributedString(string: String.nonPluralLocalizedStringWithFormat(format, learnMoreText)) attributedString.setAttributes( [.foregroundColor: UIColor.Signal.link], forSubstring: learnMoreText, @@ -263,7 +263,7 @@ public extension ConversationViewController { "APP_EXPIRED_BOTTOM_UPDATE", comment: "Shown in place of the text input box in a conversation when the app has expired and the user is no longer allowed to send messages. This value is a tappable link embedded in a larger sentence.", ) - let attributedString = NSMutableAttributedString(string: String(format: format, updateNowText)) + let attributedString = NSMutableAttributedString(string: String.nonPluralLocalizedStringWithFormat(format, updateNowText)) attributedString.setAttributes( [.foregroundColor: UIColor.Signal.link], forSubstring: updateNowText, @@ -280,7 +280,7 @@ public extension ConversationViewController { "NOT_REGISTERED_BOTTOM_REREGISTER", comment: "Shown in place of the text input box in a conversation when the user is no longer registered can't send messages. This value is a tappable link embedded in a larger sentence.", ) - let attributedString = NSMutableAttributedString(string: String(format: format, updateNowText)) + let attributedString = NSMutableAttributedString(string: String.nonPluralLocalizedStringWithFormat(format, updateNowText)) attributedString.setAttributes( [.foregroundColor: UIColor.Signal.link], forSubstring: updateNowText, @@ -297,7 +297,7 @@ public extension ConversationViewController { "NOT_LINKED_BOTTOM_RELINK", comment: "Shown in place of the text input box in a conversation when the user is no longer registered can't send messages. This value is a tappable link embedded in a larger sentence.", ) - let attributedString = NSMutableAttributedString(string: String(format: format, updateNowText)) + let attributedString = NSMutableAttributedString(string: String.nonPluralLocalizedStringWithFormat(format, updateNowText)) attributedString.setAttributes( [.foregroundColor: UIColor.Signal.link], forSubstring: updateNowText, diff --git a/Signal/ConversationView/ConversationViewController+CVComponentDelegate.swift b/Signal/ConversationView/ConversationViewController+CVComponentDelegate.swift index 3623e45829..5ce08c6bb6 100644 --- a/Signal/ConversationView/ConversationViewController+CVComponentDelegate.swift +++ b/Signal/ConversationView/ConversationViewController+CVComponentDelegate.swift @@ -774,7 +774,7 @@ extension ConversationViewController: CVComponentDelegate { let actionSheet = ActionSheetController( title: nil, - message: String(format: messageFormat, displayName), + message: String.nonPluralLocalizedStringWithFormat(messageFormat, displayName), ) actionSheet.customHeader = headerView @@ -802,8 +802,8 @@ extension ConversationViewController: CVComponentDelegate { let threadName = SSKEnvironment.shared.databaseStorageRef.read { transaction in SSKEnvironment.shared.contactManagerRef.displayName(for: self.thread, transaction: transaction) } - let alertMessage = String( - format: OWSLocalizedString( + let alertMessage = String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "CORRUPTED_SESSION_DESCRIPTION", comment: "ActionSheet title", ), @@ -895,8 +895,8 @@ extension ConversationViewController: CVComponentDelegate { let alert = ActionSheetController( title: CallStrings.callBackAlertTitle, - message: String( - format: CallStrings.callBackAlertMessageFormat, + message: String.nonPluralLocalizedStringWithFormat( + CallStrings.callBackAlertMessageFormat, displayName, ), ) @@ -931,8 +931,8 @@ extension ConversationViewController: CVComponentDelegate { let displayName = SSKEnvironment.shared.databaseStorageRef.read { tx in SSKEnvironment.shared.contactManagerRef.displayName(for: address, tx: tx).resolvedValue() } let alert = ActionSheetController( - title: String( - format: OWSLocalizedString( + title: String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "MISSED_CALL_BLOCKED_SYSTEM_SETTINGS_SHEET_TITLE", comment: "Title for sheet shown when the user taps a missed call from a contact blocked in iOS settings. Embeds {{ Contact's name }}", ), @@ -1069,8 +1069,8 @@ extension ConversationViewController: CVComponentDelegate { "GROUPS_BLOCK_REQUEST_SHEET_TITLE", comment: "Title for sheet asking if the user wants to block a request to join the group.", ), - message: String( - format: OWSLocalizedString( + message: String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "GROUPS_BLOCK_REQUEST_SHEET_MESSAGE", comment: "Message for sheet offering to let the user block a request to join the group. Embeds {{ the requester's name }}.", ), @@ -1230,7 +1230,7 @@ extension ConversationViewController: CVComponentDelegate { let shortDisplayName = SSKEnvironment.shared.databaseStorageRef.read { tx in return SSKEnvironment.shared.contactManagerRef.displayName(for: contactAddress, tx: tx).resolvedValue(useShortNameIfAvailable: true) } - return String(format: formatString, formattedPhoneNumber, shortDisplayName) + return String.nonPluralLocalizedStringWithFormat(formatString, formattedPhoneNumber, shortDisplayName) }() let customHeader: UIView = { let imageView = UIImageView(image: UIImage(named: "merged-chat")!) @@ -1276,8 +1276,8 @@ extension ConversationViewController: CVComponentDelegate { let message: String if thread is TSContactThread { - message = String( - format: OWSLocalizedString( + message = String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "INFO_MESSAGE_ACCEPTED_MESSAGE_REQUEST_OPTIONS_ACTION_SHEET_HEADER_CONTACT", comment: "Header for an action sheet providing options in response to an accepted 1:1 message request. Embeds {{ the name of your chat partner }}.", ), @@ -1296,8 +1296,8 @@ extension ConversationViewController: CVComponentDelegate { message: message, ) alert.addAction(ActionSheetAction( - title: String( - format: OWSLocalizedString( + title: String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "MESSAGE_REQUEST_ACCEPTED_INFO_MESSAGE_SHEET_OPTION_BLOCK", comment: "Sheet option for blocking a chat. In this case, the sheet appears when the user taps a button attached to a 'message request accepted' info message in-chat.", ), @@ -1311,8 +1311,8 @@ extension ConversationViewController: CVComponentDelegate { }, )) alert.addAction(ActionSheetAction( - title: String( - format: OWSLocalizedString( + title: String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "MESSAGE_REQUEST_ACCEPTED_INFO_MESSAGE_SHEET_OPTION_SPAM", comment: "Sheet option for reporting a chat as spam. In this case, the sheet appears when the user taps a button attached to a 'message request accepted' info message in-chat.", ), @@ -1326,8 +1326,8 @@ extension ConversationViewController: CVComponentDelegate { }, )) alert.addAction(ActionSheetAction( - title: String( - format: OWSLocalizedString( + title: String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "MESSAGE_REQUEST_ACCEPTED_INFO_MESSAGE_SHEET_OPTION_DELETE", comment: "Sheet option for deleting a chat. In this case, the sheet appears when the user taps a button attached to a 'message request accepted' info message in-chat.", ), diff --git a/Signal/ConversationView/ConversationViewController+MessageRequest.swift b/Signal/ConversationView/ConversationViewController+MessageRequest.swift index 48a9b8f8d6..70b614afd2 100644 --- a/Signal/ConversationView/ConversationViewController+MessageRequest.swift +++ b/Signal/ConversationView/ConversationViewController+MessageRequest.swift @@ -70,8 +70,8 @@ extension ConversationViewController: MessageRequestDelegate { return } - let title = String( - format: OWSLocalizedString( + let title = String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "BLOCK_LIST_UNBLOCK_TITLE_FORMAT", comment: "A format for the 'unblock conversation' action sheet title. Embeds the {{conversation title}}.", ), @@ -375,8 +375,8 @@ extension ConversationViewController { } actionSheet.addAction(ActionSheetAction( - title: String( - format: OWSLocalizedString( + title: String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "GROUPS_INVITE_BLOCK_INVITER_FORMAT", comment: "Label for 'block inviter' button in group invite view. Embeds {{name of user who invited you}}.", ), @@ -388,8 +388,8 @@ extension ConversationViewController { }) actionSheet.addAction(ActionSheetAction( - title: String( - format: OWSLocalizedString( + title: String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "GROUPS_INVITE_BLOCK_GROUP_AND_INVITER_FORMAT", comment: "Label for 'block group and inviter' button in group invite view. Embeds {{name of user who invited you}}.", ), @@ -437,7 +437,7 @@ extension ConversationViewController { let hasReportedSpam = finder.hasUserReportedSpam(transaction: tx) return (threadName, hasReportedSpam) } - let actionSheetTitle = String(format: actionSheetTitleFormat, threadName) + let actionSheetTitle = String.nonPluralLocalizedStringWithFormat(actionSheetTitleFormat, threadName) let actionSheet = ActionSheetController(title: actionSheetTitle, message: actionSheetMessage) let blockActionTitle = OWSLocalizedString( diff --git a/Signal/ConversationView/ConversationViewController+OWS.swift b/Signal/ConversationView/ConversationViewController+OWS.swift index bd74154a89..d8c1aee040 100644 --- a/Signal/ConversationView/ConversationViewController+OWS.swift +++ b/Signal/ConversationView/ConversationViewController+OWS.swift @@ -190,7 +190,7 @@ extension ConversationViewController { alertMessageFormat = OWSLocalizedString("ALERT_DELIVERY_ISSUE_UNKNOWN_THREAD_MESSAGE_FORMAT", comment: "Format string for delivery issue sheet message where the original thread is unknown. Embeds {{ sender name }}.") } - let alertMessage = String(format: alertMessageFormat, senderName) + let alertMessage = String.nonPluralLocalizedStringWithFormat(alertMessageFormat, senderName) let headerImageView = UIImageView(image: .init(named: "delivery-issue")) headerImageView.autoSetDimension(.height, toSize: 110) diff --git a/Signal/ConversationView/Reactions/EmojiCountsCollectionView.swift b/Signal/ConversationView/Reactions/EmojiCountsCollectionView.swift index d4ed5a53cd..ea7690245b 100644 --- a/Signal/ConversationView/Reactions/EmojiCountsCollectionView.swift +++ b/Signal/ConversationView/Reactions/EmojiCountsCollectionView.swift @@ -139,8 +139,8 @@ class EmojiCountCell: UICollectionViewCell { if item.emoji != nil { count.text = item.count.abbreviatedString } else { - count.text = String( - format: OWSLocalizedString( + count.text = String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "REACTION_DETAIL_ALL_FORMAT", comment: "The header used to indicate All reactions to a given message. Embeds {{number of reactions}}", ), diff --git a/Signal/Megaphones/UserInterface/BackupsEnabledNotificationMegaphone.swift b/Signal/Megaphones/UserInterface/BackupsEnabledNotificationMegaphone.swift index ac0e48de6a..7d5e09863d 100644 --- a/Signal/Megaphones/UserInterface/BackupsEnabledNotificationMegaphone.swift +++ b/Signal/Megaphones/UserInterface/BackupsEnabledNotificationMegaphone.swift @@ -27,8 +27,8 @@ class BackupsEnabledNotificationMegaphone: MegaphoneView { comment: "Title for system notification or megaphone when backups is enabled", ) - bodyText = String( - format: OWSLocalizedString( + bodyText = String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "BACKUPS_TURNED_ON_NOTIFICATION_BODY_FORMAT", comment: "Body for system notification or megaphone when backups is enabled. Embeds {{ time backups was enabled }}", ), diff --git a/Signal/Megaphones/UserInterface/NewLinkedDeviceNotificationMegaphone.swift b/Signal/Megaphones/UserInterface/NewLinkedDeviceNotificationMegaphone.swift index 0b3e16ae18..84a75c8ee1 100644 --- a/Signal/Megaphones/UserInterface/NewLinkedDeviceNotificationMegaphone.swift +++ b/Signal/Megaphones/UserInterface/NewLinkedDeviceNotificationMegaphone.swift @@ -26,8 +26,8 @@ final class NewLinkedDeviceNotificationMegaphone: MegaphoneView { comment: "Title for system notification when a new device is linked.", ) - let bodyText = String( - format: OWSLocalizedString( + let bodyText = String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "LINKED_DEVICE_NOTIFICATION_MEGAPHONE_BODY", comment: "Body for megaphone notification when a new device is linked. Embeds {{ time the device was linked }}", ), diff --git a/Signal/Provisioning/UserInterface/LinkAndSyncProvisioningProgressViewController.swift b/Signal/Provisioning/UserInterface/LinkAndSyncProvisioningProgressViewController.swift index 592b3b297e..c8d724a1f9 100644 --- a/Signal/Provisioning/UserInterface/LinkAndSyncProvisioningProgressViewController.swift +++ b/Signal/Provisioning/UserInterface/LinkAndSyncProvisioningProgressViewController.swift @@ -208,8 +208,8 @@ struct LinkAndSyncProvisioningProgressView: View { comment: "Progress label when the message loading has not yet started during the device linking process", ) } else if let downloadProgress = viewModel.downloadProgress { - String( - format: OWSLocalizedString( + String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "LINK_NEW_DEVICE_SYNC_DOWNLOAD_PROGRESS", comment: "Progress label showing the download progress of a linked device sync. Embeds {{ formatted downloaded size (such as megabytes), formatted total download size, formatted percentage }}", ), @@ -218,8 +218,8 @@ struct LinkAndSyncProvisioningProgressView: View { progressToShow.formatted(.owsPercent()), ) } else if !viewModel.isFinalizing { - String( - format: OWSLocalizedString( + String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "LINK_NEW_DEVICE_SYNC_PROGRESS_PERCENT", comment: "On a progress modal indicating the percent complete the sync process is. Embeds {{ formatted percentage }}", ), diff --git a/Signal/QuickRestore/OutgoingDeviceRestoreBackupPromptViewController.swift b/Signal/QuickRestore/OutgoingDeviceRestoreBackupPromptViewController.swift index 9b14e6186d..e9abdc4495 100644 --- a/Signal/QuickRestore/OutgoingDeviceRestoreBackupPromptViewController.swift +++ b/Signal/QuickRestore/OutgoingDeviceRestoreBackupPromptViewController.swift @@ -92,8 +92,8 @@ struct OutgoingDeviceRestoreBackupPromptView: View { private func lastBackupDetailsString() -> String { let date = lastBackupDetails.date - return String( - format: OWSLocalizedString( + return String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "OUTGOING_DEVICE_RESTORE_BACKUP_RESTORE_DESCRIPTION", comment: "Description for form confirming restore from backup without size detail.", ), diff --git a/Signal/Registration/UserInterface/RegistrationChangePhoneNumberConfirmationViewController.swift b/Signal/Registration/UserInterface/RegistrationChangePhoneNumberConfirmationViewController.swift index 1e45c6be95..bdf3759e28 100644 --- a/Signal/Registration/UserInterface/RegistrationChangePhoneNumberConfirmationViewController.swift +++ b/Signal/Registration/UserInterface/RegistrationChangePhoneNumberConfirmationViewController.swift @@ -54,8 +54,8 @@ class RegistrationChangePhoneNumberConfirmationViewController: OWSViewController ) let oldPhoneNumberFormatted = PhoneNumber.bestEffortLocalizedPhoneNumber(e164: state.oldE164.stringValue) let newPhoneNumberFormatted = PhoneNumber.bestEffortLocalizedPhoneNumber(e164: state.newE164.stringValue) - let descriptionText = String( - format: descriptionFormat, + let descriptionText = String.nonPluralLocalizedStringWithFormat( + descriptionFormat, oldPhoneNumberFormatted, newPhoneNumberFormatted, ) diff --git a/Signal/Registration/UserInterface/RegistrationChangePhoneNumberViewController.swift b/Signal/Registration/UserInterface/RegistrationChangePhoneNumberViewController.swift index 7cd3d58520..ca0c21c39e 100644 --- a/Signal/Registration/UserInterface/RegistrationChangePhoneNumberViewController.swift +++ b/Signal/Registration/UserInterface/RegistrationChangePhoneNumberViewController.swift @@ -115,7 +115,7 @@ class RegistrationChangePhoneNumberViewController: OWSTableViewController2 { "SETTINGS_CHANGE_PHONE_NUMBER_COUNTRY_CODE_FORMAT", comment: "Format for the 'country code' in the 'change phone number' settings. Embeds: {{ %1$@ the numeric country code prefix, %2$@ the country code abbreviation }}.", ) - let countryCodeFormatted = String(format: countryCodeFormat, valueViews.plusPrefixedCallingCode, valueViews.countryCode) + let countryCodeFormatted = String.nonPluralLocalizedStringWithFormat(countryCodeFormat, valueViews.plusPrefixedCallingCode, valueViews.countryCode) section.add(.item( name: OWSLocalizedString( "SETTINGS_CHANGE_PHONE_NUMBER_COUNTRY_CODE_FIELD", diff --git a/Signal/Registration/UserInterface/RegistrationLoadingViewController.swift b/Signal/Registration/UserInterface/RegistrationLoadingViewController.swift index a0f268dc8b..7e42ed2e3c 100644 --- a/Signal/Registration/UserInterface/RegistrationLoadingViewController.swift +++ b/Signal/Registration/UserInterface/RegistrationLoadingViewController.swift @@ -24,7 +24,7 @@ class RegistrationLoadingViewController: OWSViewController, OWSNavigationChildCo "REGISTRATION_VIEW_PHONE_NUMBER_SPINNER_LABEL_FORMAT", comment: "Label for the progress spinner shown during phone number registration. Embeds {{phone number}}.", ) - return String(format: format, e164.e164FormattedAsPhoneNumberWithoutBreaks) + return String.nonPluralLocalizedStringWithFormat(format, e164.e164FormattedAsPhoneNumberWithoutBreaks) case .submittingVerificationCode: return OWSLocalizedString( "ONBOARDING_VERIFICATION_CODE_VALIDATION_PROGRESS_LABEL", diff --git a/Signal/Registration/UserInterface/RegistrationPhoneNumberDiscoverabilityViewController.swift b/Signal/Registration/UserInterface/RegistrationPhoneNumberDiscoverabilityViewController.swift index 893168636f..c784c5daae 100644 --- a/Signal/Registration/UserInterface/RegistrationPhoneNumberDiscoverabilityViewController.swift +++ b/Signal/Registration/UserInterface/RegistrationPhoneNumberDiscoverabilityViewController.swift @@ -94,7 +94,7 @@ class RegistrationPhoneNumberDiscoverabilityViewController: OWSViewController { comment: "Explanation of the 'onboarding phone number discoverability' view. Embeds {user phone number}", ) let subtitleLabel = UILabel.explanationLabelForRegistration( - text: String(format: explanationTextFormat, formattedPhoneNumber), + text: String.nonPluralLocalizedStringWithFormat(explanationTextFormat, formattedPhoneNumber), ) subtitleLabel.accessibilityIdentifier = "registration.phoneNumberDiscoverability.explanationLabel" diff --git a/Signal/Registration/UserInterface/RegistrationPhoneNumberViewState.swift b/Signal/Registration/UserInterface/RegistrationPhoneNumberViewState.swift index 59d9fb7458..ea391d2f49 100644 --- a/Signal/Registration/UserInterface/RegistrationPhoneNumberViewState.swift +++ b/Signal/Registration/UserInterface/RegistrationPhoneNumberViewState.swift @@ -136,6 +136,6 @@ extension RegistrationPhoneNumberViewState.ValidationError.RateLimited { }() let durationString = retryAfterFormatter.string(from: Date(timeIntervalSinceReferenceDate: timeRemaining)) - return String(format: rateLimitFormat, durationString) + return String.nonPluralLocalizedStringWithFormat(rateLimitFormat, durationString) } } diff --git a/Signal/Registration/UserInterface/RegistrationReglockTimeoutViewController.swift b/Signal/Registration/UserInterface/RegistrationReglockTimeoutViewController.swift index 1f2b58ac83..f0e3108c52 100644 --- a/Signal/Registration/UserInterface/RegistrationReglockTimeoutViewController.swift +++ b/Signal/Registration/UserInterface/RegistrationReglockTimeoutViewController.swift @@ -133,8 +133,8 @@ class RegistrationReglockTimeoutViewController: OWSViewController { return UInt32(result <= oneMinute ? oneMinute : result) }() - return String( - format: format, + return String.nonPluralLocalizedStringWithFormat( + format, DateUtil.formatDuration(seconds: remainingSeconds, useShortFormat: false), ) } diff --git a/Signal/Registration/UserInterface/RegistrationRestoreFromBackupConfirmationViewController.swift b/Signal/Registration/UserInterface/RegistrationRestoreFromBackupConfirmationViewController.swift index 04c61b91b1..59df64e44e 100644 --- a/Signal/Registration/UserInterface/RegistrationRestoreFromBackupConfirmationViewController.swift +++ b/Signal/Registration/UserInterface/RegistrationRestoreFromBackupConfirmationViewController.swift @@ -206,7 +206,7 @@ struct RegistrationRestoreFromBackupConfirmationView: View { let formattedDate = DateUtil.dateFormatter.string(for: date), let formattedTime = DateUtil.timeFormatter.string(for: date) { - formattedString = String(format: formattedString, formattedDate, formattedTime) + formattedString = String.nonPluralLocalizedStringWithFormat(formattedString, formattedDate, formattedTime) return Text(formattedString) } else { return Text("") @@ -222,7 +222,7 @@ struct RegistrationRestoreFromBackupConfirmationView: View { let formattedDate = DateUtil.dateFormatter.string(for: date), let formattedTime = DateUtil.timeFormatter.string(for: date) { - formattedString = String(format: formattedString, formattedDate, formattedTime, OWSFormat.formatFileSize(size)) + formattedString = String.nonPluralLocalizedStringWithFormat(formattedString, formattedDate, formattedTime, OWSFormat.formatFileSize(size)) return Text(formattedString) } else { return Text("") diff --git a/Signal/Registration/UserInterface/RegistrationVerificationViewController.swift b/Signal/Registration/UserInterface/RegistrationVerificationViewController.swift index cc90a686f0..39bb64312f 100644 --- a/Signal/Registration/UserInterface/RegistrationVerificationViewController.swift +++ b/Signal/Registration/UserInterface/RegistrationVerificationViewController.swift @@ -136,7 +136,7 @@ class RegistrationVerificationViewController: OWSViewController { "ONBOARDING_VERIFICATION_TITLE_DEFAULT_FORMAT", comment: "Format for the title of the 'onboarding verification' view. Embeds {{the user's phone number}}.", ) - return String(format: format, state.e164.stringValue.e164FormattedAsPhoneNumberWithoutBreaks) + return String.nonPluralLocalizedStringWithFormat(format, state.e164.stringValue.e164FormattedAsPhoneNumberWithoutBreaks) } private lazy var explanationLabel: UILabel = { @@ -385,7 +385,7 @@ class RegistrationVerificationViewController: OWSViewController { button.configuration?.title = { let timeRemaining = max(date.timeIntervalSince(now), 0) let durationString = retryAfterFormatter.string(from: Date(timeIntervalSinceReferenceDate: timeRemaining)) - return String(format: countdownFormat, durationString) + return String.nonPluralLocalizedStringWithFormat(countdownFormat, durationString) }() } } @@ -513,7 +513,7 @@ class RegistrationVerificationViewController: OWSViewController { let timeRemaining = max(nextVerificationAttemptDate.timeIntervalSince(now), 0) let durationString = formatter.string(from: Date(timeIntervalSinceReferenceDate: timeRemaining)) - let message = String(format: format, durationString) + let message = String.nonPluralLocalizedStringWithFormat(format, durationString) OWSActionSheets.showActionSheet(title: nil, message: message) } } diff --git a/Signal/Registration/UserInterface/RegistrationViewUtil.swift b/Signal/Registration/UserInterface/RegistrationViewUtil.swift index 0e780b3f94..f2889bdc26 100644 --- a/Signal/Registration/UserInterface/RegistrationViewUtil.swift +++ b/Signal/Registration/UserInterface/RegistrationViewUtil.swift @@ -253,7 +253,7 @@ extension ActionSheetController { "REGISTRATION_VIEW_PHONE_NUMBER_CONFIRMATION_ALERT_TITLE_FORMAT", comment: "Title for confirmation alert during phone number registration. Embeds {{phone number}}.", ) - return String(format: format, e164.e164FormattedAsPhoneNumberWithoutBreaks) + return String.nonPluralLocalizedStringWithFormat(format, e164.e164FormattedAsPhoneNumberWithoutBreaks) }(), message: message, ) diff --git a/Signal/src/ViewControllers/AppSettings/Account/DeleteAccountConfirmationViewController.swift b/Signal/src/ViewControllers/AppSettings/Account/DeleteAccountConfirmationViewController.swift index 14cbf18b3b..7f72d846b2 100644 --- a/Signal/src/ViewControllers/AppSettings/Account/DeleteAccountConfirmationViewController.swift +++ b/Signal/src/ViewControllers/AppSettings/Account/DeleteAccountConfirmationViewController.swift @@ -230,7 +230,7 @@ class DeleteAccountConfirmationViewController: OWSTableViewController2 { "SETTINGS_DELETE_ACCOUNT_PAYMENTS_BALANCE_ALERT_MESSAGE_FORMAT", comment: "Body for the alert confirming whether the user wants transfer their payments balance before deleting their account. Embeds: {{ the current payment balance }}.", ) - let message = String(format: messageFormat, formattedBalance) + let message = String.nonPluralLocalizedStringWithFormat(messageFormat, formattedBalance) let actionSheet = ActionSheetController(title: title, message: message) diff --git a/Signal/src/ViewControllers/AppSettings/ComposeSupportEmailOperation.swift b/Signal/src/ViewControllers/AppSettings/ComposeSupportEmailOperation.swift index 58d2e0b96d..d020558ea3 100644 --- a/Signal/src/ViewControllers/AppSettings/ComposeSupportEmailOperation.swift +++ b/Signal/src/ViewControllers/AppSettings/ComposeSupportEmailOperation.swift @@ -225,8 +225,8 @@ final class ComposeSupportEmailOperation: NSObject { "SUPPORT_EMAIL_INFO_DIVIDER", comment: "Localized divider for support request emails internal information", ), - String( - format: OWSLocalizedString( + String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "SUPPORT_EMAIL_FILTER_LABEL_FORMAT", comment: "Localized label for support request email filter string. Embeds {{filter text}}.", ), @@ -236,22 +236,22 @@ final class ComposeSupportEmailOperation: NSObject { format: "Challenge Received: %@", model.hasRecentChallenge ? "yes" : "no", ), - String( - format: OWSLocalizedString( + String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "SUPPORT_EMAIL_HARDWARE_LABEL_FORMAT", comment: "Localized label for support request email hardware string (e.g. iPhone or iPad). Embeds {{hardware text}}.", ), model.deviceType, ), - String( - format: OWSLocalizedString( + String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "SUPPORT_EMAIL_HID_LABEL_FORMAT", comment: "Localized label for support request email HID string (e.g. 12,1). Embeds {{hid text}}.", ), model.deviceIdentifier, ), - String( - format: OWSLocalizedString( + String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "SUPPORT_EMAIL_IOS_VERSION_LABEL_FORMAT", comment: "Localized label for support request email iOS Version string (e.g. 13.4). Embeds {{ios version}}.", ), @@ -260,8 +260,8 @@ final class ComposeSupportEmailOperation: NSObject { "Signal Version: \(model.signalAppVersion)", { if let debugURLString = model.resolvedDebugString { - return String( - format: OWSLocalizedString( + return String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "SUPPORT_EMAIL_LOG_URL_LABEL_FORMAT", comment: "Localized label for support request email debug log URL. Embeds {{debug log url}}.", ), @@ -269,8 +269,8 @@ final class ComposeSupportEmailOperation: NSObject { ) } else { return nil } }(), - String( - format: OWSLocalizedString( + String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "SUPPORT_EMAIL_LOCALE_LABEL_FORMAT", comment: "Localized label for support request email locale string. Embeds {{locale}}.", ), diff --git a/Signal/src/ViewControllers/AppSettings/Donations/BadgeGiftingConfirmationViewController.swift b/Signal/src/ViewControllers/AppSettings/Donations/BadgeGiftingConfirmationViewController.swift index b6c751e3b7..513eda6cc1 100644 --- a/Signal/src/ViewControllers/AppSettings/Donations/BadgeGiftingConfirmationViewController.swift +++ b/Signal/src/ViewControllers/AppSettings/Donations/BadgeGiftingConfirmationViewController.swift @@ -263,7 +263,7 @@ class BadgeGiftingConfirmationViewController: OWSTableViewController2 { let durationString = String.formatDurationLossless( durationSeconds: disappearingMessagesDuration, ) - disappearingMessagesInfoLabel.text = String(format: format, durationString) + disappearingMessagesInfoLabel.text = String.nonPluralLocalizedStringWithFormat(format, durationString) cell.contentView.addSubview(disappearingMessagesInfoLabel) disappearingMessagesInfoLabel.autoPinEdgesToSuperviewMargins() diff --git a/Signal/src/ViewControllers/AppSettings/Donations/DonationSettingsViewController+MySupport.swift b/Signal/src/ViewControllers/AppSettings/Donations/DonationSettingsViewController+MySupport.swift index 4c8fa273f4..99102a2f8c 100644 --- a/Signal/src/ViewControllers/AppSettings/Donations/DonationSettingsViewController+MySupport.swift +++ b/Signal/src/ViewControllers/AppSettings/Donations/DonationSettingsViewController+MySupport.swift @@ -258,7 +258,7 @@ extension DonationSettingsViewController { let currencyString = CurrencyFormatter.format(money: amount) - return String(format: pricingFormat, currencyString) + return String.nonPluralLocalizedStringWithFormat(pricingFormat, currencyString) }() let statusSubtitle: String = { @@ -277,7 +277,7 @@ extension DonationSettingsViewController { dateFormatter.dateStyle = .medium let dateArg = dateFormatter.string(from: subscription.endOfCurrentPeriod) - return String(format: renewalFormat, dateArg) + return String.nonPluralLocalizedStringWithFormat(renewalFormat, dateArg) case .pendingAuthorization: owsFailDebug("Should not be displaying a subscription message for a donation pending authorization.") return OWSLocalizedString( @@ -375,8 +375,8 @@ extension DonationSettingsViewController { comment: "A string describing the amount and currency of a one-time payment. Embeds {{ the amount, formatted as a currency }}.", ) - return String( - format: pricingFormat, + return String.nonPluralLocalizedStringWithFormat( + pricingFormat, CurrencyFormatter.format(money: amount), ) }() @@ -496,7 +496,7 @@ extension DonationSettingsViewController { paymentMethod: paymentMethod, ) - return String(format: messageFormat, chargeFailureString) + return String.nonPluralLocalizedStringWithFormat(messageFormat, chargeFailureString) }() let actionSheet = ActionSheetController( @@ -533,7 +533,7 @@ extension DonationSettingsViewController { paymentMethod: paymentMethod, ) - return String(format: messageFormat, chargeFailureString) + return String.nonPluralLocalizedStringWithFormat(messageFormat, chargeFailureString) }() let actionSheet = ActionSheetController( diff --git a/Signal/src/ViewControllers/AppSettings/Donations/DonationSettingsViewController.swift b/Signal/src/ViewControllers/AppSettings/Donations/DonationSettingsViewController.swift index e81d81b8f8..221c1bcf2e 100644 --- a/Signal/src/ViewControllers/AppSettings/Donations/DonationSettingsViewController.swift +++ b/Signal/src/ViewControllers/AppSettings/Donations/DonationSettingsViewController.swift @@ -556,7 +556,7 @@ class DonationSettingsViewController: OWSTableViewController2 { "DONATION_SETTINGS_MY_SUPPORT_IDEAL_ONE_TIME_DONATION_NOT_CONFIRMED_MESSAGE_FORMAT", comment: "Title for a sheet explaining that a payment needs confirmation.", ) - let message = String(format: messageFormat, CurrencyFormatter.format(money: pendingOneTime.amount)) + let message = String.nonPluralLocalizedStringWithFormat(messageFormat, CurrencyFormatter.format(money: pendingOneTime.amount)) showError(title: title, message: message, donationMode: .oneTime) } return true @@ -583,7 +583,7 @@ class DonationSettingsViewController: OWSTableViewController2 { "DONATION_SETTINGS_MY_SUPPORT_IDEAL_RECURRING_SUBSCRIPTION_NOT_CONFIRMED_MESSAGE_FORMAT", comment: "Message shown in a sheet explaining that the user's iDEAL recurring monthly donation hasn't been confirmed. Embeds {{ formatted current amount }}.", ) - let message = String(format: messageFormat, CurrencyFormatter.format(money: pendingSubscription.amount)) + let message = String.nonPluralLocalizedStringWithFormat(messageFormat, CurrencyFormatter.format(money: pendingSubscription.amount)) showError(title: title, message: message, donationMode: .monthly) } return true diff --git a/Signal/src/ViewControllers/AppSettings/Linked Devices/BackupRestoreProgressModal.swift b/Signal/src/ViewControllers/AppSettings/Linked Devices/BackupRestoreProgressModal.swift index 3a9d4d5c58..44c435b86a 100644 --- a/Signal/src/ViewControllers/AppSettings/Linked Devices/BackupRestoreProgressModal.swift +++ b/Signal/src/ViewControllers/AppSettings/Linked Devices/BackupRestoreProgressModal.swift @@ -240,8 +240,8 @@ struct BackupRestoreProgressView: View { comment: "Subtitle for a progress spinner on a modal when waiting for a backup restore to start", ) } else if let downloadProgress = viewModel.downloadProgress { - String( - format: OWSLocalizedString( + String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "BACKUP_RESTORE_MODAL_DOWNLOAD_PROGRESS_SUBTITLE", comment: "Subtitle for a progress spinner on a modal tracking active downloading. Embeds 1:{{ the amount downloaded as a file size, e.g. 100 MB }}; 2:{{ the total amount to download as a file size, e.g. 1 GB }}; 3:{{ the amount downloaded as a percentage, e.g. 10% }}.", ), @@ -256,8 +256,8 @@ struct BackupRestoreProgressView: View { } private var percentCompleteString: String { - String( - format: OWSLocalizedString( + String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "LINK_NEW_DEVICE_SYNC_PROGRESS_PERCENT", comment: "On a progress modal indicating the percent complete the sync process is. Embeds {{ formatted percentage }}", ), diff --git a/Signal/src/ViewControllers/AppSettings/Linked Devices/LinkedDevicesEducationSheet.swift b/Signal/src/ViewControllers/AppSettings/Linked Devices/LinkedDevicesEducationSheet.swift index f91cab24ae..a639c8746e 100644 --- a/Signal/src/ViewControllers/AppSettings/Linked Devices/LinkedDevicesEducationSheet.swift +++ b/Signal/src/ViewControllers/AppSettings/Linked Devices/LinkedDevicesEducationSheet.swift @@ -56,8 +56,8 @@ class LinkedDevicesEducationSheet: StackSheetViewController { let desktopDownloadLinkString = "signal.org/download" let desktopDownloadURL = URL(string: "https://signal.org/download/")! - let downloadsString = String( - format: OWSLocalizedString( + let downloadsString = String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "LINKED_DEVICES_EDUCATION_POINT_DOWNLOADS", comment: "Bullet point about downloads on the linked devices education sheet. Embeds {{ %1$@ iPad download link, %2$@ desktop download link }}", ), diff --git a/Signal/src/ViewControllers/AppSettings/Linked Devices/LinkedDevicesView.swift b/Signal/src/ViewControllers/AppSettings/Linked Devices/LinkedDevicesView.swift index 641c558010..3b88e7f89e 100644 --- a/Signal/src/ViewControllers/AppSettings/Linked Devices/LinkedDevicesView.swift +++ b/Signal/src/ViewControllers/AppSettings/Linked Devices/LinkedDevicesView.swift @@ -491,7 +491,7 @@ class LinkedDevicesHostingController: HostingContainer { ) } - presentToast(text: String(format: title, deviceName)) + presentToast(text: String.nonPluralLocalizedStringWithFormat(title, deviceName)) } private func showUpdateFailureAlert(error: Error) { @@ -710,7 +710,7 @@ class LinkedDevicesHostingController: HostingContainer { "UNLINK_CONFIRMATION_ALERT_TITLE", comment: "Alert title for confirming device deletion", ) - let title = String(format: titleFormat, displayableDevice.displayName) + let title = String.nonPluralLocalizedStringWithFormat(titleFormat, displayableDevice.displayName) let message = OWSLocalizedString( "UNLINK_CONFIRMATION_ALERT_BODY", comment: "Alert message to confirm unlinking a device", @@ -890,8 +890,8 @@ struct LinkedDevicesView: View { } private func dateFormattedString(format: String, date: Date) -> String { - String( - format: format, + String.nonPluralLocalizedStringWithFormat( + format, DateUtil.dateFormatter.string(from: date), ) } diff --git a/Signal/src/ViewControllers/AppSettings/Notifications/NotificationSettingsSoundViewController.swift b/Signal/src/ViewControllers/AppSettings/Notifications/NotificationSettingsSoundViewController.swift index 2a1c20158e..9ed59f591f 100644 --- a/Signal/src/ViewControllers/AppSettings/Notifications/NotificationSettingsSoundViewController.swift +++ b/Signal/src/ViewControllers/AppSettings/Notifications/NotificationSettingsSoundViewController.swift @@ -70,8 +70,8 @@ class NotificationSettingsSoundViewController: OWSTableViewController2 { for sound in Sounds.allNotificationSounds { let soundName: String if sound == .standard(.note) { - soundName = String( - format: OWSLocalizedString( + soundName = String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "SETTINGS_AUDIO_DEFAULT_TONE_LABEL_FORMAT", comment: "Format string for the default 'Note' sound. Embeds the system {{sound name}}.", ), diff --git a/Signal/src/ViewControllers/AppSettings/Payments/PaymentsDetailViewController.swift b/Signal/src/ViewControllers/AppSettings/Payments/PaymentsDetailViewController.swift index dd40467d12..3f63686e82 100644 --- a/Signal/src/ViewControllers/AppSettings/Payments/PaymentsDetailViewController.swift +++ b/Signal/src/ViewControllers/AppSettings/Payments/PaymentsDetailViewController.swift @@ -261,7 +261,7 @@ class PaymentsDetailViewController: OWSTableViewController2 { ) } }() - title = String(format: titleFormat, username) + title = String.nonPluralLocalizedStringWithFormat(titleFormat, username) } else { if paymentItem.isIncoming { title = OWSLocalizedString( @@ -328,8 +328,8 @@ class PaymentsDetailViewController: OWSTableViewController2 { "SETTINGS_PAYMENTS_PAYMENT_DETAILS_SENDER_FORMAT", comment: "Format for the sender info in the payment details view in the app settings. Embeds {{ %1$@ the name of the sender of the payment, %2$@ the date the transaction was sent }}.", ) - value = String( - format: senderFormat, + value = String.nonPluralLocalizedStringWithFormat( + senderFormat, sender, TSPaymentModel.formatDate(mcLedgerBlockDate, isLongForm: true), ) @@ -470,7 +470,7 @@ class PaymentsDetailViewController: OWSTableViewController2 { comment: "Format string for the recipient of an outgoing payment. Embeds: {{ the name of the recipient of the payment}}.", ), ) - usernameLabel.text = String(format: displayNameFormat, displayName) + usernameLabel.text = String.nonPluralLocalizedStringWithFormat(displayNameFormat, displayName) } let headerStack = UIStackView(arrangedSubviews: stackViews) diff --git a/Signal/src/ViewControllers/AppSettings/Payments/PaymentsRestoreWalletWordViewController.swift b/Signal/src/ViewControllers/AppSettings/Payments/PaymentsRestoreWalletWordViewController.swift index 9e26ad4253..57163a4104 100644 --- a/Signal/src/ViewControllers/AppSettings/Payments/PaymentsRestoreWalletWordViewController.swift +++ b/Signal/src/ViewControllers/AppSettings/Payments/PaymentsRestoreWalletWordViewController.swift @@ -116,7 +116,7 @@ public class PaymentsRestoreWalletWordViewController: OWSViewController { "SETTINGS_PAYMENTS_RESTORE_WALLET_WORD_INSTRUCTIONS_FORMAT", comment: "Format for the instructions for the 'enter word' step of the 'restore payments wallet' views. Embeds {{ the index of the current word }}.", ) - let instructions = String(format: instructionsFormat, OWSFormat.formatInt(wordIndex + 1)) + let instructions = String.nonPluralLocalizedStringWithFormat(instructionsFormat, OWSFormat.formatInt(wordIndex + 1)) let instructionsLabel = UILabel() instructionsLabel.text = instructions @@ -152,8 +152,8 @@ public class PaymentsRestoreWalletWordViewController: OWSViewController { comment: "Format for the placeholder text in the 'confirm payments passphrase' view of the app settings. Embeds: {{ the index of the word }}.", ) let placeholder = NSAttributedString( - string: String( - format: placeholderFormat, + string: String.nonPluralLocalizedStringWithFormat( + placeholderFormat, OWSFormat.formatInt(wordIndex + 1), ), attributes: [ diff --git a/Signal/src/ViewControllers/AppSettings/Payments/PaymentsSettingsViewController.swift b/Signal/src/ViewControllers/AppSettings/Payments/PaymentsSettingsViewController.swift index 3f2a15b0f0..de74c36f8c 100644 --- a/Signal/src/ViewControllers/AppSettings/Payments/PaymentsSettingsViewController.swift +++ b/Signal/src/ViewControllers/AppSettings/Payments/PaymentsSettingsViewController.swift @@ -623,7 +623,7 @@ public class PaymentsSettingsViewController: OWSTableViewController2 { "SETTINGS_PAYMENTS_BALANCE_CONVERSION_FORMAT", comment: "Format string for the 'local balance converted into local currency' indicator. Embeds: {{ %1$@ the local balance in the local currency, %2$@ the local currency code, %3$@ the date the currency conversion rate was obtained. }}..", ) - return String(format: formatString, fiatAmountString, localCurrencyCode, conversionFreshnessString) + return String.nonPluralLocalizedStringWithFormat(formatString, fiatAmountString, localCurrencyCode, conversionFreshnessString) } private func configureHistorySection( diff --git a/Signal/src/ViewControllers/AppSettings/Payments/PaymentsViewPassphraseConfirmViewController.swift b/Signal/src/ViewControllers/AppSettings/Payments/PaymentsViewPassphraseConfirmViewController.swift index f1c1ab31e4..9e0a3adbac 100644 --- a/Signal/src/ViewControllers/AppSettings/Payments/PaymentsViewPassphraseConfirmViewController.swift +++ b/Signal/src/ViewControllers/AppSettings/Payments/PaymentsViewPassphraseConfirmViewController.swift @@ -156,7 +156,7 @@ public class PaymentsViewPassphraseConfirmViewController: OWSTableViewController "SETTINGS_PAYMENTS_VIEW_PASSPHRASE_CONFIRM_PLACEHOLDER_FORMAT", comment: "Format for the placeholder text in the 'confirm payments passphrase' view of the app settings. Embeds: {{ the index of the word }}.", ) - textfield.placeholder = String(format: placeholderFormat, OWSFormat.formatInt(wordIndex + 1)) + textfield.placeholder = String.nonPluralLocalizedStringWithFormat(placeholderFormat, OWSFormat.formatInt(wordIndex + 1)) } configureTextfield(wordTextfield0, wordIndex: wordIndex0) configureTextfield(wordTextfield1, wordIndex: wordIndex1) @@ -306,8 +306,8 @@ public class PaymentsViewPassphraseConfirmViewController: OWSTableViewController "SETTINGS_PAYMENTS_VIEW_PASSPHRASE_CONFIRM_EXPLANATION_FORMAT", comment: "Format for the explanation of the 'confirm payments passphrase word' step in the 'view payments passphrase' settings, indicating that the user needs to enter two words from their payments passphrase. Embeds: {{ %1$@ the index of the first word, %2$@ the index of the second word }}.", ) - let explanation = String( - format: explanationForm, + let explanation = String.nonPluralLocalizedStringWithFormat( + explanationForm, OWSFormat.formatInt(wordIndex0 + 1), OWSFormat.formatInt(wordIndex1 + 1), ) diff --git a/Signal/src/ViewControllers/AppSettings/Profile/ProfileBioViewController.swift b/Signal/src/ViewControllers/AppSettings/Profile/ProfileBioViewController.swift index 8ad908d8d6..1259a6da68 100644 --- a/Signal/src/ViewControllers/AppSettings/Profile/ProfileBioViewController.swift +++ b/Signal/src/ViewControllers/AppSettings/Profile/ProfileBioViewController.swift @@ -103,7 +103,7 @@ class ProfileBioViewController: OWSTableViewController2 { "PROFILE_BIO_VIEW_TITLE_FORMAT", comment: "Title for the profile bio view. Embeds {{ the number of characters that can be added to the profile bio without hitting the length limit }}.", ) - title = String(format: titleFormat, OWSFormat.formatInt(remainingGlyphCount)) + title = String.nonPluralLocalizedStringWithFormat(titleFormat, OWSFormat.formatInt(remainingGlyphCount)) } else { title = OWSLocalizedString("PROFILE_BIO_VIEW_TITLE", comment: "Title for the profile bio view.") } diff --git a/Signal/src/ViewControllers/AppSettings/Profile/ProfileSettingsViewController.swift b/Signal/src/ViewControllers/AppSettings/Profile/ProfileSettingsViewController.swift index 0f2693dbbc..7d4403ac72 100644 --- a/Signal/src/ViewControllers/AppSettings/Profile/ProfileSettingsViewController.swift +++ b/Signal/src/ViewControllers/AppSettings/Profile/ProfileSettingsViewController.swift @@ -551,8 +551,8 @@ class ProfileSettingsViewController: OWSTableViewController2 { private func offerToDeleteUsername(currentUsername: String) { OWSActionSheets.showConfirmationAlert( - message: String( - format: OWSLocalizedString( + message: String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "PROFILE_SETTINGS_USERNAME_DELETION_CONFIRMATION_ALERT_MESSAGE_FORMAT", comment: "A message asking the user if they are sure they want to remove their username and explaining what will happen. Embeds {{ the user's current username }}.", ), diff --git a/Signal/src/ViewControllers/DatabaseRecoveryViewController.swift b/Signal/src/ViewControllers/DatabaseRecoveryViewController.swift index 203fb393f6..87dbe085fd 100644 --- a/Signal/src/ViewControllers/DatabaseRecoveryViewController.swift +++ b/Signal/src/ViewControllers/DatabaseRecoveryViewController.swift @@ -468,7 +468,7 @@ class DatabaseRecoveryViewController: OWSViewController { owsFailDebug("Could not format the database size for some reason") return String(currentDatabaseSize) }() - return String(format: labelFormat, formattedBytes) + return String.nonPluralLocalizedStringWithFormat(labelFormat, formattedBytes) }() let continueButton = button( diff --git a/Signal/src/ViewControllers/Donations/BadgeDetailsSheet.swift b/Signal/src/ViewControllers/Donations/BadgeDetailsSheet.swift index 1c91b8b108..4d848beb14 100644 --- a/Signal/src/ViewControllers/Donations/BadgeDetailsSheet.swift +++ b/Signal/src/ViewControllers/Donations/BadgeDetailsSheet.swift @@ -92,7 +92,7 @@ class BadgeDetailsSheet: OWSTableSheetViewController { "BADGE_DETAILS_TITLE_FOR_SUPPORTER", comment: "When viewing someone else's donor badge, you'll see a sheet. This is the title on that sheet. Embeds {badge owner's short name}", ) - return String(format: format, remoteSupporterName) + return String.nonPluralLocalizedStringWithFormat(format, remoteSupporterName) } else { return self.focusedBadge.localizedName } diff --git a/Signal/src/ViewControllers/Donations/BadgeGiftingAlreadyRedeemedSheet.swift b/Signal/src/ViewControllers/Donations/BadgeGiftingAlreadyRedeemedSheet.swift index 32b28b6614..87e496f52c 100644 --- a/Signal/src/ViewControllers/Donations/BadgeGiftingAlreadyRedeemedSheet.swift +++ b/Signal/src/ViewControllers/Donations/BadgeGiftingAlreadyRedeemedSheet.swift @@ -58,7 +58,7 @@ class BadgeGiftingAlreadyRedeemedSheet: OWSTableSheetViewController { titleLabel.textColor = .Signal.label titleLabel.textAlignment = .center titleLabel.numberOfLines = 0 - titleLabel.text = String(format: titleFormat, self.shortName) + titleLabel.text = String.nonPluralLocalizedStringWithFormat(titleFormat, self.shortName) stackView.addArrangedSubview(titleLabel) stackView.setCustomSpacing(12, after: titleLabel) @@ -70,7 +70,7 @@ class BadgeGiftingAlreadyRedeemedSheet: OWSTableSheetViewController { label.font = .dynamicTypeBody label.textColor = .Signal.label label.numberOfLines = 0 - label.text = String(format: labelFormat, self.shortName) + label.text = String.nonPluralLocalizedStringWithFormat(labelFormat, self.shortName) label.textAlignment = .center stackView.addArrangedSubview(label) diff --git a/Signal/src/ViewControllers/Donations/BadgeGiftingThanksSheet.swift b/Signal/src/ViewControllers/Donations/BadgeGiftingThanksSheet.swift index a75773c168..db956ff63a 100644 --- a/Signal/src/ViewControllers/Donations/BadgeGiftingThanksSheet.swift +++ b/Signal/src/ViewControllers/Donations/BadgeGiftingThanksSheet.swift @@ -77,7 +77,7 @@ class BadgeGiftingThanksSheet: OWSTableViewController2 { "DONATION_ON_BEHALF_OF_A_FRIEND_THANKS_BODY_FORMAT", comment: "When you donate on behalf of a friend, a thank-you sheet will appear. This is the text on that sheet. Embeds {{recipient name}}.", ) - let infoLabel = UILabel.explanationTextLabel(text: String(format: infoLabelFormat, recipientName)) + let infoLabel = UILabel.explanationTextLabel(text: String.nonPluralLocalizedStringWithFormat(infoLabelFormat, recipientName)) cell.contentView.addSubview(infoLabel) infoLabel.autoPinEdgesToSuperviewMargins() diff --git a/Signal/src/ViewControllers/Donations/BadgeIssueSheet.swift b/Signal/src/ViewControllers/Donations/BadgeIssueSheet.swift index 464f7732a4..49e1c3232a 100644 --- a/Signal/src/ViewControllers/Donations/BadgeIssueSheet.swift +++ b/Signal/src/ViewControllers/Donations/BadgeIssueSheet.swift @@ -117,7 +117,7 @@ public class BadgeIssueSheetState { ) return Body( - String(format: formatText, chargeFailureString), + String.nonPluralLocalizedStringWithFormat(formatText, chargeFailureString), learnMoreLink: URL.Support.Donations.badgeExpiration, ) case let .boostExpired(hasCurrentSubscription): @@ -145,7 +145,7 @@ public class BadgeIssueSheetState { "DONATION_FROM_A_FRIEND_BADGE_NOT_REDEEMED_BODY_FORMAT", comment: "Someone donated on your behalf and you got a badge, which expired before you could redeem it. A sheet appears to tell you about this. This is the text on that sheet. Embeds {{contact name}}.", ) - return Body(String(format: formatText, fullName)) + return Body(String.nonPluralLocalizedStringWithFormat(formatText, fullName)) case let .bankPaymentFailed(chargeFailureCode): let formatText = OWSLocalizedString( "DONATION_BADGE_ISSUE_SHEET_BANK_PAYMENT_FAILED_MESSAGE", @@ -157,7 +157,7 @@ public class BadgeIssueSheetState { paymentMethod: .sepa, ) - return Body(String(format: formatText, chargeFailureString)) + return Body(String.nonPluralLocalizedStringWithFormat(formatText, chargeFailureString)) case .boostBankPaymentProcessing: let bodyFormat = OWSLocalizedString( "DONATION_BADGE_ISSUE_SHEET_ONE_TIME_BANK_PAYMENT_PROCESSING_MESSAGE", @@ -165,7 +165,7 @@ public class BadgeIssueSheetState { ) return Body( - String(format: bodyFormat, badge.localizedName), + String.nonPluralLocalizedStringWithFormat(bodyFormat, badge.localizedName), learnMoreLink: URL.Support.Donations.donationPending, ) case .subscriptionBankPaymentProcessing: @@ -175,7 +175,7 @@ public class BadgeIssueSheetState { ) return Body( - String(format: bodyFormat, badge.localizedName), + String.nonPluralLocalizedStringWithFormat(bodyFormat, badge.localizedName), learnMoreLink: URL.Support.Donations.donationPending, ) } diff --git a/Signal/src/ViewControllers/Donations/BadgeThanksSheet.swift b/Signal/src/ViewControllers/Donations/BadgeThanksSheet.swift index c64aec5531..15ad0e23bb 100644 --- a/Signal/src/ViewControllers/Donations/BadgeThanksSheet.swift +++ b/Signal/src/ViewControllers/Donations/BadgeThanksSheet.swift @@ -264,7 +264,7 @@ class BadgeThanksSheet: OWSTableSheetViewController { "DONATION_ON_BEHALF_OF_A_FRIEND_REDEEM_BADGE_TITLE_FORMAT", comment: "A friend has donated on your behalf and you received a badge. A sheet opens for you to redeem this badge. Embeds {{contact's short name, such as a first name}}.", ) - return String(format: formatText, shortName) + return String.nonPluralLocalizedStringWithFormat(formatText, shortName) } } @@ -280,13 +280,13 @@ class BadgeThanksSheet: OWSTableSheetViewController { "BADGE_THANKS_BODY", comment: "When you make a donation to Signal, you will receive a badge. A thank-you sheet appears when this happens. This is the body text on that sheet.", ) - return String(format: formatText, self.badge.localizedName) + return String.nonPluralLocalizedStringWithFormat(formatText, self.badge.localizedName) case let .giftReceived(shortName, _, _): let formatText = OWSLocalizedString( "DONATION_ON_BEHALF_OF_A_FRIEND_YOU_RECEIVED_A_BADGE_FORMAT", comment: "A friend has donated on your behalf and you received a badge. This text says that you received a badge, and from whom. Embeds {{contact's short name, such as a first name}}.", ) - return String(format: formatText, shortName) + return String.nonPluralLocalizedStringWithFormat(formatText, shortName) } } diff --git a/Signal/src/ViewControllers/Donations/DonateChoosePaymentMethodSheet.swift b/Signal/src/ViewControllers/Donations/DonateChoosePaymentMethodSheet.swift index 942e42bdb8..a844ca1b80 100644 --- a/Signal/src/ViewControllers/Donations/DonateChoosePaymentMethodSheet.swift +++ b/Signal/src/ViewControllers/Donations/DonateChoosePaymentMethodSheet.swift @@ -30,24 +30,24 @@ class DonateChoosePaymentMethodSheet: StackSheetViewController { "DONATE_CHOOSE_PAYMENT_METHOD_SHEET_TITLE_FOR_ONE_TIME_DONATION", comment: "When users make one-time donations, they see a sheet that lets them pick a payment method. This is the title on that sheet. Embeds {{amount of money}}, such as \"$5\".", ) - return String(format: format, currencyString) + return String.nonPluralLocalizedStringWithFormat(format, currencyString) case .monthly: let moneyPerMonthFormat = OWSLocalizedString( "SUSTAINER_VIEW_PRICING", comment: "Pricing text for sustainer view badges, embeds {{price}}", ) - let moneyPerMonthString = String(format: moneyPerMonthFormat, currencyString) + let moneyPerMonthString = String.nonPluralLocalizedStringWithFormat(moneyPerMonthFormat, currencyString) let format = OWSLocalizedString( "DONATE_CHOOSE_PAYMENT_METHOD_SHEET_TITLE_FOR_MONTHLY_DONATION", comment: "When users make monthly donations, they see a sheet that lets them pick a payment method. This is the title on that sheet. Embeds {{amount of money per month}}, such as \"$5/month\".", ) - return String(format: format, moneyPerMonthString) + return String.nonPluralLocalizedStringWithFormat(format, moneyPerMonthString) case .gift: let format = OWSLocalizedString( "DONATE_CHOOSE_PAYMENT_METHOD_SHEET_TITLE_FOR_DONATION_ON_BEHALF_OF_A_FRIEND", comment: "When users make donations on a friend's behalf, they see a sheet that lets them pick a payment method. This is the title on that sheet. Embeds {{amount of money}}, such as \"$5\".", ) - return String(format: format, currencyString) + return String.nonPluralLocalizedStringWithFormat(format, currencyString) } } @@ -58,21 +58,21 @@ class DonateChoosePaymentMethodSheet: StackSheetViewController { "DONATE_CHOOSE_PAYMENT_METHOD_SHEET_SUBTITLE_FOR_ONE_TIME_DONATION", comment: "When users make one-time donations, they see a sheet that lets them pick a payment method. It also tells them what they'll be doing when they pay: receive a badge for a month. This is the subtitle on that sheet. Embeds {{localized badge name}}, such as \"Boost\".", ) - return String(format: format, badge.localizedName) + return String.nonPluralLocalizedStringWithFormat(format, badge.localizedName) case .monthly: let format = OWSLocalizedString( "DONATE_CHOOSE_PAYMENT_METHOD_SHEET_SUBTITLE_FOR_MONTHLY_DONATION", comment: "When users make monthly donations, they see a sheet that lets them pick a payment method. It also tells them what they'll be doing when they pay: receive a badge. This is the subtitle on that sheet. Embeds {{localized badge name}}, such as \"Planet\".", ) - return String(format: format, badge.localizedName) + return String.nonPluralLocalizedStringWithFormat(format, badge.localizedName) case let .gift(recipientFullName): let format = OWSLocalizedString( "DONATE_CHOOSE_PAYMENT_METHOD_SHEET_SUBTITLE_FOR_DONATION_ON_BEHALF_OF_A_FRIEND", comment: "When users make donations on a friend's behalf, they see a sheet that lets them pick a payment method. This is the subtitle on that sheet. Embeds {{recipient's name}}.", ) - return String(format: format, recipientFullName) + return String.nonPluralLocalizedStringWithFormat(format, recipientFullName) } } diff --git a/Signal/src/ViewControllers/Donations/DonateViewController+MonthlySubscriptionLevelView.swift b/Signal/src/ViewControllers/Donations/DonateViewController+MonthlySubscriptionLevelView.swift index d22e34f257..2e52ae4c21 100644 --- a/Signal/src/ViewControllers/Donations/DonateViewController+MonthlySubscriptionLevelView.swift +++ b/Signal/src/ViewControllers/Donations/DonateViewController+MonthlySubscriptionLevelView.swift @@ -113,7 +113,7 @@ extension DonateViewController { owsFail("[Donations] No price for this currency code. This should be impossible in the UI") } let currencyString = CurrencyFormatter.format(money: price) - return String(format: format, currencyString) + return String.nonPluralLocalizedStringWithFormat(format, currencyString) }() headingStackView.removeAllSubviews() headingStackView.addArrangedSubview(headingLabel) @@ -144,13 +144,13 @@ extension DonateViewController { dateFormatter.dateStyle = .medium let dateString = dateFormatter.string(from: currentSubscription.endOfCurrentPeriod) - return String(format: format, dateString) + return String.nonPluralLocalizedStringWithFormat(format, dateString) } else { let format = OWSLocalizedString( "DONATE_SCREEN_MONTHLY_SUBSCRIPTION_SUBTITLE", comment: "On the donation screen, you can see a list of monthly subscription levels. This text will be shown in the subtitle of each level, telling you which badge you'll get. Embeds {{localized badge name}}, such as \"Planet\".", ) - return String(format: format, subscriptionLevel.badge.localizedName) + return String.nonPluralLocalizedStringWithFormat(format, subscriptionLevel.badge.localizedName) } }() } diff --git a/Signal/src/ViewControllers/Donations/DonateViewController.swift b/Signal/src/ViewControllers/Donations/DonateViewController.swift index 56549fb42a..285f9c15a5 100644 --- a/Signal/src/ViewControllers/Donations/DonateViewController.swift +++ b/Signal/src/ViewControllers/Donations/DonateViewController.swift @@ -334,8 +334,8 @@ class DonateViewController: OWSViewController, OWSNavigationChildController { "DONATE_SCREEN_ERROR_TITLE_BANK_TRANSFER_AMOUNT_TOO_LARGE", comment: "Title for an alert shown when the user tries to donate via bank transfer, but the amount they want to donate is too large.", ), - message: String( - format: messageFormat, + message: String.nonPluralLocalizedStringWithFormat( + messageFormat, CurrencyFormatter.format(money: maximumAmount), ), ) @@ -474,7 +474,7 @@ class DonateViewController: OWSViewController, OWSNavigationChildController { comment: "If the user tries to donate to Signal but they've entered an amount that's too small, this error message is shown. Embeds {{currency string}}, such as \"$5\".", ) let currencyString = CurrencyFormatter.format(money: minimumAmount) - showError(String(format: format, currencyString)) + showError(String.nonPluralLocalizedStringWithFormat(format, currencyString)) case .awaitingIDEALAuthorization: // Not pending, but awaiting approval let title = OWSLocalizedString( @@ -584,8 +584,8 @@ class DonateViewController: OWSViewController, OWSNavigationChildController { "SUSTAINER_VIEW_UPDATE_SUBSCRIPTION_CONFIRMATION_TITLE", comment: "Update Subscription? Action sheet title", ) - let message = String( - format: OWSLocalizedString( + let message = String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "SUSTAINER_VIEW_UPDATE_SUBSCRIPTION_CONFIRMATION_MESSAGE", comment: "Update Subscription? Action sheet message, embeds {{Price}}", ), diff --git a/Signal/src/ViewControllers/Donations/DonationPaymentDetailsViewController.swift b/Signal/src/ViewControllers/Donations/DonationPaymentDetailsViewController.swift index ca9cd0980b..304eb7d705 100644 --- a/Signal/src/ViewControllers/Donations/DonationPaymentDetailsViewController.swift +++ b/Signal/src/ViewControllers/Donations/DonationPaymentDetailsViewController.swift @@ -660,7 +660,7 @@ class DonationPaymentDetailsViewController: OWSTableViewController2 { comment: "Users can donate to Signal with a credit or debit card. This is the heading on that screen, telling them how much they'll donate every month. Embeds {{formatted amount of money}}, such as \"$20\".", ) } - return String(format: format, amountString) + return String.nonPluralLocalizedStringWithFormat(format, amountString) }() return UIButton( @@ -690,7 +690,7 @@ class DonationPaymentDetailsViewController: OWSTableViewController2 { let oneEuroCentString = CurrencyFormatter.format( money: FiatMoney(currencyCode: "EUR", value: 0.01), ) - let message = String(format: messageFormat, oneEuroCentString, amountString) + let message = String.nonPluralLocalizedStringWithFormat(messageFormat, oneEuroCentString, amountString) let actionSheet = ActionSheetController(title: title, message: message) actionSheet.addAction(.init( diff --git a/Signal/src/ViewControllers/Donations/DonationViewsUtil.swift b/Signal/src/ViewControllers/Donations/DonationViewsUtil.swift index 0be01bcae4..c799dec053 100644 --- a/Signal/src/ViewControllers/Donations/DonationViewsUtil.swift +++ b/Signal/src/ViewControllers/Donations/DonationViewsUtil.swift @@ -184,16 +184,16 @@ public class GiftBadgeCellView: UIStackView { return formattedDuration }() - let formattedDurationText = String( - format: OWSLocalizedString( + let formattedDurationText = String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "DONATION_FOR_A_FRIEND_ROW_DURATION", comment: "When donating on behalf of a friend, a badge will be sent. This shows how long the badge lasts. Embeds {{formatted duration}}.", ), formattedDuration, ) - return String( - format: OWSLocalizedString( + return String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "JOINED_WITH_DOT", comment: "Two strings, joined by a dot. Embeds {first} and {second}, which are on opposite sides of the dot", ), diff --git a/Signal/src/ViewControllers/HomeView/Chat List/ChatListCell.swift b/Signal/src/ViewControllers/HomeView/Chat List/ChatListCell.swift index 183fcc8ed8..1c555deef0 100644 --- a/Signal/src/ViewControllers/HomeView/Chat List/ChatListCell.swift +++ b/Signal/src/ViewControllers/HomeView/Chat List/ChatListCell.swift @@ -768,7 +768,7 @@ class ChatListCell: UITableViewCell, ReusableTableViewCell { ) return .attributedText( NSAttributedString( - string: String(format: addedToGroupFormat, addedToGroupByName), + string: String.nonPluralLocalizedStringWithFormat(addedToGroupFormat, addedToGroupByName), attributes: [ .font: snippetFont, .foregroundColor: snippetColor, diff --git a/Signal/src/ViewControllers/HomeView/Chat List/ChatListViewController+BackupDownloadProgressView.swift b/Signal/src/ViewControllers/HomeView/Chat List/ChatListViewController+BackupDownloadProgressView.swift index b9d2f341e0..6c75b09aea 100644 --- a/Signal/src/ViewControllers/HomeView/Chat List/ChatListViewController+BackupDownloadProgressView.swift +++ b/Signal/src/ViewControllers/HomeView/Chat List/ChatListViewController+BackupDownloadProgressView.swift @@ -223,15 +223,15 @@ public class CLVBackupDownloadProgressView: BackupDownloadProgressView.Delegate tintColor: UIColor.Signal.orange, backgroundColor: UIColor(rgbHex: 0xF9E4B6), ), - title: String( - format: OWSLocalizedString( + title: String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "RESTORING_MEDIA_DISK_SPACE_SHEET_TITLE_FORMAT", comment: "Title shown on a bottom sheet for restoring media from a backup when paused because the device has insufficient disk space. Embeds {{ %@ formatted number of bytes downloaded, e.g. '100 MB' }}", ), spaceRequiredString, ), - body: String( - format: OWSLocalizedString( + body: String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "RESTORING_MEDIA_DISK_SPACE_SHEET_SUBTITLE_FORMAT", comment: "Subtitle shown on a bottom sheet for restoring media from a backup when paused because the device has insufficient disk space. Embeds {{ %@ formatted number of bytes downloaded, e.g. '100 MB' }}", ), @@ -648,8 +648,8 @@ private class BackupDownloadProgressView: ChatListBackupProgressView { "RESTORING_MEDIA_BANNER_TITLE", comment: "Title shown on chat list banner for restoring media from a backup", ) - progressLabelText = String( - format: OWSLocalizedString( + progressLabelText = String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "RESTORING_MEDIA_BANNER_PROGRESS_FORMAT", comment: "Download progress for media from a backup. Embeds {{ %1$@ formatted number of bytes downloaded, e.g. '100 MB', %2$@ formatted total number of bytes to download, e.g. '3 GB' }}", ), @@ -667,8 +667,8 @@ private class BackupDownloadProgressView: ChatListBackupProgressView { comment: "Title shown on chat list banner for restoring media from a backup when paused for some reason", ) case .outOfDiskSpace(let spaceRequired): - titleLabelText = String( - format: OWSLocalizedString( + titleLabelText = String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "RESTORING_MEDIA_BANNER_DISK_SPACE_TITLE_FORMAT", comment: "Title shown on chat list banner for restoring media from a backup when paused because the device has insufficient disk space. Embeds {{ %@ formatted number of bytes downloaded, e.g. '100 MB' }}", ), diff --git a/Signal/src/ViewControllers/HomeView/Chat List/ChatListViewController+BackupExportProgressView.swift b/Signal/src/ViewControllers/HomeView/Chat List/ChatListViewController+BackupExportProgressView.swift index e61a083024..bb357db8c7 100644 --- a/Signal/src/ViewControllers/HomeView/Chat List/ChatListViewController+BackupExportProgressView.swift +++ b/Signal/src/ViewControllers/HomeView/Chat List/ChatListViewController+BackupExportProgressView.swift @@ -637,8 +637,8 @@ private class BackupExportProgressView: ChatListBackupProgressView { "CHAT_LIST_BACKUP_PROGRESS_VIEW_UPLOADING_BACKUP_TITLE", comment: "Title label shown in the chat list backup progress view while backup attachments are being uploaded.", ) - progressLabelText = String( - format: OWSLocalizedString( + progressLabelText = String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "CHAT_LIST_BACKUP_PROGRESS_VIEW_UPLOAD_PROGRESS_FORMAT", comment: "Progress label showing the amount uploaded out of the total. Embeds {{ %1$@ bytes uploaded, %2$@ total bytes to upload }}.", ), diff --git a/Signal/src/ViewControllers/HomeView/Chat List/ChatListViewController+Reminders.swift b/Signal/src/ViewControllers/HomeView/Chat List/ChatListViewController+Reminders.swift index 8d9e9642c8..e4218be249 100644 --- a/Signal/src/ViewControllers/HomeView/Chat List/ChatListViewController+Reminders.swift +++ b/Signal/src/ViewControllers/HomeView/Chat List/ChatListViewController+Reminders.swift @@ -387,8 +387,8 @@ extension ChatListViewController { extension ChatListViewController: UsernameSelectionDelegate { func usernameSelectionDidDismissAfterConfirmation(username: String) { self.presentToast( - text: String( - format: OWSLocalizedString( + text: String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "USERNAME_RESET_SUCCESSFUL_TOAST", comment: "A message in a toast informing the user their username, link, and QR code have successfully been reset. Embeds {{ the user's new username }}.", ), diff --git a/Signal/src/ViewControllers/HomeView/Chat List/ChatListViewController.swift b/Signal/src/ViewControllers/HomeView/Chat List/ChatListViewController.swift index f8acd74155..fbf2d9a401 100644 --- a/Signal/src/ViewControllers/HomeView/Chat List/ChatListViewController.swift +++ b/Signal/src/ViewControllers/HomeView/Chat List/ChatListViewController.swift @@ -993,7 +993,7 @@ public class ChatListViewController: OWSViewController, HomeTabViewController { "PAYMENTS_NOTIFICATION_BANNER_1_WITH_DETAILS_FORMAT", comment: "Format for the payments notification banner for a single payment notification with details. Embeds: {{ %1$@ the name of the user who sent you the payment, %2$@ the amount of the payment }}.", ) - let title = String(format: format, shortName, formattedAmount) + let title = String.nonPluralLocalizedStringWithFormat(format, shortName, formattedAmount) let avatarView = ConversationAvatarView(sizeClass: .customDiameter(Self.paymentsBannerAvatarSize), localUserDisplayMode: .asUser) avatarView.update(transaction) { config in @@ -1029,7 +1029,7 @@ public class ChatListViewController: OWSViewController, HomeTabViewController { "PAYMENTS_NOTIFICATION_BANNER_N_FORMAT", comment: "Format for the payments notification banner for multiple payment notifications. Embeds: {{ the number of unread payment notifications }}.", ) - title = String(format: format, OWSFormat.formatUInt(unreadCount)) + title = String.nonPluralLocalizedStringWithFormat(format, OWSFormat.formatUInt(unreadCount)) } let iconView = UIImageView.withTemplateImageName( @@ -1273,8 +1273,8 @@ public class ChatListViewController: OWSViewController, HomeTabViewController { "NOTIFICATIONS_ERROR_TITLE", comment: "Shown as the title of an alert when notifications can't be shown due to an error.", ), - message: String( - format: OWSLocalizedString( + message: String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "NOTIFICATIONS_ERROR_MESSAGE", comment: "Shown as the body of an alert when notifications can't be shown due to an error.", ), diff --git a/Signal/src/ViewControllers/HomeView/Chat List/ConversationSearchViewController.swift b/Signal/src/ViewControllers/HomeView/Chat List/ConversationSearchViewController.swift index a65bed15d1..edd237cae9 100644 --- a/Signal/src/ViewControllers/HomeView/Chat List/ConversationSearchViewController.swift +++ b/Signal/src/ViewControllers/HomeView/Chat List/ConversationSearchViewController.swift @@ -720,7 +720,7 @@ class EmptySearchResultCell: UITableViewCell { "HOME_VIEW_SEARCH_NO_RESULTS_FORMAT", comment: "Format string when search returns no results. Embeds {{search term}}", ) - messageLabel.text = String(format: format, searchText) + messageLabel.text = String.nonPluralLocalizedStringWithFormat(format, searchText) messageLabel.textColor = .Signal.label messageLabel.font = UIFont.dynamicTypeBody diff --git a/Signal/src/ViewControllers/HomeView/Stories/Replies & Views Sheets/StoryContextMenuGenerator.swift b/Signal/src/ViewControllers/HomeView/Stories/Replies & Views Sheets/StoryContextMenuGenerator.swift index ce22ae8ab7..f2e55a6232 100644 --- a/Signal/src/ViewControllers/HomeView/Stories/Replies & Views Sheets/StoryContextMenuGenerator.swift +++ b/Signal/src/ViewControllers/HomeView/Stories/Replies & Views Sheets/StoryContextMenuGenerator.swift @@ -263,8 +263,8 @@ extension StoryContextMenuGenerator { comment: "Title asking the user if they are sure they want to hide stories from another user", ), message: loadThreadDisplayNameWithSneakyTransaction(context: context).map { - String( - format: OWSLocalizedString( + String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "STORIES_HIDE_STORY_ACTION_SHEET_MESSAGE", comment: "Message asking the user if they are sure they want to hide stories from {{other user's name}}", ), diff --git a/Signal/src/ViewControllers/HomeView/Stories/Replies & Views Sheets/StoryReplyInputToolbar.swift b/Signal/src/ViewControllers/HomeView/Stories/Replies & Views Sheets/StoryReplyInputToolbar.swift index a6dfa65b6d..b78396e0fe 100644 --- a/Signal/src/ViewControllers/HomeView/Stories/Replies & Views Sheets/StoryReplyInputToolbar.swift +++ b/Signal/src/ViewControllers/HomeView/Stories/Replies & Views Sheets/StoryReplyInputToolbar.swift @@ -231,7 +231,7 @@ class StoryReplyInputToolbar: UIView { let authorName = SSKEnvironment.shared.databaseStorageRef.read { tx in return SSKEnvironment.shared.contactManagerRef.displayName(for: quotedReplyModel.originalMessageAuthorAddress, tx: tx).resolvedValue() } - return String(format: format, authorName) + return String.nonPluralLocalizedStringWithFormat(format, authorName) } else { return OWSLocalizedString( "STORY_REPLY_TEXT_FIELD_PLACEHOLDER", diff --git a/Signal/src/ViewControllers/NewGroupView/NewGroupConfirmViewController.swift b/Signal/src/ViewControllers/NewGroupView/NewGroupConfirmViewController.swift index 7ec0ce9590..657f6cac0d 100644 --- a/Signal/src/ViewControllers/NewGroupView/NewGroupConfirmViewController.swift +++ b/Signal/src/ViewControllers/NewGroupView/NewGroupConfirmViewController.swift @@ -349,7 +349,7 @@ public class NewGroupConfirmViewController: OWSTableViewController2 { "GROUP_INVITES_SENT_ALERT_MESSAGE_1_FORMAT", comment: "Format for the message for an alert indicating that a member was invited to a group. Embeds: {{ the name of the member. }}", ) - alertMessage = String(format: alertMessageFormat, inviteeName) + alertMessage = String.nonPluralLocalizedStringWithFormat(alertMessageFormat, inviteeName) } let actionSheet = ActionSheetController(title: alertTitle, message: alertMessage) diff --git a/Signal/src/ViewControllers/Payments/SendPaymentCompletionActionSheet.swift b/Signal/src/ViewControllers/Payments/SendPaymentCompletionActionSheet.swift index c3a2a48ceb..fdde9508f5 100644 --- a/Signal/src/ViewControllers/Payments/SendPaymentCompletionActionSheet.swift +++ b/Signal/src/ViewControllers/Payments/SendPaymentCompletionActionSheet.swift @@ -433,7 +433,7 @@ public class SendPaymentCompletionActionSheet: ActionSheetController { let row = addRow( to: &topGroup, - title: String(format: fiatFormat, currencyConversion.currencyCode), + title: String.nonPluralLocalizedStringWithFormat(fiatFormat, currencyConversion.currencyCode), value: fiatAmountString, titleIconView: currencyConversionInfoView, addSeparator: true, @@ -497,7 +497,7 @@ public class SendPaymentCompletionActionSheet: ActionSheetController { "PAYMENTS_NEW_PAYMENT_RECIPIENT_AMOUNT_FORMAT", comment: "Format for the 'payment recipient amount' indicator. Embeds {{ the name of the recipient of the payment }}.", ) - return String(format: userFormat, otherUserName) + return String.nonPluralLocalizedStringWithFormat(userFormat, otherUserName) } public static func formatPaymentFailure(_ error: Error, withErrorPrefix: Bool) -> String { @@ -515,7 +515,7 @@ public class SendPaymentCompletionActionSheet: ActionSheetController { "PAYMENTS_NEW_PAYMENT_ERROR_INSUFFICIENT_FUNDS_FORMAT", comment: "Indicates that a payment failed due to insufficient funds. Embeds {{ current balance }}.", ) - return String(format: format, formattedBalance) + return String.nonPluralLocalizedStringWithFormat(format, formattedBalance) } else { return OWSLocalizedString( "PAYMENTS_NEW_PAYMENT_ERROR_INSUFFICIENT_FUNDS", diff --git a/Signal/src/ViewControllers/Payments/SendPaymentHelper.swift b/Signal/src/ViewControllers/Payments/SendPaymentHelper.swift index fe0ed82f6a..00918dc85b 100644 --- a/Signal/src/ViewControllers/Payments/SendPaymentHelper.swift +++ b/Signal/src/ViewControllers/Payments/SendPaymentHelper.swift @@ -154,8 +154,8 @@ class SendPaymentHelper { "PAYMENTS_NEW_PAYMENT_BALANCE_FORMAT", comment: "Format for the 'balance' indicator. Embeds {{ the current payments balance }}.", ) - balanceLabel.text = String( - format: format, + balanceLabel.text = String.nonPluralLocalizedStringWithFormat( + format, Self.formatMobileCoinAmount(maximumPaymentAmount), ) } @@ -218,8 +218,8 @@ class SendPaymentHelper { "PAYMENTS_NEW_PAYMENT_CURRENCY_FORMAT", comment: "Format for currency amounts in the 'send payment' UI. Embeds {{ %1$@ the current payments balance, %2$@ the currency indicator }}.", ) - return String( - format: format, + return String.nonPluralLocalizedStringWithFormat( + format, formattedAmount, PaymentsConstants.mobileCoinCurrencyIdentifier, ) diff --git a/Signal/src/ViewControllers/Payments/SendPaymentMemoViewController.swift b/Signal/src/ViewControllers/Payments/SendPaymentMemoViewController.swift index 52036c2af2..31db620c7f 100644 --- a/Signal/src/ViewControllers/Payments/SendPaymentMemoViewController.swift +++ b/Signal/src/ViewControllers/Payments/SendPaymentMemoViewController.swift @@ -143,8 +143,8 @@ public class SendPaymentMemoViewController: OWSViewController { "PAYMENTS_NEW_PAYMENT_MESSAGE_COUNT_FORMAT", comment: "Format for the 'message character count indicator' for the 'new payment or payment request' view. Embeds {{ %1$@ the number of characters in the message, %2$@ the maximum number of characters in the message }}.", ) - memoCharacterCountLabel.text = String( - format: format, + memoCharacterCountLabel.text = String.nonPluralLocalizedStringWithFormat( + format, OWSFormat.formatInt(strippedMemoMessage.count), OWSFormat.formatInt(PaymentsImpl.maxPaymentMemoMessageLength), ) diff --git a/Signal/src/ViewControllers/Payments/SendPaymentViewController.swift b/Signal/src/ViewControllers/Payments/SendPaymentViewController.swift index ec3a7dd1da..50ec8fcdfd 100644 --- a/Signal/src/ViewControllers/Payments/SendPaymentViewController.swift +++ b/Signal/src/ViewControllers/Payments/SendPaymentViewController.swift @@ -294,7 +294,7 @@ public class SendPaymentViewController: OWSViewController { let recipientName: String = SSKEnvironment.shared.databaseStorageRef.read { tx in SSKEnvironment.shared.contactManagerRef.displayName(for: recipientAddress, tx: tx).resolvedValue() } - let title = String(format: titleFormat, recipientName) + let title = String.nonPluralLocalizedStringWithFormat(titleFormat, recipientName) let actionSheet = ActionSheetController( title: title, message: OWSLocalizedString( @@ -849,7 +849,7 @@ public class SendPaymentViewController: OWSViewController { "PAYMENTS_CURRENCY_CONVERSION_FRESHNESS_FORMAT", comment: "Format for indicator of a payment amount converted to fiat currency with the freshness of the conversion rate. Embeds: {{ %1$@ the payment amount, %2$@ the freshness of the currency conversion rate }}.", ) - return String(format: conversionFormat, formattedAmount, formattedFreshness) + return String.nonPluralLocalizedStringWithFormat(conversionFormat, formattedAmount, formattedFreshness) } private func updateBalanceLabel() { @@ -1068,7 +1068,7 @@ public class SendPaymentViewController: OWSViewController { "SETTINGS_PAYMENTS_PAYMENT_INSUFFICIENT_BALANCE_ALERT_MESSAGE_FORMAT", comment: "Message for the 'insufficient balance for payment' alert. Embeds: {{ The current payments balance }}.", ) - let message = String(format: messageFormat, PaymentsFormat.format( + let message = String.nonPluralLocalizedStringWithFormat(messageFormat, PaymentsFormat.format( paymentAmount: paymentBalance.amount, isShortForm: false, withCurrencyCode: true, diff --git a/Signal/src/ViewControllers/Photos/MediaControls.swift b/Signal/src/ViewControllers/Photos/MediaControls.swift index a9a01773c3..136be7fffb 100644 --- a/Signal/src/ViewControllers/Photos/MediaControls.swift +++ b/Signal/src/ViewControllers/Photos/MediaControls.swift @@ -1916,7 +1916,7 @@ extension CameraZoomSelectionControl { "CAMERA_VO_ZOOM_LEVEL", comment: "VoiceOver description of current camera zoom level.", ) - return String(format: formatString, zoomValueString) + return String.nonPluralLocalizedStringWithFormat(formatString, zoomValueString) } set { super.accessibilityValue = newValue } } diff --git a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift index e7e88e9874..af90669c7c 100644 --- a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift +++ b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift @@ -1433,15 +1433,15 @@ extension PhotoCaptureViewController: QRCodeSampleBufferScannerDelegate { // check after the username is queried before showing the sheet. guard isViewVisible else { return } OWSActionSheets.showConfirmationAlert( - title: String( - format: OWSLocalizedString( + title: String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "PHOTO_CAPTURE_USERNAME_QR_CODE_FOUND_TITLE_FORMAT", comment: "Title for sheet presented from photo capture view indicating that a username QR code was found. Embeds {{username}}.", ), username, ), - message: String( - format: OWSLocalizedString( + message: String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "PHOTO_CAPTURE_USERNAME_QR_CODE_FOUND_MESSAGE_FORMAT", comment: "Message for a sheet presented from photo capture view indicating that a username QR code was found. Embeds {{username}}.", ), diff --git a/Signal/src/ViewControllers/Polls/NewPollViewController2.swift b/Signal/src/ViewControllers/Polls/NewPollViewController2.swift index 1eeff90514..8eabddc7fc 100644 --- a/Signal/src/ViewControllers/Polls/NewPollViewController2.swift +++ b/Signal/src/ViewControllers/Polls/NewPollViewController2.swift @@ -265,8 +265,8 @@ class NewPollViewController2: OWSViewController, UITableViewDelegate, OWSNavigat cell.configure( text: optionRows[rowIndex].text, - placeholder: String( - format: OWSLocalizedString( + placeholder: String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "POLL_OPTION_PLACEHOLDER_FORMAT", comment: #"Format text for the placeholder of an option row when creating a poll. Embeds {{ the number of this option in a list, as a pre-localized string }}, so it should look like "Option 1", "Option 2"."#, ), diff --git a/Signal/src/ViewControllers/ThreadSettings/AddToGroupViewController.swift b/Signal/src/ViewControllers/ThreadSettings/AddToGroupViewController.swift index cb4e9096b1..15cf62f62b 100644 --- a/Signal/src/ViewControllers/ThreadSettings/AddToGroupViewController.swift +++ b/Signal/src/ViewControllers/ThreadSettings/AddToGroupViewController.swift @@ -126,7 +126,7 @@ public class AddToGroupViewController: OWSTableViewController2 { "ADD_TO_GROUP_ACTION_SHEET_TITLE", comment: "The title on the 'add to group' confirmation action sheet.", ), - message: String(format: messageFormat, shortName, groupThread.groupNameOrDefault), + message: String.nonPluralLocalizedStringWithFormat(messageFormat, shortName, groupThread.groupNameOrDefault), proceedTitle: OWSLocalizedString( "ADD_TO_GROUP_ACTION_PROCEED_BUTTON", comment: "The button on the 'add to group' confirmation to add the user to the group.", @@ -168,7 +168,7 @@ public class AddToGroupViewController: OWSTableViewController2 { "ADD_TO_GROUP_SUCCESS_TOAST_FORMAT", comment: "A toast on the 'add to group' view indicating the user was added. Embeds {contact name} and {group name}", ) - let toastText = String(format: toastFormat, shortName, groupThread.groupNameOrDefault) + let toastText = String.nonPluralLocalizedStringWithFormat(toastFormat, shortName, groupThread.groupNameOrDefault) presentingViewController?.presentToast(text: toastText) } } diff --git a/Signal/src/ViewControllers/ThreadSettings/ContactAboutSheet.swift b/Signal/src/ViewControllers/ThreadSettings/ContactAboutSheet.swift index 204c3d1519..a4e261d0ef 100644 --- a/Signal/src/ViewControllers/ThreadSettings/ContactAboutSheet.swift +++ b/Signal/src/ViewControllers/ThreadSettings/ContactAboutSheet.swift @@ -173,8 +173,8 @@ class ContactAboutSheet: StackSheetViewController { let nameLabel = self.nameLabel else { return } Tooltip( - message: String( - format: OWSLocalizedString( + message: String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "CONTACT_ABOUT_SHEET_SECONDARY_NAME_TOOLTIP_MESSAGE", comment: "Message for a tooltip that appears above a parenthesized name for another user, indicating that that name is the name the other user set for themself. Embeds {{name}}", ), diff --git a/Signal/src/ViewControllers/ThreadSettings/ConversationSettingsViewController+LegacyGroups.swift b/Signal/src/ViewControllers/ThreadSettings/ConversationSettingsViewController+LegacyGroups.swift index 7159cd61e7..2c3edff33c 100644 --- a/Signal/src/ViewControllers/ThreadSettings/ConversationSettingsViewController+LegacyGroups.swift +++ b/Signal/src/ViewControllers/ThreadSettings/ConversationSettingsViewController+LegacyGroups.swift @@ -24,7 +24,7 @@ class LegacyGroupView: UIView { private let label = UILabel() private func configureLabel(format: String, highlightedSubstring: String) { - let text = String(format: format, highlightedSubstring) + let text = String.nonPluralLocalizedStringWithFormat(format, highlightedSubstring) let attributedString = NSMutableAttributedString(string: text) attributedString.setAttributes( diff --git a/Signal/src/ViewControllers/ThreadSettings/ConversationSettingsViewController.swift b/Signal/src/ViewControllers/ThreadSettings/ConversationSettingsViewController.swift index ac8db15817..a6153f9b32 100644 --- a/Signal/src/ViewControllers/ThreadSettings/ConversationSettingsViewController.swift +++ b/Signal/src/ViewControllers/ThreadSettings/ConversationSettingsViewController.swift @@ -627,8 +627,8 @@ class ConversationSettingsViewController: OWSTableViewController2, BadgeCollecti return } - let confirmationTitle = String( - format: OWSLocalizedString( + let confirmationTitle = String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "END_GROUP_LABEL_SPECIFIC", comment: "End group confirmation title for a specific group. Embeds {{ group name }}", ), @@ -772,7 +772,7 @@ class ConversationSettingsViewController: OWSTableViewController2, BadgeCollecti "CONVERSATION_SETTINGS_MUTED_UNTIL_UNMUTE_FORMAT", comment: "Indicates that this thread is muted until a given date or time. Embeds {{The date or time which the thread is muted until}}.", ) - return String(format: formatString, dateFormatter.string(from: mutedUntilDate)) + return String.nonPluralLocalizedStringWithFormat(formatString, dateFormatter.string(from: mutedUntilDate)) } private class func muteUnmuteActions( diff --git a/Signal/src/ViewControllers/ThreadSettings/GroupDescriptionViewController.swift b/Signal/src/ViewControllers/ThreadSettings/GroupDescriptionViewController.swift index 165f632d4b..8add72a5b4 100644 --- a/Signal/src/ViewControllers/ThreadSettings/GroupDescriptionViewController.swift +++ b/Signal/src/ViewControllers/ThreadSettings/GroupDescriptionViewController.swift @@ -113,7 +113,7 @@ class GroupDescriptionViewController: OWSTableViewController2 { "GROUP_DESCRIPTION_VIEW_TITLE_FORMAT", comment: "Title for the group description view. Embeds {{ the number of characters that can be added to the description without hitting the length limit }}.", ) - title = String(format: titleFormat, OWSFormat.formatInt(remainingGlyphCount)) + title = String.nonPluralLocalizedStringWithFormat(titleFormat, OWSFormat.formatInt(remainingGlyphCount)) } else { title = OWSLocalizedString( "GROUP_DESCRIPTION_VIEW_TITLE", diff --git a/Signal/src/ViewControllers/ThreadSettings/GroupMemberRequestsAndInvitesViewController.swift b/Signal/src/ViewControllers/ThreadSettings/GroupMemberRequestsAndInvitesViewController.swift index 53b2c67e81..65c5edb562 100644 --- a/Signal/src/ViewControllers/ThreadSettings/GroupMemberRequestsAndInvitesViewController.swift +++ b/Signal/src/ViewControllers/ThreadSettings/GroupMemberRequestsAndInvitesViewController.swift @@ -159,7 +159,7 @@ public class GroupMemberRequestsAndInvitesViewController: OWSTableViewController comment: "Footer for the 'pending member requests' section of the 'member requests and invites' view. Embeds {{ the name of the group }}.", ) let groupName = SSKEnvironment.shared.databaseStorageRef.read { tx in SSKEnvironment.shared.contactManagerRef.displayName(for: oldGroupThread, transaction: tx) } - section.footerTitle = String(format: footerFormat, groupName) + section.footerTitle = String.nonPluralLocalizedStringWithFormat(footerFormat, groupName) if !requestingMembersSorted.isEmpty { for address in requestingMembersSorted { @@ -450,7 +450,7 @@ public class GroupMemberRequestsAndInvitesViewController: OWSTableViewController "PENDING_GROUP_MEMBERS_REVOKE_LOCAL_INVITE_CONFIRMATION_TITLE_1_FORMAT", comment: "Format for title of 'revoke invite' confirmation alert. Embeds {{ the name of the invited group member. }}.", ) - let alertTitle = String(format: format, invitedName) + let alertTitle = String.nonPluralLocalizedStringWithFormat(format, invitedName) let actionSheet = ActionSheetController(title: alertTitle) actionSheet.addAction(ActionSheetAction( title: OWSLocalizedString( @@ -530,7 +530,7 @@ public class GroupMemberRequestsAndInvitesViewController: OWSTableViewController comment: "Message indicating that a request to join the group was successfully approved. Embeds {{ the name of the approved user }}.", ) let userName = SSKEnvironment.shared.databaseStorageRef.read { tx in SSKEnvironment.shared.contactManagerRef.displayName(for: address, tx: tx).resolvedValue() } - let text = String(format: format, userName) + let text = String.nonPluralLocalizedStringWithFormat(format, userName) presentToast(text: text) } @@ -540,7 +540,7 @@ public class GroupMemberRequestsAndInvitesViewController: OWSTableViewController comment: "Message indicating that a request to join the group was successfully denied. Embeds {{ the name of the denied user }}.", ) let userName = SSKEnvironment.shared.databaseStorageRef.read { tx in SSKEnvironment.shared.contactManagerRef.displayName(for: address, tx: tx).resolvedValue() } - let text = String(format: format, userName) + let text = String.nonPluralLocalizedStringWithFormat(format, userName) presentToast(text: text) } } @@ -596,7 +596,7 @@ private extension GroupMemberRequestsAndInvitesViewController { "PENDING_GROUP_MEMBERS_ACCEPT_REQUEST_CONFIRMATION_TITLE_FORMAT", comment: "Title of 'accept member request to join group' confirmation alert. Embeds {{ the name of the requesting group member. }}.", ) - let alertTitle = String(format: format, username) + let alertTitle = String.nonPluralLocalizedStringWithFormat(format, username) let actionSheet = ActionSheetController(title: alertTitle) let actionTitle = OWSLocalizedString( @@ -618,7 +618,7 @@ private extension GroupMemberRequestsAndInvitesViewController { "PENDING_GROUP_MEMBERS_DENY_REQUEST_CONFIRMATION_TITLE_FORMAT", comment: "Title of 'deny member request to join group' confirmation alert. Embeds {{ the name of the requesting group member. }}.", ) - let alertTitle = String(format: format, username) + let alertTitle = String.nonPluralLocalizedStringWithFormat(format, username) let actionSheet = ActionSheetController(title: alertTitle) let actionTitle = OWSLocalizedString( diff --git a/Signal/src/ViewControllers/ThreadSettings/GroupViewHelper+MemberActionSheet.swift b/Signal/src/ViewControllers/ThreadSettings/GroupViewHelper+MemberActionSheet.swift index 86d1106d6e..820f2ed1d1 100644 --- a/Signal/src/ViewControllers/ThreadSettings/GroupViewHelper+MemberActionSheet.swift +++ b/Signal/src/ViewControllers/ThreadSettings/GroupViewHelper+MemberActionSheet.swift @@ -43,7 +43,7 @@ extension GroupViewHelper { }, ) } - let title = String(format: titleFormat, SSKEnvironment.shared.databaseStorageRef.read { tx in + let title = String.nonPluralLocalizedStringWithFormat(titleFormat, SSKEnvironment.shared.databaseStorageRef.read { tx in return SSKEnvironment.shared.contactManagerRef.displayName(for: address, tx: tx).resolvedValue() }) let actionSheet = ActionSheetController(title: title, message: message) diff --git a/Signal/src/ViewControllers/ThreadSettings/SoundAndNotificationsSettingsViewController.swift b/Signal/src/ViewControllers/ThreadSettings/SoundAndNotificationsSettingsViewController.swift index 73a90b50c0..740a41ae9d 100644 --- a/Signal/src/ViewControllers/ThreadSettings/SoundAndNotificationsSettingsViewController.swift +++ b/Signal/src/ViewControllers/ThreadSettings/SoundAndNotificationsSettingsViewController.swift @@ -95,8 +95,8 @@ class SoundAndNotificationsSettingsViewController: OWSTableViewController2 { "CONVERSATION_SETTINGS_MUTED_UNTIL_FORMAT", comment: "Indicates that this thread is muted until a given date or time. Embeds {{The date or time which the thread is muted until}}.", ) - muteStatus = String( - format: formatString, + muteStatus = String.nonPluralLocalizedStringWithFormat( + formatString, dateFormatter.string(from: mutedUntilDate), ) } diff --git a/Signal/src/ViewControllers/Wallpapers/PreviewWallpaperViewController.swift b/Signal/src/ViewControllers/Wallpapers/PreviewWallpaperViewController.swift index 62ea7602a6..5ec2ae8b0a 100644 --- a/Signal/src/ViewControllers/Wallpapers/PreviewWallpaperViewController.swift +++ b/Signal/src/ViewControllers/Wallpapers/PreviewWallpaperViewController.swift @@ -197,7 +197,7 @@ class PreviewWallpaperViewController: UIViewController { comment: "The outgoing bubble text when setting a wallpaper for specific chat. Embeds {{chat name}}", ) let displayName = SSKEnvironment.shared.databaseStorageRef.read { tx in SSKEnvironment.shared.contactManagerRef.displayName(for: thread, transaction: tx) } - return String(format: formatString, displayName) + return String.nonPluralLocalizedStringWithFormat(formatString, displayName) }() let incomingText: String diff --git a/Signal/src/views/ExpirationNagView.swift b/Signal/src/views/ExpirationNagView.swift index bc97bdf2be..8d7b74aa4e 100644 --- a/Signal/src/views/ExpirationNagView.swift +++ b/Signal/src/views/ExpirationNagView.swift @@ -147,8 +147,8 @@ private extension String { } static func appExpires(on date: Date) -> String { - return String( - format: OWSLocalizedString( + return String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "EXPIRATION_WARNING_SOON", comment: "Label warning the user that the app will expire soon. Embeds {{date}}.", ), @@ -164,8 +164,8 @@ private extension String { } static func osSoonToExpireAndDeviceWillBeStuck(on expirationDate: Date) -> String { - return String( - format: OWSLocalizedString( + return String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "OS_SOON_TO_EXPIRE_AND_DEVICE_WILL_BE_STUCK_FORMAT", comment: "Signal doesn't support old versions of iOS and shows a warning if you're on an old version that will soon lose support. This is the text on that warning when users can't upgrade iOS without getting a new device. Embeds {{expiration date}}.", ), @@ -174,8 +174,8 @@ private extension String { } static func osSoonToExpireAndCanUpgradeOs(expirationDate: Date) -> String { - return String( - format: OWSLocalizedString( + return String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "OS_SOON_TO_EXPIRE_AND_CAN_UPGRADE_OS_FORMAT", comment: "Signal doesn't support old versions of iOS and shows a warning if you're an old version that will soon lose support. Some users can upgrade their device to a newer version of iOS to continue using Signal. If that's the case, they'll be shown this text. Embeds {{expiration date}}.", ), diff --git a/Signal/src/views/NameCollisionReviewCell.swift b/Signal/src/views/NameCollisionReviewCell.swift index 3f6ed7cd4f..c01f917aeb 100644 --- a/Signal/src/views/NameCollisionReviewCell.swift +++ b/Signal/src/views/NameCollisionReviewCell.swift @@ -150,8 +150,8 @@ final class NameCollisionCell: UITableViewCell { "NAME_COLLISION_RECENT_CHANGE_FORMAT_STRING", comment: "Format string describing a recent profile name change that led to a name collision. Embeds {{ %1$@ current name, which may be a profile name or an address book name }}, {{ %2$@ old profile name }}, and {{ %3$@ current profile name }}", ) - let string = String( - format: formatString, + let string = String.nonPluralLocalizedStringWithFormat( + formatString, model.shortName, profileNameChange.oldestProfileName, profileNameChange.newestProfileName, diff --git a/SignalNSE/NotificationService.swift b/SignalNSE/NotificationService.swift index bba8202cad..2ebe4c7838 100644 --- a/SignalNSE/NotificationService.swift +++ b/SignalNSE/NotificationService.swift @@ -112,7 +112,7 @@ class NotificationService: UNNotificationServiceExtension { "NOTIFICATION_BODY_PHONE_LOCKED_FORMAT", comment: "Lock screen notification text presented after user powers on their device without unlocking. Embeds {{device model}} (either 'iPad' or 'iPhone')", ) - content.body = String(format: notificationFormat, UIDevice.current.localizedModel) + content.body = String.nonPluralLocalizedStringWithFormat(notificationFormat, UIDevice.current.localizedModel) return content } else { // Only show a single error if we receive multiple pushes diff --git a/SignalServiceKit/Calls/Group/OWSGroupCallMessage.swift b/SignalServiceKit/Calls/Group/OWSGroupCallMessage.swift index 072a6d5cce..48c32ae03e 100644 --- a/SignalServiceKit/Calls/Group/OWSGroupCallMessage.swift +++ b/SignalServiceKit/Calls/Group/OWSGroupCallMessage.swift @@ -84,7 +84,7 @@ extension OWSGroupCallMessage { if sortedAddresses.count == 2 { let firstName = self.participantName(for: sortedAddresses[0], tx: tx) let secondName = self.participantName(for: sortedAddresses[1], tx: tx) - return String(format: twoFormat, firstName, secondName) + return String.nonPluralLocalizedStringWithFormat(twoFormat, firstName, secondName) } if sortedAddresses.count == 0 { return someoneString @@ -94,14 +94,14 @@ extension OWSGroupCallMessage { return self.groupCallStartedByYou } else { let name = self.participantName(for: sortedAddresses[0], tx: tx) - return String(format: onlyCreatorFormat, name) + return String.nonPluralLocalizedStringWithFormat(onlyCreatorFormat, name) } } else { if sortedAddresses[0].isLocalAddress { return onlyYouString } else { let name = self.participantName(for: sortedAddresses[0], tx: tx) - return String(format: onlyOneFormat, name) + return String.nonPluralLocalizedStringWithFormat(onlyOneFormat, name) } } } @@ -123,7 +123,7 @@ extension OWSGroupCallMessage: OWSPreviewText { "GROUP_CALL_STARTED_MESSAGE_FORMAT", comment: "Text explaining that someone started a group call. Embeds {{call creator display name}}", ) - return String(format: formatString, creatorDisplayName) + return String.nonPluralLocalizedStringWithFormat(formatString, creatorDisplayName) } return OWSLocalizedString( "GROUP_CALL_SOMEONE_STARTED_MESSAGE", diff --git a/SignalServiceKit/Messages/Interactions/OWSDisappearingConfigurationUpdateInfoMessage.swift b/SignalServiceKit/Messages/Interactions/OWSDisappearingConfigurationUpdateInfoMessage.swift index 2410c3ccd3..388be63215 100644 --- a/SignalServiceKit/Messages/Interactions/OWSDisappearingConfigurationUpdateInfoMessage.swift +++ b/SignalServiceKit/Messages/Interactions/OWSDisappearingConfigurationUpdateInfoMessage.swift @@ -21,20 +21,20 @@ public extension OWSDisappearingConfigurationUpdateInfoMessage { "DISAPPEARING_MESSAGES_CONFIGURATION_GROUP_EXISTING_FORMAT", comment: "Info Message when added to a group which has enabled disappearing messages. Embeds {{time amount}} before messages disappear. See the *_TIME_AMOUNT strings for context.", ) - return String(format: format, durationString) + return String.nonPluralLocalizedStringWithFormat(format, durationString) } else if let updaterName { if newToken.isEnabled { let format = OWSLocalizedString( "OTHER_UPDATED_DISAPPEARING_MESSAGES_CONFIGURATION", comment: "Info Message when another user enabled disappearing messages. Embeds {{name of other user}} and {{time amount}} before messages disappear. See the *_TIME_AMOUNT strings for context.", ) - return String(format: format, updaterName, durationString) + return String.nonPluralLocalizedStringWithFormat(format, updaterName, durationString) } else { let format = OWSLocalizedString( "OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION", comment: "Info Message when another user disabled disappearing messages. Embeds {{name of other user}}.", ) - return String(format: format, updaterName) + return String.nonPluralLocalizedStringWithFormat(format, updaterName) } } else { // Changed by localNumber on this device or via synced transcript @@ -43,7 +43,7 @@ public extension OWSDisappearingConfigurationUpdateInfoMessage { "YOU_UPDATED_DISAPPEARING_MESSAGES_CONFIGURATION", comment: "Info Message when you update disappearing messages duration. Embeds a {{time amount}} before messages disappear. see the *_TIME_AMOUNT strings for context.", ) - return String(format: format, durationString) + return String.nonPluralLocalizedStringWithFormat(format, durationString) } else { return OWSLocalizedString( "YOU_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION", diff --git a/SignalServiceKit/Messages/Interactions/PinnedMessages/TSInfoMessage+PinnedMessage.swift b/SignalServiceKit/Messages/Interactions/PinnedMessages/TSInfoMessage+PinnedMessage.swift index c488d5af7e..5f9a83738c 100644 --- a/SignalServiceKit/Messages/Interactions/PinnedMessages/TSInfoMessage+PinnedMessage.swift +++ b/SignalServiceKit/Messages/Interactions/PinnedMessages/TSInfoMessage+PinnedMessage.swift @@ -115,6 +115,6 @@ extension TSInfoMessage { comment: "Shown when another user pins a message. Embeds {{ another user }}.", ) - return String(format: formatString, displayName.resolvedValue()) + return String.nonPluralLocalizedStringWithFormat(formatString, displayName.resolvedValue()) } } diff --git a/SignalServiceKit/Messages/Interactions/Polls/TSInfoMessage+Polls.swift b/SignalServiceKit/Messages/Interactions/Polls/TSInfoMessage+Polls.swift index a9b54c5cf0..1327043044 100644 --- a/SignalServiceKit/Messages/Interactions/Polls/TSInfoMessage+Polls.swift +++ b/SignalServiceKit/Messages/Interactions/Polls/TSInfoMessage+Polls.swift @@ -104,7 +104,7 @@ extension TSInfoMessage { "POLL_ENDED_BY_YOU_CHAT_LIST_UPDATE", comment: "Shown when the local user ends a poll. Embeds {{ poll question }}.", ) - return String(format: formatString, question) + return String.nonPluralLocalizedStringWithFormat(formatString, question) } let displayName = SSKEnvironment.shared.contactManagerRef.displayName( @@ -117,6 +117,6 @@ extension TSInfoMessage { comment: "Shown when another user ends a poll. Embeds {{ another user }} and {{ poll question }}.", ) - return String(format: formatString, displayName.resolvedValue(), question) + return String.nonPluralLocalizedStringWithFormat(formatString, displayName.resolvedValue(), question) } } diff --git a/SignalServiceKit/Messages/Interactions/Quotes/DraftQuotedReplyModel.swift b/SignalServiceKit/Messages/Interactions/Quotes/DraftQuotedReplyModel.swift index 4a402124e1..8224a58464 100644 --- a/SignalServiceKit/Messages/Interactions/Quotes/DraftQuotedReplyModel.swift +++ b/SignalServiceKit/Messages/Interactions/Quotes/DraftQuotedReplyModel.swift @@ -228,8 +228,8 @@ public class DraftQuotedReplyModel { comment: "quote text for a reaction to a story by some other user (the header on the bubble says their name, e.g. \"Bob\"). Embeds {{reaction emoji}}", ) } - let text = String( - format: formatString, + let text = String.nonPluralLocalizedStringWithFormat( + formatString, emoji, ) return MessageBody(text: text, ranges: .empty) diff --git a/SignalServiceKit/Messages/Interactions/Quotes/QuotedReplyManager.swift b/SignalServiceKit/Messages/Interactions/Quotes/QuotedReplyManager.swift index bd17be4971..94449359d8 100644 --- a/SignalServiceKit/Messages/Interactions/Quotes/QuotedReplyManager.swift +++ b/SignalServiceKit/Messages/Interactions/Quotes/QuotedReplyManager.swift @@ -293,7 +293,7 @@ class QuotedReplyManagerImpl: QuotedReplyManager { ) } }() - body = String(format: formatString, storyReactionEmoji) + body = String.nonPluralLocalizedStringWithFormat(formatString, storyReactionEmoji) bodyRanges = nil isGiftBadge = false isPoll = false diff --git a/SignalServiceKit/Messages/Interactions/TSErrorMessage.swift b/SignalServiceKit/Messages/Interactions/TSErrorMessage.swift index 7632f22331..859b17b265 100644 --- a/SignalServiceKit/Messages/Interactions/TSErrorMessage.swift +++ b/SignalServiceKit/Messages/Interactions/TSErrorMessage.swift @@ -92,6 +92,6 @@ extension TSErrorMessage { comment: "Shown when signal users safety numbers changed, embeds the user's {{name or phone number}}", ) let recipientDisplayName = SSKEnvironment.shared.contactManagerRef.displayName(for: address, tx: tx).resolvedValue() - return String(format: messageFormat, recipientDisplayName) + return String.nonPluralLocalizedStringWithFormat(messageFormat, recipientDisplayName) } } diff --git a/SignalServiceKit/Messages/Interactions/TSInfoMessage+LearnedProfileName.swift b/SignalServiceKit/Messages/Interactions/TSInfoMessage+LearnedProfileName.swift index 208711270c..05fed085f9 100644 --- a/SignalServiceKit/Messages/Interactions/TSInfoMessage+LearnedProfileName.swift +++ b/SignalServiceKit/Messages/Interactions/TSInfoMessage+LearnedProfileName.swift @@ -77,9 +77,9 @@ public extension TSInfoMessage { switch displayNameBeforeLearningProfileName { case .phoneNumber(let phoneNumber): - return String(format: format, phoneNumber) + return String.nonPluralLocalizedStringWithFormat(format, phoneNumber) case .username(let username): - return String(format: format, username) + return String.nonPluralLocalizedStringWithFormat(format, username) } } } diff --git a/SignalServiceKit/Messages/Interactions/TSInfoMessage+Payments.swift b/SignalServiceKit/Messages/Interactions/TSInfoMessage+Payments.swift index 07c09c34e2..9318a07096 100644 --- a/SignalServiceKit/Messages/Interactions/TSInfoMessage+Payments.swift +++ b/SignalServiceKit/Messages/Interactions/TSInfoMessage+Payments.swift @@ -164,7 +164,7 @@ extension TSInfoMessage { } let displayName = SSKEnvironment.shared.contactManagerRef.displayName(for: SignalServiceAddress(aci), tx: transaction) - return String(format: formatString, displayName.resolvedValue()) + return String.nonPluralLocalizedStringWithFormat(formatString, displayName.resolvedValue()) } func paymentsActivatedDescription(tx transaction: DBReadTransaction) -> String? { @@ -182,7 +182,7 @@ extension TSInfoMessage { "INFO_MESSAGE_PAYMENTS_ACTIVATION_REQUEST_FINISHED", comment: "Shown when a user activates payments from a chat. Embeds: {{ the user's name}}", ) - return String(format: format, displayName.resolvedValue()) + return String.nonPluralLocalizedStringWithFormat(format, displayName.resolvedValue()) } } } diff --git a/SignalServiceKit/Messages/Interactions/TSInfoMessage+ProfileChanges.swift b/SignalServiceKit/Messages/Interactions/TSInfoMessage+ProfileChanges.swift index 1854e7f49f..d60a65ad04 100644 --- a/SignalServiceKit/Messages/Interactions/TSInfoMessage+ProfileChanges.swift +++ b/SignalServiceKit/Messages/Interactions/TSInfoMessage+ProfileChanges.swift @@ -247,13 +247,13 @@ public final class ProfileChanges: NSObject, NSSecureCoding, NSCopying { "PROFILE_NAME_CHANGE_SYSTEM_CONTACT_FORMAT", comment: "The copy rendered in a conversation when someone in your address book changes their profile name. Embeds {contact name}, {old profile name}, {new profile name}", ) - return String(format: formatString, systemContactName.resolvedValue(), oldFullName, newFullName) + return String.nonPluralLocalizedStringWithFormat(formatString, systemContactName.resolvedValue(), oldFullName, newFullName) } else { let formatString = OWSLocalizedString( "PROFILE_NAME_CHANGE_SYSTEM_NONCONTACT_FORMAT", comment: "The copy rendered in a conversation when someone not in your address book changes their profile name. Embeds {old profile name}, {new profile name}", ) - return String(format: formatString, oldFullName, newFullName) + return String.nonPluralLocalizedStringWithFormat(formatString, oldFullName, newFullName) } } } diff --git a/SignalServiceKit/Messages/Interactions/TSInfoMessage+SessionSwitchover.swift b/SignalServiceKit/Messages/Interactions/TSInfoMessage+SessionSwitchover.swift index f8faedd641..67d7ae0147 100644 --- a/SignalServiceKit/Messages/Interactions/TSInfoMessage+SessionSwitchover.swift +++ b/SignalServiceKit/Messages/Interactions/TSInfoMessage+SessionSwitchover.swift @@ -37,7 +37,7 @@ public extension TSInfoMessage { "SESSION_SWITCHOVER_EVENT", comment: "If you send a message to a phone number, we might not know the owner of the account. When you later learn the owner of the account, we may show this message. The first parameter is a phone number; the second parameter is the contact's name. Put differently, this message indicates that a phone number belongs to a particular named recipient.", ) - return String(format: formatString, formattedPhoneNumber, displayName) + return String.nonPluralLocalizedStringWithFormat(formatString, formattedPhoneNumber, displayName) } else { let address = TSContactThread.contactAddress(fromThreadId: uniqueThreadId, transaction: tx) return TSErrorMessage.safetyNumberChangeDescription(for: address, tx: tx) diff --git a/SignalServiceKit/Messages/Interactions/TSInfoMessage+ThreadMerge.swift b/SignalServiceKit/Messages/Interactions/TSInfoMessage+ThreadMerge.swift index 2a748e1b3e..036604fc1b 100644 --- a/SignalServiceKit/Messages/Interactions/TSInfoMessage+ThreadMerge.swift +++ b/SignalServiceKit/Messages/Interactions/TSInfoMessage+ThreadMerge.swift @@ -39,13 +39,13 @@ public extension TSInfoMessage { comment: "A system event shown in a conversation when multiple conversations for the same person have been merged into one. The parameters are replaced with the contact's name (eg John Doe) and their phone number (eg +1 650 555 0100).", ) let formattedPhoneNumber = PhoneNumber.bestEffortLocalizedPhoneNumber(e164: phoneNumber) - return String(format: formatString, displayName, formattedPhoneNumber) + return String.nonPluralLocalizedStringWithFormat(formatString, displayName, formattedPhoneNumber) } else { let formatString = OWSLocalizedString( "THREAD_MERGE_NO_PHONE_NUMBER", comment: "A system event shown in a conversation when multiple conversations for the same person have been merged into one. The parameter is replaced with the contact's name (eg John Doe).", ) - return String(format: formatString, displayName) + return String.nonPluralLocalizedStringWithFormat(formatString, displayName) } } diff --git a/SignalServiceKit/Messages/Interactions/TSInfoMessage.swift b/SignalServiceKit/Messages/Interactions/TSInfoMessage.swift index 4665cb46ee..a68a2677ee 100644 --- a/SignalServiceKit/Messages/Interactions/TSInfoMessage.swift +++ b/SignalServiceKit/Messages/Interactions/TSInfoMessage.swift @@ -41,8 +41,8 @@ extension TSInfoMessage { case .userNotRegistered: if let unregisteredAddress, unregisteredAddress.isValid { let recipientName = SSKEnvironment.shared.contactManagerRef.displayNameString(for: unregisteredAddress, transaction: tx) - return String( - format: OWSLocalizedString( + return String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "ERROR_UNREGISTERED_USER_FORMAT", comment: "Format string for 'unregistered user' error. Embeds {{the unregistered user's name or signal id}}.", ), @@ -71,7 +71,7 @@ extension TSInfoMessage { let address = TSContactThread.contactAddress(fromThreadId: self.uniqueThreadId, transaction: tx) let recipientName = SSKEnvironment.shared.contactManagerRef.displayNameString(for: address!, transaction: tx) let format = OWSLocalizedString("INFO_MESSAGE_USER_JOINED_SIGNAL_BODY_FORMAT", comment: "Shown in inbox and conversation when a user joins Signal, embeds the new user's {{contact name}}") - return String(format: format, recipientName) + return String.nonPluralLocalizedStringWithFormat(format, recipientName) case .syncedThread: return "" case .profileUpdate: @@ -87,7 +87,7 @@ extension TSInfoMessage { "INFO_MESSAGE_USER_CHANGED_PHONE_NUMBER_FORMAT", comment: "Indicates that another user has changed their phone number. Embeds: {{ the user's name}}", ) - return String(format: format, userName) + return String.nonPluralLocalizedStringWithFormat(format, userName) case .recipientHidden: /// This does not control whether to show the info message in the chat /// preview. To control that, see ``TSInteraction.shouldAppearInInbox``. diff --git a/SignalServiceKit/Messages/Interactions/TSMessage.swift b/SignalServiceKit/Messages/Interactions/TSMessage.swift index fc6da7b9cb..aea78e1c8b 100644 --- a/SignalServiceKit/Messages/Interactions/TSMessage.swift +++ b/SignalServiceKit/Messages/Interactions/TSMessage.swift @@ -510,7 +510,7 @@ public extension TSMessage { "DONATION_ON_BEHALF_OF_A_FRIEND_PREVIEW_INCOMING", comment: "A friend has donated on your behalf. This text is shown in the list of chats, when the most recent message is one of these donations. Embeds {friend's short display name}.", ) - return String(format: format, senderShortName) + return String.nonPluralLocalizedStringWithFormat(format, senderShortName) } else if let outgoingMessage = self as? TSOutgoingMessage { let recipientShortName: String let recipients = outgoingMessage.recipientAddresses() @@ -527,7 +527,7 @@ public extension TSMessage { "DONATION_ON_BEHALF_OF_A_FRIEND_PREVIEW_OUTGOING", comment: "You have a made a donation on a friend's behalf. This text is shown in the list of chats, when the most recent message is one of these donations. Embeds {friend's short display name}.", ) - return String(format: format, recipientShortName) + return String.nonPluralLocalizedStringWithFormat(format, recipientShortName) } else { owsFail("Could not generate preview text because message wasn't incoming or outgoing") } @@ -636,13 +636,13 @@ public extension TSMessage { switch deleteAuthor { case .admin(_, let displayName): let format = OWSLocalizedString("DELETED_BY_ADMIN", comment: "Text indicating the message was remotely deleted by an admin. Embeds {{admin display name}}") - remoteDeleteString = String(format: format, displayName) + remoteDeleteString = String.nonPluralLocalizedStringWithFormat(format, displayName) case .regular(let displayName): let format = OWSLocalizedString( "DELETED_THIS_MESSAGE", comment: "Text indicating the message was remotely deleted by its author. Embeds {{ author name }}", ) - remoteDeleteString = String(format: format, displayName) + remoteDeleteString = String.nonPluralLocalizedStringWithFormat(format, displayName) case .localUser: remoteDeleteString = OWSLocalizedString("YOU_DELETED_THIS_MESSAGE", comment: "text indicating the message was remotely deleted by you") } @@ -662,8 +662,8 @@ public extension TSMessage { let localIdentifiers = tsAccountManager.localIdentifiers(tx: tx), localIdentifiers.contains(serviceId: storyAuthorAci) { - return .storyReactionEmoji(String( - format: OWSLocalizedString( + return .storyReactionEmoji(String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "STORY_REACTION_PREVIEW_FORMAT_THIRD_PERSON", comment: "Text explaining that someone reacted to your story. Embeds {{ %1$@ reaction emoji }}.", ), @@ -671,8 +671,8 @@ public extension TSMessage { )) } else { let storyAuthorName = contactManager.displayName(for: SignalServiceAddress(storyAuthorAci), tx: tx) - return .storyReactionEmoji(String( - format: OWSLocalizedString( + return .storyReactionEmoji(String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "STORY_REACTION_PREVIEW_FORMAT_SECOND_PERSON", comment: "Text explaining that you reacted to someone else's story. Embeds {{ %1$@ reaction emoji, %2$@ story author name }}.", ), diff --git a/SignalServiceKit/Messages/MessageSender+Errors.swift b/SignalServiceKit/Messages/MessageSender+Errors.swift index 0d02cd0fab..78966d1f91 100644 --- a/SignalServiceKit/Messages/MessageSender+Errors.swift +++ b/SignalServiceKit/Messages/MessageSender+Errors.swift @@ -138,7 +138,7 @@ public class UntrustedIdentityError: CustomNSError, UserErrorDescriptionProvider "FAILED_SENDING_BECAUSE_UNTRUSTED_IDENTITY_KEY", comment: "action sheet header when re-sending message which failed because of untrusted identity keys", ) - return String(format: format, SSKEnvironment.shared.databaseStorageRef.read { tx in + return String.nonPluralLocalizedStringWithFormat(format, SSKEnvironment.shared.databaseStorageRef.read { tx in return SSKEnvironment.shared.contactManagerRef.displayName(for: SignalServiceAddress(serviceId), tx: tx).resolvedValue() }) } @@ -171,7 +171,7 @@ public class InvalidKeySignatureError: CustomNSError, IsRetryableProvider, UserE "FAILED_SENDING_BECAUSE_INVALID_KEY_SIGNATURE", comment: "action sheet header when re-sending message which failed because of an invalid key signature", ) - return String(format: format, SSKEnvironment.shared.databaseStorageRef.read { tx in + return String.nonPluralLocalizedStringWithFormat(format, SSKEnvironment.shared.databaseStorageRef.read { tx in return SSKEnvironment.shared.contactManagerRef.displayName(for: SignalServiceAddress(serviceId), tx: tx).resolvedValue() }) } diff --git a/SignalServiceKit/Messages/Stories/StoryMessage.swift b/SignalServiceKit/Messages/Stories/StoryMessage.swift index dea50a2709..14971e6c7f 100644 --- a/SignalServiceKit/Messages/Stories/StoryMessage.swift +++ b/SignalServiceKit/Messages/Stories/StoryMessage.swift @@ -1167,12 +1167,12 @@ extension StoryMessage { public static let videoAttachmentDurationLimit: TimeInterval = 30.999 public static var videoSegmentationTooltip: String { - return String( - format: OWSLocalizedString( + return String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "STORY_VIDEO_SEGMENTATION_TOOLTIP_FORMAT", comment: "Tooltip text shown when the user selects a story as a destination for a long duration video that will be split into shorter segments. Embeds {{ segment duration in seconds }}", ), - Int(videoAttachmentDurationLimit), + String(Int(videoAttachmentDurationLimit)), ) } } diff --git a/SignalServiceKit/Notifications/NotificationPresenterImpl.swift b/SignalServiceKit/Notifications/NotificationPresenterImpl.swift index ce3ad91ebf..968fb2e2fd 100644 --- a/SignalServiceKit/Notifications/NotificationPresenterImpl.swift +++ b/SignalServiceKit/Notifications/NotificationPresenterImpl.swift @@ -436,7 +436,7 @@ public class NotificationPresenterImpl: NotificationPresenter { comment: "notification body for a missed call from more than a week ago. Embeds {{short date}}, e.g. '6/28'.", ) } - let notificationBody = String(format: notificationBodyFormat, timestampArgument) + let notificationBody = String.nonPluralLocalizedStringWithFormat(notificationBodyFormat, timestampArgument) let userInfo = userInfoForMissedCall(thread: thread, remoteAci: notificationInfo.caller) @@ -661,7 +661,7 @@ public class NotificationPresenterImpl: NotificationPresenter { transaction: transaction, ) - let notificationBody: String = "\u{1F4CA}" + String(format: pollEndedFormat, pollAuthorName.string, pollQuestion) + let notificationBody: String = "\u{1F4CA}" + String.nonPluralLocalizedStringWithFormat(pollEndedFormat, pollAuthorName.string, pollQuestion) let intent = thread.generateSendMessageIntent(context: .senderAddress(message.authorAddress), transaction: transaction) @@ -725,7 +725,7 @@ public class NotificationPresenterImpl: NotificationPresenter { transaction: transaction, ) - let notificationBody: String = "\u{1F4CA}" + String(format: pollVotedFormat, voteAuthorName.string, pollQuestion) + let notificationBody: String = "\u{1F4CA}" + String.nonPluralLocalizedStringWithFormat(pollVotedFormat, voteAuthorName.string, pollQuestion) var userInfo = AppNotificationUserInfo() let threadUniqueId = thread.uniqueId @@ -806,7 +806,7 @@ public class NotificationPresenterImpl: NotificationPresenter { ) return resolvableValue( withDisplayNameForAddress: senderAddress, - transformedBy: { displayName in String(format: format, displayName.resolvedValue(), groupName) }, + transformedBy: { displayName in String.nonPluralLocalizedStringWithFormat(format, displayName.resolvedValue(), groupName) }, tx: tx, ) } else { @@ -974,13 +974,13 @@ public class NotificationPresenterImpl: NotificationPresenter { return nil } }() { - notificationBody = String(format: NotificationStrings.incomingReactionTextMessageFormat, reaction.emoji, bodyDescription) + notificationBody = String.nonPluralLocalizedStringWithFormat(NotificationStrings.incomingReactionTextMessageFormat, reaction.emoji, bodyDescription) } else if message.isViewOnceMessage { - notificationBody = String(format: NotificationStrings.incomingReactionViewOnceMessageFormat, reaction.emoji) + notificationBody = String.nonPluralLocalizedStringWithFormat(NotificationStrings.incomingReactionViewOnceMessageFormat, reaction.emoji) } else if message.messageSticker != nil { - notificationBody = String(format: NotificationStrings.incomingReactionStickerMessageFormat, reaction.emoji) + notificationBody = String.nonPluralLocalizedStringWithFormat(NotificationStrings.incomingReactionStickerMessageFormat, reaction.emoji) } else if message.contactShare != nil { - notificationBody = String(format: NotificationStrings.incomingReactionContactShareMessageFormat, reaction.emoji) + notificationBody = String.nonPluralLocalizedStringWithFormat(NotificationStrings.incomingReactionContactShareMessageFormat, reaction.emoji) } else if let messageRowId = message.sqliteRowId, let mediaAttachments = DependenciesBridge.shared.attachmentStore @@ -997,27 +997,27 @@ public class NotificationPresenterImpl: NotificationPresenter { let firstMimeType = firstAttachment.attachment.mimeType if mediaAttachments.count > 1 { - notificationBody = String(format: NotificationStrings.incomingReactionAlbumMessageFormat, reaction.emoji) + notificationBody = String.nonPluralLocalizedStringWithFormat(NotificationStrings.incomingReactionAlbumMessageFormat, reaction.emoji) } else if MimeTypeUtil.isSupportedDefinitelyAnimatedMimeType(firstMimeType) { - notificationBody = String(format: NotificationStrings.incomingReactionGifMessageFormat, reaction.emoji) + notificationBody = String.nonPluralLocalizedStringWithFormat(NotificationStrings.incomingReactionGifMessageFormat, reaction.emoji) } else if MimeTypeUtil.isSupportedImageMimeType(firstMimeType) { - notificationBody = String(format: NotificationStrings.incomingReactionPhotoMessageFormat, reaction.emoji) + notificationBody = String.nonPluralLocalizedStringWithFormat(NotificationStrings.incomingReactionPhotoMessageFormat, reaction.emoji) } else if MimeTypeUtil.isSupportedVideoMimeType(firstMimeType), firstRenderingFlag == .shouldLoop { - notificationBody = String(format: NotificationStrings.incomingReactionGifMessageFormat, reaction.emoji) + notificationBody = String.nonPluralLocalizedStringWithFormat(NotificationStrings.incomingReactionGifMessageFormat, reaction.emoji) } else if MimeTypeUtil.isSupportedVideoMimeType(firstMimeType) { - notificationBody = String(format: NotificationStrings.incomingReactionVideoMessageFormat, reaction.emoji) + notificationBody = String.nonPluralLocalizedStringWithFormat(NotificationStrings.incomingReactionVideoMessageFormat, reaction.emoji) } else if firstRenderingFlag == .voiceMessage { - notificationBody = String(format: NotificationStrings.incomingReactionVoiceMessageFormat, reaction.emoji) + notificationBody = String.nonPluralLocalizedStringWithFormat(NotificationStrings.incomingReactionVoiceMessageFormat, reaction.emoji) } else if MimeTypeUtil.isSupportedAudioMimeType(firstMimeType) { - notificationBody = String(format: NotificationStrings.incomingReactionAudioMessageFormat, reaction.emoji) + notificationBody = String.nonPluralLocalizedStringWithFormat(NotificationStrings.incomingReactionAudioMessageFormat, reaction.emoji) } else { - notificationBody = String(format: NotificationStrings.incomingReactionFileMessageFormat, reaction.emoji) + notificationBody = String.nonPluralLocalizedStringWithFormat(NotificationStrings.incomingReactionFileMessageFormat, reaction.emoji) } } else { - notificationBody = String(format: NotificationStrings.incomingReactionFormat, reaction.emoji) + notificationBody = String.nonPluralLocalizedStringWithFormat(NotificationStrings.incomingReactionFormat, reaction.emoji) } // Don't reply from lockscreen if anyone in this conversation is @@ -1109,7 +1109,7 @@ public class NotificationPresenterImpl: NotificationPresenter { "ERROR_NOTIFICATION_MESSAGE_FORMAT", comment: "Format string for an error alert notification message. Embeds {{ error string }}", ) - let message = String(format: messageFormat, errorString) + let message = String.nonPluralLocalizedStringWithFormat(messageFormat, errorString) var userInfo = AppNotificationUserInfo() userInfo.defaultAction = .submitDebugLogs @@ -1175,8 +1175,8 @@ public class NotificationPresenterImpl: NotificationPresenter { "LINKED_DEVICE_NOTIFICATION_TITLE", comment: "Title for system notification when a new device is linked.", )), - body: String( - format: OWSLocalizedString( + body: String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "LINKED_DEVICE_NOTIFICATION_BODY", comment: "Body for system notification when a new device is linked. Embeds {{ time the device was linked }}", ), @@ -1201,8 +1201,8 @@ public class NotificationPresenterImpl: NotificationPresenter { "BACKUPS_TURNED_ON_TITLE", comment: "Title for system notification or megaphone when backups is enabled", )), - body: String( - format: OWSLocalizedString( + body: String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "BACKUPS_TURNED_ON_NOTIFICATION_BODY_FORMAT", comment: "Body for system notification or megaphone when backups is enabled. Embeds {{ time backups was enabled }}", ), diff --git a/SignalServiceKit/Security/OWSFingerprint.swift b/SignalServiceKit/Security/OWSFingerprint.swift index d1f278df78..8dab32e9b0 100644 --- a/SignalServiceKit/Security/OWSFingerprint.swift +++ b/SignalServiceKit/Security/OWSFingerprint.swift @@ -98,7 +98,7 @@ public class OWSFingerprint { "PRIVACY_VERIFICATION_FAILED_I_HAVE_WRONG_KEY_FOR_THEM", comment: "Alert body when verifying with {{contact name}}", ) - let description = String(format: descriptionFormat, self.theirName) + let description = String.nonPluralLocalizedStringWithFormat(descriptionFormat, self.theirName) return .noMatch(localizedErrorDescription: description) } if localFingerprint.identityData != Self.scannableData(from: self.myFingerprintData) { @@ -107,7 +107,7 @@ public class OWSFingerprint { "PRIVACY_VERIFICATION_FAILED_THEY_HAVE_WRONG_KEY_FOR_ME", comment: "Alert body when verifying with {{contact name}}", ) - let description = String(format: descriptionFormat, self.theirName) + let description = String.nonPluralLocalizedStringWithFormat(descriptionFormat, self.theirName) return .noMatch(localizedErrorDescription: description) } diff --git a/SignalServiceKit/Util/DateUtil.swift b/SignalServiceKit/Util/DateUtil.swift index 76859fb633..d77e609b5e 100644 --- a/SignalServiceKit/Util/DateUtil.swift +++ b/SignalServiceKit/Util/DateUtil.swift @@ -379,8 +379,8 @@ public class DateUtil { formatReplacement = seconds / secondsPerWeek } - return String( - format: formatString, + return String.nonPluralLocalizedStringWithFormat( + formatString, NumberFormatter.localizedString(from: NSNumber(value: formatReplacement), number: .none), ) } diff --git a/SignalServiceKit/Util/OWSLocalizedString.swift b/SignalServiceKit/Util/OWSLocalizedString.swift index cfd0a242fb..a27eaab3f2 100644 --- a/SignalServiceKit/Util/OWSLocalizedString.swift +++ b/SignalServiceKit/Util/OWSLocalizedString.swift @@ -25,3 +25,34 @@ public extension Bundle { public func OWSLocalizedString(_ key: String, tableName: String? = nil, value: String = "", comment: String) -> String { return NSLocalizedString(key, tableName: tableName, bundle: .main.app, value: value, comment: comment) } + +extension String { + public static func nonPluralLocalizedStringWithFormat(_ format: String, _ arguments: String...) -> String { + return nonPluralLocalizedStringWithFormat(format, arguments: arguments) + } + + public static func nonPluralLocalizedStringWithFormat(_ format: String, arguments: [String]) -> String { + var result = "" + var remainingFormat = format[...] + var remainingArguments = arguments[...] + while let range = remainingFormat.range(of: "%") { + result += remainingFormat[.. UIView { let label = UILabel() let fileSize = (try? attachment.rawValue.dataSource.readLength()) ?? 0 - label.text = String( - format: OWSLocalizedString( + label.text = String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "ATTACHMENT_APPROVAL_FILE_SIZE_FORMAT", comment: "Format string for file size label in call interstitial view. Embeds: {{file size as 'N mb' or 'N kb'}}.", ), diff --git a/SignalUI/Views/ProfileDetailLabel.swift b/SignalUI/Views/ProfileDetailLabel.swift index 9a3050dde4..d634d54c2c 100644 --- a/SignalUI/Views/ProfileDetailLabel.swift +++ b/SignalUI/Views/ProfileDetailLabel.swift @@ -249,8 +249,8 @@ public extension ProfileDetailLabel { font: UIFont = .dynamicTypeBody, ) -> ProfileDetailLabel { .init( - title: String( - format: OWSLocalizedString( + title: String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "CONTACT_ABOUT_SHEET_NO_DIRECT_MESSAGES", comment: "Indicates that the user has no messages with the other account. Embeds {{name}}", ), @@ -266,8 +266,8 @@ public extension ProfileDetailLabel { font: UIFont = .dynamicTypeBody, ) -> ProfileDetailLabel { .init( - title: String( - format: OWSLocalizedString( + title: String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "CONTACT_ABOUT_SHEET_BLOCKED_USER_FORMAT", comment: "Indicates that the user has blocked the other account. Embeds {{name}}", ), @@ -297,8 +297,8 @@ public extension ProfileDetailLabel { font: UIFont = .dynamicTypeBody, ) -> ProfileDetailLabel { .init( - title: String( - format: OWSLocalizedString( + title: String.nonPluralLocalizedStringWithFormat( + OWSLocalizedString( "CONTACT_ABOUT_SHEET_CONNECTION_IN_SYSTEM_CONTACTS", comment: "Indicates that another account is in the user's system contacts. Embeds {{name}}", ), @@ -383,7 +383,7 @@ public extension ProfileDetailLabel { "THREAD_DETAILS_ONE_MUTUAL_GROUP", comment: "A string indicating a mutual group the user shares with this contact. Embeds {{mutual group name}}", ) - return String(format: formatString, mutualGroups[0].groupNameOrDefault) + return String.nonPluralLocalizedStringWithFormat(formatString, mutualGroups[0].groupNameOrDefault) case (true, 1): return OWSLocalizedString( diff --git a/SignalUI/Views/TextFieldFormatting.swift b/SignalUI/Views/TextFieldFormatting.swift index 035b3200bb..49e908d1ce 100644 --- a/SignalUI/Views/TextFieldFormatting.swift +++ b/SignalUI/Views/TextFieldFormatting.swift @@ -191,7 +191,7 @@ public class TextFieldFormatting { "PHONE_NUMBER_EXAMPLE_FORMAT", comment: "A format for a label showing an example phone number. Embeds {{the example phone number}}.", ) - return String(format: formatString, nationalNumber) + return String.nonPluralLocalizedStringWithFormat(formatString, nationalNumber) } }