From 41e499fdf58fe63d882c8d2f5a72b71488cc2ca3 Mon Sep 17 00:00:00 2001 From: Konrad <11725227+mKoonrad@users.noreply.github.com> Date: Thu, 4 Sep 2025 20:13:58 +0200 Subject: [PATCH] [PM-25133] Plural forms (#5773) Co-authored-by: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com> Co-authored-by: Patrick Honkonen --- .../CompleteRegistrationViewModel.kt | 9 +++++++-- .../PasswordStrengthIndicator.kt | 9 +++++++-- .../resetpassword/ResetPasswordViewModel.kt | 9 +++++++-- .../feature/setpassword/SetPasswordViewModel.kt | 9 +++++++-- .../feature/addedit/VaultAddEditViewModel.kt | 8 +++++++- .../ui/vault/feature/item/VaultItemViewModel.kt | 7 ++++++- .../CompleteRegistrationViewModelTest.kt | 7 ++++++- .../resetPassword/ResetPasswordViewModelTest.kt | 8 ++++++-- .../setpassword/SetPasswordViewModelTest.kt | 8 ++++++-- .../feature/addedit/VaultAddEditViewModelTest.kt | 7 ++++++- .../vault/feature/item/VaultItemViewModelTest.kt | 7 ++++++- ui/src/main/kotlin/com/bitwarden/ui/util/Text.kt | 8 ++++++++ ui/src/main/res/values/strings.xml | 15 ++++++++++++--- 13 files changed, 91 insertions(+), 20 deletions(-) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModel.kt index e0b90ec8c8..e2416d5c79 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModel.kt @@ -5,8 +5,10 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.bitwarden.ui.platform.base.BaseViewModel import com.bitwarden.ui.platform.base.util.isValidEmail +import com.bitwarden.ui.platform.resource.BitwardenPlurals import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.util.Text +import com.bitwarden.ui.util.asPluralsText import com.bitwarden.ui.util.asText import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength @@ -321,8 +323,11 @@ class CompleteRegistrationViewModel @Inject constructor( it.copy( dialog = CompleteRegistrationDialog.Error( title = BitwardenString.an_error_has_occurred.asText(), - message = BitwardenString.master_password_length_val_message_x - .asText(MIN_PASSWORD_LENGTH), + message = BitwardenPlurals.master_password_length_val_message_x + .asPluralsText( + quantity = MIN_PASSWORD_LENGTH, + args = arrayOf(MIN_PASSWORD_LENGTH), + ), ), ) } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/completeregistration/PasswordStrengthIndicator.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/completeregistration/PasswordStrengthIndicator.kt index 015a64fc43..290382a716 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/completeregistration/PasswordStrengthIndicator.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/completeregistration/PasswordStrengthIndicator.kt @@ -24,11 +24,12 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.bitwarden.ui.platform.components.util.rememberVectorPainter import com.bitwarden.ui.platform.resource.BitwardenDrawable +import com.bitwarden.ui.platform.resource.BitwardenPlurals import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.platform.theme.BitwardenTheme import com.bitwarden.ui.util.asText @@ -154,7 +155,11 @@ private fun MinimumCharacterCount( } Spacer(modifier = Modifier.width(2.dp)) Text( - text = stringResource(BitwardenString.minimum_characters, minimumCharacterCount), + text = pluralStringResource( + id = BitwardenPlurals.minimum_characters, + count = minimumCharacterCount, + formatArgs = arrayOf(minimumCharacterCount), + ), color = BitwardenTheme.colorScheme.text.secondary, style = BitwardenTheme.typography.labelSmall, ) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/resetpassword/ResetPasswordViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/resetpassword/ResetPasswordViewModel.kt index e64bea4cfa..a28c5d623e 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/resetpassword/ResetPasswordViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/resetpassword/ResetPasswordViewModel.kt @@ -5,8 +5,10 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.bitwarden.ui.platform.base.BaseViewModel import com.bitwarden.ui.platform.base.util.orNullIfBlank +import com.bitwarden.ui.platform.resource.BitwardenPlurals import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.util.Text +import com.bitwarden.ui.util.asPluralsText import com.bitwarden.ui.util.asText import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength @@ -199,8 +201,11 @@ class ResetPasswordViewModel @Inject constructor( it.copy( dialogState = ResetPasswordState.DialogState.Error( title = BitwardenString.an_error_has_occurred.asText(), - message = BitwardenString.master_password_length_val_message_x - .asText(MIN_PASSWORD_LENGTH), + message = BitwardenPlurals.master_password_length_val_message_x + .asPluralsText( + quantity = MIN_PASSWORD_LENGTH, + args = arrayOf(MIN_PASSWORD_LENGTH), + ), ), ) } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/setpassword/SetPasswordViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/setpassword/SetPasswordViewModel.kt index 184052b7e0..eaaa19c088 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/setpassword/SetPasswordViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/setpassword/SetPasswordViewModel.kt @@ -4,8 +4,10 @@ import android.os.Parcelable import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.bitwarden.ui.platform.base.BaseViewModel +import com.bitwarden.ui.platform.resource.BitwardenPlurals import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.util.Text +import com.bitwarden.ui.util.asPluralsText import com.bitwarden.ui.util.asText import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason @@ -111,8 +113,11 @@ class SetPasswordViewModel @Inject constructor( it.copy( dialogState = SetPasswordState.DialogState.Error( title = BitwardenString.an_error_has_occurred.asText(), - message = BitwardenString.master_password_length_val_message_x - .asText(MIN_PASSWORD_LENGTH), + message = BitwardenPlurals.master_password_length_val_message_x + .asPluralsText( + quantity = MIN_PASSWORD_LENGTH, + args = arrayOf(MIN_PASSWORD_LENGTH), + ), ), ) } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt index f3725a9392..ac8042231d 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt @@ -14,8 +14,10 @@ import com.bitwarden.network.model.PolicyTypeJson import com.bitwarden.ui.platform.base.BackgroundEvent import com.bitwarden.ui.platform.base.BaseViewModel import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData +import com.bitwarden.ui.platform.resource.BitwardenPlurals import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.util.Text +import com.bitwarden.ui.util.asPluralsText import com.bitwarden.ui.util.asText import com.bitwarden.vault.CipherView import com.bitwarden.vault.DecryptCipherListResult @@ -1945,7 +1947,11 @@ class VaultAddEditViewModel @Inject constructor( is BreachCountResult.Success -> { VaultAddEditState.DialogState.Generic( message = if (result.breachCount > 0) { - BitwardenString.password_exposed.asText(result.breachCount) + BitwardenPlurals.password_exposed + .asPluralsText( + quantity = result.breachCount, + args = arrayOf(result.breachCount), + ) } else { BitwardenString.password_safe.asText() }, diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt index dcc15d5a69..088228e1ab 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt @@ -13,8 +13,10 @@ import com.bitwarden.ui.platform.base.BackgroundEvent import com.bitwarden.ui.platform.base.BaseViewModel import com.bitwarden.ui.platform.components.icon.model.IconData import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData +import com.bitwarden.ui.platform.resource.BitwardenPlurals import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.util.Text +import com.bitwarden.ui.util.asPluralsText import com.bitwarden.ui.util.asText import com.bitwarden.ui.util.concat import com.bitwarden.vault.CipherView @@ -978,7 +980,10 @@ class VaultItemViewModel @Inject constructor( is BreachCountResult.Success -> { VaultItemState.DialogState.Generic( message = if (result.breachCount > 0) { - BitwardenString.password_exposed.asText(result.breachCount) + BitwardenPlurals.password_exposed.asPluralsText( + quantity = result.breachCount, + args = arrayOf(result.breachCount), + ) } else { BitwardenString.password_safe.asText() }, diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModelTest.kt index 9b116acaf5..492577c821 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModelTest.kt @@ -5,7 +5,9 @@ import app.cash.turbine.test import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow import com.bitwarden.data.datasource.disk.base.FakeDispatcherManager import com.bitwarden.ui.platform.base.BaseViewModelTest +import com.bitwarden.ui.platform.resource.BitwardenPlurals import com.bitwarden.ui.platform.resource.BitwardenString +import com.bitwarden.ui.util.asPluralsText import com.bitwarden.ui.util.asText import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength.LEVEL_0 @@ -590,7 +592,10 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() { passwordInput = input, dialog = CompleteRegistrationDialog.Error( title = BitwardenString.an_error_has_occurred.asText(), - message = BitwardenString.master_password_length_val_message_x.asText(12), + message = BitwardenPlurals.master_password_length_val_message_x.asPluralsText( + quantity = 12, + args = arrayOf(12), + ), ), ) viewModel.trySendAction(CompleteRegistrationAction.CallToActionClick) diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/resetPassword/ResetPasswordViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/resetPassword/ResetPasswordViewModelTest.kt index 44c6706e00..c7b9d77853 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/resetPassword/ResetPasswordViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/resetPassword/ResetPasswordViewModelTest.kt @@ -3,7 +3,9 @@ package com.x8bit.bitwarden.ui.auth.feature.resetPassword import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.bitwarden.ui.platform.base.BaseViewModelTest +import com.bitwarden.ui.platform.resource.BitwardenPlurals import com.bitwarden.ui.platform.resource.BitwardenString +import com.bitwarden.ui.util.asPluralsText import com.bitwarden.ui.util.asText import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength @@ -133,8 +135,10 @@ class ResetPasswordViewModelTest : BaseViewModelTest() { resetReason = ForcePasswordResetReason.ADMIN_FORCE_PASSWORD_RESET, dialogState = ResetPasswordState.DialogState.Error( title = BitwardenString.an_error_has_occurred.asText(), - message = BitwardenString.master_password_length_val_message_x - .asText(MIN_PASSWORD_LENGTH), + message = BitwardenPlurals.master_password_length_val_message_x.asPluralsText( + quantity = MIN_PASSWORD_LENGTH, + args = arrayOf(MIN_PASSWORD_LENGTH), + ), ), passwordInput = password, passwordStrengthState = PasswordStrengthState.WEAK_1, diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/setpassword/SetPasswordViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/setpassword/SetPasswordViewModelTest.kt index afe54beb1d..70fa8adb94 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/setpassword/SetPasswordViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/setpassword/SetPasswordViewModelTest.kt @@ -3,7 +3,9 @@ package com.x8bit.bitwarden.ui.auth.feature.setpassword import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.bitwarden.ui.platform.base.BaseViewModelTest +import com.bitwarden.ui.platform.resource.BitwardenPlurals import com.bitwarden.ui.platform.resource.BitwardenString +import com.bitwarden.ui.util.asPluralsText import com.bitwarden.ui.util.asText import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason @@ -87,8 +89,10 @@ class SetPasswordViewModelTest : BaseViewModelTest() { DEFAULT_STATE.copy( dialogState = SetPasswordState.DialogState.Error( title = BitwardenString.an_error_has_occurred.asText(), - message = BitwardenString.master_password_length_val_message_x - .asText(MIN_PASSWORD_LENGTH), + message = BitwardenPlurals.master_password_length_val_message_x.asPluralsText( + quantity = MIN_PASSWORD_LENGTH, + args = arrayOf(MIN_PASSWORD_LENGTH), + ), ), passwordInput = password, retypePasswordInput = password, diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt index 8faf2b96d1..b22baa5fef 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt @@ -20,8 +20,10 @@ import com.bitwarden.network.model.SyncResponseJson import com.bitwarden.send.SendView import com.bitwarden.ui.platform.base.BaseViewModelTest import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData +import com.bitwarden.ui.platform.resource.BitwardenPlurals import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.util.Text +import com.bitwarden.ui.util.asPluralsText import com.bitwarden.ui.util.asText import com.bitwarden.vault.CipherListView import com.bitwarden.vault.CipherView @@ -2439,7 +2441,10 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { assertEquals( loginState.copy( dialog = VaultAddEditState.DialogState.Generic( - message = BitwardenString.password_exposed.asText(breachCount), + message = BitwardenPlurals.password_exposed.asPluralsText( + quantity = breachCount, + args = arrayOf(breachCount), + ), ), ), awaitItem(), diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt index 133c981d57..a29305dc57 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt @@ -13,8 +13,10 @@ import com.bitwarden.ui.platform.base.BaseViewModelTest import com.bitwarden.ui.platform.components.icon.model.IconData import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData import com.bitwarden.ui.platform.resource.BitwardenDrawable +import com.bitwarden.ui.platform.resource.BitwardenPlurals import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.util.Text +import com.bitwarden.ui.util.asPluralsText import com.bitwarden.ui.util.asText import com.bitwarden.ui.util.concat import com.bitwarden.vault.CipherView @@ -1289,7 +1291,10 @@ class VaultItemViewModelTest : BaseViewModelTest() { assertEquals( loginState.copy( dialog = VaultItemState.DialogState.Generic( - message = BitwardenString.password_exposed.asText(breachCount), + message = BitwardenPlurals.password_exposed.asPluralsText( + quantity = breachCount, + args = arrayOf(breachCount), + ), ), ), awaitItem(), diff --git a/ui/src/main/kotlin/com/bitwarden/ui/util/Text.kt b/ui/src/main/kotlin/com/bitwarden/ui/util/Text.kt index 7f7d7d5c5f..72ee30017c 100644 --- a/ui/src/main/kotlin/com/bitwarden/ui/util/Text.kt +++ b/ui/src/main/kotlin/com/bitwarden/ui/util/Text.kt @@ -115,3 +115,11 @@ fun @receiver:StringRes Int.asText(): Text = ResText(this) * Convert a resource Id to [Text] with format args. */ fun @receiver:StringRes Int.asText(vararg args: Any): Text = ResArgsText(this, args.asList()) + +/** + * Convert a resource Id to [Text] with quantity and format args. + */ +fun @receiver:PluralsRes Int.asPluralsText( + quantity: Int, + vararg args: Any, +): Text = PluralsText(id = this, quantity = quantity, args = args.asList()) diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index 2f5cdf5dc7..bed94b9fe7 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -110,7 +110,10 @@ The master password is the password you use to access your vault. It is very important that you do not forget your master password. There is no way to recover the password in the event that you forget it. Master password hint (optional) A master password hint can help you remember your password if you forget it. - Master password must be at least %1$s characters long. + + Master password must be at least %d character long. + Master password must be at least %d characters long. + Minimum numbers Minimum special Never @@ -309,7 +312,10 @@ Scanning will happen automatically. Personal Details Contact Info Check password for data breaches - This password has been exposed %1$s time(s) in data breaches. You should change it. + + This password has been exposed %d time in data breaches. You should change it. + This password has been exposed %d times in data breaches. You should change it. + This password was not found in any known data breaches. It should be safe to use. Identity name Value @@ -749,7 +755,10 @@ Do you want to switch to this account? Bitwarden cannot reset a lost or forgotten master password. Choose your master password Choose a unique and strong password to keep your information safe. - %1$s characters + + %d character + %d characters + Expired link Please restart registration or try logging in. You may already have an account. Restart registration