From ccc8843cb09a8673f3d126dd61a177231ef470e6 Mon Sep 17 00:00:00 2001 From: Aditya Bhardwaj Date: Wed, 16 Sep 2026 08:05:18 +0530 Subject: [PATCH] Localize onboarding and fix the crash that translating it would cause Onboarding titles/descriptions, Skip/Next and the splash tagline rendered in English in all 16 locales. Root cause: 14 strings were marked translatable="false" in values/strings.xml, which both excluded them from every values-*/ folder and suppressed the MissingTranslation lint check that would have caught it. The Kotlin was already using stringResource() correctly. Translating the headings alone would have crashed the app on first launch: TextHighlighter located the emphasised word with fullText.indexOf("Secure") and passed the result straight to AnnotatedString.addStyle. For any translated heading indexOf returns -1, and addStyle(start = -1, ...) throws. The highlight is now marked up as [[word]] inside each string resource, so translators choose the emphasised word in their own language, and a pure-Kotlin parser strips the markers. Unbalanced or absent markers degrade to unstyled text instead of throwing. Headings auto-size 36-64sp so longer languages do not clip. Also in this release: - Arabic was offered by the in-app picker but missing from locales_config.xml, so selecting it silently did nothing. locales_config, values-*/ and the picker now agree on all 17 locales. - Saving a preview without renaming it matched itself in the duplicate-heading check, reported "heading exists" and discarded the edit. - DetailViewModel trimmed on create but not on edit, so trailing whitespace crept into stored passwords. - The bottom navigation announced the raw enum name ("BANKS") in every locale, and the alpha-0 FAB spacer was reachable by TalkBack. - The details screen's empty state read "No Previews Found". - PasswordGenerator used kotlin.random.Random rather than SecureRandom, and did not guarantee a character from each selected class. - Removed 10 string resources that were unused in code (170 entries across all locales), so they are not translated needlessly. - proguard-rules.pro kept net.sqlcipher.**, but the shipped artifact is net.zetetic:sqlcipher-android. Those rules matched nothing. The library supplies its own consumer rules; verified against mapping.txt that all 59 net.zetetic classes are kept unrenamed without any app-side rule, so the dead rules were removed rather than replaced. MissingTranslation, ExtraTranslation, ImpliedQuantity and the string-format checks are now build-breaking errors, which is what stops this recurring. Verified on an API 36 emulator: onboarding launches without crashing in hi, ar, de, ta, ja and en, with markers stripped and text localized. 16 unit tests pass, lint passes, release build is green and the Room schema is unchanged. Co-Authored-By: Claude Opus 5 (1M context) --- app/build.gradle.kts | 17 ++- app/proguard-rules.pro | 12 +- .../domain/viewModels/DetailViewModel.kt | 6 +- .../domain/viewModels/PreviewViewModel.kt | 7 +- .../screens/detail_screen/DetailPage.kt | 2 +- .../onboarding_screens/OnBoardingPage.kt | 14 +-- .../components/HighlightMarkup.kt | 56 +++++++++ .../components/OnBoardingItem.kt | 5 +- .../components/TextHighlighter.kt | 64 ++++++----- .../screens/preview_screen/PreviewPage.kt | 11 +- .../components/PreviewBottomNavigation.kt | 31 +++-- .../com/bhardwaj/passkey/utils/Categories.kt | 19 +++- .../passkey/utils/PasswordGenerator.kt | 68 ++++++++--- app/src/main/res/values-ar/strings.xml | 30 +++-- app/src/main/res/values-bn/strings.xml | 30 +++-- app/src/main/res/values-de/strings.xml | 30 +++-- app/src/main/res/values-es/strings.xml | 30 +++-- app/src/main/res/values-fr/strings.xml | 30 +++-- app/src/main/res/values-gu/strings.xml | 30 +++-- app/src/main/res/values-hi/strings.xml | 32 ++++-- app/src/main/res/values-it/strings.xml | 30 +++-- app/src/main/res/values-ja/strings.xml | 30 +++-- app/src/main/res/values-ko/strings.xml | 30 +++-- app/src/main/res/values-mr/strings.xml | 30 +++-- app/src/main/res/values-pt/strings.xml | 30 +++-- app/src/main/res/values-ru/strings.xml | 30 +++-- app/src/main/res/values-ta/strings.xml | 30 +++-- app/src/main/res/values-te/strings.xml | 30 +++-- app/src/main/res/values-zh/strings.xml | 30 +++-- app/src/main/res/values/strings.xml | 50 +++++---- app/src/main/res/xml/locales_config.xml | 1 + .../bhardwaj/passkey/HighlightMarkupTest.kt | 106 ++++++++++++++++++ .../bhardwaj/passkey/PasswordGeneratorTest.kt | 102 +++++++++++++++++ gradlew | 0 34 files changed, 770 insertions(+), 283 deletions(-) create mode 100644 app/src/main/java/com/bhardwaj/passkey/presentation/screens/onboarding_screens/components/HighlightMarkup.kt create mode 100644 app/src/test/java/com/bhardwaj/passkey/HighlightMarkupTest.kt create mode 100644 app/src/test/java/com/bhardwaj/passkey/PasswordGeneratorTest.kt mode change 100644 => 100755 gradlew diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 33be3c1..9625743 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -20,8 +20,8 @@ android { applicationId = "com.bhardwaj.passkey" minSdk = 28 targetSdk = 36 - versionCode = 43 - versionName = "5.5.1" + versionCode = 44 + versionName = "5.5.2" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" @@ -70,6 +70,19 @@ android { compose = true buildConfig = true } + lint { + // The onboarding strings shipped in English to all 16 locales for several releases + // because they were marked translatable="false", which also suppressed this check. + // Failing the build is what stops that recurring. + error += setOf( + "MissingTranslation", + "ExtraTranslation", + "ImpliedQuantity", + "StringFormatInvalid", + "StringFormatMatches" + ) + abortOnError = true + } packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 22639f5..3ff03e3 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -20,5 +20,13 @@ # hide the original source file name. #-renamesourcefileattribute SourceFile --keep class net.sqlcipher.** { *; } --keep class net.sqlcipher.database.* { *; } \ No newline at end of file +# SQLCipher. +# The shipped artifact is net.zetetic:sqlcipher-android, whose classes live in +# net.zetetic.database.sqlcipher.**. This module's own AAR supplies consumer ProGuard rules +# (keeping native methods, constructors and mNativeHandle), so no app-side keep rules are +# needed here - verified against app/build/outputs/mapping/release/mapping.txt, where all 61 +# net.zetetic classes are kept unrenamed. +# +# The previous rules in this file referenced the legacy `net.sqlcipher.**` package, which this +# app has not used since migrating to sqlcipher-android. They matched nothing and were removed +# so they cannot be mistaken for load-bearing configuration. diff --git a/app/src/main/java/com/bhardwaj/passkey/domain/viewModels/DetailViewModel.kt b/app/src/main/java/com/bhardwaj/passkey/domain/viewModels/DetailViewModel.kt index 34aa1eb..e95cfad 100644 --- a/app/src/main/java/com/bhardwaj/passkey/domain/viewModels/DetailViewModel.kt +++ b/app/src/main/java/com/bhardwaj/passkey/domain/viewModels/DetailViewModel.kt @@ -161,8 +161,10 @@ class DetailViewModel @Inject constructor( repository.upsertDetails( it.copy( previewId = previewId, - question = detailTitle.value, - answer = detailResponse.value, + // Trimmed to match the create path above. A trailing space in a + // stored password fails silently wherever it is pasted. + question = newDetail.question, + answer = newDetail.answer, ) ) } ?: repository.upsertDetails(newDetail) diff --git a/app/src/main/java/com/bhardwaj/passkey/domain/viewModels/PreviewViewModel.kt b/app/src/main/java/com/bhardwaj/passkey/domain/viewModels/PreviewViewModel.kt index d42b218..8265817 100644 --- a/app/src/main/java/com/bhardwaj/passkey/domain/viewModels/PreviewViewModel.kt +++ b/app/src/main/java/com/bhardwaj/passkey/domain/viewModels/PreviewViewModel.kt @@ -132,7 +132,12 @@ class PreviewViewModel @Inject constructor( newPreview.heading, newPreview.categoryName.toString() ) - if (existingPreview != null) { + // When editing, the lookup finds the very row being edited. Treating that as + // a clash meant saving an edit without renaming it reported "heading exists" + // and silently discarded the edit. + val isClashWithAnotherRow = + existingPreview != null && existingPreview.previewId != preview?.previewId + if (isClashWithAnotherRow) { preview = null savedStateHandle[PREVIEW_HEADING] = "" isSheetOpen = false diff --git a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/detail_screen/DetailPage.kt b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/detail_screen/DetailPage.kt index 46f34fb..9e3197a 100644 --- a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/detail_screen/DetailPage.kt +++ b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/detail_screen/DetailPage.kt @@ -159,7 +159,7 @@ fun DetailScreen( .fillMaxWidth() .padding(16.dp), painter = painterResource(id = R.drawable.icon_empty_list), - contentDescription = "No Previews Found" + contentDescription = stringResource(id = R.string.no_details_found) ) } else { LazyColumn( diff --git a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/onboarding_screens/OnBoardingPage.kt b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/onboarding_screens/OnBoardingPage.kt index 21cb9ba..e5b540b 100644 --- a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/onboarding_screens/OnBoardingPage.kt +++ b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/onboarding_screens/OnBoardingPage.kt @@ -26,8 +26,7 @@ import kotlinx.coroutines.launch data class OnBoardingScreen( val title: String, - val description: String, - val highlightedText: List + val description: String ) @Composable @@ -35,21 +34,20 @@ fun OnBoardingScreen( onNavigate: (UiEvents.Navigate) -> Unit, viewModel: OnBoardingViewModel = hiltViewModel() ) { + // The emphasised word is marked with [[ ]] inside each heading string resource, so each + // translation controls which of its own words is highlighted. val pages = listOf( OnBoardingScreen( title = stringResource(id = R.string.first_heading), - description = stringResource(id = R.string.first_description), - highlightedText = listOf("Secure") + description = stringResource(id = R.string.first_description) ), OnBoardingScreen( title = stringResource(id = R.string.second_heading), - description = stringResource(id = R.string.second_description), - highlightedText = listOf("Passwords") + description = stringResource(id = R.string.second_description) ), OnBoardingScreen( title = stringResource(id = R.string.third_heading), - description = stringResource(id = R.string.third_description), - highlightedText = listOf("Autofill") + description = stringResource(id = R.string.third_description) ) ) diff --git a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/onboarding_screens/components/HighlightMarkup.kt b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/onboarding_screens/components/HighlightMarkup.kt new file mode 100644 index 0000000..1e9b249 --- /dev/null +++ b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/onboarding_screens/components/HighlightMarkup.kt @@ -0,0 +1,56 @@ +package com.bhardwaj.passkey.presentation.screens.onboarding_screens.components + +/** + * A span of [text] that should be rendered with the highlight colour. + */ +data class HighlightSpan(val start: Int, val endExclusive: Int) + +/** + * The result of stripping highlight markers out of a raw string resource. + * + * @param text the display text, with all markers removed + * @param spans the ranges of [text] that were wrapped in markers + */ +data class MarkedUpText(val text: String, val spans: List) + +private const val OPEN = "[[" +private const val CLOSE = "]]" + +/** + * Parses `[[ ]]` highlight markers out of a translatable string. + * + * The marker lives inside the string resource so that translators choose which word is + * emphasised in their own language. Previously the highlighted word was a hardcoded English + * literal matched with `indexOf`, which returned -1 for every translated heading and crashed + * `AnnotatedString.addStyle`. + * + * This parser never throws. Unbalanced, empty or absent markers simply yield fewer spans, so a + * translator dropping a marker degrades to unstyled text rather than a crash. + */ +fun parseHighlightMarkup(raw: String): MarkedUpText { + val out = StringBuilder(raw.length) + val spans = mutableListOf() + var i = 0 + var openAt: Int? = null + + while (i < raw.length) { + when { + openAt == null && raw.startsWith(OPEN, i) -> { + openAt = out.length + i += OPEN.length + } + + openAt != null && raw.startsWith(CLOSE, i) -> { + if (openAt < out.length) spans += HighlightSpan(openAt, out.length) + openAt = null + i += CLOSE.length + } + + else -> { + out.append(raw[i]) + i++ + } + } + } + return MarkedUpText(text = out.toString(), spans = spans) +} diff --git a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/onboarding_screens/components/OnBoardingItem.kt b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/onboarding_screens/components/OnBoardingItem.kt index 4ca4743..7d184ff 100644 --- a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/onboarding_screens/components/OnBoardingItem.kt +++ b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/onboarding_screens/components/OnBoardingItem.kt @@ -33,12 +33,11 @@ fun OnBoardingItem( ) { Image( painter = painterResource(id = R.drawable.icon_logo), - contentDescription = "App Logo" + contentDescription = null ) TextHighlighter( modifier = Modifier.padding(top = 72.dp), - fullText = screen.title, - highlightedText = screen.highlightedText + markedUpText = screen.title ) Text( modifier = Modifier.padding(top = 24.dp), diff --git a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/onboarding_screens/components/TextHighlighter.kt b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/onboarding_screens/components/TextHighlighter.kt index 2034c0c..2e00c41 100644 --- a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/onboarding_screens/components/TextHighlighter.kt +++ b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/onboarding_screens/components/TextHighlighter.kt @@ -1,50 +1,62 @@ package com.bhardwaj.passkey.presentation.screens.onboarding_screens.components +import androidx.compose.foundation.text.TextAutoSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.em import androidx.compose.ui.unit.sp import com.bhardwaj.passkey.presentation.theme.BebasNeue +/** + * Renders an onboarding heading, colouring the words the translator wrapped in `[[ ]]`. + * + * The heading auto-sizes between 36sp and 64sp: the strings are laid out on three lines and + * translations of "Generate\nSecure\nPasswords." are considerably longer in several supported + * languages, which would clip at a fixed 64sp on small screens. + */ @Composable fun TextHighlighter( + markedUpText: String, modifier: Modifier = Modifier, - fullText: String, - highlightedText: List, ) { - val annotatedString = buildAnnotatedString { - append(fullText) - highlightedText.forEach { text -> - val startIndex = fullText.indexOf(text) - val endIndex = startIndex + text.length - addStyle( - style = SpanStyle( - color = MaterialTheme.colorScheme.primary - ), - start = startIndex, - end = endIndex - ) + val highlightColor = MaterialTheme.colorScheme.primary + val parsed = remember(markedUpText) { parseHighlightMarkup(markedUpText) } + + val annotatedString = remember(parsed, highlightColor) { + buildAnnotatedString { + append(parsed.text) + parsed.spans.forEach { span -> + // Defensive: the parser cannot currently emit out-of-bounds spans, but clamping + // here means a future change can never reintroduce the addStyle crash. + val start = span.start.coerceIn(0, parsed.text.length) + val end = span.endExclusive.coerceIn(start, parsed.text.length) + if (end > start) { + addStyle( + style = SpanStyle(color = highlightColor), + start = start, + end = end + ) + } + } } - addStyle( - style = SpanStyle( - fontFamily = BebasNeue, - fontSize = 64.sp, - fontWeight = FontWeight.Normal, - fontStyle = FontStyle.Normal, - ), - start = 0, - end = fullText.length - ) } + Text( modifier = modifier, text = annotatedString, - lineHeight = 70.sp, + autoSize = TextAutoSize.StepBased(minFontSize = 36.sp, maxFontSize = 64.sp), + fontFamily = BebasNeue, + fontWeight = FontWeight.Normal, + fontStyle = FontStyle.Normal, + // Relative to the resolved font size, so it tracks auto-sizing. + lineHeight = 1.1.em, color = MaterialTheme.colorScheme.secondary ) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/preview_screen/PreviewPage.kt b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/preview_screen/PreviewPage.kt index b99c861..5e48793 100644 --- a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/preview_screen/PreviewPage.kt +++ b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/preview_screen/PreviewPage.kt @@ -78,12 +78,9 @@ fun PreviewScreen( viewModel: PreviewViewModel = hiltViewModel() ) { val categoryName by viewModel.categoryName.collectAsState() - val categoryNameMap = mapOf( - Categories.BANKS.name to stringResource(id = R.string.banks), - Categories.APPS.name to stringResource(id = R.string.apps), - Categories.MAILS.name to stringResource(id = R.string.mails), - Categories.OTHERS.name to stringResource(id = R.string.others), - ) + // Localized display names come from Categories.labelRes, which the bottom navigation + // also uses, so the two can no longer drift apart. + val categoryNameMap = Categories.entries.associate { it.name to stringResource(it.labelRes) } val previewHeading by viewModel.previewHeading.collectAsState() val bottomSheetHeading by viewModel.bottomSheetHeading.collectAsState() @@ -212,7 +209,7 @@ fun PreviewScreen( .fillMaxWidth() .padding(16.dp), painter = painterResource(id = R.drawable.icon_empty_list), - contentDescription = "No Previews Found" + contentDescription = stringResource(id = R.string.no_previews_found) ) } else { LazyColumn( diff --git a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/preview_screen/components/PreviewBottomNavigation.kt b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/preview_screen/components/PreviewBottomNavigation.kt index b2c39ca..cc90236 100644 --- a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/preview_screen/components/PreviewBottomNavigation.kt +++ b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/preview_screen/components/PreviewBottomNavigation.kt @@ -24,36 +24,42 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.unit.dp import com.bhardwaj.passkey.utils.Categories data class BottomNavigationItem( - val title: String, + val category: Categories, val icon: ImageVector, val isVisible: Boolean = true -) +) { + /** The persisted enum name, used as the navigation/selection key. */ + val title: String get() = category.name +} val bottomNavigationList = listOf( BottomNavigationItem( - title = Categories.BANKS.name, + category = Categories.BANKS, icon = Icons.Filled.AccountBalance ), BottomNavigationItem( - title = Categories.APPS.name, + category = Categories.APPS, icon = Icons.Filled.Gamepad ), + // Invisible spacer that reserves room for the centre FAB notch. It is not a destination. BottomNavigationItem( - title = Categories.BANKS.name, + category = Categories.BANKS, icon = Icons.Filled.AccountBalance, isVisible = false, ), BottomNavigationItem( - title = Categories.MAILS.name, + category = Categories.MAILS, icon = Icons.Filled.Email ), BottomNavigationItem( - title = Categories.OTHERS.name, + category = Categories.OTHERS, icon = Icons.AutoMirrored.Filled.Article ) ) @@ -76,8 +82,17 @@ fun MainBottomNavigation( bottomNavigationList.forEachIndexed { index, item -> Icon( imageVector = item.icon, - contentDescription = item.title, + // Previously the raw enum name ("BANKS"), announced in English in every locale. + contentDescription = if (item.isVisible) { + stringResource(id = item.category.labelRes) + } else { + null + }, modifier = Modifier + .then( + // The alpha-0 spacer stays reachable by TalkBack without this. + if (item.isVisible) Modifier else Modifier.clearAndSetSemantics { } + ) .size(42.dp) .padding(8.dp) .clickable( diff --git a/app/src/main/java/com/bhardwaj/passkey/utils/Categories.kt b/app/src/main/java/com/bhardwaj/passkey/utils/Categories.kt index 49989b1..bae470a 100644 --- a/app/src/main/java/com/bhardwaj/passkey/utils/Categories.kt +++ b/app/src/main/java/com/bhardwaj/passkey/utils/Categories.kt @@ -1,8 +1,15 @@ package com.bhardwaj.passkey.utils -enum class Categories { - BANKS, - MAILS, - APPS, - OTHERS -} \ No newline at end of file +import androidx.annotation.StringRes +import com.bhardwaj.passkey.R + +/** + * The entry categories. The enum *name* is the persisted value (Room column and backup files), + * so it must not change; [labelRes] carries the localized display name instead. + */ +enum class Categories(@param:StringRes val labelRes: Int) { + BANKS(R.string.banks), + MAILS(R.string.mails), + APPS(R.string.apps), + OTHERS(R.string.others) +} diff --git a/app/src/main/java/com/bhardwaj/passkey/utils/PasswordGenerator.kt b/app/src/main/java/com/bhardwaj/passkey/utils/PasswordGenerator.kt index 2f2a199..6c001af 100644 --- a/app/src/main/java/com/bhardwaj/passkey/utils/PasswordGenerator.kt +++ b/app/src/main/java/com/bhardwaj/passkey/utils/PasswordGenerator.kt @@ -1,28 +1,60 @@ package com.bhardwaj.passkey.utils +import java.security.SecureRandom +import java.util.Collections +import java.util.Random + object PasswordGenerator { + + private const val UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + private const val LOWER = "abcdefghijklmnopqrstuvwxyz" + private const val DIGITS = "0123456789" + private const val SPECIAL = "!@#$%^&*()_+-=[]{}|;:,.<>?" + + /** + * Deliberately [SecureRandom] and not `SecureRandom.getInstanceStrong()`, which can block on + * `/dev/random`. `setSeed` is never called: on API 26+ the platform provider is already seeded + * from the kernel, and seeding it ourselves would only reduce entropy. + */ + private val secureRandom: Random = SecureRandom() + + /** + * Generates a password of [length] characters. + * + * Guarantees at least one character from every selected class, which the previous + * implementation did not — a 16-character "password with symbols" could legitimately contain + * no symbol at all. If no class is selected it falls back to lowercase. + * + * @param random injectable purely so tests can seed it deterministically; production always + * uses the [SecureRandom] default. + */ fun generate( length: Int, includeUpper: Boolean, includeLower: Boolean, includeNumbers: Boolean, - includeSpecial: Boolean + includeSpecial: Boolean, + random: Random = secureRandom ): String { - val upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - val lower = "abcdefghijklmnopqrstuvwxyz" - val numbers = "0123456789" - val special = "!@#$%^&*()_+-=[]{}|;:,.<>?" - - var charPool = "" - if (includeUpper) charPool += upper - if (includeLower) charPool += lower - if (includeNumbers) charPool += numbers - if (includeSpecial) charPool += special - - if (charPool.isEmpty()) charPool = lower - - return (1..length) - .map { charPool.random() } - .joinToString("") + val pools = buildList { + if (includeUpper) add(UPPER) + if (includeLower) add(LOWER) + if (includeNumbers) add(DIGITS) + if (includeSpecial) add(SPECIAL) + }.ifEmpty { listOf(LOWER) } + + // Cannot satisfy "one per class" in fewer characters than there are classes. + val size = maxOf(length, pools.size) + val union = pools.joinToString(separator = "") + + val characters = ArrayList(size) + // nextInt(bound) performs rejection sampling internally, so it is unbiased; a manual + // modulo would reintroduce bias. + pools.forEach { pool -> characters.add(pool[random.nextInt(pool.length)]) } + repeat(size - pools.size) { characters.add(union[random.nextInt(union.length)]) } + + // Collections.shuffle(list, random) — NOT Kotlin's shuffled(), which uses kotlin.random. + Collections.shuffle(characters, random) + return characters.joinToString(separator = "") } -} \ No newline at end of file +} diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index fbb9b9f..4ed2f7c 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -1,5 +1,23 @@ - + + لا توجد إدخالات بعد + لا توجد تفاصيل بعد + + إصدار التطبيق %1$s + مدير كلمات المرور الوحيد الذي ستحتاج إليه + + + + تخطٍّ + التالي + أنشئ\nكلمات مرور\n[[آمنة]]. + كل\n[[كلمات مرورك]]\nهنا. + لا تكتبها،\n[[املأ]] بياناتك\nتلقائيًا. + توقّف عن استخدام كلمات مرور ضعيفة لحساباتك على الإنترنت، وارتقِ بها مع PassKey. احصل على كلمات مرور آمنة يصعب اختراقها. + احفظ كل كلمات مرورك وأدرها من مكان واحد. لا تحفظ مئات كلمات المرور، بل واحدة فقط. + لا تُعرّض كلمات مرورك للخطر بكتابتها أمام الآخرين، دع PassKey يملأها تلقائيًا ويحافظ على أمان بياناتك. + البنوك البريد @@ -20,7 +38,6 @@ تصدير البيانات - اكتب هنا… اكتب عنوانك هنا… اكتب ردك هنا… اكتب عنوانك هنا… @@ -35,12 +52,7 @@ مرحبًا بك في مدير PassKey - فشل التوثيق. حدث خطأ ما.. - يرجى تعيين كلمة مرور لجهازك أولاً. - الوصول إلى التخزين مطلوب لتصدير قاعدة البيانات. - تم رفض الإذن - حسنًا تغيير @@ -62,15 +74,11 @@ حالة الأمان تحليل الأمان - درجة الأمان كلمات مرور ضعيفة كلمات مرور مكررة - كلمات مرور آمنة ممتاز يحتاج إلى انتباه حرج - تم العثور على %1$d كلمات مرور ضعيفة - تم العثور على %1$d كلمات مرور مكررة مستخدمة %1$d مرات لم يتم العثور على مشاكل. أحسنت! \ No newline at end of file diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index 56fb2e3..6a115fb 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -1,5 +1,23 @@ - + + এখনও কোনো এন্ট্রি নেই + এখনও কোনো বিবরণ নেই + + অ্যাপ সংস্করণ %1$s + আপনার প্রয়োজনীয় একমাত্র পাসওয়ার্ড ম্যানেজার + + + + এড়িয়ে যান + পরবর্তী + তৈরি করুন\n[[সুরক্ষিত]]\nপাসওয়ার্ড। + আপনার সব\n[[পাসওয়ার্ড]]\nএখানেই। + টাইপ নয়,\nতথ্য [[স্বয়ংক্রিয়ভাবে]]\nপূরণ করুন। + আপনার অনলাইন অ্যাকাউন্টে দুর্বল পাসওয়ার্ড ব্যবহার করা বন্ধ করুন, PassKey দিয়ে এক ধাপ এগিয়ে যান। সবচেয়ে সুরক্ষিত ও ভাঙা কঠিন পাসওয়ার্ড পান। + এক জায়গা থেকেই আপনার সব পাসওয়ার্ড সংরক্ষণ ও পরিচালনা করুন। শত শত পাসওয়ার্ড নয়, শুধু একটি মনে রাখুন। + প্রকাশ্যে টাইপ করে আপনার পাসওয়ার্ড ঝুঁকিতে ফেলবেন না, PassKey-কে সেগুলি স্বয়ংক্রিয়ভাবে পূরণ করতে দিন এবং আপনার তথ্য সুরক্ষিত রাখুন। + ব্যাংক মেইল @@ -20,7 +38,6 @@ তথ্য রপ্তানি - এখানে লিখুন… আপনার শিরোনাম এখানে লিখুন… আপনার প্রতিক্রিয়া এখানে লিখুন… আপনার শিরোনাম এখানে লিখুন… @@ -35,12 +52,7 @@ PassKey ম্যানেজারে আপনাকে স্বাগতম - প্রাধিকারীকরণ ব্যর্থ হয়েছে। কিছু ভুল হয়েছে.. - দয়া করে আপনার ডিভাইসের জন্য পাসওয়ার্ড সেট করুন। - ডেটাবেস রপ্তানির জন্য সংগ্রহ অ্যাক্সেস প্রয়োজন। - অনুমতি অস্বীকৃত - ঠিক আছে পরিবর্তন @@ -62,15 +74,11 @@ নিরাপত্তা স্থিতি নিরাপত্তা বিশ্লেষণ - নিরাপত্তা স্কোর দুর্বল পাসওয়ার্ড পুনঃব্যবহৃত পাসওয়ার্ড - নিরাপদ পাসওয়ার্ড চমৎকার মনোযোগ প্রয়োজন জটিল - %1$d টি দুর্বল পাসওয়ার্ড পাওয়া গেছে - %1$d টি পুনঃব্যবহৃত পাসওয়ার্ড পাওয়া গেছে %1$d বার ব্যবহৃত হয়েছে কোনো সমস্যা পাওয়া যায়নি। দারুণ! diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 90c83d1..8e413f5 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -1,5 +1,23 @@ - + + Noch keine Einträge + Noch keine Details + + App-Version %1$s + Der einzige Passwort-Manager, den du je brauchst + + + + Überspringen + Weiter + Erstelle\n[[sichere]]\nPasswörter. + All deine\n[[Passwörter]]\nan einem Ort. + Nicht tippen,\nZugangsdaten\n[[automatisch]] füllen. + Verwende keine unsicheren Passwörter mehr für deine Online-Konten – mit PassKey geht es besser. Hol dir besonders sichere und schwer zu knackende Passwörter. + Speichere und verwalte all deine Passwörter an einem Ort. Merke dir nicht Hunderte von Passwörtern, sondern nur eines. + Gefährde deine Passwörter nicht, indem du sie in der Öffentlichkeit eintippst – lass sie PassKey automatisch ausfüllen und halte deine Zugangsdaten sicher. + Banken E-Mails @@ -20,7 +38,6 @@ Daten exportieren - Hier eingeben… Geben Sie hier Ihren Titel ein… Geben Sie hier Ihre Antwort ein… Geben Sie hier Ihre Überschrift ein… @@ -35,12 +52,7 @@ Willkommen beim PassKey Manager - Authentifizierung fehlgeschlagen. Etwas ist schiefgegangen.. - Bitte legen Sie zuerst ein Passwort für Ihr Gerät fest. - Speicherzugriff ist erforderlich, um die Datenbank zu exportieren. - Berechtigung verweigert - OK Ändern @@ -62,15 +74,11 @@ Sicherheitsstatus Sicherheitsanalyse - Sicherheitsbewertung Schwache Passwörter Wiederverwendete Passwörter - Sichere Passwörter Ausgezeichnet Aufmerksamkeit erforderlich Kritisch - %1$d schwache Passwörter gefunden - %1$d wiederverwendete Passwörter gefunden %1$d mal verwendet Keine Probleme gefunden. Gut gemacht! \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 84fc626..447b9ee 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -1,5 +1,23 @@ - + + Aún no hay entradas + Aún no hay detalles + + Versión de la app %1$s + El único gestor de contraseñas que necesitarás + + + + Omitir + Siguiente + Crea\ncontraseñas\n[[seguras]]. + Todas tus\n[[contraseñas]]\nestán aquí. + No escribas,\n[[autocompleta]]\ntus credenciales. + Deja de usar contraseñas inseguras en tus cuentas en línea y da el salto con PassKey. Consigue contraseñas muy seguras y difíciles de descifrar. + Guarda y gestiona todas tus contraseñas desde un solo lugar. No memorices cientos de contraseñas, solo una. + No pongas en riesgo tus contraseñas escribiéndolas en público; deja que PassKey las autocomplete y mantenga tus credenciales seguras. + Bancos Correos @@ -20,7 +38,6 @@ Exportar Datos - Escriba aquí… Escriba su título aquí… Escriba su respuesta aquí… Escriba su encabezado aquí… @@ -35,12 +52,7 @@ Bienvenido a PassKey Manager - Autenticación Fallida. Algo Salió Mal.. - Por favor establezca la contraseña de su dispositivo primero. - Se necesita acceso al almacenamiento para exportar la base de datos. - Permiso Denegado - OK Cambiar @@ -62,15 +74,11 @@ Estado de seguridad Análisis de seguridad - Puntuación de seguridad Contraseñas débiles Contraseñas reutilizadas - Contraseñas seguras Excelente Necesita atención Crítico - Se encontraron %1$d contraseñas débiles - Se encontraron %1$d contraseñas reutilizadas Usado %1$d veces No se encontraron problemas. ¡Bien hecho! diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index ad6a7da..f95faa4 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -1,5 +1,23 @@ - + + Aucune entrée pour l\'instant + Aucun détail pour l\'instant + + Version de l\'application %1$s + Le seul gestionnaire de mots de passe dont vous aurez besoin + + + + Passer + Suivant + Créez des\nmots de passe\n[[sécurisés]]. + Tous vos\n[[mots de passe]]\nsont ici. + Ne tapez plus,\n[[remplissez]]\nautomatiquement. + Arrêtez d\'utiliser des mots de passe peu sûrs pour vos comptes en ligne, passez à PassKey. Obtenez des mots de passe très sécurisés et difficiles à craquer. + Stockez et gérez tous vos mots de passe au même endroit. Ne retenez pas des centaines de mots de passe, un seul suffit. + Ne compromettez pas vos mots de passe en les saisissant en public : laissez PassKey les remplir automatiquement et garder vos identifiants en sécurité. + Banques Courriels @@ -20,7 +38,6 @@ Exporter les données - Tapez ici… Tapez votre titre ici… Tapez votre réponse ici… Tapez votre en-tête ici… @@ -35,12 +52,7 @@ Bienvenue dans PassKey Manager - Échec de l\'authentification. Quelque chose s\'est mal passé.. - Veuillez définir un mot de passe pour votre appareil d\'abord. - L\'accès au stockage est requis pour exporter la base de données. - Autorisation refusée - OK Changer @@ -62,15 +74,11 @@ État de sécurité Analyse de sécurité - Score de sécurité Mots de passe faibles Mots de passe réutilisés - Mots de passe sûrs Excellent Nécessite une attention Critique - %1$d mots de passe faibles trouvés - %1$d mots de passe réutilisés trouvés Utilisé %1$d fois Aucun problème trouvé. Bien joué ! diff --git a/app/src/main/res/values-gu/strings.xml b/app/src/main/res/values-gu/strings.xml index 38d8feb..5e7bedd 100644 --- a/app/src/main/res/values-gu/strings.xml +++ b/app/src/main/res/values-gu/strings.xml @@ -1,5 +1,23 @@ - + + હજી કોઈ એન્ટ્રી નથી + હજી કોઈ વિગત નથી + + ઍપ સંસ્કરણ %1$s + તમને જરૂરી એકમાત્ર પાસવર્ડ મેનેજર + + + + છોડો + આગળ + બનાવો\n[[સુરક્ષિત]]\nપાસવર્ડ. + તમારા બધા\n[[પાસવર્ડ]]\nઅહીં છે. + ટાઇપ નહીં,\nવિગતો\n[[ઑટોફિલ]] કરો. + તમારા ઓનલાઇન ખાતાં માટે અસુરક્ષિત પાસવર્ડ વાપરવાનું બંધ કરો, PassKey સાથે આગળ વધો. સૌથી સુરક્ષિત અને તોડવા મુશ્કેલ પાસવર્ડ મેળવો. + તમારા બધા પાસવર્ડ એક જ જગ્યાએથી સાચવો અને સંભાળો. સેંકડો પાસવર્ડ નહીં, ફક્ત એક જ યાદ રાખો. + જાહેરમાં ટાઇપ કરીને તમારા પાસવર્ડ જોખમમાં ન મૂકો, PassKey ને તે ઑટોફિલ કરવા દો અને તમારી વિગતો સુરક્ષિત રાખો. + બેંકો મેલ @@ -20,7 +38,6 @@ ડેટા નિર્યાત કરો - અહીં ટાઇપ કરો… તમારો શીર્ષક અહીં ટાઇપ કરો… તમારો જવાબ અહીં ટાઇપ કરો… તમારો શીર્ષક અહીં ટાઇપ કરો… @@ -35,12 +52,7 @@ PassKey મેનેજર માં આપનું સ્વાગત છે - પ્રમાણીકરણ નિષ્ફળ થયું. કંઈક ખોટું થયું છે.. - કૃપા કરીને પ્રથમ તમારા ડિવાઇસ માટે પાસવર્ડ સેટ કરો. - ડેટાબેઝ નિર્યાત કરવા માટે સ્ટોરેજ ઍક્સેસ જરૂરી છે. - પરવાનગી નકારી - ઠીક છે બદલવું @@ -62,15 +74,11 @@ સુરક્ષા સ્થિતિ સુરક્ષા વિશ્લેષણ - સુરક્ષા સ્કોર નબળા પાસવર્ડ્સ પુનઃઉપયોગમાં લેવાયેલા પાસવર્ડ્સ - સુરક્ષિત પાસવર્ડ્સ ઉત્તમ ધ્યાન આપવાની જરૂર છે ગંભીર - %1$d નબળા પાસવર્ડ્સ મળ્યા - %1$d પુનઃઉપયોગમાં લેવાયેલા પાસવર્ડ્સ મળ્યા %1$d વખત ઉપયોગ થયો કોઈ સમસ્યા મળી નથી. સરસ! diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 931ac0f..22e3195 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -1,5 +1,23 @@ - + + अभी कोई प्रविष्टि नहीं + अभी कोई विवरण नहीं + + ऐप संस्करण %1$s + आपको बस इसी पासवर्ड मैनेजर की ज़रूरत है + + + + छोड़ें + आगे + बनाएँ\n[[सुरक्षित]]\nपासवर्ड। + आपके सारे\n[[पासवर्ड]]\nयहीं हैं। + टाइप नहीं,\nजानकारी\n[[ऑटोफ़िल]] करें। + अपने ऑनलाइन खातों के लिए असुरक्षित पासवर्ड इस्तेमाल करना बंद करें, PassKey के साथ आगे बढ़ें। सबसे सुरक्षित और मुश्किल से टूटने वाले पासवर्ड पाएँ। + अपने सभी पासवर्ड एक ही जगह से सहेजें और संभालें। सैकड़ों पासवर्ड नहीं, बस एक याद रखें। + सबके सामने टाइप करके अपने पासवर्ड जोखिम में न डालें, PassKey को उन्हें ऑटोफ़िल करने दें और अपनी जानकारी सुरक्षित रखें। + बैंक मेल @@ -20,7 +38,6 @@ फ़ाइलें निर्यात करें - यहा लिखें… यहाँ अपने शीर्षक लिखें… अपना उत्तर यहां लिखें… अपना शीर्षक यहां लिखें… @@ -35,12 +52,7 @@ PassKey प्रबंधक में आपका स्वागत है - प्रमाणीकरण विफल हो गया। कुछ गलत हो गया.. - कृपया पहले अपने डिवाइस के लिए पासवर्ड सेट करें। - संग्रहण अनुमति की आवश्यकता है - अनुमति नहीं है - स्वीकृति दें भाषा @@ -62,15 +74,11 @@ सुरक्षा स्थिति सुरक्षा विश्लेषण - सुरक्षा स्कोर कमजोर पासवर्ड पुनः उपयोग किए गए पासवर्ड - सुरक्षित पासवर्ड बहुत बढ़िया ध्यान देने की आवश्यकता है गंभीर - %1$d कमजोर पासवर्ड मिले - %1$d पुनः उपयोग किए गए पासवर्ड मिले %1$d बार उपयोग किया गया - कोई समस्या नहीं मिली। बहुत बढ़िया!v + कोई समस्या नहीं मिली। बहुत बढ़िया! \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index b2af754..d80980e 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -1,5 +1,23 @@ - + + Nessuna voce per ora + Nessun dettaglio per ora + + Versione app %1$s + L\'unico gestore di password di cui avrai bisogno + + + + Salta + Avanti + Genera\npassword\n[[sicure]]. + Tutte le tue\n[[password]]\nsono qui. + Non digitare,\n[[compila]]\nle credenziali. + Smetti di usare password poco sicure per i tuoi account online, fai il salto con PassKey. Ottieni password sicurissime e difficili da violare. + Salva e gestisci tutte le tue password da un unico posto. Non ricordare centinaia di password, ricordane una sola. + Non mettere a rischio le tue password digitandole in pubblico: lascia che PassKey le compili automaticamente e protegga le tue credenziali. + Banche Email @@ -20,7 +38,6 @@ Esporta Dati - Digita qui… Digita il tuo titolo qui… Digita qui la tua risposta… Digita qui il tuo titolo… @@ -35,12 +52,7 @@ Benvenuti in PassKey Manager - Autenticazione Fallita. Qualcosa è andato storto.. - Si prega di impostare prima la password per il dispositivo. - È necessario l\'accesso alla memoria per esportare il database. - Permesso Negato - OK Cambia @@ -62,15 +74,11 @@ Stato di Sicurezza Analisi di Sicurezza - Punteggio di Sicurezza Password Deboli Password Riutilizzate - Password Sicure Eccellente Richiede Attenzione Critico - Trovate %1$d password deboli - Trovate %1$d password riutilizzate Usata %1$d volte Nessun problema trovato. Ottimo lavoro! diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 581c4be..668931f 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -1,5 +1,23 @@ - + + まだ項目がありません + まだ詳細がありません + + アプリバージョン %1$s + 必要なパスワード管理は、これひとつ + + + + スキップ + 次へ + [[安全な]]\nパスワードを\n作成。 + すべての\n[[パスワード]]が\nここに。 + 入力せずに\n認証情報を\n[[自動入力]]。 + オンラインアカウントに安全でないパスワードを使うのはやめて、PassKey で一歩先へ。最も安全で解読されにくいパスワードを手に入れましょう。 + すべてのパスワードをひとつの場所で保存・管理。何百ものパスワードではなく、覚えるのはひとつだけ。 + 人前で入力してパスワードを危険にさらさないでください。PassKey に自動入力させて、認証情報を安全に保ちましょう。 + 銀行 メール @@ -20,7 +38,6 @@ データをエクスポート - ここに入力… ここにタイトルを入力… ここに応答を入力… ここに見出しを入力… @@ -35,12 +52,7 @@ PassKey Managerへようこそ - 認証に失敗しました。 何かがうまくいかない.. - まずデバイスのパスワードを設定してください。 - データベースをエクスポートするにはストレージアクセスが必要です。 - 許可が拒否されました - OK 変更 @@ -62,15 +74,11 @@ 安全性ステータス セキュリティ分析 - セキュリティスコア 脆弱なパスワード 使い回しのパスワード - 安全なパスワード 優秀 注意が必要 危険 - %1$d 個の脆弱なパスワードが見つかりました - %1$d 個の使い回しパスワードが見つかりました %1$d 回使用 問題は見つかりませんでした。素晴らしい! \ No newline at end of file diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index ea8f025..80f19c6 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -1,5 +1,23 @@ - + + 아직 항목이 없습니다 + 아직 세부 정보가 없습니다 + + 앱 버전 %1$s + 당신에게 필요한 단 하나의 비밀번호 관리자 + + + + 건너뛰기 + 다음 + [[안전한]]\n비밀번호를\n만드세요. + 모든\n[[비밀번호]]가\n여기에. + 입력하지 말고\n자격 증명을\n[[자동 완성]]. + 온라인 계정에 취약한 비밀번호를 쓰지 마세요. PassKey로 한 단계 올라서세요. 가장 안전하고 뚫기 어려운 비밀번호를 만들어 드립니다. + 모든 비밀번호를 한곳에서 저장하고 관리하세요. 수백 개를 외울 필요 없이 하나만 기억하면 됩니다. + 사람들 앞에서 입력하다 비밀번호를 노출하지 마세요. PassKey가 자동으로 채워 자격 증명을 안전하게 지켜 드립니다. + 은행 메일 @@ -20,7 +38,6 @@ 데이터 내보내기 - 여기에 입력… 여기에 제목 입력… 여기에 응답 입력… 여기에 제목 입력… @@ -35,12 +52,7 @@ PassKey Manager에 오신 것을 환영합니다 - 인증 실패했습니다. 문제가 발생했습니다.. - 먼저 장치의 비밀번호를 설정하십시오. - 데이터베이스를 내보내려면 저장소 액세스가 필요합니다. - 허가가 거부되었습니다 - 확인 변경 @@ -62,15 +74,11 @@ 안전 상태 보안 분석 - 보안 점수 취약한 비밀번호 재사용된 비밀번호 - 안전한 비밀번호 훌륭함 주의 필요 심각함 - %1$d개의 취약한 비밀번호 발견 - %1$d개의 재사용된 비밀번호 발견 %1$d회 사용됨 문제가 발견되지 않았습니다. 잘하셨습니다! \ No newline at end of file diff --git a/app/src/main/res/values-mr/strings.xml b/app/src/main/res/values-mr/strings.xml index c84027a..657cd85 100644 --- a/app/src/main/res/values-mr/strings.xml +++ b/app/src/main/res/values-mr/strings.xml @@ -1,5 +1,23 @@ - + + अद्याप कोणतीही नोंद नाही + अद्याप कोणताही तपशील नाही + + ॲप आवृत्ती %1$s + तुम्हाला लागणारा एकमेव पासवर्ड व्यवस्थापक + + + + वगळा + पुढे + तयार करा\n[[सुरक्षित]]\nपासवर्ड. + तुमचे सर्व\n[[पासवर्ड]]\nइथेच आहेत. + टाइप नको,\nमाहिती\n[[ऑटोफिल]] करा. + तुमच्या ऑनलाइन खात्यांसाठी असुरक्षित पासवर्ड वापरणे थांबवा, PassKey सोबत पुढे जा. सर्वात सुरक्षित आणि तोडायला कठीण पासवर्ड मिळवा. + तुमचे सर्व पासवर्ड एकाच ठिकाणी साठवा आणि व्यवस्थापित करा. शेकडो पासवर्ड नको, फक्त एकच लक्षात ठेवा. + सार्वजनिक ठिकाणी टाइप करून तुमचे पासवर्ड धोक्यात आणू नका, PassKey ला ते ऑटोफिल करू द्या आणि तुमची माहिती सुरक्षित ठेवा. + बँक पत्र @@ -20,7 +38,6 @@ डेटा निर्यात करा - येथे टाईप करा… आपले शीर्षक येथे टाईप करा… आपला प्रतिसाद येथे टाईप करा… आपले शीर्षक येथे टाईप करा… @@ -35,12 +52,7 @@ पासकी मॅनेजरमध्ये आपले स्वागत आहे - प्रमाणीकरण अयशस्वी झाले. काहीतरी चुकलं आहे.. - कृपया प्रथम आपल्या डिव्हाइससाठी संकेतशब्द सेट करा. - डेटाबेस निर्यात करण्यासाठी स्टोरेज प्रवेश आवश्यक आहे. - परवानगी नकारली - ठीक आहे बदला @@ -62,15 +74,11 @@ सुरक्षा स्थिती सुरक्षा विश्लेषण - सुरक्षा स्कोअर कमकुवत पासवर्ड पुन्हा वापरलेले पासवर्ड - सुरक्षित पासवर्ड उत्कृष्ट लक्ष देणे आवश्यक आहे गंभीर - %1$d कमकुवत पासवर्ड आढळले - %1$d पुन्हा वापरलेले पासवर्ड आढळले %1$d वेळा वापरले कोणतीही समस्या आढळली नाही. छान! \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index ab5a4b8..f39b7b3 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -1,5 +1,23 @@ - + + Nenhum item ainda + Nenhum detalhe ainda + + Versão do app %1$s + O único gerenciador de senhas que você vai precisar + + + + Pular + Avançar + Crie\nsenhas\n[[seguras]]. + Todas as suas\n[[senhas]]\nestão aqui. + Não digite,\n[[preencha]]\nsuas credenciais. + Pare de usar senhas inseguras nas suas contas online e avance com o PassKey. Tenha senhas muito seguras e difíceis de quebrar. + Guarde e gerencie todas as suas senhas em um só lugar. Não decore centenas de senhas, memorize apenas uma. + Não comprometa suas senhas digitando-as em público; deixe o PassKey preencher tudo automaticamente e manter suas credenciais seguras. + Bancos Correios @@ -20,7 +38,6 @@ Exportar Dados - Digite aqui… Digite seu título aqui… Digite sua resposta aqui… Digite seu título aqui… @@ -35,12 +52,7 @@ Bem-vindo ao Gerenciador de Senhas - Autenticação Falhou. Algo deu errado.. - Por favor, defina uma senha para o seu dispositivo primeiro. - Acesso ao armazenamento é necessário para exportar o banco de dados. - Permissão Negada - OK Alterar @@ -62,15 +74,11 @@ Status de Segurança Análise de Segurança - Pontuação de Segurança Senhas Fracas Senhas Reutilizadas - Senhas Seguras Excelente Requer Atenção Crítico - %1$d senhas fracas encontradas - %1$d senhas reutilizadas encontradas Usado %1$d vezes Nenhum problema encontrado. Bom trabalho! \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 04dd34d..a2e5db9 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1,5 +1,23 @@ - + + Пока нет записей + Пока нет данных + + Версия приложения %1$s + Единственный менеджер паролей, который вам нужен + + + + Пропустить + Далее + Создавайте\n[[надёжные]]\nпароли. + Все ваши\n[[пароли]]\nздесь. + Не печатайте,\n[[заполняйте]]\nданные сами. + Перестаньте использовать ненадёжные пароли для своих аккаунтов — переходите на PassKey. Получайте самые надёжные и трудные для взлома пароли. + Храните все пароли и управляйте ими в одном месте. Запоминайте не сотни паролей, а всего один. + Не рискуйте паролями, вводя их на виду у других — позвольте PassKey заполнять их автоматически и хранить ваши данные в безопасности. + Банки Почта @@ -20,7 +38,6 @@ Экспортировать данные - Введите здесь… Введите ваш заголовок здесь… Введите ваш ответ здесь… Введите ваш заголовок здесь… @@ -35,12 +52,7 @@ Добро пожаловать в Менеджер паролей - Ошибка аутентификации. Что-то пошло не так.. - Пожалуйста, сначала установите пароль для вашего устройства. - Требуется доступ к хранилищу для экспорта базы данных. - Доступ запрещен - OK Изменить @@ -62,15 +74,11 @@ Состояние безопасности Анализ безопасности - Оценка безопасности Слабые пароли Повторяющиеся пароли - Надежные пароли Отлично Требует внимания Критично - Найдено %1$d слабых паролей - Найдено %1$d повторяющихся паролей Использовано %1$d раз Проблем не обнаружено. Отличная работа! diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 51f1619..aafd990 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -1,5 +1,23 @@ - + + இன்னும் உள்ளீடுகள் இல்லை + இன்னும் விவரங்கள் இல்லை + + ஆப் பதிப்பு %1$s + உங்களுக்குத் தேவையான ஒரே கடவுச்சொல் மேலாளர் + + + + தவிர் + அடுத்து + [[பாதுகாப்பான]]\nகடவுச்சொற்களை\nஉருவாக்கு. + உங்கள் அனைத்து\n[[கடவுச்சொற்களும்]]\nஇங்கே. + தட்டச்சு வேண்டாம்,\nதகவலை\n[[தானாக நிரப்பு]]. + உங்கள் ஆன்லைன் கணக்குகளுக்குப் பாதுகாப்பற்ற கடவுச்சொற்களைப் பயன்படுத்துவதை நிறுத்துங்கள், PassKey உடன் முன்னேறுங்கள். மிகவும் பாதுகாப்பான, உடைக்கக் கடினமான கடவுச்சொற்களைப் பெறுங்கள். + உங்கள் அனைத்துக் கடவுச்சொற்களையும் ஒரே இடத்தில் சேமித்து நிர்வகியுங்கள். நூற்றுக்கணக்கானவை அல்ல, ஒன்றை மட்டும் நினைவில் வையுங்கள். + பொது இடத்தில் தட்டச்சு செய்து உங்கள் கடவுச்சொற்களை ஆபத்தில் ஆழ்த்த வேண்டாம், PassKey அவற்றைத் தானாக நிரப்பி உங்கள் தகவலைப் பாதுகாக்கட்டும். + வங்கிகள் மின்னஞ்சல்கள் @@ -20,7 +38,6 @@ தரவு ஏற்றுமதி - இங்கே தட்டச்சு செய்யவும்… உங்கள் தலைப்பு இங்கே தட்டச்சு செய்யவும்… உங்கள் பதில் இங்கே தட்டச்சு செய்யவும்… உங்கள் தலைப்பு இங்கே தட்டச்சு செய்யவும்… @@ -35,12 +52,7 @@ பாஸ்கீ மேனேஜருக்கு வரவேற்கிறோம் - அங்கீகாரம் தோல்வியடைந்தது. எதாவது தவறு நடந்தது.. - தயவுசெய்து முதலில் உங்கள் சான்றுகளுக்கு கடவுச்சொல் அமைக்கவும். - தரவுத்தள ஏற்றுமதி செய்யும் கடன் பெற சேமிக்கும் அணுகல் தேவைப்படுகிறது. - அனுமதி நிராகரிக்கப்பட்டது - சரி மாற்று @@ -62,15 +74,11 @@ பாதுகாப்பு நிலை பாதுகாப்பு பகுப்பாய்வு - பாதுகாப்பு மதிப்பெண் பலவீனமான கடவுச்சொற்கள் மீண்டும் பயன்படுத்தப்பட்ட கடவுச்சொற்கள் - பாதுகாப்பான கடவுச்சொற்கள் சிறந்தது கவனம் தேவை மிக முக்கியம் - %1$d பலவீனமான கடவுச்சொற்கள் கண்டறியப்பட்டன - %1$d மீண்டும் பயன்படுத்தப்பட்ட கடவுச்சொற்கள் கண்டறியப்பட்டன %1$d முறை பயன்படுத்தப்பட்டது எந்த பிரச்சனையும் இல்லை. நன்று! diff --git a/app/src/main/res/values-te/strings.xml b/app/src/main/res/values-te/strings.xml index d209ba3..ea7126f 100644 --- a/app/src/main/res/values-te/strings.xml +++ b/app/src/main/res/values-te/strings.xml @@ -1,5 +1,23 @@ - + + ఇంకా ఎంట్రీలు లేవు + ఇంకా వివరాలు లేవు + + యాప్ వెర్షన్ %1$s + మీకు కావలసిన ఏకైక పాస్‌వర్డ్ మేనేజర్ + + + + దాటవేయి + తదుపరి + [[సురక్షిత]]\nపాస్‌వర్డ్‌లను\nసృష్టించండి. + మీ అన్ని\n[[పాస్‌వర్డ్‌లు]]\nఇక్కడే. + టైప్ చేయవద్దు,\nవివరాలను\n[[ఆటోఫిల్]] చేయండి. + మీ ఆన్‌లైన్ ఖాతాలకు అసురక్షిత పాస్‌వర్డ్‌లను ఉపయోగించడం ఆపండి, PassKey తో ముందుకు సాగండి. అత్యంత సురక్షితమైన, ఛేదించడం కష్టమైన పాస్‌వర్డ్‌లను పొందండి. + మీ అన్ని పాస్‌వర్డ్‌లను ఒకే చోట నుండి భద్రపరచండి, నిర్వహించండి. వందల పాస్‌వర్డ్‌లు కాదు, ఒక్కటే గుర్తుంచుకోండి. + బహిరంగంగా టైప్ చేసి మీ పాస్‌వర్డ్‌లను ప్రమాదంలో పెట్టవద్దు, PassKey వాటిని ఆటోఫిల్ చేసి మీ వివరాలను సురక్షితంగా ఉంచనివ్వండి. + బ్యాంకులు మెయిల్‌లు @@ -20,7 +38,6 @@ డేటా ఎగురుతున్నది - ఇక్కడ టైప్ చేయండి… మీ శీర్షికను ఇక్కడ టైప్ చేయండి… మీ సమాధానం ఇక్కడ టైప్ చేయండి… మీ శీర్షికను ఇక్కడ టైప్ చేయండి… @@ -35,12 +52,7 @@ పాస్‌కీ మేనేజర్‌కు స్వాగతం - ప్రమాణీకరణ విఫలమైంది. ఏదో తప్పు జరిగింది.. - దయచేసి మీ ఉపకరణానికి మొదటిసారిగా పాస్‌వర్డ్‌ను సెట్ చేయండి. - పరిమితితములు యిక్కడి ప్రవేశించి డేటాబేస్‌ను ఎగురుతున్నాయి. - అనుమతి తప్పబడింది - సరే మార్చు @@ -62,15 +74,11 @@ భద్రతా స్థితి భద్రతా విశ్లేషణ - భద్రతా స్కోర్ బలహీనమైన పాస్‌వర్డ్‌లు తిరిగి ఉపయోగించిన పాస్‌వర్డ్‌లు - సురక్షిత పాస్‌వర్డ్‌లు అద్భుతం శ్రద్ధ అవసరం క్లిష్టమైన - %1$d బలహీనమైన పాస్‌వర్డ్‌లు కనుగొనబడ్డాయి - %1$d తిరిగి ఉపయోగించిన పాస్‌వర్డ్‌లు కనుగొనబడ్డాయి %1$d సార్లు ఉపయోగించబడింది ఎటువంటి సమస్యలు కనుగొనబడలేదు. బాగా చేసారు! diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 8a752e4..d6594a0 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -1,5 +1,23 @@ - + + 暂无条目 + 暂无详情 + + 应用版本 %1$s + 你唯一需要的密码管理器 + + + + 跳过 + 下一步 + 生成\n[[安全]]\n密码。 + 你的所有\n[[密码]]\n都在这里。 + 无需输入,\n[[自动填充]]\n你的凭据。 + 别再为在线账户使用不安全的密码,用 PassKey 更进一步。获取最安全、最难破解的密码。 + 在一个地方存储和管理你的所有密码。不必记住几百个密码,只需记住一个。 + 不要在公共场合输入密码而让它们暴露风险,让 PassKey 自动填充,保护你的凭据安全。 + 银行 邮件 @@ -20,7 +38,6 @@ 导出数据 - 在此输入… 在此输入您的标题… 在此输入您的响应… 在此输入您的标题… @@ -35,12 +52,7 @@ 欢迎使用PassKey Manager - 身份验证失败。 出了点问题.. - 请先为您的设备设置密码。 - 需要访问存储空间以导出数据库。 - 权限被拒绝 - 确定 更改 @@ -62,15 +74,11 @@ 安全状态 安全分析 - 安全评分 弱密码 重复使用的密码 - 安全密码 优秀 需要注意 严重 - 发现 %1$d 个弱密码 - 发现 %1$d 个重复使用的密码 已使用 %1$d 次 未发现问题。干得好! diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6a043c7..9da5e95 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,22 +1,36 @@ - - + - App Version %1$s + PassKey - The only password manager you\'ll ever need + App Version %1$s + The only password manager you\'ll ever need - Skip - Next - Generate\nSecure\nPasswords. - All Your\nPasswords Are\nHere. - Don\'t Type,\nAutofill Your\nCredentials. - Stop using unsecure passwords for your online accounts, level up with PassKey. Get the most secure and difficult-to-crack passwords. - Store and manage all of your passwords from one place. Don’t remember hundreds of passwords, just remember one. - Don’t compromise your passwords by typing them in public, let PassKey autofill those and keep your credentials secure. + + Skip + Next + Generate\n[[Secure]]\nPasswords. + All Your\n[[Passwords]] Are\nHere. + Don\'t Type,\n[[Autofill]] Your\nCredentials. + Stop using unsecure passwords for your online accounts, level up with PassKey. Get the most secure and difficult-to-crack passwords. + Store and manage all of your passwords from one place. Don’t remember hundreds of passwords, just remember one. + Don’t compromise your passwords by typing them in public, let PassKey autofill those and keep your credentials secure. + Welcome to Passkey: Password Manager, the ultimate solution for managing all your passwords and private information securely!\n\nAre you tired of remembering multiple passwords or annoyed of forgetting them? Passkey: Password Manager helps you store all your logins, passwords, and other private information safe and secure in an encrypted database.\n\nPasskey: Password Manager is an open-source application developed by Aditya Bhardwaj. It is designed to provide a secure way to manage all your passwords without worrying about data leaks. With Passkey: Password Manager, you can store your passwords locally on your device, and the application does not require internet permissions, making it even more secure.\n\nPasskey: Password Manager features an encrypted database to store your passwords, and biometric authentication is used to access your information. The application supports dark mode, and screenshot blocking features to ensure that your information is not accessible by unauthorized persons.\n\nThe application uses Drag & Drop and Swipe to Delete features, providing a user-friendly interface to manage all your passwords easily. The application also has a Separation of Content feature, which allows you to categorize your passwords into different sections for easy management.\n\nPasskey: Password Manager uses LiveData and MVVM architecture to ensure that the application is scalable, maintainable, and easy to use. SQL Cipher is used for Room DB Backup Encryption and Decryption, and Dagger-Hilt is used for dependency injection.\n\nIn conclusion, Passkey: Password Manager is a must-have application for everyone who wants to manage their passwords securely. With its features and user-friendly interface, you can keep all your passwords and private information safe and secure. Download Passkey: Password Manager now, and never worry about forgetting your passwords again! At Passkey: Password Manager, we are committed to protecting your privacy. This privacy policy explains how we collect, use, and share your personal information when you use our application.\n\nInformation we collect: We do not collect any of your passwords or private information that you store in the Passkey: Password Manager application. Your information is stored locally on your phone and encrypted for added security.\n\nHow we use your information: We use your information to store your passwords and private information securely in an encrypted database. We do not share your information with any third-party.\n\nHow we protect your information: We use encrypted database storage to protect your information, and biometric authentication is used to access your information.\n\nHow we share your information: We do not share your information with any third-party.\n\nChanges to our privacy policy: We reserve the right to modify this privacy policy at any time without prior notice.\n\nIf you have any questions or concerns about our terms and conditions or privacy policy, please contact us at yrkkh.cclub@gmail.com\n\nWe hope you enjoy using Passkey: Password Manager to store your passwords and private information securely. Welcome to Passkey: Password Manager! By downloading and using this application, you agree to comply with and be bound by the following terms and conditions:\n\n1. Passkey: Password Manager is an application developed by Aditya Bhardwaj, and the intellectual property rights are owned by Aditya Bhardwaj.\n\n2. The Passkey: Password Manager application is designed to store your passwords and private information securely in an encrypted database.\n\n3. The Passkey: Password Manager application does not store your credentials on servers, so your passwords and private information are in your hands.\n\n4. The Passkey: Password Manager application does not require any internet permission, and it is an open-source application.\n\n5. The Passkey: Password Manager application uses encrypted database storage to store your information, and biometric authentication is used to access your information.\n\n6. The Passkey: Password Manager application is provided "as is" without any warranty of any kind, either express or implied, including but not limited to the implied warranties of merchantability and fitness for a particular purpose.\n\n7. Aditya Bhardwaj is not liable for any damages arising from the use of the Passkey: Password Manager application, including but not limited to direct, indirect, incidental, or consequential damages.\n\n8. By downloading and using the Passkey: Password Manager application, you agree to indemnify and hold harmless Aditya Bhardwaj, its officers, employees, agents, and affiliates from any and all claims, damages, losses, liabilities, and expenses arising from your use of the application.\n\n9. Aditya Bhardwaj reserves the right to modify, suspend, or discontinue the Passkey: Password Manager application at any time without prior notice. @@ -24,6 +38,8 @@ + No entries yet + No details yet Banks Mails Apps @@ -43,7 +59,6 @@ Export Data - Type here… Type your title here… Type your response here… Type your heading here… @@ -58,12 +73,7 @@ Welcome to PassKey Manager - Authentication Failed. Something Went Wrong.. - Please set password for your device first. - Storage access is required to export database. - Permission Denied - OK Change @@ -85,15 +95,11 @@ Safety Status Security Analysis - Security Score Weak Passwords Reused Passwords - Safe Passwords Excellent Needs Attention Critical - Found %1$d weak passwords - Found %1$d reused passwords Used %1$d times No issues found. Good job! diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml index 29c6964..ec31e4d 100644 --- a/app/src/main/res/xml/locales_config.xml +++ b/app/src/main/res/xml/locales_config.xml @@ -16,4 +16,5 @@ + \ No newline at end of file diff --git a/app/src/test/java/com/bhardwaj/passkey/HighlightMarkupTest.kt b/app/src/test/java/com/bhardwaj/passkey/HighlightMarkupTest.kt new file mode 100644 index 0000000..2345642 --- /dev/null +++ b/app/src/test/java/com/bhardwaj/passkey/HighlightMarkupTest.kt @@ -0,0 +1,106 @@ +package com.bhardwaj.passkey + +import com.bhardwaj.passkey.presentation.screens.onboarding_screens.components.HighlightSpan +import com.bhardwaj.passkey.presentation.screens.onboarding_screens.components.parseHighlightMarkup +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * Regression guard for the onboarding crash. + * + * The previous implementation looked up a hardcoded English word with `indexOf` and passed the + * result straight to `AnnotatedString.addStyle`. For any translated heading `indexOf` returned + * -1 and `addStyle(start = -1, ...)` threw. Every case below must therefore produce in-bounds + * spans and never throw. + */ +class HighlightMarkupTest { + + @Test + fun `strips markers and reports the highlighted range`() { + val result = parseHighlightMarkup("Generate\n[[Secure]]\nPasswords.") + + assertEquals("Generate\nSecure\nPasswords.", result.text) + assertEquals(listOf(HighlightSpan(9, 15)), result.spans) + assertEquals("Secure", result.text.substring(9, 15)) + } + + @Test + fun `text with no markers yields no spans`() { + val result = parseHighlightMarkup("All Your Passwords Are Here.") + + assertEquals("All Your Passwords Are Here.", result.text) + assertTrue(result.spans.isEmpty()) + } + + @Test + fun `unclosed marker is dropped rather than throwing`() { + val result = parseHighlightMarkup("Generate [[Secure Passwords.") + + assertEquals("Generate Secure Passwords.", result.text) + assertTrue(result.spans.isEmpty()) + } + + @Test + fun `stray closing marker is dropped rather than throwing`() { + val result = parseHighlightMarkup("Generate Secure]] Passwords.") + + assertEquals("Generate Secure]] Passwords.", result.text) + assertTrue(result.spans.isEmpty()) + } + + @Test + fun `empty marker pair produces no span`() { + val result = parseHighlightMarkup("Generate [[]] Passwords.") + + assertEquals("Generate Passwords.", result.text) + assertTrue(result.spans.isEmpty()) + } + + @Test + fun `supports multiple highlighted spans`() { + val result = parseHighlightMarkup("[[All]] Your [[Passwords]]") + + assertEquals("All Your Passwords", result.text) + assertEquals(listOf(HighlightSpan(0, 3), HighlightSpan(9, 18)), result.spans) + assertEquals("All", result.text.substring(0, 3)) + assertEquals("Passwords", result.text.substring(9, 18)) + } + + @Test + fun `handles markers at both string boundaries`() { + val result = parseHighlightMarkup("[[Autofill]]") + + assertEquals("Autofill", result.text) + assertEquals(listOf(HighlightSpan(0, 8)), result.spans) + } + + @Test + fun `empty input is handled`() { + val result = parseHighlightMarkup("") + + assertEquals("", result.text) + assertTrue(result.spans.isEmpty()) + } + + @Test + fun `every shipped heading parses to in-bounds spans`() { + // Representative of the real translated resources, including RTL and CJK. + val shipped = listOf( + "Generate\n[[Secure]]\nPasswords.", + "बनाएँ\n[[सुरक्षित]]\nपासवर्ड।", + "أنشئ\nكلمات مرور\n[[آمنة]].", + "[[安全な]]\nパスワードを\n作成。", + "Créez des\nmots de passe\n[[sécurisés]]." + ) + shipped.forEach { raw -> + val result = parseHighlightMarkup(raw) + assertEquals("one highlight expected in: $raw", 1, result.spans.size) + result.spans.forEach { span -> + assertTrue("start in bounds: $raw", span.start in 0..result.text.length) + assertTrue("end in bounds: $raw", span.endExclusive in 0..result.text.length) + assertTrue("non-empty span: $raw", span.endExclusive > span.start) + } + } + } +} diff --git a/app/src/test/java/com/bhardwaj/passkey/PasswordGeneratorTest.kt b/app/src/test/java/com/bhardwaj/passkey/PasswordGeneratorTest.kt new file mode 100644 index 0000000..ad3d2a6 --- /dev/null +++ b/app/src/test/java/com/bhardwaj/passkey/PasswordGeneratorTest.kt @@ -0,0 +1,102 @@ +package com.bhardwaj.passkey + +import com.bhardwaj.passkey.utils.PasswordGenerator +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import java.util.Random + +class PasswordGeneratorTest { + + private companion object { + const val UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + const val LOWER = "abcdefghijklmnopqrstuvwxyz" + const val DIGITS = "0123456789" + const val SPECIAL = "!@#$%^&*()_+-=[]{}|;:,.<>?" + } + + private fun generate( + length: Int = 16, + upper: Boolean = true, + lower: Boolean = true, + numbers: Boolean = true, + special: Boolean = true, + random: Random = Random(20260916) + ) = PasswordGenerator.generate(length, upper, lower, numbers, special, random) + + @Test + fun `produces a password of the requested length`() { + for (length in 4..32) { + assertEquals(length, generate(length = length).length) + } + } + + @Test + fun `includes at least one character from every selected class`() { + // The old implementation sampled uniformly from the union, so a password could contain + // no symbol despite symbols being requested. Run many trials to catch a regression. + repeat(500) { seed -> + val password = generate(length = 8, random = Random(seed.toLong())) + assertTrue("no uppercase in $password", password.any { it in UPPER }) + assertTrue("no lowercase in $password", password.any { it in LOWER }) + assertTrue("no digit in $password", password.any { it in DIGITS }) + assertTrue("no symbol in $password", password.any { it in SPECIAL }) + } + } + + @Test + fun `never emits characters from a deselected class`() { + repeat(200) { seed -> + val password = generate( + length = 12, upper = true, lower = true, + numbers = false, special = false, random = Random(seed.toLong()) + ) + assertTrue("digit leaked into $password", password.none { it in DIGITS }) + assertTrue("symbol leaked into $password", password.none { it in SPECIAL }) + } + } + + @Test + fun `falls back to lowercase when no class is selected`() { + val password = generate( + length = 10, upper = false, lower = false, numbers = false, special = false + ) + assertEquals(10, password.length) + assertTrue("expected lowercase only, got $password", password.all { it in LOWER }) + } + + @Test + fun `length shorter than the number of selected classes is widened to fit them`() { + // Four classes requested but only two characters asked for: honouring "one per class" + // has to win, otherwise the guarantee above is a lie. + val password = generate(length = 2) + assertEquals(4, password.length) + } + + @Test + fun `consecutive calls with the real generator differ`() { + val results = List(1_000) { + PasswordGenerator.generate( + length = 16, + includeUpper = true, includeLower = true, + includeNumbers = true, includeSpecial = true + ) + } + assertEquals("generated passwords repeated", results.size, results.toSet().size) + } + + @Test + fun `first character is not always from the same class`() { + // Regression guard for emitting the per-class characters in a fixed order without + // shuffling, which would make the first character's class fully predictable. + val firstClasses = (0 until 200) + .map { generate(length = 12, random = Random(it.toLong())).first() } + .map { c -> + when (c) { + in UPPER -> "upper"; in LOWER -> "lower"; in DIGITS -> "digit"; else -> "special" + } + } + .toSet() + assertTrue("first character class never varied: $firstClasses", firstClasses.size > 1) + } +} diff --git a/gradlew b/gradlew old mode 100644 new mode 100755