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