refactor(apple): type the merged relay/network resources

Convert master's raw-string localization keys and SF Symbols in the new
relay/network code to typed accessors, matching this branch's typed-resources
convention: relay mode labels/descriptions, NetworkSettings strings, the endpoint
id and relay-validation messages (now typed L10n functions), and SF Symbols via
SFSafeSymbols. Retype the model's relayApplyErrorKey from a raw String key to
String.LocalizationValue so no loose key literals remain in the settings layer.
This commit is contained in:
2026-07-24 18:15:36 +02:00
parent 9b15a388d8
commit 3042005226
4 changed files with 43 additions and 52 deletions

View File

@@ -303,9 +303,9 @@ struct TransferSharePanel: View {
image.interpolation(.none).resizable().scaledToFit().padding(14) image.interpolation(.none).resizable().scaledToFit().padding(14)
} else { } else {
VStack(spacing: 10) { VStack(spacing: 10) {
Image(systemName: "qrcode") Image(systemSymbol: .qrcode)
.font(.system(size: 36, weight: .medium)) .font(.system(size: 36, weight: .medium))
Text(LocalizedStringKey("transfer_qr_unavailable")) Text(String(localized: L10n.Transfer.qrUnavailable))
.font(VniType.bodySmall) .font(VniType.bodySmall)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
} }

View File

@@ -52,7 +52,7 @@ struct SettingsState: Equatable {
var isApplyingRelayConfiguration = false var isApplyingRelayConfiguration = false
var hasActiveNetworkWork = false var hasActiveNetworkWork = false
var endpointId: String? var endpointId: String?
var relayApplyErrorKey: String? var relayApplyErrorKey: String.LocalizationValue?
var deviceInfo: DeviceInfo? var deviceInfo: DeviceInfo?
var appVersion = "" var appVersion = ""
var isLoadingDeviceInfo = false var isLoadingDeviceInfo = false
@@ -169,7 +169,7 @@ final class SettingsModel: ObservableObject {
|| coreState.transfers.contains(where: { $0.status.isActiveTransfer }) || coreState.transfers.contains(where: { $0.status.isActiveTransfer })
self.state.hasActiveNetworkWork = hasActiveWork self.state.hasActiveNetworkWork = hasActiveWork
self.state.endpointId = coreState.status?.endpointId self.state.endpointId = coreState.status?.endpointId
if !hasActiveWork && self.state.relayApplyErrorKey == "relay_apply_active_transfers" { if !hasActiveWork && self.state.relayApplyErrorKey == L10n.Relay.applyActiveTransfers {
self.state.relayApplyErrorKey = nil self.state.relayApplyErrorKey = nil
} }
} }
@@ -296,8 +296,8 @@ final class SettingsModel: ObservableObject {
|| coreState.transfers.contains(where: { $0.status.isActiveTransfer }) || coreState.transfers.contains(where: { $0.status.isActiveTransfer })
guard !hasActiveWork else { guard !hasActiveWork else {
state.hasActiveNetworkWork = true state.hasActiveNetworkWork = true
state.relayApplyErrorKey = "relay_apply_active_transfers" state.relayApplyErrorKey = L10n.Relay.applyActiveTransfers
messages.show(UiMessage(text: .resource("relay_apply_active_transfers"), tone: .warning)) messages.show(UiMessage(text: .resource(L10n.Relay.applyActiveTransfers), tone: .warning))
return return
} }
@@ -316,16 +316,16 @@ final class SettingsModel: ObservableObject {
preferences.setRelayConfiguration(configuration) preferences.setRelayConfiguration(configuration)
state.isApplyingRelayConfiguration = false state.isApplyingRelayConfiguration = false
state.relayConfigurationIsDirty = false state.relayConfigurationIsDirty = false
messages.show(UiMessage(text: .resource("relay_settings_applied"), tone: .success)) messages.show(UiMessage(text: .resource(L10n.Relay.settingsApplied), tone: .success))
case .failure(let error): case .failure(let error):
if let lifecycleError = error as? CoreNetworkLifecycleError { if let lifecycleError = error as? CoreNetworkLifecycleError {
state.isApplyingRelayConfiguration = false state.isApplyingRelayConfiguration = false
switch lifecycleError { switch lifecycleError {
case .activeNetworkWork: case .activeNetworkWork:
state.hasActiveNetworkWork = true state.hasActiveNetworkWork = true
state.relayApplyErrorKey = "relay_apply_active_transfers" state.relayApplyErrorKey = L10n.Relay.applyActiveTransfers
case .transitionInProgress: case .transitionInProgress:
state.relayApplyErrorKey = "relay_apply_failed" state.relayApplyErrorKey = L10n.Relay.applyFailed
} }
return return
} }
@@ -335,11 +335,11 @@ final class SettingsModel: ObservableObject {
) )
state.isApplyingRelayConfiguration = false state.isApplyingRelayConfiguration = false
if case .success = rollbackResult { if case .success = rollbackResult {
state.relayApplyErrorKey = "relay_apply_failed" state.relayApplyErrorKey = L10n.Relay.applyFailed
messages.show(UiMessage(text: .resource("relay_apply_failed"), tone: .error)) messages.show(UiMessage(text: .resource(L10n.Relay.applyFailed), tone: .error))
} else { } else {
state.relayApplyErrorKey = "relay_restore_failed" state.relayApplyErrorKey = L10n.Relay.restoreFailed
messages.show(UiMessage(text: .resource("relay_restore_failed"), tone: .error)) messages.show(UiMessage(text: .resource(L10n.Relay.restoreFailed), tone: .error))
} }
} }
} }

