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

@@ -183,7 +183,7 @@ fun ChannelsScreen(
SearchBar(
query = query,
onQueryChange = { query = it },
placeholder = "Search channels…",
placeholder = stringResource(R.string.channels_search_placeholder),
)
}
@@ -350,7 +350,7 @@ internal fun ConfigureForm(
}
field.expiresAt?.let { expiry ->
Text(
text = "Expires: ${formatTimestamp(expiry)}",
text = stringResource(R.string.channels_expiry_format, formatTimestamp(expiry)),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.labelSmall,
)
@@ -438,7 +438,7 @@ private fun AdminSection(envPath: String) {
)
Spacer(modifier = Modifier.width(6.dp))
Text(
text = "Credentials are written to $envPath",
text = stringResource(R.string.channels_creds_path_format, envPath),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -226,7 +226,7 @@ internal fun PlatformCard(
} else {
Icon(
imageVector = Icons.Filled.Delete,
contentDescription = "Remove platform",
contentDescription = stringResource(R.string.platform_remove_desc),
tint = MaterialTheme.colorScheme.error,
)
}
@@ -246,7 +246,7 @@ internal fun PlatformCard(
)
Spacer(modifier = Modifier.width(6.dp))
Text(
text = "Set up with QR",
text = stringResource(R.string.platform_setup_qr),
style = MaterialTheme.typography.labelSmall,
)
}
@@ -267,8 +267,8 @@ internal fun PlatformCard(
if (showRemoveConfirm) {
AlertDialog(
onDismissRequest = { showRemoveConfirm = false },
title = { Text("Remove ${platform.name}?") },
text = { Text("This will permanently remove this platform's configuration.") },
title = { Text(stringResource(R.string.platform_remove_confirm_title, platform.name)) },
text = { Text(stringResource(R.string.platform_remove_confirm_text)) },
confirmButton = {
TextButton(
onClick = {
@@ -28,7 +28,9 @@ import androidx.compose.runtime.getValue
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.unit.dp
import com.m57.hermescontrol.R
import com.m57.hermescontrol.theme.LocalHermesStatusColors
import com.m57.hermescontrol.ui.channels.ChannelsUiState
import com.m57.hermescontrol.ui.channels.OnboardingPhase
@@ -87,12 +89,12 @@ internal fun TelegramOnboardingDialog(
OnboardingPhase.WAITING -> {
Text(
text = "Open Telegram and scan the QR code displayed in the app.",
text = stringResource(R.string.tg_onboarding_scan_hint),
style = MaterialTheme.typography.bodyMedium,
)
if (state.onboardingExpiresIn.isNotEmpty()) {
Text(
text = "QR code expires in: ${state.onboardingExpiresIn}",
text = stringResource(R.string.tg_onboarding_qr_expires, state.onboardingExpiresIn),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -102,13 +104,13 @@ internal fun TelegramOnboardingDialog(
OnboardingPhase.READY -> {
state.onboardingBotUsername?.let { username ->
Text(
text = "Connected as @$username",
text = stringResource(R.string.tg_onboarding_connected_as, username),
style = MaterialTheme.typography.bodyMedium,
)
}
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Allowed users:",
text = stringResource(R.string.tg_onboarding_allowed_users),
style = MaterialTheme.typography.titleSmall,
)
state.onboardingDetectedOwnerId?.let { ownerId ->
@@ -146,12 +148,15 @@ internal fun TelegramOnboardingDialog(
OutlinedTextField(
value = state.onboardingNewAllowedId,
onValueChange = onNewAllowedIdChange,
placeholder = { Text("Telegram user ID") },
placeholder = { Text(stringResource(R.string.tg_onboarding_user_id_placeholder)) },
singleLine = true,
modifier = Modifier.weight(1f),
)
IconButton(onClick = onAddAllowedId) {
Icon(Icons.Filled.Add, contentDescription = "Add user ID")
Icon(
Icons.Filled.Add,
contentDescription = stringResource(R.string.tg_onboarding_add_user_id),
)
}
}
}
@@ -165,7 +170,7 @@ internal fun TelegramOnboardingDialog(
CircularProgressIndicator()
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Saving configuration…",
text = stringResource(R.string.tg_onboarding_saving),
style = MaterialTheme.typography.bodyMedium,
)
}
@@ -183,7 +188,7 @@ internal fun TelegramOnboardingDialog(
onClick = onApply,
enabled = state.onboardingAllowedIds.isNotEmpty(),
) {
Text("Save and restart")
Text(stringResource(R.string.tg_onboarding_save_restart))
}
}