diff --git a/android/app/src/main/java/io/clawdroid/CalendarPickerActivity.kt b/android/app/src/main/java/io/clawdroid/CalendarPickerActivity.kt
index d484afaddb..808c8ce1e2 100644
--- a/android/app/src/main/java/io/clawdroid/CalendarPickerActivity.kt
+++ b/android/app/src/main/java/io/clawdroid/CalendarPickerActivity.kt
@@ -20,6 +20,7 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.clawdroid.core.ui.theme.ClawDroidTheme
import io.clawdroid.core.ui.theme.GlassBorder
@@ -94,7 +95,7 @@ class CalendarPickerActivity : ComponentActivity() {
onDismissRequest = onCancel,
containerColor = DarkCard,
title = {
- Text("Select Calendar", color = NeonCyan)
+ Text(stringResource(R.string.calendar_picker_title), color = NeonCyan)
},
text = {
Surface(
@@ -126,7 +127,7 @@ class CalendarPickerActivity : ComponentActivity() {
confirmButton = {},
dismissButton = {
TextButton(onClick = onCancel) {
- Text("Cancel", color = TextSecondary)
+ Text(stringResource(R.string.action_cancel), color = TextSecondary)
}
},
)
diff --git a/android/app/src/main/java/io/clawdroid/assistant/AssistantService.kt b/android/app/src/main/java/io/clawdroid/assistant/AssistantService.kt
index 42c41951ff..bcd3e9c115 100644
--- a/android/app/src/main/java/io/clawdroid/assistant/AssistantService.kt
+++ b/android/app/src/main/java/io/clawdroid/assistant/AssistantService.kt
@@ -115,7 +115,7 @@ class AssistantService : LifecycleService(), SavedStateRegistryOwner {
serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
- connection = AssistantConnectionImpl(httpClient)
+ connection = AssistantConnectionImpl(httpClient, applicationContext)
toolRequestHandler = ToolRequestHandler(
context = applicationContext,
@@ -345,15 +345,11 @@ class AssistantService : LifecycleService(), SavedStateRegistryOwner {
) {
Column(modifier = Modifier.padding(24.dp)) {
Text(
- text = "画面キャプチャの設定",
+ text = getString(io.clawdroid.R.string.assistant_accessibility_title),
style = MaterialTheme.typography.headlineSmall
)
Text(
- text = "画面キャプチャを使用するには、ユーザー補助の設定で" +
- " ClawDroid を有効にしてください。\n\n" +
- "有効にできない場合は、アプリ情報から" +
- "「制限付き設定を許可」を実行してから" +
- "再度お試しください。",
+ text = getString(io.clawdroid.R.string.assistant_accessibility_body),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(top = 16.dp)
)
@@ -364,7 +360,7 @@ class AssistantService : LifecycleService(), SavedStateRegistryOwner {
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = { showAccessibilityGuide = false }) {
- Text("キャンセル")
+ Text(getString(io.clawdroid.R.string.action_cancel))
}
TextButton(onClick = {
showAccessibilityGuide = false
@@ -372,7 +368,7 @@ class AssistantService : LifecycleService(), SavedStateRegistryOwner {
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
}) {
- Text("設定を開く")
+ Text(getString(io.clawdroid.R.string.assistant_accessibility_open_settings))
}
}
}
@@ -414,7 +410,7 @@ class AssistantService : LifecycleService(), SavedStateRegistryOwner {
return NotificationCompat.Builder(this, NotificationHelper.ASSISTANT_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle("ClawDroid Assistant")
- .setContentText("Listening...")
+ .setContentText(getString(io.clawdroid.R.string.assistant_notification_listening))
.setPriority(NotificationCompat.PRIORITY_LOW)
.setOngoing(true)
.build()
diff --git a/android/app/src/main/java/io/clawdroid/di/AppModule.kt b/android/app/src/main/java/io/clawdroid/di/AppModule.kt
index dc4be240ce..375d3b98be 100644
--- a/android/app/src/main/java/io/clawdroid/di/AppModule.kt
+++ b/android/app/src/main/java/io/clawdroid/di/AppModule.kt
@@ -81,7 +81,7 @@ val appModule = module {
val clientId = prefs.getString("client_id", null) ?: UUID.randomUUID().toString().also {
prefs.edit { putString("client_id", it) }
}
- WebSocketClient(get(), get(), clientId)
+ WebSocketClient(get(), get(), clientId, context = androidContext())
}
// ImageFileStorage
diff --git a/android/app/src/main/res/values-ja/strings.xml b/android/app/src/main/res/values-ja/strings.xml
index 48cbf7455c..86df5e5271 100644
--- a/android/app/src/main/res/values-ja/strings.xml
+++ b/android/app/src/main/res/values-ja/strings.xml
@@ -63,4 +63,18 @@
ゲートウェイ
ゲートウェイバックエンドサービス
ClawDroid
+
+
+ キャンセル
+
+
+ 画面キャプチャの設定
+ 画面キャプチャを使用するには、ユーザー補助の設定で ClawDroid を有効にしてください。\n\n有効にできない場合は、アプリ情報から「制限付き設定を許可」を実行してから再度お試しください。
+ 設定を開く
+
+
+ 聞いています…
+
+
+ カレンダーを選択
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index af8f928ef7..46e0dd7d44 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -63,4 +63,18 @@
Gateway
Gateway backend service
ClawDroid
+
+
+ Cancel
+
+
+ Screen Capture Setup
+ To use screen capture, enable ClawDroid in Accessibility settings.\n\nIf you cannot enable it, go to App Info and select \"Allow restricted settings\", then try again.
+ Open Settings
+
+
+ Listening…
+
+
+ Select Calendar
diff --git a/android/backend/config/src/main/java/io/clawdroid/backend/config/ConfigApiClient.kt b/android/backend/config/src/main/java/io/clawdroid/backend/config/ConfigApiClient.kt
index 5e02d6a7ec..df0a42e43e 100644
--- a/android/backend/config/src/main/java/io/clawdroid/backend/config/ConfigApiClient.kt
+++ b/android/backend/config/src/main/java/io/clawdroid/backend/config/ConfigApiClient.kt
@@ -18,9 +18,9 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
+import android.content.Context
import java.io.Closeable
import java.io.IOException
-import java.util.Locale
@Serializable
data class ConfigSchema(val sections: List)
@@ -48,7 +48,7 @@ data class SaveConfigResult(
class AuthException(message: String) : IOException(message)
-class ConfigApiClient(private val settingsStore: GatewaySettingsStore) : Closeable {
+class ConfigApiClient(private val settingsStore: GatewaySettingsStore, private val context: Context) : Closeable {
private val baseUrl: String
get() = settingsStore.settings.value.httpBaseUrl
@@ -64,7 +64,7 @@ class ConfigApiClient(private val settingsStore: GatewaySettingsStore) : Closeab
suspend fun getSchema(): ConfigSchema {
return client.get("$baseUrl/api/config/schema") {
if (apiKey.isNotEmpty()) header("Authorization", "Bearer $apiKey")
- header("Accept-Language", Locale.getDefault().language)
+ header("Accept-Language", context.resources.configuration.locales[0].language)
}.ensureSuccess().body()
}
diff --git a/android/backend/config/src/main/java/io/clawdroid/backend/config/ConfigModule.kt b/android/backend/config/src/main/java/io/clawdroid/backend/config/ConfigModule.kt
index 7720cce6bf..3e8d8dc221 100644
--- a/android/backend/config/src/main/java/io/clawdroid/backend/config/ConfigModule.kt
+++ b/android/backend/config/src/main/java/io/clawdroid/backend/config/ConfigModule.kt
@@ -1,11 +1,12 @@
package io.clawdroid.backend.config
+import org.koin.android.ext.koin.androidContext
import org.koin.core.definition.Callbacks
import org.koin.core.module.dsl.viewModel
import org.koin.core.module.dsl.withOptions
import org.koin.dsl.module
val configModule = module {
- single { ConfigApiClient(get()) } withOptions { callbacks = Callbacks(onClose = { it?.close() }) }
+ single { ConfigApiClient(get(), androidContext()) } withOptions { callbacks = Callbacks(onClose = { it?.close() }) }
viewModel { ConfigViewModel(get()) }
}
diff --git a/android/backend/config/src/main/java/io/clawdroid/backend/config/ConfigSectionDetailScreen.kt b/android/backend/config/src/main/java/io/clawdroid/backend/config/ConfigSectionDetailScreen.kt
index def21ce7c5..494cafff7a 100644
--- a/android/backend/config/src/main/java/io/clawdroid/backend/config/ConfigSectionDetailScreen.kt
+++ b/android/backend/config/src/main/java/io/clawdroid/backend/config/ConfigSectionDetailScreen.kt
@@ -55,6 +55,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
@@ -85,16 +86,19 @@ fun ConfigSectionDetailScreen(
val detail = uiState.detailState
val isDirty = detail?.fields?.any { it.value != it.originalValue } == true
val isSaving = uiState.saveState is SaveState.Saving
+ val savedMsg = stringResource(R.string.config_saved)
+ val savedRestartingMsg = stringResource(R.string.config_saved_restarting)
+ val errorPrefix = stringResource(R.string.config_error_prefix)
LaunchedEffect(uiState.saveState) {
when (val state = uiState.saveState) {
is SaveState.Success -> {
- val msg = if (state.restart) "Saved. Server restarting..." else "Saved."
+ val msg = if (state.restart) savedRestartingMsg else savedMsg
snackbarHostState.showSnackbar(msg)
viewModel.dismissSaveResult()
}
is SaveState.Error -> {
- snackbarHostState.showSnackbar("Error: ${state.message}")
+ snackbarHostState.showSnackbar("$errorPrefix${state.message}")
viewModel.dismissSaveResult()
}
else -> {}
@@ -118,7 +122,7 @@ fun ConfigSectionDetailScreen(
}) {
Icon(
painter = painterResource(LucideR.drawable.lucide_ic_arrow_left),
- contentDescription = "Back",
+ contentDescription = stringResource(R.string.config_back),
tint = TextSecondary,
)
}
@@ -142,7 +146,7 @@ fun ConfigSectionDetailScreen(
strokeWidth = 2.dp,
)
} else {
- Text("Save")
+ Text(stringResource(R.string.config_save))
}
}
},
@@ -265,7 +269,7 @@ private fun StringField(field: FieldState, onValueChanged: (String) -> Unit) {
{
TextButton(onClick = { hidden = !hidden }) {
Text(
- if (hidden) "Show" else "Hide",
+ stringResource(if (hidden) R.string.config_show else R.string.config_hide),
color = NeonCyan,
style = MaterialTheme.typography.labelSmall,
)
@@ -334,7 +338,7 @@ private fun StringArrayField(field: FieldState, onValueChanged: (String) -> Unit
value = field.value,
onValueChange = onValueChanged,
label = { Text(field.label, color = TextSecondary) },
- placeholder = { Text("Comma-separated values", color = TextSecondary.copy(alpha = 0.5f)) },
+ placeholder = { Text(stringResource(R.string.config_comma_separated), color = TextSecondary.copy(alpha = 0.5f)) },
singleLine = true,
colors = configFieldColors(),
modifier = Modifier.fillMaxWidth(),
@@ -375,6 +379,7 @@ private fun DirectoryField(
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
+ val internalStorageOnlyMsg = stringResource(R.string.config_internal_storage_only)
val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocumentTree()
@@ -389,7 +394,7 @@ private fun DirectoryField(
onValueChanged(path)
} else {
scope.launch {
- snackbarHostState?.showSnackbar("Internal storage only")
+ snackbarHostState?.showSnackbar(internalStorageOnlyMsg)
}
}
}
@@ -404,7 +409,7 @@ private fun DirectoryField(
IconButton(onClick = { launcher.launch(null) }) {
Icon(
painter = painterResource(LucideR.drawable.lucide_ic_folder_open),
- contentDescription = "Browse",
+ contentDescription = stringResource(R.string.config_browse),
tint = NeonCyan,
)
}
@@ -413,7 +418,7 @@ private fun DirectoryField(
modifier = Modifier.fillMaxWidth(),
)
Text(
- "Internal storage only for SAF picker",
+ stringResource(R.string.config_internal_storage_saf),
style = MaterialTheme.typography.labelSmall,
color = TextSecondary.copy(alpha = 0.6f),
modifier = Modifier.padding(start = 16.dp, top = 2.dp),
@@ -455,7 +460,7 @@ private fun CalendarField(
null
}
}
- val displayText = if (field.value.isEmpty()) "Pick each time"
+ val displayText = if (field.value.isEmpty()) stringResource(R.string.config_pick_each_time)
else resolvedName ?: field.value
Row(
@@ -490,7 +495,7 @@ private fun CalendarField(
}
Icon(
painter = painterResource(LucideR.drawable.lucide_ic_chevron_right),
- contentDescription = "Select",
+ contentDescription = stringResource(R.string.config_select),
tint = if (enabled) NeonCyan else NeonCyan.copy(alpha = 0.3f),
modifier = Modifier.size(20.dp),
)
@@ -499,12 +504,12 @@ private fun CalendarField(
if (showDialog && calendars.isNotEmpty()) {
androidx.compose.material3.AlertDialog(
onDismissRequest = { showDialog = false },
- title = { Text("Select Calendar") },
+ title = { Text(stringResource(R.string.config_select_calendar)) },
text = {
Column {
// "Pick each time" option to clear the fixed setting
Text(
- "Pick each time",
+ stringResource(R.string.config_pick_each_time),
style = MaterialTheme.typography.bodyMedium,
color = NeonCyan,
modifier = Modifier
@@ -535,7 +540,7 @@ private fun CalendarField(
confirmButton = {},
dismissButton = {
TextButton(onClick = { showDialog = false }) {
- Text("Cancel", color = TextSecondary)
+ Text(stringResource(R.string.config_cancel), color = TextSecondary)
}
},
)
diff --git a/android/backend/config/src/main/java/io/clawdroid/backend/config/ConfigSectionListScreen.kt b/android/backend/config/src/main/java/io/clawdroid/backend/config/ConfigSectionListScreen.kt
index c53802c4e7..9ebe0ccc14 100644
--- a/android/backend/config/src/main/java/io/clawdroid/backend/config/ConfigSectionListScreen.kt
+++ b/android/backend/config/src/main/java/io/clawdroid/backend/config/ConfigSectionListScreen.kt
@@ -35,6 +35,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.clawdroid.core.ui.theme.DeepBlack
import io.clawdroid.core.ui.theme.GlassBorder
@@ -58,7 +59,7 @@ fun ConfigSectionListScreen(
containerColor = Color.Transparent,
topBar = {
TopAppBar(
- title = { Text("Backend Config") },
+ title = { Text(stringResource(R.string.config_backend_config)) },
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent,
),
@@ -66,7 +67,7 @@ fun ConfigSectionListScreen(
IconButton(onClick = onNavigateBack) {
Icon(
painter = painterResource(LucideR.drawable.lucide_ic_arrow_left),
- contentDescription = "Back",
+ contentDescription = stringResource(R.string.config_back),
tint = TextSecondary,
)
}
@@ -107,7 +108,7 @@ fun ConfigSectionListScreen(
contentColor = DeepBlack,
),
) {
- Text("Retry")
+ Text(stringResource(R.string.config_retry))
}
}
}
@@ -134,7 +135,7 @@ fun ConfigSectionListScreen(
contentColor = DeepBlack,
),
) {
- Text("Connection Settings")
+ Text(stringResource(R.string.config_connection_settings))
}
Spacer(Modifier.height(8.dp))
Button(
@@ -144,7 +145,7 @@ fun ConfigSectionListScreen(
contentColor = NeonCyan,
),
) {
- Text("Retry")
+ Text(stringResource(R.string.config_retry))
}
}
}
@@ -193,14 +194,14 @@ private fun SectionCard(
color = TextPrimary,
)
Text(
- "${section.fieldCount} fields",
+ stringResource(R.string.config_fields_count, section.fieldCount),
style = MaterialTheme.typography.bodySmall,
color = TextSecondary,
)
}
Icon(
painter = painterResource(LucideR.drawable.lucide_ic_chevron_right),
- contentDescription = "Open",
+ contentDescription = stringResource(R.string.config_open),
tint = TextSecondary,
)
}
diff --git a/android/backend/config/src/main/res/values-ja/strings.xml b/android/backend/config/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000000..0b1f7a0452
--- /dev/null
+++ b/android/backend/config/src/main/res/values-ja/strings.xml
@@ -0,0 +1,22 @@
+
+ 保存
+ 表示
+ 非表示
+ カンマ区切りの値
+ 内部ストレージのみ
+ SAFピッカーは内部ストレージのみ
+ カレンダーを選択
+ 毎回選択する
+ キャンセル
+ 戻る
+ 参照
+ 保存しました。
+ 保存しました。サーバーを再起動中…
+ エラー:
+ バックエンド設定
+ リトライ
+ 接続設定
+ 開く
+ %1$d 個の項目
+ 選択
+
diff --git a/android/backend/config/src/main/res/values/strings.xml b/android/backend/config/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..adf4e1e150
--- /dev/null
+++ b/android/backend/config/src/main/res/values/strings.xml
@@ -0,0 +1,22 @@
+
+ Save
+ Show
+ Hide
+ Comma-separated values
+ Internal storage only
+ Internal storage only for SAF picker
+ Select Calendar
+ Pick each time
+ Cancel
+ Back
+ Browse
+ Saved.
+ Saved. Server restarting…
+ Error:
+ Backend Config
+ Retry
+ Connection Settings
+ Open
+ %1$d fields
+ Select
+
diff --git a/android/core/data/src/main/java/io/clawdroid/core/data/remote/WebSocketClient.kt b/android/core/data/src/main/java/io/clawdroid/core/data/remote/WebSocketClient.kt
index cf1e90526d..c691f9a762 100644
--- a/android/core/data/src/main/java/io/clawdroid/core/data/remote/WebSocketClient.kt
+++ b/android/core/data/src/main/java/io/clawdroid/core/data/remote/WebSocketClient.kt
@@ -22,14 +22,15 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
-import java.util.Locale
+import android.content.Context
class WebSocketClient(
private val client: HttpClient,
private val scope: CoroutineScope,
private val clientId: String,
- private val clientType: String = "main"
+ private val clientType: String = "main",
+ private val context: Context
) {
private val _connectionState = MutableStateFlow(ConnectionState.DISCONNECTED)
@@ -57,7 +58,7 @@ class WebSocketClient(
_setupRequired.value = false
val currentWsUrl = wsUrl
val separator = if ('?' in currentWsUrl) '&' else '?'
- val locale = Locale.getDefault().language
+ val locale = context.resources.configuration.locales[0].language
val url = "${currentWsUrl}${separator}client_id=$clientId&client_type=$clientType&locale=$locale"
client.webSocket(url) {
session = this
diff --git a/android/core/data/src/main/java/io/clawdroid/core/data/repository/AssistantConnectionImpl.kt b/android/core/data/src/main/java/io/clawdroid/core/data/repository/AssistantConnectionImpl.kt
index cae5e0c7c7..6fa2ae46f7 100644
--- a/android/core/data/src/main/java/io/clawdroid/core/data/repository/AssistantConnectionImpl.kt
+++ b/android/core/data/src/main/java/io/clawdroid/core/data/repository/AssistantConnectionImpl.kt
@@ -1,5 +1,6 @@
package io.clawdroid.core.data.repository
+import android.content.Context
import android.util.Log
import io.ktor.client.HttpClient
import io.clawdroid.core.data.remote.WebSocketClient
@@ -25,12 +26,13 @@ import java.util.UUID
typealias ToolRequestCallback = suspend (ToolRequest) -> String
class AssistantConnectionImpl(
- private val httpClient: HttpClient
+ private val httpClient: HttpClient,
+ private val context: Context
) : AssistantConnection {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val clientId = UUID.randomUUID().toString()
- private val wsClient = WebSocketClient(httpClient, scope, clientId, "assistant")
+ private val wsClient = WebSocketClient(httpClient, scope, clientId, "assistant", context = context)
private val json = Json { ignoreUnknownKeys = true }
private val _messages = MutableSharedFlow(extraBufferCapacity = 64)
diff --git a/android/feature/chat/src/main/java/io/clawdroid/feature/chat/component/MessageInput.kt b/android/feature/chat/src/main/java/io/clawdroid/feature/chat/component/MessageInput.kt
index a5eead431c..0f62adab5d 100644
--- a/android/feature/chat/src/main/java/io/clawdroid/feature/chat/component/MessageInput.kt
+++ b/android/feature/chat/src/main/java/io/clawdroid/feature/chat/component/MessageInput.kt
@@ -21,6 +21,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.clawdroid.core.ui.theme.DeepBlack
import io.clawdroid.core.ui.theme.GlassBorder
@@ -29,6 +30,7 @@ import io.clawdroid.core.ui.theme.NeonCyan
import io.clawdroid.core.ui.theme.TextPrimary
import io.clawdroid.core.ui.theme.TextSecondary
import io.clawdroid.core.ui.theme.TextTertiary
+import io.clawdroid.feature.chat.R
import com.composables.icons.lucide.R as LucideR
@Composable
@@ -60,21 +62,21 @@ fun MessageInput(
IconButton(onClick = onCameraClick) {
Icon(
painter = painterResource(LucideR.drawable.lucide_ic_camera),
- contentDescription = "Camera",
+ contentDescription = stringResource(R.string.input_camera),
tint = TextSecondary
)
}
IconButton(onClick = onGalleryClick) {
Icon(
painter = painterResource(LucideR.drawable.lucide_ic_image),
- contentDescription = "Gallery",
+ contentDescription = stringResource(R.string.input_gallery),
tint = TextSecondary
)
}
IconButton(onClick = onMicClick) {
Icon(
painter = painterResource(LucideR.drawable.lucide_ic_mic),
- contentDescription = "Voice",
+ contentDescription = stringResource(R.string.input_voice),
tint = TextSecondary
)
}
@@ -82,7 +84,7 @@ fun MessageInput(
value = text,
onValueChange = onTextChanged,
modifier = Modifier.weight(1f),
- placeholder = { Text("Message...", color = TextTertiary) },
+ placeholder = { Text(stringResource(R.string.input_placeholder), color = TextTertiary) },
maxLines = 4,
shape = RoundedCornerShape(24.dp),
colors = OutlinedTextFieldDefaults.colors(
@@ -106,7 +108,7 @@ fun MessageInput(
) {
Icon(
painter = painterResource(LucideR.drawable.lucide_ic_send_horizontal),
- contentDescription = "Send"
+ contentDescription = stringResource(R.string.input_send)
)
}
}
diff --git a/android/feature/chat/src/main/java/io/clawdroid/feature/chat/screen/ChatScreen.kt b/android/feature/chat/src/main/java/io/clawdroid/feature/chat/screen/ChatScreen.kt
index 41dffd6dd4..098648e4e2 100644
--- a/android/feature/chat/src/main/java/io/clawdroid/feature/chat/screen/ChatScreen.kt
+++ b/android/feature/chat/src/main/java/io/clawdroid/feature/chat/screen/ChatScreen.kt
@@ -35,6 +35,7 @@ import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
import androidx.core.content.FileProvider
import io.clawdroid.core.domain.model.ImageAttachment
import io.clawdroid.core.ui.theme.DeepBlack
@@ -44,6 +45,7 @@ import io.clawdroid.core.ui.theme.GradientPurple
import io.clawdroid.core.ui.theme.TextSecondary
import io.clawdroid.feature.chat.ChatEvent
import io.clawdroid.feature.chat.ChatViewModel
+import io.clawdroid.feature.chat.R
import com.composables.icons.lucide.R as LucideR
import io.clawdroid.feature.chat.component.ConnectionBanner
import io.clawdroid.feature.chat.component.ImagePreviewRow
@@ -227,7 +229,7 @@ fun ChatScreen(
containerColor = Color.Transparent,
topBar = {
TopAppBar(
- title = { Text("ClawDroid") },
+ title = { Text(stringResource(R.string.chat_title)) },
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent
),
@@ -235,7 +237,7 @@ fun ChatScreen(
IconButton(onClick = onNavigateToSettings) {
Icon(
painter = painterResource(LucideR.drawable.lucide_ic_settings),
- contentDescription = "Settings",
+ contentDescription = stringResource(R.string.chat_settings),
tint = TextSecondary
)
}
diff --git a/android/feature/chat/src/main/java/io/clawdroid/feature/chat/voice/VoiceModeOverlay.kt b/android/feature/chat/src/main/java/io/clawdroid/feature/chat/voice/VoiceModeOverlay.kt
index 231cb44757..75bb032d19 100644
--- a/android/feature/chat/src/main/java/io/clawdroid/feature/chat/voice/VoiceModeOverlay.kt
+++ b/android/feature/chat/src/main/java/io/clawdroid/feature/chat/voice/VoiceModeOverlay.kt
@@ -36,10 +36,12 @@ import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import io.clawdroid.core.domain.model.VoicePhase
+import io.clawdroid.feature.chat.R
import io.clawdroid.core.ui.theme.DeepBlack
import io.clawdroid.core.ui.theme.GradientCyan
import io.clawdroid.core.ui.theme.GradientPurple
@@ -96,7 +98,7 @@ fun VoiceModeOverlay(
) {
Icon(
painter = painterResource(LucideR.drawable.lucide_ic_x),
- contentDescription = "Close",
+ contentDescription = stringResource(R.string.voice_close),
modifier = Modifier.size(28.dp),
tint = TextSecondary
)
@@ -199,7 +201,7 @@ fun VoiceModeOverlay(
if (state.isCameraActive) LucideR.drawable.lucide_ic_camera_off
else LucideR.drawable.lucide_ic_camera
),
- contentDescription = if (state.isCameraActive) "Turn off camera" else "Turn on camera",
+ contentDescription = stringResource(if (state.isCameraActive) R.string.voice_camera_off else R.string.voice_camera_on),
modifier = Modifier.size(28.dp),
tint = if (state.isCameraActive) GradientCyan else TextSecondary
)
@@ -209,12 +211,13 @@ fun VoiceModeOverlay(
}
}
+@Composable
private fun phaseLabel(phase: VoicePhase): String = when (phase) {
VoicePhase.IDLE -> ""
- VoicePhase.LISTENING -> "Listening..."
- VoicePhase.PAUSED -> "Paused"
- VoicePhase.SENDING -> "Sending..."
- VoicePhase.THINKING -> "Thinking..."
- VoicePhase.SPEAKING -> "Speaking..."
- VoicePhase.ERROR -> "Error"
+ VoicePhase.LISTENING -> stringResource(R.string.voice_listening)
+ VoicePhase.PAUSED -> stringResource(R.string.voice_paused)
+ VoicePhase.SENDING -> stringResource(R.string.voice_sending)
+ VoicePhase.THINKING -> stringResource(R.string.voice_thinking)
+ VoicePhase.SPEAKING -> stringResource(R.string.voice_speaking)
+ VoicePhase.ERROR -> stringResource(R.string.voice_error)
}
diff --git a/android/feature/chat/src/main/res/values-ja/strings.xml b/android/feature/chat/src/main/res/values-ja/strings.xml
index 372ae80ce1..bb8358a4a4 100644
--- a/android/feature/chat/src/main/res/values-ja/strings.xml
+++ b/android/feature/chat/src/main/res/values-ja/strings.xml
@@ -21,4 +21,26 @@
音声
開く
戻る
+
+
+ 閉じる
+ カメラをオン
+ カメラをオフ
+ 聞いています…
+ 一時停止
+ 送信中…
+ 思考中…
+ 話しています…
+ エラー
+
+
+ カメラ
+ ギャラリー
+ 音声
+ 送信
+ メッセージ…
+
+
+ ClawDroid
+ 設定
diff --git a/android/feature/chat/src/main/res/values/strings.xml b/android/feature/chat/src/main/res/values/strings.xml
index 937a1f5f5d..55580361bc 100644
--- a/android/feature/chat/src/main/res/values/strings.xml
+++ b/android/feature/chat/src/main/res/values/strings.xml
@@ -21,4 +21,26 @@
Voice
Open
Back
+
+
+ Close
+ Turn on camera
+ Turn off camera
+ Listening…
+ Paused
+ Sending…
+ Thinking…
+ Speaking…
+ Error
+
+
+ Camera
+ Gallery
+ Voice
+ Send
+ Message…
+
+
+ ClawDroid
+ Settings
diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go
index 61abecfbb2..e3f1824811 100644
--- a/pkg/agent/loop.go
+++ b/pkg/agent/loop.go
@@ -455,6 +455,18 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage)
return al.processSystemMessage(ctx, msg)
}
+ // Extract input_mode and locale from metadata
+ inputMode := "text"
+ locale := "en"
+ if msg.Metadata != nil {
+ if mode, ok := msg.Metadata["input_mode"]; ok && mode != "" {
+ inputMode = mode
+ }
+ if l, ok := msg.Metadata["locale"]; ok && l != "" {
+ locale = i18n.NormalizeLocale(l)
+ }
+ }
+
// Check request rate limit
if err := al.rateLimiter.checkRequest(); err != nil {
logger.WarnCF("agent", "Request rate limited",
@@ -462,26 +474,14 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage)
"channel": msg.Channel,
"sender_id": msg.SenderID,
})
- return fmt.Sprintf("Rate limited: %v. Please try again later.", err), nil
+ return i18n.Tf(locale, "agent.rate_limited", err), nil
}
// Check for commands
- if response, handled := al.handleCommand(ctx, msg); handled {
+ if response, handled := al.handleCommand(ctx, msg, locale); handled {
return response, nil
}
- // Extract input_mode and locale from metadata
- inputMode := "text"
- locale := "en"
- if msg.Metadata != nil {
- if mode, ok := msg.Metadata["input_mode"]; ok && mode != "" {
- inputMode = mode
- }
- if l, ok := msg.Metadata["locale"]; ok && l != "" {
- locale = i18n.NormalizeLocale(l)
- }
- }
-
// Send migration notice once on first message
al.migrationOnce.Do(func() {
if al.userStore != nil && al.userStore.NeedsMigration() {
@@ -937,7 +937,7 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, messages []providers.M
})
toolResultMsg := providers.Message{
Role: "tool",
- Content: fmt.Sprintf("Rate limited: %v", err),
+ Content: i18n.Tf(locale, "agent.rate_limited_tool", err),
ToolCallID: tc.ID,
}
messages = append(messages, toolResultMsg)
@@ -1361,7 +1361,7 @@ func (al *AgentLoop) estimateTokens(messages []providers.Message) int {
return totalChars * 2 / 5
}
-func (al *AgentLoop) handleCommand(ctx context.Context, msg bus.InboundMessage) (string, bool) {
+func (al *AgentLoop) handleCommand(ctx context.Context, msg bus.InboundMessage, locale string) (string, bool) {
content := strings.TrimSpace(msg.Content)
if !strings.HasPrefix(content, "/") {
return "", false
@@ -1378,41 +1378,40 @@ func (al *AgentLoop) handleCommand(ctx context.Context, msg bus.InboundMessage)
switch cmd {
case "/show":
if len(args) < 1 {
- return "Usage: /show [model|channel]", true
+ return i18n.T(locale, "cmd.show.usage"), true
}
switch args[0] {
case "model":
- return fmt.Sprintf("Current model: %s", al.model), true
+ return i18n.Tf(locale, "agent.cmd.show.model", al.model), true
case "channel":
- return fmt.Sprintf("Current channel: %s", msg.Channel), true
+ return i18n.Tf(locale, "agent.cmd.show.channel", msg.Channel), true
default:
- return fmt.Sprintf("Unknown show target: %s", args[0]), true
+ return i18n.Tf(locale, "agent.cmd.show.unknown", args[0]), true
}
case "/list":
if len(args) < 1 {
- return "Usage: /list [models|channels]", true
+ return i18n.T(locale, "cmd.list.usage"), true
}
switch args[0] {
case "models":
- // TODO: Fetch available models dynamically if possible
- return "Available models: glm-4.7, claude-3-5-sonnet, gpt-4o (configured in config.json/env)", true
+ return i18n.T(locale, "agent.cmd.list.models"), true
case "channels":
if al.channelManager == nil {
- return "Channel manager not initialized", true
+ return i18n.T(locale, "agent.cmd.channel_mgr_error"), true
}
channels := al.channelManager.GetEnabledChannels()
if len(channels) == 0 {
- return "No channels enabled", true
+ return i18n.T(locale, "agent.cmd.list.no_channels"), true
}
- return fmt.Sprintf("Enabled channels: %s", strings.Join(channels, ", ")), true
+ return i18n.Tf(locale, "agent.cmd.list.channels", strings.Join(channels, ", ")), true
default:
- return fmt.Sprintf("Unknown list target: %s", args[0]), true
+ return i18n.Tf(locale, "agent.cmd.list.unknown", args[0]), true
}
case "/switch":
if len(args) < 3 || args[1] != "to" {
- return "Usage: /switch [model|channel] to ", true
+ return i18n.T(locale, "agent.cmd.switch.usage"), true
}
target := args[0]
value := args[2]
@@ -1421,23 +1420,17 @@ func (al *AgentLoop) handleCommand(ctx context.Context, msg bus.InboundMessage)
case "model":
oldModel := al.model
al.model = value
- return fmt.Sprintf("Switched model from %s to %s", oldModel, value), true
+ return i18n.Tf(locale, "agent.cmd.switch.model", oldModel, value), true
case "channel":
- // This changes the 'default' channel for some operations, or effectively redirects output?
- // For now, let's just validate if the channel exists
if al.channelManager == nil {
- return "Channel manager not initialized", true
+ return i18n.T(locale, "agent.cmd.channel_mgr_error"), true
}
if _, exists := al.channelManager.GetChannel(value); !exists && value != "cli" {
- return fmt.Sprintf("Channel '%s' not found or not enabled", value), true
+ return i18n.Tf(locale, "agent.cmd.switch.not_found", value), true
}
-
- // If message came from CLI, maybe we want to redirect CLI output to this channel?
- // That would require state persistence about "redirected channel"
- // For now, just acknowledged.
- return fmt.Sprintf("Switched target channel to %s (Note: this currently only validates existence)", value), true
+ return i18n.Tf(locale, "agent.cmd.switch.channel", value), true
default:
- return fmt.Sprintf("Unknown switch target: %s", target), true
+ return i18n.Tf(locale, "agent.cmd.switch.unknown", target), true
}
}
diff --git a/pkg/channels/telegram.go b/pkg/channels/telegram.go
index 43d25f4c30..915ca7dfd9 100644
--- a/pkg/channels/telegram.go
+++ b/pkg/channels/telegram.go
@@ -17,6 +17,7 @@ import (
"github.com/KarakuriAgent/clawdroid/pkg/bus"
"github.com/KarakuriAgent/clawdroid/pkg/config"
+ "github.com/KarakuriAgent/clawdroid/pkg/i18n"
"github.com/KarakuriAgent/clawdroid/pkg/logger"
"github.com/KarakuriAgent/clawdroid/pkg/utils"
)
@@ -307,7 +308,9 @@ func (c *TelegramChannel) handleMessage(ctx context.Context, message *telego.Mes
_, thinkCancel := context.WithTimeout(ctx, 5*time.Minute)
c.stopThinking.Store(chatIDStr, &thinkingCancel{fn: thinkCancel})
- pMsg, err := c.bot.SendMessage(ctx, tu.Message(tu.ID(chatID), "Thinking... 💭"))
+ locale := localeFromMessage(*message)
+
+ pMsg, err := c.bot.SendMessage(ctx, tu.Message(tu.ID(chatID), i18n.T(locale, "channel.thinking")))
if err == nil {
pID := pMsg.MessageID
c.placeholders.Store(chatIDStr, pID)
@@ -324,6 +327,7 @@ func (c *TelegramChannel) handleMessage(ctx context.Context, message *telego.Mes
"first_name": user.FirstName,
"sender_name": senderName,
"is_group": fmt.Sprintf("%t", message.Chat.Type != "private"),
+ "locale": locale,
}
c.HandleMessage(fmt.Sprintf("%d", user.ID), fmt.Sprintf("%d", chatID), content, mediaPaths, metadata)
diff --git a/pkg/channels/telegram_commands.go b/pkg/channels/telegram_commands.go
index ea512100a3..8a8a27b59f 100644
--- a/pkg/channels/telegram_commands.go
+++ b/pkg/channels/telegram_commands.go
@@ -2,10 +2,10 @@ package channels
import (
"context"
- "fmt"
"strings"
"github.com/KarakuriAgent/clawdroid/pkg/config"
+ "github.com/KarakuriAgent/clawdroid/pkg/i18n"
"github.com/mymmrac/telego"
)
@@ -36,14 +36,10 @@ func commandArgs(text string) string {
return strings.TrimSpace(parts[1])
}
func (c *cmd) Help(ctx context.Context, message telego.Message) error {
- msg := `/start - Start the bot
-/help - Show this help message
-/show [model|channel] - Show current configuration
-/list [models|channels] - List available options
- `
+ locale := localeFromMessage(message)
_, err := c.bot.SendMessage(ctx, &telego.SendMessageParams{
ChatID: telego.ChatID{ID: message.Chat.ID},
- Text: msg,
+ Text: i18n.T(locale, "cmd.help"),
ReplyParameters: &telego.ReplyParameters{
MessageID: message.MessageID,
},
@@ -52,9 +48,10 @@ func (c *cmd) Help(ctx context.Context, message telego.Message) error {
}
func (c *cmd) Start(ctx context.Context, message telego.Message) error {
+ locale := localeFromMessage(message)
_, err := c.bot.SendMessage(ctx, &telego.SendMessageParams{
ChatID: telego.ChatID{ID: message.Chat.ID},
- Text: "Hello! I am ClawDroid 🦞",
+ Text: i18n.T(locale, "cmd.start"),
ReplyParameters: &telego.ReplyParameters{
MessageID: message.MessageID,
},
@@ -63,11 +60,12 @@ func (c *cmd) Start(ctx context.Context, message telego.Message) error {
}
func (c *cmd) Show(ctx context.Context, message telego.Message) error {
+ locale := localeFromMessage(message)
args := commandArgs(message.Text)
if args == "" {
_, err := c.bot.SendMessage(ctx, &telego.SendMessageParams{
ChatID: telego.ChatID{ID: message.Chat.ID},
- Text: "Usage: /show [model|channel]",
+ Text: i18n.T(locale, "cmd.show.usage"),
ReplyParameters: &telego.ReplyParameters{
MessageID: message.MessageID,
},
@@ -78,12 +76,11 @@ func (c *cmd) Show(ctx context.Context, message telego.Message) error {
var response string
switch args {
case "model":
- response = fmt.Sprintf("Current Model: %s",
- c.config.LLM.Model)
+ response = i18n.Tf(locale, "cmd.show.model", c.config.LLM.Model)
case "channel":
- response = "Current Channel: telegram"
+ response = i18n.T(locale, "cmd.show.channel")
default:
- response = fmt.Sprintf("Unknown parameter: %s. Try 'model' or 'channel'.", args)
+ response = i18n.Tf(locale, "cmd.show.unknown", args)
}
_, err := c.bot.SendMessage(ctx, &telego.SendMessageParams{
@@ -96,11 +93,12 @@ func (c *cmd) Show(ctx context.Context, message telego.Message) error {
return err
}
func (c *cmd) List(ctx context.Context, message telego.Message) error {
+ locale := localeFromMessage(message)
args := commandArgs(message.Text)
if args == "" {
_, err := c.bot.SendMessage(ctx, &telego.SendMessageParams{
ChatID: telego.ChatID{ID: message.Chat.ID},
- Text: "Usage: /list [models|channels]",
+ Text: i18n.T(locale, "cmd.list.usage"),
ReplyParameters: &telego.ReplyParameters{
MessageID: message.MessageID,
},
@@ -111,8 +109,7 @@ func (c *cmd) List(ctx context.Context, message telego.Message) error {
var response string
switch args {
case "models":
- response = fmt.Sprintf("Configured Model: %s\n\nTo change models, update config.json",
- c.config.LLM.Model)
+ response = i18n.Tf(locale, "cmd.list.models", c.config.LLM.Model)
case "channels":
var enabled []string
@@ -131,10 +128,10 @@ func (c *cmd) List(ctx context.Context, message telego.Message) error {
if c.config.Channels.LINE.Enabled {
enabled = append(enabled, "line")
}
- response = fmt.Sprintf("Enabled Channels:\n- %s", strings.Join(enabled, "\n- "))
+ response = i18n.Tf(locale, "cmd.list.channels", strings.Join(enabled, "\n- "))
default:
- response = fmt.Sprintf("Unknown parameter: %s. Try 'models' or 'channels'.", args)
+ response = i18n.Tf(locale, "cmd.list.unknown", args)
}
_, err := c.bot.SendMessage(ctx, &telego.SendMessageParams{
@@ -146,3 +143,10 @@ func (c *cmd) List(ctx context.Context, message telego.Message) error {
})
return err
}
+
+func localeFromMessage(message telego.Message) string {
+ if message.From != nil {
+ return i18n.NormalizeLocale(message.From.LanguageCode)
+ }
+ return "en"
+}
diff --git a/pkg/channels/websocket.go b/pkg/channels/websocket.go
index 945d78f1d6..234145bcd3 100644
--- a/pkg/channels/websocket.go
+++ b/pkg/channels/websocket.go
@@ -12,6 +12,7 @@ import (
"github.com/KarakuriAgent/clawdroid/pkg/broadcast"
"github.com/KarakuriAgent/clawdroid/pkg/bus"
"github.com/KarakuriAgent/clawdroid/pkg/config"
+ "github.com/KarakuriAgent/clawdroid/pkg/i18n"
"github.com/KarakuriAgent/clawdroid/pkg/logger"
"github.com/KarakuriAgent/clawdroid/pkg/tools"
"github.com/google/uuid"
@@ -254,7 +255,7 @@ func (c *WebSocketChannel) handleWS(w http.ResponseWriter, r *http.Request) {
if c.configPath != "" {
if _, err := os.Stat(c.configPath); os.IsNotExist(err) {
setupMsg := wsOutgoing{
- Content: "Configuration required",
+ Content: i18n.T(locale, "channel.config_required"),
Type: "setup_required",
}
if data, err := json.Marshal(setupMsg); err == nil {
diff --git a/pkg/i18n/messages_agent.go b/pkg/i18n/messages_agent.go
index 49e51e23d4..dc81697ac9 100644
--- a/pkg/i18n/messages_agent.go
+++ b/pkg/i18n/messages_agent.go
@@ -5,11 +5,15 @@ func init() {
"agent.migration_notice": "USER.md found. User management has migrated to a new format (users.json).\nAsk in chat to migrate, or update manually.\n\nFor manual update, create ~/.clawdroid/data/users.json in this format:\n```json\n{\n \"users\": [{\n \"name\": \"Your Name\",\n \"channels\": { \"websocket\": [\"default\"] },\n \"memo\": [\"Preferred language: English\"]\n }]\n}\n```",
"agent.context_window_warning": "⚠️ Context window exceeded. Compressing history and retrying...",
"agent.memory_threshold_warning": "⚠️ Memory threshold reached. Optimizing conversation history...",
+ "agent.rate_limited": "Rate limited: %s. Please try again later.",
+ "agent.rate_limited_tool": "Rate limited: %s",
})
register("ja", map[string]string{
"agent.migration_notice": "USER.md が見つかりました。ユーザー管理が新しい形式(users.json)に変わりました。\nチャットで移行を依頼するか、手動で更新してください。\n\n手動更新の場合、以下の形式で ~/.clawdroid/data/users.json を作成:\n```json\n{\n \"users\": [{\n \"name\": \"あなたの名前\",\n \"channels\": { \"websocket\": [\"default\"] },\n \"memo\": [\"Preferred language: Japanese\"]\n }]\n}\n```",
"agent.context_window_warning": "⚠️ コンテキストウィンドウの上限を超えました。履歴を圧縮してリトライしています...",
"agent.memory_threshold_warning": "⚠️ メモリしきい値に達しました。会話履歴を最適化しています...",
+ "agent.rate_limited": "レート制限中: %s。しばらくしてからお試しください。",
+ "agent.rate_limited_tool": "レート制限中: %s",
})
}
diff --git a/pkg/i18n/messages_channel.go b/pkg/i18n/messages_channel.go
new file mode 100644
index 0000000000..253b573a2a
--- /dev/null
+++ b/pkg/i18n/messages_channel.go
@@ -0,0 +1,83 @@
+package i18n
+
+func init() {
+ register("en", map[string]string{
+ // Telegram
+ "channel.thinking": "Thinking... 💭",
+
+ // WebSocket
+ "channel.config_required": "Configuration required",
+
+ // Telegram commands (/help, /start, /show, /list)
+ "cmd.help": `/start - Start the bot
+/help - Show this help message
+/show [model|channel] - Show current configuration
+/list [models|channels] - List available options
+`,
+ "cmd.start": "Hello! I am ClawDroid 🦞",
+ "cmd.show.usage": "Usage: /show [model|channel]",
+ "cmd.show.model": "Current Model: %s",
+ "cmd.show.channel": "Current Channel: telegram",
+ "cmd.show.unknown": "Unknown parameter: %s. Try 'model' or 'channel'.",
+ "cmd.list.usage": "Usage: /list [models|channels]",
+ "cmd.list.models": "Configured Model: %s\n\nTo change models, update config.json",
+ "cmd.list.channels": "Enabled Channels:\n- %s",
+ "cmd.list.unknown": "Unknown parameter: %s. Try 'models' or 'channels'.",
+
+ // Agent loop commands (/show, /list, /switch)
+ // cmd.show.usage and cmd.list.usage are shared with Telegram commands
+ "agent.cmd.show.model": "Current model: %s",
+ "agent.cmd.show.channel": "Current channel: %s",
+ "agent.cmd.show.unknown": "Unknown show target: %s",
+ "agent.cmd.list.models": "Available models: glm-4.7, claude-3-5-sonnet, gpt-4o (configured in config.json/env)",
+ "agent.cmd.list.no_channels": "No channels enabled",
+ "agent.cmd.list.channels": "Enabled channels: %s",
+ "agent.cmd.list.unknown": "Unknown list target: %s",
+ "agent.cmd.switch.usage": "Usage: /switch [model|channel] to ",
+ "agent.cmd.switch.model": "Switched model from %s to %s",
+ "agent.cmd.switch.channel": "Switched target channel to %s (Note: this currently only validates existence)",
+ "agent.cmd.switch.not_found": "Channel '%s' not found or not enabled",
+ "agent.cmd.switch.unknown": "Unknown switch target: %s",
+ "agent.cmd.channel_mgr_error": "Channel manager not initialized",
+ })
+
+ register("ja", map[string]string{
+ // Telegram
+ "channel.thinking": "考え中... 💭",
+
+ // WebSocket
+ "channel.config_required": "設定が必要です",
+
+ // Telegram commands
+ "cmd.help": `/start - ボットを開始
+/help - このヘルプメッセージを表示
+/show [model|channel] - 現在の設定を表示
+/list [models|channels] - 利用可能なオプションを一覧表示
+`,
+ "cmd.start": "こんにちは!ClawDroid です 🦞",
+ "cmd.show.usage": "使い方: /show [model|channel]",
+ "cmd.show.model": "現在のモデル: %s",
+ "cmd.show.channel": "現在のチャンネル: telegram",
+ "cmd.show.unknown": "不明なパラメータ: %s。'model' か 'channel' を指定してください。",
+ "cmd.list.usage": "使い方: /list [models|channels]",
+ "cmd.list.models": "設定済みモデル: %s\n\nモデルを変更するには config.json を更新してください",
+ "cmd.list.channels": "有効なチャンネル:\n- %s",
+ "cmd.list.unknown": "不明なパラメータ: %s。'models' か 'channels' を指定してください。",
+
+ // Agent loop commands
+ // cmd.show.usage and cmd.list.usage are shared with Telegram commands
+ "agent.cmd.show.model": "現在のモデル: %s",
+ "agent.cmd.show.channel": "現在のチャンネル: %s",
+ "agent.cmd.show.unknown": "不明な表示対象: %s",
+ "agent.cmd.list.models": "利用可能なモデル: glm-4.7, claude-3-5-sonnet, gpt-4o(config.json/env で設定)",
+ "agent.cmd.list.no_channels": "有効なチャンネルはありません",
+ "agent.cmd.list.channels": "有効なチャンネル: %s",
+ "agent.cmd.list.unknown": "不明な一覧対象: %s",
+ "agent.cmd.switch.usage": "使い方: /switch [model|channel] to <名前>",
+ "agent.cmd.switch.model": "モデルを %s から %s に切り替えました",
+ "agent.cmd.switch.channel": "対象チャンネルを %s に切り替えました(注: 現在は存在確認のみ)",
+ "agent.cmd.switch.not_found": "チャンネル '%s' が見つからないか有効ではありません",
+ "agent.cmd.switch.unknown": "不明な切り替え対象: %s",
+ "agent.cmd.channel_mgr_error": "チャンネルマネージャーが初期化されていません",
+ })
+}