mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-16 08:56:10 +00:00
fix(android): keep cron detail localization reactive
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
package ai.openclaw.app
|
||||
|
||||
import ai.openclaw.app.i18n.nativeString
|
||||
import ai.openclaw.app.i18n.NativeText
|
||||
import ai.openclaw.app.i18n.joinedNativeText
|
||||
import ai.openclaw.app.i18n.nativeText
|
||||
import ai.openclaw.app.i18n.verbatimText
|
||||
import ai.openclaw.app.node.asObjectOrNull
|
||||
import ai.openclaw.app.node.asStringOrNull
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
@@ -17,8 +20,8 @@ data class GatewayCronJobDetail(
|
||||
val enabled: Boolean,
|
||||
val deleteAfterRun: Boolean,
|
||||
val scheduleKind: String,
|
||||
val scheduleLabel: String,
|
||||
val scheduleDetail: String,
|
||||
val scheduleLabel: NativeText,
|
||||
val scheduleDetail: NativeText,
|
||||
val scheduleAt: String?,
|
||||
val scheduleEveryMs: Long?,
|
||||
val scheduleAnchorMs: Long?,
|
||||
@@ -31,13 +34,13 @@ data class GatewayCronJobDetail(
|
||||
val wakeMode: String,
|
||||
val payloadKind: String,
|
||||
val payloadText: String?,
|
||||
val payloadLabel: String,
|
||||
val payloadLabel: NativeText,
|
||||
val payloadModel: String?,
|
||||
val payloadThinking: String?,
|
||||
val payloadCommandArgv: List<String>?,
|
||||
val payloadCommandCwd: String?,
|
||||
val deliveryLabel: String,
|
||||
val failureAlertLabel: String,
|
||||
val deliveryLabel: NativeText,
|
||||
val failureAlertLabel: NativeText,
|
||||
val createdAtMs: Long,
|
||||
val updatedAtMs: Long,
|
||||
val configRevision: String?,
|
||||
@@ -67,7 +70,7 @@ sealed interface GatewayCronJobDetailState {
|
||||
|
||||
data class Error(
|
||||
val id: String,
|
||||
val message: String,
|
||||
val message: NativeText,
|
||||
) : GatewayCronJobDetailState
|
||||
}
|
||||
|
||||
@@ -204,41 +207,41 @@ internal fun parseGatewayCronJobDetail(job: JsonObject?): GatewayCronJobDetail?
|
||||
)
|
||||
}
|
||||
|
||||
internal fun formatCronInterval(everyMs: Long): String {
|
||||
internal fun formatCronInterval(everyMs: Long): NativeText {
|
||||
val minutes = everyMs / 60_000L
|
||||
val hours = minutes / 60L
|
||||
val days = hours / 24L
|
||||
return when {
|
||||
days >= 1 && hours % 24L == 0L -> nativeString("Every \${days}d", days)
|
||||
hours >= 1 && minutes % 60L == 0L -> nativeString("Every \${hours}h", hours)
|
||||
minutes >= 1 -> nativeString("Every \${minutes}m", minutes)
|
||||
else -> nativeString("Repeating")
|
||||
days >= 1 && hours % 24L == 0L -> nativeText("Every \${days}d", days)
|
||||
hours >= 1 && minutes % 60L == 0L -> nativeText("Every \${hours}h", hours)
|
||||
minutes >= 1 -> nativeText("Every \${minutes}m", minutes)
|
||||
else -> nativeText("Repeating")
|
||||
}
|
||||
}
|
||||
|
||||
private fun cronScheduleLabel(schedule: JsonObject): String =
|
||||
private fun cronScheduleLabel(schedule: JsonObject): NativeText =
|
||||
when (schedule.string("kind")) {
|
||||
"at" -> nativeString("One time")
|
||||
"every" -> schedule.long("everyMs")?.let(::formatCronInterval) ?: nativeString("Repeating")
|
||||
"cron" -> schedule.string("expr") ?: nativeString("Cron")
|
||||
else -> nativeString("Scheduled")
|
||||
"at" -> nativeText("One time")
|
||||
"every" -> schedule.long("everyMs")?.let(::formatCronInterval) ?: nativeText("Repeating")
|
||||
"cron" -> schedule.string("expr")?.let(::verbatimText) ?: nativeText("Cron")
|
||||
else -> nativeText("Scheduled")
|
||||
}
|
||||
|
||||
private fun cronScheduleDetail(schedule: JsonObject): String =
|
||||
private fun cronScheduleDetail(schedule: JsonObject): NativeText =
|
||||
when (schedule.string("kind")) {
|
||||
"at" -> schedule.string("at") ?: nativeString("One time")
|
||||
"at" -> schedule.string("at")?.let(::verbatimText) ?: nativeText("One time")
|
||||
"every" -> {
|
||||
val every = schedule.long("everyMs")?.let(::formatCronInterval) ?: nativeString("Repeating")
|
||||
val anchor = schedule.long("anchorMs")?.let { nativeString("Anchor \$it", it) }
|
||||
listOfNotNull(every, anchor).joinToString(" · ")
|
||||
val every = schedule.long("everyMs")?.let(::formatCronInterval) ?: nativeText("Repeating")
|
||||
val anchor = schedule.long("anchorMs")?.let { nativeText("Anchor \$it", it) }
|
||||
joinedNativeText(" · ", listOfNotNull(every, anchor))
|
||||
}
|
||||
"cron" -> {
|
||||
val expression = schedule.string("expr") ?: nativeString("Cron")
|
||||
val timezone = schedule.string("tz")
|
||||
val stagger = schedule.long("staggerMs")?.takeIf { it > 0L }?.let { nativeString("Stagger \${formatCronInterval(it)}", formatCronInterval(it)) }
|
||||
listOfNotNull(expression, timezone, stagger).joinToString(" · ")
|
||||
val expression = schedule.string("expr")?.let(::verbatimText) ?: nativeText("Cron")
|
||||
val timezone = schedule.string("tz")?.let(::verbatimText)
|
||||
val stagger = schedule.long("staggerMs")?.takeIf { it > 0L }?.let { nativeText("Stagger \${formatCronInterval(it)}", formatCronInterval(it)) }
|
||||
joinedNativeText(" · ", listOfNotNull(expression, timezone, stagger))
|
||||
}
|
||||
else -> nativeString("Scheduled")
|
||||
else -> nativeText("Scheduled")
|
||||
}
|
||||
|
||||
private fun cronPayloadText(payload: JsonObject): String? =
|
||||
@@ -252,40 +255,44 @@ private fun cronPayloadText(payload: JsonObject): String? =
|
||||
else -> null
|
||||
}
|
||||
|
||||
private fun cronPayloadLabel(payload: JsonObject): String =
|
||||
private fun cronPayloadLabel(payload: JsonObject): NativeText =
|
||||
when (payload.string("kind")) {
|
||||
"systemEvent" -> nativeString("System event")
|
||||
"systemEvent" -> nativeText("System event")
|
||||
"agentTurn" -> {
|
||||
val model = payload.string("model")
|
||||
val thinking = payload.string("thinking")?.let { nativeString("Thinking \$it", it) }
|
||||
listOfNotNull(nativeString("Agent turn"), model, thinking).joinToString(" · ")
|
||||
val model = payload.string("model")?.let(::verbatimText)
|
||||
val thinking = payload.string("thinking")?.let { nativeText("Thinking \$it", it) }
|
||||
joinedNativeText(" · ", listOfNotNull(nativeText("Agent turn"), model, thinking))
|
||||
}
|
||||
"command" -> nativeString("Command")
|
||||
else -> nativeString("Payload")
|
||||
"command" -> nativeText("Command")
|
||||
else -> nativeText("Payload")
|
||||
}
|
||||
|
||||
private fun cronDeliveryLabel(delivery: JsonObject?): String {
|
||||
val value = delivery ?: return nativeString("Default")
|
||||
val mode = value.string("mode") ?: return nativeString("Default")
|
||||
return listOfNotNull(
|
||||
mode.replaceFirstChar { it.uppercaseChar() },
|
||||
value.string("channel"),
|
||||
value.string("to"),
|
||||
value.string("accountId")?.let { nativeString("Account \$it", it) },
|
||||
).joinToString(" · ")
|
||||
private fun cronDeliveryLabel(delivery: JsonObject?): NativeText {
|
||||
val value = delivery ?: return nativeText("Default")
|
||||
val mode = value.string("mode") ?: return nativeText("Default")
|
||||
return joinedNativeText(
|
||||
" · ",
|
||||
listOfNotNull(
|
||||
verbatimText(mode.replaceFirstChar { it.uppercaseChar() }),
|
||||
value.string("channel")?.let(::verbatimText),
|
||||
value.string("to")?.let(::verbatimText),
|
||||
value.string("accountId")?.let { nativeText("Account \$it", it) },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun cronFailureAlertLabel(failureAlert: JsonElement?): String {
|
||||
if ((failureAlert as? JsonPrimitive)?.booleanOrNull == false) return nativeString("Off")
|
||||
val alert = failureAlert.asObjectOrNull() ?: return nativeString("Default")
|
||||
return listOfNotNull(
|
||||
alert.long("after")?.let { nativeString("After \$it", it) },
|
||||
alert.string("mode")?.replaceFirstChar { it.uppercaseChar() },
|
||||
alert.string("channel"),
|
||||
alert.string("to"),
|
||||
alert.long("cooldownMs")?.takeIf { it > 0L }?.let { nativeString("Cooldown \${formatCronInterval(it)}", formatCronInterval(it)) },
|
||||
).joinToString(" · ")
|
||||
.ifBlank { nativeString("On") }
|
||||
private fun cronFailureAlertLabel(failureAlert: JsonElement?): NativeText {
|
||||
if ((failureAlert as? JsonPrimitive)?.booleanOrNull == false) return nativeText("Off")
|
||||
val alert = failureAlert.asObjectOrNull() ?: return nativeText("Default")
|
||||
val parts =
|
||||
listOfNotNull(
|
||||
alert.long("after")?.let { nativeText("After \$it", it) },
|
||||
alert.string("mode")?.replaceFirstChar { it.uppercaseChar() }?.let(::verbatimText),
|
||||
alert.string("channel")?.let(::verbatimText),
|
||||
alert.string("to")?.let(::verbatimText),
|
||||
alert.long("cooldownMs")?.takeIf { it > 0L }?.let { nativeText("Cooldown \${formatCronInterval(it)}", formatCronInterval(it)) },
|
||||
)
|
||||
return if (parts.isEmpty()) nativeText("On") else joinedNativeText(" · ", parts)
|
||||
}
|
||||
|
||||
private fun JsonObject.string(key: String): String? =
|
||||
|
||||
@@ -2121,7 +2121,7 @@ class NodeRuntime private constructor(
|
||||
cronJobDetailRequestGuard.publishIfCurrent(detailRequest) {
|
||||
_cronJobDetailState.value =
|
||||
detail?.let(GatewayCronJobDetailState::Loaded)
|
||||
?: GatewayCronJobDetailState.Error(detailRequest.id, "Gateway returned an invalid cron job.")
|
||||
?: GatewayCronJobDetailState.Error(detailRequest.id, nativeText("Gateway returned an invalid cron job."))
|
||||
}
|
||||
publishScreenshotCronHistory(historyRequest)
|
||||
}
|
||||
@@ -4211,7 +4211,7 @@ class NodeRuntime private constructor(
|
||||
val gatewayScope = captureGatewayDataScope() ?: return
|
||||
if (!operatorConnected) {
|
||||
cronJobDetailRequestGuard.publishIfCurrent(request) {
|
||||
_cronJobDetailState.value = GatewayCronJobDetailState.Error(request.id, nativeString("Connect the gateway to inspect cron jobs."))
|
||||
_cronJobDetailState.value = GatewayCronJobDetailState.Error(request.id, nativeText("Connect the gateway to inspect cron jobs."))
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -4221,11 +4221,11 @@ class NodeRuntime private constructor(
|
||||
cronJobDetailRequestGuard.publishIfCurrent(request) {
|
||||
_cronJobDetailState.value =
|
||||
parseGatewayCronJobDetail(root)?.let(GatewayCronJobDetailState::Loaded)
|
||||
?: GatewayCronJobDetailState.Error(request.id, nativeString("Gateway returned an invalid cron job."))
|
||||
?: GatewayCronJobDetailState.Error(request.id, nativeText("Gateway returned an invalid cron job."))
|
||||
}
|
||||
} catch (_: Throwable) {
|
||||
cronJobDetailRequestGuard.publishIfCurrent(request) {
|
||||
_cronJobDetailState.value = GatewayCronJobDetailState.Error(request.id, nativeString("Could not load cron job."))
|
||||
_cronJobDetailState.value = GatewayCronJobDetailState.Error(request.id, nativeText("Could not load cron job."))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,11 @@ sealed interface NativeText {
|
||||
data class Verbatim(
|
||||
val value: String,
|
||||
) : NativeText
|
||||
|
||||
data class Composite(
|
||||
val parts: List<NativeText>,
|
||||
val separator: String,
|
||||
) : NativeText
|
||||
}
|
||||
|
||||
private val nativeLocaleRevision = MutableStateFlow(0L)
|
||||
@@ -37,12 +42,40 @@ internal fun nativeText(
|
||||
|
||||
internal fun verbatimText(value: String): NativeText = NativeText.Verbatim(value)
|
||||
|
||||
internal fun joinedNativeText(
|
||||
separator: String,
|
||||
parts: List<NativeText>,
|
||||
): NativeText = NativeText.Composite(parts = parts, separator = separator)
|
||||
|
||||
internal fun NativeText.resolveNativeText(): String =
|
||||
when (this) {
|
||||
is NativeText.Resource -> nativeString(source, *formatArgs.toTypedArray())
|
||||
is NativeText.Resource -> nativeString(source, *formatArgs.map(::resolveNativeFormatArg).toTypedArray())
|
||||
is NativeText.Verbatim -> value
|
||||
is NativeText.Composite -> parts.joinToString(separator, transform = NativeText::resolveNativeText)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun NativeText.resolveNativeTextResource(): String =
|
||||
when (this) {
|
||||
is NativeText.Resource -> {
|
||||
val resolvedArgs = mutableListOf<Any>()
|
||||
for (formatArg in formatArgs) {
|
||||
resolvedArgs += if (formatArg is NativeText) formatArg.resolveNativeTextResource() else formatArg
|
||||
}
|
||||
nativeStringResource(source, *resolvedArgs.toTypedArray())
|
||||
}
|
||||
is NativeText.Verbatim -> value
|
||||
is NativeText.Composite -> {
|
||||
val resolvedParts = mutableListOf<String>()
|
||||
for (part in parts) {
|
||||
resolvedParts += part.resolveNativeTextResource()
|
||||
}
|
||||
resolvedParts.joinToString(separator)
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveNativeFormatArg(value: Any): Any = if (value is NativeText) value.resolveNativeText() else value
|
||||
|
||||
internal fun notifyNativeLocaleChanged() {
|
||||
nativeLocaleRevision.update { it + 1 }
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import ai.openclaw.app.gatewayTalkSetupStatusText
|
||||
import ai.openclaw.app.hasPhotoReadPermission
|
||||
import ai.openclaw.app.i18n.nativeString
|
||||
import ai.openclaw.app.i18n.resolveNativeText
|
||||
import ai.openclaw.app.i18n.resolveNativeTextResource
|
||||
import ai.openclaw.app.isReady
|
||||
import ai.openclaw.app.loadAndroidLicenseNotices
|
||||
import ai.openclaw.app.locationModeAfterBackgroundSettings
|
||||
@@ -456,7 +457,7 @@ private fun CronJobDetailSettingsScreen(
|
||||
}
|
||||
errorText != null ->
|
||||
ClawPanel {
|
||||
Text(text = errorText, style = ClawTheme.type.body, color = ClawTheme.colors.warning)
|
||||
Text(text = errorText.resolveNativeTextResource(), style = ClawTheme.type.body, color = ClawTheme.colors.warning)
|
||||
}
|
||||
current == null ->
|
||||
ClawPanel {
|
||||
@@ -2221,7 +2222,7 @@ private fun CronJobDetailPanel(
|
||||
rows =
|
||||
listOf(
|
||||
SettingsMetric(nativeString("Status"), if (job.enabled) nativeString("Enabled") else nativeString("Off")),
|
||||
SettingsMetric(nativeString("Schedule"), job.scheduleLabel),
|
||||
SettingsMetric(nativeString("Schedule"), job.scheduleLabel.resolveNativeTextResource()),
|
||||
SettingsMetric(nativeString("Next Wake"), formatCronWake(job.nextRunAtMs)),
|
||||
SettingsMetric(nativeString("Last Run"), formatCronTimestamp(job.lastRunAtMs)),
|
||||
),
|
||||
@@ -2231,13 +2232,13 @@ private fun CronJobDetailPanel(
|
||||
listOf(
|
||||
SettingsMetric("ID", job.id, copyable = true),
|
||||
SettingsMetric(nativeString("Description"), job.description.ifBlank { nativeString("None") }),
|
||||
SettingsMetric(nativeString("Schedule Detail"), job.scheduleDetail),
|
||||
SettingsMetric(nativeString("Schedule Detail"), job.scheduleDetail.resolveNativeTextResource()),
|
||||
SettingsMetric(nativeString("Session Target"), job.sessionTarget),
|
||||
SettingsMetric(nativeString("Wake Mode"), job.wakeMode),
|
||||
SettingsMetric(nativeString("Delete After Run"), if (job.deleteAfterRun) nativeString("Yes") else nativeString("No")),
|
||||
SettingsMetric(nativeString("Payload"), job.payloadLabel),
|
||||
SettingsMetric(nativeString("Delivery"), job.deliveryLabel),
|
||||
SettingsMetric(nativeString("Failure Alert"), job.failureAlertLabel),
|
||||
SettingsMetric(nativeString("Payload"), job.payloadLabel.resolveNativeTextResource()),
|
||||
SettingsMetric(nativeString("Delivery"), job.deliveryLabel.resolveNativeTextResource()),
|
||||
SettingsMetric(nativeString("Failure Alert"), job.failureAlertLabel.resolveNativeTextResource()),
|
||||
SettingsMetric(nativeString("Created"), formatCronTimestamp(job.createdAtMs)),
|
||||
SettingsMetric(nativeString("Updated"), formatCronTimestamp(job.updatedAtMs)),
|
||||
SettingsMetric(nativeString("Running Since"), formatCronTimestamp(job.runningAtMs)),
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package ai.openclaw.app
|
||||
|
||||
import ai.openclaw.app.i18n.NativeStringResources
|
||||
import ai.openclaw.app.i18n.joinedNativeText
|
||||
import ai.openclaw.app.i18n.nativeText
|
||||
import ai.openclaw.app.i18n.resolveNativeText
|
||||
import ai.openclaw.app.i18n.verbatimText
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -119,13 +121,22 @@ class AppLanguageTest {
|
||||
val activity = Robolectric.buildActivity(LocaleTestActivity::class.java).setup()
|
||||
NativeStringResources.install(activity.get())
|
||||
val retained = MutableStateFlow(nativeText("Mic off")).resolveNativeText()
|
||||
val retainedComposite =
|
||||
MutableStateFlow(
|
||||
joinedNativeText(
|
||||
separator = " · ",
|
||||
parts = listOf(nativeText("Mic off"), verbatimText("raw")),
|
||||
),
|
||||
).resolveNativeText()
|
||||
val previous = currentAppLanguage()
|
||||
try {
|
||||
setAppLanguage(AppLanguage.English)
|
||||
assertEquals("Mic off", retained.value)
|
||||
assertEquals("Mic off · raw", retainedComposite.value)
|
||||
|
||||
setAppLanguage(AppLanguage.French)
|
||||
assertEquals("Micro désactivé", retained.value)
|
||||
assertEquals("Micro désactivé · raw", retainedComposite.value)
|
||||
assertEquals("Connexion…", gatewayConnectionStatusForDisplay("Connecting…"))
|
||||
assertEquals(
|
||||
"Impossible de charger les approbations.",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package ai.openclaw.app
|
||||
|
||||
import ai.openclaw.app.i18n.resolveNativeText
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
@@ -20,17 +21,17 @@ class CronJobDetailTest {
|
||||
assertEquals("Daily report", detail.name)
|
||||
assertEquals("sha256:fixture", detail.configRevision)
|
||||
assertEquals("cron", detail.scheduleKind)
|
||||
assertEquals("0 9 * * *", detail.scheduleLabel)
|
||||
assertEquals("0 9 * * * · Europe/Vienna · Stagger Every 5m", detail.scheduleDetail)
|
||||
assertEquals("0 9 * * *", detail.scheduleLabel.resolveNativeText())
|
||||
assertEquals("0 9 * * * · Europe/Vienna · Stagger Every 5m", detail.scheduleDetail.resolveNativeText())
|
||||
assertEquals("0 9 * * *", detail.scheduleCronExpr)
|
||||
assertEquals("Europe/Vienna", detail.scheduleTimezone)
|
||||
assertEquals(300000L, detail.scheduleStaggerMs)
|
||||
assertEquals("Agent turn · openai/gpt-5.5 · Thinking high", detail.payloadLabel)
|
||||
assertEquals("Agent turn · openai/gpt-5.5 · Thinking high", detail.payloadLabel.resolveNativeText())
|
||||
assertEquals("Summarize the day", detail.payloadText)
|
||||
assertEquals("openai/gpt-5.5", detail.payloadModel)
|
||||
assertEquals("high", detail.payloadThinking)
|
||||
assertEquals("Announce · telegram · chat-42 · Account primary", detail.deliveryLabel)
|
||||
assertEquals("After 3 · Announce · telegram · ops · Cooldown Every 1h", detail.failureAlertLabel)
|
||||
assertEquals("Announce · telegram · chat-42 · Account primary", detail.deliveryLabel.resolveNativeText())
|
||||
assertEquals("After 3 · Announce · telegram · ops · Cooldown Every 1h", detail.failureAlertLabel.resolveNativeText())
|
||||
assertEquals(2L, detail.consecutiveErrors)
|
||||
assertEquals("error", detail.lastRunStatus)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user