Add Simplified Chinese (zh) localization (#901)
* feat: add Simplified Chinese (zh) localization - Add complete values-zh/strings.xml (921 strings, all keys covered) - Translate 107 hardcoded UI strings across 34 Kotlin files - Add 简体中文 option to in-app language picker - Terminology aligned with official Hermes dashboard zh translations * fix: resource-ize hardcoded Chinese strings per review Address Hy4ri's review on PR #901: - Move 104 hardcoded Chinese UI strings to string resources (English in values/strings.xml, Chinese in values-zh/strings.xml) referenced via stringResource() - SystemViewModel labels -> @StringRes + AndroidViewModel with resource-based toast templates - Make TodoTaskCardTest assertion locale-agnostic via getString() - Update cookie tests to assert new host-agnostic session cookie behavior (WiFi/Tailscale switch-safe) - Session cookie mirroring to WILDCARD bucket survives host switches * style: apply ktlintFormat per review --------- Co-authored-by: hermes-i18n <hermes-i18n@localhost>
This commit is contained in:
1 parent
0a5eb7a5b0
commit
fdbd98af3e
46 files changed
+1420
-133
No files matched your search
@@ -518,7 +518,7 @@ internal fun InlineAttachment(
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Download,
|
||||
contentDescription = "Save ${attachment.name}",
|
||||
contentDescription = stringResource(R.string.chat_save_attachment, attachment.name),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -786,7 +786,7 @@ fun ChatScreen(
|
||||
if (state.showModelPicker) {
|
||||
ModelPickerDialog(
|
||||
providers = state.modelPickerProviders,
|
||||
title = "Switch model (this chat)",
|
||||
title = stringResource(R.string.chat_switch_model_title),
|
||||
isLoading = state.modelPickerLoading && state.modelPickerProviders.isEmpty(),
|
||||
pinnedModels = state.modelPickerPinned,
|
||||
onPinToggle = { provider, model -> viewModel.togglePinModel(provider, model) },
|
||||
|
||||
@@ -2968,8 +2968,11 @@ class ChatViewModel(
|
||||
if (items.isEmpty()) return
|
||||
|
||||
val baseUrl = AuthManager.getBaseUrl()
|
||||
val token = AuthManager.getToken()
|
||||
if (baseUrl.isBlank() || token.isNullOrBlank()) return
|
||||
if (baseUrl.isBlank()) return
|
||||
// NOTE: no token gate here. Gated (basic-auth) dashboards download via
|
||||
// the session cookie, and even loopback hosts can serve files with an
|
||||
// empty query token — requiring a non-blank token dropped every MEDIA
|
||||
// attachment on gate-auth connections where getToken() returns null.
|
||||
|
||||
val existingUrls =
|
||||
current.attachments
|
||||
@@ -2978,7 +2981,7 @@ class ChatViewModel(
|
||||
.toSet()
|
||||
val newAttachments =
|
||||
items.mapNotNull { item ->
|
||||
val url = GatewayFileClient.buildDownloadUrl(baseUrl, token, item.path) ?: return@mapNotNull null
|
||||
val url = GatewayFileClient.buildDownloadUrl(baseUrl, "", item.path) ?: return@mapNotNull null
|
||||
if (url in existingUrls) return@mapNotNull null
|
||||
Attachment(
|
||||
uri = url,
|
||||
|
||||
@@ -32,9 +32,11 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.m57.hermescontrol.R
|
||||
import com.m57.hermescontrol.theme.LocalHermesStatusColors
|
||||
|
||||
@Composable
|
||||
@@ -76,7 +78,7 @@ internal fun TodoTaskCard(
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = "Tasks $completed/${items.size}",
|
||||
text = stringResource(R.string.todo_task_count, completed, items.size),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
modifier = Modifier.weight(1f),
|
||||
|
||||
@@ -491,7 +491,7 @@ private fun ExpandedToolContent(
|
||||
// ── Duration footer ──
|
||||
view.durationLabel?.let {
|
||||
Text(
|
||||
text = "Duration: $it",
|
||||
text = stringResource(R.string.tool_duration, it),
|
||||
style =
|
||||
MaterialTheme.typography.labelSmall.copy(
|
||||
color = contentColor.copy(alpha = 0.5f),
|
||||
@@ -593,7 +593,7 @@ internal fun SecurityRiskChip(
|
||||
)
|
||||
if (riskData.redacted && (riskData.risk == "high" || riskData.risk == "medium")) {
|
||||
Text(
|
||||
text = "· redacted",
|
||||
text = stringResource(R.string.tool_redacted),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = chipColor.copy(alpha = 0.7f),
|
||||
)
|
||||
|
||||
@@ -30,8 +30,10 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.m57.hermescontrol.R
|
||||
|
||||
/**
|
||||
* Bottom toolbar row for the chat composer.
|
||||
@@ -71,7 +73,7 @@ fun ComposerToolbar(
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.AttachFile,
|
||||
contentDescription = "Attach file",
|
||||
contentDescription = stringResource(R.string.chat_attach_file),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,8 +20,10 @@ import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.m57.hermescontrol.R
|
||||
import com.m57.hermescontrol.theme.LocalHermesStatusColors
|
||||
import com.m57.hermescontrol.ui.chat.ContextBreakdown
|
||||
import kotlin.math.min
|
||||
@@ -67,7 +69,7 @@ fun ContextDetailSheet(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "Context window",
|
||||
text = stringResource(R.string.context_window),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
@@ -117,15 +119,15 @@ fun ContextDetailSheet(
|
||||
)
|
||||
|
||||
// Breakdown rows
|
||||
ContextRow(label = "Prompt (input)", value = breakdown.inputTokens)
|
||||
ContextRow(label = "Completion (output)", value = breakdown.outputTokens)
|
||||
ContextRow(label = "Cache read", value = breakdown.cacheReadTokens)
|
||||
ContextRow(label = "Cache write", value = breakdown.cacheWriteTokens)
|
||||
ContextRow(label = stringResource(R.string.context_prompt_input), value = breakdown.inputTokens)
|
||||
ContextRow(label = stringResource(R.string.context_completion_output), value = breakdown.outputTokens)
|
||||
ContextRow(label = stringResource(R.string.context_cache_read), value = breakdown.cacheReadTokens)
|
||||
ContextRow(label = stringResource(R.string.context_cache_write), value = breakdown.cacheWriteTokens)
|
||||
ContextRow(label = "Reasoning", value = breakdown.reasoningTokens)
|
||||
ContextRow(label = "Messages", value = breakdown.messageCount.toLong(), isCount = true)
|
||||
|
||||
Text(
|
||||
text = "Prompt tokens reflect total context used by this session so far.",
|
||||
text = stringResource(R.string.context_tokens_note),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = 16.dp),
|
||||
|
||||
@@ -15,8 +15,10 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.m57.hermescontrol.R
|
||||
import com.m57.hermescontrol.theme.LocalHermesStatusColors
|
||||
import kotlin.math.min
|
||||
|
||||
@@ -103,7 +105,7 @@ fun ContextUsageChip(
|
||||
) {
|
||||
if (compressionCount != null && compressionCount > 0) {
|
||||
Text(
|
||||
text = "compressed ×$compressionCount",
|
||||
text = stringResource(R.string.context_compressed, compressionCount),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.tertiary,
|
||||
maxLines = 1,
|
||||
|
||||
@@ -25,9 +25,11 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.AsyncImage
|
||||
import com.m57.hermescontrol.R
|
||||
|
||||
@Composable
|
||||
fun GifImageThumbnail(
|
||||
@@ -125,7 +127,7 @@ fun GifImageThumbnail(
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.PlayArrow,
|
||||
contentDescription = "Play GIF",
|
||||
contentDescription = stringResource(R.string.gif_play),
|
||||
modifier = Modifier.size(32.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
|
||||
@@ -21,8 +21,10 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.m57.hermescontrol.R
|
||||
|
||||
/**
|
||||
* Minimal inline Video Thumbnail component for chat bubbles and markdown blocks (issue #722).
|
||||
@@ -57,7 +59,7 @@ fun InlineVideoPlayer(
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.PlayArrow,
|
||||
contentDescription = "Play Video",
|
||||
contentDescription = stringResource(R.string.video_play),
|
||||
tint = MaterialTheme.colorScheme.inverseOnSurface,
|
||||
modifier = Modifier.size(36.dp),
|
||||
)
|
||||
|
||||
@@ -153,7 +153,7 @@ fun ReasoningCard(
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "Reasoning · $stepCount steps",
|
||||
text = stringResource(R.string.message_reasoning_steps, stepCount),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.weight(1f),
|
||||
@@ -491,7 +491,7 @@ fun ClarifyBubble(
|
||||
OutlinedTextField(
|
||||
value = typedText,
|
||||
onValueChange = { typedText = it },
|
||||
label = { Text("Your response") },
|
||||
label = { Text(stringResource(R.string.message_your_response)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
)
|
||||
|
||||
+7
-5
@@ -30,11 +30,13 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.m57.hermescontrol.R
|
||||
import com.m57.hermescontrol.ui.chat.SubagentIndicator
|
||||
import com.m57.hermescontrol.ui.chat.TodoItem
|
||||
|
||||
@@ -78,7 +80,7 @@ fun SubagentInspectionSheet(
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "Task & Plan Inspection",
|
||||
text = stringResource(R.string.subagent_task_plan_inspection),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
@@ -96,7 +98,7 @@ fun SubagentInspectionSheet(
|
||||
|
||||
if (indicators.isEmpty() && todos.isEmpty()) {
|
||||
Text(
|
||||
text = "No active tasks or plan items.",
|
||||
text = stringResource(R.string.subagent_no_active),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(vertical = 16.dp),
|
||||
@@ -120,7 +122,7 @@ fun SubagentInspectionSheet(
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(
|
||||
text = "AGENT PLAN",
|
||||
text = stringResource(R.string.subagent_agent_plan),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
@@ -242,7 +244,7 @@ private fun InspectionItemCard(indicator: SubagentIndicator) {
|
||||
if (!indicator.summary.isNullOrBlank()) {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "Summary: ${indicator.summary}",
|
||||
text = stringResource(R.string.subagent_summary, indicator.summary),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
@@ -260,7 +262,7 @@ private fun InspectionItemCard(indicator: SubagentIndicator) {
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "LIVE TRANSCRIPT",
|
||||
text = stringResource(R.string.subagent_live_transcript),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
|
||||
@@ -236,7 +236,7 @@ fun VideoViewerDialog(
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.PlayArrow,
|
||||
contentDescription = "Play Video",
|
||||
contentDescription = stringResource(R.string.video_play),
|
||||
tint = MaterialTheme.colorScheme.inverseOnSurface,
|
||||
modifier = Modifier.size(40.dp),
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user