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:
gslzwhlandhermes-i18n authored and GitHub committed 2026-08-15 11:33:43 +03:00
1 parent 0a5eb7a5b0
commit fdbd98af3e
46 files changed
+1420 -133

No files matched your search

@@ -157,7 +157,7 @@ fun ModelScreen(
SearchBar(
query = query,
onQueryChange = { query = it },
placeholder = "Search models...",
placeholder = stringResource(R.string.model_search_placeholder),
)
}
@@ -221,7 +221,7 @@ fun ModelScreen(
if (state.modelPickerConfirmMessage != null) {
AlertDialog(
onDismissRequest = { viewModel.dismissModelPickerConfirm() },
title = { Text("Expensive Model") },
title = { Text(stringResource(R.string.model_expensive_title)) },
text = { Text(state.modelPickerConfirmMessage!!) },
confirmButton = {
Button(onClick = { viewModel.confirmModelPickerExpensive() }) {
@@ -240,7 +240,7 @@ fun ModelScreen(
if (state.showMainModelPicker) {
ModelPickerDialog(
providers = state.providers,
title = "Set Main Model",
title = stringResource(R.string.model_set_main),
isLoading = state.isLoading && state.providers.isEmpty(),
pinnedModels = state.pinnedModels,
onPinToggle = { provider, model -> viewModel.togglePinModel(provider, model) },
@@ -266,7 +266,7 @@ fun ModelScreen(
if (state.showAuxModelPicker) {
ModelPickerDialog(
providers = state.providers,
title = "Set Aux: ${state.auxPickerTask}",
title = stringResource(R.string.model_set_aux, state.auxPickerTask),
isLoading = state.isLoading && state.providers.isEmpty(),
pinnedModels = state.pinnedModels,
onPinToggle = { provider, model -> viewModel.togglePinModel(provider, model) },
@@ -330,12 +330,12 @@ private fun ModelSettingsSection(
modifier = Modifier.size(16.dp),
)
Text(
text = "Model Settings",
text = stringResource(R.string.model_settings),
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.Bold,
)
Text(
text = "applies to new sessions",
text = stringResource(R.string.model_new_sessions),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -353,7 +353,7 @@ private fun ModelSettingsSection(
modifier = Modifier.size(16.dp),
)
},
label = "Main model",
label = stringResource(R.string.model_main_label),
value = if (mainProvider.isNotBlank()) "$mainProvider · $mainModel" else "(unset)",
actionLabel = "Change",
onAction = onSetMainModel,
@@ -371,7 +371,7 @@ private fun ModelSettingsSection(
modifier = Modifier.size(16.dp),
)
},
label = "Auxiliary tasks",
label = stringResource(R.string.model_aux_tasks_label),
value =
if (auxOverrideCount > 0) {
"$auxOverrideCount override · ${AUX_TASKS.size - auxOverrideCount} auto"
@@ -394,7 +394,7 @@ private fun ModelSettingsSection(
modifier = Modifier.size(16.dp),
)
},
label = "Mixture of Agents",
label = stringResource(R.string.model_moa_label),
value = if (moaEnabled) moaSummary else "not loaded",
actionLabel = "Configure",
onAction = onConfigureMoa,
@@ -461,7 +461,7 @@ private fun AuxTasksDialog(
) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Auxiliary Tasks") },
title = { Text(stringResource(R.string.model_aux_tasks_title)) },
text = {
Column {
Text(
@@ -530,7 +530,7 @@ private fun AuxTasksDialog(
) {
Icon(Icons.Filled.Delete, contentDescription = null, modifier = Modifier.size(14.dp))
Spacer(modifier = Modifier.width(4.dp))
Text("Reset all to auto")
Text(stringResource(R.string.model_reset_all_auto))
}
}
},
@@ -34,8 +34,10 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
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.data.model.MoaConfigPreset
import com.m57.hermescontrol.data.model.MoaModelSlot
import com.m57.hermescontrol.ui.common.SearchBar
@@ -58,7 +60,7 @@ internal fun MoaConfigDialog(
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Mixture of Agents") },
title = { Text(stringResource(R.string.moa_title)) },
text = {
if (pickerTarget != null && effectivePreset != null) {
// Inline model picker within the dialog
@@ -228,13 +230,13 @@ private fun MoaConfigEditor(
) {
Column {
Text(
text = "Presets appear as models under the Mixture of Agents provider.",
text = stringResource(R.string.moa_presets_note),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
if (effectivePreset == null) {
Text("No presets configured", style = MaterialTheme.typography.bodyMedium)
Text(stringResource(R.string.moa_no_presets), style = MaterialTheme.typography.bodyMedium)
return
}
@@ -248,7 +250,7 @@ private fun MoaConfigEditor(
) {
// Simplified preset dropdown with buttons
if (presetNames.isNotEmpty()) {
Text("Preset:", style = MaterialTheme.typography.bodySmall)
Text(stringResource(R.string.moa_preset_label), style = MaterialTheme.typography.bodySmall)
presetNames.forEach { name ->
TextButton(
onClick = { onSelectPreset(name) },
@@ -286,7 +288,7 @@ private fun MoaConfigEditor(
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp),
enabled = presetNames.size > 1,
) {
Text("Set default", style = MaterialTheme.typography.labelSmall)
Text(stringResource(R.string.moa_set_default), style = MaterialTheme.typography.labelSmall)
}
OutlinedButton(
onClick = onDeletePreset,
@@ -306,13 +308,18 @@ private fun MoaConfigEditor(
OutlinedTextField(
value = newPresetName,
onValueChange = onNewPresetNameChange,
placeholder = { Text("new preset name", style = MaterialTheme.typography.bodySmall) },
placeholder = {
Text(
stringResource(R.string.moa_new_preset_name),
style = MaterialTheme.typography.bodySmall,
)
},
singleLine = true,
modifier = Modifier.weight(1f).height(48.dp),
textStyle = MaterialTheme.typography.bodySmall,
)
IconButton(onClick = onAddPreset, enabled = newPresetName.isNotBlank()) {
Icon(Icons.Outlined.Add, contentDescription = "Add preset")
Icon(Icons.Outlined.Add, contentDescription = stringResource(R.string.moa_add_preset))
}
}
@@ -320,7 +327,7 @@ private fun MoaConfigEditor(
// Reference models
Text(
text = "Reference models",
text = stringResource(R.string.moa_reference_models),
style = MaterialTheme.typography.bodySmall,
fontWeight = FontWeight.Bold,
)
@@ -355,7 +362,7 @@ private fun MoaConfigEditor(
) {
Icon(Icons.Outlined.Add, contentDescription = null, modifier = Modifier.size(14.dp))
Spacer(modifier = Modifier.width(4.dp))
Text("Add reference model", style = MaterialTheme.typography.labelSmall)
Text(stringResource(R.string.moa_add_reference), style = MaterialTheme.typography.labelSmall)
}
Spacer(modifier = Modifier.height(8.dp))
@@ -412,7 +419,7 @@ private fun MoaSlotPicker(
SearchBar(
query = pickerQuery,
onQueryChange = { pickerQuery = it },
placeholder = "Search providers...",
placeholder = stringResource(R.string.moa_search_providers),
)
Spacer(modifier = Modifier.height(8.dp))
@@ -32,9 +32,11 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.m57.hermescontrol.R
import com.m57.hermescontrol.data.model.ModelProvider
import com.m57.hermescontrol.data.model.PinnedModel
import com.m57.hermescontrol.ui.common.LoadingState
@@ -95,12 +97,12 @@ fun ModelPickerDialog(
Column {
if (isLoading) {
LoadingState(
subtitle = "Loading models…",
subtitle = stringResource(R.string.model_picker_loading),
modifier = Modifier.fillMaxWidth(),
)
} else if (providers.isEmpty() && pinnedModels.isEmpty()) {
Text(
text = "No models available.",
text = stringResource(R.string.model_picker_no_models),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -143,7 +145,7 @@ fun ModelPickerDialog(
SearchBar(
query = pickerQuery,
onQueryChange = { pickerQuery = it },
placeholder = "Search models and providers...",
placeholder = stringResource(R.string.model_picker_search),
)
Spacer(modifier = Modifier.height(12.dp))