diff --git a/app/src/main/java/de/psdev/devdrawer/DevDrawerApp.kt b/app/src/main/java/de/psdev/devdrawer/DevDrawerApp.kt index e10ace2b..0992fe83 100644 --- a/app/src/main/java/de/psdev/devdrawer/DevDrawerApp.kt +++ b/app/src/main/java/de/psdev/devdrawer/DevDrawerApp.kt @@ -69,6 +69,8 @@ fun DevDrawerApp( } val snackbarHostState = remember { SnackbarHostState() } + val optInThanksMessage = stringResource(id = R.string.analytics_opt_in_thanks) + val okLabel = stringResource(id = R.string.ok) val scope = rememberCoroutineScope() DevDrawerTheme( @@ -114,8 +116,8 @@ fun DevDrawerApp( trackingService.optIn() scope.launch { snackbarHostState.showSnackbar( - message = "Thank you! You can change your decision anytime on the settings tab.", - actionLabel = "OK", + message = optInThanksMessage, + actionLabel = okLabel, duration = SnackbarDuration.Long ) } @@ -195,23 +197,17 @@ fun AnalyticsOptInDialog( AlertDialog( onDismissRequest = { /* Not cancelable */ }, title = { - Text(text = "Usage analytics", fontWeight = FontWeight.Bold) + Text(text = stringResource(id = R.string.analytics_opt_in_title), fontWeight = FontWeight.Bold) }, text = { - Text( - text = "In order for us to be able to better understand your use of the app we would like to analyse your usage.\n\n" + - "We use Firebase Analytics to track opened screens and certain interactions.\n\n" + - "We don't store personally identifiable data.\n\n" + - "Additionally we use Firebase Crashlytics for app crashes.\n\n" + - "Thank you for considering!" - ) + Text(text = stringResource(id = R.string.analytics_opt_in_message)) }, confirmButton = { TextButton( enabled = buttonsEnabled, onClick = onOptIn ) { - Text("Opt-in") + Text(stringResource(id = R.string.analytics_opt_in)) } }, dismissButton = { @@ -219,7 +215,7 @@ fun AnalyticsOptInDialog( enabled = buttonsEnabled, onClick = onOptOut ) { - Text("Opt-out") + Text(stringResource(id = R.string.analytics_opt_out)) } } ) diff --git a/app/src/main/java/de/psdev/devdrawer/appwidget/ChooseActivityDialog.kt b/app/src/main/java/de/psdev/devdrawer/appwidget/ChooseActivityDialog.kt index bfbce4a2..aeacc3fb 100644 --- a/app/src/main/java/de/psdev/devdrawer/appwidget/ChooseActivityDialog.kt +++ b/app/src/main/java/de/psdev/devdrawer/appwidget/ChooseActivityDialog.kt @@ -9,6 +9,7 @@ import android.os.Bundle import android.view.View import android.widget.AdapterView import android.widget.Toast +import de.psdev.devdrawer.R import de.psdev.devdrawer.adapters.ActivityListAdapter import de.psdev.devdrawer.databinding.ActivityChoiceBinding import mu.KLogging @@ -46,7 +47,7 @@ class ChooseActivityDialog: Activity(), AdapterView.OnItemClickListener { finish() } if (activitiesList.isEmpty()) { - Toast.makeText(this, "This app has no activities", Toast.LENGTH_LONG).show() + Toast.makeText(this, R.string.no_activities, Toast.LENGTH_LONG).show() finish() } binding.listView.adapter = ActivityListAdapter(this, activitiesList) diff --git a/app/src/main/java/de/psdev/devdrawer/profiles/WidgetInUseErrorAlertDialog.kt b/app/src/main/java/de/psdev/devdrawer/profiles/WidgetInUseErrorAlertDialog.kt index 3965e8ce..ce743c96 100644 --- a/app/src/main/java/de/psdev/devdrawer/profiles/WidgetInUseErrorAlertDialog.kt +++ b/app/src/main/java/de/psdev/devdrawer/profiles/WidgetInUseErrorAlertDialog.kt @@ -15,10 +15,16 @@ fun WidgetInUseErrorAlertDialog( AlertDialog( onDismissRequest = { }, title = { - Text(text = "Error") + Text(text = stringResource(id = R.string.error_profile_in_use)) }, text = { - Text(text = "The profile ${state.widgetProfile.name} is used by: \n" + state.widgets.joinToString("\n") { it.name }) + Text( + text = stringResource( + id = R.string.profile_used_by_widgets, + state.widgetProfile.name, + state.widgets.joinToString("\n") { it.name } + ) + ) }, confirmButton = { TextButton(onClick = { diff --git a/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/AddAppSignaturePackageFilterDialog.kt b/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/AddAppSignaturePackageFilterDialog.kt index 66d4694d..ac91b456 100644 --- a/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/AddAppSignaturePackageFilterDialog.kt +++ b/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/AddAppSignaturePackageFilterDialog.kt @@ -140,7 +140,7 @@ internal fun AddAppSignaturePackageFilterDialog( } } - is ViewState.Error -> Text(text = "Error: ${viewState.message}") + is ViewState.Error -> Text(text = stringResource(id = R.string.error_message, viewState.message)) } } } diff --git a/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/AddPackageNamePackageFilterDialog.kt b/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/AddPackageNamePackageFilterDialog.kt index ce5b472e..fed7f656 100644 --- a/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/AddPackageNamePackageFilterDialog.kt +++ b/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/AddPackageNamePackageFilterDialog.kt @@ -104,7 +104,7 @@ internal fun AddPackageNamePackageFilterDialog( } } } - is Error -> Text(text = "Error: ${viewState.message}") + is Error -> Text(text = stringResource(id = R.string.error_message, viewState.message)) } } } diff --git a/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/PackageFilterPreviewDialog.kt b/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/PackageFilterPreviewDialog.kt index 5ee16f6b..ddca646e 100644 --- a/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/PackageFilterPreviewDialog.kt +++ b/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/PackageFilterPreviewDialog.kt @@ -83,7 +83,7 @@ internal fun PackageFilterPreviewDialog( } } - is Error -> Text(text = "Error: ${viewState.message}") + is Error -> Text(text = stringResource(id = R.string.error_message, viewState.message)) } } } diff --git a/app/src/main/java/de/psdev/devdrawer/profiles/ui/list/WidgetProfilesListScreen.kt b/app/src/main/java/de/psdev/devdrawer/profiles/ui/list/WidgetProfilesListScreen.kt index 4349d68c..5451b840 100644 --- a/app/src/main/java/de/psdev/devdrawer/profiles/ui/list/WidgetProfilesListScreen.kt +++ b/app/src/main/java/de/psdev/devdrawer/profiles/ui/list/WidgetProfilesListScreen.kt @@ -68,29 +68,13 @@ fun WidgetProfilesScreen( ) when (val state = deleteDialogShown) { DeleteDialogState.Hidden -> Unit - is DeleteDialogState.Showing -> AlertDialog( - onDismissRequest = { deleteDialogShown = DeleteDialogState.Hidden }, - title = { - Text(text = "Confirm") - }, - text = { - Text(text = "Do you really want to delete the profile '${state.widgetProfile.name}'?") - }, - confirmButton = { - TextButton(onClick = { - viewModel.deleteProfile(state.widgetProfile) - deleteDialogShown = DeleteDialogState.Hidden - }) { - Text("Delete") - } + is DeleteDialogState.Showing -> DeleteProfileDialog( + widgetProfile = state.widgetProfile, + onConfirm = { + viewModel.deleteProfile(state.widgetProfile) + deleteDialogShown = DeleteDialogState.Hidden }, - dismissButton = { - TextButton(onClick = { - deleteDialogShown = DeleteDialogState.Hidden - }) { - Text("Cancel") - } - } + onDismiss = { deleteDialogShown = DeleteDialogState.Hidden } ) is DeleteDialogState.InUseError -> { @@ -101,6 +85,33 @@ fun WidgetProfilesScreen( } } +@Composable +internal fun DeleteProfileDialog( + widgetProfile: WidgetProfile, + onConfirm: () -> Unit, + onDismiss: () -> Unit +) { + AlertDialog( + onDismissRequest = onDismiss, + title = { + Text(text = stringResource(id = R.string.delete_profile)) + }, + text = { + Text(text = stringResource(id = R.string.delete_profile_confirmation, widgetProfile.name)) + }, + confirmButton = { + TextButton(onClick = onConfirm) { + Text(stringResource(id = R.string.delete)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(id = R.string.cancel)) + } + } + ) +} + @Composable fun WidgetProfileListScreen( viewState: UiState>, diff --git a/app/src/main/java/de/psdev/devdrawer/settings/ListPreference.kt b/app/src/main/java/de/psdev/devdrawer/settings/ListPreference.kt index 61b971c8..b13d5923 100644 --- a/app/src/main/java/de/psdev/devdrawer/settings/ListPreference.kt +++ b/app/src/main/java/de/psdev/devdrawer/settings/ListPreference.kt @@ -39,7 +39,7 @@ fun ListPreference( label: String, values: Map, currentValue: T, - dialogTitle: String = "Select option", + dialogTitle: String = stringResource(id = R.string.select_option), onClick: (T) -> Unit = {} ) { var selectionDialog by remember { diff --git a/app/src/main/java/de/psdev/devdrawer/widgets/ui/editor/ColorSelectionDialog.kt b/app/src/main/java/de/psdev/devdrawer/widgets/ui/editor/ColorSelectionDialog.kt index 5be8dd6c..a75e0d04 100644 --- a/app/src/main/java/de/psdev/devdrawer/widgets/ui/editor/ColorSelectionDialog.kt +++ b/app/src/main/java/de/psdev/devdrawer/widgets/ui/editor/ColorSelectionDialog.kt @@ -22,7 +22,7 @@ fun ColorSelectionDialog( AlertDialog( onDismissRequest = { }, title = { - Text(text = "Select color") + Text(text = stringResource(id = R.string.pick_widget_color)) }, text = { ColorGrid( diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 7e763925..6c88adec 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -77,4 +77,15 @@ Hell Dunkel + + Option auswählen + Nutzungsanalyse + Damit wir besser verstehen, wie du die App verwendest, würden wir deine Nutzung gerne analysieren.\n\nWir verwenden Firebase Analytics, um geöffnete Bildschirme und bestimmte Interaktionen zu erfassen.\n\nWir speichern keine personenbezogenen Daten.\n\nZusätzlich verwenden wir Firebase Crashlytics für App-Abstürze.\n\nDanke, dass du darüber nachdenkst! + Zustimmen + Ablehnen + Danke! Du kannst deine Entscheidung jederzeit im Einstellungen-Tab ändern. + Möchtest du das Profil \'%1$s\' wirklich löschen? + Fehler: %1$s + Das Profil %1$s wird verwendet von: \n%2$s + Diese App hat keine Activities diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e1d47c85..70c8dd75 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -116,4 +116,16 @@ Discard changes? You have unsaved changes. Do you want to discard them? Discard + + + Select option + Usage analytics + In order for us to be able to better understand your use of the app we would like to analyse your usage.\n\nWe use Firebase Analytics to track opened screens and certain interactions.\n\nWe don\'t store personally identifiable data.\n\nAdditionally we use Firebase Crashlytics for app crashes.\n\nThank you for considering! + Opt-in + Opt-out + Thank you! You can change your decision anytime on the settings tab. + Do you really want to delete the profile \'%1$s\'? + Error: %1$s + The profile %1$s is used by: \n%2$s + This app has no activities diff --git a/app/src/test/java/de/psdev/devdrawer/DialogStringResourcesTest.kt b/app/src/test/java/de/psdev/devdrawer/DialogStringResourcesTest.kt new file mode 100644 index 00000000..cfd314f3 --- /dev/null +++ b/app/src/test/java/de/psdev/devdrawer/DialogStringResourcesTest.kt @@ -0,0 +1,137 @@ +package de.psdev.devdrawer + +import android.graphics.Color +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import de.psdev.devdrawer.appwidget.SortOrder +import de.psdev.devdrawer.database.Widget +import de.psdev.devdrawer.database.WidgetProfile +import de.psdev.devdrawer.profiles.DeleteDialogState +import de.psdev.devdrawer.profiles.WidgetInUseErrorAlertDialog +import de.psdev.devdrawer.profiles.ui.editor.AddAppSignaturePackageFilterDialog +import de.psdev.devdrawer.profiles.ui.editor.AddAppSignaturePackageFilterDialogViewModel +import de.psdev.devdrawer.profiles.ui.editor.AddPackageNamePackageFilterDialog +import de.psdev.devdrawer.profiles.ui.editor.AddPackageNamePackageFilterDialogViewModel +import de.psdev.devdrawer.profiles.ui.editor.PackageFilterPreviewDialog +import de.psdev.devdrawer.profiles.ui.editor.PackageFilterPreviewDialogViewModel +import de.psdev.devdrawer.profiles.ui.list.DeleteProfileDialog +import de.psdev.devdrawer.settings.ListPreference +import de.psdev.devdrawer.ui.theme.DevDrawerTheme +import de.psdev.devdrawer.widgets.ui.editor.ColorSelectionDialog +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** + * Dialog text comes from string resources, so it is translated: under a German locale every dialog reads German. + */ +@RunWith(RobolectricTestRunner::class) +// SDK 36 requires Java 21; use SDK 33 which is compatible with the project's Java 17 toolchain. +@Config(sdk = [33], qualifiers = "de") +class DialogStringResourcesTest { + + @get:Rule + val composeTestRule = createComposeRule() + + private val widgetProfile = WidgetProfile(name = "Arbeit") + + @Test + fun `given a German locale, when showing the analytics opt-in dialog, then it reads German`() { + // Given / When + composeTestRule.setContent { + DevDrawerTheme { AnalyticsOptInDialog(onOptIn = {}, onOptOut = {}) } + } + + // Then + assertTexts("Nutzungsanalyse", "Zustimmen", "Ablehnen") + composeTestRule.onNodeWithText("Wir verwenden Firebase Analytics", substring = true).assertExists() + } + + @Test + fun `given a German locale, when showing the delete profile dialog, then it reads German`() { + // Given / When + composeTestRule.setContent { + DevDrawerTheme { DeleteProfileDialog(widgetProfile = widgetProfile, onConfirm = {}, onDismiss = {}) } + } + + // Then + assertTexts("Profil löschen?", "Möchtest du das Profil 'Arbeit' wirklich löschen?", "Löschen", "Abbrechen") + } + + @Test + fun `given a German locale, when showing the profile in use error, then it reads German`() { + // Given + val state = DeleteDialogState.InUseError( + widgetProfile = widgetProfile, + widgets = listOf(Widget(1, "Widget 1", Color.BLACK, widgetProfile.id)) + ) + + // When + composeTestRule.setContent { + DevDrawerTheme { WidgetInUseErrorAlertDialog(state = state) } + } + + // Then + assertTexts("Profil kann nicht gelöscht werden, da es noch von Widgets verwendet wird", "Schließen") + composeTestRule.onNodeWithText("Das Profil Arbeit wird verwendet von:", substring = true).assertExists() + } + + @Test + fun `given a German locale, when showing the color selection dialog, then it reads German`() { + // Given / When + composeTestRule.setContent { + DevDrawerTheme { ColorSelectionDialog(initialColor = Color.BLACK) } + } + + // Then + assertTexts("Widget-Farbe wählen") + } + + @Test + fun `given a German locale, when opening a list preference, then the default dialog title reads German`() { + // Given + composeTestRule.setContent { + DevDrawerTheme { + ListPreference( + label = "Sortierung", + values = mapOf(SortOrder.LAST_UPDATED to "Zuletzt aktualisiert"), + currentValue = SortOrder.LAST_UPDATED + ) + } + } + + // When + composeTestRule.onNodeWithText("Sortierung").performClick() + + // Then + assertTexts("Option auswählen") + } + + @Test + fun `given a German locale, when a filter dialog fails to load, then the error reads German`() { + // Given / When + composeTestRule.setContent { + DevDrawerTheme { + AddAppSignaturePackageFilterDialog( + viewState = AddAppSignaturePackageFilterDialogViewModel.ViewState.Error("kaputt") + ) + AddPackageNamePackageFilterDialog( + viewState = AddPackageNamePackageFilterDialogViewModel.ViewState.Error("defekt") + ) + PackageFilterPreviewDialog( + viewState = PackageFilterPreviewDialogViewModel.ViewState.Error("weg") + ) + } + } + + // Then + assertTexts("Fehler: kaputt", "Fehler: defekt", "Fehler: weg") + } + + private fun assertTexts(vararg texts: String) { + texts.forEach { composeTestRule.onNodeWithText(it).assertExists() } + } +}