[PM-25133] Plural forms (#5773)

Co-authored-by: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com>
Co-authored-by: Patrick Honkonen <phonkonen@bitwarden.com>
This commit is contained in:
Konrad 2025-09-04 20:13:58 +02:00 committed by GitHub
parent aa39e6c6be
commit 41e499fdf5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 91 additions and 20 deletions

View File

@ -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),
),
),
)
}

View File

@ -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,
)

View File

@ -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),
),
),
)
}

View File

@ -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),
),
),
)
}

View File

@ -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()
},

View File

@ -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()
},

View File

@ -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)

View File

@ -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,

View File

@ -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,

View File

@ -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(),

View File

@ -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(),

View File

@ -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())

View File

@ -110,7 +110,10 @@
<string name="master_password_description">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.</string>
<string name="master_password_hint">Master password hint (optional)</string>
<string name="master_password_hint_description">A master password hint can help you remember your password if you forget it.</string>
<string name="master_password_length_val_message_x">Master password must be at least %1$s characters long.</string>
<plurals name="master_password_length_val_message_x">
<item quantity="one">Master password must be at least %d character long.</item>
<item quantity="other">Master password must be at least %d characters long.</item>
</plurals>
<string name="min_numbers">Minimum numbers</string>
<string name="min_special">Minimum special</string>
<string name="never">Never</string>
@ -309,7 +312,10 @@ Scanning will happen automatically.</string>
<string name="personal_details">Personal Details</string>
<string name="contact_info">Contact Info</string>
<string name="check_password_for_data_breaches">Check password for data breaches</string>
<string name="password_exposed">This password has been exposed %1$s time(s) in data breaches. You should change it.</string>
<plurals name="password_exposed">
<item quantity="one">This password has been exposed %d time in data breaches. You should change it.</item>
<item quantity="other">This password has been exposed %d times in data breaches. You should change it.</item>
</plurals>
<string name="password_safe">This password was not found in any known data breaches. It should be safe to use.</string>
<string name="identity_name">Identity name</string>
<string name="value">Value</string>
@ -749,7 +755,10 @@ Do you want to switch to this account?</string>
<string name="bitwarden_cannot_reset_a_lost_or_forgotten_master_password">Bitwarden cannot reset a lost or forgotten master password.</string>
<string name="choose_your_master_password">Choose your master password</string>
<string name="choose_a_unique_and_strong_password_to_keep_your_information_safe">Choose a unique and strong password to keep your information safe.</string>
<string name="minimum_characters">%1$s characters</string>
<plurals name="minimum_characters">
<item quantity="one">%d character</item>
<item quantity="other">%d characters</item>
</plurals>
<string name="expired_link">Expired link</string>
<string name="please_restart_registration_or_try_logging_in">Please restart registration or try logging in. You may already have an account.</string>
<string name="restart_registration">Restart registration</string>