diff --git a/apps/android/app/src/main/java/ai/openclaw/app/ui/CommandPalette.kt b/apps/android/app/src/main/java/ai/openclaw/app/ui/CommandPalette.kt index 9e2654d68537..44273d86698b 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/ui/CommandPalette.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/ui/CommandPalette.kt @@ -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, models: List, ): 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 { diff --git a/apps/android/app/src/main/java/ai/openclaw/app/ui/GatewayConfigResolver.kt b/apps/android/app/src/main/java/ai/openclaw/app/ui/GatewayConfigResolver.kt index 7f55a51c8ef7..89ab67559c1e 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/ui/GatewayConfigResolver.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/ui/GatewayConfigResolver.kt @@ -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.") } } diff --git a/apps/android/app/src/main/java/ai/openclaw/app/ui/GatewayDiagnostics.kt b/apps/android/app/src/main/java/ai/openclaw/app/ui/GatewayDiagnostics.kt index 5f7eefa8fe9d..358aec450cd0 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/ui/GatewayDiagnostics.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/ui/GatewayDiagnostics.kt @@ -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() } diff --git a/apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt b/apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt index e865443df46d..35f037392d0c 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt @@ -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, diff --git a/apps/android/app/src/main/java/ai/openclaw/app/ui/ShellScreen.kt b/apps/android/app/src/main/java/ai/openclaw/app/ui/ShellScreen.kt index 542615cc2b0a..d617aff0a66c 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/ui/ShellScreen.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/ui/ShellScreen.kt @@ -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): 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): 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") } }