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.
This commit is contained in:
Syntax-Error-1337
2026-09-10 01:05:45 +08:00
parent eadeca636d
commit 908ac8f734
17 changed files with 493 additions and 121 deletions
+1 -1
View File
@@ -56,7 +56,7 @@ struct BugReportView: View {
if isRunning { if isRunning {
ProgressView().tint(.white) ProgressView().tint(.white)
} }
Text(isRunning ? "正在生成报告…" : "生成 Bug 报告") (isRunning ? Text("正在生成报告…") : Text("生成 Bug 报告"))
.font(.body.weight(.medium)) .font(.body.weight(.medium))
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
+5 -5
View File
@@ -20,9 +20,9 @@ struct ContentView: View {
Image(systemName: "location.fill") Image(systemName: "location.fill")
.font(.system(size: 48)).foregroundStyle(.blue) .font(.system(size: 48)).foregroundStyle(.blue)
ProgressView() ProgressView()
Text(runtimeMode.hasSelectedMode && runtimeMode.mode == .localWiFi (runtimeMode.hasSelectedMode && runtimeMode.mode == .localWiFi
? "正在初始化地图与本地代理…" ? Text("正在初始化地图与本地代理…")
: "正在初始化地图…") : Text("正在初始化地图…"))
.font(.subheadline).foregroundStyle(.secondary) .font(.subheadline).foregroundStyle(.secondary)
} }
case .setup: case .setup:
@@ -177,7 +177,7 @@ struct ContentView: View {
switch prompt.requirement { switch prompt.requirement {
case .required: case .required:
let details = prompt.releaseNotes let details = prompt.releaseNotes
?? "更新说明暂时无法加载,请前往最新 Release 页面查看。" ?? String(localized: "更新说明暂时无法加载,请前往最新 Release 页面查看。")
return Alert( return Alert(
title: Text("需要更新"), title: Text("需要更新"),
message: Text( message: Text(
@@ -189,7 +189,7 @@ struct ContentView: View {
) )
case .recommended: case .recommended:
let details = prompt.releaseNotes let details = prompt.releaseNotes
?? "更新说明暂时无法加载,请前往最新 Release 页面查看。" ?? String(localized: "更新说明暂时无法加载,请前往最新 Release 页面查看。")
return Alert( return Alert(
title: Text("发现新版本"), title: Text("发现新版本"),
message: Text( message: Text(
+18 -12
View File
@@ -12,6 +12,7 @@ struct RuntimeLogsView: View {
@State private var entries: [RuntimeLogEntry] = [] @State private var entries: [RuntimeLogEntry] = []
@State private var isTesting = false @State private var isTesting = false
@State private var testResult = "" @State private var testResult = ""
@State private var testSucceeded = false
@State private var testMessage = "" @State private var testMessage = ""
@State private var showClearConfirm = false @State private var showClearConfirm = false
@State private var copiedEntryID: UUID? @State private var copiedEntryID: UUID?
@@ -42,7 +43,7 @@ struct RuntimeLogsView: View {
if filteredEntries.isEmpty { if filteredEntries.isEmpty {
VStack(spacing: 10) { VStack(spacing: 10) {
Image(systemName: "doc.text.magnifyingglass").font(.largeTitle).foregroundStyle(.secondary) Image(systemName: "doc.text.magnifyingglass").font(.largeTitle).foregroundStyle(.secondary)
Text(entries.isEmpty ? "暂无运行日志" : "无匹配日志").foregroundStyle(.secondary) (entries.isEmpty ? Text("暂无运行日志") : Text("无匹配日志")).foregroundStyle(.secondary)
}.frame(maxWidth: .infinity, maxHeight: .infinity) }.frame(maxWidth: .infinity, maxHeight: .infinity)
} else { } else {
ScrollView { ScrollView {
@@ -103,8 +104,11 @@ struct RuntimeLogsView: View {
await runThirdPartyConnectionTest() await runThirdPartyConnectionTest()
} else { } else {
let result = await setup.runVerificationTest() let result = await setup.runVerificationTest()
testResult = result.isSuccess ? "环境检测通过" : "环境检测失败: \(result.id)" testSucceeded = result.isSuccess
if !result.isSuccess { testResult += ",查看下方日志" } testResult = result.isSuccess
? String(localized: "环境检测通过")
: String(localized: "环境检测失败: \(result.localizedTitle)")
if !result.isSuccess { testResult += String(localized: ",查看下方日志") }
testMessage = setup.testLog testMessage = setup.testLog
} }
isTesting = false; refresh() isTesting = false; refresh()
@@ -116,7 +120,7 @@ struct RuntimeLogsView: View {
} else { } else {
Image(systemName: "play.fill").font(.system(size: 13, weight: .bold)) Image(systemName: "play.fill").font(.system(size: 13, weight: .bold))
} }
Text(isTesting ? "正在检测…" : "环境检测").font(.subheadline.weight(.semibold)) (isTesting ? Text("正在检测…") : Text("环境检测")).font(.subheadline.weight(.semibold))
Spacer() Spacer()
Image(systemName: "chevron.right").font(.system(size: 12, weight: .semibold)).opacity(0.5) Image(systemName: "chevron.right").font(.system(size: 12, weight: .semibold)).opacity(0.5)
} }
@@ -131,9 +135,9 @@ struct RuntimeLogsView: View {
.fill(isTesting ? Color.gray : Color.blue) .fill(isTesting ? Color.gray : Color.blue)
) )
.disabled(isTesting || actions.state.isBusy) .disabled(isTesting || actions.state.isBusy)
Text(runtimeMode.mode == .thirdParty (runtimeMode.mode == .thirdParty
? "检查第三方模块能否拦截并响应 query 请求;不会写入测试坐标。" ? Text("检查第三方模块能否拦截并响应 query 请求;不会写入测试坐标。")
: "依次检查:本地代理 → CA 证书信任 → Wi-Fi 代理链路。") : Text("依次检查:本地代理 → CA 证书信任 → Wi-Fi 代理链路。"))
.font(.caption).foregroundStyle(.secondary) .font(.caption).foregroundStyle(.secondary)
if !testMessage.isEmpty { if !testMessage.isEmpty {
VStack(alignment: .leading, spacing: 6) { VStack(alignment: .leading, spacing: 6) {
@@ -180,13 +184,13 @@ struct RuntimeLogsView: View {
} }
if runtimeMode.mode == .localWiFi { if runtimeMode.mode == .localWiFi {
HStack(spacing: 14) { HStack(spacing: 14) {
Label(proxy.isRunning ? "代理运行中" : "代理未运行", systemImage: proxy.isRunning ? "play.circle" : "stop.circle") (proxy.isRunning ? Label("代理运行中", systemImage: "play.circle") : Label("代理未运行", systemImage: "stop.circle"))
Label(setup.canModify ? "可修改" : "不可修改", systemImage: setup.canModify ? "checkmark.shield.fill" : "xmark.shield") (setup.canModify ? Label("可修改", systemImage: "checkmark.shield.fill") : Label("不可修改", systemImage: "xmark.shield"))
}.font(.caption).foregroundStyle(.secondary) }.font(.caption).foregroundStyle(.secondary)
} }
if !testResult.isEmpty { if !testResult.isEmpty {
Text(testResult).font(.footnote.weight(.medium)) Text(testResult).font(.footnote.weight(.medium))
.foregroundStyle(testResult.contains("通过") ? .green : .red) .foregroundStyle(testSucceeded ? .green : .red)
} }
}.padding(14).background(Color(.secondarySystemBackground)) }.padding(14).background(Color(.secondarySystemBackground))
} }
@@ -240,7 +244,8 @@ struct RuntimeLogsView: View {
do { do {
let response = try await thirdPartyProxy.query() let response = try await thirdPartyProxy.query()
let active = response.success && response.latitude != nil && response.longitude != nil let active = response.success && response.latitude != nil && response.longitude != nil
testResult = active ? "第三方模块连接通过,已有坐标" : "第三方模块连接通过,暂无坐标" testSucceeded = true
testResult = active ? String(localized: "第三方模块连接通过,已有坐标") : String(localized: "第三方模块连接通过,暂无坐标")
testMessage = """ testMessage = """
======== 第三方代理连接检测 ======== ======== 第三方代理连接检测 ========
模式: 测试模式 模式: 测试模式
@@ -249,7 +254,8 @@ struct RuntimeLogsView: View {
已保存坐标: \(active ? "" : "") 已保存坐标: \(active ? "" : "")
""" """
} catch { } catch {
testResult = "第三方模块连接失败" testSucceeded = false
testResult = String(localized: "第三方模块连接失败")
testMessage = """ testMessage = """
======== 第三方代理连接检测 ======== ======== 第三方代理连接检测 ========
模式: 测试模式 模式: 测试模式
+31 -33
View File
@@ -150,7 +150,7 @@ struct FirstSetupView: View {
.clipShape(RoundedRectangle(cornerRadius: 8)) .clipShape(RoundedRectangle(cornerRadius: 8))
.padding() .padding()
} }
.navigationTitle(preview.title) .navigationTitle(LocalizedStringKey(preview.title))
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {
ToolbarItem(placement: .confirmationAction) { ToolbarItem(placement: .confirmationAction) {
@@ -188,7 +188,7 @@ struct FirstSetupView: View {
Circle() Circle()
.fill(value.rawValue <= step.rawValue ? Color.blue : Color.gray.opacity(0.3)) .fill(value.rawValue <= step.rawValue ? Color.blue : Color.gray.opacity(0.3))
.frame(width: 10, height: 10) .frame(width: 10, height: 10)
Text(value.title).font(.caption).foregroundStyle(.secondary) Text(LocalizedStringKey(value.title)).font(.caption).foregroundStyle(.secondary)
} }
if value != visibleSteps.last { if value != visibleSteps.last {
Rectangle().fill(Color.gray.opacity(0.3)).frame(width: 28, height: 2) Rectangle().fill(Color.gray.opacity(0.3)).frame(width: 28, height: 2)
@@ -274,10 +274,10 @@ struct FirstSetupView: View {
} }
private func modeCard( private func modeCard(
title: String, title: LocalizedStringKey,
icon: String, icon: String,
badges: [String], badges: [String],
description: String, description: LocalizedStringKey,
tint: Color, tint: Color,
action: @escaping () -> Void action: @escaping () -> Void
) -> some View { ) -> some View {
@@ -288,7 +288,7 @@ struct FirstSetupView: View {
.foregroundStyle(tint) .foregroundStyle(tint)
HStack(spacing: 6) { HStack(spacing: 6) {
ForEach(badges, id: \.self) { badge in ForEach(badges, id: \.self) { badge in
Text(badge) Text(LocalizedStringKey(badge))
.font(.caption2.weight(.semibold)) .font(.caption2.weight(.semibold))
.padding(.horizontal, 8) .padding(.horizontal, 8)
.padding(.vertical, 4) .padding(.vertical, 4)
@@ -354,7 +354,7 @@ struct FirstSetupView: View {
if let url = await setup.proxy.prepareCertificateDownloadURL() { if let url = await setup.proxy.prepareCertificateDownloadURL() {
certificateDownloadDestination = CertificateDownloadDestination(url: url) certificateDownloadDestination = CertificateDownloadDestination(url: url)
} else { } else {
setupActionError = setup.proxy.error ?? "无法准备证书下载页面,请查看诊断日志" setupActionError = setup.proxy.error ?? String(localized: "无法准备证书下载页面,请查看诊断日志")
} }
} }
}, },
@@ -507,7 +507,7 @@ struct FirstSetupView: View {
UIPasteboard.general.string = client.subscriptionURL.absoluteString UIPasteboard.general.string = client.subscriptionURL.absoluteString
copiedSubscriptionURL = true copiedSubscriptionURL = true
} label: { } label: {
Label(copiedSubscriptionURL ? "已复制模块订阅地址" : "复制模块订阅地址", systemImage: "doc.on.doc") (copiedSubscriptionURL ? Label("已复制模块订阅地址", systemImage: "doc.on.doc") : Label("复制模块订阅地址", systemImage: "doc.on.doc"))
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
} }
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
@@ -565,7 +565,7 @@ struct FirstSetupView: View {
if let thirdPartyFailureLog { if let thirdPartyFailureLog {
testResultView( testResultView(
success: false, success: false,
title: "接口连接失败", title: String(localized: "接口连接失败"),
log: thirdPartyFailureLog log: thirdPartyFailureLog
) )
.id("thirdPartyFailureLog") .id("thirdPartyFailureLog")
@@ -615,16 +615,15 @@ struct FirstSetupView: View {
UIPasteboard.general.string = ThirdPartyProxyManager.interceptionHostnamesText UIPasteboard.general.string = ThirdPartyProxyManager.interceptionHostnamesText
copiedMITMHostname = true copiedMITMHostname = true
} label: { } label: {
Label( (copiedMITMHostname
copiedMITMHostname ? "已复制解密域名" : "复制解密域名", ? Label("已复制解密域名", systemImage: "doc.on.doc")
systemImage: "doc.on.doc" : Label("复制解密域名", systemImage: "doc.on.doc"))
)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
} }
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
} }
private func instructionRow(_ number: Int, _ text: String) -> some View { private func instructionRow(_ number: Int, _ text: LocalizedStringKey) -> some View {
HStack(alignment: .top, spacing: 8) { HStack(alignment: .top, spacing: 8) {
Text("\(number)") Text("\(number)")
.font(.caption2.bold()) .font(.caption2.bold())
@@ -642,7 +641,7 @@ struct FirstSetupView: View {
private func setupScreenshot( private func setupScreenshot(
assetName: String, assetName: String,
title: String, title: String,
caption: String caption: LocalizedStringKey
) -> some View { ) -> some View {
if let image = UIImage(named: assetName) { if let image = UIImage(named: assetName) {
Button { Button {
@@ -673,7 +672,7 @@ struct FirstSetupView: View {
) )
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.accessibilityLabel("\(title)\(caption)") .accessibilityLabel(LocalizedStringKey(title))
.accessibilityHint("轻点查看大图") .accessibilityHint("轻点查看大图")
} }
} }
@@ -683,7 +682,7 @@ struct FirstSetupView: View {
UIApplication.shared.open(url, options: [:]) { opened in UIApplication.shared.open(url, options: [:]) { opened in
guard !opened else { return } guard !opened else { return }
Task { @MainActor in Task { @MainActor in
manualHint = "无法打开 \(client.name),请确认客户端已安装后手动打开。" manualHint = String(localized: "无法打开 \(client.name),请确认客户端已安装后手动打开。")
} }
} }
} }
@@ -705,10 +704,10 @@ struct FirstSetupView: View {
} }
private func certificateCard( private func certificateCard(
title: String, title: LocalizedStringKey,
icon: String, icon: String,
description: String, description: LocalizedStringKey,
actionTitle: String, actionTitle: LocalizedStringKey,
actionIcon: String, actionIcon: String,
complete: Bool, complete: Bool,
action: @escaping () -> Void, action: @escaping () -> Void,
@@ -728,10 +727,9 @@ struct FirstSetupView: View {
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
.tint(.blue) .tint(.blue)
Button(action: markComplete) { Button(action: markComplete) {
Label( (complete
complete ? "已完成 ✓" : "已完成", ? Label("已完成 ✓", systemImage: "checkmark.circle.fill")
systemImage: complete ? "checkmark.circle.fill" : "circle" : Label("已完成", systemImage: "circle"))
)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
} }
.buttonStyle(.bordered) .buttonStyle(.bordered)
@@ -746,7 +744,7 @@ struct FirstSetupView: View {
let success = result.isSuccess let success = result.isSuccess
testResultView( testResultView(
success: success, success: success,
title: success ? "环境检测通过" : failureSummary(result), title: success ? String(localized: "环境检测通过") : failureSummary(result),
log: setup.testLog log: setup.testLog
) )
} }
@@ -905,7 +903,7 @@ struct FirstSetupView: View {
} }
} }
private func actionLabel(_ title: String) -> some View { private func actionLabel(_ title: LocalizedStringKey) -> some View {
HStack { HStack {
if isVerifying { ProgressView().tint(.white).controlSize(.small) } if isVerifying { ProgressView().tint(.white).controlSize(.small) }
Text(title) Text(title)
@@ -954,14 +952,14 @@ struct FirstSetupView: View {
private func failureSummary(_ result: VerificationResult) -> String { private func failureSummary(_ result: VerificationResult) -> String {
switch result { switch result {
case .certNotTrusted: return "证书尚未安装或信任" case .certNotTrusted: return String(localized: "证书尚未安装或信任")
case .wifiProxyNotConfigured: return "Wi-Fi 代理未正确设置" case .wifiProxyNotConfigured: return String(localized: "Wi-Fi 代理未正确设置")
case .proxyNotRunning: return "本地代理未能启动" case .proxyNotRunning: return String(localized: "本地代理未能启动")
case .verificationInProgress: return "检测仍在进行" case .verificationInProgress: return String(localized: "检测仍在进行")
case .verificationSuperseded: return "检测结果已过期" case .verificationSuperseded: return String(localized: "检测结果已过期")
case .coordinateWriteFailed: return "坐标写入失败" case .coordinateWriteFailed: return String(localized: "坐标写入失败")
case .patchFailed: return "定位改写检测失败" case .patchFailed: return String(localized: "定位改写检测失败")
case .success: return "环境检测通过" case .success: return String(localized: "环境检测通过")
} }
} }
+16 -16
View File
@@ -301,7 +301,7 @@ struct MapHomeView: View {
} }
} message: { } message: {
Text( Text(
"你正在使用 \(communityContributionClient?.name ?? "第三方客户端")。点击“去提交”会先复制投稿模板,并在 App 内打开社区页面。采纳后将收录到 README,可选择是否匿名署名。" "你正在使用 \(communityContributionClient?.name ?? String(localized: "第三方客户端"))。点击“去提交”会先复制投稿模板,并在 App 内打开社区页面。采纳后将收录到 README,可选择是否匿名署名。"
) )
} }
.alert("已复制投稿模板", isPresented: $showCommunityTemplateCopied) { .alert("已复制投稿模板", isPresented: $showCommunityTemplateCopied) {
@@ -483,7 +483,7 @@ struct MapHomeView: View {
// //
HStack { HStack {
VStack(alignment: .leading, spacing: 3) { VStack(alignment: .leading, spacing: 3) {
Text(mapState.displayName ?? "当前选点").font(.subheadline.weight(.semibold)).lineLimit(1) Text(mapState.displayName ?? String(localized: "当前选点")).font(.subheadline.weight(.semibold)).lineLimit(1)
coordinateRow(label: "GCJ-02(国内)", system: .gcj02) coordinateRow(label: "GCJ-02(国内)", system: .gcj02)
coordinateRow(label: "WGS-84(国际)", system: .wgs84) coordinateRow(label: "WGS-84(国际)", system: .wgs84)
} }
@@ -496,7 +496,7 @@ struct MapHomeView: View {
activeTip = .deactivation activeTip = .deactivation
} }
} label: { } label: {
Text(spoofState == .active ? "无法生效?" : "无法取消?") (spoofState == .active ? Text("无法生效?") : Text("无法取消?"))
.font(.system(size: 10, weight: .medium)) .font(.system(size: 10, weight: .medium))
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.padding(.horizontal, 8) .padding(.horizontal, 8)
@@ -520,7 +520,7 @@ struct MapHomeView: View {
.buttonStyle(.plain) .buttonStyle(.plain)
.foregroundStyle(favorites.selectedFavoriteID != nil ? .orange : .gray) .foregroundStyle(favorites.selectedFavoriteID != nil ? .orange : .gray)
.disabled(favoriteSaveTask != nil) .disabled(favoriteSaveTask != nil)
.accessibilityLabel(favorites.selectedFavoriteID != nil ? "已收藏,点击取消收藏" : "收藏当前选点") .accessibilityLabel(favorites.selectedFavoriteID != nil ? Text("已收藏,点击取消收藏") : Text("收藏当前选点"))
} }
// //
if favorites.favorites.isEmpty { if favorites.favorites.isEmpty {
@@ -537,7 +537,7 @@ struct MapHomeView: View {
if spoofState == .verifying { if spoofState == .verifying {
ProgressView().tint(.white) ProgressView().tint(.white)
} }
Text(spoofState == .active && needsSwitchButton ? "关闭" : buttonTitle) (spoofState == .active && needsSwitchButton ? Text("关闭") : Text(buttonTitle))
.font(.headline).lineLimit(1) .font(.headline).lineLimit(1)
} }
.frame(maxWidth: needsSwitchButton ? nil : .infinity) .frame(maxWidth: needsSwitchButton ? nil : .infinity)
@@ -585,15 +585,15 @@ struct MapHomeView: View {
private var buttonTitle: String { private var buttonTitle: String {
if runtimeMode.mode == .thirdParty { if runtimeMode.mode == .thirdParty {
switch spoofState { switch spoofState {
case .idle: return "同步到第三方代理" case .idle: return String(localized: "同步到第三方代理")
case .verifying: return "检测并同步中…" case .verifying: return String(localized: "检测并同步中…")
case .active: return "停止第三方虚拟定位" case .active: return String(localized: "停止第三方虚拟定位")
} }
} }
switch spoofState { switch spoofState {
case .idle: return "开始虚拟定位" case .idle: return String(localized: "开始虚拟定位")
case .verifying: return "验证环境中…" case .verifying: return String(localized: "验证环境中…")
case .active: return "停止虚拟定位" case .active: return String(localized: "停止虚拟定位")
} }
} }
@@ -856,7 +856,7 @@ struct MapHomeView: View {
} }
private func coordinateRow( private func coordinateRow(
label: String, label: LocalizedStringKey,
system: CoordinateConverter.MapCoordinateSystem system: CoordinateConverter.MapCoordinateSystem
) -> some View { ) -> some View {
let coordinate = currentSelectionPair.coordinate(for: system) let coordinate = currentSelectionPair.coordinate(for: system)
@@ -1091,7 +1091,7 @@ struct MapHomeView: View {
"请求动作": "WLOC query", "请求动作": "WLOC query",
"错误": response.error ?? "未知错误" "错误": response.error ?? "未知错误"
]) ])
setup.requestThirdPartySetup(message: response.error ?? "第三方代理查询失败") setup.requestThirdPartySetup(message: response.error ?? String(localized: "第三方代理查询失败"))
} }
} catch { } catch {
spoofState = .idle spoofState = .idle
@@ -1144,7 +1144,7 @@ struct MapHomeView: View {
"Wi-Fi接口": String(net.isWiFiEnabled) "Wi-Fi接口": String(net.isWiFiEnabled)
]) ])
activeTip = nil activeTip = nil
setup.requestSetup(message: "当前未连接可用的 Wi-Fi,请连接 Wi-Fi 后配置 127.0.0.1:8888 手动代理。") setup.requestSetup(message: String(localized: "当前未连接可用的 Wi-Fi,请连接 Wi-Fi 后配置 127.0.0.1:8888 手动代理。"))
return return
} }
@@ -1517,7 +1517,7 @@ struct MapHomeView: View {
} }
searchResults = (response?.mapItems ?? []).prefix(6).map { item in searchResults = (response?.mapItems ?? []).prefix(6).map { item in
let r = SearchLocationResult( let r = SearchLocationResult(
name: item.name ?? "未命名", name: item.name ?? String(localized: "未命名"),
subtitle: [item.placemark.locality, item.placemark.subLocality, item.placemark.thoroughfare] subtitle: [item.placemark.locality, item.placemark.subLocality, item.placemark.thoroughfare]
.compactMap { $0 } .compactMap { $0 }
.filter { !$0.isEmpty } .filter { !$0.isEmpty }
@@ -1529,7 +1529,7 @@ struct MapHomeView: View {
]) ])
return r return r
} }
if searchResults.isEmpty { searchError = "没有找到相关地点" } if searchResults.isEmpty { searchError = String(localized: "没有找到相关地点") }
} }
} }
} }
+2 -2
View File
@@ -154,13 +154,13 @@ final class ProxyManager: ObservableObject {
do { do {
if !isRunning { try await start() } if !isRunning { try await start() }
guard let url = URL(string: "http://127.0.0.1:8888/cert") else { guard let url = URL(string: "http://127.0.0.1:8888/cert") else {
error = "证书下载地址无效" error = String(localized: "证书下载地址无效")
return nil return nil
} }
error = nil error = nil
return url return url
} catch { } catch {
self.error = "启动代理失败: \(error.localizedDescription)" self.error = String(localized: "启动代理失败: \(error.localizedDescription)")
RuntimeLogger.error("APP", "Certificate", "准备证书下载失败", error: error) RuntimeLogger.error("APP", "Certificate", "准备证书下载失败", error: error)
return nil return nil
} }
+26 -26
View File
@@ -30,7 +30,7 @@ struct SettingsView: View {
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@State private var activeTip: TipKind? @State private var activeTip: TipKind?
@State private var proxyOperationError = "" @State private var proxyOperationError = ""
@State private var proxyOperationAlertTitle = "代理操作失败" @State private var proxyOperationAlertTitle = String(localized: "代理操作失败")
@State private var modeOperationRunning = false @State private var modeOperationRunning = false
@State private var copiedClient: ThirdPartyProxyClient? @State private var copiedClient: ThirdPartyProxyClient?
@State private var copiedMITMHostnames = false @State private var copiedMITMHostnames = false
@@ -268,7 +268,7 @@ struct SettingsView: View {
} }
} }
private func valueRow(_ title: String, value: String) -> some View { private func valueRow(_ title: LocalizedStringKey, value: String) -> some View {
HStack { Text(title); Spacer(); Text(value).font(.footnote.monospaced()).foregroundStyle(.secondary) } HStack { Text(title); Spacer(); Text(value).font(.footnote.monospaced()).foregroundStyle(.secondary) }
} }
@@ -319,15 +319,15 @@ struct SettingsView: View {
) )
case .available(let prompt): case .available(let prompt):
let details = prompt.releaseNotes let details = prompt.releaseNotes
?? "更新说明暂时无法加载,请前往最新 Release 页面查看。" ?? String(localized: "更新说明暂时无法加载,请前往最新 Release 页面查看。")
let message: String let message: String
if prompt.requirement == .required { if prompt.requirement == .required {
message = "当前版本 \(prompt.currentVersion) 已停止支持,请更新到 \(prompt.latestVersion) 后继续使用。\n\n\(details)" message = String(localized: "当前版本 \(prompt.currentVersion) 已停止支持,请更新到 \(prompt.latestVersion) 后继续使用。\n\n\(details)")
} else { } else {
message = "当前版本 \(prompt.currentVersion),最新版本 \(prompt.latestVersion)\n\n\(details)" message = String(localized: "当前版本 \(prompt.currentVersion),最新版本 \(prompt.latestVersion)\n\n\(details)")
} }
return Alert( return Alert(
title: Text(prompt.requirement == .required ? "需要更新" : "发现新版本"), title: prompt.requirement == .required ? Text("需要更新") : Text("发现新版本"),
message: Text(message), message: Text(message),
primaryButton: .default(Text("前往更新")) { primaryButton: .default(Text("前往更新")) {
UIApplication.shared.open(AppRemoteConfigurationService.releasesURL) UIApplication.shared.open(AppRemoteConfigurationService.releasesURL)
@@ -351,7 +351,7 @@ struct SettingsView: View {
try await proxy.start() try await proxy.start()
} catch { } catch {
proxy.error = error.localizedDescription proxy.error = error.localizedDescription
proxyOperationAlertTitle = "代理操作失败" proxyOperationAlertTitle = String(localized: "代理操作失败")
proxyOperationError = error.localizedDescription proxyOperationError = error.localizedDescription
} }
} else { } else {
@@ -422,14 +422,14 @@ struct SettingsView: View {
UIPasteboard.general.string = thirdPartyClient.selectedClient.subscriptionURL.absoluteString UIPasteboard.general.string = thirdPartyClient.selectedClient.subscriptionURL.absoluteString
copiedClient = thirdPartyClient.selectedClient copiedClient = thirdPartyClient.selectedClient
} label: { } label: {
Label(copiedClient == thirdPartyClient.selectedClient ? "已复制模块订阅地址" : "复制模块订阅地址", systemImage: "doc.on.doc") (copiedClient == thirdPartyClient.selectedClient ? Label("已复制模块订阅地址", systemImage: "doc.on.doc") : Label("复制模块订阅地址", systemImage: "doc.on.doc"))
} }
Button { Button {
UIPasteboard.general.string = ThirdPartyProxyManager.interceptionHostnamesText UIPasteboard.general.string = ThirdPartyProxyManager.interceptionHostnamesText
copiedMITMHostnames = true copiedMITMHostnames = true
} label: { } label: {
Label(copiedMITMHostnames ? "已复制解密域名" : "复制解密域名", systemImage: "doc.on.doc") (copiedMITMHostnames ? Label("已复制解密域名", systemImage: "doc.on.doc") : Label("复制解密域名", systemImage: "doc.on.doc"))
} }
Button { Button {
@@ -468,31 +468,31 @@ struct SettingsView: View {
private var thirdPartyStatusText: String { private var thirdPartyStatusText: String {
switch thirdPartyProxy.connectionState { switch thirdPartyProxy.connectionState {
case .unknown: return "未检测" case .unknown: return String(localized: "未检测")
case .connected(let active): return active ? "已连接,有坐标" : "已连接,无坐标" case .connected(let active): return active ? String(localized: "已连接,有坐标") : String(localized: "已连接,无坐标")
case .failed: return "连接失败" case .failed: return String(localized: "连接失败")
} }
} }
private var virtualLocationStatusText: String { private var virtualLocationStatusText: String {
if runtimeMode.mode == .localWiFi { if runtimeMode.mode == .localWiFi {
return actions.virtualLocationEnabled ? "已开启" : "已关闭" return actions.virtualLocationEnabled ? String(localized: "已开启") : String(localized: "已关闭")
} }
if case .connected(let active) = thirdPartyProxy.connectionState { if case .connected(let active) = thirdPartyProxy.connectionState {
return active ? "第三方已保存" : "未保存" return active ? String(localized: "第三方已保存") : String(localized: "未保存")
} }
return "未知" return String(localized: "未知")
} }
private var workflowDescription: String { private var workflowDescription: String {
if runtimeMode.mode == .thirdParty { if runtimeMode.mode == .thirdParty {
return "App 只负责地图选点、收藏和发送 WGS-84 坐标。第三方代理客户端通过模块拦截 Apple WLOC 请求并持久化当前坐标;本模式不启动本机代理,不使用 App 的 CA,也不需要配置 127.0.0.1:8888。" return String(localized: "App 只负责地图选点、收藏和发送 WGS-84 坐标。第三方代理客户端通过模块拦截 Apple WLOC 请求并持久化当前坐标;本模式不启动本机代理,不使用 App 的 CA,也不需要配置 127.0.0.1:8888。")
} }
return """ return String(localized: """
App 在设备本地运行一个代理服务器(127.0.0.1:8888)。 App 在设备本地运行一个代理服务器(127.0.0.1:8888)。
通过 WiFi 手动代理配置,让系统发往 Apple 定位域名(gs-loc.apple.com、gsp-ssl.ls.apple.com、bluedot.is.autonavi.com 等)的定位请求经过这个本地代理。代理使用已安装的 CA 证书对 HTTPS 流量做中间人解密,把 Apple 返回的定位坐标改写为你设置的虚拟坐标,再加密返回给系统,从而实现虚拟定位。 通过 WiFi 手动代理配置,让系统发往 Apple 定位域名(gs-loc.apple.com、gsp-ssl.ls.apple.com、bluedot.is.autonavi.com 等)的定位请求经过这个本地代理。代理使用已安装的 CA 证书对 HTTPS 流量做中间人解密,把 Apple 返回的定位坐标改写为你设置的虚拟坐标,再加密返回给系统,从而实现虚拟定位。
""" """)
} }
private func switchRuntimeMode(to newMode: ProxyRuntimeMode) { private func switchRuntimeMode(to newMode: ProxyRuntimeMode) {
@@ -509,8 +509,8 @@ struct SettingsView: View {
if runtimeMode.isInitialized(.thirdParty) { if runtimeMode.isInitialized(.thirdParty) {
do { do {
_ = try await thirdPartyProxy.query() _ = try await thirdPartyProxy.query()
proxyOperationAlertTitle = "模式已切换" proxyOperationAlertTitle = String(localized: "模式已切换")
proxyOperationError = "第三方代理模式检测通过。请关闭 Wi-Fi 中的 127.0.0.1:8888 手动代理,避免双重拦截。" proxyOperationError = String(localized: "第三方代理模式检测通过。请关闭 Wi-Fi 中的 127.0.0.1:8888 手动代理,避免双重拦截。")
} catch { } catch {
openThirdPartySetup(for: error) openThirdPartySetup(for: error)
} }
@@ -532,8 +532,8 @@ struct SettingsView: View {
let result = await setup.runVerificationTest() let result = await setup.runVerificationTest()
setup.applyVerificationResult(result) setup.applyVerificationResult(result)
if result.isSuccess { if result.isSuccess {
proxyOperationAlertTitle = "模式已切换" proxyOperationAlertTitle = String(localized: "模式已切换")
proxyOperationError = "APP 模式环境检测通过。请停用第三方 WLOC 模块或代理连接,避免双重拦截。" proxyOperationError = String(localized: "APP 模式环境检测通过。请停用第三方 WLOC 模块或代理连接,避免双重拦截。")
} else { } else {
dismiss() dismiss()
} }
@@ -594,14 +594,14 @@ struct SettingsView: View {
try setup.certificateStore.reset() try setup.certificateStore.reset()
runtimeMode.resetInitialization(.localWiFi) runtimeMode.resetInitialization(.localWiFi)
guard await setup.prepareLocalServices() else { guard await setup.prepareLocalServices() else {
proxyOperationAlertTitle = "证书重置失败" proxyOperationAlertTitle = String(localized: "证书重置失败")
proxyOperationError = setup.message proxyOperationError = setup.message
return return
} }
setup.requestCertificateSetup() setup.requestCertificateSetup()
dismiss() dismiss()
} catch { } catch {
proxyOperationAlertTitle = "证书重置失败" proxyOperationAlertTitle = String(localized: "证书重置失败")
proxyOperationError = error.localizedDescription proxyOperationError = error.localizedDescription
} }
} }
@@ -612,8 +612,8 @@ struct SettingsView: View {
UIApplication.shared.open(url, options: [:]) { opened in UIApplication.shared.open(url, options: [:]) { opened in
guard !opened else { return } guard !opened else { return }
Task { @MainActor in Task { @MainActor in
proxyOperationAlertTitle = "无法打开客户端" proxyOperationAlertTitle = String(localized: "无法打开客户端")
proxyOperationError = "无法打开 \(client.name),请确认客户端已安装后手动打开。" proxyOperationError = String(localized: "无法打开 \(client.name),请确认客户端已安装后手动打开。")
} }
} }
} }
+4 -4
View File
@@ -36,7 +36,7 @@ final class SetupCoordinator: ObservableObject {
message = "" message = ""
return true return true
} catch { } catch {
message = "本地代理初始化失败:\(error.localizedDescription)" message = String(localized: "本地代理初始化失败:\(error.localizedDescription)")
RuntimeLogger.error("APP", "Startup", "本地服务初始化失败", error: error) RuntimeLogger.error("APP", "Startup", "本地服务初始化失败", error: error)
return false return false
} }
@@ -48,19 +48,19 @@ final class SetupCoordinator: ObservableObject {
case .success: case .success:
trustState = .trusted trustState = .trusted
needsSetup = false needsSetup = false
message = "✓ 定位环境正常" message = String(localized: "✓ 定位环境正常")
case .certNotTrusted: case .certNotTrusted:
trustState = .unavailable trustState = .unavailable
setupStep = .cert setupStep = .cert
needsSetup = true needsSetup = true
message = "CA 证书未安装或未信任" message = String(localized: "CA 证书未安装或未信任")
case .verificationInProgress, .verificationSuperseded: case .verificationInProgress, .verificationSuperseded:
break break
default: default:
trustState = .unavailable trustState = .unavailable
setupStep = .proxy setupStep = .proxy
needsSetup = true needsSetup = true
message = "Wi-Fi 代理未正确设置,请检查 127.0.0.1:8888" message = String(localized: "Wi-Fi 代理未正确设置,请检查 127.0.0.1:8888")
} }
} }
+4 -4
View File
@@ -25,13 +25,13 @@ enum SystemSettingsDestination {
var manualPath: String { var manualPath: String {
switch self { switch self {
case .appPermissions: case .appPermissions:
return "请手动打开「设置」,找到本 App 后检查定位权限。" return String(localized: "请手动打开「设置」,找到本 App 后检查定位权限。")
case .general: case .general:
return "请手动打开「设置 → 通用」。" return String(localized: "请手动打开「设置 → 通用」。")
case .wifi: case .wifi:
return "请手动打开「设置 → 无线局域网」,进入当前 Wi-Fi 的详情页。" return String(localized: "请手动打开「设置 → 无线局域网」,进入当前 Wi-Fi 的详情页。")
case .locationServices: case .locationServices:
return "请手动打开「设置 → 隐私与安全性 → 定位服务」。" return String(localized: "请手动打开「设置 → 隐私与安全性 → 定位服务」。")
} }
} }
} }
+5 -5
View File
@@ -23,7 +23,7 @@ struct TipSheetView: View {
} }
}.padding(16) }.padding(16)
} }
.navigationTitle(kind.rawValue).navigationBarTitleDisplayMode(.inline) .navigationTitle(LocalizedStringKey(kind.rawValue)).navigationBarTitleDisplayMode(.inline)
.safeAreaInset(edge: .bottom) { .safeAreaInset(edge: .bottom) {
Button { dismiss() } label: { Button { dismiss() } label: {
Text("知道了").font(.body.weight(.medium)).frame(maxWidth: .infinity).padding(.vertical, 12) Text("知道了").font(.body.weight(.medium)).frame(maxWidth: .infinity).padding(.vertical, 12)
@@ -80,7 +80,7 @@ struct ActivationTipContent: View {
} }
private func step(_ n: Int, _ title: String, _ detail: String) -> some View { private func step(_ n: Int, _ title: LocalizedStringKey, _ detail: LocalizedStringKey) -> some View {
HStack(alignment: .top, spacing: 8) { HStack(alignment: .top, spacing: 8) {
Text("\(n)").font(.caption2.bold()) Text("\(n)").font(.caption2.bold())
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
@@ -92,7 +92,7 @@ struct ActivationTipContent: View {
} }
} }
private func systemStep(_ n: Int, _ title: String, _ detail: String) -> some View { private func systemStep(_ n: Int, _ title: LocalizedStringKey, _ detail: LocalizedStringKey) -> some View {
HStack(alignment: .top, spacing: 8) { HStack(alignment: .top, spacing: 8) {
Text("\(n)").font(.caption2.bold()) Text("\(n)").font(.caption2.bold())
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
@@ -141,7 +141,7 @@ struct DeactivationTipContent: View {
} }
private func step(_ n: Int, _ title: String, _ detail: String) -> some View { private func step(_ n: Int, _ title: LocalizedStringKey, _ detail: LocalizedStringKey) -> some View {
HStack(alignment: .top, spacing: 8) { HStack(alignment: .top, spacing: 8) {
Text("\(n)").font(.caption2.bold()) Text("\(n)").font(.caption2.bold())
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
@@ -153,7 +153,7 @@ struct DeactivationTipContent: View {
} }
} }
private func systemStep(_ n: Int, _ title: String, _ detail: String) -> some View { private func systemStep(_ n: Int, _ title: LocalizedStringKey, _ detail: LocalizedStringKey) -> some View {
HStack(alignment: .top, spacing: 8) { HStack(alignment: .top, spacing: 8) {
Text("\(n)").font(.caption2.bold()) Text("\(n)").font(.caption2.bold())
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
+6
View File
@@ -0,0 +1,6 @@
/* App display name */
"CFBundleDisplayName" = "Location Spoofer";
/* Location permission prompts */
"NSLocationWhenInUseUsageDescription" = "Used to display and verify whether the simulated location is active.";
"NSLocationAlwaysAndWhenInUseUsageDescription" = "Used to display and verify whether the simulated location is active.";
+336
View File
@@ -0,0 +1,336 @@
/* ============================================================
English localization overlay.
Keys are the original Simplified-Chinese source strings
(development language = zh-Hans). Any key not found here
falls back to the Chinese key text automatically.
============================================================ */
/* MARK: - Shared models (ProxyRuntimeMode / VerificationResult) */
"APP模式" = "App Mode";
"第三方代理模式" = "Third-party Proxy Mode";
"成功" = "Success";
"已有验证正在进行" = "A verification is already in progress";
"验证已被新位置取代" = "Verification superseded by a new location";
"证书未信任" = "Certificate not trusted";
"WiFi代理未配置" = "Wi-Fi proxy not configured";
"坐标写入失败" = "Failed to write coordinate";
"改写验证失败" = "Rewrite verification failed";
/* MARK: - SettingsView */
"运行模式" = "Runtime Mode";
"模式" = "Mode";
"状态" = "Status";
"本机代理" = "Local Proxy";
"第三方模块" = "Third-party Module";
"检测连接" = "Test Connection";
"虚拟定位" = "Simulated Location";
"定位模拟" = "Location Simulation";
"运动状态模拟" = "Motion State Simulation";
"实验性功能,默认关闭。开启后会同时模拟定位响应中的运动状态。" = "Experimental, off by default. When enabled, the motion state in the location response is simulated too.";
"随机扰动" = "Random Jitter";
"开启后,下次同步坐标时会给目标点添加随机偏移(默认半径 50 米),避免位置固定在同一点。" = "When enabled, the next coordinate sync adds a random offset to the target point (default radius 50 m) so the location is not fixed at a single point.";
"说明" = "Instructions";
"工作原理" = "How It Works";
"应用" = "App";
"进入引导页" = "Open Setup Guide";
"正在检查…" = "Checking…";
"检查更新" = "Check for Updates";
"版本" = "Version";
"证书" = "Certificate";
"重置证书" = "Reset Certificate";
"仅删除 App 钥匙串中的设备 CA。iOS 中已经安装的旧证书需要在系统设置里手动移除。" = "Only removes the device CA from the app keychain. Any old certificate already installed in iOS must be removed manually in Settings.";
"支持" = "Support";
"使用帮助" = "Usage Help";
"功能建议" = "Feature Request";
"分享第三方配置" = "Share Third-party Config";
"关于" = "About";
"如果觉得好用,欢迎去 GitHub 给项目点个 Star" = "If you find it useful, please star the project on GitHub";
"致谢" = "Acknowledgements";
"核心定位改写逻辑移植自 Yu9191/wloc" = "Core location-rewrite logic ported from Yu9191/wloc";
"设置" = "Settings";
"完成" = "Done";
"代理操作失败" = "Proxy Operation Failed";
"重置证书?" = "Reset certificate?";
"重置并生成新证书" = "Reset and Generate New Certificate";
"当前虚拟定位和本地代理将停止。App 会删除钥匙串中的设备 CA、立即生成新证书,并打开安装与信任引导。你还需要前往 iOS「设置 → 通用 → VPN 与设备管理」手动删除旧证书,然后重新下载安装并完全信任新证书。" = "The current simulated location and local proxy will stop. The app deletes the device CA from the keychain, generates a new certificate immediately, and opens the install-and-trust guide. You must also go to iOS Settings → General → VPN & Device Management to manually delete the old certificate, then re-download, install, and fully trust the new one.";
"第三方代理配置" = "Third-party Proxy Configuration";
"客户端" = "Client";
"使用国内镜像下载模块" = "Use domestic mirror to download modules";
"仅影响之后复制和重新导入的模块地址;已安装模块需要重新导入后切换来源。" = "Only affects module URLs you copy or re-import afterward; installed modules must be re-imported to switch source.";
"验证状态" = "Verification Status";
"已复制模块订阅地址" = "Subscription URL copied";
"复制模块订阅地址" = "Copy module subscription URL";
"已复制解密域名" = "Decryption hostnames copied";
"复制解密域名" = "Copy decryption hostnames";
"打开 %@" = "Open %@";
"重新打开配置引导" = "Reopen Setup Guide";
"Egern 直接使用 Surge 的 .sgmodule 模块。" = "Egern uses Surge's .sgmodule module directly.";
"Stash 直接订阅 .stoverride,不要通过 Script Hub 转换。" = "Stash subscribes to .stoverride directly; do not convert it via Script Hub.";
"复制模块订阅地址后,在对应代理客户端中添加模块/重写订阅,并为复制的全部域名(含 gsp-ssl.ls.apple.com、bluedot.is.autonavi.com)启用 MITM。第三方客户端保存坐标后,即使关闭本 App,坐标仍由代理客户端持久化并继续生效。" = "After copying the subscription URL, add the module/rewrite subscription in your proxy client and enable MITM for all copied hostnames (including gsp-ssl.ls.apple.com and bluedot.is.autonavi.com). Once the third-party client saves the coordinate, it persists and stays in effect even after you close this app.";
"已是最新版本" = "You are on the latest version";
"当前版本 %@,远程最新版本 %@。" = "Current version %1$@, latest remote version %2$@.";
"检查更新失败" = "Update Check Failed";
"无法获取远程版本信息,请检查网络后重试。" = "Could not fetch remote version info. Check your network and try again.";
"未检测" = "Not checked";
"已连接,有坐标" = "Connected, coordinate present";
"已连接,无坐标" = "Connected, no coordinate";
"连接失败" = "Connection failed";
"已开启" = "On";
"已关闭" = "Off";
"第三方已保存" = "Saved by third-party";
"未保存" = "Not saved";
"未知" = "Unknown";
"App 只负责地图选点、收藏和发送 WGS-84 坐标。第三方代理客户端通过模块拦截 Apple WLOC 请求并持久化当前坐标;本模式不启动本机代理,不使用 App 的 CA,也不需要配置 127.0.0.1:8888。" = "The app only handles map selection, favorites, and sending WGS-84 coordinates. The third-party proxy client intercepts Apple WLOC requests through its module and persists the current coordinate. This mode does not start the local proxy, does not use the app's CA, and does not require configuring 127.0.0.1:8888.";
"App 在设备本地运行一个代理服务器(127.0.0.1:8888)。\n\n通过 WiFi 手动代理配置,让系统发往 Apple 定位域名(gs-loc.apple.com、gsp-ssl.ls.apple.com、bluedot.is.autonavi.com 等)的定位请求经过这个本地代理。代理使用已安装的 CA 证书对 HTTPS 流量做中间人解密,把 Apple 返回的定位坐标改写为你设置的虚拟坐标,再加密返回给系统,从而实现虚拟定位。" = "The app runs a proxy server locally on the device (127.0.0.1:8888).\n\nA manual Wi-Fi proxy routes location requests sent to Apple location hostnames (gs-loc.apple.com, gsp-ssl.ls.apple.com, bluedot.is.autonavi.com, etc.) through this local proxy. Using the installed CA certificate, the proxy performs man-in-the-middle decryption of the HTTPS traffic, rewrites the location coordinate returned by Apple to the one you set, then re-encrypts and returns it to the system — producing the simulated location.";
"模式已切换" = "Mode Switched";
"第三方代理模式检测通过。请关闭 Wi-Fi 中的 127.0.0.1:8888 手动代理,避免双重拦截。" = "Third-party proxy mode check passed. Please turn off the 127.0.0.1:8888 manual proxy in Wi-Fi to avoid double interception.";
"APP 模式环境检测通过。请停用第三方 WLOC 模块或代理连接,避免双重拦截。" = "App mode environment check passed. Please disable the third-party WLOC module or proxy connection to avoid double interception.";
"证书重置失败" = "Certificate Reset Failed";
"无法打开客户端" = "Could Not Open Client";
"无法打开 %@,请确认客户端已安装后手动打开。" = "Could not open %@. Make sure the client is installed, then open it manually.";
/* MARK: - ContentView */
"正在初始化地图与本地代理…" = "Initializing map and local proxy…";
"正在初始化地图…" = "Initializing map…";
"更新说明暂时无法加载,请前往最新 Release 页面查看。" = "Release notes could not be loaded. Please check the latest Release page.";
"需要更新" = "Update Required";
"当前版本 %@ 已停止支持,请更新到 %@ 后继续使用。\n\n%@" = "Version %1$@ is no longer supported. Please update to %2$@ to continue.\n\n%3$@";
"立即更新" = "Update Now";
"发现新版本" = "New Version Available";
"当前版本 %@,最新版本 %@。\n\n%@" = "Current version %1$@, latest version %2$@.\n\n%3$@";
"前往更新" = "Update";
"稍后" = "Later";
/* MARK: - BugReportView */
"遇到问题时,在这里生成 Bug 报告。系统会运行一次诊断测试,并将完整报告复制到剪切板。跳转到 GitHub 后,请粘贴到“App 生成的诊断报告”字段。" = "When you run into a problem, generate a bug report here. The app runs a diagnostic test and copies the full report to the clipboard. After opening GitHub, paste it into the “App-generated diagnostic report” field.";
"可复现环境" = "Reproducible";
"当前设备上问题稳定复现,非偶发性。" = "The issue reproduces reliably on this device and is not intermittent.";
"问题描述" = "Problem Description";
"正在生成报告…" = "Generating report…";
"生成 Bug 报告" = "Generate Bug Report";
"报告 Bug" = "Report a Bug";
"已生成" = "Generated";
"打开 GitHub 表单" = "Open GitHub Form";
"稍后再说" = "Later";
"Bug 报告已复制到剪切板。请在 GitHub 表单的“App 生成的诊断报告”字段中粘贴并提交。" = "The bug report has been copied to the clipboard. Paste it into the “App-generated diagnostic report” field of the GitHub form and submit.";
/* MARK: - DiagnosticsView */
"过滤日志" = "Filter logs";
"暂无运行日志" = "No runtime logs";
"无匹配日志" = "No matching logs";
"运行日志" = "Runtime Logs";
"日志自动清理,仅保留近 3 天" = "Logs are cleared automatically; only the last 3 days are kept";
"关闭" = "Close";
"复制全部日志" = "Copy all logs";
"清空全部日志" = "Clear all logs";
"清空所有运行日志?" = "Clear all runtime logs?";
"清空日志" = "Clear Logs";
"取消" = "Cancel";
"正在检测…" = "Checking…";
"环境检测" = "Environment Check";
"检查第三方模块能否拦截并响应 query 请求;不会写入测试坐标。" = "Checks whether the third-party module can intercept and respond to the query request; no test coordinates are written.";
"依次检查:本地代理 → CA 证书信任 → Wi-Fi 代理链路。" = "Checks in order: local proxy → CA certificate trust → Wi-Fi proxy chain.";
"测试日志" = "Test Log";
"已复制" = "Copied";
"代理运行中" = "Proxy running";
"代理未运行" = "Proxy stopped";
"可修改" = "Modifiable";
"不可修改" = "Not modifiable";
"环境检测通过" = "Environment check passed";
"环境检测失败: %@" = "Environment check failed: %@";
",查看下方日志" = ", see the log below";
"第三方模块连接通过,已有坐标" = "Third-party module connected, coordinate present";
"第三方模块连接通过,暂无坐标" = "Third-party module connected, no coordinate yet";
"第三方模块连接失败" = "Third-party module connection failed";
/* MARK: - TipViews */
"生效说明" = "How to Activate";
"失效说明" = "How to Deactivate";
"关闭 WiFi 代理" = "Remove Wi-Fi Proxy";
"知道了" = "Got It";
"让虚拟定位生效" = "Make the simulated location take effect";
"确认第三方代理已开启" = "Confirm the third-party proxy is on";
"保持已导入的 WLOC 模块、HTTPS 解密和第三方代理/VPN 连接开启。" = "Keep the imported WLOC module, HTTPS decryption, and the third-party proxy/VPN connection enabled.";
"开启飞行模式" = "Turn on Airplane Mode";
"从控制中心打开飞行模式(点飞机图标),Wi‑Fi 会自动断开。这是为了清除 iOS 的定位缓存。等待 2 秒。" = "Open Control Center and enable Airplane Mode (tap the airplane icon); Wi-Fi disconnects automatically. This clears the iOS location cache. Wait 2 seconds.";
"关闭 WiFi" = "Turn off Wi-Fi";
"从控制中心再点一下 Wi‑Fi 图标,确认 Wi‑Fi 已关闭。等待 2 秒。" = "Tap the Wi-Fi icon in Control Center again and confirm Wi-Fi is off. Wait 2 seconds.";
"关闭系统定位服务" = "Turn off Location Services";
"打开系统「设置 → 隐私与安全性 → 定位服务」,关闭顶部的总开关。等待 2 秒。" = "Open Settings → Privacy & Security → Location Services and turn off the master switch at the top. Wait 2 seconds.";
"打开 WiFi,启动虚拟定位" = "Turn on Wi-Fi and start the simulated location";
"从控制中心打开 Wi‑Fi(飞行模式保持开启),确认第三方代理已连接。坐标已经同步到第三方代理。等待 2 秒。" = "Turn on Wi-Fi from Control Center (keep Airplane Mode on) and confirm the third-party proxy is connected. The coordinate is already synced to the third-party proxy. Wait 2 seconds.";
"从控制中心打开 Wi‑Fi(飞行模式保持开启),进入 App 点底部「开始虚拟定位」。等待 2 秒。" = "Turn on Wi-Fi from Control Center (keep Airplane Mode on), then open the app and tap “Start Simulated Location” at the bottom. Wait 2 seconds.";
"关闭飞行模式" = "Turn off Airplane Mode";
"从控制中心关闭飞行模式。等待 2 秒。" = "Turn off Airplane Mode from Control Center. Wait 2 seconds.";
"重新开启定位服务" = "Turn Location Services back on";
"再次进入「设置 → 隐私与安全性 → 定位服务」,打开总开关。完成后打开地图验证定位是否已变化。" = "Open Settings → Privacy & Security → Location Services again and turn the master switch on. Then open a map app to verify the location has changed.";
"还是无法生效?" = "Still not working?";
"操作到第 3 步时关机重启,开机后从第 4 步继续。这样能彻底清除系统缓存的定位数据。" = "At step 3, power off and restart the device, then continue from step 4. This fully clears the system's cached location data.";
"去设置" = "Open Settings";
"取消虚拟定位" = "Cancel the simulated location";
"从控制中心打开飞行模式,Wi‑Fi 会自动断开。等待 2 秒。" = "Enable Airplane Mode from Control Center; Wi-Fi disconnects automatically. Wait 2 seconds.";
"从控制中心确认 Wi‑Fi 已关闭。等待 2 秒。" = "Confirm Wi-Fi is off from Control Center. Wait 2 seconds.";
"打开「设置 → 隐私与安全性 → 定位服务」,关闭总开关。等待 2 秒。" = "Open Settings → Privacy & Security → Location Services and turn off the master switch. Wait 2 seconds.";
"确认坐标已清除" = "Confirm the coordinate is cleared";
"App 已通知第三方代理清除虚拟坐标。保持网络可用并等待 2 秒,让系统重新获取真实定位。" = "The app has told the third-party proxy to clear the simulated coordinate. Keep the network available and wait 2 seconds so the system can reacquire the real location.";
"打开 WiFi,移除代理" = "Turn on Wi-Fi and remove the proxy";
"从控制中心打开 Wi‑Fi。然后进入「设置 → 无线局域网 → 点 WiFi 右侧 (i) → HTTP 代理」,选择「关闭」后存储。等待 2 秒。" = "Turn on Wi-Fi from Control Center. Then go to Settings → Wi-Fi → tap the (i) next to your Wi-Fi → HTTP Proxy, choose Off, and save. Wait 2 seconds.";
"再次进入「设置 → 隐私与安全性 → 定位服务」打开总开关。打开地图验证定位是否恢复。" = "Open Settings → Privacy & Security → Location Services again and turn the master switch on. Open a map app to verify the location is restored.";
"还是无法取消?" = "Still can't cancel?";
"操作到第 3 步时关机重启,开机后从第 4 步继续。" = "At step 3, power off and restart the device, then continue from step 4.";
"移除代理配置" = "Remove the proxy configuration";
"停止虚拟定位后,需要手动移除 WiFi 代理配置,否则可能无法上网。\n\n1. 打开「设置 → 无线局域网」\n2. 点击当前 WiFi 右侧 (i) 图标\n3. 找到「HTTP 代理」\n4. 选择「关闭」\n5. 点右上角「存储」" = "After stopping the simulated location, you must manually remove the Wi-Fi proxy configuration or you may lose internet access.\n\n1. Open Settings → Wi-Fi\n2. Tap the (i) icon next to your current Wi-Fi\n3. Find “HTTP Proxy”\n4. Choose “Off”\n5. Tap “Save” in the top-right corner";
/* MARK: - MapHomeView */
"社区分享成功配置?" = "Share your working config with the community?";
"去提交" = "Submit";
"复制模板" = "Copy Template";
"不再提示" = "Don't Show Again";
"第三方客户端" = "third-party client";
"你正在使用 %@。点击“去提交”会先复制投稿模板,并在 App 内打开社区页面。采纳后将收录到 README,可选择是否匿名署名。" = "You are using %@. Tapping “Submit” first copies the submission template and opens the community page inside the app. Accepted submissions are added to the README; you can choose to stay anonymous.";
"已复制投稿模板" = "Submission Template Copied";
"如果 GitHub 登录或浏览器跳转后模板没有自动填充,可以直接粘贴。" = "If the template is not filled in automatically after you sign in to GitHub or the browser switches, just paste it manually.";
"无法直接跳转" = "Cannot Open Directly";
"定位失败" = "Location Failed";
"打开设置" = "Open Settings";
"无法获取当前定位,请检查定位服务是否已开启" = "Could not get the current location. Please check whether Location Services is enabled.";
"编辑收藏名称" = "Edit Favorite Name";
"名称" = "Name";
"保存" = "Save";
"修改收藏地点名称" = "Rename the favorite location";
"搜索地点或坐标" = "Search a place or coordinates";
"日志" = "Logs";
"更多" = "More";
"当前选点" = "Current Selection";
"GCJ-02(国内)" = "GCJ-02 (Domestic)";
"WGS-84(国际)" = "WGS-84 (Intl.)";
"无法生效?" = "Not working?";
"无法取消?" = "Can't cancel?";
"已收藏,点击取消收藏" = "Favorited. Tap to remove.";
"收藏当前选点" = "Favorite the current selection";
"搜索或点击地图选点后,保存为收藏。" = "Search or tap the map to pick a point, then save it as a favorite.";
"切换到此处" = "Switch Here";
"同步到第三方代理" = "Sync to Third-party Proxy";
"检测并同步中…" = "Checking and syncing…";
"停止第三方虚拟定位" = "Stop Third-party Simulation";
"开始虚拟定位" = "Start Simulated Location";
"验证环境中…" = "Verifying environment…";
"停止虚拟定位" = "Stop Simulated Location";
"当前未连接可用的 Wi-Fi,请连接 Wi-Fi 后配置 127.0.0.1:8888 手动代理。" = "No usable Wi-Fi is connected. Connect to Wi-Fi, then configure the 127.0.0.1:8888 manual proxy.";
"第三方代理查询失败" = "Third-party proxy query failed";
"未命名" = "Unnamed";
"没有找到相关地点" = "No matching places found";
"虚拟定位已开启" = "Simulated Location On";
"不再提醒" = "Don't Remind Me";
"虚拟定位已关闭" = "Simulated Location Off";
/* MARK: - FirstSetupView */
"选择模式" = "Choose Mode";
"配置 Wi-Fi 代理" = "Configure Wi-Fi Proxy";
"初始化 CA 证书" = "Initialize CA Certificate";
"选择客户端" = "Choose Client";
"导入并检测" = "Import & Test";
"上一步" = "Back";
"开始使用" = "Get Started";
"操作失败" = "Operation Failed";
"选择运行模式" = "Choose a Runtime Mode";
"后续可在“设置 → 运行模式”中切换。两种模式不要同时拦截 WLOC 请求。" = "You can switch later in Settings → Runtime Mode. Do not intercept WLOC requests in both modes at the same time.";
"仅 Wi-Fi" = "Wi-Fi only";
"无外部依赖" = "No external deps";
"App 在设备本地启动代理,通过当前 Wi-Fi 的手动 HTTP 代理改写定位响应。免费自签应用无法使用系统 VPN 的 Network Extension 能力,因此 APP模式不支持蜂窝网络,需要配置 Wi-Fi 代理并安装 App 生成的 CA。" = "The app starts a proxy locally on the device and rewrites location responses through a manual HTTP proxy on the current Wi-Fi. Free self-signed apps cannot use the system VPN's Network Extension, so App Mode does not support cellular; it requires configuring a Wi-Fi proxy and installing the CA the app generates.";
"测试模式" = "Test mode";
"App 负责选点,并通过 WLOC 配置接口查询和同步坐标;第三方代理客户端负责网络代理、模块拦截、MITM 和持久化。证书、VPN 与代理连接均由第三方客户端处理。" = "The app handles point selection and queries/syncs coordinates through the WLOC configuration endpoint; the third-party proxy client handles network proxying, module interception, MITM, and persistence. Certificates, VPN, and proxy connections are all handled by the third-party client.";
"正在准备 APP模式本地服务…" = "Preparing App Mode local services…";
"先配置 Wi-Fi 系统代理" = "First configure the Wi-Fi system proxy";
"在当前 Wi-Fi 的详情页,将「HTTP 代理」设为「手动」:服务器填 127.0.0.1,端口填 8888。配置后点击下方「完成」。检测会自动判断是 Wi-Fi 代理还是证书信任有问题。" = "On the details page of your current Wi-Fi, set “HTTP Proxy” to “Manual”: server 127.0.0.1, port 8888. After configuring, tap “Done” below. The check automatically detects whether the Wi-Fi proxy or certificate trust is the problem.";
"Wi-Fi 代理设置" = "Wi-Fi Proxy Settings";
"1 选择手动,2 填写服务器 127.0.0.13 填写端口 8888。" = "1) Choose Manual, 2) enter server 127.0.0.1, 3) enter port 8888.";
"复制地址" = "Copy Address";
"打开 Wi-Fi 设置" = "Open Wi-Fi Settings";
"第 1 步:下载证书" = "Step 1: Download the Certificate";
"下载本机随机生成的 CA 根证书。私钥仅保存在此设备的钥匙串中,不会随证书文件导出。App 会弹出 Safari 下载页;出现配置描述文件下载提示时,选择「允许」。" = "Download the randomly generated CA root certificate for this device. The private key is stored only in this device's keychain and is never exported with the certificate file. The app opens a Safari download page; when prompted to download a configuration profile, choose “Allow”.";
"打开下载页" = "Open Download Page";
"无法准备证书下载页面,请查看诊断日志" = "Could not prepare the certificate download page. Please check the diagnostic log.";
"第 2 步:安装证书" = "Step 2: Install the Certificate";
"下载完成后打开系统「设置」。如果顶部显示「已下载描述文件」,点进去安装;否则进入「通用 → VPN 与设备管理」,找到 Location Spoofer CA 并完成安装。" = "After the download finishes, open Settings. If “Profile Downloaded” appears at the top, tap it to install; otherwise go to General → VPN & Device Management, find Location Spoofer CA, and install it.";
"去安装" = "Install";
"安装证书" = "Install Certificate";
"1 在「VPN 与设备管理」中打开 Location Spoofer CA 描述文件并完成安装。" = "1) In VPN & Device Management, open the Location Spoofer CA profile and install it.";
"第 3 步:信任证书" = "Step 3: Trust the Certificate";
"安装后进入「设置 → 通用 → 关于本机 → 证书信任设置」,找到 Location Spoofer CA 并开启完全信任。iOS 保留钥匙串数据时,重装 App 会继续复用同一证书。" = "After installing, go to Settings → General → About → Certificate Trust Settings, find Location Spoofer CA, and enable full trust. As long as iOS keeps the keychain data, reinstalling the app reuses the same certificate.";
"去信任" = "Trust";
"信任证书" = "Trust Certificate";
"1 在「证书信任设置」中为 Location Spoofer CA 开启完全信任。" = "1) In Certificate Trust Settings, enable full trust for Location Spoofer CA.";
"已完成" = "Done";
"已完成 ✓" = "Done ✓";
"iOS 27 beta 6 起,系统已禁止对 gs-loc.apple.com 进行 MITM 拦截。该版本及之后的 beta 版本暂时无法使用本项目,等待后续适配方案。" = "Starting with iOS 27 beta 6, the system blocks MITM interception of gs-loc.apple.com. This project is temporarily unusable on that and later beta versions until a workaround is available.";
"选择第三方代理客户端" = "Choose a Third-party Proxy Client";
"除 Shadowrocket 外,当前客户端配置尚未完成真机验证,页面只提供模块导入入口和通用配置提醒。" = "Except for Shadowrocket, the current client configurations have not been fully verified on real devices; this page only provides a module import entry and general configuration reminders.";
"第三方客户端适配说明" = "Third-party Client Integration Notes";
"App 不连接远程坐标服务器,而是向 Apple 域名发起一个约定请求。第三方客户端需要在本机拦截该请求、保存 WGS-84 坐标并返回 JSON;定位模块再读取同一份数据,修改 Apple WLOC 响应。" = "The app does not connect to a remote coordinate server; instead it sends an agreed-upon request to an Apple hostname. The third-party client must intercept that request locally, store the WGS-84 coordinate, and return JSON; the location module then reads the same data and modifies the Apple WLOC response.";
"配置接口" = "Configuration Endpoint";
"查询:GET ?action=query\n保存:GET ?lon=<经度>&lat=<纬度>&acc=<精度>\n清除:GET ?action=clear" = "Query: GET ?action=query\nSave: GET ?lon=<lon>&lat=<lat>&acc=<accuracy>\nClear: GET ?action=clear";
"返回格式" = "Response Format";
"适配要求" = "Integration Requirements";
"客户端需要支持请求脚本、持久化存储、HTTP 200 JSON 响应、Apple WLOC 响应脚本,以及 Apple 定位域名(gs-loc.apple.com、gsp-ssl.ls.apple.com、bluedot.is.autonavi.com 等)的 HTTPS 解密。保存接口和 WLOC 响应脚本必须读取同一份持久化数据。" = "The client must support request scripts, persistent storage, HTTP 200 JSON responses, an Apple WLOC response script, and HTTPS decryption for Apple location hostnames (gs-loc.apple.com, gsp-ssl.ls.apple.com, bluedot.is.autonavi.com, etc.). The save endpoint and the WLOC response script must read the same persistent data.";
"检测到第三方代理连接异常,请检查模块、MITM 和代理连接后重新检测。" = "A third-party proxy connection problem was detected. Check the module, MITM, and proxy connection, then test again.";
"第 1 步:导入 %@ 模块" = "Step 1: Import the %@ module";
"复制 %@ 的模块订阅地址。" = "Copy the module subscription URL for %@.";
"打开 Shadowrocket,进入“配置 → 模块”。" = "Open Shadowrocket and go to Config → Modules.";
"打开 %@。" = "Open %@.";
"进入 Shadowrocket 配置" = "Open Shadowrocket Config";
"1 点击「模块」进入模块列表,2 可打开当前本地配置详情。" = "1) Tap “Modules” to open the module list, 2) open the current local config details.";
"点击右上角“+”,粘贴模块订阅地址并导入,然后确认模块已启用。" = "Tap “+” in the top-right, paste the module subscription URL and import it, then confirm the module is enabled.";
"导入 Shadowrocket 模块" = "Import Shadowrocket Module";
"1 点击右上角加号导入模块,2 确认模块已启用。" = "1) Tap the plus in the top-right to import the module, 2) confirm the module is enabled.";
"在 %@ 中导入刚才复制的模块订阅地址。" = "Import the module subscription URL you just copied into %@.";
"第 2 步:完成 %@ 配置" = "Step 2: Finish configuring %@";
"请在 %@ 中完成相应配置。" = "Complete the corresponding configuration in %@.";
"配置时请复制下方全部解密域名(含 gsp-ssl.ls.apple.com、bluedot.is.autonavi.com)。" = "When configuring, copy all the decryption hostnames below (including gsp-ssl.ls.apple.com and bluedot.is.autonavi.com).";
"第 2 步:配置 HTTPS 解密" = "Step 2: Configure HTTPS Decryption";
"进入“配置 → 本地文件”,找到带黄点的配置,点击右侧 i 图标。" = "Go to Config → Local Files, find the config with the yellow dot, and tap the i icon on the right.";
"进入“HTTPS 解密”,开启解密开关。" = "Open “HTTPS Decryption” and turn on the decryption switch.";
"在域名列表中添加下方复制的全部解密域名。" = "Add all the decryption hostnames copied below to the hostname list.";
"配置 HTTPS 解密" = "Configure HTTPS Decryption";
"1 开启 HTTPS 解密,2 添加复制的全部解密域名,3 打开证书设置。" = "1) Turn on HTTPS Decryption, 2) add all copied decryption hostnames, 3) open certificate settings.";
"按 Shadowrocket 提示生成并完成证书授权。" = "Follow Shadowrocket's prompts to generate and complete the certificate authorization.";
"授权 Shadowrocket 证书" = "Authorize Shadowrocket Certificate";
"1 打开 Shadowrocket 证书项并按提示安装、授权。" = "1) Open the Shadowrocket certificate item and install/authorize it as prompted.";
"返回 HTTPS 解密页面,点击右上角勾号保存,然后开启代理。" = "Return to the HTTPS Decryption page, tap the checkmark in the top-right to save, then enable the proxy.";
"打开 Shadowrocket 继续配置" = "Open Shadowrocket to Continue";
"App 只能唤起 Shadowrocket,无法通过公开接口直接跳转到“模块”或“HTTPS 解密”页面。" = "The app can only launch Shadowrocket; it cannot jump directly to the “Modules” or “HTTPS Decryption” page through a public API.";
"轻点查看大图" = "Tap to view full image";
"证书尚未安装或信任" = "Certificate not installed or trusted";
"Wi-Fi 代理未正确设置" = "Wi-Fi proxy is not set up correctly";
"本地代理未能启动" = "The local proxy could not start";
"检测仍在进行" = "The check is still running";
"检测结果已过期" = "The check result is out of date";
"定位改写检测失败" = "Location-rewrite check failed";
"接口连接失败" = "Endpoint connection failed";
/* MARK: - SetupCoordinator */
"本地代理初始化失败:%@" = "Local proxy initialization failed: %@";
"✓ 定位环境正常" = "✓ Location environment OK";
"CA 证书未安装或未信任" = "CA certificate not installed or not trusted";
"Wi-Fi 代理未正确设置,请检查 127.0.0.1:8888" = "Wi-Fi proxy is not set up correctly. Please check 127.0.0.1:8888";
/* MARK: - SystemSettingsNavigator */
"请手动打开「设置」,找到本 App 后检查定位权限。" = "Please open Settings manually, find this app, and check its location permission.";
"请手动打开「设置 → 通用」。" = "Please open Settings → General manually.";
"请手动打开「设置 → 无线局域网」,进入当前 Wi-Fi 的详情页。" = "Please open Settings → Wi-Fi manually and go to the details page of your current Wi-Fi.";
"请手动打开「设置 → 隐私与安全性 → 定位服务」。" = "Please open Settings → Privacy & Security → Location Services manually.";
/* MARK: - ThirdPartyProxyManager */
"第三方代理返回了无法识别的数据" = "The third-party proxy returned unrecognizable data";
"请求未被第三方代理模块拦截,请检查模块、MITM 和代理连接" = "The request was not intercepted by the third-party proxy module. Check the module, MITM, and proxy connection.";
"第三方代理保存的坐标与当前选点不一致" = "The coordinate saved by the third-party proxy does not match the current selection";
"第三方代理请求失败:%@" = "Third-party proxy request failed: %@";
"检查模块、MITM、证书和代理/VPN连接" = "Check the module, MITM, certificate, and proxy/VPN connection";
"第三方代理拒绝保存坐标" = "The third-party proxy refused to save the coordinate";
"第三方代理清除坐标失败" = "The third-party proxy failed to clear the coordinate";
"已有第三方代理请求正在执行" = "A third-party proxy request is already running";
"配置已提供,尚未验证" = "Configuration provided, not yet verified";
/* MARK: - ProxyManager */
"证书下载地址无效" = "Invalid certificate download URL";
"启动代理失败: %@" = "Failed to start proxy: %@";
@@ -0,0 +1,6 @@
/* App display name */
"CFBundleDisplayName" = "Location Spoofer";
/* Location permission prompts */
"NSLocationWhenInUseUsageDescription" = "用于显示和验证虚拟定位是否生效";
"NSLocationAlwaysAndWhenInUseUsageDescription" = "用于显示和验证虚拟定位是否生效";
+2 -2
View File
@@ -8,8 +8,8 @@ enum ProxyRuntimeMode: String, CaseIterable, Codable, Identifiable {
var displayName: String { var displayName: String {
switch self { switch self {
case .localWiFi: return "APP模式" case .localWiFi: return String(localized: "APP模式")
case .thirdParty: return "第三方代理模式" case .thirdParty: return String(localized: "第三方代理模式")
} }
} }
} }
+11 -11
View File
@@ -25,25 +25,25 @@ enum ThirdPartyProxyError: LocalizedError, Equatable {
var errorDescription: String? { var errorDescription: String? {
switch self { switch self {
case .invalidResponse: case .invalidResponse:
return "第三方代理返回了无法识别的数据" return String(localized: "第三方代理返回了无法识别的数据")
case .moduleNotIntercepted: case .moduleNotIntercepted:
return "请求未被第三方代理模块拦截,请检查模块、MITM 和代理连接" return String(localized: "请求未被第三方代理模块拦截,请检查模块、MITM 和代理连接")
case .rejected(let message): case .rejected(let message):
return message return message
case .coordinateMismatch: case .coordinateMismatch:
return "第三方代理保存的坐标与当前选点不一致" return String(localized: "第三方代理保存的坐标与当前选点不一致")
case .network(let message): case .network(let message):
return "第三方代理请求失败:\(message)" return String(localized: "第三方代理请求失败:\(message)")
} }
} }
var recoverySuggestion: String { var recoverySuggestion: String {
return "检查模块、MITM、证书和代理/VPN连接" return String(localized: "检查模块、MITM、证书和代理/VPN连接")
} }
static func recoverySuggestion(for error: Error) -> String { static func recoverySuggestion(for error: Error) -> String {
(error as? Self)?.recoverySuggestion (error as? Self)?.recoverySuggestion
?? "检查模块、MITM、证书和代理/VPN连接" ?? String(localized: "检查模块、MITM、证书和代理/VPN连接")
} }
} }
@@ -106,7 +106,7 @@ final class ThirdPartyProxyManager: ObservableObject {
randomRadius: RandomRadiusStore.shared.isEnabled ? RandomRadiusStore.shared.radius : 0 randomRadius: RandomRadiusStore.shared.isEnabled ? RandomRadiusStore.shared.radius : 0
)) ))
guard response.success else { guard response.success else {
throw ThirdPartyProxyError.rejected(response.error ?? "第三方代理拒绝保存坐标") throw ThirdPartyProxyError.rejected(response.error ?? String(localized: "第三方代理拒绝保存坐标"))
} }
guard let latitude = response.latitude, guard let latitude = response.latitude,
let longitude = response.longitude, let longitude = response.longitude,
@@ -127,7 +127,7 @@ final class ThirdPartyProxyManager: ObservableObject {
func clear() async throws { func clear() async throws {
let response = try await perform(action: .clear) let response = try await perform(action: .clear)
guard response.success else { guard response.success else {
throw ThirdPartyProxyError.rejected(response.error ?? "第三方代理清除坐标失败") throw ThirdPartyProxyError.rejected(response.error ?? String(localized: "第三方代理清除坐标失败"))
} }
activeSettings = nil activeSettings = nil
connectionState = .connected(active: false) connectionState = .connected(active: false)
@@ -143,7 +143,7 @@ final class ThirdPartyProxyManager: ObservableObject {
if response.error?.contains("无已保存") == true { if response.error?.contains("无已保存") == true {
return false return false
} }
throw ThirdPartyProxyError.rejected(response.error ?? "第三方代理查询失败") throw ThirdPartyProxyError.rejected(response.error ?? String(localized: "第三方代理查询失败"))
} }
private enum Action { private enum Action {
@@ -154,7 +154,7 @@ final class ThirdPartyProxyManager: ObservableObject {
private func perform(action: Action) async throws -> ThirdPartyProxySettingsResponse { private func perform(action: Action) async throws -> ThirdPartyProxySettingsResponse {
guard !isRequesting else { guard !isRequesting else {
throw ThirdPartyProxyError.rejected("已有第三方代理请求正在执行") throw ThirdPartyProxyError.rejected(String(localized: "已有第三方代理请求正在执行"))
} }
isRequesting = true isRequesting = true
defer { isRequesting = false } defer { isRequesting = false }
@@ -224,7 +224,7 @@ enum ThirdPartyProxyClient: String, CaseIterable, Identifiable {
} }
var verificationText: String? { var verificationText: String? {
self == .shadowrocket ? nil : "配置已提供,尚未验证" self == .shadowrocket ? nil : String(localized: "配置已提供,尚未验证")
} }
var moduleFileName: String { var moduleFileName: String {
+14
View File
@@ -26,4 +26,18 @@ enum VerificationResult: Equatable, Identifiable {
var isSuccess: Bool { self == .success } var isSuccess: Bool { self == .success }
/// Localized, user-facing title. `id` stays stable for routing/equality.
var localizedTitle: String {
switch self {
case .success: return String(localized: "成功")
case .proxyNotRunning: return String(localized: "代理未运行")
case .verificationInProgress: return String(localized: "已有验证正在进行")
case .verificationSuperseded: return String(localized: "验证已被新位置取代")
case .certNotTrusted: return String(localized: "证书未信任")
case .wifiProxyNotConfigured: return String(localized: "WiFi代理未配置")
case .coordinateWriteFailed: return String(localized: "坐标写入失败")
case .patchFailed: return String(localized: "改写验证失败")
}
}
} }
+6
View File
@@ -4,6 +4,12 @@ options:
deploymentTarget: deploymentTarget:
iOS: "15.0" iOS: "15.0"
createIntermediateGroups: true createIntermediateGroups: true
developmentLanguage: zh-Hans
knownRegions:
- zh-Hans
- en
- Base
settings: settings:
base: base: