diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatBubble.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatBubble.kt index 0d6c6654..036bda66 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatBubble.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatBubble.kt @@ -54,6 +54,7 @@ import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString @@ -64,6 +65,7 @@ import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.m57.hermescontrol.R import com.m57.hermescontrol.theme.AssistantBubble import com.m57.hermescontrol.theme.AssistantBubbleLight import com.m57.hermescontrol.theme.StatusGreen @@ -232,7 +234,7 @@ private fun UserBubble( ) { Icon( Icons.Filled.ContentCopy, - contentDescription = "Copy", + contentDescription = stringResource(R.string.content_desc_copy), modifier = Modifier.size(16.dp), tint = MaterialTheme.colorScheme.onSurface, ) @@ -347,7 +349,7 @@ private fun AssistantBubble( ) { Icon( Icons.Filled.ContentCopy, - contentDescription = "Copy", + contentDescription = stringResource(R.string.content_desc_copy), modifier = Modifier.size(16.dp), tint = MaterialTheme.colorScheme.onSurface, ) @@ -489,7 +491,7 @@ private fun ParsedToolContent( } parsed.stderr?.let { Text( - text = "Error Output (stderr):\n$it", + text = stringResource(R.string.chat_tool_stderr, it), style = MaterialTheme.typography.bodySmall.copy( color = StatusRed.copy(alpha = 0.9f), @@ -500,7 +502,7 @@ private fun ParsedToolContent( } parsed.error?.let { Text( - text = "Execution Error: $it", + text = stringResource(R.string.chat_tool_execution_error, it), style = MaterialTheme.typography.bodySmall.copy( color = StatusRed, @@ -513,7 +515,7 @@ private fun ParsedToolContent( parsed.exitCode?.let { code -> if (code != 0) { Text( - text = "Exit Code: $code", + text = stringResource(R.string.chat_tool_exit_code, code), style = MaterialTheme.typography.labelSmall.copy( color = StatusRed, @@ -619,7 +621,7 @@ private fun ToolBubble( } Text( - text = message.toolName ?: "Tool", + text = message.toolName ?: stringResource(R.string.chat_tool_fallback), style = MaterialTheme.typography.labelMedium.copy( color = contentColor, @@ -635,7 +637,7 @@ private fun ToolBubble( Spacer(modifier = Modifier.height(8.dp)) Text( - text = "Show Raw JSON", + text = stringResource(R.string.chat_tool_show_raw), style = MaterialTheme.typography.labelSmall.copy( color = MaterialTheme.colorScheme.primary, @@ -660,7 +662,7 @@ private fun ToolBubble( if (parsedOutput != null) { Spacer(modifier = Modifier.height(8.dp)) Text( - text = "Show Parsed Output", + text = stringResource(R.string.chat_tool_show_parsed), style = MaterialTheme.typography.labelSmall.copy( color = MaterialTheme.colorScheme.primary, diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt index e0de3b84..1ddf7929 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt @@ -293,14 +293,20 @@ fun ChatScreen( imageVector = if (state.isSearchActive) Icons.Filled.Close else Icons.Filled.Search, contentDescription = - if (state.isSearchActive) "Close search" else "Search", + if (state.isSearchActive) { + stringResource( + R.string.chat_action_close_search, + ) + } else { + stringResource(R.string.chat_action_search) + }, ) } IconButton(onClick = { viewModel.createNewSession() }) { Icon( imageVector = Icons.Filled.Add, - contentDescription = "New Chat", + contentDescription = stringResource(R.string.content_desc_new_chat), ) } }, @@ -335,9 +341,9 @@ fun ChatScreen( Text( text = if (state.connectionStatus == ConnectionStatus.RECONNECTING) { - "Reconnecting to Hermes…" + stringResource(R.string.chat_status_reconnecting) } else { - "Disconnected from Hermes" + stringResource(R.string.chat_status_disconnected) }, style = MaterialTheme.typography.bodyMedium, ) @@ -349,7 +355,7 @@ fun ChatScreen( contentColor = MaterialTheme.colorScheme.onErrorContainer, ), ) { - Text("Reconnect") + Text(stringResource(R.string.chat_action_reconnect)) } } } @@ -393,8 +399,8 @@ fun ChatScreen( ) { if (state.messages.isEmpty() && !state.isLoading) { EmptyState( - title = "Ready to chat", - subtitle = "Send a message to start a conversation", + title = stringResource(R.string.chat_empty_title), + subtitle = stringResource(R.string.chat_empty_subtitle), ) } @@ -482,7 +488,7 @@ fun ChatScreen( ) Spacer(modifier = Modifier.height(16.dp)) Text( - text = "Connecting to Hermes…", + text = stringResource(R.string.chat_status_connecting), style = MaterialTheme.typography.bodyMedium, ) } @@ -512,7 +518,7 @@ fun ChatScreen( ) { Icon( imageVector = Icons.Filled.KeyboardArrowDown, - contentDescription = "Scroll to bottom", + contentDescription = stringResource(R.string.content_desc_scroll_to_bottom), ) } } @@ -584,9 +590,9 @@ private fun ThinkingIndicator(thinkingText: String) { text = if (thinkingText.isNotBlank()) { val display = thinkingText.takeLast(100) - "Thinking: $display…" + stringResource(R.string.chat_thinking_param, display) } else { - "Thinking…" + stringResource(R.string.chat_thinking) }, style = MaterialTheme.typography.bodySmall.copy( @@ -671,14 +677,14 @@ private fun ChatInputBar( items(filteredCommands) { cmd -> val description = when (cmd) { - "/help" -> "Show help menu" - "/status" -> "Check gateway and platform status" - "/sessions" -> "List all chat sessions" - "/stats" -> "Check system resource usage (stats)" - "/system" -> "Check system resource usage (system)" - "/new" -> "Create a new chat session" - "/stop" -> "Interrupt the active run" - "/interrupt" -> "Interrupt the active run" + "/help" -> stringResource(R.string.command_help_desc) + "/status" -> stringResource(R.string.command_status_desc) + "/sessions" -> stringResource(R.string.command_sessions_desc) + "/stats" -> stringResource(R.string.command_stats_desc) + "/system" -> stringResource(R.string.command_system_desc) + "/new" -> stringResource(R.string.command_new_desc) + "/stop" -> stringResource(R.string.command_stop_desc) + "/interrupt" -> stringResource(R.string.command_stop_desc) else -> "" } DropdownMenuItem( @@ -725,11 +731,11 @@ private fun ChatInputBar( placeholder = { Text( if (!isConnected) { - "Not connected…" + stringResource(R.string.chat_input_placeholder_not_connected) } else if (isAgentTyping) { - "Waiting for response…" + stringResource(R.string.chat_input_placeholder_waiting) } else { - "Type a message…" + stringResource(R.string.chat_input_placeholder_type_message) }, ) }, @@ -759,7 +765,7 @@ private fun ChatInputBar( ) { Icon( imageVector = Icons.Filled.Stop, - contentDescription = "Interrupt", + contentDescription = stringResource(R.string.content_desc_interrupt), tint = MaterialTheme.colorScheme.error, ) } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/common/SharedComponents.kt b/app/src/main/java/com/m57/hermescontrol/ui/common/SharedComponents.kt index 8f527582..b8d19649 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/common/SharedComponents.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/common/SharedComponents.kt @@ -250,7 +250,15 @@ fun FilterChipRow( FilterChip( selected = chip == selectedChip, onClick = { onChipSelected(chip) }, - label = { Text(chip) }, + label = { + Text( + if (chip == "All") { + stringResource(R.string.skills_category_all) + } else { + chip + }, + ) + }, ) } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/connect/ConnectScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/connect/ConnectScreen.kt index ce62929b..a14a3ec1 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/connect/ConnectScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/connect/ConnectScreen.kt @@ -45,6 +45,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation @@ -53,6 +54,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel +import com.m57.hermescontrol.R @Composable fun ConnectScreen( @@ -106,7 +108,7 @@ fun ConnectScreen( } Text( - text = "Hermes", + text = stringResource(R.string.notif_title), style = MaterialTheme.typography.displayMedium.copy( fontWeight = FontWeight.Bold, @@ -114,7 +116,7 @@ fun ConnectScreen( ), ) Text( - text = "AI Agent Control", + text = stringResource(R.string.nav_drawer_subtitle), style = MaterialTheme.typography.bodyLarge.copy( color = MaterialTheme.colorScheme.onSurfaceVariant, @@ -134,9 +136,9 @@ fun ConnectScreen( Text( text = if (state.selectedProfile != null) { - "Profile: ${state.selectedProfile!!.name}" + stringResource(R.string.connect_selected_profile, state.selectedProfile!!.name) } else { - "Select Saved Profile" + stringResource(R.string.connect_action_select_profile) }, ) } @@ -146,7 +148,7 @@ fun ConnectScreen( modifier = Modifier.fillMaxWidth(0.85f), ) { DropdownMenuItem( - text = { Text("None (Standalone)") }, + text = { Text(stringResource(R.string.connect_profile_none)) }, onClick = { viewModel.selectProfile(null) profilesExpanded = false @@ -171,7 +173,7 @@ fun ConnectScreen( verticalArrangement = Arrangement.spacedBy(12.dp), ) { Text( - text = "Pairing string", + text = stringResource(R.string.connect_pairing_string), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.onBackground, ) @@ -182,7 +184,7 @@ fun ConnectScreen( pairingString = it if (it.isNotBlank()) viewModel.onPairingString(it) }, - placeholder = { Text("Paste hermes://… or Base64 config") }, + placeholder = { Text(stringResource(R.string.connect_placeholder_pairing)) }, singleLine = true, modifier = Modifier.fillMaxWidth(), ) @@ -215,7 +217,7 @@ fun ConnectScreen( onCheckedChange = viewModel::onSaveProfileChange, ) Text( - text = "Save connection profile", + text = stringResource(R.string.connect_save_profile), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onBackground, ) @@ -225,8 +227,8 @@ fun ConnectScreen( OutlinedTextField( value = state.profileName, onValueChange = viewModel::onProfileNameChange, - label = { Text("Profile Name") }, - placeholder = { Text("e.g. Local Hermit, Remote Server") }, + label = { Text(stringResource(R.string.connect_profile_name)) }, + placeholder = { Text(stringResource(R.string.connect_placeholder_profile_name)) }, singleLine = true, modifier = Modifier.fillMaxWidth(), ) @@ -247,7 +249,7 @@ fun ConnectScreen( strokeWidth = 2.dp, ) } else { - Text("Connect", style = MaterialTheme.typography.labelLarge) + Text(stringResource(R.string.action_connect), style = MaterialTheme.typography.labelLarge) } } @@ -256,7 +258,12 @@ fun ConnectScreen( modifier = Modifier.fillMaxWidth(), ) { Text( - text = if (showManualFields) "Hide manual entry" else "Enter manually instead", + text = + if (showManualFields) { + stringResource(R.string.connect_action_hide_manual) + } else { + stringResource(R.string.connect_action_show_manual) + }, style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.primary, ) @@ -271,8 +278,8 @@ fun ConnectScreen( OutlinedTextField( value = state.token, onValueChange = viewModel::onTokenChange, - label = { Text("Auth Token") }, - placeholder = { Text("API token") }, + label = { Text(stringResource(R.string.settings_field_token)) }, + placeholder = { Text(stringResource(R.string.connect_placeholder_token)) }, singleLine = true, visualTransformation = if (passwordVisible) { @@ -290,7 +297,11 @@ fun ConnectScreen( Icons.Filled.VisibilityOff }, contentDescription = - if (passwordVisible) "Hide token" else "Show token", + if (passwordVisible) { + stringResource(R.string.content_desc_hide_token) + } else { + stringResource(R.string.content_desc_show_token) + }, ) } }, @@ -299,16 +310,16 @@ fun ConnectScreen( OutlinedTextField( value = state.host, onValueChange = viewModel::onHostChange, - label = { Text("Host") }, - placeholder = { Text("127.0.0.1") }, + label = { Text(stringResource(R.string.settings_field_host)) }, + placeholder = { Text(stringResource(R.string.connect_placeholder_host)) }, singleLine = true, modifier = Modifier.fillMaxWidth(), ) OutlinedTextField( value = state.port, onValueChange = viewModel::onPortChange, - label = { Text("Port") }, - placeholder = { Text("9119") }, + label = { Text(stringResource(R.string.settings_field_port)) }, + placeholder = { Text(stringResource(R.string.connect_placeholder_port)) }, singleLine = true, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), modifier = Modifier.fillMaxWidth(), diff --git a/app/src/main/java/com/m57/hermescontrol/ui/kanban/KanbanScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/kanban/KanbanScreen.kt index b903be4c..da81522b 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/kanban/KanbanScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/kanban/KanbanScreen.kt @@ -39,10 +39,12 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel +import com.m57.hermescontrol.R import com.m57.hermescontrol.data.model.KanbanTask import com.m57.hermescontrol.ui.common.ErrorState import com.m57.hermescontrol.ui.common.HermesScaffold @@ -73,7 +75,7 @@ fun KanbanScreen( } HermesScaffold( - title = { Text("Kanban Board") }, + title = { Text(stringResource(R.string.kanban_board_title)) }, onOpenDrawer = onOpenDrawer, isRefreshing = state.isLoading, onRefresh = { viewModel.loadBoards() }, @@ -99,10 +101,12 @@ fun KanbanScreen( Text( text = state.errorMessage ?: "", color = MaterialTheme.colorScheme.error, - modifier = Modifier.padding(16.dp), + modifier = Modifier.padding(paddingValues), ) } else { - Column(modifier = Modifier.fillMaxSize()) { + Column( + modifier = Modifier.fillMaxSize().padding(paddingValues), + ) { // Board selector tab row if (state.boards.isNotEmpty()) { PrimaryScrollableTabRow( @@ -121,11 +125,11 @@ fun KanbanScreen( if (state.selectedBoard == null) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text("No boards found.") + Text(stringResource(R.string.kanban_no_boards)) } } else if (state.columns.isEmpty()) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text("No columns found for this board.") + Text(stringResource(R.string.kanban_no_columns)) } } else { LazyRow( @@ -219,7 +223,10 @@ fun TaskCard( ) { if (onMoveLeft != null) { IconButton(onClick = onMoveLeft) { - Icon(imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Move Left") + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(R.string.content_desc_move_left), + ) } } else { Spacer(modifier = Modifier.size(48.dp)) @@ -227,7 +234,10 @@ fun TaskCard( if (onMoveRight != null) { IconButton(onClick = onMoveRight) { - Icon(imageVector = Icons.Filled.ArrowForward, contentDescription = "Move Right") + Icon( + imageVector = Icons.Filled.ArrowForward, + contentDescription = stringResource(R.string.content_desc_move_right), + ) } } else { Spacer(modifier = Modifier.size(48.dp)) @@ -248,20 +258,20 @@ fun AddTaskDialog( androidx.compose.material3.AlertDialog( onDismissRequest = onDismiss, - title = { Text("Add New Task") }, + title = { Text(stringResource(R.string.kanban_add_new_task)) }, text = { Column { OutlinedTextField( value = title, onValueChange = { title = it }, - label = { Text("Task Title") }, + label = { Text(stringResource(R.string.kanban_task_title)) }, modifier = Modifier.fillMaxWidth(), ) Spacer(modifier = Modifier.height(8.dp)) OutlinedTextField( value = desc, onValueChange = { desc = it }, - label = { Text("Task Description") }, + label = { Text(stringResource(R.string.kanban_task_desc)) }, modifier = Modifier.fillMaxWidth(), ) } @@ -271,12 +281,12 @@ fun AddTaskDialog( onClick = { if (title.isNotBlank()) onConfirm(title, desc.ifBlank { null }) }, enabled = title.isNotBlank(), ) { - Text("Add") + Text(stringResource(R.string.action_add)) } }, dismissButton = { Button(onClick = onDismiss) { - Text("Cancel") + Text(stringResource(R.string.action_cancel)) } }, ) diff --git a/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsScreen.kt index 4696497a..06ab577f 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsScreen.kt @@ -53,6 +53,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation @@ -61,6 +62,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.m57.hermescontrol.BuildConfig +import com.m57.hermescontrol.R import com.m57.hermescontrol.theme.ThemePreference import com.m57.hermescontrol.ui.common.HermesScaffold @@ -77,7 +79,7 @@ fun SettingsScreen( BackHandler(onBack = onBack) HermesScaffold( - title = { Text("Settings") }, + title = { Text(stringResource(R.string.screen_settings)) }, showBack = true, onBack = onBack, ) { @@ -90,12 +92,12 @@ fun SettingsScreen( verticalArrangement = Arrangement.spacedBy(16.dp), ) { // Connection section - SectionCard(title = "Connection") { + SectionCard(title = stringResource(R.string.settings_sec_connection)) { // Connection Profiles management in Settings if (state.profiles.isNotEmpty()) { var profilesExpanded by remember { mutableStateOf(false) } Text( - text = "Connection Profile", + text = stringResource(R.string.settings_item_profile), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -105,7 +107,7 @@ fun SettingsScreen( modifier = Modifier.fillMaxWidth(), ) { val activeProfile = state.profiles.firstOrNull { it.id == state.selectedProfileId } - Text(text = activeProfile?.name ?: "Standalone / Default") + Text(text = activeProfile?.name ?: stringResource(R.string.settings_profile_default)) } DropdownMenu( expanded = profilesExpanded, @@ -113,7 +115,7 @@ fun SettingsScreen( modifier = Modifier.fillMaxWidth(0.85f), ) { DropdownMenuItem( - text = { Text("None (Standalone / Default)") }, + text = { Text(stringResource(R.string.settings_profile_none)) }, onClick = { viewModel.selectProfile(null) profilesExpanded = false @@ -136,7 +138,10 @@ fun SettingsScreen( ) { Icon( imageVector = Icons.Filled.Close, - contentDescription = "Delete Profile", + contentDescription = + stringResource( + R.string.content_desc_delete_profile, + ), tint = MaterialTheme.colorScheme.error, ) } @@ -155,7 +160,7 @@ fun SettingsScreen( OutlinedTextField( value = state.renameProfileName, onValueChange = viewModel::onRenameProfileNameChange, - label = { Text("Rename Profile") }, + label = { Text(stringResource(R.string.settings_action_rename_profile)) }, singleLine = true, modifier = Modifier.fillMaxWidth(), trailingIcon = { @@ -163,7 +168,7 @@ fun SettingsScreen( onClick = viewModel::renameProfile, modifier = Modifier.padding(end = 4.dp), ) { - Text("Rename") + Text(stringResource(R.string.action_rename)) } }, ) @@ -174,7 +179,7 @@ fun SettingsScreen( OutlinedTextField( value = state.host, onValueChange = viewModel::onHostChange, - label = { Text("Host") }, + label = { Text(stringResource(R.string.settings_field_host)) }, singleLine = true, modifier = Modifier.fillMaxWidth(), colors = @@ -189,7 +194,7 @@ fun SettingsScreen( OutlinedTextField( value = state.port, onValueChange = viewModel::onPortChange, - label = { Text("Port") }, + label = { Text(stringResource(R.string.settings_field_port)) }, singleLine = true, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), modifier = Modifier.fillMaxWidth(), @@ -205,7 +210,7 @@ fun SettingsScreen( OutlinedTextField( value = state.token, onValueChange = viewModel::onTokenChange, - label = { Text("Auth Token") }, + label = { Text(stringResource(R.string.settings_field_token)) }, singleLine = true, visualTransformation = if (passwordVisible) { @@ -224,9 +229,9 @@ fun SettingsScreen( }, contentDescription = if (passwordVisible) { - "Hide token" + stringResource(R.string.content_desc_hide_token) } else { - "Show token" + stringResource(R.string.content_desc_show_token) }, ) } @@ -253,12 +258,12 @@ fun SettingsScreen( }, modifier = Modifier.fillMaxWidth(), ) { - Text("Clear Token") + Text(stringResource(R.string.settings_action_clear_token)) } } // Behavior section - SectionCard(title = "Behavior") { + SectionCard(title = stringResource(R.string.settings_sec_behavior)) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, @@ -266,11 +271,11 @@ fun SettingsScreen( ) { Column { Text( - text = "Auto-Reconnect", + text = stringResource(R.string.settings_item_auto_reconnect), style = MaterialTheme.typography.bodyLarge, ) Text( - text = "Automatically reconnect on disconnect", + text = stringResource(R.string.settings_desc_auto_reconnect), style = MaterialTheme.typography.bodySmall.copy( color = MaterialTheme.colorScheme.onSurfaceVariant, @@ -290,9 +295,9 @@ fun SettingsScreen( } // Appearance section - SectionCard(title = "Appearance") { + SectionCard(title = stringResource(R.string.settings_sec_appearance)) { Text( - text = "Theme", + text = stringResource(R.string.settings_item_theme), style = MaterialTheme.typography.bodyLarge, ) Spacer(modifier = Modifier.height(8.dp)) @@ -308,9 +313,11 @@ fun SettingsScreen( ), ) { Text( - pref.name - .lowercase() - .replaceFirstChar { it.uppercase() }, + when (pref) { + ThemePreference.SYSTEM -> stringResource(R.string.theme_system) + ThemePreference.LIGHT -> stringResource(R.string.theme_light) + ThemePreference.DARK -> stringResource(R.string.theme_dark) + }, ) } } @@ -318,7 +325,7 @@ fun SettingsScreen( } // Chat section - SectionCard(title = "Chat") { + SectionCard(title = stringResource(R.string.settings_sec_chat)) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, @@ -326,11 +333,11 @@ fun SettingsScreen( ) { Column { Text( - text = "Typing Effect", + text = stringResource(R.string.settings_item_typing_effect), style = MaterialTheme.typography.bodyLarge, ) Text( - text = "Animate new messages word by word", + text = stringResource(R.string.settings_desc_typing_effect), style = MaterialTheme.typography.bodySmall.copy( color = MaterialTheme.colorScheme.onSurfaceVariant, @@ -352,7 +359,7 @@ fun SettingsScreen( AnimatedVisibility(visible = state.typingEffectEnabled) { Column(modifier = Modifier.padding(top = 12.dp)) { Text( - text = "Delay: ${state.typingEffectDelayMs} ms", + text = stringResource(R.string.settings_item_typing_delay, state.typingEffectDelayMs), style = MaterialTheme.typography.bodyMedium, ) Spacer(modifier = Modifier.height(4.dp)) @@ -368,14 +375,14 @@ fun SettingsScreen( horizontalArrangement = Arrangement.SpaceBetween, ) { Text( - text = "10 ms", + text = stringResource(R.string.settings_delay_10ms), style = MaterialTheme.typography.labelSmall.copy( color = MaterialTheme.colorScheme.onSurfaceVariant, ), ) Text( - text = "100 ms", + text = stringResource(R.string.settings_delay_100ms), style = MaterialTheme.typography.labelSmall.copy( color = MaterialTheme.colorScheme.onSurfaceVariant, @@ -387,15 +394,16 @@ fun SettingsScreen( } // Navigation Bar section - SectionCard(title = "Navigation Bar") { + SectionCard(title = stringResource(R.string.settings_sec_nav_bar)) { Text( - text = "Bottom bar items (${state.selectedNavItems.size}/5)", + text = stringResource(R.string.settings_item_nav_items, state.selectedNavItems.size), style = MaterialTheme.typography.bodyMedium, ) Spacer(modifier = Modifier.height(8.dp)) state.selectedNavItems.forEachIndexed { index, name -> - val label = viewModel.availableNavItems.firstOrNull { it.first == name }?.second ?: name + val labelRes = viewModel.availableNavItems.firstOrNull { it.first == name }?.second + val label = if (labelRes != null) stringResource(labelRes) else name Row( modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp), horizontalArrangement = Arrangement.SpaceBetween, @@ -417,7 +425,10 @@ fun SettingsScreen( }, enabled = index > 0, ) { - Icon(Icons.Filled.KeyboardArrowUp, contentDescription = "Move up") + Icon( + Icons.Filled.KeyboardArrowUp, + contentDescription = stringResource(R.string.content_desc_move_up), + ) } IconButton( onClick = { @@ -429,10 +440,16 @@ fun SettingsScreen( }, enabled = index < state.selectedNavItems.lastIndex, ) { - Icon(Icons.Filled.KeyboardArrowDown, contentDescription = "Move down") + Icon( + Icons.Filled.KeyboardArrowDown, + contentDescription = stringResource(R.string.content_desc_move_down), + ) } IconButton(onClick = { viewModel.removeNavItem(name) }) { - Icon(Icons.Filled.Close, contentDescription = "Remove $label") + Icon( + Icons.Filled.Close, + contentDescription = stringResource(R.string.content_desc_remove_item, label), + ) } } } @@ -446,12 +463,12 @@ fun SettingsScreen( OutlinedButton(onClick = { expanded = true }) { Icon(Icons.Filled.Add, contentDescription = null) Spacer(Modifier.size(4.dp)) - Text("Add Item") + Text(stringResource(R.string.settings_action_add_item)) } DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { - available.forEach { (name, label) -> + available.forEach { (name, labelRes) -> DropdownMenuItem( - text = { Text(label) }, + text = { Text(stringResource(labelRes)) }, onClick = { viewModel.addNavItem(name) expanded = false @@ -463,7 +480,7 @@ fun SettingsScreen( } } else { Text( - text = "Maximum of 5 items", + text = stringResource(R.string.settings_nav_max_reached), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -505,7 +522,7 @@ fun SettingsScreen( exit = fadeOut(), ) { Text( - text = "✅ Settings saved", + text = stringResource(R.string.settings_save_success), style = MaterialTheme.typography.bodyMedium.copy( color = MaterialTheme.colorScheme.primary, @@ -529,7 +546,7 @@ fun SettingsScreen( strokeWidth = 2.dp, ) } else { - Text("Test Connection") + Text(stringResource(R.string.settings_action_test_connection)) } } @@ -541,17 +558,17 @@ fun SettingsScreen( containerColor = MaterialTheme.colorScheme.primary, ), ) { - Text("Save") + Text(stringResource(R.string.action_save)) } } Spacer(modifier = Modifier.height(16.dp)) // ── About section ───────────────────────────────────────── - SectionCard(title = "About") { + SectionCard(title = stringResource(R.string.settings_sec_about)) { Row(verticalAlignment = Alignment.CenterVertically) { Text( - text = "⚡ Hermes Control", + text = stringResource(R.string.settings_about_app_name), style = MaterialTheme.typography.titleMedium.copy( fontWeight = FontWeight.Bold, @@ -563,13 +580,21 @@ fun SettingsScreen( HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant) Spacer(modifier = Modifier.height(8.dp)) - InfoRow(label = "Version", value = "v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})") InfoRow( - label = "Build", - value = if (BuildConfig.DEBUG) "Debug" else "Release", + label = stringResource(R.string.settings_about_version), + value = "v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})", + ) + InfoRow( + label = stringResource(R.string.settings_about_build), + value = + if (BuildConfig.DEBUG) { + stringResource(R.string.settings_about_debug) + } else { + stringResource(R.string.settings_about_release) + }, ) if (BuildConfig.GIT_SHA.isNotBlank()) { - InfoRow(label = "Commit", value = BuildConfig.GIT_SHA) + InfoRow(label = stringResource(R.string.settings_about_commit), value = BuildConfig.GIT_SHA) } Spacer(modifier = Modifier.height(12.dp)) diff --git a/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsViewModel.kt index 362deb31..16244565 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsViewModel.kt @@ -2,6 +2,7 @@ package com.m57.hermescontrol.ui.settings import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.m57.hermescontrol.R import com.m57.hermescontrol.data.local.AuthManager import com.m57.hermescontrol.data.remote.ApiClient import com.m57.hermescontrol.data.remote.NetworkResult @@ -129,28 +130,28 @@ class SettingsViewModel : ViewModel() { } /** All screens available for bottom-nav selection (name → display label). */ - val availableNavItems: List> = + val availableNavItems: List> = listOf( - "ChatScreen" to "Chat", - "SkillsScreen" to "Skills", - "CronJobsScreen" to "Cron Jobs", - "SystemScreen" to "System", - "SettingsScreen" to "Settings", - "ProfilesScreen" to "Profiles", - "WebhooksScreen" to "Webhooks", - "GatewayScreen" to "Gateway", - "HistoryScreen" to "History", - "ToolsetsScreen" to "Toolsets", - "PluginsScreen" to "Plugins", - "ConfigScreen" to "Config", - "McpServersScreen" to "MCP Servers", - "ModelScreen" to "Models", - "PairingScreen" to "Pairing", - "KeysScreen" to "Keys", - "ChannelsScreen" to "Channels", - "LogsScreen" to "Logs", - "KanbanScreen" to "Kanban", - "AchievementsScreen" to "Achievements", + "ChatScreen" to R.string.screen_chat, + "SkillsScreen" to R.string.screen_skills, + "CronJobsScreen" to R.string.screen_cron, + "SystemScreen" to R.string.screen_system, + "SettingsScreen" to R.string.screen_settings, + "ProfilesScreen" to R.string.screen_profiles, + "WebhooksScreen" to R.string.screen_webhooks, + "GatewayScreen" to R.string.screen_gateway, + "HistoryScreen" to R.string.screen_history, + "ToolsetsScreen" to R.string.screen_toolsets, + "PluginsScreen" to R.string.screen_plugins, + "ConfigScreen" to R.string.screen_config, + "McpServersScreen" to R.string.screen_mcp_servers, + "ModelScreen" to R.string.screen_models, + "PairingScreen" to R.string.screen_pairing, + "KeysScreen" to R.string.screen_keys, + "ChannelsScreen" to R.string.screen_channels, + "LogsScreen" to R.string.screen_logs, + "KanbanScreen" to R.string.screen_kanban, + "AchievementsScreen" to R.string.screen_achievements, ) /** Add a nav item to the bottom bar (max 5). Auto-saves. */ diff --git a/app/src/main/java/com/m57/hermescontrol/ui/skills/SkillsScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/skills/SkillsScreen.kt index a8b3ca54..db6bea87 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/skills/SkillsScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/skills/SkillsScreen.kt @@ -35,6 +35,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight @@ -43,6 +44,7 @@ import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel +import com.m57.hermescontrol.R import com.m57.hermescontrol.theme.LocalSpacing import com.m57.hermescontrol.ui.common.EmptyState import com.m57.hermescontrol.ui.common.ErrorState @@ -117,7 +119,7 @@ fun SkillsScreen( } HermesScaffold( - title = { Text("Skills") }, + title = { Text(stringResource(R.string.screen_skills)) }, onOpenDrawer = onOpenDrawer, isRefreshing = state.isLoading, onRefresh = { viewModel.loadSkills() }, @@ -129,15 +131,15 @@ fun SkillsScreen( state.errorMessage != null -> { ErrorState( - message = state.errorMessage ?: "Unknown error", + message = state.errorMessage ?: stringResource(R.string.error_unknown), onRetry = { viewModel.loadSkills() }, ) } state.skills.isEmpty() -> { EmptyState( - title = "No skills found", - subtitle = "Skills loaded from Hermes will appear here.", + title = stringResource(R.string.skills_no_skills), + subtitle = stringResource(R.string.skills_no_skills_desc), icon = Icons.Filled.Extension, ) } @@ -152,7 +154,7 @@ fun SkillsScreen( SearchBar( query = query, onQueryChange = { query = it }, - placeholder = "Search skills…", + placeholder = stringResource(R.string.skills_search_placeholder), ) } item { @@ -164,7 +166,7 @@ fun SkillsScreen( verticalArrangement = Arrangement.spacedBy(spacing.xs), ) { Text( - text = "Status", + text = stringResource(R.string.skills_filter_status), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -175,7 +177,16 @@ fun SkillsScreen( FilterChip( selected = selectedStatus == status, onClick = { selectedStatus = status }, - label = { Text(status) }, + label = { + Text( + when (status) { + "All Statuses" -> stringResource(R.string.skills_status_all) + "Enabled" -> stringResource(R.string.skills_status_enabled) + "Disabled" -> stringResource(R.string.skills_status_disabled) + else -> status + }, + ) + }, ) } } @@ -191,7 +202,7 @@ fun SkillsScreen( verticalArrangement = Arrangement.spacedBy(spacing.xs), ) { Text( - text = "Category", + text = stringResource(R.string.skills_filter_category), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(horizontal = spacing.md), @@ -218,14 +229,14 @@ fun SkillsScreen( verticalArrangement = Arrangement.spacedBy(spacing.xs), ) { Text( - text = "No matching skills", + text = stringResource(R.string.skills_no_matching), style = MaterialTheme.typography.titleMedium.copy( fontWeight = FontWeight.Bold, ), ) Text( - text = "Try adjusting your search or filters.", + text = stringResource(R.string.skills_no_matching_desc), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -293,7 +304,7 @@ fun SkillsScreen( ) { Icon( imageVector = Icons.Filled.Edit, - contentDescription = "Edit Skill File", + contentDescription = stringResource(R.string.content_desc_edit_skill), tint = MaterialTheme.colorScheme.onSurfaceVariant, ) } @@ -373,7 +384,7 @@ fun SkillEditorDialog( ), ) { HermesScaffold( - title = { Text("Edit: $skillName") }, + title = { Text(stringResource(R.string.skills_edit_title, skillName)) }, onBack = { if (hasChanges) { showDiscardConfirm = true @@ -397,7 +408,7 @@ fun SkillEditorDialog( } else { Icon( imageVector = Icons.Filled.Save, - contentDescription = "Save Changes", + contentDescription = stringResource(R.string.content_desc_save_changes), ) } } @@ -429,7 +440,7 @@ fun SkillEditorDialog( color = MaterialTheme.colorScheme.onSurface, ), placeholder = { - Text("Enter skill content...") + Text(stringResource(R.string.skills_content_placeholder)) }, ) } @@ -442,8 +453,8 @@ fun SkillEditorDialog( if (showDiscardConfirm) { AlertDialog( onDismissRequest = { showDiscardConfirm = false }, - title = { Text("Discard changes?") }, - text = { Text("You have unsaved changes. Are you sure you want to discard them?") }, + title = { Text(stringResource(R.string.skills_discard_title)) }, + text = { Text(stringResource(R.string.skills_discard_message)) }, confirmButton = { TextButton( onClick = { @@ -451,12 +462,12 @@ fun SkillEditorDialog( onDismiss() }, ) { - Text("Discard") + Text(stringResource(R.string.action_discard)) } }, dismissButton = { TextButton(onClick = { showDiscardConfirm = false }) { - Text("Cancel") + Text(stringResource(R.string.action_cancel)) } }, ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3f375975..afa5e105 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -70,4 +70,129 @@ Hermes Chat Replies Alerts for new replies from Hermes Reply + + + Search + Close search + Reconnect + Ready to chat + Send a message to start a conversation + Connecting to Hermes\u2026 + Reconnecting to Hermes\u2026 + Disconnected from Hermes + Thinking\u2026 + Thinking: %1$s\u2026 + Not connected\u2026 + Waiting for response\u2026 + Type a message\u2026 + Error Output (stderr):\n%1$s + Execution Error: %1$s + Exit Code: %1$d + Tool + Show Raw JSON + Show Parsed Output + Show help menu + Check gateway and platform status + List all chat sessions + Check system resource usage (stats) + Check system resource usage (system) + Create a new chat session + Interrupt the active run + New Chat + Scroll to bottom + Interrupt + Copy + + + Connection + Connection Profile + Standalone / Default + None (Standalone / Default) + Rename Profile + Host + Port + Auth Token + Clear Token + Behavior + Auto-Reconnect + Automatically reconnect on disconnect + Appearance + Theme + Chat + Typing Effect + Animate new messages word by word + Delay: %1$d ms + 10 ms + 100 ms + Navigation Bar + Bottom bar items (%1$d/5) + Add Item + Maximum of 5 items + ✅ Settings saved + Test Connection + About + ⚡ Hermes Control + Version + Build + Debug + Release + Commit + Rename + Save + Delete Profile + Hide token + Show token + Move up + Move down + Remove %1$s + System + Light + Dark + + + Profile: %1$s + Select Saved Profile + None (Standalone) + Pairing string + Paste hermes://\u2026 or Base64 config + Save connection profile + Profile Name + e.g. Local Hermit, Remote Server + Hide manual entry + Enter manually instead + API token + 127.0.0.1 + 9119 + Connect + + + Kanban Board + No boards found. + No columns found for this board. + Add New Task + Task Title + Task Description + Add + Move Left + Move Right + + + All Statuses + Enabled + Disabled + All + No skills found + Skills loaded from Hermes will appear here. + Search skills\u2026 + Status + Category + No matching skills + Try adjusting your search or filters. + Edit: %1$s + Enter skill content\u2026 + Discard changes? + You have unsaved changes. Are you sure you want to discard them? + Discard + Edit Skill File + Save Changes \ No newline at end of file