feat(android): localize onboarding and navigation

This commit is contained in:
Vincent Koc
2026-07-12 07:13:46 +02:00
committed by Vincent Koc
parent 869d1b0238
commit 2c19c8d4d2
5 changed files with 243 additions and 223 deletions

View File

@@ -3,6 +3,7 @@ package ai.openclaw.app.ui
import ai.openclaw.app.GatewayModelProviderSummary
import ai.openclaw.app.GatewayModelSummary
import ai.openclaw.app.MainViewModel
import ai.openclaw.app.i18n.nativeString
import ai.openclaw.app.ui.design.ClawEmptyState
import ai.openclaw.app.ui.design.ClawPanel
import ai.openclaw.app.ui.design.ClawPlainIconButton
@@ -97,25 +98,25 @@ internal fun CommandPalette(
) {
ClawPlainIconButton(
icon = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Close search",
contentDescription = nativeString("Close search"),
onClick = onDismiss,
)
Text(text = "Search", style = ClawTheme.type.title, color = ClawTheme.colors.text, modifier = Modifier.weight(1f), textAlign = TextAlign.Center)
Text(text = nativeString("Search"), style = ClawTheme.type.title, color = ClawTheme.colors.text, modifier = Modifier.weight(1f), textAlign = TextAlign.Center)
CommandAvatar(text = "OC")
}
}
item {
ClawTextField(value = query, onValueChange = { query = it }, placeholder = "Search OpenClaw")
ClawTextField(value = query, onValueChange = { query = it }, placeholder = nativeString("Search OpenClaw"))
}
item {
CommandSectionLabel(title = "Quick actions")
CommandSectionLabel(title = nativeString("Quick actions"))
}
if (actionRows.isEmpty()) {
item {
ClawEmptyState(title = "No actions found", body = "Try Chat, Voice, Sessions, Providers, or Settings.")
ClawEmptyState(title = nativeString("No actions found"), body = nativeString("Try Chat, Voice, Sessions, Providers, or Settings."))
}
} else {
item {
@@ -124,14 +125,14 @@ internal fun CommandPalette(
}
item {
CommandSectionLabel(title = "Sessions")
CommandSectionLabel(title = nativeString("Sessions"))
}
if (sessionRows.isEmpty()) {
item {
ClawPanel {
Text(
text = if (isConnected) "No matching sessions yet." else "Connect the Gateway to search sessions.",
text = if (isConnected) nativeString("No matching sessions yet.") else nativeString("Connect the Gateway to search sessions."),
style = ClawTheme.type.body,
color = ClawTheme.colors.textMuted,
)
@@ -145,7 +146,7 @@ internal fun CommandPalette(
CommandSessionRow(
key = session.key,
title = commandSessionTitle(session.displayName),
subtitle = if (pendingRunCount > 0) "Assistant working" else "OpenClaw session",
subtitle = if (pendingRunCount > 0) nativeString("Assistant working") else nativeString("OpenClaw session"),
metadata = session.updatedAtMs?.let(::commandRelativeTime) ?: "now",
)
},
@@ -200,10 +201,10 @@ private fun CommandActionRow(row: CommandItem) {
) {
CommandRowIcon(icon = row.icon)
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(1.dp)) {
Text(text = row.title, style = ClawTheme.type.body, color = ClawTheme.colors.text, maxLines = 1, overflow = TextOverflow.Ellipsis)
Text(text = row.subtitle, style = ClawTheme.type.caption, color = ClawTheme.colors.textMuted, maxLines = 1, overflow = TextOverflow.Ellipsis)
Text(text = nativeString(row.title), style = ClawTheme.type.body, color = ClawTheme.colors.text, maxLines = 1, overflow = TextOverflow.Ellipsis)
Text(text = nativeString(row.subtitle), style = ClawTheme.type.caption, color = ClawTheme.colors.textMuted, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
CommandRowChevron(contentDescription = "Open ${row.title}")
CommandRowChevron(contentDescription = nativeString("Open \${row.title}", row.title))
}
}
}
@@ -243,7 +244,7 @@ private fun CommandSessionListRow(
Text(text = row.subtitle, style = ClawTheme.type.caption, color = ClawTheme.colors.textSubtle, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
Text(text = row.metadata, style = ClawTheme.type.caption, color = ClawTheme.colors.textMuted, maxLines = 1, overflow = TextOverflow.Ellipsis)
CommandRowChevron(contentDescription = "Open session")
CommandRowChevron(contentDescription = nativeString("Open session"))
}
}
}
@@ -301,16 +302,16 @@ internal fun providerCommandSubtitle(
providers: List<GatewayModelProviderSummary>,
models: List<GatewayModelSummary>,
): String {
if (!isConnected) return "Connect Gateway to view providers"
if (!isConnected) return nativeString("Connect Gateway to view providers")
val rows = providerRows(providers = providers, models = models)
val readyProviderCount = rows.count { it.ready }
if (readyProviderCount > 0) return "$readyProviderCount providers ready"
if (rows.any { it.availability == ProviderAvailability.Unknown }) return "Provider availability unknown"
return "No ready providers"
if (readyProviderCount > 0) return nativeString("\$readyProviderCount providers ready", readyProviderCount)
if (rows.any { it.availability == ProviderAvailability.Unknown }) return nativeString("Provider availability unknown")
return nativeString("No ready providers")
}
/** Falls back to the canonical main-session label when gateway display names are blank. */
private fun commandSessionTitle(displayName: String?): String = displayName?.takeIf { it.isNotBlank() } ?: "Main session"
private fun commandSessionTitle(displayName: String?): String = displayName?.takeIf { it.isNotBlank() } ?: nativeString("Main session")
/** Formats command-palette session timestamps for compact rows. */
private fun commandRelativeTime(updatedAtMs: Long): String {

View File

@@ -1,6 +1,7 @@
package ai.openclaw.app.ui
import ai.openclaw.app.gateway.isLocalCleartextGatewayHost
import ai.openclaw.app.i18n.nativeString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
@@ -304,26 +305,26 @@ internal fun gatewayEndpointValidationMessage(
GatewayEndpointValidationError.INSECURE_REMOTE_URL ->
when (source) {
GatewayEndpointInputSource.SETUP_CODE ->
"Setup code points to an insecure remote gateway. $remoteGatewaySecurityRule $remoteGatewaySecurityFix"
nativeString("Setup code points to an insecure remote gateway. \$remoteGatewaySecurityRule \$remoteGatewaySecurityFix", remoteGatewaySecurityRule, remoteGatewaySecurityFix)
GatewayEndpointInputSource.QR_SCAN ->
"QR code points to an insecure remote gateway. $remoteGatewaySecurityRule $remoteGatewaySecurityFix"
nativeString("QR code points to an insecure remote gateway. \$remoteGatewaySecurityRule \$remoteGatewaySecurityFix", remoteGatewaySecurityRule, remoteGatewaySecurityFix)
GatewayEndpointInputSource.MANUAL ->
"$remoteGatewaySecurityRule $remoteGatewaySecurityFix"
nativeString("\$remoteGatewaySecurityRule \$remoteGatewaySecurityFix", remoteGatewaySecurityRule, remoteGatewaySecurityFix)
}
GatewayEndpointValidationError.IPV6_ZONE_ID_UNSUPPORTED ->
when (source) {
GatewayEndpointInputSource.SETUP_CODE ->
"Setup code uses an IPv6 zone ID. Use an unscoped IPv6 address or a LAN hostname."
nativeString("Setup code uses an IPv6 zone ID. Use an unscoped IPv6 address or a LAN hostname.")
GatewayEndpointInputSource.QR_SCAN ->
"QR code uses an IPv6 zone ID. Use an unscoped IPv6 address or a LAN hostname."
nativeString("QR code uses an IPv6 zone ID. Use an unscoped IPv6 address or a LAN hostname.")
GatewayEndpointInputSource.MANUAL ->
"IPv6 zone IDs are not supported. Use an unscoped IPv6 address or a LAN hostname."
nativeString("IPv6 zone IDs are not supported. Use an unscoped IPv6 address or a LAN hostname.")
}
GatewayEndpointValidationError.INVALID_URL ->
when (source) {
GatewayEndpointInputSource.SETUP_CODE -> "Setup code has invalid gateway URL."
GatewayEndpointInputSource.QR_SCAN -> "QR code did not contain a valid setup code."
GatewayEndpointInputSource.MANUAL -> "Enter a valid manual endpoint to connect."
GatewayEndpointInputSource.SETUP_CODE -> nativeString("Setup code has invalid gateway URL.")
GatewayEndpointInputSource.QR_SCAN -> nativeString("QR code did not contain a valid setup code.")
GatewayEndpointInputSource.MANUAL -> nativeString("Enter a valid manual endpoint to connect.")
}
}

View File

@@ -5,6 +5,7 @@ import ai.openclaw.app.GatewayConnectionProblem
import ai.openclaw.app.GatewayNodeApprovalState
import ai.openclaw.app.GatewayNodeCapabilityApproval
import ai.openclaw.app.gateway.normalizeGatewayApprovalRequestId
import ai.openclaw.app.i18n.nativeString
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
@@ -22,7 +23,7 @@ internal fun openClawAndroidVersionLabel(): String {
}
/** Normalizes blank gateway status text for display and diagnostics copy. */
internal fun gatewayStatusForDisplay(statusText: String): String = statusText.trim().ifEmpty { "Offline" }
internal fun gatewayStatusForDisplay(statusText: String): String = statusText.trim().ifEmpty { nativeString("Offline") }
/** Resolves the best non-secret endpoint label available to diagnostics surfaces. */
internal fun gatewayDiagnosticsEndpoint(
@@ -77,15 +78,15 @@ private enum class GatewayAuthRecoveryLabelKind {
private fun gatewayAuthRecoveryLabel(kind: GatewayAuthRecoveryLabelKind): String =
when (kind) {
GatewayAuthRecoveryLabelKind.SETUP_CODE_EXPIRED -> "Setup code expired"
GatewayAuthRecoveryLabelKind.TOKEN_NEEDED -> "Gateway token needed"
GatewayAuthRecoveryLabelKind.TOKEN_NOT_CONFIGURED -> "Gateway token not configured"
GatewayAuthRecoveryLabelKind.PASSWORD_NEEDED -> "Gateway password needed"
GatewayAuthRecoveryLabelKind.PASSWORD_INVALID -> "Gateway password invalid"
GatewayAuthRecoveryLabelKind.PASSWORD_NOT_CONFIGURED -> "Gateway password not configured"
GatewayAuthRecoveryLabelKind.ACCESS_NEEDS_REVIEW -> "Gateway access needs review"
GatewayAuthRecoveryLabelKind.SAVED_AUTH_INVALID -> "Saved auth invalid"
GatewayAuthRecoveryLabelKind.DEVICE_IDENTITY_REQUIRED -> "Device identity required"
GatewayAuthRecoveryLabelKind.SETUP_CODE_EXPIRED -> nativeString("Setup code expired")
GatewayAuthRecoveryLabelKind.TOKEN_NEEDED -> nativeString("Gateway token needed")
GatewayAuthRecoveryLabelKind.TOKEN_NOT_CONFIGURED -> nativeString("Gateway token not configured")
GatewayAuthRecoveryLabelKind.PASSWORD_NEEDED -> nativeString("Gateway password needed")
GatewayAuthRecoveryLabelKind.PASSWORD_INVALID -> nativeString("Gateway password invalid")
GatewayAuthRecoveryLabelKind.PASSWORD_NOT_CONFIGURED -> nativeString("Gateway password not configured")
GatewayAuthRecoveryLabelKind.ACCESS_NEEDS_REVIEW -> nativeString("Gateway access needs review")
GatewayAuthRecoveryLabelKind.SAVED_AUTH_INVALID -> nativeString("Saved auth invalid")
GatewayAuthRecoveryLabelKind.DEVICE_IDENTITY_REQUIRED -> nativeString("Device identity required")
}
/** Returns the exact host command for one node's approval state when available. */
@@ -168,5 +169,5 @@ internal fun copyGatewayDiagnosticsReport(
val clipboard = context.getSystemService(ClipboardManager::class.java) ?: return
val report = buildGatewayDiagnosticsReport(screen = screen, gatewayAddress = gatewayAddress, statusText = statusText)
clipboard.setPrimaryClip(ClipData.newPlainText("OpenClaw gateway diagnostics", report))
Toast.makeText(context, "Copied gateway diagnostics", Toast.LENGTH_SHORT).show()
Toast.makeText(context, nativeString("Copied gateway diagnostics"), Toast.LENGTH_SHORT).show()
}

View File

@@ -9,6 +9,7 @@ import ai.openclaw.app.SensitiveFeatureConfig
import ai.openclaw.app.gateway.GatewayEndpoint
import ai.openclaw.app.gateway.isLocalCleartextGatewayHost
import ai.openclaw.app.hasPhotoReadPermission
import ai.openclaw.app.i18n.nativeString
import ai.openclaw.app.node.DeviceNotificationListenerService
import ai.openclaw.app.photoReadPermissionsForRequest
import ai.openclaw.app.ui.design.ClawDesignTheme
@@ -695,12 +696,12 @@ fun OnboardingFlow(
},
confirmButton = {
TextButton(onClick = viewModel::acceptGatewayTrustPrompt) {
Text("Trust")
Text(nativeString("Trust"))
}
},
dismissButton = {
TextButton(onClick = viewModel::declineGatewayTrustPrompt) {
Text("Cancel")
Text(nativeString("Cancel"))
}
},
)
@@ -891,8 +892,8 @@ private fun WelcomeScreen(
Column(modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally) {
OnboardingHeroTopSpacer(afterHeader = false)
OnboardingIntroHero(
title = "Welcome to OpenClaw",
subtitle = "Turn this device into a secure OpenClaw node for chat, voice, camera, and device tools.",
title = nativeString("Welcome to OpenClaw"),
subtitle = nativeString("Turn this device into a secure OpenClaw node for chat, voice, camera, and device tools."),
mark = { WelcomeLogo() },
)
Spacer(modifier = Modifier.height(24.dp))
@@ -901,7 +902,7 @@ private fun WelcomeScreen(
SecurityNotice()
Spacer(modifier = Modifier.weight(1f))
OnboardingActions {
ClawPrimaryButton(text = "Continue", onClick = onConnect, modifier = Modifier.onboardingActionButton())
ClawPrimaryButton(text = nativeString("Continue"), onClick = onConnect, modifier = Modifier.onboardingActionButton())
}
}
}
@@ -917,7 +918,7 @@ private fun WelcomeLogo() {
border = BorderStroke(1.dp, ClawTheme.colors.border),
) {
Box(modifier = Modifier.fillMaxSize().padding(12.dp), contentAlignment = Alignment.Center) {
OpenClawMascot(contentDescription = "OpenClaw logo", modifier = Modifier.fillMaxSize())
OpenClawMascot(contentDescription = nativeString("OpenClaw logo"), modifier = Modifier.fillMaxSize())
}
}
}
@@ -973,9 +974,9 @@ private fun OnboardingHeroTopSpacer(afterHeader: Boolean) {
private fun WelcomeChecklist() {
SoftPanel {
Column(verticalArrangement = Arrangement.spacedBy(13.dp)) {
WelcomeChecklistRow(icon = Icons.Default.Link, text = "Connect to your Gateway")
WelcomeChecklistRow(icon = Icons.Default.Security, text = "Choose device permissions")
WelcomeChecklistRow(icon = Icons.Default.CheckCircle, text = "Use OpenClaw from your phone")
WelcomeChecklistRow(icon = Icons.Default.Link, text = nativeString("Connect to your Gateway"))
WelcomeChecklistRow(icon = Icons.Default.Security, text = nativeString("Choose device permissions"))
WelcomeChecklistRow(icon = Icons.Default.CheckCircle, text = nativeString("Use OpenClaw from your phone"))
}
}
}
@@ -997,9 +998,9 @@ private fun SecurityNotice() {
Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.Top) {
Icon(imageVector = Icons.Default.ErrorOutline, contentDescription = null, modifier = Modifier.size(24.dp), tint = ClawTheme.colors.warning)
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
Text(text = "Security notice", style = ClawTheme.type.section, color = ClawTheme.colors.text)
Text(text = nativeString("Security notice"), style = ClawTheme.type.section, color = ClawTheme.colors.text)
Text(
text = "The connected OpenClaw agent can use device capabilities you enable. Continue only if you trust the Gateway and agent you connect to.",
text = nativeString("The connected OpenClaw agent can use device capabilities you enable. Continue only if you trust the Gateway and agent you connect to."),
style = ClawTheme.type.body,
color = ClawTheme.colors.textMuted,
)
@@ -1040,8 +1041,8 @@ private fun GatewaySetupScreen(
OnboardingHeroTopSpacer(afterHeader = true)
Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
OnboardingIntroHero(
title = "Connect Gateway",
subtitle = "Scan a QR code or use the setup code from your OpenClaw Gateway.",
title = nativeString("Connect Gateway"),
subtitle = nativeString("Scan a QR code or use the setup code from your OpenClaw Gateway."),
mark = { GatewayLogo() },
)
Spacer(modifier = Modifier.height(24.dp))
@@ -1050,7 +1051,7 @@ private fun GatewaySetupScreen(
runCatching {
uriHandler.openUri(ANDROID_SETUP_GUIDE_URL)
}.onFailure {
Toast.makeText(context, "Could not open setup guide.", Toast.LENGTH_SHORT).show()
Toast.makeText(context, nativeString("Could not open setup guide."), Toast.LENGTH_SHORT).show()
}
},
)
@@ -1058,13 +1059,13 @@ private fun GatewaySetupScreen(
Spacer(modifier = Modifier.weight(1f))
OnboardingActions {
ClawPrimaryButton(
text = "Scan QR or setup code",
text = nativeString("Scan QR or setup code"),
icon = Icons.Default.QrCode2,
onClick = onSetupCode,
modifier = Modifier.onboardingActionButton(),
)
ClawSecondaryButton(
text = "Set up manually",
text = nativeString("Set up manually"),
icon = Icons.Default.Link,
onClick = onManualSetup,
modifier = Modifier.onboardingActionButton(),
@@ -1086,24 +1087,24 @@ private fun OnboardingActions(content: @Composable ColumnScope.() -> Unit) {
private fun GatewayPrerequisites(onOpenSetupGuide: () -> Unit) {
Column(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(14.dp)) {
Text(
text = "Before you start",
text = nativeString("Before you start"),
style = ClawTheme.type.label,
color = ClawTheme.colors.text,
modifier = Modifier.fillMaxWidth(),
)
GatewayPrerequisiteRow(
title = "Access to the Gateway device",
body = "Have a terminal open on the device running OpenClaw.",
title = nativeString("Access to the Gateway device"),
body = nativeString("Have a terminal open on the device running OpenClaw."),
)
GatewayPrerequisiteRow(
title = "Phone can reach the Gateway",
body = "Use the same network, or a secure remote Gateway URL.",
title = nativeString("Phone can reach the Gateway"),
body = nativeString("Use the same network, or a secure remote Gateway URL."),
)
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
TextButton(onClick = onOpenSetupGuide) {
Icon(imageVector = Icons.Default.Link, contentDescription = null, modifier = Modifier.size(16.dp), tint = ClawTheme.colors.primary)
Spacer(modifier = Modifier.width(7.dp))
Text(text = "Android setup guide", style = ClawTheme.type.label, color = ClawTheme.colors.primary)
Text(text = nativeString("Android setup guide"), style = ClawTheme.type.label, color = ClawTheme.colors.primary)
}
}
}
@@ -1146,19 +1147,19 @@ private fun SetupCodeInstructionsScreen(
verticalArrangement = Arrangement.spacedBy(18.dp),
) {
item {
OnboardingHeader(title = "Setup Gateway", onBack = onBack)
OnboardingHeader(title = nativeString("Setup Gateway"), onBack = onBack)
}
item {
Column(modifier = Modifier.fillMaxWidth().padding(top = 8.dp), verticalArrangement = Arrangement.spacedBy(18.dp)) {
SetupInstruction(
step = "Step 1",
title = "Start your Gateway.",
title = nativeString("Start your Gateway."),
body = "openclaw gateway",
monospaceBody = true,
)
SetupInstruction(
step = "Step 2",
title = "Generate a QR code.",
title = nativeString("Generate a QR code."),
body = "openclaw qr",
monospaceBody = true,
)
@@ -1179,13 +1180,13 @@ private fun SetupCodeInstructionsScreen(
}
OnboardingActions {
ClawSecondaryButton(
text = "Choose from gallery",
text = nativeString("Choose from gallery"),
icon = Icons.Default.Image,
onClick = onChooseFromGallery,
modifier = Modifier.onboardingActionButton(),
)
ClawSecondaryButton(
text = "Enter setup code",
text = nativeString("Enter setup code"),
icon = Icons.Default.QrCode2,
onClick = onEnterSetupCode,
modifier = Modifier.onboardingActionButton(),
@@ -1233,7 +1234,7 @@ private fun SetupScanErrorDialog(
}
}
Text(
text = "QR code not accepted",
text = nativeString("QR code not accepted"),
style = ClawTheme.type.title,
color = ClawTheme.colors.text,
modifier = Modifier.weight(1f),
@@ -1248,13 +1249,13 @@ private fun SetupScanErrorDialog(
Column(verticalArrangement = Arrangement.spacedBy(OnboardingActionGap), modifier = Modifier.fillMaxWidth()) {
ClawPrimaryButton(
text = "Choose another image",
text = nativeString("Choose another image"),
icon = Icons.Default.Image,
onClick = onChooseAnotherImage,
modifier = Modifier.onboardingActionButton(),
)
ClawSecondaryButton(
text = "Enter setup code",
text = nativeString("Enter setup code"),
icon = Icons.Default.QrCode2,
onClick = onEnterSetupCode,
modifier = Modifier.onboardingActionButton(),
@@ -1301,9 +1302,9 @@ private fun ScanQrTile(
Icon(imageVector = Icons.Default.CameraAlt, contentDescription = null, modifier = Modifier.size(28.dp))
}
}
Text(text = "Scan QR code", style = ClawTheme.type.title.copy(fontSize = 20.sp, lineHeight = 25.sp), color = ClawTheme.colors.text, textAlign = TextAlign.Center)
Text(text = nativeString("Scan QR code"), style = ClawTheme.type.title.copy(fontSize = 20.sp, lineHeight = 25.sp), color = ClawTheme.colors.text, textAlign = TextAlign.Center)
Text(
text = "Open the camera and frame the code from openclaw qr.",
text = nativeString("Open the camera and frame the code from openclaw qr."),
style = ClawTheme.type.caption,
color = ClawTheme.colors.textMuted,
textAlign = TextAlign.Center,
@@ -1337,7 +1338,7 @@ private fun ScanQrTile(
)
}
Text(
text = "Align the QR code inside the square.",
text = nativeString("Align the QR code inside the square."),
style = ClawTheme.type.caption,
color = Color.White,
modifier =
@@ -1355,12 +1356,12 @@ private fun ScanQrTile(
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(12.dp)) {
Icon(imageVector = Icons.Default.CameraAlt, contentDescription = null, modifier = Modifier.size(36.dp), tint = ClawTheme.colors.text)
Text(
text = "Camera access is needed to scan the setup QR.",
text = nativeString("Camera access is needed to scan the setup QR."),
style = ClawTheme.type.body,
color = ClawTheme.colors.textMuted,
textAlign = TextAlign.Center,
)
ClawPrimaryButton(text = "Allow camera", icon = Icons.Default.CameraAlt, onClick = onRequestCameraPermission, modifier = Modifier.onboardingActionButton())
ClawPrimaryButton(text = nativeString("Allow camera"), icon = Icons.Default.CameraAlt, onClick = onRequestCameraPermission, modifier = Modifier.onboardingActionButton())
}
}
}
@@ -1381,7 +1382,7 @@ private fun ScannerCloseButton(
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.26f)),
) {
Box(contentAlignment = Alignment.Center) {
Icon(imageVector = Icons.Default.Close, contentDescription = "Close scanner", modifier = Modifier.size(20.dp))
Icon(imageVector = Icons.Default.Close, contentDescription = nativeString("Close scanner"), modifier = Modifier.size(20.dp))
}
}
}
@@ -1543,20 +1544,20 @@ private fun SetupCodeEntryScreen(
ClawScaffold(modifier = modifier, contentPadding = onboardingContentPadding()) {
Column(modifier = Modifier.fillMaxSize().imePadding(), verticalArrangement = Arrangement.SpaceBetween) {
Column(verticalArrangement = Arrangement.spacedBy(18.dp)) {
OnboardingHeader(title = "Enter setup code", onBack = onBack)
LabeledField(label = "Setup code") {
OnboardingHeader(title = nativeString("Enter setup code"), onBack = onBack)
LabeledField(label = nativeString("Setup code")) {
ClawTextField(
value = setupCode,
onValueChange = onSetupCodeChange,
placeholder = "Paste setup code",
placeholder = nativeString("Paste setup code"),
)
}
error?.let { message ->
InlineError(title = "Setup code was not accepted", body = message)
InlineError(title = nativeString("Setup code was not accepted"), body = message)
}
}
OnboardingActions {
ClawPrimaryButton(text = "Use setup code", icon = Icons.Default.QrCode2, onClick = onUseSetupCode, modifier = Modifier.onboardingActionButton())
ClawPrimaryButton(text = nativeString("Use setup code"), icon = Icons.Default.QrCode2, onClick = onUseSetupCode, modifier = Modifier.onboardingActionButton())
}
}
}
@@ -1594,47 +1595,47 @@ private fun ManualGatewaySetupScreen(
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
item {
OnboardingHeader(title = "Manual setup", onBack = onBack)
OnboardingHeader(title = nativeString("Manual setup"), onBack = onBack)
}
item {
LabeledField(label = "Gateway URL") {
LabeledField(label = nativeString("Gateway URL")) {
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
ClawTextField(value = manualHost, onValueChange = onManualHostChange, placeholder = "Host", modifier = Modifier.weight(1f))
ClawTextField(value = manualPort, onValueChange = onManualPortChange, placeholder = "Port", modifier = Modifier.width(104.dp))
ClawTextField(value = manualHost, onValueChange = onManualHostChange, placeholder = nativeString("Host"), modifier = Modifier.weight(1f))
ClawTextField(value = manualPort, onValueChange = onManualPortChange, placeholder = nativeString("Port"), modifier = Modifier.width(104.dp))
}
Text(
text = "Use the Gateway computer's LAN address or secure remote hostname.",
text = nativeString("Use the Gateway computer's LAN address or secure remote hostname."),
style = ClawTheme.type.caption,
color = ClawTheme.colors.textMuted,
)
}
}
item {
LabeledField(label = "Token") {
ClawTextField(value = token, onValueChange = onTokenChange, placeholder = "Paste token")
LabeledField(label = nativeString("Token")) {
ClawTextField(value = token, onValueChange = onTokenChange, placeholder = nativeString("Paste token"))
Text(
text = "Paste a shared Gateway token or operator-issued token.",
text = nativeString("Paste a shared Gateway token or operator-issued token."),
style = ClawTheme.type.caption,
color = ClawTheme.colors.textMuted,
)
}
}
item {
LabeledField(label = "Password") {
ClawTextField(value = password, onValueChange = onPasswordChange, placeholder = "Password optional")
LabeledField(label = nativeString("Password")) {
ClawTextField(value = password, onValueChange = onPasswordChange, placeholder = nativeString("Password optional"))
}
}
item {
LabeledField(label = "Connection security") {
LabeledField(label = nativeString("Connection security")) {
Row(horizontalArrangement = Arrangement.spacedBy(9.dp)) {
TogglePill(
text = "Unencrypted",
text = nativeString("Unencrypted"),
selected = !transport.effectiveTls,
enabled = !transport.requiresTls,
onClick = { onManualTlsChange(false) },
)
TogglePill(
text = "Secure (TLS)",
text = nativeString("Secure (TLS)"),
selected = transport.effectiveTls,
onClick = { onManualTlsChange(true) },
)
@@ -1650,12 +1651,12 @@ private fun ManualGatewaySetupScreen(
}
error?.let { message ->
item {
InlineError(title = "Could not test connection", body = message)
InlineError(title = nativeString("Could not test connection"), body = message)
}
}
}
OnboardingActions {
ClawPrimaryButton(text = "Test connection", icon = Icons.Default.Security, onClick = onPair, modifier = Modifier.onboardingActionButton())
ClawPrimaryButton(text = nativeString("Test connection"), icon = Icons.Default.Security, onClick = onPair, modifier = Modifier.onboardingActionButton())
}
}
}
@@ -1740,18 +1741,21 @@ private fun GatewayRecoveryScreen(
val protocolUpdateCommand = recoveryGatewayProtocolMismatchCommand(gatewayConnectionProblem)
val recoveryTitle =
when {
recoveryState == GatewayRecoveryUiState.Connected -> "Gateway paired"
gatewayConnectionProblem?.code == "AUTH_BOOTSTRAP_TOKEN_INVALID" -> "Setup code was not accepted"
else -> recoveryState.title
recoveryState == GatewayRecoveryUiState.Connected -> nativeString("Gateway paired")
gatewayConnectionProblem?.code == "AUTH_BOOTSTRAP_TOKEN_INVALID" ->
nativeString("Setup code was not accepted")
else -> nativeString(recoveryState.title)
}
val recoveryMessage =
when {
recoveryState == GatewayRecoveryUiState.Connected ->
"Your phone is paired with ${recoveryGatewayName(serverName = serverName, attemptedGatewayName = attemptedGatewayName)}. " +
"Continue to finish node access."
nativeString(
"Your phone is paired with \${recoveryGatewayName(serverName = serverName, attemptedGatewayName = attemptedGatewayName)}. Continue to finish node access.",
recoveryGatewayName(serverName = serverName, attemptedGatewayName = attemptedGatewayName),
)
gatewayConnectionProblem != null && recoveryState == GatewayRecoveryUiState.Failed ->
recoveryGatewayAuthDetail(gatewayConnectionProblem)
else -> recoveryState.message
else -> nativeString(recoveryState.message)
}
val recoveryProgressItems =
gatewayRecoveryProgressItems(
@@ -1797,8 +1801,8 @@ private fun GatewayRecoveryScreen(
OnboardingHeader(
title =
when (recoveryState) {
GatewayRecoveryUiState.Connected -> "Gateway paired"
else -> "Pair Gateway"
GatewayRecoveryUiState.Connected -> nativeString("Gateway paired")
else -> nativeString("Pair Gateway")
},
onBack = onBack,
)
@@ -1830,7 +1834,7 @@ private fun GatewayRecoveryScreen(
protocolUpdateCommand?.let { command ->
Spacer(modifier = Modifier.height(18.dp))
Text(
text = "On the Gateway computer, run:",
text = nativeString("On the Gateway computer, run:"),
style = ClawTheme.type.caption,
color = ClawTheme.colors.textMuted,
)
@@ -1844,7 +1848,7 @@ private fun GatewayRecoveryScreen(
if (showDiagnosticAction) {
Spacer(modifier = Modifier.height(14.dp))
TextButton(onClick = { diagnosticDialogVisible = true }) {
Text("View details", style = ClawTheme.type.body, color = ClawTheme.colors.text)
Text(nativeString("View details"), style = ClawTheme.type.body, color = ClawTheme.colors.text)
}
}
}
@@ -1852,7 +1856,7 @@ private fun GatewayRecoveryScreen(
primaryAction?.let { action ->
OnboardingActions {
ClawPrimaryButton(
text = action.text,
text = nativeString(action.text),
icon = action.icon,
onClick =
when (action) {
@@ -1877,7 +1881,7 @@ private fun GatewayRecoveryDiagnosticDialog(
AlertDialog(
onDismissRequest = onDismiss,
containerColor = ClawTheme.colors.surfaceRaised,
title = { Text("Connection details", style = ClawTheme.type.section, color = ClawTheme.colors.text) },
title = { Text(nativeString("Connection details"), style = ClawTheme.type.section, color = ClawTheme.colors.text) },
text = {
SelectionContainer {
Text(
@@ -1889,12 +1893,12 @@ private fun GatewayRecoveryDiagnosticDialog(
},
confirmButton = {
TextButton(onClick = onCopy) {
Text("Copy")
Text(nativeString("Copy"))
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("Close")
Text(nativeString("Close"))
}
},
)
@@ -1906,7 +1910,7 @@ private fun copyGatewayDiagnostic(
) {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(ClipData.newPlainText("OpenClaw gateway diagnostic", diagnosticText))
Toast.makeText(context, "Details copied", Toast.LENGTH_SHORT).show()
Toast.makeText(context, nativeString("Details copied"), Toast.LENGTH_SHORT).show()
}
@Composable
@@ -1976,7 +1980,7 @@ private fun NodeApprovalScreen(
ClawScaffold(modifier = modifier, contentPadding = onboardingContentPadding()) {
Column(modifier = Modifier.fillMaxSize()) {
OnboardingHeader(title = "Approve node access", onBack = onBack)
OnboardingHeader(title = nativeString("Approve node access"), onBack = onBack)
Column(
modifier =
@@ -1991,14 +1995,14 @@ private fun NodeApprovalScreen(
GatewayRecoveryIcon(state = GatewayRecoveryUiState.NodeCapabilityApprovalPending)
Spacer(modifier = Modifier.height(13.dp))
Text(
text = "Approve node access",
text = nativeString("Approve node access"),
style = ClawTheme.type.display,
color = ClawTheme.colors.text,
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Gateway pairing is complete. Approve this phone as a node so OpenClaw can use the device capabilities you enable.",
text = nativeString("Gateway pairing is complete. Approve this phone as a node so OpenClaw can use the device capabilities you enable."),
style = ClawTheme.type.body,
color = ClawTheme.colors.textMuted,
textAlign = TextAlign.Center,
@@ -2006,7 +2010,7 @@ private fun NodeApprovalScreen(
Spacer(modifier = Modifier.height(18.dp))
Column(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(
text = "On the Gateway computer, run:",
text = nativeString("On the Gateway computer, run:"),
style = ClawTheme.type.caption,
color = ClawTheme.colors.textMuted,
textAlign = TextAlign.Center,
@@ -2015,7 +2019,7 @@ private fun NodeApprovalScreen(
ApprovalCommandBlock(command = "openclaw nodes pending", onCopy = { onCopyCommand("openclaw nodes pending") })
ApprovalCommandBlock(command = approveCommand, onCopy = { onCopyCommand(approveCommand) })
Text(
text = "Use the requestId from the pending command in the approve command.",
text = nativeString("Use the requestId from the pending command in the approve command."),
style = ClawTheme.type.caption,
color = ClawTheme.colors.textSubtle,
textAlign = TextAlign.Center,
@@ -2039,17 +2043,17 @@ private fun NodeApprovalScreen(
if (showWaitingDialog) {
AlertDialog(
onDismissRequest = { waitingDialogDismissed = true },
title = { Text(text = "Still waiting for approval") },
title = { Text(text = nativeString("Still waiting for approval")) },
text = {
Text(
text = "Run the approve command on the Gateway computer, then check again.",
text = nativeString("Run the approve command on the Gateway computer, then check again."),
style = ClawTheme.type.body,
color = ClawTheme.colors.textMuted,
)
},
confirmButton = {
TextButton(onClick = { waitingDialogDismissed = true }) {
Text(text = "OK")
Text(text = nativeString("OK"))
}
},
)
@@ -2088,7 +2092,7 @@ private fun OnboardingLoadingPrimaryButton(
)
Spacer(modifier = Modifier.width(8.dp))
}
Text(text = if (loading) loadingText else text, style = ClawTheme.type.label)
Text(text = nativeString(if (loading) loadingText else text), style = ClawTheme.type.label)
}
}
@@ -2199,7 +2203,7 @@ private fun ApprovalCommandBlock(
border = BorderStroke(1.dp, ClawTheme.colors.border),
) {
Box(contentAlignment = Alignment.Center) {
Icon(imageVector = Icons.Default.ContentCopy, contentDescription = "Copy approval command", modifier = Modifier.size(18.dp))
Icon(imageVector = Icons.Default.ContentCopy, contentDescription = nativeString("Copy approval command"), modifier = Modifier.size(18.dp))
}
}
}
@@ -2225,7 +2229,7 @@ private fun PermissionSetupScreen(
}
item {
Text(
text = "Only enable access you are comfortable letting OpenClaw use while this phone is connected. You can change these later in Android Settings.",
text = nativeString("Only enable access you are comfortable letting OpenClaw use while this phone is connected. You can change these later in Android Settings."),
style = ClawTheme.type.body,
color = ClawTheme.colors.textMuted,
textAlign = TextAlign.Center,
@@ -2237,7 +2241,7 @@ private fun PermissionSetupScreen(
}
}
OnboardingActions {
ClawPrimaryButton(text = "Continue", onClick = onContinue, modifier = Modifier.onboardingActionButton())
ClawPrimaryButton(text = nativeString("Continue"), onClick = onContinue, modifier = Modifier.onboardingActionButton())
}
}
}
@@ -2264,7 +2268,7 @@ private fun OnboardingHeader(
contentColor = ClawTheme.colors.text,
) {
Box(contentAlignment = Alignment.CenterStart) {
Icon(imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back", modifier = Modifier.size(23.dp))
Icon(imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = nativeString("Back"), modifier = Modifier.size(23.dp))
}
}
}
@@ -2274,10 +2278,10 @@ private fun OnboardingHeader(
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
if (title.isNotBlank()) {
Text(text = title, style = ClawTheme.type.title, color = ClawTheme.colors.text, textAlign = TextAlign.Center)
Text(text = nativeString(title), style = ClawTheme.type.title, color = ClawTheme.colors.text, textAlign = TextAlign.Center)
}
subtitle?.let {
Text(text = it, style = ClawTheme.type.body, color = ClawTheme.colors.textMuted, textAlign = TextAlign.Center)
Text(text = nativeString(it), style = ClawTheme.type.body, color = ClawTheme.colors.textMuted, textAlign = TextAlign.Center)
}
}
action?.let {
@@ -2313,7 +2317,7 @@ private fun TogglePill(
@Composable
private fun PermissionTopBar(onBack: () -> Unit) {
OnboardingHeader(title = "Permissions", onBack = onBack)
OnboardingHeader(title = nativeString("Permissions"), onBack = onBack)
}
@Composable
@@ -2343,19 +2347,19 @@ private fun PermissionRow(row: PermissionRowModel) {
}
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text(
text = row.title,
text = nativeString(row.title),
style = ClawTheme.type.title.copy(fontSize = 18.sp, lineHeight = 23.sp),
color = ClawTheme.colors.text,
)
Text(
text = row.subtitle,
text = nativeString(row.subtitle),
style = ClawTheme.type.body,
color = ClawTheme.colors.textMuted,
)
}
Icon(
imageVector = if (row.granted) Icons.Default.CheckCircle else Icons.Default.Close,
contentDescription = row.statusText,
contentDescription = nativeString(row.statusText),
modifier = Modifier.size(20.dp),
tint = if (row.granted) ClawTheme.colors.success else ClawTheme.colors.danger,
)
@@ -2526,7 +2530,7 @@ private fun finishingGatewayProgressItems(
val nodeAccessCurrent = gatewayAccessComplete
return listOf(
GatewayRecoveryProgressItem(
label = "Opening Gateway connection",
label = nativeString("Opening Gateway connection"),
status =
if (gatewayAccessComplete) {
GatewayRecoveryProgressStatus.Complete
@@ -2535,7 +2539,7 @@ private fun finishingGatewayProgressItems(
},
),
GatewayRecoveryProgressItem(
label = "Checking pairing access",
label = nativeString("Checking pairing access"),
status =
when {
gatewayAccessComplete -> GatewayRecoveryProgressStatus.Complete
@@ -2543,7 +2547,7 @@ private fun finishingGatewayProgressItems(
},
),
GatewayRecoveryProgressItem(
label = "Checking node access",
label = nativeString("Checking node access"),
status =
when {
nodeAccessCurrent -> GatewayRecoveryProgressStatus.Current
@@ -2636,21 +2640,21 @@ internal fun recoveryGatewayDetail(
internal fun recoveryGatewayAuthDetail(gatewayConnectionProblem: GatewayConnectionProblem): String =
when (gatewayConnectionProblem.code) {
"PROTOCOL_MISMATCH" -> recoveryGatewayProtocolMismatchDetail(gatewayConnectionProblem)
"AUTH_BOOTSTRAP_TOKEN_INVALID" -> "The code may have expired or been generated for another Gateway."
"AUTH_BOOTSTRAP_TOKEN_INVALID" -> nativeString("The code may have expired or been generated for another Gateway.")
"AUTH_DEVICE_TOKEN_MISMATCH",
"AUTH_TOKEN_MISMATCH",
-> "Saved authentication is invalid. Re-authenticate or reset this gateway connection."
"AUTH_PASSWORD_MISSING" -> "Gateway password is required. Enter it again or edit this connection."
"AUTH_PASSWORD_MISMATCH" -> "Gateway password is invalid. Re-enter it or reset this gateway connection."
"AUTH_TOKEN_MISSING" -> "Gateway token is required. Enter it again or edit this connection."
-> nativeString("Saved authentication is invalid. Re-authenticate or reset this gateway connection.")
"AUTH_PASSWORD_MISSING" -> nativeString("Gateway password is required. Enter it again or edit this connection.")
"AUTH_PASSWORD_MISMATCH" -> nativeString("Gateway password is invalid. Re-enter it or reset this gateway connection.")
"AUTH_TOKEN_MISSING" -> nativeString("Gateway token is required. Enter it again or edit this connection.")
"CONTROL_UI_DEVICE_IDENTITY_REQUIRED",
"DEVICE_IDENTITY_REQUIRED",
-> "Gateway requires this device identity. Re-authenticate or reset this gateway connection."
-> nativeString("Gateway requires this device identity. Re-authenticate or reset this gateway connection.")
else ->
when (gatewayConnectionProblem.recommendedNextStep) {
"update_auth_credentials" -> "Saved authentication is invalid. Re-authenticate or reset this gateway connection."
"update_auth_configuration" -> "Gateway authentication is not configured. Edit this connection and try again."
"review_auth_configuration" -> "Gateway authentication needs review. Check gateway settings, then retry."
"update_auth_credentials" -> nativeString("Saved authentication is invalid. Re-authenticate or reset this gateway connection.")
"update_auth_configuration" -> nativeString("Gateway authentication is not configured. Edit this connection and try again.")
"review_auth_configuration" -> nativeString("Gateway authentication needs review. Check gateway settings, then retry.")
else -> gatewayConnectionProblem.message.takeIf { it.isNotBlank() } ?: "Gateway authentication needs attention."
}
}
@@ -2798,7 +2802,7 @@ private fun copyApprovalCommand(
) {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(ClipData.newPlainText("OpenClaw pairing approval command", command))
Toast.makeText(context, "Approval command copied", Toast.LENGTH_SHORT).show()
Toast.makeText(context, nativeString("Approval command copied"), Toast.LENGTH_SHORT).show()
}
private fun copyGatewayCommand(
@@ -2807,7 +2811,7 @@ private fun copyGatewayCommand(
) {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(ClipData.newPlainText("OpenClaw gateway command", command))
Toast.makeText(context, "Command copied", Toast.LENGTH_SHORT).show()
Toast.makeText(context, nativeString("Command copied"), Toast.LENGTH_SHORT).show()
}
/** One permission row plus launcher callback for onboarding's final setup step. */
@@ -2869,7 +2873,7 @@ internal fun cameraCapabilityAfterRowTap(
androidCameraPermissionGranted: Boolean,
): Boolean? = if (androidCameraPermissionGranted) !currentCapabilityEnabled else null
private fun permissionRowStatusText(granted: Boolean): String = if (granted) "Granted" else "Not granted"
private fun permissionRowStatusText(granted: Boolean): String = if (granted) nativeString("Granted") else nativeString("Not granted")
internal fun permissionChangesRequireNodeApproval(
currentCameraEnabled: Boolean,

View File

@@ -16,6 +16,7 @@ import ai.openclaw.app.NodeRuntime
import ai.openclaw.app.R
import ai.openclaw.app.chat.ChatSessionEntry
import ai.openclaw.app.currentAppLanguage
import ai.openclaw.app.i18n.nativeString
import ai.openclaw.app.node.CanvasController
import ai.openclaw.app.ui.chat.ChatScreen
import ai.openclaw.app.ui.design.AgentAvatarSource
@@ -215,7 +216,10 @@ fun ShellScreen(
bottomBar = {
if (showBottomNav) {
ClawBottomNav(
items = shellNavTabs.map { ClawNavItem(key = it.key, label = it.label, icon = it.icon) },
items =
shellNavTabs.map {
ClawNavItem(key = it.key, label = nativeString(it.label), icon = it.icon)
},
selectedKey = if (nav.activeTab in shellNavTabs) nav.activeTab.key else Tab.Overview.key,
onSelect = { key ->
nav.selectTab(shellNavTabs.firstOrNull { it.key == key } ?: Tab.Overview)
@@ -344,7 +348,7 @@ private fun CanvasOverlay(
if (visible) {
ClawIconButton(
icon = Icons.Default.Close,
contentDescription = "Close Canvas",
contentDescription = nativeString("Close Canvas"),
onClick = onClose,
modifier =
Modifier
@@ -492,7 +496,7 @@ private fun OverviewScreen(
item {
Text(
text = "Overview",
text = nativeString("Overview"),
style = ClawTheme.type.display.copy(fontSize = 24.sp, lineHeight = 28.sp),
color = ClawTheme.colors.text,
)
@@ -538,9 +542,9 @@ private fun OverviewScreen(
if (visibleRecentRows.isEmpty()) {
item {
ClawEmptyState(
title = "No recent sessions",
body = "Start a chat and your active OpenClaw conversations will appear here.",
action = { ClawPrimaryButton(text = "Start Chat", onClick = { onSelectTab(Tab.Chat) }) },
title = nativeString("No recent sessions"),
body = nativeString("Start a chat and your active OpenClaw conversations will appear here."),
action = { ClawPrimaryButton(text = nativeString("Start Chat"), onClick = { onSelectTab(Tab.Chat) }) },
)
}
} else {
@@ -586,7 +590,7 @@ private fun OverviewHeader(
) {
OpenClawMascot(modifier = Modifier.size(25.dp), tint = ClawTheme.colors.text)
Text(
text = "OpenClaw",
text = nativeString("OpenClaw"),
style = ClawTheme.type.title.copy(fontSize = 17.sp, lineHeight = 21.sp),
color = ClawTheme.colors.text,
modifier = Modifier.weight(1f),
@@ -594,7 +598,7 @@ private fun OverviewHeader(
overflow = TextOverflow.Ellipsis,
)
OverviewStatusPill(status = status, onClick = onOpenStatus)
ClawPlainIconButton(icon = Icons.Default.Search, contentDescription = "Search", onClick = onOpenCommand)
ClawPlainIconButton(icon = Icons.Default.Search, contentDescription = nativeString("Search"), onClick = onOpenCommand)
}
}
@@ -624,7 +628,7 @@ private fun OverviewStatusPill(
horizontalArrangement = Arrangement.spacedBy(6.dp),
) {
Box(modifier = Modifier.size(6.dp).clip(CircleShape).background(dotColor))
Text(text = status.label, style = ClawTheme.type.caption.copy(fontSize = 13.sp, lineHeight = 17.sp), color = ClawTheme.colors.text, maxLines = 1)
Text(text = nativeString(status.label), style = ClawTheme.type.caption.copy(fontSize = 13.sp, lineHeight = 17.sp), color = ClawTheme.colors.text, maxLines = 1)
Icon(imageVector = Icons.Default.ArrowDropDown, contentDescription = null, modifier = Modifier.size(15.dp), tint = ClawTheme.colors.textMuted)
}
}
@@ -647,7 +651,7 @@ private fun OverviewPrimaryPanel(
) {
OverviewLayeredPanel(contentPadding = PaddingValues(ClawTheme.spacing.sm), elevated = true) {
Column(verticalArrangement = Arrangement.spacedBy(ClawTheme.spacing.xs)) {
Text(text = "ACTIVE AGENT", style = ClawTheme.type.caption.copy(fontSize = 12.sp, lineHeight = 15.sp), color = ClawTheme.colors.textMuted)
Text(text = nativeString("ACTIVE AGENT"), style = ClawTheme.type.caption.copy(fontSize = 12.sp, lineHeight = 15.sp), color = ClawTheme.colors.textMuted)
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(9.dp)) {
OverviewAgentBadge(text = agentBadge, active = isConnected, avatarSource = agentAvatarSource)
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(3.dp)) {
@@ -656,19 +660,19 @@ private fun OverviewPrimaryPanel(
}
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)
}
ClawSecondaryButton(text = "View", onClick = onOpenAgent)
ClawSecondaryButton(text = nativeString("View"), onClick = onOpenAgent)
}
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp)) {
OverviewStateChip(label = "Runs", value = if (pendingRunCount > 0) "$pendingRunCount active" else "Idle", modifier = Modifier.weight(1f))
OverviewStateChip(label = "Sessions", value = if (sessionCount == 0) "None" else "$sessionCount recent", modifier = Modifier.weight(1f))
OverviewStateChip(label = "Cron", value = cronJobsSummary(cronJobCount), modifier = Modifier.weight(1f))
OverviewStateChip(label = nativeString("Runs"), value = if (pendingRunCount > 0) nativeString("\$pendingRunCount active", pendingRunCount) else nativeString("Idle"), modifier = Modifier.weight(1f))
OverviewStateChip(label = nativeString("Sessions"), value = if (sessionCount == 0) nativeString("None") else nativeString("\$sessionCount recent", sessionCount), modifier = Modifier.weight(1f))
OverviewStateChip(label = nativeString("Cron"), value = cronJobsSummary(cronJobCount), modifier = Modifier.weight(1f))
}
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp)) {
OverviewActionPill(text = "Chat", icon = Icons.Outlined.ChatBubbleOutline, emphasized = true, onClick = onOpenChat, modifier = Modifier.weight(1f))
OverviewActionPill(text = "Talk", icon = Icons.Outlined.MicNone, emphasized = false, onClick = onOpenVoice, modifier = Modifier.weight(1f))
OverviewActionPill(text = nativeString("Chat"), icon = Icons.Outlined.ChatBubbleOutline, emphasized = true, onClick = onOpenChat, modifier = Modifier.weight(1f))
OverviewActionPill(text = nativeString("Talk"), icon = Icons.Outlined.MicNone, emphasized = false, onClick = onOpenVoice, modifier = Modifier.weight(1f))
}
if (!isConnected) {
ClawSecondaryButton(text = "Reconnect gateway", icon = Icons.Default.Cloud, onClick = onOpenGateway, modifier = Modifier.fillMaxWidth())
ClawSecondaryButton(text = nativeString("Reconnect gateway"), icon = Icons.Default.Cloud, onClick = onOpenGateway, modifier = Modifier.fillMaxWidth())
}
}
}
@@ -818,11 +822,11 @@ private fun OverviewMetricTile(
Column(modifier = Modifier.padding(ClawTheme.spacing.xs), verticalArrangement = Arrangement.spacedBy(6.dp)) {
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Icon(imageVector = card.icon, contentDescription = null, modifier = Modifier.size(17.dp), tint = card.tint)
Text(text = card.title.uppercase(), style = ClawTheme.type.caption.copy(fontSize = 10.5.sp, lineHeight = 13.sp), color = ClawTheme.colors.textMuted, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f))
Icon(imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = "Open ${card.title}", modifier = Modifier.size(15.dp), tint = ClawTheme.colors.textMuted)
Text(text = nativeString(card.title).uppercase(), style = ClawTheme.type.caption.copy(fontSize = 10.5.sp, lineHeight = 13.sp), color = ClawTheme.colors.textMuted, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f))
Icon(imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = nativeString("Open \${card.title}", card.title), modifier = Modifier.size(15.dp), tint = ClawTheme.colors.textMuted)
}
Text(text = card.value, style = ClawTheme.type.title.copy(fontSize = 22.sp, lineHeight = 25.sp), color = ClawTheme.colors.text, maxLines = 1, overflow = TextOverflow.Ellipsis)
Text(text = card.subtitle, style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textSubtle, maxLines = 2, overflow = TextOverflow.Ellipsis)
Text(text = nativeString(card.value), style = ClawTheme.type.title.copy(fontSize = 22.sp, lineHeight = 25.sp), color = ClawTheme.colors.text, maxLines = 1, overflow = TextOverflow.Ellipsis)
Text(text = nativeString(card.subtitle), style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textSubtle, maxLines = 2, overflow = TextOverflow.Ellipsis)
card.progressFraction?.let { progress ->
OverviewProgressBar(progress = progress, tint = card.tint)
}
@@ -887,10 +891,10 @@ private fun TalkEntryPanel(
}
}
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text(text = "Talk", style = ClawTheme.type.caption.copy(fontSize = 12.sp, lineHeight = 15.sp), color = ClawTheme.colors.textMuted)
Text(text = "Open Talk", style = ClawTheme.type.body, color = ClawTheme.colors.text, maxLines = 1, overflow = TextOverflow.Ellipsis)
Text(text = nativeString("Talk"), style = ClawTheme.type.caption.copy(fontSize = 12.sp, lineHeight = 15.sp), color = ClawTheme.colors.textMuted)
Text(text = nativeString("Open Talk"), style = ClawTheme.type.body, color = ClawTheme.colors.text, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
ClawPlainIconButton(icon = Icons.Default.Tune, contentDescription = "Talk settings", onClick = onOpenVoiceSettings)
ClawPlainIconButton(icon = Icons.Default.Tune, contentDescription = nativeString("Talk settings"), onClick = onOpenVoiceSettings)
}
}
}
@@ -898,7 +902,7 @@ private fun TalkEntryPanel(
@Composable
private fun RecentSessionsHeader(onOpenSessions: () -> Unit) {
SectionLabel(
title = "Recent Sessions",
title = nativeString("Recent Sessions"),
action = {
Surface(
onClick = onOpenSessions,
@@ -908,7 +912,7 @@ private fun RecentSessionsHeader(onOpenSessions: () -> Unit) {
) {
Box(contentAlignment = Alignment.Center) {
Text(
text = "View all",
text = nativeString("View all"),
style = ClawTheme.type.caption,
color = ClawTheme.colors.textMuted,
)
@@ -1022,10 +1026,10 @@ internal fun overviewMetricCardSpecs(
val nodeCount = nodesDevicesSummary.nodes.size
return listOf(
OverviewMetricCardSpec(
title = "Gateway",
title = nativeString("Gateway"),
value =
when {
!isConnected -> "Offline"
!isConnected -> nativeString("Offline")
hasAttention -> "Online"
else -> "Healthy"
},
@@ -1046,8 +1050,8 @@ internal fun overviewMetricCardSpecs(
settingsRoute = SettingsRoute.Gateway,
),
OverviewMetricCardSpec(
title = "Nodes",
value = if (nodeCount == 0) "None" else "$onlineNodes/$nodeCount",
title = nativeString("Nodes"),
value = if (nodeCount == 0) nativeString("None") else nativeString("\$onlineNodes/\$nodeCount", onlineNodes, nodeCount),
subtitle =
if (nodesDevicesSummary.hasNodeCapabilityApprovalPending()) {
"Review node access"
@@ -1068,7 +1072,7 @@ internal fun overviewMetricCardSpecs(
progressFraction = if (nodeCount > 0) onlineNodes.toFloat() / nodeCount.toFloat() else null,
),
OverviewMetricCardSpec(
title = "Approvals",
title = nativeString("Approvals"),
value = pendingApprovals.toString(),
subtitle = approvalsSummary(pendingApprovals),
icon = Icons.Default.Security,
@@ -1077,17 +1081,17 @@ internal fun overviewMetricCardSpecs(
settingsRoute = SettingsRoute.Approvals,
),
OverviewMetricCardSpec(
title = "Sessions",
title = nativeString("Sessions"),
value = sessionCount.toString(),
subtitle = if (sessionCount == 0) "No recent sessions" else "Recent conversations",
subtitle = if (sessionCount == 0) nativeString("No recent sessions") else nativeString("Recent conversations"),
icon = Icons.Default.Groups,
status = if (sessionCount > 0) ClawStatus.Success else ClawStatus.Neutral,
tab = Tab.Sessions,
),
OverviewMetricCardSpec(
title = "Files",
value = if (isConnected) "Browse" else "Offline",
subtitle = "Agent workspace files",
title = nativeString("Files"),
value = if (isConnected) nativeString("Browse") else nativeString("Offline"),
subtitle = nativeString("Agent workspace files"),
icon = Icons.Outlined.Folder,
status = if (isConnected) ClawStatus.Success else ClawStatus.Neutral,
tab = Tab.Files,
@@ -1100,7 +1104,7 @@ internal fun overviewAgentName(
defaultAgentId: String?,
): String {
val agent = overviewAgent(agents = agents, defaultAgentId = defaultAgentId)
return agent?.name?.takeIf { it.isNotBlank() } ?: agent?.id?.takeIf { it.isNotBlank() } ?: "OpenClaw"
return agent?.name?.takeIf { it.isNotBlank() } ?: agent?.id?.takeIf { it.isNotBlank() } ?: nativeString("OpenClaw")
}
internal fun overviewAgentBadgeText(
@@ -1114,7 +1118,7 @@ internal fun overviewAgentBadgeText(
?.takeIf { it.isNotEmpty() }
?.let { return it }
if (agent == null) return "OC"
val source = agent.name?.takeIf { it.isNotBlank() } ?: agent.id.takeIf { it.isNotBlank() } ?: "OpenClaw"
val source = agent.name?.takeIf { it.isNotBlank() } ?: agent.id.takeIf { it.isNotBlank() } ?: nativeString("OpenClaw")
return agentInitials(source)
}
@@ -1201,7 +1205,7 @@ internal fun sessionSourceLabel(
} else {
normalized
}
if (!scopedKey.contains(':') && !scopedKey.contains('#')) return "OpenClaw"
if (!scopedKey.contains(':') && !scopedKey.contains('#')) return nativeString("OpenClaw")
val source = scopedKey.substringBefore(':').substringBefore('#').lowercase()
val channelLabel =
channelsSummary.channels
@@ -1210,7 +1214,7 @@ internal fun sessionSourceLabel(
}?.label
?.takeIf { it.isNotBlank() }
if (channelLabel != null) return channelLabel
return sessionSourceLabels[source] ?: "OpenClaw"
return nativeString(sessionSourceLabels[source] ?: "OpenClaw")
}
internal data class HomeAttentionRow(
@@ -1265,7 +1269,7 @@ private fun HomeAttentionPanel(
) {
OverviewLayeredPanel(contentPadding = PaddingValues(horizontal = 14.dp, vertical = 8.dp)) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(text = "Needs attention", style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.warning)
Text(text = nativeString("Needs attention"), style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.warning)
rows.forEach { row ->
ModuleListRow(
row = ModuleRow(row.title, row.subtitle, row.icon, row.tab, row.settingsRoute),
@@ -1293,7 +1297,7 @@ private fun SectionLabel(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(text = title.uppercase(), style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted)
Text(text = nativeString(title).uppercase(), style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted)
action?.invoke()
}
}
@@ -1318,19 +1322,19 @@ private fun ModuleListRow(
Icon(imageVector = row.icon, contentDescription = null, modifier = Modifier.size(20.dp), tint = ClawTheme.colors.text)
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(1.dp)) {
Text(
text = row.title,
text = nativeString(row.title),
style = ClawTheme.type.body,
color = ClawTheme.colors.text,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
row.subtitle?.let {
Text(text = it, style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textSubtle, maxLines = 1, overflow = TextOverflow.Ellipsis)
Text(text = nativeString(it), style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textSubtle, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
}
Icon(
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
contentDescription = "Open ${row.title}",
contentDescription = nativeString("Open \${row.title}", row.title),
modifier = Modifier.size(17.dp),
tint = ClawTheme.colors.textMuted,
)
@@ -1433,12 +1437,12 @@ private fun RecentSessionRowContent(
}
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(1.dp)) {
Text(text = title, style = ClawTheme.type.body, color = ClawTheme.colors.text, maxLines = 1, overflow = TextOverflow.Ellipsis)
Text(text = source, style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textSubtle, maxLines = 1, overflow = TextOverflow.Ellipsis)
Text(text = nativeString(source), style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textSubtle, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
Text(text = metadata, style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted)
Icon(
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
contentDescription = "Open session",
contentDescription = nativeString("Open session"),
modifier = Modifier.size(14.dp),
tint = ClawTheme.colors.textMuted,
)
@@ -1556,13 +1560,13 @@ private fun SettingsShellScreen(
) {
ClawPlainIconButton(
icon = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back",
contentDescription = nativeString("Back"),
onClick = onBack,
)
Text(text = "Settings", style = ClawTheme.type.display.copy(fontSize = 24.sp, lineHeight = 28.sp), color = ClawTheme.colors.text, modifier = Modifier.weight(1f))
Text(text = nativeString("Settings"), style = ClawTheme.type.display.copy(fontSize = 24.sp, lineHeight = 28.sp), color = ClawTheme.colors.text, modifier = Modifier.weight(1f))
ClawPlainIconButton(
icon = Icons.Default.Search,
contentDescription = "Search settings",
contentDescription = nativeString("Search settings"),
onClick = onOpenCommand,
)
}
@@ -1614,10 +1618,10 @@ private fun SettingsShellScreen(
),
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("Voice", if (speakerEnabled) "Speaker on" else "Speaker muted", Icons.Default.Mic, route = SettingsRoute.Voice),
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("Notifications", if (notificationForwardingEnabled) "Smart delivery" else "Off", Icons.Default.Notifications, route = SettingsRoute.Notifications),
SettingsRow("Phone Capabilities", if (cameraEnabled) "Camera enabled" else "Locked", Icons.Default.Lock, status = !cameraEnabled, route = SettingsRoute.PhoneCapabilities),
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(
"Appearance",
"${appearanceThemeSummary(appearanceThemeMode)} · ${appLanguage.displayName}",
@@ -1664,10 +1668,10 @@ private fun SettingsShellScreen(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(3.dp),
) {
Text(text = "OpenClaw ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})", style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted)
Text(text = nativeString("OpenClaw \${BuildConfig.VERSION_NAME} (\${BuildConfig.VERSION_CODE})", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE), style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted)
Row(horizontalArrangement = Arrangement.spacedBy(5.dp), verticalAlignment = Alignment.CenterVertically) {
Text(
text = if (isConnected) "All systems operational" else "Gateway not connected",
text = if (isConnected) nativeString("All systems operational") else nativeString("Gateway not connected"),
style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp),
color = ClawTheme.colors.textSubtle,
)
@@ -1682,7 +1686,7 @@ private fun SettingsShellScreen(
private fun approvalsSummary(count: Int): String =
when (count) {
0 -> "No pending approvals"
1 -> "1 pending"
1 -> nativeString("1 pending")
else -> "$count pending"
}
@@ -1699,15 +1703,19 @@ private fun cronJobsSummary(count: Int): String =
/** Summarizes provider usage buckets without exposing detailed billing data. */
private fun usageSummaryText(count: Int): String =
when (count) {
0 -> "No provider usage"
1 -> "1 provider"
else -> "$count providers"
0 -> nativeString("No provider usage")
1 -> nativeString("1 provider")
else -> nativeString("\$count providers", count)
}
/** Reports how many gateway skills are enabled, eligible, and dependency-complete. */
private fun skillsSummaryText(skills: List<GatewaySkillSummary>): String {
val ready = skills.count { !it.disabled && it.eligible && it.missingCount == 0 }
return if (skills.isEmpty()) "No skills" else "$ready/${skills.size} ready"
return if (skills.isEmpty()) {
nativeString("No skills")
} else {
nativeString("\$ready/\${skills.size} ready", ready, skills.size)
}
}
/** Converts gateway skill health into a tri-state settings status dot. */
@@ -1721,13 +1729,13 @@ private fun skillsStatus(skills: List<GatewaySkillSummary>): Boolean? =
/** Mirrors the Skill Workshop review queue in one compact Settings row. */
internal fun skillWorkshopSummaryText(summary: GatewaySkillWorkshopSummary): String {
val pending = summary.proposals.count { it.status == "pending" }
if (pending > 0) return if (pending == 1) "1 pending" else "$pending pending"
if (pending > 0) return if (pending == 1) nativeString("1 pending") else nativeString("\$pending pending", pending)
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"
held > 0 -> if (held == 1) "1 held" else "$held held"
applied > 0 -> if (applied == 1) "1 applied" else "$applied applied"
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"
}
}
@@ -1792,9 +1800,9 @@ private fun channelsStatus(summary: GatewayChannelsSummary): Boolean? =
/** Summarizes dreaming memory health before enabled/off state. */
private fun dreamingSummaryText(summary: GatewayDreamingSummary): String =
when {
!summary.storeHealthy || !summary.phaseSignalHealthy -> "Needs attention"
!summary.storeHealthy || !summary.phaseSignalHealthy -> nativeString("Needs attention")
summary.enabled -> "${summary.shortTermCount} waiting"
else -> "Off"
else -> nativeString("Off")
}
/** Maps dreaming store/phase health and enabled state to a settings status dot. */
@@ -1870,7 +1878,7 @@ internal fun settingsSectionTitleForRoute(route: SettingsRoute): String =
@Composable
private fun SettingsSectionTitle(title: String) {
Text(
text = title.uppercase(),
text = nativeString(title).uppercase(),
style = ClawTheme.type.caption.copy(fontSize = 12.sp, lineHeight = 16.sp),
color = ClawTheme.colors.textMuted,
)
@@ -1908,11 +1916,11 @@ private fun ProfilePanel(
}
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text(text = displayName, style = ClawTheme.type.section, color = ClawTheme.colors.text, maxLines = 1)
Text(text = "OpenClaw mobile", style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted, maxLines = 1)
Text(text = nativeString("OpenClaw mobile"), style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted, maxLines = 1)
}
Icon(
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
contentDescription = "Open profile",
contentDescription = nativeString("Open profile"),
modifier = Modifier.size(15.dp),
tint = ClawTheme.colors.text,
)
@@ -1967,10 +1975,10 @@ private fun SettingsListRow(
horizontalArrangement = Arrangement.spacedBy(10.dp),
) {
Icon(imageVector = row.icon, contentDescription = null, modifier = Modifier.size(20.dp), tint = ClawTheme.colors.text)
Text(text = row.title, style = ClawTheme.type.body, color = ClawTheme.colors.text, modifier = Modifier.weight(1f), maxLines = 1)
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 = row.value, style = ClawTheme.type.caption.copy(fontSize = 13.sp, lineHeight = 17.sp), color = ClawTheme.colors.textMuted, maxLines = 1, overflow = TextOverflow.Ellipsis)
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)
}
row.status?.let { active ->
Box(modifier = Modifier.size(4.5.dp).clip(CircleShape).background(if (active) ClawTheme.colors.success else ClawTheme.colors.textSubtle))
@@ -1978,7 +1986,12 @@ private fun SettingsListRow(
if (showDisclosure) {
Icon(
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
contentDescription = if (row.route != null) "Open ${row.title}" else row.title,
contentDescription =
if (row.route != null) {
nativeString("Open \${row.title}", row.title)
} else {
nativeString(row.title)
},
modifier = Modifier.size(17.dp),
tint = ClawTheme.colors.text,
)
@@ -1990,30 +2003,30 @@ private fun SettingsListRow(
private fun relativeSessionTime(updatedAtMs: Long): String {
val deltaMs = (System.currentTimeMillis() - updatedAtMs).coerceAtLeast(0L)
val minutes = deltaMs / 60_000L
if (minutes < 1) return "now"
if (minutes < 1) return nativeString("now")
if (minutes < 60) return "${minutes}m"
val hours = minutes / 60
if (hours < 24) return "${hours}h"
return "${hours / 24}d"
}
private fun displaySessionTitle(displayName: String?): String = displayName?.takeIf { it.isNotBlank() } ?: "Main session"
private fun displaySessionTitle(displayName: String?): String = displayName?.takeIf { it.isNotBlank() } ?: nativeString("Main session")
internal fun gatewaySummary(
statusText: String,
isConnected: Boolean,
gatewayConnectionProblem: GatewayConnectionProblem? = null,
): String {
if (isConnected) return "Online and ready"
if (isConnected) return nativeString("Online and ready")
val status = statusText.trim().lowercase()
return when {
status.contains("connecting") || status.contains("reconnecting") -> "Connecting..."
status.contains("pairing") -> "Waiting for pairing"
status.contains("auth") || status.contains("device identity") -> gatewayAuthRecoveryLabel(gatewayConnectionProblem) ?: "Authentication needed"
status.contains("fingerprint verification timed out") -> "TLS timed out"
status.contains("no tls endpoint") -> "No TLS endpoint"
status.contains("certificate") || status.contains("tls") -> "Certificate review needed"
else -> "Not connected"
status.contains("connecting") || status.contains("reconnecting") -> nativeString("Connecting...")
status.contains("pairing") -> nativeString("Waiting for pairing")
status.contains("auth") || status.contains("device identity") -> gatewayAuthRecoveryLabel(gatewayConnectionProblem) ?: nativeString("Authentication needed")
status.contains("fingerprint verification timed out") -> nativeString("TLS timed out")
status.contains("no tls endpoint") -> nativeString("No TLS endpoint")
status.contains("certificate") || status.contains("tls") -> nativeString("Certificate review needed")
else -> nativeString("Not connected")
}
}