Fix localization

This commit is contained in:
Kaede Akino committed 2026-09-09 17:02:03 +08:00
1 parent 58d2842a9e
commit d2f3a80d8d
8 files changed
+290 -19

No files matched your search

@@ -47,7 +47,7 @@ nonisolated struct PortForwardingRule: Identifiable, Codable, Hashable, Sendable
case .local:
"\(bindAddress) → \(Self.address(targetHost ?? "", port: targetPort ?? 0))"
case .remote:
"SSH \(bindAddress) → This Mac \(Self.address(targetHost ?? "", port: targetPort ?? 0))"
String(localized: "SSH \(bindAddress) → This Mac \(Self.address(targetHost ?? "", port: targetPort ?? 0))")
case .dynamic: "SOCKS5 \(bindAddress)"
}
}
@@ -405,7 +405,7 @@ private struct SoftwareKeyEditor: View {
}
let existing: SSHKeyRef?
let title: String
let title: LocalizedStringKey
let symbol: String
let close: () -> Void
let loadPrivateKey: (() throws -> String)?
@@ -433,7 +433,7 @@ private struct SoftwareKeyEditor: View {
@Environment(\.accessibilityReduceMotion) private var reduceMotion
init(
existing: SSHKeyRef? = nil, title: String,
existing: SSHKeyRef? = nil, title: LocalizedStringKey,
symbol: String, close: @escaping () -> Void,
loadPrivateKey: (() throws -> String)? = nil,
save:
@@ -91,7 +91,7 @@ struct NewIdentitySheet: View {
var body: some View {
VaultDetailScaffold(
title: existing == nil ? configuration.newTitle : configuration.editTitle,
title: LocalizedStringKey(existing == nil ? configuration.newTitle : configuration.editTitle),
symbol: "person.badge.key.fill", close: close,
closeLabel: requestedClose == nil ? "Back" : "Close",
closeSymbol: requestedClose == nil ? "chevron.backward" : "xmark"
@@ -161,7 +161,7 @@ private struct HostForwardingContextMenu: View {
var body: some View {
let isRunning = app.forwarding.isEnabled(rule.id)
Button(
isRunning ? "Stop" : "Start",
isRunning ? String(localized: "Stop") : String(localized: "Start"),
systemImage: isRunning ? MenuSymbol.stop : MenuSymbol.start,
action: toggle
)
@@ -302,19 +302,20 @@ struct PortForwardingEditorSheet: View {
}
private var bindPortLabel: String {
draft.kind == .dynamic ? "SOCKS Listen Port" : "Listen Port"
draft.kind == .dynamic ? String(localized: "SOCKS Listen Port") : String(localized: "Listen Port")
}
private var bindAddressLabel: String {
switch draft.kind {
case .local: "Listen Address on This Mac"
case .remote: "Listen Address on SSH Host"
case .dynamic: "SOCKS Listen Address on This Mac"
case .local: String(localized: "Listen Address on This Mac")
case .remote: String(localized: "Listen Address on SSH Host")
case .dynamic: String(localized: "SOCKS Listen Address on This Mac")
}
}
private var targetHostLabel: String {
draft.kind == .remote ? "Destination Host on This Mac" : "Destination Host from SSH Host"
draft.kind == .remote
? String(localized: "Destination Host on This Mac") : String(localized: "Destination Host from SSH Host")
}
private var exposesNetwork: Bool {
@@ -346,8 +347,8 @@ struct PortForwardingEditorSheet: View {
switch draft.kind {
case .local: return "\(start) → \(ssh) → \(targetHost):\(targetPort)"
case .remote:
return "\(ssh) → listen \(start) → This Mac → \(targetHost):\(targetPort)"
case .dynamic: return "SOCKS5 \(start) → \(ssh) → Requested Target"
return String(localized: "\(ssh) → listen \(start) → This Mac → \(targetHost):\(targetPort)")
case .dynamic: return String(localized: "SOCKS5 \(start) → \(ssh) → Requested Target")
}
}
@@ -99,12 +99,12 @@ private nonisolated final class SSHForwardingWorker: @unchecked Sendable {
guard let routeTransport else { throw TermindSSHError.disconnected }
socket = routeTransport.socket
guard let created = libssh2_session_init_ex(nil, nil, nil, nil) else {
throw TermindSSHError.library("Unable to create an SSH forwarding session.")
throw TermindSSHError.library(String(localized: "Unable to create an SSH forwarding session."))
}
session = created
libssh2_session_set_blocking(created, 1)
guard libssh2_session_handshake(created, socket) == 0 else {
throw SSHSessionWorker.sessionError(created, prefix: "SSH handshake failed")
throw SSHSessionWorker.sessionError(created, prefix: String(localized: "SSH handshake failed"))
}
let observed = try SSHSessionWorker.observation(
host: configuration.host, port: configuration.port, session: created
@@ -146,7 +146,8 @@ private nonisolated final class SSHForwardingWorker: @unchecked Sendable {
)
} catch {
throw TermindSSHError.route(
"Unable to listen on \(rule.bindAddress): \(error.localizedDescription)"
String(
localized: "Unable to listen on \(rule.bindAddress): \(error.localizedDescription)")
)
}
case .remote:
@@ -159,7 +160,7 @@ private nonisolated final class SSHForwardingWorker: @unchecked Sendable {
guard let listener else {
throw TermindSSHError.route(
SSHSessionWorker.sessionError(
session, prefix: "Unable to start remote forwarding"
session, prefix: String(localized: "Unable to start remote forwarding")
).localizedDescription
)
}
@@ -214,7 +215,7 @@ private nonisolated final class SSHForwardingWorker: @unchecked Sendable {
let keepalive = libssh2_keepalive_send(session, &next)
if keepalive < 0, keepalive != LIBSSH2_ERROR_EAGAIN {
let message = SSHSessionWorker.sessionError(
session, prefix: "The forwarding SSH connection closed"
session, prefix: String(localized: "The forwarding SSH connection closed")
).localizedDescription
cleanup()
failureHandler(message)
+220
View File
@@ -223,6 +223,16 @@
}
}
},
"%@ → listen %@ → This Mac → %@:%@" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "%1$@→监听%2$@→这台Mac→%3$@:%4$@"
}
}
}
},
"%@%@ of %@ (%lld%%)" : {
"localizations" : {
"en" : {
@@ -3634,6 +3644,26 @@
}
}
},
"Destination Host from SSH Host" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "SSH主机可访问的目标主机"
}
}
}
},
"Destination Host on This Mac" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "这台Mac可访问的目标主机"
}
}
}
},
"Destination Port" : {
"localizations" : {
"zh-Hans" : {
@@ -4006,6 +4036,26 @@
}
}
},
"Edit Group" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "编辑组"
}
}
}
},
"Edit Host" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "编辑主机"
}
}
}
},
"Edit Identity" : {
"localizations" : {
"zh-Hans" : {
@@ -4016,6 +4066,26 @@
}
}
},
"Edit Key" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "编辑密钥"
}
}
}
},
"Edit Package" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "编辑包"
}
}
}
},
"Edit Path" : {
"comment" : "A button that opens a dialog to edit the current path.",
"isCommentAutoGenerated" : true,
@@ -4048,6 +4118,26 @@
}
}
},
"Edit Port Forward" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "编辑端口转发"
}
}
}
},
"Edit Snippet" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "编辑片段"
}
}
}
},
"Effort" : {
"localizations" : {
"zh-Hans" : {
@@ -5940,6 +6030,36 @@
}
}
},
"Listen Address on SSH Host" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "SSH主机上的监听地址"
}
}
}
},
"Listen Address on This Mac" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "这台Mac上的监听地址"
}
}
}
},
"Listen Port" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "监听端口"
}
}
}
},
"Listening on localhost:%@" : {
"localizations" : {
"zh-Hans" : {
@@ -6452,6 +6572,16 @@
}
}
},
"New Port Forward" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "新建端口转发"
}
}
}
},
"New Provider" : {
"localizations" : {
"zh-Hans" : {
@@ -9152,6 +9282,16 @@
}
}
},
"SSH handshake failed" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "SSH握手失败"
}
}
}
},
"Save" : {
"localizations" : {
"zh-Hans" : {
@@ -10068,6 +10208,26 @@
}
}
},
"SOCKS Listen Address on This Mac" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "这台Mac上的SOCKS监听地址"
}
}
}
},
"SOCKS Listen Port" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "SOCKS监听端口"
}
}
}
},
"SOCKS5" : {
"localizations" : {
"zh-Hans" : {
@@ -10078,6 +10238,16 @@
}
}
},
"SOCKS5 %@ → %@ → Requested Target" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "SOCKS5 %1$@→%2$@→请求目标"
}
}
}
},
"SOCKS5 on This Mac → SSH Host → Requested Target" : {
"localizations" : {
"zh-Hans" : {
@@ -10160,6 +10330,16 @@
}
}
},
"SSH %@ → This Mac %@" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "SSH %1$@→这台Mac %2$@"
}
}
}
},
"SSH Command" : {
"localizations" : {
"zh-Hans" : {
@@ -11022,6 +11202,16 @@
}
}
},
"The forwarding SSH connection closed" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "用于转发的SSH连接已关闭"
}
}
}
},
"The forwarding host is unavailable." : {
"localizations" : {
"zh-Hans" : {
@@ -12238,6 +12428,16 @@
}
}
},
"Unable to create an SSH forwarding session." : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "无法创建SSH转发会话。"
}
}
}
},
"Unable to generate a secure token." : {
"localizations" : {
"zh-Hans" : {
@@ -12248,6 +12448,26 @@
}
}
},
"Unable to listen on %@: %@" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "无法监听%1$@:%2$@"
}
}
}
},
"Unable to start remote forwarding" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "无法启动远程转发"
}
}
}
},
"Unable to store the token." : {
"localizations" : {
"zh-Hans" : {
+2 -2
View File
@@ -119,10 +119,10 @@ struct SheetScaffold<Header: View, Content: View, Footer: View>: View {
/// resembles the manager toolbars rather than a floating sheet: a clear close
/// affordance, a quiet title, scrollable work, and actions anchored at the foot.
struct VaultDetailScaffold<Content: View, Footer: View>: View {
let title: String
let title: LocalizedStringKey
let symbol: String
let close: () -> Void
var closeLabel = "Close"
var closeLabel: LocalizedStringKey = "Close"
var closeSymbol = "xmark"
@ViewBuilder let content: Content
@ViewBuilder let footer: Footer
+49
View File
@@ -66,6 +66,55 @@ struct AppLanguageTests {
}
}
@Test
func editorLabelsAndForwardingMessagesHaveChineseTranslations() throws {
let repository = URL(fileURLWithPath: #filePath)
.deletingLastPathComponent().deletingLastPathComponent()
let url = repository.appending(path: "Termind/Resources/Localizable.xcstrings")
let root = try #require(
JSONSerialization.jsonObject(with: Data(contentsOf: url)) as? [String: Any]
)
let strings = try #require(root["strings"] as? [String: Any])
let labels = [
"New Host", "Edit Host", "Import Key", "Generate Key", "Edit Key", "Export to Hosts",
"New Group", "Edit Group", "New Snippet", "Edit Snippet", "New Package", "Edit Package",
"New Identity", "Edit Identity", "Add Hosts", "Close",
"New Port Forward", "Edit Port Forward", "Back", "Start", "Stop",
"SOCKS Listen Port", "Listen Port", "Listen Address on This Mac",
"Listen Address on SSH Host", "SOCKS Listen Address on This Mac",
"Destination Host on This Mac", "Destination Host from SSH Host",
"Unable to create an SSH forwarding session.", "SSH handshake failed",
"Unable to start remote forwarding", "The forwarding SSH connection closed",
]
let interpolations: [String: [String]] = [
"%@ → listen %@ → This Mac → %@:%@": ["jump.example", "[::1]:8080", "db.internal", "5432"],
"SOCKS5 %@ → %@ → Requested Target": ["[::1]:1080", "jump.example"],
"SSH %@ → This Mac %@": ["[::1]:8080", "[2001:db8::1]:443"],
"Unable to listen on %@: %@": ["[::1]:8080", "Address already in use"],
]
for key in labels + interpolations.keys.sorted() {
let entry = try #require(strings[key] as? [String: Any], "Missing editor or forwarding string: \(key)")
let localizations = try #require(entry["localizations"] as? [String: Any])
let chinese = try #require(localizations["zh-Hans"] as? [String: Any])
let unit = try #require(chinese["stringUnit"] as? [String: Any])
let translation = try #require(unit["value"] as? String)
#expect(unit["state"] as? String == "translated")
#expect(translation.range(of: "\\p{Han}", options: .regularExpression) != nil)
if let arguments = interpolations[key] {
let placeholders = translation.matches(of: /%(?:[1-9][0-9]*\$)?@/)
#expect(placeholders.count == arguments.count, "Placeholder mismatch: \(key)")
let rendered = String(format: translation, arguments: arguments)
var remaining = rendered[...]
for argument in arguments {
let range = try #require(remaining.range(of: argument), "Lost or reordered value: \(key)")
remaining = remaining[range.upperBound...]
}
}
}
}
@Test
func countedToolResourcesUseCatalogPluralRules() throws {
let repository = URL(fileURLWithPath: #filePath)