View File

@@ -125,19 +125,19 @@ private struct SettingsSectionContent: View {
func relayModeLabel(_ mode: RelayPreferenceMode) -> String { func relayModeLabel(_ mode: RelayPreferenceMode) -> String {
switch mode { switch mode {
case .automatic: return String(localized: "relay_mode_automatic") case .automatic: return String(localized: L10n.Relay.modeAutomatic)
case .strictCustom: return String(localized: "relay_mode_custom") case .strictCustom: return String(localized: L10n.Relay.modeCustom)
case .customWithDirectFallback: return String(localized: "relay_mode_custom_direct_fallback") case .customWithDirectFallback: return String(localized: L10n.Relay.modeCustomDirectFallback)
case .localOnly: return String(localized: "relay_mode_local_only") case .localOnly: return String(localized: L10n.Relay.modeLocalOnly)
} }
} }
func relayModeDescriptionKey(_ mode: RelayPreferenceMode) -> String { func relayModeDescription(_ mode: RelayPreferenceMode) -> String.LocalizationValue {
switch mode { switch mode {
case .automatic: return "relay_mode_automatic_description" case .automatic: return L10n.Relay.modeAutomaticDescription
case .strictCustom: return "relay_mode_custom_description" case .strictCustom: return L10n.Relay.modeCustomDescription
case .customWithDirectFallback: return "relay_mode_custom_direct_fallback_description" case .customWithDirectFallback: return L10n.Relay.modeCustomDirectFallbackDescription
case .localOnly: return "relay_mode_local_only_description" case .localOnly: return L10n.Relay.modeLocalOnlyDescription
} }
} }

View File

@@ -93,22 +93,22 @@ struct NetworkSettings: View {
} header: { } header: {
Text(String(localized: L10n.Settings.networkTitle)) Text(String(localized: L10n.Settings.networkTitle))
} footer: { } footer: {
Text(LocalizedStringKey(relayModeDescriptionKey(model.state.relayMode))) Text(String(localized: relayModeDescription(model.state.relayMode)))
} }
Section { Section {
Label { Label {
Text(LocalizedStringKey("relay_privacy_description")) Text(String(localized: L10n.Relay.privacyDescription))
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
} icon: { } icon: {
Image(systemName: "lock.shield") Image(systemSymbol: .lockShield)
} }
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
if let endpointId = model.state.endpointId, !endpointId.isEmpty { if let endpointId = model.state.endpointId, !endpointId.isEmpty {
Section { Section {
Text(String(format: String(localized: "approval_endpoint_id"), endpointId)) Text(L10n.Approval.endpointId(deviceId: endpointId))
.font(.footnote.monospaced()) .font(.footnote.monospaced())
.textSelection(.enabled) .textSelection(.enabled)
} }
@@ -118,10 +118,10 @@ struct NetworkSettings: View {
Section { Section {
if model.state.relayMode == .strictCustom { if model.state.relayMode == .strictCustom {
Label { Label {
Text(LocalizedStringKey("relay_strict_warning")) Text(String(localized: L10n.Relay.strictWarning))
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
} icon: { } icon: {
Image(systemName: "exclamationmark.shield.fill") Image(systemSymbol: .exclamationmarkShieldFill)
} }
.foregroundStyle(.orange) .foregroundStyle(.orange)
} }
@@ -154,10 +154,10 @@ struct NetworkSettings: View {
Button(role: .destructive) { Button(role: .destructive) {
model.removeRelayURL(at: index) model.removeRelayURL(at: index)
} label: { } label: {
Image(systemName: "minus.circle.fill") Image(systemSymbol: .minusCircleFill)
} }
.buttonStyle(.borderless) .buttonStyle(.borderless)
.accessibilityLabel(Text(LocalizedStringKey("relay_remove_url"))) .accessibilityLabel(Text(String(localized: L10n.Relay.removeUrl)))
.disabled(model.state.isApplyingRelayConfiguration) .disabled(model.state.isApplyingRelayConfiguration)
} }
@@ -170,16 +170,16 @@ struct NetworkSettings: View {
} }
Button(action: model.addRelayURL) { Button(action: model.addRelayURL) {
Label(String(localized: "relay_add_url"), systemImage: "plus.circle") Label(String(localized: L10n.Relay.addUrl), systemSymbol: .plusCircle)
} }
.disabled( .disabled(
model.state.relayURLs.count >= RelayConfigurationValidator.maximumRelayCount model.state.relayURLs.count >= RelayConfigurationValidator.maximumRelayCount
|| model.state.isApplyingRelayConfiguration || model.state.isApplyingRelayConfiguration
) )
} header: { } header: {
Text(LocalizedStringKey("relay_custom_urls_label")) Text(String(localized: L10n.Relay.customUrlsLabel))
} footer: { } footer: {
Text(LocalizedStringKey("relay_custom_urls_help")) Text(String(localized: L10n.Relay.customUrlsHelp))
} }
} }
@@ -188,7 +188,7 @@ struct NetworkSettings: View {
Label { Label {
Text(relayValidationMessage(error)) Text(relayValidationMessage(error))
} icon: { } icon: {
Image(systemName: "exclamationmark.triangle.fill") Image(systemSymbol: .exclamationmarkTriangleFill)
} }
.foregroundStyle(.red) .foregroundStyle(.red)
} }
@@ -197,13 +197,9 @@ struct NetworkSettings: View {
if model.state.hasActiveNetworkWork || model.state.relayApplyErrorKey != nil { if model.state.hasActiveNetworkWork || model.state.relayApplyErrorKey != nil {
Section { Section {
Label { Label {
Text(LocalizedStringKey( Text(String(localized: model.state.hasActiveNetworkWork ? L10n.Relay.applyActiveTransfers : (model.state.relayApplyErrorKey ?? L10n.Relay.applyFailed)))
model.state.hasActiveNetworkWork
? "relay_apply_active_transfers"
: model.state.relayApplyErrorKey ?? "relay_apply_failed"
))
} icon: { } icon: {
Image(systemName: "exclamationmark.triangle.fill") Image(systemSymbol: .exclamationmarkTriangleFill)
} }
.foregroundStyle(.red) .foregroundStyle(.red)
} }
@@ -212,9 +208,7 @@ struct NetworkSettings: View {
Section { Section {
Button(action: model.applyRelayConfiguration) { Button(action: model.applyRelayConfiguration) {
HStack { HStack {
Text(LocalizedStringKey( Text(String(localized: model.state.isApplyingRelayConfiguration ? L10n.Relay.applying : L10n.Relay.apply))
model.state.isApplyingRelayConfiguration ? "relay_applying" : "relay_apply"
))
if model.state.isApplyingRelayConfiguration { if model.state.isApplyingRelayConfiguration {
Spacer() Spacer()
ProgressView() ProgressView()
@@ -227,7 +221,7 @@ struct NetworkSettings: View {
|| model.state.hasActiveNetworkWork || model.state.hasActiveNetworkWork
) )
} footer: { } footer: {
Text(LocalizedStringKey("relay_apply_restart_description")) Text(String(localized: L10n.Relay.applyRestartDescription))
} }
} }
} }
@@ -235,18 +229,15 @@ struct NetworkSettings: View {
private func relayValidationMessage(_ error: RelayConfigurationValidationError) -> String { private func relayValidationMessage(_ error: RelayConfigurationValidationError) -> String {
switch error { switch error {
case .missingURL: case .missingURL:
return String(localized: "relay_validation_missing_url") return String(localized: L10n.Relay.validationMissingUrl)
case .tooManyURLs: case .tooManyURLs:
return String( return L10n.Relay.validationTooManyUrls(maximum: RelayConfigurationValidator.maximumRelayCount)
format: String(localized: "relay_validation_too_many_urls"),
RelayConfigurationValidator.maximumRelayCount
)
case .httpsRequired(let index): case .httpsRequired(let index):
return String(format: String(localized: "relay_validation_https_required"), index + 1) return L10n.Relay.validationHttpsRequired(line: index + 1)
case .invalidURL(let index): case .invalidURL(let index):
return String(format: String(localized: "relay_validation_invalid_url"), index + 1) return L10n.Relay.validationInvalidUrl(line: index + 1)
case .duplicateURL(let index): case .duplicateURL(let index):
return String(format: String(localized: "relay_validation_duplicate_url"), index + 1) return L10n.Relay.validationDuplicateUrl(line: index + 1)
} }
} }