mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
fix(android): localize settings and cron helpers
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package ai.openclaw.app
|
||||
|
||||
import ai.openclaw.app.gateway.GatewaySession
|
||||
import ai.openclaw.app.i18n.nativeString
|
||||
import ai.openclaw.app.node.asObjectOrNull
|
||||
import ai.openclaw.app.node.asStringOrNull
|
||||
import kotlinx.serialization.json.Json
|
||||
@@ -253,14 +254,23 @@ internal fun CronEditorDraftState.reconcileRestoredAction(
|
||||
return if (isConnected && retainedSaveState) this else saveAborted()
|
||||
}
|
||||
|
||||
internal enum class GatewayCronRunSkipReason(
|
||||
val message: String,
|
||||
) {
|
||||
NotDue("Cron job is not due yet."),
|
||||
AlreadyRunning("Cron job is already running."),
|
||||
RestartRecoveryPending("Gateway restart recovery is still in progress."),
|
||||
InvalidSpec("Cron job has an invalid configuration."),
|
||||
Stopped("Cron scheduler is stopped."),
|
||||
internal enum class GatewayCronRunSkipReason {
|
||||
NotDue,
|
||||
AlreadyRunning,
|
||||
RestartRecoveryPending,
|
||||
InvalidSpec,
|
||||
Stopped,
|
||||
;
|
||||
|
||||
val message: String
|
||||
get() =
|
||||
when (this) {
|
||||
NotDue -> nativeString("Cron job is not due yet.")
|
||||
AlreadyRunning -> nativeString("Cron job is already running.")
|
||||
RestartRecoveryPending -> nativeString("Gateway restart recovery is still in progress.")
|
||||
InvalidSpec -> nativeString("Cron job has an invalid configuration.")
|
||||
Stopped -> nativeString("Cron scheduler is stopped.")
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed interface GatewayCronRunOutcome {
|
||||
@@ -288,10 +298,10 @@ internal fun cronRunCompletionNotice(
|
||||
): GatewayCronActionState.Notice {
|
||||
val (message, kind) =
|
||||
when (status) {
|
||||
"ok" -> "Cron run finished." to GatewayCronNoticeKind.Success
|
||||
"skipped" -> "Cron run skipped." to GatewayCronNoticeKind.Warning
|
||||
"error" -> "Cron run failed." to GatewayCronNoticeKind.Error
|
||||
else -> "Cron run finished with an unknown status." to GatewayCronNoticeKind.Warning
|
||||
"ok" -> nativeString("Cron run finished.") to GatewayCronNoticeKind.Success
|
||||
"skipped" -> nativeString("Cron run skipped.") to GatewayCronNoticeKind.Warning
|
||||
"error" -> nativeString("Cron run failed.") to GatewayCronNoticeKind.Error
|
||||
else -> nativeString("Cron run finished with an unknown status.") to GatewayCronNoticeKind.Warning
|
||||
}
|
||||
return GatewayCronActionState.Notice(id = jobId, message = message, kind = kind)
|
||||
}
|
||||
|
||||
@@ -119,9 +119,9 @@ internal fun CronJobManagementPanel(
|
||||
Text(
|
||||
text =
|
||||
if (editorDraft.hasIncomingConflict) {
|
||||
"This job changed while you were editing. Revert to the latest gateway version before saving."
|
||||
nativeString("This job changed while you were editing. Revert to the latest gateway version before saving.")
|
||||
} else {
|
||||
"Save or revert your edits before running, enabling, disabling, deleting, or refreshing this job."
|
||||
nativeString("Save or revert your edits before running, enabling, disabling, deleting, or refreshing this job.")
|
||||
},
|
||||
style = ClawTheme.type.body,
|
||||
color = ClawTheme.colors.warning,
|
||||
@@ -176,9 +176,9 @@ private fun CronAdminAccessPanel() {
|
||||
}
|
||||
Text(
|
||||
text =
|
||||
nativeString("Cron changes require operator.admin. Setup codes intentionally do not grant it. ") +
|
||||
"Reconnect with the gateway's shared token or password to request admin access. " +
|
||||
"If this device still lacks it, approve the pending scope upgrade from an existing admin client.",
|
||||
nativeString(
|
||||
"Cron changes require operator.admin. Setup codes intentionally do not grant it. Reconnect with the gateway's shared token or password to request admin access. If this device still lacks it, approve the pending scope upgrade from an existing admin client.",
|
||||
),
|
||||
style = ClawTheme.type.body,
|
||||
color = ClawTheme.colors.textMuted,
|
||||
)
|
||||
@@ -206,8 +206,8 @@ private fun CronActionPanel(
|
||||
text =
|
||||
when {
|
||||
busy -> nativeString("Working")
|
||||
runPending -> "Run Pending"
|
||||
else -> "Run Now"
|
||||
runPending -> nativeString("Run Pending")
|
||||
else -> nativeString("Run Now")
|
||||
},
|
||||
onClick = onRun,
|
||||
modifier = Modifier.weight(1f),
|
||||
@@ -525,9 +525,9 @@ private fun CronPayloadEditor(
|
||||
placeholder = nativeString("Optional path"),
|
||||
label =
|
||||
if (commandCwdCanBeCleared) {
|
||||
"Command working directory"
|
||||
nativeString("Command working directory")
|
||||
} else {
|
||||
"Command working directory · cannot clear"
|
||||
nativeString("Command working directory · cannot clear")
|
||||
},
|
||||
enabled = enabled,
|
||||
)
|
||||
@@ -624,14 +624,14 @@ private fun cronRunSubtitle(run: GatewayCronRunSummary): String =
|
||||
run.deliveryStatus,
|
||||
run.model,
|
||||
run.error ?: run.summary,
|
||||
).joinToString(" · ").ifBlank { "No details" }
|
||||
).joinToString(" · ").ifBlank { nativeString("No details") }
|
||||
|
||||
private fun cronRunStatusText(status: String?): String =
|
||||
when (status?.lowercase()) {
|
||||
"ok" -> "OK"
|
||||
"error" -> "Issue"
|
||||
"skipped" -> "Skipped"
|
||||
else -> "Unknown"
|
||||
"error" -> nativeString("Issue")
|
||||
"skipped" -> nativeString("Skipped")
|
||||
else -> nativeString("Unknown")
|
||||
}
|
||||
|
||||
private fun cronRunStatus(status: String?): ClawStatus =
|
||||
|
||||
@@ -106,7 +106,7 @@ private fun DreamingPanel(summary: GatewayDreamingSummary) {
|
||||
HorizontalDivider(color = ClawTheme.colors.border, thickness = 1.dp)
|
||||
ClawStatusRow(
|
||||
title = nativeString("Promoted"),
|
||||
value = "${summary.promotedToday} today · ${summary.promotedTotal} total",
|
||||
value = nativeString("\${summary.promotedToday} today · \${summary.promotedTotal} total", summary.promotedToday, summary.promotedTotal),
|
||||
healthy = true,
|
||||
)
|
||||
}
|
||||
@@ -173,13 +173,13 @@ private fun DreamDiaryRow(entry: GatewayDreamDiaryEntry) {
|
||||
|
||||
/** Formats the next dreaming cycle as a compact relative label. */
|
||||
private fun formatDreamingNextRun(nextRunAtMs: Long?): String {
|
||||
val next = nextRunAtMs ?: return "Not scheduled"
|
||||
val next = nextRunAtMs ?: return nativeString("Not scheduled")
|
||||
val deltaMinutes = ((next - System.currentTimeMillis()) / 60_000L).coerceAtLeast(0L)
|
||||
val hours = deltaMinutes / 60L
|
||||
return when {
|
||||
hours >= 24L -> "In ${hours / 24L}d"
|
||||
hours >= 1L -> "In ${hours}h"
|
||||
deltaMinutes >= 1L -> "In ${deltaMinutes}m"
|
||||
else -> "Soon"
|
||||
hours >= 24L -> nativeString("In \${hours / 24L}d", hours / 24L)
|
||||
hours >= 1L -> nativeString("In \${hours}h", hours)
|
||||
deltaMinutes >= 1L -> nativeString("In \${deltaMinutes}m", deltaMinutes)
|
||||
else -> nativeString("Soon")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ internal fun HealthLogsSettingsScreen(
|
||||
),
|
||||
)
|
||||
HealthStatusPanel(
|
||||
gateway = nativeString(gatewayConnectionDisplay.statusText),
|
||||
gateway = gatewayStatusForDisplay(gatewayConnectionDisplay.statusText),
|
||||
node = if (isNodeConnected) nativeString("Online") else nativeString("Waiting"),
|
||||
chat = if (chatHealthOk) nativeString("Ready") else nativeString("Needs connection"),
|
||||
models = nativeString("\${modelCount.size} available", modelCount.size),
|
||||
@@ -232,7 +232,7 @@ private fun GatewayLogRow(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClickLabel = "Open log entry", onClick = onClick)
|
||||
.clickable(onClickLabel = nativeString("Open log entry"), onClick = onClick)
|
||||
.padding(horizontal = 10.dp, vertical = 7.dp),
|
||||
verticalAlignment = Alignment.Top,
|
||||
horizontalArrangement = Arrangement.spacedBy(9.dp),
|
||||
|
||||
@@ -230,7 +230,7 @@ private fun DeviceListRow(
|
||||
private fun GatewayNodesDevicesSummary.isEmpty(): Boolean = nodes.isEmpty() && pendingDevices.isEmpty() && pairedDevices.isEmpty()
|
||||
|
||||
private fun nodeSubtitle(node: GatewayNodeSummary): String {
|
||||
val kind = node.deviceFamily ?: "Node host"
|
||||
val kind = node.deviceFamily ?: nativeString("Node host")
|
||||
val version = node.version?.let { "OpenClaw $it" }
|
||||
val status = if (node.paired) nativeString("Paired") else nativeString("Unpaired")
|
||||
val approval = nodeApprovalSubtitle(node.approvalState)
|
||||
|
||||
@@ -364,8 +364,8 @@ private fun ProviderAvailability.color(): Color =
|
||||
private val ProviderAvailability.modelLabel: String
|
||||
get() =
|
||||
when (this) {
|
||||
ProviderAvailability.Available -> "Available"
|
||||
ProviderAvailability.Unavailable -> "Unavailable"
|
||||
ProviderAvailability.Available -> nativeString("Available")
|
||||
ProviderAvailability.Unavailable -> nativeString("Unavailable")
|
||||
ProviderAvailability.Unknown -> nativeString("Unknown")
|
||||
}
|
||||
|
||||
|
||||
@@ -902,4 +902,4 @@ private fun relativeSessionTime(updatedAtMs: Long): String {
|
||||
private fun displaySessionTitle(session: ChatSessionEntry): String =
|
||||
session.label?.takeIf { it.isNotBlank() }
|
||||
?: session.displayName?.takeIf { it.isNotBlank() }
|
||||
?: "Main session"
|
||||
?: nativeString("Main session")
|
||||
|
||||
@@ -428,7 +428,7 @@ private fun CronJobDetailSettingsScreen(
|
||||
if (deleted) leaveDetail()
|
||||
}
|
||||
SettingsDetailFrame(
|
||||
title = current?.name ?: jobName ?: "Cron Job",
|
||||
title = current?.name ?: jobName ?: nativeString("Cron Job"),
|
||||
subtitle = nativeString("Inspect scheduled gateway work."),
|
||||
icon = Icons.Default.Bolt,
|
||||
onBack = ::leaveDetail,
|
||||
@@ -1274,8 +1274,10 @@ private fun PhoneCapabilitiesScreen(
|
||||
title = { Text(nativeString("Allow background location?")) },
|
||||
text = {
|
||||
Text(
|
||||
nativeString("OpenClaw only checks location when your paired Gateway requests it. ") +
|
||||
"On the next Android screen, choose $backgroundPermissionLabel to allow checks while the app is in the background.",
|
||||
nativeString(
|
||||
"OpenClaw only checks location when your paired Gateway requests it. On the next Android screen, choose \$backgroundPermissionLabel to allow checks while the app is in the background.",
|
||||
backgroundPermissionLabel,
|
||||
),
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
@@ -1895,8 +1897,9 @@ internal fun androidDistributionChannel(flavor: String = BuildConfig.FLAVOR): St
|
||||
}
|
||||
|
||||
internal fun gatewaySettingsSetupResetConfirmationText(): String =
|
||||
"Replacing the setup code clears this phone's saved setup credentials and device tokens before reconnecting. " +
|
||||
"This phone may need node capability approval again; continue only when you mean to pair with a fresh gateway setup code."
|
||||
nativeString(
|
||||
"Replacing the setup code clears this phone's saved setup credentials and device tokens before reconnecting. This phone may need node capability approval again; continue only when you mean to pair with a fresh gateway setup code.",
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun AboutStatusRow(
|
||||
@@ -1920,9 +1923,9 @@ private fun AboutStatusRow(
|
||||
/** Chooses about-screen copy based on whether the gateway advertises an update. */
|
||||
private fun aboutUpdateText(latestVersion: String?): String =
|
||||
if (latestVersion == null) {
|
||||
"OpenClaw turns this phone into a clean mobile command surface for sessions, voice, providers, and Gateway."
|
||||
nativeString("OpenClaw turns this phone into a clean mobile command surface for sessions, voice, providers, and Gateway.")
|
||||
} else {
|
||||
"A Gateway update is available. Run the update from the Web UI or CLI when you are ready."
|
||||
nativeString("A Gateway update is available. Run the update from the Web UI or CLI when you are ready.")
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2172,7 +2175,7 @@ private fun CronJobListRow(
|
||||
ClawDetailRow(
|
||||
title = job.name,
|
||||
subtitle = cronJobSubtitle(job),
|
||||
modifier = Modifier.clickable(onClickLabel = "Open cron job detail", onClick = onClick),
|
||||
modifier = Modifier.clickable(onClickLabel = nativeString("Open cron job detail"), onClick = onClick),
|
||||
leading = { ClawIconBadge(icon = Icons.Default.Bolt) },
|
||||
trailing = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
@@ -2264,7 +2267,7 @@ private fun CronJobFieldsPanel(rows: List<SettingsMetric>) {
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 46.dp)
|
||||
.clickable(onClickLabel = "Copy ${row.title}") { copyCronDetailValue(context, row.title, row.value) }
|
||||
.clickable(onClickLabel = nativeString("Copy \${row.title}", row.title)) { copyCronDetailValue(context, row.title, row.value) }
|
||||
.padding(vertical = 6.dp)
|
||||
} else {
|
||||
Modifier
|
||||
@@ -2475,7 +2478,7 @@ private fun cronJobStatusText(job: GatewayCronJobSummary): String {
|
||||
return when (job.lastRunStatus?.lowercase()) {
|
||||
"error" -> nativeString("Issue")
|
||||
"ok" -> "OK"
|
||||
"skipped" -> "Skipped"
|
||||
"skipped" -> nativeString("Skipped")
|
||||
else -> nativeString("Ready")
|
||||
}
|
||||
}
|
||||
@@ -2485,7 +2488,7 @@ private fun cronJobStatusText(job: GatewayCronJobDetail): String {
|
||||
return when (job.lastRunStatus?.lowercase()) {
|
||||
"error" -> nativeString("Issue")
|
||||
"ok" -> "OK"
|
||||
"skipped" -> "Skipped"
|
||||
"skipped" -> nativeString("Skipped")
|
||||
else -> nativeString("Ready")
|
||||
}
|
||||
}
|
||||
@@ -2535,15 +2538,19 @@ private fun notificationPackageSelectionSummary(
|
||||
when (mode) {
|
||||
NotificationPackageFilterMode.Allowlist ->
|
||||
if (selectedCount == 0) {
|
||||
"No apps selected. Nothing forwards until you add apps."
|
||||
nativeString("No apps selected. Nothing forwards until you add apps.")
|
||||
} else if (selectedCount == 1) {
|
||||
nativeString("\$selectedCount app allowed to forward.", selectedCount)
|
||||
} else {
|
||||
"$selectedCount ${if (selectedCount == 1) "app" else "apps"} allowed to forward."
|
||||
nativeString("\$selectedCount apps allowed to forward.", selectedCount)
|
||||
}
|
||||
NotificationPackageFilterMode.Blocklist ->
|
||||
if (selectedCount == 0) {
|
||||
"No apps blocked. Apps can forward unless you add blocks."
|
||||
nativeString("No apps blocked. Apps can forward unless you add blocks.")
|
||||
} else if (selectedCount == 1) {
|
||||
nativeString("\$selectedCount app blocked from forwarding.", selectedCount)
|
||||
} else {
|
||||
"$selectedCount ${if (selectedCount == 1) "app" else "apps"} blocked from forwarding."
|
||||
nativeString("\$selectedCount apps blocked from forwarding.", selectedCount)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -656,7 +656,7 @@ private fun OverviewPrimaryPanel(
|
||||
OverviewAgentBadge(text = agentBadge, active = isConnected, avatarSource = agentAvatarSource)
|
||||
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(3.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(5.dp)) {
|
||||
Text(text = if (pendingRunCount > 0) "$agentName is working" else agentName, style = ClawTheme.type.title.copy(fontSize = 19.sp, lineHeight = 23.sp), color = ClawTheme.colors.text, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f, fill = false))
|
||||
Text(text = if (pendingRunCount > 0) nativeString("\$agentName is working", agentName) else agentName, style = ClawTheme.type.title.copy(fontSize = 19.sp, lineHeight = 23.sp), color = ClawTheme.colors.text, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f, fill = false))
|
||||
}
|
||||
Text(text = overviewAgentActivityText(isConnected = isConnected, pendingRunCount = pendingRunCount, sessionCount = sessionCount, cronJobCount = cronJobCount, statusText = statusText), style = ClawTheme.type.caption.copy(fontSize = 13.5.sp, lineHeight = 17.sp), color = ClawTheme.colors.textMuted, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
@@ -1147,10 +1147,18 @@ internal fun overviewAgentActivityText(
|
||||
statusText: String,
|
||||
): String {
|
||||
if (!isConnected) return statusText
|
||||
if (pendingRunCount > 0) return "Working · $pendingRunCount active ${pluralize("run", pendingRunCount)}"
|
||||
if (pendingRunCount > 0) {
|
||||
return if (pendingRunCount == 1) {
|
||||
nativeString("Working · 1 active run")
|
||||
} else {
|
||||
nativeString("Working · \$pendingRunCount active runs", pendingRunCount)
|
||||
}
|
||||
}
|
||||
return when {
|
||||
sessionCount > 0 -> "Monitoring · $sessionCount ${pluralize("session", sessionCount)}"
|
||||
cronJobCount > 0 -> "Monitoring · ${cronJobsSummary(cronJobCount)}"
|
||||
sessionCount == 1 -> nativeString("Monitoring · 1 session")
|
||||
sessionCount > 1 -> nativeString("Monitoring · \$sessionCount sessions", sessionCount)
|
||||
cronJobCount == 1 -> nativeString("Monitoring · 1 scheduled job")
|
||||
cronJobCount > 1 -> nativeString("Monitoring · \$cronJobCount scheduled jobs", cronJobCount)
|
||||
else -> statusText
|
||||
}
|
||||
}
|
||||
@@ -1165,11 +1173,6 @@ internal fun nodeOnlinePercent(
|
||||
((onlineNodes.coerceAtLeast(0) * 100) + (nodeCount / 2)) / nodeCount
|
||||
}
|
||||
|
||||
private fun pluralize(
|
||||
noun: String,
|
||||
count: Int,
|
||||
): String = if (count == 1) noun else "${noun}s"
|
||||
|
||||
private fun agentInitials(name: String): String =
|
||||
name
|
||||
.split(' ', '-', '_')
|
||||
@@ -1587,13 +1590,13 @@ private fun SettingsShellScreen(
|
||||
),
|
||||
SettingsRow("Nodes & Devices", nodesDevicesSummaryText(nodesDevicesSummary), Icons.Default.Cloud, status = nodesDevicesStatus(nodesDevicesSummary), route = SettingsRoute.NodesDevices),
|
||||
SettingsRow("Channels", channelsSummaryText(channelsSummary), Icons.Default.Notifications, status = channelsStatus(channelsSummary), route = SettingsRoute.Channels),
|
||||
SettingsRow("Agents", if (agents.isEmpty()) "Load from gateway" else "${agents.size} available", Icons.Default.Person, status = agents.isNotEmpty(), route = SettingsRoute.Agents),
|
||||
SettingsRow("Agents", if (agents.isEmpty()) nativeString("Load from gateway") else nativeString("\${agents.size} available", agents.size), Icons.Default.Person, status = agents.isNotEmpty(), route = SettingsRoute.Agents),
|
||||
SettingsRow(
|
||||
"Providers & Models",
|
||||
when {
|
||||
readyProviderCount > 0 -> "$readyProviderCount ready"
|
||||
unknownProviderCount > 0 -> "Availability unknown"
|
||||
else -> "Review readiness"
|
||||
readyProviderCount > 0 -> nativeString("\$readyProviderCount ready", readyProviderCount)
|
||||
unknownProviderCount > 0 -> nativeString("Availability unknown")
|
||||
else -> nativeString("Review readiness")
|
||||
},
|
||||
Icons.Outlined.Inventory2,
|
||||
status =
|
||||
@@ -1617,9 +1620,9 @@ private fun SettingsShellScreen(
|
||||
route = SettingsRoute.SkillWorkshop,
|
||||
),
|
||||
SettingsRow("Dreaming", dreamingSummaryText(dreamingSummary), Icons.Default.Storage, status = dreamingStatus(dreamingSummary), route = SettingsRoute.Dreaming),
|
||||
SettingsRow("Terminal", "Shell in the agent workspace", Icons.Outlined.Terminal, status = isConnected, route = SettingsRoute.Terminal),
|
||||
SettingsRow("Terminal", nativeString("Shell in the agent workspace"), Icons.Outlined.Terminal, status = isConnected, route = SettingsRoute.Terminal),
|
||||
SettingsRow("Voice", if (speakerEnabled) nativeString("Speaker on") else nativeString("Speaker muted"), Icons.Default.Mic, route = SettingsRoute.Voice),
|
||||
SettingsRow("Canvas", "Screen surface", Icons.AutoMirrored.Filled.ScreenShare, status = isConnected, route = SettingsRoute.Canvas),
|
||||
SettingsRow("Canvas", nativeString("Screen surface"), Icons.AutoMirrored.Filled.ScreenShare, status = isConnected, route = SettingsRoute.Canvas),
|
||||
SettingsRow("Notifications", if (notificationForwardingEnabled) nativeString("Smart delivery") else nativeString("Off"), Icons.Default.Notifications, route = SettingsRoute.Notifications),
|
||||
SettingsRow("Phone Capabilities", if (cameraEnabled) nativeString("Camera enabled") else nativeString("Locked"), Icons.Default.Lock, status = !cameraEnabled, route = SettingsRoute.PhoneCapabilities),
|
||||
SettingsRow(
|
||||
@@ -1628,8 +1631,8 @@ private fun SettingsShellScreen(
|
||||
Icons.Default.Palette,
|
||||
route = SettingsRoute.Appearance,
|
||||
),
|
||||
SettingsRow("About", "Version and update", Icons.Default.Storage, route = SettingsRoute.About),
|
||||
SettingsRow("Health", "Diagnostics", Icons.Default.Settings, status = isConnected, route = SettingsRoute.Health),
|
||||
SettingsRow("About", nativeString("Version and update"), Icons.Default.Storage, route = SettingsRoute.About),
|
||||
SettingsRow("Health", nativeString("Diagnostics"), Icons.Default.Settings, status = isConnected, route = SettingsRoute.Health),
|
||||
)
|
||||
|
||||
settingsSections(settingsRows).forEach { section ->
|
||||
@@ -1646,7 +1649,7 @@ private fun SettingsShellScreen(
|
||||
}
|
||||
item {
|
||||
SettingsGroup(
|
||||
rows = listOf(SettingsRow("Sign Out", "Return to setup", Icons.AutoMirrored.Filled.ExitToApp)),
|
||||
rows = listOf(SettingsRow("Sign Out", nativeString("Return to setup"), Icons.AutoMirrored.Filled.ExitToApp)),
|
||||
onOpen = { },
|
||||
onAction = { viewModel.pairNewGateway() },
|
||||
)
|
||||
@@ -1685,9 +1688,9 @@ private fun SettingsShellScreen(
|
||||
|
||||
private fun approvalsSummary(count: Int): String =
|
||||
when (count) {
|
||||
0 -> "No pending approvals"
|
||||
0 -> nativeString("No pending approvals")
|
||||
1 -> nativeString("1 pending")
|
||||
else -> "$count pending"
|
||||
else -> nativeString("\$count pending", count)
|
||||
}
|
||||
|
||||
private fun approvalsStatus(count: Int): Boolean? = if (count > 0) true else null
|
||||
@@ -1695,9 +1698,9 @@ private fun approvalsStatus(count: Int): Boolean? = if (count > 0) true else nul
|
||||
/** Summarizes scheduled gateway jobs for overview and settings rows. */
|
||||
private fun cronJobsSummary(count: Int): String =
|
||||
when (count) {
|
||||
0 -> "No scheduled jobs"
|
||||
1 -> "1 scheduled"
|
||||
else -> "$count scheduled"
|
||||
0 -> nativeString("No scheduled jobs")
|
||||
1 -> nativeString("1 scheduled")
|
||||
else -> nativeString("\$count scheduled", count)
|
||||
}
|
||||
|
||||
/** Summarizes provider usage buckets without exposing detailed billing data. */
|
||||
@@ -1733,10 +1736,10 @@ internal fun skillWorkshopSummaryText(summary: GatewaySkillWorkshopSummary): Str
|
||||
val held = summary.proposals.count { it.status == "quarantined" || it.status == "stale" }
|
||||
val applied = summary.proposals.count { it.status == "applied" }
|
||||
return when {
|
||||
summary.proposals.isEmpty() -> "No proposals"
|
||||
summary.proposals.isEmpty() -> nativeString("No proposals")
|
||||
held > 0 -> if (held == 1) nativeString("1 held") else nativeString("\$held held", held)
|
||||
applied > 0 -> if (applied == 1) nativeString("1 applied") else nativeString("\$applied applied", applied)
|
||||
else -> "${summary.proposals.size} proposals"
|
||||
else -> nativeString("\${summary.proposals.size} proposals", summary.proposals.size)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1753,11 +1756,11 @@ private fun nodesDevicesSummaryText(summary: GatewayNodesDevicesSummary): String
|
||||
val online = summary.nodes.count { it.connected }
|
||||
val devices = summary.pairedDevices.size
|
||||
return when {
|
||||
summary.pendingDevices.isNotEmpty() -> "${summary.pendingDevices.size} pending"
|
||||
summary.hasNodeCapabilityApprovalPending() -> "Node approval pending"
|
||||
summary.nodes.isNotEmpty() -> "$online/${summary.nodes.size} online"
|
||||
devices > 0 -> "$devices paired"
|
||||
else -> "No devices"
|
||||
summary.pendingDevices.isNotEmpty() -> nativeString("\${summary.pendingDevices.size} pending", summary.pendingDevices.size)
|
||||
summary.hasNodeCapabilityApprovalPending() -> nativeString("Node approval pending")
|
||||
summary.nodes.isNotEmpty() -> nativeString("\$online/\${summary.nodes.size} online", online, summary.nodes.size)
|
||||
devices > 0 -> nativeString("\$devices paired", devices)
|
||||
else -> nativeString("No devices")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1779,12 +1782,14 @@ private fun GatewayNodesDevicesSummary.hasNodeCapabilityApprovalPending(): Boole
|
||||
}
|
||||
|
||||
/** Summarizes channel connection state, surfacing errors before connected counts. */
|
||||
private fun channelsSummaryText(summary: GatewayChannelsSummary): String {
|
||||
internal fun channelsSummaryText(summary: GatewayChannelsSummary): String {
|
||||
val connected = summary.channels.count { it.connected }
|
||||
val issueCount = summary.channels.count { it.error != null }
|
||||
return when {
|
||||
summary.channels.any { it.error != null } -> "${summary.channels.count { it.error != null }} issue"
|
||||
summary.channels.isNotEmpty() -> "$connected/${summary.channels.size} connected"
|
||||
else -> "No channels"
|
||||
issueCount == 1 -> nativeString("1 issue")
|
||||
issueCount > 1 -> nativeString("\$issueCount issues", issueCount)
|
||||
summary.channels.isNotEmpty() -> nativeString("\$connected/\${summary.channels.size} connected", connected, summary.channels.size)
|
||||
else -> nativeString("No channels")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1801,7 +1806,7 @@ private fun channelsStatus(summary: GatewayChannelsSummary): Boolean? =
|
||||
private fun dreamingSummaryText(summary: GatewayDreamingSummary): String =
|
||||
when {
|
||||
!summary.storeHealthy || !summary.phaseSignalHealthy -> nativeString("Needs attention")
|
||||
summary.enabled -> "${summary.shortTermCount} waiting"
|
||||
summary.enabled -> nativeString("\${summary.shortTermCount} waiting", summary.shortTermCount)
|
||||
else -> nativeString("Off")
|
||||
}
|
||||
|
||||
@@ -1978,7 +1983,7 @@ private fun SettingsListRow(
|
||||
Text(text = nativeString(row.title), style = ClawTheme.type.body, color = ClawTheme.colors.text, modifier = Modifier.weight(1f), maxLines = 1)
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(5.dp)) {
|
||||
if (row.value.isNotBlank()) {
|
||||
Text(text = nativeString(row.value), style = ClawTheme.type.caption.copy(fontSize = 13.sp, lineHeight = 17.sp), color = ClawTheme.colors.textMuted, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
Text(text = row.value, style = ClawTheme.type.caption.copy(fontSize = 13.sp, lineHeight = 17.sp), color = ClawTheme.colors.textMuted, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
row.status?.let { active ->
|
||||
Box(modifier = Modifier.size(4.5.dp).clip(CircleShape).background(if (active) ClawTheme.colors.success else ClawTheme.colors.textSubtle))
|
||||
|
||||
@@ -499,7 +499,7 @@ private fun SkillWorkshopProposalDetail(
|
||||
),
|
||||
)
|
||||
Text(
|
||||
text = proposal.content ?: "Inspect this proposal to load its markdown.",
|
||||
text = proposal.content ?: nativeString("Inspect this proposal to load its markdown."),
|
||||
style = ClawTheme.type.body,
|
||||
color = if (proposal.content == null) ClawTheme.colors.textSubtle else ClawTheme.colors.text,
|
||||
)
|
||||
@@ -687,4 +687,4 @@ private fun skillWorkshopStatusPill(status: String): ClawStatus =
|
||||
else -> ClawStatus.Neutral
|
||||
}
|
||||
|
||||
private fun skillWorkshopDateLabel(value: String): String = value.trim().takeIf { it.isNotEmpty() }?.take(10) ?: "unknown"
|
||||
private fun skillWorkshopDateLabel(value: String): String = value.trim().takeIf { it.isNotEmpty() }?.take(10) ?: nativeString("Unknown")
|
||||
|
||||
@@ -202,7 +202,7 @@ private fun SkillListRow(
|
||||
ClawDetailRow(
|
||||
title = skill.name,
|
||||
subtitle = skillSubtitle(skill),
|
||||
modifier = Modifier.clickable(onClickLabel = "Open skill detail", onClick = onClick),
|
||||
modifier = Modifier.clickable(onClickLabel = nativeString("Open skill detail"), onClick = onClick),
|
||||
leading = { ClawTextBadge(text = skillBadge(skill)) },
|
||||
trailing = {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
|
||||
@@ -660,6 +660,38 @@ class ShellScreenLogicTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun channelsSummaryTextUsesDistinctIssuePluralization() {
|
||||
fun channel(
|
||||
id: String,
|
||||
error: String?,
|
||||
) =
|
||||
GatewayChannelSummary(
|
||||
id = id,
|
||||
label = id,
|
||||
accountCount = 1,
|
||||
enabled = true,
|
||||
configured = true,
|
||||
linked = true,
|
||||
running = error == null,
|
||||
connected = error == null,
|
||||
error = error,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
"1 issue",
|
||||
channelsSummaryText(GatewayChannelsSummary(channels = listOf(channel("one", "offline")))),
|
||||
)
|
||||
assertEquals(
|
||||
"2 issues",
|
||||
channelsSummaryText(
|
||||
GatewayChannelsSummary(
|
||||
channels = listOf(channel("one", "offline"), channel("two", "unauthorized")),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sessionSourceLabelDerivesCompactSourceFromRealSessionKey() {
|
||||
assertEquals("Telegram", sessionSourceLabel("telegram:8227096397"))
|
||||
|
||||
Reference in New Issue
Block a user