diff --git a/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/ui/screen/taskListScreen.android.kt b/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/ui/screen/taskListScreen.android.kt index dbffcfcf..158db792 100644 --- a/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/ui/screen/taskListScreen.android.kt +++ b/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/ui/screen/taskListScreen.android.kt @@ -39,7 +39,7 @@ import net.opatry.tasks.app.ui.component.LoadingPane import net.opatry.tasks.app.ui.component.NoTaskListEmptyState import net.opatry.tasks.app.ui.component.NoTaskListSelectedEmptyState import net.opatry.tasks.resources.Res -import net.opatry.tasks.resources.default_task_list_title +import net.opatry.tasks.resources.task_lists_screen_default_task_list_title import org.jetbrains.compose.resources.stringResource @OptIn(ExperimentalMaterial3AdaptiveApi::class) @@ -69,7 +69,7 @@ actual fun TaskListsMasterDetail( list == null -> LoadingPane() list.isEmpty() -> { - val newTaskListName = stringResource(Res.string.default_task_list_title) + val newTaskListName = stringResource(Res.string.task_lists_screen_default_task_list_title) NoTaskListEmptyState { onNewTaskList(newTaskListName) } diff --git a/tasks-app-shared/src/commonMain/composeResources/values/strings.xml b/tasks-app-shared/src/commonMain/composeResources/values/strings.xml index 12253fa4..e04261a5 100644 --- a/tasks-app-shared/src/commonMain/composeResources/values/strings.xml +++ b/tasks-app-shared/src/commonMain/composeResources/values/strings.xml @@ -23,6 +23,11 @@ Taskfolio + Cancel + + Title + Text cannot be empty + Tasks Calendar Search @@ -33,13 +38,78 @@ Skip Authorize - My tasks + No email information + Sign in and authorize access to your Google Tasks to enable sync. + Sign out + + My tasks + No task list selected + Select a task list to see its tasks No task lists + Create a new task list to get started + New task list + Add task list… No tasks yet Start planning to keep track of your pending tasks. Add task + New task list + Create + + Task deleted + Undo + Task restored + Rename list + Rename + Clear all completed tasks? + All completed tasks will be permanently deleted from this list. + Clear + Delete this list? + All tasks in this list will be permanently deleted. + Delete + All tasks complete + Nice work! + Completed (%1$s) + Delete task + Task options + + %1$s days ago + %1$s weeks ago + Yesterday + Today + Tomorrow + Update + + Edit task + New task + Title + Title cannot be empty + Notes + No due date + List title + Validate + + Sort by + Manual + Due date + Rename + Clear all completed tasks + Delete list + Default list can’t be deleted + + Move to top + Add subtask + Indent + Unindent + Move to… + New list + Delete - Search + Version %1$s + Website + Github + Privacy Policy + Credits - Settings + Credits + Unknown authors \ No newline at end of file diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/TasksApp.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/TasksApp.kt index e11d0a8f..c8f823c3 100644 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/TasksApp.kt +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/TasksApp.kt @@ -61,6 +61,8 @@ import net.opatry.tasks.resources.navigation_about import net.opatry.tasks.resources.navigation_calendar import net.opatry.tasks.resources.navigation_search import net.opatry.tasks.resources.navigation_tasks +import net.opatry.tasks.resources.task_lists_screen_create_task_list_dialog_confirm +import net.opatry.tasks.resources.task_lists_screen_create_task_list_dialog_title import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.stringResource @@ -138,12 +140,12 @@ fun TasksApp(aboutApp: AboutApp, userViewModel: UserViewModel, tasksViewModel: T if (showNewTaskListDialog) { EditTextDialog( onDismissRequest = { showNewTaskListDialog = false }, - validateLabel = "Create", + validateLabel = stringResource(Res.string.task_lists_screen_create_task_list_dialog_confirm), onValidate = { title -> showNewTaskListDialog = false tasksViewModel.createTaskList(title) }, - dialogTitle = "New task list", + dialogTitle = stringResource(Res.string.task_lists_screen_create_task_list_dialog_title), initialText = newTaskListDefaultTitle, allowBlank = false ) diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/EditTextDialog.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/EditTextDialog.kt index 63f817bf..bf36d7fd 100644 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/EditTextDialog.kt +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/EditTextDialog.kt @@ -45,6 +45,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties +import net.opatry.tasks.resources.Res +import net.opatry.tasks.resources.dialog_cancel +import net.opatry.tasks.resources.edit_text_dialog_empty_title_error +import net.opatry.tasks.resources.edit_text_dialog_title +import org.jetbrains.compose.resources.stringResource @Composable fun EditTextDialog( @@ -77,12 +82,12 @@ fun EditTextDialog( OutlinedTextField( newTitle, onValueChange = { newTitle = it }, - label = { Text("Title") }, + label = { Text(stringResource(Res.string.edit_text_dialog_title)) }, maxLines = 1, supportingText = if (allowBlank) null else { { AnimatedVisibility(visible = hasError) { - Text("Text cannot be empty") + Text(stringResource(Res.string.edit_text_dialog_empty_title_error)) } } }, @@ -94,7 +99,7 @@ fun EditTextDialog( horizontalArrangement = Arrangement.spacedBy(8.dp) ) { TextButton(onClick = onDismissRequest) { - Text("Cancel") + Text(stringResource(Res.string.dialog_cancel)) } Button( onClick = { onValidate(newTitle) }, diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskListMenu.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskListMenu.kt index 9da29852..7ef77256 100644 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskListMenu.kt +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskListMenu.kt @@ -51,6 +51,15 @@ import androidx.compose.ui.unit.dp import net.opatry.tasks.app.ui.model.TaskListUIModel import net.opatry.tasks.app.ui.tooling.TaskfolioPreview import net.opatry.tasks.app.ui.tooling.TaskfolioThemedPreview +import net.opatry.tasks.resources.Res +import net.opatry.tasks.resources.task_list_menu_clear_all_completed_tasks +import net.opatry.tasks.resources.task_list_menu_default_list_cannot_be_deleted +import net.opatry.tasks.resources.task_list_menu_delete +import net.opatry.tasks.resources.task_list_menu_rename +import net.opatry.tasks.resources.task_list_menu_sort_by +import net.opatry.tasks.resources.task_list_menu_sort_due_date +import net.opatry.tasks.resources.task_list_menu_sort_manual +import org.jetbrains.compose.resources.stringResource enum class TaskListMenuAction { Dismiss, @@ -71,7 +80,7 @@ fun TaskListMenu(taskList: TaskListUIModel, expanded: Boolean, onAction: (TaskLi ) { DropdownMenuItem( text = { - Text(text = "Sort by", style = MaterialTheme.typography.titleSmall) + Text(stringResource(Res.string.task_list_menu_sort_by), style = MaterialTheme.typography.titleSmall) }, enabled = false, onClick = {} @@ -79,7 +88,9 @@ fun TaskListMenu(taskList: TaskListUIModel, expanded: Boolean, onAction: (TaskLi DropdownMenuItem( text = { - RowWithIcon("Manual", LucideIcons.Check.takeIf { false/*taskList.sorting == TaskListSorting.Manual*/ }) + RowWithIcon( + stringResource(Res.string.task_list_menu_sort_manual), + LucideIcons.Check.takeIf { false/*taskList.sorting == TaskListSorting.Manual*/ }) }, enabled = false, // TODO enable when sorting is implemented onClick = { onAction(TaskListMenuAction.SortManual) } @@ -87,7 +98,9 @@ fun TaskListMenu(taskList: TaskListUIModel, expanded: Boolean, onAction: (TaskLi DropdownMenuItem( text = { - RowWithIcon("Due date", LucideIcons.Check.takeIf { false/*taskList.sorting == TaskListSorting.Date*/ }) + RowWithIcon( + stringResource(Res.string.task_list_menu_sort_due_date), + LucideIcons.Check.takeIf { false/*taskList.sorting == TaskListSorting.Date*/ }) }, enabled = false, // TODO enable when sorting is implemented onClick = { onAction(TaskListMenuAction.SortDate) } @@ -97,14 +110,14 @@ fun TaskListMenu(taskList: TaskListUIModel, expanded: Boolean, onAction: (TaskLi DropdownMenuItem( text = { - Text(text = "Rename") + Text(stringResource(Res.string.task_list_menu_rename)) }, onClick = { onAction(TaskListMenuAction.Rename) } ) DropdownMenuItem( text = { - Text(text = "Clear all completed tasks") + Text(stringResource(Res.string.task_list_menu_clear_all_completed_tasks)) }, enabled = taskList.hasCompletedTasks, onClick = { onAction(TaskListMenuAction.ClearCompletedTasks) } @@ -120,9 +133,12 @@ fun TaskListMenu(taskList: TaskListUIModel, expanded: Boolean, onAction: (TaskLi } CompositionLocalProvider(LocalContentColor provides color) { Column(Modifier.padding(vertical = 4.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { - RowWithIcon("Delete list", LucideIcons.Trash2) + RowWithIcon(stringResource(Res.string.task_list_menu_delete), LucideIcons.Trash2) if (!allowDelete) { - Text("Default list can't be deleted", style = MaterialTheme.typography.bodySmall) + Text( + stringResource(Res.string.task_list_menu_default_list_cannot_be_deleted), + style = MaterialTheme.typography.bodySmall + ) } } } diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskMenu.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskMenu.kt index b3ee20ca..e3bdffe3 100644 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskMenu.kt +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskMenu.kt @@ -43,6 +43,15 @@ import androidx.compose.runtime.remember import androidx.compose.ui.text.style.TextOverflow import net.opatry.tasks.app.ui.model.TaskListUIModel import net.opatry.tasks.app.ui.model.TaskUIModel +import net.opatry.tasks.resources.Res +import net.opatry.tasks.resources.task_menu_add_subtask +import net.opatry.tasks.resources.task_menu_delete +import net.opatry.tasks.resources.task_menu_indent +import net.opatry.tasks.resources.task_menu_move_to +import net.opatry.tasks.resources.task_menu_move_to_top +import net.opatry.tasks.resources.task_menu_new_list +import net.opatry.tasks.resources.task_menu_unindent +import org.jetbrains.compose.resources.stringResource sealed class TaskMenuAction { data object Dismiss : TaskMenuAction() @@ -68,7 +77,7 @@ fun TaskMenu(taskLists: List, task: TaskUIModel, expanded: Bool if (canMoveToTop) { DropdownMenuItem( text = { - RowWithIcon("Move to top") + RowWithIcon(stringResource(Res.string.task_menu_move_to_top)) }, onClick = { onAction(TaskMenuAction.MoveToTop) }, enabled = false @@ -78,7 +87,7 @@ fun TaskMenu(taskLists: List, task: TaskUIModel, expanded: Bool if (task.canCreateSubTask) { DropdownMenuItem( text = { - RowWithIcon("Add subtask", LucideIcons.SquareStack) + RowWithIcon(stringResource(Res.string.task_menu_add_subtask), LucideIcons.SquareStack) }, onClick = { onAction(TaskMenuAction.AddSubTask) }, enabled = false @@ -88,7 +97,7 @@ fun TaskMenu(taskLists: List, task: TaskUIModel, expanded: Bool if (task.canIndent && taskPosition > 0) { DropdownMenuItem( text = { - RowWithIcon("Indent") + RowWithIcon(stringResource(Res.string.task_menu_indent)) }, onClick = { onAction(TaskMenuAction.Indent) }, enabled = false @@ -98,7 +107,7 @@ fun TaskMenu(taskLists: List, task: TaskUIModel, expanded: Bool if (task.canUnindent) { DropdownMenuItem( text = { - RowWithIcon("Unindent") + RowWithIcon(stringResource(Res.string.task_menu_unindent)) }, onClick = { onAction(TaskMenuAction.Unindent) }, enabled = false @@ -109,7 +118,7 @@ fun TaskMenu(taskLists: List, task: TaskUIModel, expanded: Bool DropdownMenuItem( text = { - Text(text = "Move to…", style = MaterialTheme.typography.titleSmall) + Text(stringResource(Res.string.task_menu_move_to), style = MaterialTheme.typography.titleSmall) }, enabled = false, onClick = {} @@ -117,7 +126,7 @@ fun TaskMenu(taskLists: List, task: TaskUIModel, expanded: Bool DropdownMenuItem( text = { - RowWithIcon("New list", LucideIcons.ListPlus) + RowWithIcon(stringResource(Res.string.task_menu_new_list), LucideIcons.ListPlus) }, onClick = { onAction(TaskMenuAction.MoveToNewList) }, enabled = false, @@ -148,7 +157,7 @@ fun TaskMenu(taskLists: List, task: TaskUIModel, expanded: Bool DropdownMenuItem( text = { CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.error) { - RowWithIcon("Delete", LucideIcons.Trash2) + RowWithIcon(stringResource(Res.string.task_menu_delete), LucideIcons.Trash2) } }, onClick = { onAction(TaskMenuAction.Delete) } diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/emptyStates.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/emptyStates.kt index f5757d9a..af7d5a83 100644 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/emptyStates.kt +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/emptyStates.kt @@ -35,13 +35,20 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import net.opatry.tasks.resources.Res +import net.opatry.tasks.resources.task_lists_screen_empty_state_cta +import net.opatry.tasks.resources.task_lists_screen_empty_state_desc +import net.opatry.tasks.resources.task_lists_screen_empty_state_no_selection_desc +import net.opatry.tasks.resources.task_lists_screen_empty_state_no_selection_title +import net.opatry.tasks.resources.task_lists_screen_empty_state_title +import org.jetbrains.compose.resources.stringResource @Composable fun NoTaskListSelectedEmptyState() { EmptyState( icon = LucideIcons.CircleOff, - title = "No task list selected", - description = "Select a task list to see its tasks", + title = stringResource(Res.string.task_lists_screen_empty_state_no_selection_title), + description = stringResource(Res.string.task_lists_screen_empty_state_no_selection_desc), modifier = Modifier.fillMaxSize() ) } @@ -54,12 +61,12 @@ fun NoTaskListEmptyState(onNewTaskListClick: () -> Unit) { ) { EmptyState( icon = LucideIcons.CheckCheck, - title = "No task list", - description = "Create a new task list to get started", + title = stringResource(Res.string.task_lists_screen_empty_state_title), + description = stringResource(Res.string.task_lists_screen_empty_state_desc), modifier = Modifier.fillMaxWidth(1f) ) Button(onClick = onNewTaskListClick) { - Text("New task list") + Text(stringResource(Res.string.task_lists_screen_empty_state_cta)) } } } \ No newline at end of file diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/profileIcon.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/profileIcon.kt index ad1b4fa5..c5ea9dd0 100644 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/profileIcon.kt +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/profileIcon.kt @@ -58,6 +58,11 @@ import androidx.compose.ui.window.Popup import coil3.compose.AsyncImage import net.opatry.tasks.app.ui.UserState import net.opatry.tasks.app.ui.UserViewModel +import net.opatry.tasks.resources.Res +import net.opatry.tasks.resources.profile_popup_no_email +import net.opatry.tasks.resources.profile_popup_sign_explanation +import net.opatry.tasks.resources.profile_popup_sign_out +import org.jetbrains.compose.resources.stringResource @Composable fun ProfileIcon(viewModel: UserViewModel) { @@ -109,7 +114,7 @@ fun ProfileIcon(viewModel: UserViewModel) { is UserState.SignedIn -> { Text(state.name, style = MaterialTheme.typography.titleMedium) Text( - state.email ?: "No email information", + state.email ?: stringResource(Res.string.profile_popup_no_email), style = MaterialTheme.typography.bodySmall, fontFamily = FontFamily.Monospace ) @@ -123,13 +128,13 @@ fun ProfileIcon(viewModel: UserViewModel) { modifier = Modifier.align(Alignment.CenterHorizontally) ) { // TODO confirmation dialog? - Text("Sign out") + Text(stringResource(Res.string.profile_popup_sign_out)) } } UserState.Unsigned -> { Text( - "Sign in and authorize access to your Google Tasks to enable sync.", + stringResource(Res.string.profile_popup_sign_explanation), style = MaterialTheme.typography.bodyMedium ) diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/aboutScreen.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/aboutScreen.kt index f47ea20a..4c687b46 100644 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/aboutScreen.kt +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/aboutScreen.kt @@ -56,11 +56,14 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp +import net.opatry.tasks.resources.Res +import net.opatry.tasks.resources.about_screen_app_version_subtitle +import net.opatry.tasks.resources.about_screen_credits_item +import net.opatry.tasks.resources.about_screen_github_item +import net.opatry.tasks.resources.about_screen_privacy_policy_item +import net.opatry.tasks.resources.about_screen_website_item +import org.jetbrains.compose.resources.stringResource data class AboutApp( @@ -77,31 +80,35 @@ enum class AboutScreenDestination { @Composable expect fun AboutScreen(aboutApp: AboutApp) +private const val TASKFOLIO_WEBSITE_URL = "https://opatry.github.io/taskfolio/" +private const val TASKFOLIO_GITHUB_URL = "https://github.com/opatry/taskfolio" +private const val TASKFOLIO_PRIVACY_POLICY_URL = "https://opatry.github.io/taskfolio/privacy-policy" + @Composable fun AboutScreenContent(aboutApp: AboutApp, onNavigate: (AboutScreenDestination) -> Unit) { val uriHandler = LocalUriHandler.current LazyColumn { item { - AboutExternalLink("Website", LucideIcons.Earth) { - uriHandler.openUri("https://opatry.github.io/taskfolio") + AboutExternalLink(stringResource(Res.string.about_screen_website_item), LucideIcons.Earth) { + uriHandler.openUri(TASKFOLIO_WEBSITE_URL) } } item { - AboutExternalLink("Github", LucideIcons.Github) { - uriHandler.openUri("https://github.com/opatry/taskfolio") + AboutExternalLink(stringResource(Res.string.about_screen_github_item), LucideIcons.Github) { + uriHandler.openUri(TASKFOLIO_GITHUB_URL) } } item { - AboutExternalLink("Privacy Policy", LucideIcons.ShieldCheck) { - uriHandler.openUri("https://opatry.github.io/taskfolio/privacy-policy") + AboutExternalLink(stringResource(Res.string.about_screen_privacy_policy_item), LucideIcons.ShieldCheck) { + uriHandler.openUri(TASKFOLIO_PRIVACY_POLICY_URL) } } item { ListItem( modifier = Modifier.clickable(onClick = { onNavigate(AboutScreenDestination.Credits) }), leadingContent = { Icon(LucideIcons.Copyright, null) }, - headlineContent = { Text("Credits") }, + headlineContent = { Text(stringResource(Res.string.about_screen_credits_item)) }, trailingContent = { Icon(LucideIcons.ChevronRight, null) }, ) } @@ -115,12 +122,8 @@ internal fun AboutScreenTopAppBar(appName: String, appVersion: String) { title = { Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { Text(appName) - Text(buildAnnotatedString { - append("Version ") - withStyle(SpanStyle(fontFamily = FontFamily.Monospace)) { - append(appVersion) - } - }, style = MaterialTheme.typography.labelSmall) + // FIXME How to style only the version part taking into account localization? + Text(stringResource(Res.string.about_screen_app_version_subtitle, appVersion), style = MaterialTheme.typography.labelSmall) } }, navigationIcon = { AppIcon() } diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/creditsScreen.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/creditsScreen.kt index 75a61fde..54f3f0ad 100644 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/creditsScreen.kt +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/creditsScreen.kt @@ -60,9 +60,12 @@ import com.mikepenz.aboutlibraries.entity.Library import com.mikepenz.aboutlibraries.entity.License import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import net.opatry.tasks.resources.Res +import net.opatry.tasks.resources.credits_screen_license_unknown_authors +import net.opatry.tasks.resources.credits_screen_title +import org.jetbrains.compose.resources.stringResource private fun Library.authors(max: Int = 3): String? { - if (developers.size > 1) println("THIS=$uniqueId HAS ${developers.size} DEVELOPERS") return organization?.name ?: developers.mapNotNull(Developer::name) .take(max) @@ -84,7 +87,7 @@ private fun rememberLibraries(block: suspend () -> String): State = produ fun CreditsScreenTopAppBar(onBack: () -> Unit) { TopAppBar( title = { - Text("Credits") + Text(stringResource(Res.string.credits_screen_title)) }, navigationIcon = { IconButton(onClick = onBack) { @@ -119,7 +122,7 @@ fun CreditsScreenContent(librariesJsonProvider: suspend () -> String, modifier: LazyColumn(modifier) { libraryGroups?.forEach { (authors, libs) -> stickyHeader { - LibraryAuthorsRow(authors ?: "Unknown authors") + LibraryAuthorsRow(authors ?: stringResource(Res.string.credits_screen_license_unknown_authors)) } items(libs) { lib -> LibraryRow(lib, uriHandler::openUri) diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/searchScreen.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/searchScreen.kt deleted file mode 100644 index b8859d0c..00000000 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/searchScreen.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (c) 2024 Olivier Patry - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the Software - * is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, - * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE - * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package net.opatry.tasks.app.ui.screen - -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import net.opatry.tasks.resources.Res -import net.opatry.tasks.resources.search_screen_tbd -import org.jetbrains.compose.resources.stringResource - - -@Composable -fun SearchScreen() { - Text(stringResource(Res.string.search_screen_tbd)) -} \ No newline at end of file diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/taskListsPane.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/taskListsPane.kt index 69b62959..c59738c6 100644 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/taskListsPane.kt +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/taskListsPane.kt @@ -53,6 +53,9 @@ import net.opatry.tasks.app.ui.component.RowWithIcon import net.opatry.tasks.app.ui.model.TaskListUIModel import net.opatry.tasks.app.ui.tooling.TaskfolioPreview import net.opatry.tasks.app.ui.tooling.TaskfolioThemedPreview +import net.opatry.tasks.resources.Res +import net.opatry.tasks.resources.task_lists_screen_add_task_list_cta +import org.jetbrains.compose.resources.stringResource @OptIn(ExperimentalFoundationApi::class) @@ -76,7 +79,7 @@ fun TaskListsColumn( TextButton( onClick = onNewTaskList, ) { - RowWithIcon("Add task list…", LucideIcons.CircleFadingPlus) + RowWithIcon(stringResource(Res.string.task_lists_screen_add_task_list_cta), LucideIcons.CircleFadingPlus) } } diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/tasksPane.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/tasksPane.kt index 1363b7b6..e4d7f4d1 100644 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/tasksPane.kt +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/tasksPane.kt @@ -94,10 +94,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.PlatformImeOptions import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -130,6 +128,37 @@ import net.opatry.tasks.app.ui.model.TaskUIModel import net.opatry.tasks.app.ui.tooling.TaskfolioPreview import net.opatry.tasks.app.ui.tooling.TaskfolioThemedPreview import net.opatry.tasks.resources.Res +import net.opatry.tasks.resources.dialog_cancel +import net.opatry.tasks.resources.task_due_date_label_days_ago +import net.opatry.tasks.resources.task_due_date_label_today +import net.opatry.tasks.resources.task_due_date_label_tomorrow +import net.opatry.tasks.resources.task_due_date_label_weeks_ago +import net.opatry.tasks.resources.task_due_date_label_yesterday +import net.opatry.tasks.resources.task_due_date_update_cta +import net.opatry.tasks.resources.task_editor_sheet_edit_title +import net.opatry.tasks.resources.task_editor_sheet_list_dropdown_label +import net.opatry.tasks.resources.task_editor_sheet_new_title +import net.opatry.tasks.resources.task_editor_sheet_no_due_date_fallback +import net.opatry.tasks.resources.task_editor_sheet_notes_field_label +import net.opatry.tasks.resources.task_editor_sheet_title_field_empty_error +import net.opatry.tasks.resources.task_editor_sheet_title_field_label +import net.opatry.tasks.resources.task_editor_sheet_validate +import net.opatry.tasks.resources.task_list_pane_all_tasks_complete_desc +import net.opatry.tasks.resources.task_list_pane_all_tasks_complete_title +import net.opatry.tasks.resources.task_list_pane_clear_completed_confirm_dialog_confirm +import net.opatry.tasks.resources.task_list_pane_clear_completed_confirm_dialog_message +import net.opatry.tasks.resources.task_list_pane_clear_completed_confirm_dialog_title +import net.opatry.tasks.resources.task_list_pane_completed_section_title_with_count +import net.opatry.tasks.resources.task_list_pane_delete_list_confirm_dialog_confirm +import net.opatry.tasks.resources.task_list_pane_delete_list_confirm_dialog_message +import net.opatry.tasks.resources.task_list_pane_delete_list_confirm_dialog_title +import net.opatry.tasks.resources.task_list_pane_delete_task_icon_content_desc +import net.opatry.tasks.resources.task_list_pane_rename_dialog_cta +import net.opatry.tasks.resources.task_list_pane_rename_dialog_title +import net.opatry.tasks.resources.task_list_pane_task_deleted_snackbar +import net.opatry.tasks.resources.task_list_pane_task_deleted_undo_snackbar +import net.opatry.tasks.resources.task_list_pane_task_options_icon_content_desc +import net.opatry.tasks.resources.task_list_pane_task_restored_snackbar import net.opatry.tasks.resources.task_lists_screen_empty_list_desc import net.opatry.tasks.resources.task_lists_screen_empty_list_title import org.jetbrains.compose.resources.stringResource @@ -162,11 +191,14 @@ fun TaskListDetail( val enableUndoTaskDeletion = false if (enableUndoTaskDeletion && showUndoTaskDeletionSnackbar) { + val taskDeletedMessage = stringResource(Res.string.task_list_pane_task_deleted_snackbar) + val taskDeletedUndo = stringResource(Res.string.task_list_pane_task_deleted_undo_snackbar) + val taskRestoredMessage = stringResource(Res.string.task_list_pane_task_restored_snackbar) LaunchedEffect(Unit) { taskOfInterest?.let { task -> val result = snackbarHostState.showSnackbar( - message = "Task deleted", - actionLabel = "Undo", + message = taskDeletedMessage, + actionLabel = taskDeletedUndo, duration = SnackbarDuration.Short ) taskOfInterest = null @@ -175,7 +207,7 @@ fun TaskListDetail( SnackbarResult.Dismissed -> viewModel.confirmTaskDeletion(task) SnackbarResult.ActionPerformed -> { viewModel.restoreTask(task) - snackbarHostState.showSnackbar("Task restored") + snackbarHostState.showSnackbar(taskRestoredMessage, duration = SnackbarDuration.Short) } } } @@ -269,8 +301,8 @@ fun TaskListDetail( showRenameTaskListDialog = false viewModel.renameTaskList(taskList, newTitle) }, - validateLabel = "Rename", - dialogTitle = "Rename list", + validateLabel = stringResource(Res.string.task_list_pane_rename_dialog_cta), + dialogTitle = stringResource(Res.string.task_list_pane_rename_dialog_title), initialText = taskList.title, allowBlank = false, ) @@ -280,14 +312,14 @@ fun TaskListDetail( AlertDialog( onDismissRequest = { showClearTaskListCompletedTasksDialog = false }, title = { - Text("Clear all completed tasks?") + Text(stringResource(Res.string.task_list_pane_clear_completed_confirm_dialog_title)) }, text = { - Text("All completed tasks will be permanently deleted from this list.") + Text(stringResource(Res.string.task_list_pane_clear_completed_confirm_dialog_message)) }, dismissButton = { TextButton(onClick = { showClearTaskListCompletedTasksDialog = false }) { - Text("Cancel") + Text(stringResource(Res.string.dialog_cancel)) } }, confirmButton = { @@ -295,7 +327,7 @@ fun TaskListDetail( showClearTaskListCompletedTasksDialog = false viewModel.clearTaskListCompletedTasks(taskList) }) { - Text("Clear") + Text(stringResource(Res.string.task_list_pane_clear_completed_confirm_dialog_confirm)) } }, ) @@ -305,14 +337,14 @@ fun TaskListDetail( AlertDialog( onDismissRequest = { showDeleteTaskListDialog = false }, title = { - Text("Delete this list?") + Text(stringResource(Res.string.task_list_pane_delete_list_confirm_dialog_title)) }, text = { - Text("All tasks in this list will be permanently deleted.") + Text(stringResource(Res.string.task_list_pane_delete_list_confirm_dialog_message)) }, dismissButton = { TextButton(onClick = { showDeleteTaskListDialog = false }) { - Text("Cancel") + Text(stringResource(Res.string.dialog_cancel)) } }, confirmButton = { @@ -321,7 +353,7 @@ fun TaskListDetail( viewModel.deleteTaskList(taskList) onNavigateTo(null) }) { - Text("Delete") + Text(stringResource(Res.string.task_list_pane_delete_list_confirm_dialog_confirm)) } }, ) @@ -338,7 +370,7 @@ fun TaskListDetail( showNewTaskSheet = false } ) { - val sheetTitle = if (showEditTaskSheet) "Edit task" else "New task" + val sheetTitleRes = if (showEditTaskSheet) Res.string.task_editor_sheet_edit_title else Res.string.task_editor_sheet_new_title var newTitle by remember { mutableStateOf(task?.title ?: "") } val titleHasError by remember { derivedStateOf { @@ -361,17 +393,17 @@ fun TaskListDetail( .verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(8.dp) ) { - Text(sheetTitle, style = MaterialTheme.typography.titleLarge) + Text(stringResource(sheetTitleRes), style = MaterialTheme.typography.titleLarge) OutlinedTextField( newTitle, onValueChange = { newTitle = it }, modifier = Modifier.fillMaxWidth(), - label = { Text("Title") }, + label = { Text(stringResource(Res.string.task_editor_sheet_title_field_label)) }, maxLines = 1, supportingText = { AnimatedVisibility(visible = titleHasError) { - Text("Title cannot be empty") + Text(stringResource(Res.string.task_editor_sheet_title_field_empty_error)) } }, keyboardOptions = KeyboardOptions( @@ -385,7 +417,7 @@ fun TaskListDetail( newNotes, onValueChange = { newNotes = it }, modifier = Modifier.fillMaxWidth(), - label = { Text("Notes") }, + label = { Text(stringResource(Res.string.task_editor_sheet_notes_field_label)) }, leadingIcon = { Icon(LucideIcons.NotepadText, null) }, singleLine = false, minLines = 2, @@ -397,7 +429,8 @@ fun TaskListDetail( Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { // TODO Add shortcuts for Today, Tomorrow, Next Week // FIXME when dialog is dismissed, current state is reset but shouldn't need to extract date picker dialog use - val dueDateLabel = task?.dateRange?.toLabel()?.takeUnless(String::isBlank) ?: "No due date" + val dueDateLabel = task?.dateRange?.toLabel()?.takeUnless(String::isBlank) + ?: stringResource(Res.string.task_editor_sheet_no_due_date_fallback) AssistChip( onClick = { showDatePickerDialog = true }, enabled = false, // TODO not supported for now, super imposed dialogs breaks the flow @@ -416,7 +449,7 @@ fun TaskListDetail( targetList.title, onValueChange = {}, modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable, true), - label = { Text("List title") }, + label = { Text(stringResource(Res.string.task_editor_sheet_list_dropdown_label)) }, readOnly = true, trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expandTaskListsDropDown) @@ -457,7 +490,7 @@ fun TaskListDetail( showEditTaskSheet = false showNewTaskSheet = false }) { - Text("Cancel") + Text(stringResource(Res.string.dialog_cancel)) } Button( onClick = { @@ -476,7 +509,7 @@ fun TaskListDetail( }, enabled = newTitle.isNotBlank() ) { - Text("Validate") + Text(stringResource(Res.string.task_editor_sheet_validate)) } } } @@ -496,7 +529,7 @@ fun TaskListDetail( taskOfInterest = null showDatePickerDialog = false }) { - Text("Cancel") + Text(stringResource(Res.string.dialog_cancel)) } }, confirmButton = { @@ -509,7 +542,7 @@ fun TaskListDetail( ?.date viewModel.updateTaskDueDate(task, dueDate = newDate) }) { - Text("Update") + Text(stringResource(Res.string.task_due_date_update_cta)) } }, ) { @@ -568,8 +601,8 @@ fun TasksColumn( item { EmptyState( icon = LucideIcons.CheckCheck, - title = "All tasks complete", - description = "Nice work!", + title = stringResource(Res.string.task_list_pane_all_tasks_complete_title), + description = stringResource(Res.string.task_list_pane_all_tasks_complete_desc), modifier = Modifier.fillMaxWidth().padding(vertical = 24.dp), ) } @@ -595,7 +628,7 @@ fun TasksColumn( } ) { Text( - "Completed (${completedCount})", + stringResource(Res.string.task_list_pane_completed_section_title_with_count, completedCount), style = MaterialTheme.typography.titleSmall ) } @@ -637,17 +670,16 @@ fun DateRange?.toColor(): Color = when (this) { @Composable fun DateRange.toLabel(): String = when (this) { is DateRange.Overdue -> { - // TODO string resources with quantity if (numberOfDays < 7) { - "$numberOfDays days ago" + stringResource(Res.string.task_due_date_label_days_ago, numberOfDays) } else { - "${numberOfDays / 7} weeks ago" + stringResource(Res.string.task_due_date_label_weeks_ago, numberOfDays / 7) } } - DateRange.Yesterday -> "Yesterday" - DateRange.Today -> "Today" - DateRange.Tomorrow -> "Tomorrow" + DateRange.Yesterday -> stringResource(Res.string.task_due_date_label_yesterday) + DateRange.Today -> stringResource(Res.string.task_due_date_label_today) + DateRange.Tomorrow -> stringResource(Res.string.task_due_date_label_tomorrow) // TODO localize names & format is DateRange.Later -> LocalDate.Format { if (date.year == Clock.System.todayIn(TimeZone.currentSystemDefault()).year) { @@ -733,12 +765,12 @@ fun TaskRow( } if (task.isCompleted) { IconButton(onClick = onDeleteTask) { - Icon(LucideIcons.Trash, "Delete task") + Icon(LucideIcons.Trash, stringResource(Res.string.task_list_pane_delete_task_icon_content_desc)) } } else { Box { IconButton(onClick = { showContextualMenu = true }) { - Icon(LucideIcons.EllipsisVertical, "Task options") + Icon(LucideIcons.EllipsisVertical, stringResource(Res.string.task_list_pane_task_options_icon_content_desc)) } TaskMenu(taskLists, task, showContextualMenu) { action -> showContextualMenu = false diff --git a/tasks-app-shared/src/jvmMain/kotlin/net/opatry/tasks/app/ui/screen/taskListScreen.jvm.kt b/tasks-app-shared/src/jvmMain/kotlin/net/opatry/tasks/app/ui/screen/taskListScreen.jvm.kt index 04d69fcd..a06ffd3b 100644 --- a/tasks-app-shared/src/jvmMain/kotlin/net/opatry/tasks/app/ui/screen/taskListScreen.jvm.kt +++ b/tasks-app-shared/src/jvmMain/kotlin/net/opatry/tasks/app/ui/screen/taskListScreen.jvm.kt @@ -38,7 +38,7 @@ import net.opatry.tasks.app.ui.component.LoadingPane import net.opatry.tasks.app.ui.component.NoTaskListEmptyState import net.opatry.tasks.app.ui.component.NoTaskListSelectedEmptyState import net.opatry.tasks.resources.Res -import net.opatry.tasks.resources.default_task_list_title +import net.opatry.tasks.resources.task_lists_screen_default_task_list_title import org.jetbrains.compose.resources.stringResource @Composable @@ -58,7 +58,7 @@ actual fun TaskListsMasterDetail( list == null -> LoadingPane() list.isEmpty() -> { - val newTaskListName = stringResource(Res.string.default_task_list_title) + val newTaskListName = stringResource(Res.string.task_lists_screen_default_task_list_title) NoTaskListEmptyState { onNewTaskList(newTaskListName) }