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

@@ -1,5 +1,6 @@
package com.m57.hermescontrol.ui.system
import android.app.Application
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
@@ -125,8 +126,10 @@ private fun formatDuration(totalSeconds: Double): String {
fun SystemScreen(
modifier: Modifier = Modifier,
onOpenDrawer: (() -> Unit)? = null,
viewModel: SystemViewModel = viewModel { SystemViewModel() },
viewModel: SystemViewModel? = null,
) {
val app = LocalContext.current.applicationContext as Application
val viewModel = viewModel ?: viewModel { SystemViewModel(app) }
val state by viewModel.uiState.collectAsStateWithLifecycle()
val spacing = LocalSpacing.current
val statusColors = LocalHermesStatusColors.current
@@ -854,7 +857,7 @@ private fun LazyListScope.gatewaySection(
// Auth required
status.auth_required?.let { auth ->
InfoRow(
label = "Auth required",
label = stringResource(R.string.system_auth_required),
value = if (auth) "yes" else "no",
)
}
@@ -1670,7 +1673,7 @@ private fun LazyListScope.actionLogSection(
}
if (lines.size > 20) {
Text(
text = "... ${lines.size - 20} more line(s)",
text = stringResource(R.string.system_more_lines, lines.size - 20),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -1,9 +1,12 @@
package com.m57.hermescontrol.ui.system
import android.app.Application
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.annotation.StringRes
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.m57.hermescontrol.BuildConfig
import com.m57.hermescontrol.R
import com.m57.hermescontrol.data.model.ActionResponse
import com.m57.hermescontrol.data.model.ActionStatusResponse
import com.m57.hermescontrol.data.model.BackupTriggerRequest
@@ -83,8 +86,8 @@ data class SystemUiState(
val updateConfirmOpen: Boolean = false,
)
class SystemViewModel :
ViewModel(),
class SystemViewModel(application: Application) :
AndroidViewModel(application),
ToastHost {
companion object {
private const val TAG = "SystemViewModel"
@@ -348,7 +351,7 @@ class SystemViewModel :
fun runCuratorNow() {
runOperation(
apiCall = { safeApiCall { ApiClient.hermesApi.runCurator() } },
label = "Curator run",
labelRes = R.string.system_curator_run,
)
}
@@ -433,35 +436,35 @@ class SystemViewModel :
fun runSecurityAudit() {
runOperation(
apiCall = { safeApiCall { ApiClient.hermesApi.runSecurityAudit() } },
label = "Security audit",
labelRes = R.string.system_security_audit,
)
}
fun runPromptSize() {
runOperation(
apiCall = { safeApiCall { ApiClient.hermesApi.runPromptSize() } },
label = "Prompt size check",
labelRes = R.string.system_prompt_size_check,
)
}
fun runDump() {
runOperation(
apiCall = { safeApiCall { ApiClient.hermesApi.runDump() } },
label = "Dump",
labelRes = R.string.system_dump,
)
}
fun runConfigMigrate() {
runOperation(
apiCall = { safeApiCall { ApiClient.hermesApi.runConfigMigrate() } },
label = "Config migrate",
labelRes = R.string.system_config_migrate,
)
}
fun runUpdateSkills() {
runOperation(
apiCall = { safeApiCall { ApiClient.hermesApi.updateSkillsFromHub() } },
label = "Skills update",
labelRes = R.string.system_skills_update,
)
}
@@ -654,7 +657,7 @@ class SystemViewModel :
fun pruneCheckpoints() {
runOperation(
apiCall = { safeApiCall { ApiClient.hermesApi.pruneCheckpoints() } },
label = "Checkpoint prune",
labelRes = R.string.system_checkpoint_prune,
)
}
@@ -786,18 +789,35 @@ class SystemViewModel :
private fun runOperation(
apiCall: suspend () -> NetworkResult<ActionResponse>,
label: String,
@StringRes labelRes: Int,
) {
viewModelScope.launch {
val result = withContext(Dispatchers.IO) { apiCall() }
when (result) {
is NetworkResult.Success -> {
result.data.name?.let { pollActionStatus(it) }
_uiState.update { it.copy(toastMessage = "$label started") }
_uiState.update {
it.copy(
toastMessage =
getApplication<Application>().getString(
R.string.system_action_started,
getApplication<Application>().getString(labelRes),
),
)
}
}
is NetworkResult.Failure -> {
_uiState.update { it.copy(toastMessage = "$label failed: ${result.error.message}") }
_uiState.update {
it.copy(
toastMessage =
getApplication<Application>().getString(
R.string.system_action_failed,
getApplication<Application>().getString(labelRes),
result.error.message,
),
)
}
}
}
}