Files
Syntax-Error-1337 908ac8f734 Add English localization (device-language based)
Add English as an overlay localization following the device language,
keeping Simplified Chinese as the base/development language. Untranslated
strings fall back to Chinese automatically.

- project.yml: set developmentLanguage zh-Hans and knownRegions (zh-Hans/en/Base)
- Resources/en.lproj/{Localizable,InfoPlist}.strings: ~230 English strings
- Resources/zh-Hans.lproj/InfoPlist.strings: Chinese permission prompts
- Restructure ternary Text/Label and String-typed UI messages so they
  localize (String(localized:), LocalizedStringKey helper params); replace
  a .contains("通过") success check with a dedicated flag
- Localize user-facing model/coordinator/error messages across App and Shared

Internal RuntimeLogger logs and diagnostic/report bodies remain in Chinese.
2026-09-10 01:05:45 +08:00

215 lines
9.0 KiB
Swift

import SwiftUI
import UIKit
struct ContentView: View {
@Environment(\.scenePhase) private var scenePhase
@StateObject private var setup = SetupCoordinator()
@ObservedObject private var runtimeMode = ProxyRuntimeModeStore.shared
@ObservedObject private var remoteConfiguration = AppRemoteConfigurationStore.shared
@State private var phase: AppPhase = .splash
@State private var updatePrompt: AppUpdatePrompt?
@State private var requiredUpdatePrompt: AppUpdatePrompt?
enum AppPhase { case splash, setup, map }
var body: some View {
Group {
switch phase {
case .splash:
VStack(spacing: 16) {
Image(systemName: "location.fill")
.font(.system(size: 48)).foregroundStyle(.blue)
ProgressView()
(runtimeMode.hasSelectedMode && runtimeMode.mode == .localWiFi
? Text("正在初始化地图与本地代理…")
: Text("正在初始化地图…"))
.font(.subheadline).foregroundStyle(.secondary)
}
case .setup:
FirstSetupView(setup: setup, onComplete: finishInitialSetup)
case .map:
NavigationView {
MapHomeView(setup: setup)
}
.fullScreenCover(isPresented: $setup.needsSetup) {
FirstSetupView(setup: setup, onComplete: finishPresentedSetup)
}
}
}
.task { await bootstrap() }
.task { await checkForUpdates() }
.onChange(of: scenePhase) { newPhase in
guard newPhase == .active, let requiredUpdatePrompt else { return }
updatePrompt = requiredUpdatePrompt
}
.alert(item: $updatePrompt) { prompt in
updateAlert(for: prompt)
}
}
@MainActor
private func bootstrap() async {
guard runtimeMode.hasSelectedMode else {
ProxyManager.shared.stop()
RuntimeLogger.info("APP", "Startup", "尚未选择运行模式,跳过本地 CA 和代理初始化")
setup.requestModeSelection()
phase = .setup
return
}
let launchMode = runtimeMode.mode
guard runtimeMode.isInitialized(launchMode) else {
if launchMode == .localWiFi {
await setup.prepareLocalServices()
setup.requestSetup()
} else {
ProxyManager.shared.stop()
BackgroundKeepAlive.shared.stop()
setup.requestThirdPartyOnboarding()
}
phase = .setup
return
}
if launchMode == .localWiFi {
await setup.prepareLocalServices()
} else {
ProxyManager.shared.stop()
BackgroundKeepAlive.shared.stop()
RuntimeLogger.info("APP", "Startup", "第三方代理测试模式:跳过本地 CA、代理和环境检测")
}
do {
try CoordinateStorageMigration.migrateIfNeeded(favorites: FavoriteLocationStore())
} catch {
RuntimeLogger.error("APP", "Startup", "旧坐标数据迁移失败,将在下次启动重试", error: error)
}
// MapHomeView is intentionally constructed only after this required
// coordinate-system gate resolves, so cached pins are never replayed
// into an unknown Apple Maps coordinate system.
let mapCoordinateSystem = await CoordinateConverter.resolveInitialMapCoordinateSystem()
guard !Task.isCancelled else { return }
RuntimeLogger.info("APP", "Startup", "地图坐标标准初始化完成,开始后续启动流程", details: [
"地图标准": mapCoordinateSystem.rawValue,
"使用兜底": String(CoordinateConverter.initialMapCoordinateSystemUsedFallback)
])
// Resolve the first map center before constructing MapHomeView. This
// prevents a Shenzhen/cache frame followed by a second realtime frame.
if LastCoordinateStore.load() == nil {
RuntimeLogger.info("APP", "Startup", "没有持久化图钉,地图创建前请求实时定位")
if let realtime = await RealtimeLocationManager.shared.requestLocation() {
let mapCoordinateSystemChange = CoordinateConverter.correctMapCoordinateSystemUsingRealtime(realtime)
let pair = CoordinateConverter.coordinatePair(
lat: realtime.latitude,
lon: realtime.longitude,
mapCoordinateSystem: .wgs84
)
LastCoordinateStore.save(coordinatePair: pair, zoomMeters: 1_000)
RuntimeLogger.info("APP", "Startup", "已使用实时定位准备唯一初始地图状态", details: [
"地图标准": CoordinateConverter.currentMapCoordinateSystem.rawValue,
"修正兜底标准": String(mapCoordinateSystemChange != nil),
"缩放米": "1000"
])
RealtimeLocationTrace.coordinate(
"地图创建前取得的初始实时位置(WGS-84)",
coordinate: realtime
)
} else {
RuntimeLogger.warning("APP", "Startup", "地图创建前无法取得实时定位,唯一初始位置使用深圳", details: [
"地图标准": mapCoordinateSystem.rawValue,
"缩放米": "1000"
])
}
} else {
RuntimeLogger.info("APP", "Startup", "已找到持久化图钉,直接准备唯一初始地图状态")
}
guard !Task.isCancelled else { return }
setup.completeSetup()
RuntimeLogger.info("APP", "Startup", "启动门禁全部完成,现在创建 MapHomeView")
phase = .map
}
private func finishInitialSetup() {
let completedMode = runtimeMode.mode
runtimeMode.markInitialized(completedMode)
setup.completeSetup()
phase = .splash
Task { await bootstrap() }
}
private func finishPresentedSetup() {
runtimeMode.markInitialized(runtimeMode.mode)
setup.completeSetup()
}
@MainActor
private func checkForUpdates() async {
let currentVersion = Bundle.main.object(
forInfoDictionaryKey: "CFBundleShortVersionString"
) as? String ?? AppRemoteConfiguration.fallback.latestVersion
let configuration: AppRemoteConfiguration
if let remote = await AppRemoteConfigurationService.fetch() {
remoteConfiguration.apply(remote)
configuration = remote
} else {
configuration = remoteConfiguration.configuration
}
guard let pendingPrompt = configuration.updatePrompt(currentVersion: currentVersion) else { return }
let releaseNotes = await AppRemoteConfigurationService.fetchReleaseNotes(
version: pendingPrompt.latestVersion
)
guard !Task.isCancelled,
let prompt = configuration.updatePrompt(
currentVersion: currentVersion,
releaseNotes: releaseNotes
) else {
return
}
if prompt.requirement == .required {
requiredUpdatePrompt = prompt
}
updatePrompt = prompt
}
private func updateAlert(for prompt: AppUpdatePrompt) -> Alert {
switch prompt.requirement {
case .required:
let details = prompt.releaseNotes
?? String(localized: "更新说明暂时无法加载,请前往最新 Release 页面查看。")
return Alert(
title: Text("需要更新"),
message: Text(
"当前版本 \(prompt.currentVersion) 已停止支持,请更新到 \(prompt.latestVersion) 后继续使用。\n\n\(details)"
),
dismissButton: .default(Text("立即更新")) {
openUpdatePage(requiredPrompt: prompt)
}
)
case .recommended:
let details = prompt.releaseNotes
?? String(localized: "更新说明暂时无法加载,请前往最新 Release 页面查看。")
return Alert(
title: Text("发现新版本"),
message: Text(
"当前版本 \(prompt.currentVersion),最新版本 \(prompt.latestVersion)\n\n\(details)"
),
primaryButton: .default(Text("前往更新")) {
openUpdatePage(requiredPrompt: nil)
},
secondaryButton: .cancel(Text("稍后"))
)
}
}
private func openUpdatePage(requiredPrompt: AppUpdatePrompt?) {
UIApplication.shared.open(AppRemoteConfigurationService.releasesURL)
guard requiredPrompt != nil else { return }
Task { @MainActor in
try? await Task.sleep(nanoseconds: 800_000_000)
updatePrompt = self.requiredUpdatePrompt
}
}
}