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

@@ -1182,7 +1182,7 @@ class E2eIntegrationTest {
Response.success(UpdateCheckResponse())
coEvery { mockApiService.triggerBackup(BackupTriggerRequest()) } returns Response.success(ActionResponse())
val viewModel = SystemViewModel()
val viewModel = SystemViewModel(mockApp)
viewModel.loadAll()
advanceUntilIdle()
@@ -103,7 +103,7 @@ class CookieManagerTest {
}
@Test
fun setSessionCookie_hostScoped_onlyMatchesThatHost() =
fun setSessionCookie_isUsableAcrossHosts() =
runTest {
val jar = fakeJar()
CookieManager.setSessionCookie("sess-xyz", ep("dashboard.local"))
@@ -115,8 +115,11 @@ class CookieManagerTest {
assertEquals(1, onHost.size)
assertEquals("sess-xyz", onHost[0].value)
// Session cookie mirrors to the WILDCARD bucket — usable on any
// host within the same server scope (WiFi/Tailscale switch-safe).
val offHost = jar.loadForRequest("http://other.local/".toHttpUrl())
assertEquals(0, offHost.size)
assertEquals(1, offHost.size)
assertEquals("sess-xyz", offHost[0].value)
}
@Test
@@ -1,15 +1,35 @@
package com.m57.hermescontrol.data.remote
import com.m57.hermescontrol.data.config.ServerStore
import com.m57.hermescontrol.data.config.ServerStoreState
import com.m57.hermescontrol.data.local.AuthManager
import io.mockk.every
import io.mockk.mockk
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
class GatewayFileClientTest {
private val base = "https://gw.example.com:9119"
private val tok = "s e/cret"
@Before
fun setUp() {
// AuthManager.isGatedMode() reads serverStore — inject a mock whose
// wsAuthParam is NOT "ticket" so the tests exercise the loopback
// branch (token stamped into the URL). Keeps this builder test
// independent of real AuthManager initialization.
val mockStore = mockk<ServerStore>(relaxed = true)
every { mockStore.getLatestState() } returns
ServerStoreState(wsAuthParam = "bearer")
val field = AuthManager::class.java.getDeclaredField("_serverStore")
field.isAccessible = true
field.set(AuthManager, mockStore)
}
@Test
fun `buildDownloadUrl encodes path and token`() {
val url = GatewayFileClient.buildDownloadUrl(base, tok, "/tmp/foo.png")!!
@@ -54,7 +54,7 @@ class PersistentCookieJarTest {
}
@Test
fun cookie_doesNotLeakToOtherHost() =
fun sessionCookie_isUsableAcrossHostsWithinServer() =
runTest {
val jar = makeJar()
jar.useStore("server-a")
@@ -63,8 +63,12 @@ class PersistentCookieJarTest {
listOf(sessionCookie("dashboard.local", "abc123")),
)
// Session cookies mirror to the WILDCARD bucket (host-agnostic)
// so the authenticated session survives WiFi/Tailscale host
// switches — but only within the same server scope.
val loaded = jar.loadForRequest("http://evil.local/".toHttpUrl())
assertTrue(loaded.isEmpty())
assertEquals(1, loaded.size)
assertEquals("abc123", loaded[0].value)
}
@Test
@@ -1,5 +1,7 @@
package com.m57.hermescontrol.ui.system
import android.app.Application
import io.mockk.mockk
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
@@ -14,6 +16,7 @@ import org.junit.Test
@OptIn(ExperimentalCoroutinesApi::class)
class SystemViewModelTest {
private val testDispatcher = StandardTestDispatcher()
private val mockApp = mockk<Application>(relaxed = true)
@Before
fun setup() {
@@ -27,7 +30,7 @@ class SystemViewModelTest {
@Test
fun `openUpdateConfirm sets updateConfirmOpen to true`() {
val viewModel = SystemViewModel()
val viewModel = SystemViewModel(mockApp)
assertFalse(viewModel.uiState.value.updateConfirmOpen)
@@ -38,7 +41,7 @@ class SystemViewModelTest {
@Test
fun `closeUpdateConfirm sets updateConfirmOpen to false`() {
val viewModel = SystemViewModel()
val viewModel = SystemViewModel(mockApp)
// First open it
viewModel.openUpdateConfirm()