export literal strings to resources

This commit is contained in:
ShenLong 2026-06-17 16:53:08 +07:00
parent e6d05e5d6d
commit dfd8414be6
25 changed files with 255 additions and 95 deletions

View File

@ -13,7 +13,7 @@ abstract class BaseRepository {
return if (response.isSuccessful) { return if (response.isSuccessful) {
response.body()?.let { response.body()?.let {
ApiResult.Success(it) ApiResult.Success(it)
} ?: ApiResult.Error(-999, "Empty response body") } ?: ApiResult.Error(ApiResult.Error.CODE_UNKNOWN, "Empty response body")
} else { } else {
response.errorBody().toApiError() response.errorBody().toApiError()
} }
@ -28,7 +28,7 @@ abstract class BaseRepository {
return if (response.isSuccessful) { return if (response.isSuccessful) {
response.body()?.let { response.body()?.let {
ApiResult.Success(transform(it)) ApiResult.Success(transform(it))
} ?: ApiResult.Error(-999, "Empty response body") } ?: ApiResult.Error(ApiResult.Error.CODE_UNKNOWN, "Empty response body")
} else { } else {
response.errorBody().toApiError() response.errorBody().toApiError()
} }

View File

@ -7,12 +7,12 @@ import okhttp3.ResponseBody
fun ResponseBody?.toApiError(): ApiResult.Error { fun ResponseBody?.toApiError(): ApiResult.Error {
if (this == null) { if (this == null) {
return ApiResult.Error(-999, "Unknown error") return ApiResult.Error(ApiResult.Error.CODE_UNKNOWN, "Unknown error")
} }
return try { return try {
val error = json.decodeFromString<ErrorResponse>(string()) val error = json.decodeFromString<ErrorResponse>(string())
ApiResult.Error(error.error, error.message) ApiResult.Error(error.error, error.message)
} catch (_: Exception) { } catch (_: Exception) {
ApiResult.Error(-999, "Unknown error") ApiResult.Error(ApiResult.Error.CODE_UNKNOWN, "Unknown error")
} }
} }

View File

@ -2,5 +2,10 @@ package com.longnh15.heimdall.domain.model
sealed class ApiResult<out T> { sealed class ApiResult<out T> {
data class Success<T>(val data: T): ApiResult<T>() data class Success<T>(val data: T): ApiResult<T>()
data class Error(val code: Int, val message: String): ApiResult<Nothing>() data class Error(val code: Int, val message: String): ApiResult<Nothing>() {
companion object {
/** Sentinel code for client-side failures (no/garbled response). [message] is an internal default, not shown to users. */
const val CODE_UNKNOWN = -999
}
}
} }

View File

@ -38,6 +38,7 @@ import androidx.compose.ui.tooling.preview.AndroidUiModes.UI_MODE_NIGHT_YES
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.longnh15.heimdall.ui.common.UiText
import com.longnh15.heimdall.ui.theme.HeimdallTheme import com.longnh15.heimdall.ui.theme.HeimdallTheme
@Composable @Composable
@ -61,7 +62,7 @@ fun AuthFormTextField(
onValueChange: (String) -> Unit, onValueChange: (String) -> Unit,
label: String, label: String,
placeholder: String, placeholder: String,
error: String? = null, error: UiText? = null,
isHideContent: Boolean = false, isHideContent: Boolean = false,
isLastField: Boolean = false, isLastField: Boolean = false,
autoCorrectEnabled: Boolean = true, autoCorrectEnabled: Boolean = true,
@ -121,7 +122,7 @@ fun AuthFormTextField(
} }
if (error != null) { if (error != null) {
Text( Text(
text = error, text = error.asString(),
modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp), modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp),
color = MaterialTheme.colorScheme.error, color = MaterialTheme.colorScheme.error,
fontSize = 12.sp fontSize = 12.sp

View File

@ -1,6 +1,8 @@
package com.longnh15.heimdall.ui.auth.login package com.longnh15.heimdall.ui.auth.login
import com.longnh15.heimdall.ui.common.UiText
sealed class LoginEvent { sealed class LoginEvent {
data object LoginSuccess: LoginEvent() data object LoginSuccess: LoginEvent()
data class LoginError(val message: String): LoginEvent() data class LoginError(val message: UiText): LoginEvent()
} }

View File

@ -38,12 +38,15 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.longnh15.heimdall.R
import com.longnh15.heimdall.ui.theme.HeimdallTheme import com.longnh15.heimdall.ui.theme.HeimdallTheme
@Composable @Composable
@ -55,12 +58,13 @@ fun LoginScreen(
) { ) {
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
val state = viewModel.state val state = viewModel.state
val context = LocalContext.current
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.uiEvent.collect { event -> viewModel.uiEvent.collect { event ->
when (event) { when (event) {
is LoginEvent.LoginSuccess -> navigateToVault() is LoginEvent.LoginSuccess -> navigateToVault()
is LoginEvent.LoginError -> snackbarHostState.showSnackbar(event.message) is LoginEvent.LoginError -> snackbarHostState.showSnackbar(event.message.asString(context))
} }
} }
} }
@ -87,7 +91,7 @@ fun LoginScreen(
Spacer(Modifier.height(24.dp)) Spacer(Modifier.height(24.dp))
Text( Text(
text = "Welcome back", text = stringResource(R.string.login_title),
style = MaterialTheme.typography.headlineLarge, style = MaterialTheme.typography.headlineLarge,
color = MaterialTheme.colorScheme.onBackground color = MaterialTheme.colorScheme.onBackground
) )
@ -95,7 +99,7 @@ fun LoginScreen(
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
Text( Text(
text = "Access your secure vault credentials", text = stringResource(R.string.login_subtitle),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center textAlign = TextAlign.Center
@ -107,8 +111,8 @@ fun LoginScreen(
AuthFormTextField( AuthFormTextField(
value = state.email, value = state.email,
onValueChange = viewModel::onEmailChanged, onValueChange = viewModel::onEmailChanged,
label = "Email", label = stringResource(R.string.field_email_label),
placeholder = "you@email.com", placeholder = stringResource(R.string.login_email_placeholder),
error = state.emailError, error = state.emailError,
keyboardType = KeyboardType.Email keyboardType = KeyboardType.Email
) )
@ -116,8 +120,8 @@ fun LoginScreen(
AuthFormTextField( AuthFormTextField(
value = state.password, value = state.password,
onValueChange = viewModel::onPasswordChanged, onValueChange = viewModel::onPasswordChanged,
label = "Password", label = stringResource(R.string.field_password_label),
placeholder = "••••••••", placeholder = stringResource(R.string.field_password_placeholder),
error = state.passwordError, error = state.passwordError,
isHideContent = !state.isShowPassword, isHideContent = !state.isShowPassword,
autoCorrectEnabled = false, autoCorrectEnabled = false,
@ -143,7 +147,7 @@ fun LoginScreen(
.align(Alignment.End) .align(Alignment.End)
.widthIn(max = 480.dp) .widthIn(max = 480.dp)
) { ) {
Text("Forgot password?") Text(stringResource(R.string.login_forgot_password))
} }
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
@ -161,7 +165,7 @@ fun LoginScreen(
) )
) { ) {
Text( Text(
text = "Login", text = stringResource(R.string.login_button),
style = MaterialTheme.typography.titleMedium style = MaterialTheme.typography.titleMedium
) )
} }
@ -173,13 +177,13 @@ fun LoginScreen(
horizontalArrangement = Arrangement.Center horizontalArrangement = Arrangement.Center
) { ) {
Text( Text(
text = "Don't have an account?", text = stringResource(R.string.login_no_account),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )
TextButton(onClick = navigateToRegister) { TextButton(onClick = navigateToRegister) {
Text( Text(
text = "Create one", text = stringResource(R.string.login_create_one),
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold
) )
} }

View File

@ -1,11 +1,13 @@
package com.longnh15.heimdall.ui.auth.login package com.longnh15.heimdall.ui.auth.login
import com.longnh15.heimdall.ui.common.UiText
data class LoginState( data class LoginState(
val email: String = "", val email: String = "",
val password: String = "", val password: String = "",
val emailError: String? = null, val emailError: UiText? = null,
val passwordError: String? = null, val passwordError: UiText? = null,
val isShowPassword: Boolean = false, val isShowPassword: Boolean = false,
) )

View File

@ -5,9 +5,12 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.longnh15.heimdall.R
import com.longnh15.heimdall.common.ValidationHelper import com.longnh15.heimdall.common.ValidationHelper
import com.longnh15.heimdall.domain.model.ApiResult import com.longnh15.heimdall.domain.model.ApiResult
import com.longnh15.heimdall.domain.repository.AuthRepository import com.longnh15.heimdall.domain.repository.AuthRepository
import com.longnh15.heimdall.ui.common.UiText
import com.longnh15.heimdall.ui.common.toUiText
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -38,18 +41,18 @@ class LoginViewModel(
viewModelScope.launch { viewModelScope.launch {
when (val result = authRepository.login(state.email, state.password)) { when (val result = authRepository.login(state.email, state.password)) {
is ApiResult.Success -> _uiEvent.send(LoginEvent.LoginSuccess) is ApiResult.Success -> _uiEvent.send(LoginEvent.LoginSuccess)
is ApiResult.Error -> _uiEvent.send(LoginEvent.LoginError(result.message)) is ApiResult.Error -> _uiEvent.send(LoginEvent.LoginError(result.toUiText()))
} }
} }
} }
private fun validate(): Boolean { private fun validate(): Boolean {
val emailError = when { val emailError = when {
state.email.isBlank() -> "Email is required" state.email.isBlank() -> UiText.Res(R.string.error_email_required)
!ValidationHelper.isEmail(state.email) -> "Enter a valid email address" !ValidationHelper.isEmail(state.email) -> UiText.Res(R.string.error_email_invalid)
else -> null else -> null
} }
val passwordError = if (state.password.isBlank()) "Password is required" else null val passwordError = if (state.password.isBlank()) UiText.Res(R.string.error_password_required) else null
state = state.copy(emailError = emailError, passwordError = passwordError) state = state.copy(emailError = emailError, passwordError = passwordError)
return emailError == null && passwordError == null return emailError == null && passwordError == null

View File

@ -1,6 +1,8 @@
package com.longnh15.heimdall.ui.auth.register package com.longnh15.heimdall.ui.auth.register
import com.longnh15.heimdall.ui.common.UiText
sealed class RegisterEvent { sealed class RegisterEvent {
data object RegisterSuccess: RegisterEvent() data object RegisterSuccess: RegisterEvent()
data class RegisterError(val message: String): RegisterEvent() data class RegisterError(val message: UiText): RegisterEvent()
} }

View File

@ -27,19 +27,25 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.longnh15.heimdall.R
import com.longnh15.heimdall.ui.auth.login.AuthDivider import com.longnh15.heimdall.ui.auth.login.AuthDivider
import com.longnh15.heimdall.ui.auth.login.AuthForm import com.longnh15.heimdall.ui.auth.login.AuthForm
import com.longnh15.heimdall.ui.auth.login.AuthFormTextField import com.longnh15.heimdall.ui.auth.login.AuthFormTextField
@ -51,7 +57,18 @@ fun RegisterScreen(
viewModel: RegisterViewModel = viewModel(), viewModel: RegisterViewModel = viewModel(),
navigateToLogin: () -> Unit = {}, navigateToLogin: () -> Unit = {},
) { ) {
val snackbarHostState = remember { SnackbarHostState() }
val state = viewModel.state val state = viewModel.state
val context = LocalContext.current
LaunchedEffect(Unit) {
viewModel.uiEvent.collect { event ->
when (event) {
is RegisterEvent.RegisterSuccess -> navigateToLogin()
is RegisterEvent.RegisterError -> snackbarHostState.showSnackbar(event.message.asString(context))
}
}
}
HeimdallTheme { HeimdallTheme {
Scaffold( Scaffold(
@ -72,7 +89,7 @@ fun RegisterScreen(
Spacer(Modifier.height(24.dp)) Spacer(Modifier.height(24.dp))
Text( Text(
text = "Create account", text = stringResource(R.string.register_title),
style = MaterialTheme.typography.headlineLarge, style = MaterialTheme.typography.headlineLarge,
color = MaterialTheme.colorScheme.onBackground color = MaterialTheme.colorScheme.onBackground
) )
@ -80,7 +97,7 @@ fun RegisterScreen(
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
Text( Text(
text = "Secure your digital life with enterprise-grade encryption.", text = stringResource(R.string.register_subtitle),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center textAlign = TextAlign.Center
@ -92,24 +109,26 @@ fun RegisterScreen(
AuthFormTextField( AuthFormTextField(
value = state.name, value = state.name,
onValueChange = viewModel::onNameChanged, onValueChange = viewModel::onNameChanged,
label = "Full name", label = stringResource(R.string.register_name_label),
placeholder = "John Doe", placeholder = stringResource(R.string.register_name_placeholder),
keyboardType = KeyboardType.Text keyboardType = KeyboardType.Text
) )
AuthDivider() AuthDivider()
AuthFormTextField( AuthFormTextField(
value = state.email, value = state.email,
onValueChange = viewModel::onEmailChanged, onValueChange = viewModel::onEmailChanged,
label = "Email", label = stringResource(R.string.field_email_label),
placeholder = "name@vault.com", placeholder = stringResource(R.string.register_email_placeholder),
error = state.emailError,
keyboardType = KeyboardType.Email, keyboardType = KeyboardType.Email,
) )
AuthDivider() AuthDivider()
AuthFormTextField( AuthFormTextField(
value = state.password, value = state.password,
onValueChange = viewModel::onPasswordChanged, onValueChange = viewModel::onPasswordChanged,
label = "Password", label = stringResource(R.string.field_password_label),
placeholder = "••••••••", placeholder = stringResource(R.string.field_password_placeholder),
error = state.passwordError,
isHideContent = !state.isShowPassword, isHideContent = !state.isShowPassword,
autoCorrectEnabled = false, autoCorrectEnabled = false,
isLastField = true, isLastField = true,
@ -143,7 +162,7 @@ fun RegisterScreen(
) )
) { ) {
Text( Text(
text = "Create account", text = stringResource(R.string.register_button),
style = MaterialTheme.typography.titleMedium style = MaterialTheme.typography.titleMedium
) )
} }
@ -151,7 +170,7 @@ fun RegisterScreen(
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
Text( Text(
text = "By creating an account you agree to our Terms of Service and Privacy Policy.", text = stringResource(R.string.register_terms),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
@ -165,13 +184,13 @@ fun RegisterScreen(
horizontalArrangement = Arrangement.Center horizontalArrangement = Arrangement.Center
) { ) {
Text( Text(
text = "Already have an account?", text = stringResource(R.string.register_have_account),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )
TextButton(onClick = navigateToLogin) { TextButton(onClick = navigateToLogin) {
Text( Text(
text = "Sign in", text = stringResource(R.string.register_sign_in),
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold
) )
} }

View File

@ -1,12 +1,14 @@
package com.longnh15.heimdall.ui.auth.register package com.longnh15.heimdall.ui.auth.register
import com.longnh15.heimdall.ui.common.UiText
data class RegisterState( data class RegisterState(
val name: String = "", val name: String = "",
val email: String = "", val email: String = "",
val password: String = "", val password: String = "",
val emailError: String? = null, val emailError: UiText? = null,
val passwordError: String? = null, val passwordError: UiText? = null,
val isShowPassword: Boolean = false val isShowPassword: Boolean = false
) )

View File

@ -5,9 +5,12 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.longnh15.heimdall.R
import com.longnh15.heimdall.common.ValidationHelper import com.longnh15.heimdall.common.ValidationHelper
import com.longnh15.heimdall.domain.model.ApiResult import com.longnh15.heimdall.domain.model.ApiResult
import com.longnh15.heimdall.domain.repository.AuthRepository import com.longnh15.heimdall.domain.repository.AuthRepository
import com.longnh15.heimdall.ui.common.UiText
import com.longnh15.heimdall.ui.common.toUiText
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -42,20 +45,21 @@ class RegisterViewModel(
viewModelScope.launch { viewModelScope.launch {
when (val result = authRepository.register(state.email, state.password, state.name)) { when (val result = authRepository.register(state.email, state.password, state.name)) {
is ApiResult.Success -> _uiEvent.send(RegisterEvent.RegisterSuccess) is ApiResult.Success -> _uiEvent.send(RegisterEvent.RegisterSuccess)
is ApiResult.Error -> _uiEvent.send(RegisterEvent.RegisterError(result.message)) is ApiResult.Error -> _uiEvent.send(RegisterEvent.RegisterError(result.toUiText()))
} }
} }
} }
private fun validate(): Boolean { private fun validate(): Boolean {
val emailError = when { val emailError = when {
state.email.isBlank() -> "Email is required" state.email.isBlank() -> UiText.Res(R.string.error_email_required)
!ValidationHelper.isEmail(state.email) -> "Enter a valid email address" !ValidationHelper.isEmail(state.email) -> UiText.Res(R.string.error_email_invalid)
else -> null else -> null
} }
val passwordError = when { val passwordError = when {
state.password.isBlank() -> "Password is required" state.password.isBlank() -> UiText.Res(R.string.error_password_required)
state.password.length < 12 -> "Password must be at least 12 characters" state.password.length < MIN_PASSWORD_LENGTH ->
UiText.Res(R.string.error_password_too_short, MIN_PASSWORD_LENGTH)
else -> null else -> null
} }
@ -63,4 +67,7 @@ class RegisterViewModel(
return emailError == null && passwordError == null return emailError == null && passwordError == null
} }
private companion object {
const val MIN_PASSWORD_LENGTH = 12
}
} }

View File

@ -7,6 +7,8 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.longnh15.heimdall.R
@Composable @Composable
fun AutofillNotEnabledDialog( fun AutofillNotEnabledDialog(
@ -18,15 +20,15 @@ fun AutofillNotEnabledDialog(
icon = { icon = {
Icon(Icons.Outlined.Warning, contentDescription = null) Icon(Icons.Outlined.Warning, contentDescription = null)
}, },
title = { Text("Autofill not enabled") }, title = { Text(stringResource(R.string.autofill_dialog_title)) },
text = { text = {
Text("Heimdall wasn't selected as your autofill provider. Open settings and tap Heimdall to enable it.") Text(stringResource(R.string.autofill_dialog_body))
}, },
confirmButton = { confirmButton = {
TextButton(onClick = onRetry) { Text("Open settings") } TextButton(onClick = onRetry) { Text(stringResource(R.string.open_settings)) }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onDismiss) { Text("Skip for now") } TextButton(onClick = onDismiss) { Text(stringResource(R.string.autofill_dialog_skip)) }
} }
) )
} }

View File

@ -18,6 +18,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.longnh15.heimdall.R import com.longnh15.heimdall.R
@ -33,7 +34,7 @@ fun OnboardingFinalPage(
verticalArrangement = Arrangement.spacedBy(32.dp) verticalArrangement = Arrangement.spacedBy(32.dp)
) { ) {
OnboardingBadge { OnboardingBadge {
Text("You're all set") Text(stringResource(R.string.onboarding_final_badge))
} }
Box( Box(
modifier = Modifier modifier = Modifier
@ -43,20 +44,20 @@ fun OnboardingFinalPage(
) { ) {
Image( Image(
painterResource(R.drawable.tick_icon), painterResource(R.drawable.tick_icon),
"Page icon", stringResource(R.string.onboarding_page_icon_desc),
) )
} }
Text("Heimdall is watching", style = MaterialTheme.typography.headlineMedium) Text(stringResource(R.string.onboarding_final_title), style = MaterialTheme.typography.headlineMedium)
Text( Text(
text = "Your vault is ready. Start adding passwords or import from your browser to get going.", text = stringResource(R.string.onboarding_final_body),
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
Button(onClick = navigateToVault) { Button(onClick = navigateToVault) {
Text("Enter my vault ↗") Text(stringResource(R.string.onboarding_final_enter_vault))
} }
} }
} }

View File

@ -20,6 +20,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -34,11 +35,11 @@ fun OnboardingIntroducePage(modifier: Modifier = Modifier) {
verticalArrangement = Arrangement.spacedBy(32.dp) verticalArrangement = Arrangement.spacedBy(32.dp)
) { ) {
OnboardingBadge { OnboardingBadge {
Text("What you get") Text(stringResource(R.string.onboarding_introduce_badge))
} }
Text("Everything in one vault", style = MaterialTheme.typography.headlineMedium) Text(stringResource(R.string.onboarding_introduce_title), style = MaterialTheme.typography.headlineMedium)
Text( Text(
text = "Heimdall keeps your passwords safe and fills them in automatically", text = stringResource(R.string.onboarding_introduce_body),
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
Column( Column(
@ -47,18 +48,18 @@ fun OnboardingIntroducePage(modifier: Modifier = Modifier) {
) { ) {
FeatureItem( FeatureItem(
iconDrawableRes = R.drawable.lock_icon, iconDrawableRes = R.drawable.lock_icon,
text = "End-to-end encrypted", text = stringResource(R.string.onboarding_feature_encrypted_title),
sub = "Only you can see your data" sub = stringResource(R.string.onboarding_feature_encrypted_sub)
) )
FeatureItem( FeatureItem(
iconDrawableRes = R.drawable.crosshair_icon, iconDrawableRes = R.drawable.crosshair_icon,
text = "Autofill anywhere", text = stringResource(R.string.onboarding_feature_autofill_title),
sub = "Works across all your apps" sub = stringResource(R.string.onboarding_feature_autofill_sub)
) )
FeatureItem( FeatureItem(
iconDrawableRes = R.drawable.person_icon, iconDrawableRes = R.drawable.person_icon,
text = "Biometric unlock", text = stringResource(R.string.onboarding_feature_biometric_title),
sub = "Fingerprint or face to open" sub = stringResource(R.string.onboarding_feature_biometric_sub)
) )
} }
} }
@ -89,7 +90,7 @@ fun FeatureItem(
.background(Color(0xFFEEEDFE), RoundedCornerShape(8.dp)), .background(Color(0xFFEEEDFE), RoundedCornerShape(8.dp)),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Image(painterResource(iconDrawableRes), "Icon") Image(painterResource(iconDrawableRes), stringResource(R.string.onboarding_feature_icon_desc))
} }
Column { Column {
Text(text = text, fontSize = 13.sp, fontWeight = FontWeight(500), color = MaterialTheme.colorScheme.onPrimary) Text(text = text, fontSize = 13.sp, fontWeight = FontWeight(500), color = MaterialTheme.colorScheme.onPrimary)

View File

@ -19,6 +19,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.longnh15.heimdall.R import com.longnh15.heimdall.R
@ -34,7 +35,7 @@ fun OnboardingRequestEnablePage(
verticalArrangement = Arrangement.spacedBy(32.dp) verticalArrangement = Arrangement.spacedBy(32.dp)
) { ) {
OnboardingBadge { OnboardingBadge {
Text("Setup · 1 of 2") Text(stringResource(R.string.onboarding_enable_badge))
} }
Box( Box(
modifier = Modifier modifier = Modifier
@ -44,20 +45,20 @@ fun OnboardingRequestEnablePage(
) { ) {
Image( Image(
painterResource(R.drawable.page_icon), painterResource(R.drawable.page_icon),
"Page icon", stringResource(R.string.onboarding_page_icon_desc),
) )
} }
Text("Enable autofill", style = MaterialTheme.typography.headlineMedium) Text(stringResource(R.string.onboarding_enable_title), style = MaterialTheme.typography.headlineMedium)
Text( Text(
text = "Let Heimdall fill your passwords automatically. You'll be taken to Android settings — tap Heimdall and you're done.", text = stringResource(R.string.onboarding_enable_body),
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
Button(onClick = onOpenAutofillSettings) { Button(onClick = onOpenAutofillSettings) {
Text("Open settings") Text(stringResource(R.string.open_settings))
} }
} }
} }

View File

@ -16,6 +16,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -36,12 +37,12 @@ fun OnboardingWelcomePage(modifier: Modifier = Modifier) {
) { ) {
Image( Image(
painterResource(R.drawable.shield_icon), painterResource(R.drawable.shield_icon),
"Heimdall Logo", stringResource(R.string.onboarding_welcome_logo_desc),
) )
} }
Text("Welcome to Heimdall", style = MaterialTheme.typography.headlineMedium) Text(stringResource(R.string.onboarding_welcome_title), style = MaterialTheme.typography.headlineMedium)
Text( Text(
text = "Your all-seeing guardian for passwords. Secure, private, and always watching over your credentials", text = stringResource(R.string.onboarding_welcome_body),
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
} }

View File

@ -19,7 +19,9 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.longnh15.heimdall.R
@Composable @Composable
fun LockOverlay( fun LockOverlay(
@ -43,12 +45,12 @@ fun LockOverlay(
modifier = Modifier.size(48.dp) modifier = Modifier.size(48.dp)
) )
Text( Text(
text = "Vault is locked", text = stringResource(R.string.vault_locked_title),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onBackground color = MaterialTheme.colorScheme.onBackground
) )
Text( Text(
text = "Authenticate to access your credentials", text = stringResource(R.string.vault_locked_subtitle),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )
@ -60,7 +62,7 @@ fun LockOverlay(
modifier = Modifier.size(18.dp) modifier = Modifier.size(18.dp)
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Text("Unlock with biometrics") Text(stringResource(R.string.vault_unlock_biometrics))
} }
} }
} }

View File

@ -17,14 +17,16 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.longnh15.heimdall.R
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun VaultHeader() { fun VaultHeader() {
TopAppBar( TopAppBar(
title = { title = {
Text("My vault") Text(stringResource(R.string.vault_title))
}, },
colors = TopAppBarDefaults.topAppBarColors().copy( colors = TopAppBarDefaults.topAppBarColors().copy(
containerColor = MaterialTheme.colorScheme.background containerColor = MaterialTheme.colorScheme.background

View File

@ -13,8 +13,10 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.longnh15.heimdall.R
@Composable @Composable
fun VaultScreen( fun VaultScreen(
@ -33,12 +35,14 @@ fun VaultScreen(
} }
} }
) )
val requestBiometric = remember { { val unlockTitle = stringResource(R.string.vault_unlock_title)
val unlockSubtitle = stringResource(R.string.vault_unlock_subtitle)
val requestBiometric = remember(unlockTitle, unlockSubtitle) { {
val request = biometricRequest( val request = biometricRequest(
title = "Unlock Heimdall", title = unlockTitle,
AuthenticationRequest.Biometric.Fallback.DeviceCredential, AuthenticationRequest.Biometric.Fallback.DeviceCredential,
) { ) {
setSubtitle("Confirm your identity to access your vault") setSubtitle(unlockSubtitle)
} }
launcher.launch(request) launcher.launch(request)
} } } }

View File

@ -1,6 +1,8 @@
package com.longnh15.heimdall.ui.vault.edit package com.longnh15.heimdall.ui.vault.edit
import com.longnh15.heimdall.ui.common.UiText
sealed class AddEditEvent { sealed class AddEditEvent {
data object SaveSuccess: AddEditEvent() data object SaveSuccess: AddEditEvent()
data class SaveError(val message: String): AddEditEvent() data class SaveError(val message: UiText): AddEditEvent()
} }

View File

@ -20,9 +20,11 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.longnh15.heimdall.R
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -34,7 +36,7 @@ fun AddEditHeader(
) { ) {
TopAppBar( TopAppBar(
title = { title = {
Text("New item") Text(stringResource(R.string.add_edit_title))
}, },
modifier = Modifier.padding(end = 16.dp), modifier = Modifier.padding(end = 16.dp),
colors = TopAppBarDefaults.topAppBarColors().copy( colors = TopAppBarDefaults.topAppBarColors().copy(
@ -64,7 +66,7 @@ fun AddEditHeader(
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
} }
Text( Text(
text = if (isSaving) "Saving…" else "Save", text = if (isSaving) stringResource(R.string.add_edit_saving) else stringResource(R.string.add_edit_save),
fontSize = 13.sp, fontSize = 13.sp,
fontWeight = FontWeight(500), fontWeight = FontWeight(500),
color = MaterialTheme.colorScheme.onTertiaryContainer color = MaterialTheme.colorScheme.onTertiaryContainer

View File

@ -24,10 +24,13 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.AndroidUiModes.UI_MODE_NIGHT_YES import androidx.compose.ui.tooling.preview.AndroidUiModes.UI_MODE_NIGHT_YES
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.longnh15.heimdall.R
import com.longnh15.heimdall.ui.theme.HeimdallTheme import com.longnh15.heimdall.ui.theme.HeimdallTheme
@Composable @Composable
@ -37,12 +40,13 @@ fun AddEditVaultItemScreen(
popScreen: () -> Unit, popScreen: () -> Unit,
) { ) {
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
val context = LocalContext.current
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.uiEvent.collect { event -> viewModel.uiEvent.collect { event ->
when (event) { when (event) {
AddEditEvent.SaveSuccess -> popScreen() AddEditEvent.SaveSuccess -> popScreen()
is AddEditEvent.SaveError -> snackbarHostState.showSnackbar(event.message) is AddEditEvent.SaveError -> snackbarHostState.showSnackbar(event.message.asString(context))
} }
} }
} }
@ -66,20 +70,20 @@ fun AddEditVaultItemScreen(
modifier.padding(paddingValues).padding(horizontal = 16.dp), modifier.padding(paddingValues).padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(10.dp) verticalArrangement = Arrangement.spacedBy(10.dp)
) { ) {
AddEditFormHeader("details") AddEditFormHeader(stringResource(R.string.add_edit_section_details))
AddEditForm { AddEditForm {
AddEditFormTextField( AddEditFormTextField(
value = viewModel.state.name, value = viewModel.state.name,
onValueChange = viewModel::onNameChanged, onValueChange = viewModel::onNameChanged,
label = "Name", label = stringResource(R.string.add_edit_name_label),
placeholder = "e.g. Google" placeholder = stringResource(R.string.add_edit_name_placeholder)
) )
AddEditDivider() AddEditDivider()
AddEditFormTextField( AddEditFormTextField(
value = viewModel.state.url, value = viewModel.state.url,
onValueChange = viewModel::onUrlChanged, onValueChange = viewModel::onUrlChanged,
label = "URL", label = stringResource(R.string.add_edit_url_label),
placeholder = "google.com", placeholder = stringResource(R.string.add_edit_url_placeholder),
actions = { actions = {
IconButton( IconButton(
onClick = {}, onClick = {},
@ -90,20 +94,20 @@ fun AddEditVaultItemScreen(
} }
) )
} }
AddEditFormHeader("credentials") AddEditFormHeader(stringResource(R.string.add_edit_section_credentials))
AddEditForm { AddEditForm {
AddEditFormTextField( AddEditFormTextField(
viewModel.state.username, viewModel.state.username,
onValueChange = viewModel::onUsernameChanged, onValueChange = viewModel::onUsernameChanged,
label = "Username", label = stringResource(R.string.add_edit_username_label),
placeholder = "you@email.com" placeholder = stringResource(R.string.add_edit_username_placeholder)
) )
AddEditDivider() AddEditDivider()
AddEditFormTextField( AddEditFormTextField(
value = viewModel.state.password, value = viewModel.state.password,
onValueChange = viewModel::onPasswordChanged, onValueChange = viewModel::onPasswordChanged,
label = "Password", label = stringResource(R.string.field_password_label),
placeholder = "••••••••", placeholder = stringResource(R.string.field_password_placeholder),
isHideContent = viewModel.state.isShowPassword.not(), isHideContent = viewModel.state.isShowPassword.not(),
isLastField = true, isLastField = true,
autoCorrectEnabled = false, autoCorrectEnabled = false,

View File

@ -5,7 +5,9 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.longnh15.heimdall.R
import com.longnh15.heimdall.domain.repository.CredentialRepository import com.longnh15.heimdall.domain.repository.CredentialRepository
import com.longnh15.heimdall.ui.common.UiText
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -52,7 +54,8 @@ class AddEditVaultViewModel(
_uiEvent.send(AddEditEvent.SaveSuccess) _uiEvent.send(AddEditEvent.SaveSuccess)
} catch (e: Exception) { } catch (e: Exception) {
state = state.copy(isSaving = false) state = state.copy(isSaving = false)
_uiEvent.send(AddEditEvent.SaveError(e.message ?: "Something went wrong.")) val message = e.message?.let { UiText.Dynamic(it) } ?: UiText.Res(R.string.error_generic)
_uiEvent.send(AddEditEvent.SaveError(message))
} }
} }
} }

View File

@ -1,3 +1,91 @@
<resources> <resources>
<string name="app_name">Heimdall</string> <string name="app_name">Heimdall</string>
</resources>
<!-- Errors / validation -->
<string name="error_email_required">Email is required</string>
<string name="error_email_invalid">Enter a valid email address</string>
<string name="error_password_required">Password is required</string>
<string name="error_password_too_short">Password must be at least %1$d characters</string>
<string name="error_generic">Something went wrong.</string>
<!-- Shared form fields -->
<string name="field_email_label">Email</string>
<string name="field_password_label">Password</string>
<string name="field_password_placeholder">••••••••</string>
<string name="open_settings">Open settings</string>
<!-- Login -->
<string name="login_title">Welcome back</string>
<string name="login_subtitle">Access your secure vault credentials</string>
<string name="login_email_placeholder">you@email.com</string>
<string name="login_forgot_password">Forgot password?</string>
<string name="login_button">Login</string>
<string name="login_no_account">Don\'t have an account?</string>
<string name="login_create_one">Create one</string>
<!-- Register -->
<string name="register_title">Create account</string>
<string name="register_subtitle">Secure your digital life with enterprise-grade encryption.</string>
<string name="register_name_label">Full name</string>
<string name="register_name_placeholder">John Doe</string>
<string name="register_email_placeholder">name@vault.com</string>
<string name="register_button">Create account</string>
<string name="register_terms">By creating an account you agree to our Terms of Service and Privacy Policy.</string>
<string name="register_have_account">Already have an account?</string>
<string name="register_sign_in">Sign in</string>
<!-- Onboarding: welcome -->
<string name="onboarding_welcome_logo_desc">Heimdall Logo</string>
<string name="onboarding_welcome_title">Welcome to Heimdall</string>
<string name="onboarding_welcome_body">Your all-seeing guardian for passwords. Secure, private, and always watching over your credentials</string>
<!-- Onboarding: introduce -->
<string name="onboarding_introduce_badge">What you get</string>
<string name="onboarding_introduce_title">Everything in one vault</string>
<string name="onboarding_introduce_body">Heimdall keeps your passwords safe and fills them in automatically</string>
<string name="onboarding_feature_icon_desc">Icon</string>
<string name="onboarding_feature_encrypted_title">End-to-end encrypted</string>
<string name="onboarding_feature_encrypted_sub">Only you can see your data</string>
<string name="onboarding_feature_autofill_title">Autofill anywhere</string>
<string name="onboarding_feature_autofill_sub">Works across all your apps</string>
<string name="onboarding_feature_biometric_title">Biometric unlock</string>
<string name="onboarding_feature_biometric_sub">Fingerprint or face to open</string>
<!-- Onboarding: request enable -->
<string name="onboarding_page_icon_desc">Page icon</string>
<string name="onboarding_enable_badge">Setup · 1 of 2</string>
<string name="onboarding_enable_title">Enable autofill</string>
<string name="onboarding_enable_body">Let Heimdall fill your passwords automatically. You\'ll be taken to Android settings — tap Heimdall and you\'re done.</string>
<!-- Onboarding: final -->
<string name="onboarding_final_badge">You\'re all set</string>
<string name="onboarding_final_title">Heimdall is watching</string>
<string name="onboarding_final_body">Your vault is ready. Start adding passwords or import from your browser to get going.</string>
<string name="onboarding_final_enter_vault">Enter my vault ↗</string>
<!-- Autofill not enabled dialog -->
<string name="autofill_dialog_title">Autofill not enabled</string>
<string name="autofill_dialog_body">Heimdall wasn\'t selected as your autofill provider. Open settings and tap Heimdall to enable it.</string>
<string name="autofill_dialog_skip">Skip for now</string>
<!-- Vault -->
<string name="vault_title">My vault</string>
<string name="vault_unlock_title">Unlock Heimdall</string>
<string name="vault_unlock_subtitle">Confirm your identity to access your vault</string>
<string name="vault_locked_title">Vault is locked</string>
<string name="vault_locked_subtitle">Authenticate to access your credentials</string>
<string name="vault_unlock_biometrics">Unlock with biometrics</string>
<!-- Add / edit vault item -->
<string name="add_edit_title">New item</string>
<string name="add_edit_saving">Saving…</string>
<string name="add_edit_save">Save</string>
<string name="add_edit_section_details">details</string>
<string name="add_edit_section_credentials">credentials</string>
<string name="add_edit_name_label">Name</string>
<string name="add_edit_name_placeholder">e.g. Google</string>
<string name="add_edit_url_label">URL</string>
<string name="add_edit_url_placeholder">google.com</string>
<string name="add_edit_username_label">Username</string>
<string name="add_edit_username_placeholder">you@email.com</string>
</resources>