merge: 合并 PR #11 英文本地化

This commit is contained in:
xweiba
2026-09-11 02:39:10 +08:00
50 changed files with 2495 additions and 277 deletions
+33 -19
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)
@@ -95,9 +95,10 @@ struct BugReportView: 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
testLog = "第三方代理测试模式:模块连接成功;已保存坐标=\(active ? "" : "")" let savedCoordinate = active ? String(localized: "") : String(localized: "")
testLog = String(localized: "第三方代理测试模式:模块连接成功;已保存坐标=\(savedCoordinate)")
} catch { } catch {
testLog = "第三方代理测试模式:模块连接失败;\(error.localizedDescription)" testLog = String(localized: "第三方代理测试模式:模块连接失败;\(error.localizedDescription)")
} }
} else { } else {
_ = await setup.runVerificationTest() _ = await setup.runVerificationTest()
@@ -113,22 +114,11 @@ struct BugReportView: View {
let systemVersion = UIDevice.current.systemVersion let systemVersion = UIDevice.current.systemVersion
// //
let report = """ let report = bugReport(
### 环境信息 appVersion: appVersion,
App 版本: \(appVersion) systemVersion: systemVersion,
系统版本: iOS \(systemVersion) testLog: testLog
运行模式: \(runtimeMode.mode.displayName) )
第三方客户端: \(runtimeMode.mode == .thirdParty ? thirdPartyClient.selectedClient.name : "不适用")
可复现环境: \(isReproducible ? "" : "")
### 问题描述
\(description.trimmingCharacters(in: .whitespacesAndNewlines))
### 诊断日志
```
\(testLog.isEmpty ? "(无诊断数据)" : testLog)
```
"""
// //
UIPasteboard.general.string = report UIPasteboard.general.string = report
@@ -138,4 +128,28 @@ struct BugReportView: View {
showCopiedAlert = true showCopiedAlert = true
} }
} }
private func bugReport(appVersion: String, systemVersion: String, testLog: String) -> String {
let client = runtimeMode.mode == .thirdParty
? thirdPartyClient.selectedClient.name
: String(localized: "不适用")
let reproducible = isReproducible ? String(localized: "") : String(localized: "")
let diagnostics = testLog.isEmpty ? String(localized: "(无诊断数据)") : testLog
return """
### \(String(localized: "环境信息"))
\(String(localized: "App 版本")): \(appVersion)
\(String(localized: "系统版本")): iOS \(systemVersion)
\(String(localized: "运行模式")): \(runtimeMode.mode.displayName)
\(String(localized: "第三方客户端")): \(client)
\(String(localized: "可复现环境")): \(reproducible)
### \(String(localized: "问题描述"))
\(description.trimmingCharacters(in: .whitespacesAndNewlines))
### \(String(localized: "诊断日志"))
```
\(diagnostics)
```
"""
}
} }
+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(
+42 -29
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?
@@ -22,7 +23,10 @@ struct RuntimeLogsView: View {
private var filteredEntries: [RuntimeLogEntry] { private var filteredEntries: [RuntimeLogEntry] {
let q = logFilter.trimmingCharacters(in: .whitespacesAndNewlines) let q = logFilter.trimmingCharacters(in: .whitespacesAndNewlines)
guard !q.isEmpty else { return entries } guard !q.isEmpty else { return entries }
return entries.filter { $0.message.localizedCaseInsensitiveContains(q) } return entries.filter {
$0.localizedMessage.localizedCaseInsensitiveContains(q)
|| $0.localizedCategory.localizedCaseInsensitiveContains(q)
}
} }
var body: some View { var body: some View {
@@ -42,7 +46,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 +107,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 +123,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 +138,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 +187,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))
} }
@@ -197,7 +204,7 @@ struct RuntimeLogsView: View {
.foregroundStyle(entry.level == .error ? .red : entry.level == .warning ? .orange : .blue).frame(width: 18) .foregroundStyle(entry.level == .error ? .red : entry.level == .warning ? .orange : .blue).frame(width: 18)
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
HStack { HStack {
Text("\(entry.source) \(entry.category)").font(.caption.weight(.semibold)) Text("\(entry.source) \(entry.localizedCategory)").font(.caption.weight(.semibold))
Spacer() Spacer()
Button { Button {
UIPasteboard.general.string = entry.renderedText UIPasteboard.general.string = entry.renderedText
@@ -221,9 +228,9 @@ struct RuntimeLogsView: View {
} }
.buttonStyle(.plain) .buttonStyle(.plain)
} }
Text(entry.message).font(.caption.monospaced()).textSelection(.enabled) Text(entry.localizedMessage).font(.caption.monospaced()).textSelection(.enabled)
if !entry.details.isEmpty { if !entry.details.isEmpty {
Text(entry.details.sorted(by: { $0.key < $1.key }).map { "\($0.key): \($0.value)" }.joined(separator: "\n")) Text(entry.localizedDetailsText)
.font(.caption2.monospaced()).foregroundStyle(.secondary).textSelection(.enabled) .font(.caption2.monospaced()).foregroundStyle(.secondary).textSelection(.enabled)
} }
} }
@@ -240,22 +247,28 @@ 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
testMessage = """ testResult = active ? String(localized: "第三方模块连接通过,已有坐标") : String(localized: "第三方模块连接通过,暂无坐标")
======== 第三方代理连接检测 ======== testMessage = thirdPartyTestLog(active: active)
模式: 测试模式
请求: wloc-settings/save?action=query
拦截响应: 有效 JSON
已保存坐标: \(active ? "" : "")
"""
} catch { } catch {
testResult = "第三方模块连接失败" testSucceeded = false
testMessage = """ testResult = String(localized: "第三方模块连接失败")
======== 第三方代理连接检测 ======== testMessage = thirdPartyTestLog(error: error)
模式: 测试模式
请求: wloc-settings/save?action=query
结果: \(error.localizedDescription)
"""
} }
} }
private func thirdPartyTestLog(active: Bool? = nil, error: Error? = nil) -> String {
var lines = [
String(localized: "======== 第三方代理连接检测 ========"),
String(localized: "模式: 测试模式"),
String(localized: "请求: wloc-settings/save?action=query")
]
if let active {
lines.append(String(localized: "拦截响应: 有效 JSON"))
lines.append(String(localized: "已保存坐标: \(active ? String(localized: "") : String(localized: ""))"))
} else if let error {
lines.append(String(localized: "结果: \(error.localizedDescription)"))
}
return lines.joined(separator: "\n")
}
} }
+62 -62
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)
@@ -220,15 +220,15 @@ struct FirstSetupView: View {
return thirdPartyTestFailure.message return thirdPartyTestFailure.message
} }
guard !setup.message.isEmpty else { return nil } guard !setup.message.isEmpty else { return nil }
return """ return [
======== 第三方代理运行检测 ======== String(localized: "======== 第三方代理运行检测 ========"),
当前客户端:\(thirdPartyClient.selectedClient.name) String(localized: "当前客户端:\(thirdPartyClient.selectedClient.name)"),
触发来源:地图或设置中的第三方代理操作 String(localized: "触发来源:地图或设置中的第三方代理操作"),
请求动作:WLOC 配置接口 String(localized: "请求动作:WLOC 配置接口"),
检测结果:失败 String(localized: "检测结果:失败"),
错误详情:\(setup.message) String(localized: "错误详情:\(setup.message)"),
处理建议:确认模块已启用,并检查 MITM、证书和代理/VPN 连接。 String(localized: "处理建议:确认模块已启用,并检查 MITM、证书和代理/VPN 连接。")
""" ].joined(separator: "\n")
} }
private var modeStep: some View { private var modeStep: some View {
@@ -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: "无法准备证书下载页面,请查看诊断日志")
} }
} }
}, },
@@ -468,10 +468,10 @@ struct FirstSetupView: View {
Text("返回格式") Text("返回格式")
.font(.subheadline.bold()) .font(.subheadline.bold())
Text(""" Text(String(localized: """
成功:{"success":true,"longitude":113.0,"latitude":22.0,"accuracy":25} 成功:{"success":true,"longitude":113.0,"latitude":22.0,"accuracy":25}
失败:{"success":false,"error":""} 失败:{"success":false,"error":""}
""") """))
.font(.caption.monospaced()) .font(.caption.monospaced())
.textSelection(.enabled) .textSelection(.enabled)
@@ -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())
@@ -658,7 +657,7 @@ struct FirstSetupView: View {
.clipShape(RoundedRectangle(cornerRadius: 6)) .clipShape(RoundedRectangle(cornerRadius: 6))
HStack(spacing: 6) { HStack(spacing: 6) {
Image(systemName: "arrow.up.left.and.arrow.down.right") Image(systemName: "arrow.up.left.and.arrow.down.right")
Text(caption) Text(LocalizedStringKey(caption))
} }
.font(.caption2) .font(.caption2)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
@@ -673,7 +672,11 @@ struct FirstSetupView: View {
) )
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.accessibilityLabel("\(title)\(caption)") .accessibilityLabel(
Text(LocalizedStringKey(title))
+ Text(": ")
+ Text(LocalizedStringKey(caption))
)
.accessibilityHint("轻点查看大图") .accessibilityHint("轻点查看大图")
} }
} }
@@ -683,7 +686,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 +708,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 +731,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 +748,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
) )
} }
@@ -874,21 +876,19 @@ struct FirstSetupView: View {
"处理建议": ThirdPartyProxyError.recoverySuggestion(for: error) "处理建议": ThirdPartyProxyError.recoverySuggestion(for: error)
] ]
) )
thirdPartyTestFailure = ThirdPartyConnectionTestFailure( thirdPartyTestFailure = ThirdPartyConnectionTestFailure(message: [
message: """ String(localized: "======== 第三方代理连接检测 ========"),
======== 第三方代理连接检测 ======== String(localized: "当前客户端:\(client.name)"),
当前客户端:\(client.name) String(localized: "配置接口:/wloc-settings/save"),
配置接口:/wloc-settings/save String(localized: "请求动作:WLOC query"),
请求动作:WLOC query String(localized: "检查范围:模块拦截、MITM、证书、代理/VPN 连接"),
检查范围:模块拦截、MITM、证书、代理/VPN 连接 String(localized: "连接状态:\(connectionState)"),
连接状态:\(connectionState) String(localized: "检测结果:失败"),
检测结果:失败 String(localized: "耗时:\(elapsedMilliseconds) ms"),
耗时\(elapsedMilliseconds) ms String(localized: "错误类型\(errorType)"),
错误类型\(errorType) String(localized: "错误详情\(error.localizedDescription)"),
错误详情:\(error.localizedDescription) String(localized: "处理建议:\(ThirdPartyProxyError.recoverySuggestion(for: error))")
处理建议:\(ThirdPartyProxyError.recoverySuggestion(for: error)) ].joined(separator: "\n"))
"""
)
showsThirdPartyFailureLog = true showsThirdPartyFailureLog = true
} }
} }
@@ -897,15 +897,15 @@ struct FirstSetupView: View {
private var thirdPartyConnectionStateDescription: String { private var thirdPartyConnectionStateDescription: String {
switch thirdPartyProxy.connectionState { switch thirdPartyProxy.connectionState {
case .unknown: case .unknown:
return "未检测" return String(localized: "未检测")
case .connected(let active): case .connected(let active):
return active ? "已连接,有保存坐标" : "已连接,无保存坐标" return active ? String(localized: "已连接,有保存坐标") : String(localized: "已连接,无保存坐标")
case .failed(let message): case .failed(let message):
return "连接失败(\(message)" return String(localized: "连接失败(\(message)")
} }
} }
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 +954,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: "环境检测通过")
} }
} }
+21 -18
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: "停止虚拟定位")
} }
} }
@@ -766,7 +766,8 @@ struct MapHomeView: View {
private func presentSuccessfulOperationTip(_ kind: VirtualLocationTipKind) { private func presentSuccessfulOperationTip(_ kind: VirtualLocationTipKind) {
let count = tipPreferences.recordSuccessfulOperation(kind) let count = tipPreferences.recordSuccessfulOperation(kind)
let operationName = kind == .activation ? "开启" : "关闭" let operationName = kind == .activation ? "开启" : "关闭"
RuntimeLogger.info("APP", "提醒", "累计\(operationName)虚拟定位次数", details: [ RuntimeLogger.info("APP", "提醒", "累计虚拟定位操作次数", details: [
"操作": operationName,
"次数": String(count), "次数": String(count),
"运行模式": runtimeMode.mode.displayName, "运行模式": runtimeMode.mode.displayName,
"可显示不再提醒": String(tipPreferences.canSuppress(kind)) "可显示不再提醒": String(tipPreferences.canSuppress(kind))
@@ -856,7 +857,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 +1092,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 +1145,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
} }
@@ -1335,7 +1336,9 @@ struct MapHomeView: View {
context.showFailureAlert, context.showFailureAlert,
!Task.isCancelled, !Task.isCancelled,
mapState.selection.revision == context.intent.selectionRevision else { return } mapState.selection.revision == context.intent.selectionRevision else { return }
RuntimeLogger.info("APP", "地图", "定位失败 status=\(realtime.authorizationStatus.rawValue)") RuntimeLogger.info("APP", "地图", "定位失败", details: [
"授权状态rawValue": String(realtime.authorizationStatus.rawValue)
])
showLocationAlert = true showLocationAlert = true
return return
} }
@@ -1517,7 +1520,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 +1532,7 @@ struct MapHomeView: View {
]) ])
return r return r
} }
if searchResults.isEmpty { searchError = "没有找到相关地点" } if searchResults.isEmpty { searchError = String(localized: "没有找到相关地点") }
} }
} }
} }
+3 -3
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
} }
@@ -182,5 +182,5 @@ struct ProxyCoordinateSnapshot: Equatable {
enum ProxyError: LocalizedError { enum ProxyError: LocalizedError {
case startFailed case startFailed
var errorDescription: String? { "Go proxy 启动失败" } var errorDescription: String? { String(localized: "Go proxy 启动失败") }
} }
+28 -28
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
@@ -222,11 +222,11 @@ struct SettingsView: View {
Section("致谢") { Section("致谢") {
Button { Button {
if let url = URL(string: "https://github.com/Yu9191/wloc") { if let url = URL(string: "https://github.com/xweiba/location-spoofer/blob/main/docs/THIRD_PARTY_MODULES.md") {
UIApplication.shared.open(url) UIApplication.shared.open(url)
} }
} label: { } label: {
Label("核心定位改写逻辑移植自 Yu9191/wloc", systemImage: "heart.fill") Label("定位改写脚本源自 Yu9191/wloc,现由本项目维护", systemImage: "heart.fill")
.foregroundStyle(.pink) .foregroundStyle(.pink)
} }
} }
@@ -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),请确认客户端已安装后手动打开。")
} }
} }
} }
+24 -24
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")
} }
} }
@@ -108,36 +108,36 @@ final class SetupCoordinator: ObservableObject {
testLog = "" testLog = ""
let log = { (msg: String) in self.testLog += msg + "\n" } let log = { (msg: String) in self.testLog += msg + "\n" }
log("======== 代理验证测试 ========") log(String(localized: "======== 代理验证测试 ========"))
log("App 版本: \(appVersion)") log(String(localized: "App 版本: \(appVersion)"))
log("系统版本: iOS \(UIDevice.current.systemVersion)") log(String(localized: "系统版本: iOS \(UIDevice.current.systemVersion)"))
log("") log("")
// Step A: Proxy running // Step A: Proxy running
log("[步骤 A] 检查代理是否运行…") log(String(localized: "[步骤 A] 检查代理是否运行…"))
log(" 端口: 127.0.0.1:8888") log(String(localized: " 端口: 127.0.0.1:8888"))
let stepAStart = Date() let stepAStart = Date()
if !proxy.isRunning { if !proxy.isRunning {
log(" ⚠ 代理未运行,尝试启动…") log(String(localized: " ⚠ 代理未运行,尝试启动…"))
do { try await proxy.start() } catch { do { try await proxy.start() } catch {
log(" ✗ 启动失败: \(error.localizedDescription)") log(String(localized: " ✗ 启动失败: \(error.localizedDescription)"))
return .proxyNotRunning return .proxyNotRunning
} }
log(" ✓ 代理启动成功") log(String(localized: " ✓ 代理启动成功"))
} else { } else {
log(" ✓ 代理已在运行中") log(String(localized: " ✓ 代理已在运行中"))
} }
collectProxyLogs(since: stepAStart, to: log) collectProxyLogs(since: stepAStart, to: log)
// Step B: Combined CA + WiFi proxy check (single request) // Step B: Combined CA + WiFi proxy check (single request)
log("") log("")
log("[步骤 B] 检测证书与 WiFi 代理…") log(String(localized: "[步骤 B] 检测证书与 WiFi 代理…"))
log(" 方式: 请求 baidu.com/paopao-verify-<token>") log(String(localized: " 方式: 请求 baidu.com/paopao-verify-<token>"))
log(" 结果判定: TLS 错误=证书问题 / 响应不匹配=代理未配置 / 匹配=通过") log(String(localized: " 结果判定: TLS 错误=证书问题 / 响应不匹配=代理未配置 / 匹配=通过"))
let stepBStart = Date() let stepBStart = Date()
let verifyToken = CoreBridge.refreshVerifyToken() let verifyToken = CoreBridge.refreshVerifyToken()
guard !verifyToken.isEmpty else { guard !verifyToken.isEmpty else {
log(" ✗ 无法生成验证 token") log(String(localized: " ✗ 无法生成验证 token"))
return .certNotTrusted return .certNotTrusted
} }
do { do {
@@ -150,17 +150,17 @@ final class SetupCoordinator: ObservableObject {
let statusCode = (resp as? HTTPURLResponse)?.statusCode ?? 0 let statusCode = (resp as? HTTPURLResponse)?.statusCode ?? 0
let body = String(data: data, encoding: .utf8) ?? "" let body = String(data: data, encoding: .utf8) ?? ""
if body == verifyToken { if body == verifyToken {
log(" ✓ 证书已信任,WiFi 代理已配置") log(String(localized: " ✓ 证书已信任,WiFi 代理已配置"))
} else { } else {
log(" ✗ 响应不匹配: HTTP \(statusCode), \(data.count) bytesWiFi 代理未配置") log(String(localized: " ✗ 响应不匹配: HTTP \(statusCode), \(data.count) bytesWiFi 代理未配置"))
return .wifiProxyNotConfigured return .wifiProxyNotConfigured
} }
} catch { } catch {
let ns = error as NSError let ns = error as NSError
let msg = error.localizedDescription let msg = error.localizedDescription
log(" ✗ 请求失败 [\(ns.domain) code=\(ns.code)]: \(msg)") log(String(localized: " ✗ 请求失败 [\(ns.domain) code=\(ns.code)]: \(msg)"))
if isCertificateTrustError(nsError: ns, message: msg) { if isCertificateTrustError(nsError: ns, message: msg) {
log(" TLS/证书校验失败,CA 证书未信任") log(String(localized: " TLS/证书校验失败,CA 证书未信任"))
return .certNotTrusted return .certNotTrusted
} }
return .wifiProxyNotConfigured return .wifiProxyNotConfigured
@@ -168,7 +168,7 @@ final class SetupCoordinator: ObservableObject {
collectProxyLogs(since: stepBStart, to: log) collectProxyLogs(since: stepBStart, to: log)
log("") log("")
log("======== 环境检测通过 ✓ ========") log(String(localized: "======== 环境检测通过 ✓ ========"))
return .success return .success
} }
@@ -202,9 +202,9 @@ final class SetupCoordinator: ObservableObject {
$0.source == "CORE" && $0.category == "Proxy" && $0.timestamp >= date $0.source == "CORE" && $0.category == "Proxy" && $0.timestamp >= date
} }
guard !proxyEntries.isEmpty else { return } guard !proxyEntries.isEmpty else { return }
log(" --- 代理日志 ---") log(String(localized: " --- 代理日志 ---"))
for e in proxyEntries { for e in proxyEntries {
log(" " + e.message) log(" " + e.localizedMessage)
} }
} }
} }
+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)
+5 -4
View File
@@ -229,10 +229,10 @@ Current client status:
Community configurations are reviewed by client. Accepted submissions are linked in this table with attribution unless Community configurations are reviewed by client. Accepted submissions are linked in this table with attribution unless
the contributor requests anonymous inclusion. the contributor requests anonymous inclusion.
Module snapshots and provenance: Modules and scripts are now hosted by this repository. The original
`Yu9191/wloc` repository was deleted and remains attribution only, not a runtime dependency:
- [Third-party module documentation](docs/THIRD_PARTY_MODULES.md) - [Third-party module documentation](docs/THIRD_PARTY_MODULES.md)
- [Yu9191/wloc](https://github.com/Yu9191/wloc)
The selected client owns its certificates, MITM configuration, and proxy switches. Review third-party modules and The selected client owns its certificates, MITM configuration, and proxy switches. Review third-party modules and
scripts before importing them. scripts before importing them.
@@ -364,7 +364,7 @@ Deploy it to a test device using your own signing and installation process.
- Runtime logs remain in the device App Group container and retain only the latest three days; - Runtime logs remain in the device App Group container and retain only the latest three days;
- Issue reports are copied by the user before being submitted to GitHub; - Issue reports are copied by the user before being submitted to GitHub;
- App Mode accesses the local proxy and the environment-verification URL; - App Mode accesses the local proxy and the environment-verification URL;
- Third-party Proxy Mode may access the upstream module URL and the WLOC configuration endpoint; - Third-party Proxy Mode may access module URLs hosted by this repository and the WLOC configuration endpoint;
- The CA private key generated by the app is stored in the device Keychain; - The CA private key generated by the app is stored in the device Keychain;
- Third-party MITM, certificates, and proxy behavior are owned by the selected client. - Third-party MITM, certificates, and proxy behavior are owned by the selected client.
@@ -443,7 +443,8 @@ guarantee for every app or release.
## Acknowledgements and Links ## Acknowledgements and Links
The core location-response handling approach, Go implementation, and third-party modules are based on: The core location-response handling approach, Go implementation, and third-party modules are based on
the following projects (`Yu9191/wloc` has since been deleted):
- [Yu9191/wloc](https://github.com/Yu9191/wloc) - [Yu9191/wloc](https://github.com/Yu9191/wloc)
- [ios-location-spoofer](https://github.com/mekos2772/ios-location-spoofer) - [ios-location-spoofer](https://github.com/mekos2772/ios-location-spoofer)
+3 -4
View File
@@ -219,10 +219,9 @@ App 只验证配置接口的 HTTP 状态、JSON 格式和坐标回读,不管
社区配置按客户端分区审核;采纳后会在上表链接教程和投稿者,投稿者也可以选择匿名收录。 社区配置按客户端分区审核;采纳后会在上表链接教程和投稿者,投稿者也可以选择匿名收录。
相关模块快照和来源记录 模块与脚本现由本仓库托管,原 `Yu9191/wloc` 仓库已删除,仅保留来源致谢,不再作为运行时依赖
- [第三方模块说明](docs/THIRD_PARTY_MODULES.md) - [第三方模块说明](docs/THIRD_PARTY_MODULES.md)
- [Yu9191/wloc](https://github.com/Yu9191/wloc)
第三方客户端、证书、MITM 和代理开关由客户端自身负责。导入任何第三方模块前,请先审查其配置和脚本内容。 第三方客户端、证书、MITM 和代理开关由客户端自身负责。导入任何第三方模块前,请先审查其配置和脚本内容。
@@ -351,7 +350,7 @@ dist/PaopaoLocationSpoofer-unsigned.ipa
- 运行日志保存在设备 App Group 容器中,并自动保留近三天; - 运行日志保存在设备 App Group 容器中,并自动保留近三天;
- 问题报告需要用户主动复制后提交到 GitHub; - 问题报告需要用户主动复制后提交到 GitHub;
- APP 模式会访问本机代理和环境验证地址; - APP 模式会访问本机代理和环境验证地址;
- 第三方代理模式可能访问上游模块地址和 WLOC 配置接口; - 第三方代理模式可能访问本仓库托管的模块地址和 WLOC 配置接口;
- App 生成的 CA 私钥保存在设备 Keychain 中; - App 生成的 CA 私钥保存在设备 Keychain 中;
- 第三方客户端模块、MITM 和证书链路由用户选择的客户端负责。 - 第三方客户端模块、MITM 和证书链路由用户选择的客户端负责。
@@ -430,7 +429,7 @@ GitHub Issue Form 中的“App 生成的诊断报告”字段与 App 复制内
## 致谢与友链 ## 致谢与友链
核心定位响应处理思路、Go 实现和第三方模块参考自: 核心定位响应处理思路、Go 实现和第三方模块参考自`Yu9191/wloc` 原仓库已删除)
- [Yu9191/wloc](https://github.com/Yu9191/wloc) - [Yu9191/wloc](https://github.com/Yu9191/wloc)
- [ios-location-spoofer](https://github.com/mekos2772/ios-location-spoofer) - [ios-location-spoofer](https://github.com/mekos2772/ios-location-spoofer)
@@ -0,0 +1,11 @@
#!name=Apple WLOC 定位修改
#!desc=修改 Apple 网络定位返回坐标 | 快捷指令(推荐): 设置地理位置 https://www.icloud.com/shortcuts/a82717d8fdad4e6280866fcf911173f7 清理恢复位置 https://www.icloud.com/shortcuts/f42632d406504f24a2cd163af4fe012f | 选点页面: https://wloc-pages.pages.dev/
#!author=xweiba
#!homepage=https://github.com/xweiba/location-spoofer
[rewrite_local]
^https?:\/\/(?:gs-loc(?:-cn)?\.apple\.com|gsp-ssl\.ls\.apple\.com|bluedot\.is\.autonavi\.com(?:\.gds\.alibabadns\.com)?)\/clls\/wloc url script-response-body https://gh-proxy.org/https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc.js?v=1.0.7
^https?:\/\/gs-loc(-cn)?\.apple\.com\/wloc-settings\/(save|version) url script-echo-response https://gh-proxy.org/https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc-settings.js?v=1.0.7
[mitm]
hostname = gs-loc.apple.com, gs-loc-cn.apple.com, gsp-ssl.ls.apple.com, bluedot.is.autonavi.com, bluedot.is.autonavi.com.gds.alibabadns.com
+19
View File
@@ -0,0 +1,19 @@
#!name=Apple WLOC 定位修改
#!desc=修改 Apple 网络定位返回坐标 | 快捷指令(推荐): 设置地理位置 https://www.icloud.com/shortcuts/a82717d8fdad4e6280866fcf911173f7 清理恢复位置 https://www.icloud.com/shortcuts/f42632d406504f24a2cd163af4fe012f
#!author=xweiba
#!homepage=https://github.com/xweiba/location-spoofer
#!openUrl=https://wloc-pages.pages.dev/
[Argument]
longitude = input, "113.94114", tag=经度(在线选点优先)
latitude = input, "22.544577", tag=纬度(在线选点优先)
accuracy = input, "25", tag=精度(米)
randomRadius = input, "0", tag=扰动半径(米,0为关闭)
logLevel = select, "info", "off", "error", "warn", "debug", "all", tag=日志级别
[Script]
http-response ^https?:\/\/(?:gs-loc(?:-cn)?\.apple\.com|gsp-ssl\.ls\.apple\.com|bluedot\.is\.autonavi\.com(?:\.gds\.alibabadns\.com)?)\/clls\/wloc script-path=https://gh-proxy.org/https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc.js?v=1.0.7, requires-body=true, binary-body-mode=true, timeout=30, tag=Apple WLOC, argument=[{longitude},{latitude},{accuracy},{randomRadius},{logLevel}]
http-request ^https?:\/\/gs-loc(-cn)?\.apple\.com\/wloc-settings\/(save|version) script-path=https://gh-proxy.org/https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc-settings.js?v=1.0.7, timeout=10, tag=WLOC Settings
[MITM]
hostname = gs-loc.apple.com, gs-loc-cn.apple.com, gsp-ssl.ls.apple.com, bluedot.is.autonavi.com, bluedot.is.autonavi.com.gds.alibabadns.com
@@ -0,0 +1,13 @@
#!name=Apple WLOC 定位修改
#!desc=修改 Apple 网络定位返回坐标 (Shadowrocket 小火箭) | 快捷指令(推荐): 设置地理位置 https://www.icloud.com/shortcuts/a82717d8fdad4e6280866fcf911173f7 清理恢复位置 https://www.icloud.com/shortcuts/f42632d406504f24a2cd163af4fe012f | 选点页面: https://wloc-pages.pages.dev/
#!author=Yu9191 Rewrite
#!homepage=https://github.com/xweiba/location-spoofer
#!icon=https://raw.githubusercontent.com/xweiba/location-spoofer/main/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon.png
#!category=Tools
[Script]
Apple WLOC = type=http-response,pattern=^https?:\/\/(?:gs-loc(?:-cn)?\.apple\.com|gsp-ssl\.ls\.apple\.com|bluedot\.is\.autonavi\.com(?:\.gds\.alibabadns\.com)?)\/clls\/wloc,requires-body=1,binary-body-mode=1,max-size=0,timeout=30,script-path=https://gh-proxy.org/https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc.js?v=1.0.7,argument=longitude=113.94114&latitude=22.544577&accuracy=25&randomRadius=0&logLevel=info
WLOC Settings = type=http-request,pattern=^https?:\/\/gs-loc(-cn)?\.apple\.com\/wloc-settings\/(save|version),requires-body=0,max-size=0,timeout=10,script-path=https://gh-proxy.org/https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc-settings.js?v=1.0.7
[MITM]
hostname = %APPEND% gs-loc.apple.com, gs-loc-cn.apple.com, gsp-ssl.ls.apple.com, bluedot.is.autonavi.com, bluedot.is.autonavi.com.gds.alibabadns.com
@@ -0,0 +1,14 @@
#!name=Apple WLOC 定位修改
#!desc=修改 Apple 网络定位返回坐标 | 快捷指令(推荐): 设置地理位置 https://www.icloud.com/shortcuts/a82717d8fdad4e6280866fcf911173f7 清理恢复位置 https://www.icloud.com/shortcuts/f42632d406504f24a2cd163af4fe012f | 选点页面: https://wloc-pages.pages.dev/
#!author=xweiba
#!homepage=https://github.com/xweiba/location-spoofer
#!category=Tools
#!arguments=经度:113.94114, 纬度:22.544577, 精度:25, 扰动半径:0, 日志级别:info
#!arguments-desc=经度/纬度: 默认坐标(在线选点储存后优先)\n精度: GPS精度(米)\n扰动半径: 每次响应在目标点周围随机偏移的最大距离(米),0为关闭\n日志级别: off/error/warn/info/debug/all\n\n使用方法: 打开选点页面 -> 选位置 -> 储存到设备
[Script]
Apple WLOC = type=http-response, pattern="^https?:\/\/(?:gs-loc(?:-cn)?\.apple\.com|gsp-ssl\.ls\.apple\.com|bluedot\.is\.autonavi\.com(?:\.gds\.alibabadns\.com)?)\/clls\/wloc", requires-body=1, binary-body-mode=1, max-size=0, timeout=30, script-path=https://gh-proxy.org/https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc.js?v=1.0.7, argument=longitude={{{经度}}}&latitude={{{纬度}}}&accuracy={{{精度}}}&randomRadius={{{扰动半径}}}&logLevel={{{日志级别}}}
WLOC Settings = type=http-request, pattern="^https?:\/\/gs-loc(-cn)?\.apple\.com\/wloc-settings\/(save|version)", requires-body=0, max-size=0, timeout=10, script-path=https://gh-proxy.org/https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc-settings.js?v=1.0.7
[MITM]
hostname = %APPEND% gs-loc.apple.com, gs-loc-cn.apple.com, gsp-ssl.ls.apple.com, bluedot.is.autonavi.com, bluedot.is.autonavi.com.gds.alibabadns.com
@@ -0,0 +1,35 @@
name: Apple WLOC 定位修改
desc: "修改 Apple 网络定位返回坐标 | 快捷指令(推荐): 设置地理位置 https://www.icloud.com/shortcuts/a82717d8fdad4e6280866fcf911173f7 清理恢复位置 https://www.icloud.com/shortcuts/f42632d406504f24a2cd163af4fe012f | 选点页面: https://wloc-pages.pages.dev/"
author: xweiba
homepage: https://github.com/xweiba/location-spoofer
category: Tools
http:
mitm:
- "gs-loc.apple.com"
- "gs-loc-cn.apple.com"
- "gsp-ssl.ls.apple.com"
- "bluedot.is.autonavi.com"
- "bluedot.is.autonavi.com.gds.alibabadns.com"
script:
- match: ^https?:\/\/(?:gs-loc(?:-cn)?\.apple\.com|gsp-ssl\.ls\.apple\.com|bluedot\.is\.autonavi\.com(?:\.gds\.alibabadns\.com)?)\/clls\/wloc
name: WLOC.Location
type: response
require-body: true
binary-mode: true
max-size: 0
timeout: 30
argument: longitude=113.94114&latitude=22.544577&accuracy=25&randomRadius=0&logLevel=info
- match: ^https?:\/\/gs-loc(-cn)?\.apple\.com\/wloc-settings\/(save|version)
name: WLOC.Settings
type: request
require-body: false
timeout: 10
script-providers:
WLOC.Location:
url: https://gh-proxy.org/https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc.js?v=1.0.7
interval: 86400
WLOC.Settings:
url: https://gh-proxy.org/https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc-settings.js?v=1.0.7
interval: 86400
+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.";
+703
View File
@@ -0,0 +1,703 @@
/* ============================================================
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";
"======== 第三方代理连接检测 ========" = "======== Third-party Proxy Connection Check ========";
"模式: 测试模式" = "Mode: Test mode";
"请求: wloc-settings/save?action=query" = "Request: wloc-settings/save?action=query";
"拦截响应: 有效 JSON" = "Intercepted response: Valid JSON";
"已保存坐标: %@" = "Saved coordinate: %@";
"结果: %@" = "Result: %@";
/* 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";
"已连接,有保存坐标" = "Connected, coordinate saved";
"已连接,无保存坐标" = "Connected, no saved coordinate";
"连接失败(%@" = "Connection failed (%@)";
"当前客户端:%@" = "Current client: %@";
"配置接口:/wloc-settings/save" = "Configuration endpoint: /wloc-settings/save";
"请求动作:WLOC query" = "Request action: WLOC query";
"检查范围:模块拦截、MITM、证书、代理/VPN 连接" = "Check scope: module interception, MITM, certificate, and proxy/VPN connection";
"连接状态:%@" = "Connection status: %@";
"检测结果:失败" = "Check result: Failed";
"耗时:%lld ms" = "Duration: %lld ms";
"错误类型:%@" = "Error type: %@";
"错误详情:%@" = "Error details: %@";
"处理建议:%@。" = "Suggested action: %@.";
"======== 第三方代理运行检测 ========" = "======== Third-party Proxy Runtime Check ========";
"触发来源:地图或设置中的第三方代理操作" = "Triggered by: third-party proxy action from the map or Settings";
"请求动作:WLOC 配置接口" = "Request action: WLOC configuration endpoint";
"处理建议:确认模块已启用,并检查 MITM、证书和代理/VPN 连接。" = "Suggested action: confirm the module is enabled, then check MITM, the certificate, and the proxy/VPN connection.";
/* MARK: - Exported diagnostics */
"环境信息" = "Environment";
"App 版本" = "App version";
"系统版本" = "System version";
"诊断日志" = "Diagnostic Logs";
"不适用" = "Not applicable";
"是" = "Yes";
"否" = "No";
"(无诊断数据)" = "(No diagnostic data)";
"第三方代理测试模式:模块连接成功;已保存坐标=%@" = "Third-party proxy test mode: module connected; saved coordinate=%@";
"第三方代理测试模式:模块连接失败;%@" = "Third-party proxy test mode: module connection failed; %@";
"======== 代理验证测试 ========" = "======== Proxy Verification Test ========";
"App 版本: %@" = "App version: %@";
"系统版本: iOS %@" = "System version: iOS %@";
"[步骤 A] 检查代理是否运行…" = "[Step A] Check whether the proxy is running…";
" 端口: 127.0.0.1:8888" = " Port: 127.0.0.1:8888";
" ⚠ 代理未运行,尝试启动…" = " ⚠ Proxy is not running; attempting to start it…";
" ✗ 启动失败: %@" = " ✗ Failed to start: %@";
" ✓ 代理启动成功" = " ✓ Proxy started";
" ✓ 代理已在运行中" = " ✓ Proxy is already running";
"[步骤 B] 检测证书与 WiFi 代理…" = "[Step B] Check certificate and Wi-Fi proxy…";
" 方式: 请求 baidu.com/paopao-verify-<token>" = " Method: request baidu.com/paopao-verify-<token>";
" 结果判定: TLS 错误=证书问题 / 响应不匹配=代理未配置 / 匹配=通过" = " Criteria: TLS error=certificate issue / response mismatch=proxy not configured / match=passed";
" ✗ 无法生成验证 token" = " ✗ Could not generate a verification token";
" ✓ 证书已信任,WiFi 代理已配置" = " ✓ Certificate is trusted and the Wi-Fi proxy is configured";
" ✗ 响应不匹配: HTTP %lld, %lld bytesWiFi 代理未配置" = " ✗ Response mismatch: HTTP %1$lld, %2$lld bytes; Wi-Fi proxy is not configured";
" ✗ 请求失败 [%@ code=%lld]: %@" = " ✗ Request failed [%1$@ code=%2$lld]: %3$@";
" TLS/证书校验失败,CA 证书未信任" = " TLS/certificate validation failed; the CA certificate is not trusted";
"======== 环境检测通过 ✓ ========" = "======== Environment Check Passed ✓ ========";
" --- 代理日志 ---" = " --- Proxy Logs ---";
/* MARK: - Setup examples and acknowledgements */
"成功:{\"success\":true,\"longitude\":113.0,\"latitude\":22.0,\"accuracy\":25}\n失败:{\"success\":false,\"error\":\"错误说明\"}" = "Success: {\"success\":true,\"longitude\":113.0,\"latitude\":22.0,\"accuracy\":25}\nFailure: {\"success\":false,\"error\":\"Error details\"}";
"定位改写脚本源自 Yu9191/wloc,现由本项目维护" = "Location-rewrite scripts originated from Yu9191/wloc and are now maintained by this project";
/* MARK: - Runtime log categories and messages */
"地图" = "Map";
"坐标转换" = "Coordinate Conversion";
"定位" = "Location";
"实时定位" = "Live Location";
"提醒" = "Reminder";
"搜索" = "Search";
"收藏" = "Favorites";
"缩放" = "Zoom";
"========== App 启动 ==========" = "========== App Launch ==========";
"尚未选择运行模式,跳过本地 CA 和代理初始化" = "No runtime mode selected; skipping local CA and proxy initialization";
"第三方代理测试模式:跳过本地 CA、代理和环境检测" = "Third-party proxy test mode: skipping local CA, proxy, and environment checks";
"旧坐标数据迁移失败,将在下次启动重试" = "Legacy coordinate migration failed; it will be retried on the next launch";
"地图坐标标准初始化完成,开始后续启动流程" = "Map coordinate-system initialization completed; continuing app startup";
"没有持久化图钉,地图创建前请求实时定位" = "No persisted pin; requesting live location before creating the map";
"已使用实时定位准备唯一初始地图状态" = "Prepared the initial map state using live location";
"地图创建前取得的初始实时位置(WGS-84" = "Initial live location obtained before map creation (WGS-84)";
"地图创建前无法取得实时定位,唯一初始位置使用深圳" = "Could not obtain live location before map creation; using Shenzhen as the initial location";
"已找到持久化图钉,直接准备唯一初始地图状态" = "Found a persisted pin and prepared the initial map state";
"启动门禁全部完成,现在创建 MapHomeView" = "All startup gates completed; creating MapHomeView";
"初始化" = "Initialize";
"启动代理 127.0.0.1:8888" = "Starting proxy at 127.0.0.1:8888";
"启动代理: 恢复上次 WGS-84 定位" = "Starting proxy: restoring the last WGS-84 location";
"启动成功" = "Started successfully";
"启动失败" = "Failed to start";
"写入坐标" = "Write coordinate";
"跳过过期坐标写入" = "Skipped stale coordinate write";
"跳过旧验证坐标恢复" = "Skipped restoring coordinates from an outdated verification";
"运动状态模拟设置已更新" = "Motion simulation setting updated";
"准备证书下载失败" = "Failed to prepare certificate download";
"设置虚拟定位坐标" = "Set simulated-location coordinate";
"apply失败" = "Apply failed";
"保存收藏失败" = "Failed to save favorite";
"本地服务初始化失败" = "Local service initialization failed";
"代理运行模式已切换" = "Proxy runtime mode changed";
"已迁移旧版模式初始化状态" = "Migrated legacy mode initialization state";
"地图坐标标准检测已有请求进行中" = "A map coordinate-system check is already running";
"地图坐标标准检测开始" = "Map coordinate-system check started";
"地图坐标标准检测获得明确结果" = "Map coordinate-system check returned a definitive result";
"地图坐标标准检测不可用,开始实时定位兜底" = "Map coordinate-system check unavailable; starting live-location fallback";
"地图坐标标准检测使用兜底结果" = "Map coordinate-system check used the fallback result";
"地图坐标标准检测被取消,保留默认国内标准" = "Map coordinate-system check was cancelled; keeping the default mainland-China system";
"地图坐标标准已确定,允许创建地图" = "Map coordinate system determined; map creation is now allowed";
"地图坐标标准运行期检测合并到进行中请求" = "Runtime map coordinate-system check joined the request already in progress";
"地图坐标标准运行期检测开始" = "Runtime map coordinate-system check started";
"地图坐标标准运行期检测结果已过期,取消写入" = "Runtime map coordinate-system result became stale; discarding it";
"地图坐标标准运行期检测完成,标准未变化" = "Runtime map coordinate-system check completed with no change";
"地图坐标标准运行期检测发现切换" = "Runtime map coordinate-system check detected a change";
"地图坐标标准运行期检测失败,保留当前标准" = "Runtime map coordinate-system check failed; keeping the current system";
"地图坐标标准运行期检测已取消" = "Runtime map coordinate-system check cancelled";
"实时定位不覆盖固定锚点的明确检测结果" = "Live location did not override the definitive fixed-anchor result";
"实时定位确认兜底地图坐标标准无需修正" = "Live location confirmed that the fallback map coordinate system needs no correction";
"实时定位修正启动兜底地图坐标标准" = "Live location corrected the startup fallback map coordinate system";
"第三方代理已保存 WGS-84 坐标" = "Third-party proxy saved the WGS-84 coordinate";
"第三方代理坐标已清除" = "Third-party proxy coordinate cleared";
"第三方代理请求失败" = "Third-party proxy request failed";
"第三方代理坐标同步成功" = "Third-party proxy coordinate synchronized";
"同步坐标到第三方客户端失败" = "Failed to synchronize the coordinate to the third-party client";
"验证结果" = "Verification result";
"验证失败" = "Verification failed";
"开启前检测失败,进入对应环境引导" = "Pre-start check failed; opening the relevant setup guide";
"清除第三方客户端坐标失败" = "Failed to clear the coordinate from the third-party client";
"累计虚拟定位操作次数" = "Counted simulated-location operations";
"已复制坐标" = "Coordinate copied";
"地图坐标标准切换时未找到当前选点缓存" = "No current-selection cache was found when the map coordinate system changed";
"地图坐标标准切换后已使用缓存坐标对回显当前选点" = "Restored the current selection from cached coordinates after the map coordinate system changed";
"地图坐标类型已变化" = "Map coordinate type changed";
"取消保存收藏:检测期间当前选点已变化" = "Cancelled saving favorite because the current selection changed during detection";
"保存当前选点为收藏" = "Saving the current selection as a favorite";
"第三方代理查询返回失败" = "Third-party proxy query returned a failure";
"启动后第三方代理状态查询失败" = "Failed to query third-party proxy status after startup";
"检测到 Wi-Fi 网络变化" = "Detected a Wi-Fi network change";
"网络仍在变化,重新计算环境检测等待时间" = "Network is still changing; recalculating the environment-check delay";
"等待 Wi-Fi 连接稳定后检测" = "Waiting for the Wi-Fi connection to stabilize before checking";
"延时检测已被更新的网络事件取消" = "Delayed check was cancelled by a newer network event";
"稳定等待结束后仍未连接 Wi-Fi,提示检查代理" = "Wi-Fi is still disconnected after the stability delay; prompting the user to check the proxy";
"开始后台环境检测" = "Starting background environment check";
"已有环境检测运行,1 秒后重试" = "An environment check is already running; retrying in 1 second";
"后台环境检测完成" = "Background environment check completed";
"环境检测连续被占用,本次不重复弹窗" = "Environment check remained busy; suppressing a duplicate alert";
"用户点击实时定位" = "User requested live location";
"MapKit 蓝点尚不可用,启动 CLLocationManager 兜底" = "MapKit user location is unavailable; starting the CLLocationManager fallback";
"MapKit 蓝点抢先完成请求,取消 CLLocationManager 兜底" = "MapKit completed the request first; cancelling the CLLocationManager fallback";
"虚拟定位开启后的 MapKit 蓝点标准判定" = "Determined the MapKit user-location coordinate system after simulated location started";
"登记实时定位请求上下文" = "Registered live-location request context";
"复用进行中的 CLLocationManager 请求并更新意图上下文" = "Reusing the active CLLocationManager request and updating its intent context";
"CLLocationManager 兜底未返回坐标" = "CLLocationManager fallback returned no coordinate";
"丢弃 CLLocationManager 结果:任务已取消或蓝点已抢先完成" = "Discarded CLLocationManager result because the task was cancelled or MapKit completed first";
"实时定位坐标完成标准判断并提交到地图" = "Live-location coordinate system determined and submitted to the map";
"MKLocalSearch 返回地点结果" = "MKLocalSearch returned a place result";
"反向地理编码网络失败,准备重试" = "Reverse geocoding network request failed; preparing to retry";
"反向地理编码失败" = "Reverse geocoding failed";
"获得搜索结果" = "Received search results";
"点击收藏点并回显到地图" = "Selected a favorite and displayed it on the map";
"忽略重复 CLLocationManager 请求" = "Ignored duplicate CLLocationManager request";
"CLLocationManager 请求入口" = "CLLocationManager request entry";
"授权状态不允许定位" = "Authorization status does not permit location access";
"创建 CLLocationManager 请求" = "Created CLLocationManager request";
"请求前台定位授权" = "Requesting foreground location authorization";
"定位授权状态变化" = "Location authorization status changed";
"CLLocationManager 返回样本批次" = "CLLocationManager returned a batch of samples";
"本批次没有可完成当前请求的样本" = "This batch contains no sample that can complete the current request";
"接受 CLLocationManager 样本并完成请求" = "Accepted CLLocationManager sample and completed the request";
"定位回调失败" = "Location callback failed";
"跳过 CLLocationManager 缓存样本" = "Skipped cached CLLocationManager sample";
"等待定位授权超时" = "Timed out waiting for location authorization";
"单次定位超时,切换持续定位" = "One-shot location timed out; switching to continuous updates";
"持续定位超时" = "Continuous location updates timed out";
"开始 CLLocationManager 单次定位" = "Starting one-shot CLLocationManager request";
"开始 CLLocationManager 持续定位兜底" = "Starting continuous CLLocationManager fallback";
"CLLocationManager 请求完成" = "CLLocationManager request completed";
"CLLocationManager 请求结束但没有坐标" = "CLLocationManager request ended without a coordinate";
"使用 CLLocationManager 新鲜缓存" = "Using a fresh CLLocationManager cache entry";
"实时定位按钮直接使用 MapKit 蓝点缓存" = "Live-location action used the MapKit user-location cache directly";
"主页收到待处理请求所需的 MapKit 蓝点回调" = "Home screen received the MapKit user-location callback for the pending request";
"主页收到 CLLocationManager 兜底坐标" = "Home screen received the CLLocationManager fallback coordinate";
"原始实时定位坐标" = "Original live-location coordinate";
"检查 CLLocationManager 样本" = "Checking CLLocationManager sample";
"拒绝 CLLocationManager 样本:坐标或精度无效" = "Rejected CLLocationManager sample because its coordinate or accuracy is invalid";
"MapKit 蓝点更新但 location 为空" = "MapKit user location updated but its location value is empty";
"拒绝 MapKit 蓝点样本:坐标无效" = "Rejected MapKit user-location sample because its coordinate is invalid";
"拒绝 MapKit 蓝点样本:水平精度无效" = "Rejected MapKit user-location sample because horizontal accuracy is invalid";
"音频中断恢复" = "Recovered from audio interruption";
"音频会话失败" = "Audio session failed";
"无法创建静音音频缓冲区" = "Could not create the silent-audio buffer";
"引擎启动失败" = "Audio engine failed to start";
"后台保活已启动(静音音频)" = "Background keep-alive started (silent audio)";
"后台保活已停止" = "Background keep-alive stopped";
"保存当前图钉失败" = "Failed to save the current pin";
"存储缩放" = "Stored zoom level";
"旧坐标数据迁移完成" = "Legacy coordinate migration completed";
"远程版本配置加载成功" = "Remote version configuration loaded";
"版本配置源不可用,尝试下一地址" = "Version configuration source unavailable; trying the next URL";
"版本配置加载失败,继续使用内置配置" = "Failed to load version configuration; continuing with built-in values";
"版本说明源不可用,尝试下一地址" = "Release-notes source unavailable; trying the next URL";
"版本说明加载失败,将使用最新 Release 页面" = "Failed to load release notes; using the latest Release page";
"复用设备钥匙串中的 CA" = "Reusing the CA from the device keychain";
"旧 CA 已迁移到设备钥匙串" = "Legacy CA migrated to the device keychain";
"已生成并保存设备专属 CA" = "Generated and saved a device-specific CA";
"已删除设备 CA,等待重新生成" = "Deleted the device CA; waiting to regenerate it";
"钥匙串中的 CA 无效,准备回退" = "CA in the keychain is invalid; preparing fallback";
"删除旧 CA 文件失败,将在下次启动重试" = "Failed to delete legacy CA files; it will be retried on the next launch";
"无法解析 CA 证书 PEM" = "Could not parse the CA certificate PEM";
"无法创建 SecTrust" = "Could not create SecTrust";
"SecTrust 评估返回错误" = "SecTrust evaluation returned an error";
"CA 证书已被系统信任" = "CA certificate is trusted by the system";
"CA 证书未被系统信任" = "CA certificate is not trusted by the system";
"调用 Go Core 生成 CA" = "Calling Go Core to generate the CA";
"Go Core CA 生成成功" = "Go Core generated the CA";
"本地证书服务已在运行" = "Local certificate server is already running";
"调用 Go Core 启动本地证书服务" = "Calling Go Core to start the local certificate server";
"本地证书服务启动成功" = "Local certificate server started";
"停止本地证书服务" = "Stopping local certificate server";
"开始第三方代理连接检测" = "Starting third-party proxy connection check";
"第三方代理连接检测通过" = "Third-party proxy connection check passed";
"第三方代理连接检测失败" = "Third-party proxy connection check failed";
"关闭代理前已同步关闭虚拟定位" = "Stopped simulated location before stopping the proxy";
"切换 APP 模式前无法清除第三方坐标" = "Could not clear the third-party coordinate before switching to App Mode";
"设置页第三方连接检测通过" = "Third-party connection check passed in Settings";
"设置页第三方连接检测失败" = "Third-party connection check failed in Settings";
"地图实际显示坐标" = "Coordinate displayed on map";
/* MARK: - Runtime log fields and values */
"App回到前台" = "App returned to foreground";
"App已确认地图标准" = "App confirmed the map coordinate system";
"CLLocationManager请求中" = "CLLocationManager request active";
"Go proxy 启动失败" = "Go proxy failed to start";
"MapKit地图蓝点" = "MapKit user location";
"MapKit蓝点新样本" = "New MapKit user-location sample";
"MapKit蓝点缓存" = "MapKit user-location cache";
"SSID可读取" = "SSID readable";
"WLOC写入标准" = "WLOC write coordinate system";
"WLOC目标标准" = "WLOC target coordinate system";
"Wi-Fi接口" = "Wi-Fi interface";
"intent选点revision" = "Intent selection revision";
"manager请求中" = "Manager request active";
"上下文存在" = "Context present";
"事件原因" = "Event reason";
"任务已取消" = "Task cancelled";
"任务已存在" = "Task already present";
"使用兜底" = "Used fallback";
"保存收藏" = "Save favorite";
"保存数据包含" = "Saved data contains";
"保持未启用" = "Kept disabled";
"保留原第三方坐标" = "Kept previous third-party coordinate";
"保留已启用状态" = "Kept enabled state";
"保留标准" = "Retained coordinate system";
"修正兜底标准" = "Corrected fallback coordinate system";
"内存缓存存在" = "In-memory cache present";
"初始坐标来源" = "Initial coordinate source";
"初始来源" = "Initial source";
"初始阶段" = "Initial phase";
"判定规则" = "Decision rule";
"原兜底来源" = "Original fallback source";
"原因" = "Reason";
"取值字段" = "Source field";
"可显示不再提醒" = "Can show suppress option";
"启动代理…" = "Starting proxy…";
"命中林士街" = "Matched Lin Shi Street";
"固定锚点" = "Fixed anchor";
"固定锚点查询超过5秒" = "Fixed-anchor query exceeded 5 seconds";
"固定锚点查询返回空结果" = "Fixed-anchor query returned no results";
"国内标准(GCJ-02)" = "Mainland China system (GCJ-02)";
"国内转换区域" = "Mainland conversion region";
"国外非转换区域" = "Non-conversion region outside mainland China";
"国际标准(WGS-84)" = "International system (WGS-84)";
"国际标准(WGS-84)+国内标准(GCJ-02)" = "International (WGS-84) + mainland China (GCJ-02)";
"图钉已按新类型重设" = "Pin reset for the new type";
"地图取值字段" = "Map source field";
"地图标准" = "Map coordinate system";
"坐标有效" = "Coordinate valid";
"坐标标准" = "Coordinate system";
"垂直精度米" = "Vertical accuracy (m)";
"基础校验通过" = "Basic validation passed";
"处理建议" = "Suggested action";
"失败时提示" = "Show alert on failure";
"实时定位存在" = "Live location present";
"实时定位服务区域" = "Live-location service region";
"客户端模式" = "Client mode";
"尝试" = "Attempt";
"已取消位置更新" = "Location updates cancelled";
"已恢复真实定位" = "Real location restored";
"已清理" = "Cleared";
"已重置" = "Reset";
"开启" = "Enable";
"异步地理编码" = "Asynchronous geocoding";
"当前地图标准" = "Current map coordinate system";
"当前客户端" = "Current client";
"当前标准" = "Current coordinate system";
"当前选点revision" = "Current selection revision";
"恢复状态" = "Restore state";
"批次索引" = "Batch index";
"持久化字段" = "Persisted field";
"授权状态" = "Authorization status";
"授权状态rawValue" = "Authorization status raw value";
"探测失败原因" = "Probe failure reason";
"搜索结果" = "Search results";
"操作" = "Operation";
"新类型" = "New type";
"无已保存" = "None saved";
"无法判定" = "Undetermined";
"无法启动本地证书服务" = "Could not start the local certificate server";
"无法生成本地证书" = "Could not generate the local certificate";
"日志策略" = "Logging policy";
"旧类型" = "Previous type";
"显示坐标字段" = "Displayed coordinate field";
"最低版本" = "Minimum version";
"最新版本" = "Latest version";
"最终标准" = "Final coordinate system";
"最终阶段" = "Final phase";
"有坐标" = "Coordinate present";
"有效样本数" = "Valid sample count";
"有缓存" = "Cache present";
"未知错误" = "Unknown error";
"本地 CA 证书或私钥无效" = "Local CA certificate or private key is invalid";
"无法写入设备钥匙串(%d" = "Could not write to the device keychain (%d)";
"来源" = "Source";
"来源类型" = "Source type";
"林士街" = "Lin Shi Street";
"查看诊断日志" = "View diagnostic logs";
"样本年龄秒" = "Sample age (s)";
"样本数" = "Sample count";
"样本时间" = "Sample time";
"检查范围" = "Check scope";
"检测前标准" = "Coordinate system before check";
"模块拦截、MITM、代理/VPN连接" = "Module interception, MITM, and proxy/VPN connection";
"次数" = "Count";
"每次开启首次或判定变化" = "First sample after each start or classification change";
"水平精度米" = "Horizontal accuracy (m)";
"活动requestID" = "Active request ID";
"活动阶段" = "Active phase";
"海拔米" = "Altitude (m)";
"满足当前请求时间窗" = "Within current request time window";
"点击实时定位" = "Live-location action";
"目标所在区域" = "Target region";
"确认标准" = "Confirmed coordinate system";
"社区征集客户端数" = "Clients requesting community contributions";
"等待秒数" = "Wait duration (s)";
"系统缓存存在" = "System cache present";
"结果" = "Result";
"结果数" = "Result count";
"结果来源" = "Result source";
"缓存" = "Cache";
"缓存上限秒" = "Cache limit (s)";
"缓存时效通过" = "Cache freshness valid";
"缩放米" = "Zoom (m)";
"网络可用" = "Network available";
"耗时毫秒" = "Duration (ms)";
"蓝点回调更接近" = "User-location callback is closer";
"蓝点缓存" = "User-location cache";
"蓝点缓存存在" = "User-location cache present";
"蓝点距GCJ目标米" = "User-location distance to GCJ target (m)";
"蓝点距WGS目标米" = "User-location distance to WGS target (m)";
"触发原因" = "Trigger reason";
"诊断位置" = "Diagnostic location";
"请求动作" = "Request action";
"超时毫秒" = "Timeout (ms)";
"输入坐标标准" = "Input coordinate system";
"连接状态" = "Connection status";
"选点revision" = "Selection revision";
"选点期间发生变化" = "Selection changed during operation";
"错误" = "Error";
"错误类型" = "Error type";
"锚点" = "Anchor";
"阶段" = "Phase";
"首条名称" = "First result name";
"首条名称=林士街→GCJ-02,否则→WGS-84" = "First name=Lin Shi Street → GCJ-02; otherwise → WGS-84";
"默认国内标准" = "Default mainland-China coordinate system";
/* 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
@@ -13,8 +13,8 @@ enum CertificateAuthorityStoreError: LocalizedError {
var errorDescription: String? { var errorDescription: String? {
switch self { switch self {
case .invalidAuthority: return "本地 CA 证书或私钥无效" case .invalidAuthority: return String(localized: "本地 CA 证书或私钥无效")
case let .keychain(status): return "无法写入设备钥匙串(\(status)" case let .keychain(status): return String(localized: "无法写入设备钥匙串(\(status)")
} }
} }
} }
+2 -2
View File
@@ -12,8 +12,8 @@ enum CoreBridgeError: LocalizedError {
var errorDescription: String? { var errorDescription: String? {
switch self { switch self {
case .generationFailed: return "无法生成本地证书" case .generationFailed: return String(localized: "无法生成本地证书")
case .serverStartFailed: return "无法启动本地证书服务" case .serverStartFailed: return String(localized: "无法启动本地证书服务")
} }
} }
} }
+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: "第三方代理模式")
} }
} }
} }
+28 -3
View File
@@ -38,10 +38,35 @@ struct RuntimeLogEntry: Codable, Identifiable, Equatable {
var renderedText: String { var renderedText: String {
let formatter = ISO8601DateFormatter() let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
let suffix = details.isEmpty let localizedDetails = details.map {
(localizedDiagnosticText($0.key), localizedDiagnosticValue($0.value))
}
let suffix = localizedDetails.isEmpty
? "" ? ""
: " " + details.sorted(by: { $0.key < $1.key }).map { "\($0.key)=\($0.value)" }.joined(separator: " ") : " " + localizedDetails.sorted(by: { $0.0 < $1.0 }).map { "\($0.0)=\($0.1)" }.joined(separator: " ")
return "\(formatter.string(from: timestamp)) [\(source)] [\(level.rawValue.uppercased())] [\(category)] \(message)\(suffix)" return "\(formatter.string(from: timestamp)) [\(source)] [\(level.rawValue.uppercased())] [\(localizedCategory)] \(localizedMessage)\(suffix)"
}
var localizedCategory: String { localizedDiagnosticText(category) }
var localizedMessage: String { localizedDiagnosticText(message) }
var localizedDetailsText: String {
details.map { (localizedDiagnosticText($0.key), localizedDiagnosticValue($0.value)) }
.sorted(by: { $0.0 < $1.0 })
.map { "\($0.0): \($0.1)" }
.joined(separator: "\n")
}
private func localizedDiagnosticText(_ text: String) -> String {
String(localized: String.LocalizationValue(text))
}
private func localizedDiagnosticValue(_ value: String) -> String {
switch value {
case "true": return String(localized: "")
case "false": return String(localized: "")
default: return localizedDiagnosticText(value)
}
} }
} }
+26 -27
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 }
@@ -170,7 +170,10 @@ final class ThirdPartyProxyManager: ObservableObject {
URLQueryItem(name: "lon", value: String(format: "%.8f", locale: Locale(identifier: "en_US_POSIX"), longitude)), URLQueryItem(name: "lon", value: String(format: "%.8f", locale: Locale(identifier: "en_US_POSIX"), longitude)),
URLQueryItem(name: "lat", value: String(format: "%.8f", locale: Locale(identifier: "en_US_POSIX"), latitude)), URLQueryItem(name: "lat", value: String(format: "%.8f", locale: Locale(identifier: "en_US_POSIX"), latitude)),
URLQueryItem(name: "acc", value: String(accuracy)), URLQueryItem(name: "acc", value: String(accuracy)),
URLQueryItem(name: "randomRadius", value: String(randomRadius)) URLQueryItem(
name: "randomRadius",
value: String(format: "%g", locale: Locale(identifier: "en_US_POSIX"), randomRadius)
)
] ]
} }
guard let url = components.url else { throw ThirdPartyProxyError.invalidResponse } guard let url = components.url else { throw ThirdPartyProxyError.invalidResponse }
@@ -203,6 +206,9 @@ final class ThirdPartyProxyManager: ObservableObject {
} }
enum ThirdPartyProxyClient: String, CaseIterable, Identifiable { enum ThirdPartyProxyClient: String, CaseIterable, Identifiable {
/// Bump when a hosted module or script changes to invalidate proxy-client caches.
static let moduleSubscriptionVersion = "1.0.7"
case shadowrocket case shadowrocket
case surge case surge
case quantumultX case quantumultX
@@ -224,7 +230,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 {
@@ -239,21 +245,14 @@ enum ThirdPartyProxyClient: String, CaseIterable, Identifiable {
@MainActor @MainActor
var subscriptionURL: URL { var subscriptionURL: URL {
// Third-party modules are served directly from the upstream Yu9191/wloc let directory = ThirdPartyModuleSourceStore.shared.useMirror
// repository (mirror via gh-proxy when enabled). We no longer maintain ? "Resources/ThirdPartyProxyModules"
// project-owned module copies, so the URL tracks upstream releases. : "ThirdParty/WlocScripts/modules/direct"
let fileName: String let prefix = ThirdPartyModuleSourceStore.shared.useMirror
switch self { ? "https://gh-proxy.org/https://raw.githubusercontent.com/xweiba/location-spoofer/main/"
case .surge, .egern: fileName = "wloc.sgmodule" : "https://raw.githubusercontent.com/xweiba/location-spoofer/main/"
case .quantumultX: fileName = "wloc.conf" let base = "\(prefix)\(directory)/\(moduleFileName)"
case .loon: fileName = "wloc.lpx" return URL(string: "\(base)?v=\(Self.moduleSubscriptionVersion)")!
case .stash: fileName = "wloc.stoverride"
case .shadowrocket: fileName = "wloc.module"
}
let base = ThirdPartyModuleSourceStore.shared.useMirror
? "https://gh-proxy.org/https://raw.githubusercontent.com/Yu9191/wloc/refs/heads/main/modules/\(fileName)"
: "https://raw.githubusercontent.com/Yu9191/wloc/refs/heads/main/modules/\(fileName)"
return URL(string: base)!
} }
var launchURL: URL? { var launchURL: URL? {
+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: "改写验证失败")
}
}
} }
@@ -73,9 +73,13 @@ final class RealtimeLocationManagerTests: XCTestCase {
func testOneShotTimeoutTransitionsToContinuousFallback() async { func testOneShotTimeoutTransitionsToContinuousFallback() async {
let driver = FakeRealtimeLocationDriver() let driver = FakeRealtimeLocationDriver()
let manager = RealtimeLocationManager(driver: driver, oneShotTimeoutNanoseconds: 5_000_000, fallbackTimeoutNanoseconds: 1_000_000_000) let manager = RealtimeLocationManager(driver: driver, oneShotTimeoutNanoseconds: 5_000_000, fallbackTimeoutNanoseconds: 1_000_000_000)
let fallbackStarted = expectation(description: "Continuous location fallback started")
driver.onStartUpdating = {
fallbackStarted.fulfill()
}
let request = Task { await manager.requestLocation() } let request = Task { await manager.requestLocation() }
try? await Task.sleep(nanoseconds: 20_000_000) await fulfillment(of: [fallbackStarted], timeout: 1)
XCTAssertEqual(driver.startUpdatingCallCount, 1) XCTAssertEqual(driver.startUpdatingCallCount, 1)
driver.emit(CLLocation(latitude: 39.90, longitude: 116.40)) driver.emit(CLLocation(latitude: 39.90, longitude: 116.40))
@@ -148,6 +152,7 @@ private final class FakeRealtimeLocationDriver: RealtimeLocationDriving {
var authorizationStatus: CLAuthorizationStatus = .authorizedWhenInUse var authorizationStatus: CLAuthorizationStatus = .authorizedWhenInUse
weak var delegate: CLLocationManagerDelegate? weak var delegate: CLLocationManagerDelegate?
var onRequestLocation: (() -> Void)? var onRequestLocation: (() -> Void)?
var onStartUpdating: (() -> Void)?
private(set) var requestAuthorizationCallCount = 0 private(set) var requestAuthorizationCallCount = 0
private(set) var requestLocationCallCount = 0 private(set) var requestLocationCallCount = 0
private(set) var startUpdatingCallCount = 0 private(set) var startUpdatingCallCount = 0
@@ -164,6 +169,7 @@ private final class FakeRealtimeLocationDriver: RealtimeLocationDriving {
func startUpdatingLocation() { func startUpdatingLocation() {
startUpdatingCallCount += 1 startUpdatingCallCount += 1
onStartUpdating?()
} }
func stopUpdatingLocation() { func stopUpdatingLocation() {
@@ -125,16 +125,19 @@ final class ThirdPartyProxyManagerTests: XCTestCase {
} }
} }
func testClientLinksUseUpstreamModulesAndVerificationLabels() { func testClientLinksUseProjectOwnedModulesAndVerificationLabels() {
XCTAssertEqual( XCTAssertEqual(
ThirdPartyProxyManager.interceptionHostnamesText, ThirdPartyProxyManager.interceptionHostnamesText,
"gs-loc.apple.com, gs-loc-cn.apple.com, gsp-ssl.ls.apple.com, bluedot.is.autonavi.com, bluedot.is.autonavi.com.gds.alibabadns.com" "gs-loc.apple.com, gs-loc-cn.apple.com, gsp-ssl.ls.apple.com, bluedot.is.autonavi.com, bluedot.is.autonavi.com.gds.alibabadns.com"
) )
XCTAssertNil(ThirdPartyProxyClient.shadowrocket.verificationText) XCTAssertNil(ThirdPartyProxyClient.shadowrocket.verificationText)
XCTAssertTrue(ThirdPartyProxyClient.surge.verificationText?.contains("尚未验证") == true) XCTAssertEqual(
ThirdPartyProxyClient.surge.verificationText,
String(localized: "配置已提供,尚未验证")
)
XCTAssertEqual(ThirdPartyProxyClient.egern.subscriptionURL, ThirdPartyProxyClient.surge.subscriptionURL) XCTAssertEqual(ThirdPartyProxyClient.egern.subscriptionURL, ThirdPartyProxyClient.surge.subscriptionURL)
XCTAssertTrue(ThirdPartyProxyClient.stash.subscriptionURL.absoluteString.hasPrefix( XCTAssertTrue(ThirdPartyProxyClient.stash.subscriptionURL.absoluteString.hasPrefix(
"https://gh-proxy.org/https://raw.githubusercontent.com/Yu9191/wloc/refs/heads/main/modules/" "https://gh-proxy.org/https://raw.githubusercontent.com/xweiba/location-spoofer/main/Resources/ThirdPartyProxyModules/"
)) ))
let stashComponents = URLComponents( let stashComponents = URLComponents(
url: ThirdPartyProxyClient.stash.subscriptionURL, url: ThirdPartyProxyClient.stash.subscriptionURL,
@@ -144,10 +147,10 @@ final class ThirdPartyProxyManagerTests: XCTestCase {
url: ThirdPartyProxyClient.shadowrocket.subscriptionURL, url: ThirdPartyProxyClient.shadowrocket.subscriptionURL,
resolvingAgainstBaseURL: false resolvingAgainstBaseURL: false
) )
XCTAssertEqual(stashComponents?.path, "/https://raw.githubusercontent.com/Yu9191/wloc/refs/heads/main/modules/wloc.stoverride") XCTAssertEqual(stashComponents?.path, "/https://raw.githubusercontent.com/xweiba/location-spoofer/main/Resources/ThirdPartyProxyModules/wloc.stoverride")
XCTAssertEqual(shadowrocketComponents?.path, "/https://raw.githubusercontent.com/Yu9191/wloc/refs/heads/main/modules/wloc.module") XCTAssertEqual(shadowrocketComponents?.path, "/https://raw.githubusercontent.com/xweiba/location-spoofer/main/Resources/ThirdPartyProxyModules/wloc.module")
XCTAssertTrue(stashComponents?.queryItems?.isEmpty ?? true) XCTAssertEqual(stashComponents?.queryItems?.first?.value, ThirdPartyProxyClient.moduleSubscriptionVersion)
XCTAssertTrue(shadowrocketComponents?.queryItems?.isEmpty ?? true) XCTAssertEqual(shadowrocketComponents?.queryItems?.first?.value, ThirdPartyProxyClient.moduleSubscriptionVersion)
XCTAssertEqual(ThirdPartyProxyClient.shadowrocket.launchURL?.scheme, "shadowrocket") XCTAssertEqual(ThirdPartyProxyClient.shadowrocket.launchURL?.scheme, "shadowrocket")
XCTAssertEqual(ThirdPartyProxyClient.surge.launchURL?.scheme, "surge") XCTAssertEqual(ThirdPartyProxyClient.surge.launchURL?.scheme, "surge")
XCTAssertEqual(ThirdPartyProxyClient.quantumultX.launchURL?.scheme, "quantumult-x") XCTAssertEqual(ThirdPartyProxyClient.quantumultX.launchURL?.scheme, "quantumult-x")
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
STRINGS="$ROOT/Resources/en.lproj/Localizable.strings"
fail() {
echo "FAIL: $1" >&2
exit 1
}
plutil -lint "$STRINGS" >/dev/null || fail "English localization file is invalid"
ruby - "$STRINGS" <<'RUBY' || exit 1
path = ARGV.fetch(0)
entries = File.readlines(path).map do |line|
match = line.match(/^"((?:\\.|[^"])*)"\s*=\s*"((?:\\.|[^"])*)";/)
[match[1], match[2]] if match
end.compact
duplicates = entries.group_by(&:first).select { |_key, values| values.length > 1 }.keys
abort "FAIL: duplicate English localization keys: #{duplicates.join(', ')}" unless duplicates.empty?
placeholder = /%(?:\d+\$)?(?:@|lld|ld|d|f)/
entries.each do |source, target|
source_count = source.scan(placeholder).length
target_count = target.scan(placeholder).length
abort "FAIL: placeholder mismatch for #{source.inspect}" unless source_count == target_count
end
RUBY
for key in \
'环境信息' \
'诊断日志' \
'======== 代理验证测试 ========' \
'======== 第三方代理连接检测 ========' \
'======== 第三方代理运行检测 ========' \
'第三方代理测试模式:模块连接成功;已保存坐标=%@' \
'成功:{\"success\":true,\"longitude\":113.0,\"latitude\":22.0,\"accuracy\":25}\n失败:{\"success\":false,\"error\":\"错误说明\"}'; do
grep -Fq "\"$key\" = " "$STRINGS" || fail "missing critical English localization: $key"
done
grep -q 'localizedCategory' "$ROOT/Shared/RuntimeLog.swift" \
|| fail "runtime log categories must be localized when rendered"
grep -q 'localizedDetailsText' "$ROOT/App/DiagnosticsView.swift" \
|| fail "runtime log details must be localized in the diagnostics UI"
grep -q 'String(localized: "诊断日志")' "$ROOT/App/BugReportView.swift" \
|| fail "generated bug reports must localize their diagnostic section"
grep -q 'log(" " + e.localizedMessage)' "$ROOT/App/SetupCoordinator.swift" \
|| fail "bug-report verification logs must render stored messages in the active language"
if grep -R -n 'raw.githubusercontent.com/Yu9191/wloc' \
"$ROOT/App" "$ROOT/Shared" "$ROOT/Resources/ThirdPartyProxyModules" \
"$ROOT/ThirdParty/WlocScripts/modules"; then
fail "deleted Yu9191/wloc repository must not remain a runtime dependency"
fi
echo "PASS: localization and diagnostic export contract"
+20 -5
View File
@@ -10,8 +10,8 @@ CONTENT="$ROOT/App/ContentView.swift"
SETUP="$ROOT/App/FirstSetupView.swift" SETUP="$ROOT/App/FirstSetupView.swift"
SETTINGS="$ROOT/App/SettingsView.swift" SETTINGS="$ROOT/App/SettingsView.swift"
grep -q 'return "APP模式"' "$MODE" || fail "APP mode display name is missing" grep -q 'String(localized: "APP模式")' "$MODE" || fail "localized APP mode display name is missing"
grep -q 'return "第三方代理模式"' "$MODE" || fail "third-party mode display name is missing" grep -q 'String(localized: "第三方代理模式")' "$MODE" || fail "localized third-party mode display name is missing"
grep -q 'hasSelectedMode' "$MODE" || fail "first-launch mode selection must be persisted" grep -q 'hasSelectedMode' "$MODE" || fail "first-launch mode selection must be persisted"
grep -q 'guard runtimeMode.hasSelectedMode else' "$CONTENT" || fail "mode selection must gate startup" grep -q 'guard runtimeMode.hasSelectedMode else' "$CONTENT" || fail "mode selection must gate startup"
grep -q 'phase = .setup' "$CONTENT" || fail "first launch must enter setup before map construction" grep -q 'phase = .setup' "$CONTENT" || fail "first launch must enter setup before map construction"
@@ -37,8 +37,23 @@ grep -Fq '保存:GET ?lon=<经度>&lat=<纬度>&acc=<精度>' "$SETUP" \
grep -Fq '清除:GET ?action=clear' "$SETUP" \ grep -Fq '清除:GET ?action=clear' "$SETUP" \
|| fail "client integration guidance must document the clear action" || fail "client integration guidance must document the clear action"
grep -q 'Yu9191/wloc/refs/heads/main/modules' "$MANAGER" \ grep -q 'raw.githubusercontent.com/xweiba/location-spoofer/main' "$MANAGER" \
|| fail "third-party subscription must point at upstream Yu9191 modules" || fail "third-party subscription must point at project-owned modules"
! grep -q 'raw.githubusercontent.com/Yu9191/wloc' "$MANAGER" \
|| fail "third-party subscription must not depend on the removed upstream repository"
MODULES="$ROOT/Resources/ThirdPartyProxyModules"
SCRIPTS="$ROOT/ThirdParty/WlocScripts"
for file in wloc.module wloc.sgmodule wloc.conf wloc.lpx wloc.stoverride; do
test -s "$MODULES/$file" || fail "missing project-owned mirrored module: $file"
test -s "$SCRIPTS/modules/direct/$file" || fail "missing project-owned direct module: $file"
! grep 'ThirdParty/WlocScripts/dist/v1/wloc' "$MODULES/$file" "$SCRIPTS/modules/direct/$file" \
| grep -Fvq '?v=1.0.7' \
|| fail "hosted script URLs must carry the current cache version: $file"
done
test -s "$SCRIPTS/dist/v1/wloc.js" || fail "missing hosted WLOC response script"
test -s "$SCRIPTS/dist/v1/wloc-settings.js" || fail "missing hosted WLOC settings script"
! grep -R -q 'raw.githubusercontent.com/Yu9191/wloc' "$MODULES" "$SCRIPTS/modules/direct" \
|| fail "hosted modules must not reference the removed upstream repository"
grep -q 'wloc.sgmodule' "$MANAGER" || fail "Surge/Egern module mapping is missing" grep -q 'wloc.sgmodule' "$MANAGER" || fail "Surge/Egern module mapping is missing"
grep -q 'wloc.stoverride' "$MANAGER" || fail "Stash must use .stoverride directly" grep -q 'wloc.stoverride' "$MANAGER" || fail "Stash must use .stoverride directly"
grep -q 'shadowrocket://' "$MANAGER" || fail "Shadowrocket launch URL is missing" grep -q 'shadowrocket://' "$MANAGER" || fail "Shadowrocket launch URL is missing"
@@ -88,7 +103,7 @@ grep -q 'setup.requestThirdPartySetup(message: error.localizedDescription)' "$RO
|| fail "third-party coordinate sync failures must open the import guide" || fail "third-party coordinate sync failures must open the import guide"
grep -q '检测到第三方代理连接异常,请检查模块、MITM 和代理连接后重新检测' "$SETUP" \ grep -q '检测到第三方代理连接异常,请检查模块、MITM 和代理连接后重新检测' "$SETUP" \
|| fail "runtime repair must explain why the import guide opened" || fail "runtime repair must explain why the import guide opened"
test "$(grep -c 'title: \"接口连接失败\"' "$SETUP")" -eq 1 \ test "$(grep -c 'title: String(localized: \"接口连接失败\")' "$SETUP")" -eq 1 \
|| fail "third-party failure details must render in one shared result area" || fail "third-party failure details must render in one shared result area"
grep -Fq '当前客户端:\(client.name)' "$SETUP" \ grep -Fq '当前客户端:\(client.name)' "$SETUP" \
|| fail "third-party failure logs must identify the selected client" || fail "third-party failure logs must identify the selected client"
@@ -25,7 +25,7 @@ import sys
with open(sys.argv[1], encoding="utf-8") as handle: with open(sys.argv[1], encoding="utf-8") as handle:
config = json.load(handle) config = json.load(handle)
assert config["latestVersion"] == "1.0.5" assert config["latestVersion"] == "1.0.6"
assert config["minimumSupportedVersion"] == "1.0.0" assert config["minimumSupportedVersion"] == "1.0.0"
assert "shadowrocket" not in config["communityPromptClients"] assert "shadowrocket" not in config["communityPromptClients"]
assert set(config["communityPromptClients"]) == { assert set(config["communityPromptClients"]) == {
@@ -121,7 +121,7 @@ grep -Fq 'SafariView(url: destination.url)' "$BUG_REPORT" \
|| fail "the App bug report must not be handed to an external GitHub client" || fail "the App bug report must not be handed to an external GitHub client"
grep -Fq 'App 生成的诊断报告' "$BUG_REPORT" \ grep -Fq 'App 生成的诊断报告' "$BUG_REPORT" \
|| fail "the App must tell users where to paste the generated report" || fail "the App must tell users where to paste the generated report"
grep -Fq '第三方客户端:' "$BUG_REPORT" \ grep -Fq 'String(localized: "第三方客户端")' "$BUG_REPORT" \
|| fail "the generated report must identify the selected third-party client" || fail "the generated report must identify the selected third-party client"
grep -Fq 'Label("报告 Bug"' "$SETTINGS" \ grep -Fq 'Label("报告 Bug"' "$SETTINGS" \
|| fail "Settings must identify the support action as a bug report" || fail "Settings must identify the support action as a bug report"
+6
View File
@@ -0,0 +1,6 @@
# Third-Party Notices
The generated proxy scripts bundle:
- `pako` 2.1.0, Copyright (C) 2014-2017 Vitaly Puzrin and contributors, MIT License.
- `esbuild` is used only as a development/build dependency and is distributed under the MIT License.
+19
View File
@@ -0,0 +1,19 @@
import { build } from "esbuild";
import { mkdir } from "node:fs/promises";
await mkdir(new URL("./dist/v1/", import.meta.url), { recursive: true });
for (const [entry, outfile] of [
["src/response-entry.js", "dist/v1/wloc.js"],
["src/settings-entry.js", "dist/v1/wloc-settings.js"]
]) {
await build({
entryPoints: [entry],
outfile,
bundle: true,
format: "iife",
target: ["es2017"],
minify: true,
legalComments: "eof"
});
}
+1
View File
@@ -0,0 +1 @@
(()=>{var y="1.0.0";var m=["wifi","cellTower","arpc","marker","synthetic","bare","motionSimulation"],c="locationSpoofer.settings.v1";function u(){return typeof $task!="undefined"?"quantumultX":typeof $loon!="undefined"?"loon":typeof $rocket!="undefined"?"shadowrocket":typeof Egern!="undefined"?"egern":typeof $environment!="undefined"&&$environment["stash-version"]?"stash":typeof $environment!="undefined"&&$environment["surge-version"]?"surge":"unknown"}function h(e){let t=u()==="quantumultX"?$prefs.valueForKey(e):$persistentStore.read(e);if(!t)return null;try{return JSON.parse(t)}catch(n){return null}}function l(e,t){let n=t==null?"":JSON.stringify(t);return u()==="quantumultX"?$prefs.setValueForKey(n,e):$persistentStore.write(n,e)}function i(e){let t={status:200,headers:{"Content-Type":"application/json; charset=utf-8"},body:JSON.stringify(e)};u()==="quantumultX"?$done(Object.assign({},t,{status:"HTTP/1.1 200 OK"})):u()==="stash"?$done(t):$done({response:t})}function g(e){let t=e.split("?")[1]||"",n={};return t.split("&").forEach(s=>{if(!s)return;let o=s.indexOf("="),f=o<0?s:s.slice(0,o),d=o<0?"":s.slice(o+1),a=f,p=d;try{a=decodeURIComponent(f.replace(/\+/g," "))}catch($){}try{p=decodeURIComponent(d.replace(/\+/g," "))}catch($){}Object.prototype.hasOwnProperty.call(n,a)||(n[a]=p)}),n}function O(e){let t=e.split("?")[0],n=t.indexOf("://");if(n<0)return t;let s=t.indexOf("/",n+3);return s<0?"/":t.slice(s)}var b=typeof $request=="undefined"?"":$request.url||"",E=O(b),r=g(b);if(E==="/wloc-settings/version")i({success:!0,moduleVersion:y,protocolVersion:1,capabilities:m});else if(r.action==="query"){let e=h(c);i(e&&e.enabled?{success:!0,longitude:e.longitude,latitude:e.latitude,accuracy:e.accuracy,motionSimulationEnabled:e.motionSimulationEnabled===!0}:{success:!1,error:"\u65E0\u5DF2\u4FDD\u5B58\u7684\u5750\u6807"})}else if(r.action==="clear")l(c,null),i({success:!0});else{let e=Number(r.lon!=null?r.lon:r.longitude),t=Number(r.lat!=null?r.lat:r.latitude),n=Number(r.acc!=null?r.acc:r.accuracy!=null?r.accuracy:25);if(!Number.isFinite(e)||!Number.isFinite(t))i({success:!1,error:"\u7F3A\u5C11 lon/lat \u53C2\u6570"});else{let s={enabled:!0,longitude:e,latitude:t,accuracy:n,motionSimulationEnabled:r.motion==="1"},o=l(c,s);i(o?{success:!0,longitude:e,latitude:t,accuracy:n,motionSimulationEnabled:s.motionSimulationEnabled}:{success:!1,error:"\u4FDD\u5B58\u914D\u7F6E\u5931\u8D25"})}}})();
File diff suppressed because one or more lines are too long
+11
View File
@@ -0,0 +1,11 @@
#!name=Apple WLOC 定位修改
#!desc=Location Spoofer 第三方代理模块
#!author=xweiba
#!homepage=https://github.com/xweiba/location-spoofer
[rewrite_local]
^https?:\/\/(?:gs-loc(?:-cn)?\.apple\.com|gsp-ssl\.ls\.apple\.com|bluedot\.is\.autonavi\.com(?:\.gds\.alibabadns\.com)?)\/clls\/wloc url script-response-body https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc.js?v=1.0.7
^https?:\/\/gs-loc(-cn)?\.apple\.com\/wloc-settings\/(save|version) url script-echo-response https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc-settings.js?v=1.0.7
[mitm]
hostname = gs-loc.apple.com, gs-loc-cn.apple.com, gsp-ssl.ls.apple.com, bluedot.is.autonavi.com, bluedot.is.autonavi.com.gds.alibabadns.com
+18
View File
@@ -0,0 +1,18 @@
#!name=Apple WLOC 定位修改
#!desc=Location Spoofer 第三方代理模块
#!author=xweiba
#!homepage=https://github.com/xweiba/location-spoofer
[Argument]
longitude = input, "113.94114", tag=经度(在线选点优先)
latitude = input, "22.544577", tag=纬度(在线选点优先)
accuracy = input, "25", tag=精度(米)
randomRadius = input, "0", tag=扰动半径(米,0为关闭)
logLevel = select, "info", "off", "error", "warn", "debug", "all", tag=日志级别
[Script]
http-response ^https?:\/\/(?:gs-loc(?:-cn)?\.apple\.com|gsp-ssl\.ls\.apple\.com|bluedot\.is\.autonavi\.com(?:\.gds\.alibabadns\.com)?)\/clls\/wloc script-path=https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc.js?v=1.0.7, requires-body=true, binary-body-mode=true, timeout=30, tag=Apple WLOC, argument=[{longitude},{latitude},{accuracy},{randomRadius},{logLevel}]
http-request ^https?:\/\/gs-loc(-cn)?\.apple\.com\/wloc-settings\/(save|version) script-path=https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc-settings.js?v=1.0.7, timeout=10, tag=WLOC Settings
[MITM]
hostname = gs-loc.apple.com, gs-loc-cn.apple.com, gsp-ssl.ls.apple.com, bluedot.is.autonavi.com, bluedot.is.autonavi.com.gds.alibabadns.com
+13
View File
@@ -0,0 +1,13 @@
#!name=Apple WLOC 定位修改
#!desc=修改 Apple 网络定位返回坐标 (Shadowrocket 小火箭) | 快捷指令(推荐): 设置地理位置 https://www.icloud.com/shortcuts/a82717d8fdad4e6280866fcf911173f7 清理恢复位置 https://www.icloud.com/shortcuts/f42632d406504f24a2cd163af4fe012f | 选点页面: https://wloc-pages.pages.dev/
#!author=Yu9191 Rewrite
#!homepage=https://github.com/xweiba/location-spoofer
#!icon=https://raw.githubusercontent.com/xweiba/location-spoofer/main/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon.png
#!category=Tools
[Script]
Apple WLOC = type=http-response,pattern=^https?:\/\/(?:gs-loc(?:-cn)?\.apple\.com|gsp-ssl\.ls\.apple\.com|bluedot\.is\.autonavi\.com(?:\.gds\.alibabadns\.com)?)\/clls\/wloc,requires-body=1,binary-body-mode=1,max-size=0,timeout=30,script-path=https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc.js?v=1.0.7,argument=longitude=113.94114&latitude=22.544577&accuracy=25&randomRadius=0&logLevel=info
WLOC Settings = type=http-request,pattern=^https?:\/\/gs-loc(-cn)?\.apple\.com\/wloc-settings\/(save|version),requires-body=0,max-size=0,timeout=10,script-path=https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc-settings.js?v=1.0.7
[MITM]
hostname = %APPEND% gs-loc.apple.com, gs-loc-cn.apple.com, gsp-ssl.ls.apple.com, bluedot.is.autonavi.com, bluedot.is.autonavi.com.gds.alibabadns.com
+14
View File
@@ -0,0 +1,14 @@
#!name=Apple WLOC 定位修改
#!desc=Location Spoofer 第三方代理模块
#!author=xweiba
#!homepage=https://github.com/xweiba/location-spoofer
#!category=Tools
#!arguments=经度:113.94114, 纬度:22.544577, 精度:25, 扰动半径:0, 日志级别:info
#!arguments-desc=经度/纬度: 默认坐标(在线选点储存后优先)\n精度: GPS精度(米)\n扰动半径: 每次响应在目标点周围随机偏移的最大距离(米),0为关闭\n日志级别: off/error/warn/info/debug/all\n\n使用方法: 打开选点页面 -> 选位置 -> 储存到设备
[Script]
Apple WLOC = type=http-response, pattern="^https?:\/\/(?:gs-loc(?:-cn)?\.apple\.com|gsp-ssl\.ls\.apple\.com|bluedot\.is\.autonavi\.com(?:\.gds\.alibabadns\.com)?)\/clls\/wloc", requires-body=1, binary-body-mode=1, max-size=0, timeout=30, script-path=https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc.js?v=1.0.7, argument=longitude={{{经度}}}&latitude={{{纬度}}}&accuracy={{{精度}}}&randomRadius={{{扰动半径}}}&logLevel={{{日志级别}}}
WLOC Settings = type=http-request, pattern="^https?:\/\/gs-loc(-cn)?\.apple\.com\/wloc-settings\/(save|version)", requires-body=0, max-size=0, timeout=10, script-path=https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc-settings.js?v=1.0.7
[MITM]
hostname = %APPEND% gs-loc.apple.com, gs-loc-cn.apple.com, gsp-ssl.ls.apple.com, bluedot.is.autonavi.com, bluedot.is.autonavi.com.gds.alibabadns.com
+35
View File
@@ -0,0 +1,35 @@
name: Apple WLOC 定位修改
desc: Location Spoofer 第三方代理模块
author: xweiba
homepage: https://github.com/xweiba/location-spoofer
category: Tools
http:
mitm:
- "gs-loc.apple.com"
- "gs-loc-cn.apple.com"
- "gsp-ssl.ls.apple.com"
- "bluedot.is.autonavi.com"
- "bluedot.is.autonavi.com.gds.alibabadns.com"
script:
- match: ^https?:\/\/(?:gs-loc(?:-cn)?\.apple\.com|gsp-ssl\.ls\.apple\.com|bluedot\.is\.autonavi\.com(?:\.gds\.alibabadns\.com)?)\/clls\/wloc
name: WLOC.Location
type: response
require-body: true
binary-mode: true
max-size: 0
timeout: 30
argument: longitude=113.94114&latitude=22.544577&accuracy=25&randomRadius=0&logLevel=info
- match: ^https?:\/\/gs-loc(-cn)?\.apple\.com\/wloc-settings\/(save|version)
name: WLOC.Settings
type: request
require-body: false
timeout: 10
script-providers:
WLOC.Location:
url: https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc.js?v=1.0.7
interval: 86400
WLOC.Settings:
url: https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/dist/v1/wloc-settings.js?v=1.0.7
interval: 86400
+508
View File
@@ -0,0 +1,508 @@
{
"name": "@location-spoofer/wloc-scripts",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@location-spoofer/wloc-scripts",
"version": "1.0.0",
"dependencies": {
"pako": "2.1.0"
},
"devDependencies": {
"esbuild": "0.25.8"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz",
"integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.25.8.tgz",
"integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz",
"integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.25.8.tgz",
"integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz",
"integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz",
"integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz",
"integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz",
"integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz",
"integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz",
"integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz",
"integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz",
"integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz",
"integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz",
"integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz",
"integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz",
"integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz",
"integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz",
"integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz",
"integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz",
"integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz",
"integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz",
"integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz",
"integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz",
"integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz",
"integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz",
"integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/esbuild": {
"version": "0.25.8",
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.8.tgz",
"integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.8",
"@esbuild/android-arm": "0.25.8",
"@esbuild/android-arm64": "0.25.8",
"@esbuild/android-x64": "0.25.8",
"@esbuild/darwin-arm64": "0.25.8",
"@esbuild/darwin-x64": "0.25.8",
"@esbuild/freebsd-arm64": "0.25.8",
"@esbuild/freebsd-x64": "0.25.8",
"@esbuild/linux-arm": "0.25.8",
"@esbuild/linux-arm64": "0.25.8",
"@esbuild/linux-ia32": "0.25.8",
"@esbuild/linux-loong64": "0.25.8",
"@esbuild/linux-mips64el": "0.25.8",
"@esbuild/linux-ppc64": "0.25.8",
"@esbuild/linux-riscv64": "0.25.8",
"@esbuild/linux-s390x": "0.25.8",
"@esbuild/linux-x64": "0.25.8",
"@esbuild/netbsd-arm64": "0.25.8",
"@esbuild/netbsd-x64": "0.25.8",
"@esbuild/openbsd-arm64": "0.25.8",
"@esbuild/openbsd-x64": "0.25.8",
"@esbuild/openharmony-arm64": "0.25.8",
"@esbuild/sunos-x64": "0.25.8",
"@esbuild/win32-arm64": "0.25.8",
"@esbuild/win32-ia32": "0.25.8",
"@esbuild/win32-x64": "0.25.8"
}
},
"node_modules/pako": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/pako/-/pako-2.1.0.tgz",
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
"license": "(MIT AND Zlib)"
}
}
}
+16
View File
@@ -0,0 +1,16 @@
{
"name": "@location-spoofer/wloc-scripts",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "node build.mjs",
"test": "node --test"
},
"dependencies": {
"pako": "2.1.0"
},
"devDependencies": {
"esbuild": "0.25.8"
}
}
+263
View File
@@ -0,0 +1,263 @@
export const MOTION_ACTIVITY_TYPE = 63;
export const MOTION_ACTIVITY_CONFIDENCE = 467;
export const WLOC_MARKER = Uint8Array.from([0, 0, 0, 1, 0, 0]);
const UINT32_RANGE = 0x100000000;
const concat = (...parts) => {
const length = parts.reduce((sum, part) => sum + part.length, 0);
const out = new Uint8Array(length);
let offset = 0;
for (const part of parts) {
out.set(part, offset);
offset += part.length;
}
return out;
};
const equal = (left, right) =>
left.length === right.length && left.every((value, index) => value === right[index]);
function readVarint(data, offset) {
let value = 0;
let multiplier = 1;
for (let index = 0; index < 10 && offset + index < data.length; index += 1) {
const byte = data[offset + index];
const chunk = byte & 0x7f;
if (value !== null && chunk <= Math.floor((Number.MAX_SAFE_INTEGER - value) / multiplier)) {
value += chunk * multiplier;
} else {
value = null;
}
if ((byte & 0x80) === 0) return { value, next: offset + index + 1 };
multiplier *= 0x80;
}
throw new Error("invalid varint");
}
function writeVarint(input) {
const value = Math.trunc(Number(input));
if (!Number.isSafeInteger(value)) throw new Error("varint value is not a safe integer");
let low = value >>> 0;
let high = Math.floor(value / UINT32_RANGE) >>> 0;
const bytes = [];
do {
const byte = low & 0x7f;
low = ((low >>> 7) | ((high & 0x7f) << 25)) >>> 0;
high >>>= 7;
const hasMore = high !== 0 || low !== 0;
bytes.push(byte | (hasMore ? 0x80 : 0));
} while (high !== 0 || low !== 0);
return Uint8Array.from(bytes);
}
const writeTag = (number, wireType) => writeVarint((number << 3) | wireType);
function writeLengthDelimited(number, value) {
return concat(writeTag(number, 2), writeVarint(value.length), value);
}
export function parseFields(data) {
const fields = [];
let offset = 0;
while (offset < data.length) {
const start = offset;
const tag = readVarint(data, offset);
offset = tag.next;
if (!Number.isSafeInteger(tag.value)) throw new Error("protobuf tag is too large");
const number = Math.floor(tag.value / 8);
const wireType = tag.value & 7;
if (number === 0) throw new Error("invalid protobuf field 0");
let value;
if (wireType === 0) {
const item = readVarint(data, offset);
value = data.slice(offset, item.next);
offset = item.next;
} else if (wireType === 1) {
if (offset + 8 > data.length) throw new Error("truncated fixed64");
value = data.slice(offset, offset + 8);
offset += 8;
} else if (wireType === 2) {
const length = readVarint(data, offset);
offset = length.next;
const size = length.value;
if (!Number.isSafeInteger(size) || offset + size > data.length) {
throw new Error("truncated length-delimited field");
}
value = data.slice(offset, offset + size);
offset += size;
} else if (wireType === 5) {
if (offset + 4 > data.length) throw new Error("truncated fixed32");
value = data.slice(offset, offset + 4);
offset += 4;
} else {
throw new Error(`unsupported wire type ${wireType}`);
}
fields.push({ number, wireType, value, raw: data.slice(start, offset) });
}
return fields;
}
function patchLocation(data, config, stats) {
const fields = parseFields(data);
if (!fields.some((field) => field.number === 1 && field.wireType === 0) ||
!fields.some((field) => field.number === 2 && field.wireType === 0)) {
return data;
}
let hasMotionType = false;
let hasMotionConfidence = false;
const parts = fields.map((field) => {
if (field.wireType !== 0) return field.raw;
if (field.number === 1) return concat(writeTag(1, 0), writeVarint(Math.round(config.latitude * 1e8)));
if (field.number === 2) return concat(writeTag(2, 0), writeVarint(Math.round(config.longitude * 1e8)));
if (field.number === 3) return concat(writeTag(3, 0), writeVarint(config.accuracy));
if (config.motionSimulationEnabled && field.number === 11) {
hasMotionType = true;
return concat(writeTag(11, 0), writeVarint(MOTION_ACTIVITY_TYPE));
}
if (config.motionSimulationEnabled && field.number === 12) {
hasMotionConfidence = true;
return concat(writeTag(12, 0), writeVarint(MOTION_ACTIVITY_CONFIDENCE));
}
return field.raw;
});
if (config.motionSimulationEnabled && !hasMotionType) {
parts.push(concat(writeTag(11, 0), writeVarint(MOTION_ACTIVITY_TYPE)));
}
if (config.motionSimulationEnabled && !hasMotionConfidence) {
parts.push(concat(writeTag(12, 0), writeVarint(MOTION_ACTIVITY_CONFIDENCE)));
}
const out = concat(...parts);
if (!equal(out, data)) stats.locations += 1;
return out;
}
function patchWifi(data, config, stats) {
const fields = parseFields(data);
const hasMac = fields.some((field) =>
field.number === 1 && field.wireType === 2 &&
/^[0-9a-fA-F]{1,2}(:[0-9a-fA-F]{1,2}){5}$/.test(
Array.from(field.value, (byte) => String.fromCharCode(byte)).join("")
)
);
if (!hasMac) return data;
let changed = false;
const parts = fields.map((field) => {
if (field.number !== 2 || field.wireType !== 2) return field.raw;
const value = patchLocation(field.value, config, stats);
changed ||= !equal(value, field.value);
return writeLengthDelimited(2, value);
});
if (changed) stats.wifi += 1;
return concat(...parts);
}
function patchCell(data, config, stats) {
let changed = false;
const parts = parseFields(data).map((field) => {
if (field.number !== 5 || field.wireType !== 2) return field.raw;
const value = patchLocation(field.value, config, stats);
changed ||= !equal(value, field.value);
return writeLengthDelimited(5, value);
});
if (changed) stats.cell += 1;
return concat(...parts);
}
export function patchPayload(data, config, stats = { wifi: 0, cell: 0, locations: 0 }) {
const parts = parseFields(data).map((field) => {
if (field.number === 2 && field.wireType === 2) {
return writeLengthDelimited(2, patchWifi(field.value, config, stats));
}
if ((field.number === 22 || field.number === 24) && field.wireType === 2) {
return writeLengthDelimited(field.number, patchCell(field.value, config, stats));
}
return field.raw;
});
return { data: concat(...parts), stats };
}
const uint16 = (data, offset) => (data[offset] << 8) | data[offset + 1];
const uint32 = (data, offset) =>
((data[offset] * 0x1000000) + (data[offset + 1] << 16) +
(data[offset + 2] << 8) + data[offset + 3]) >>> 0;
const writeUint16 = (value) => Uint8Array.from([(value >>> 8) & 0xff, value & 0xff]);
const writeUint32 = (value) => Uint8Array.from([
(value >>> 24) & 0xff, (value >>> 16) & 0xff, (value >>> 8) & 0xff, value & 0xff
]);
function findBytes(data, marker) {
outer: for (let offset = 0; offset <= data.length - marker.length; offset += 1) {
for (let index = 0; index < marker.length; index += 1) {
if (data[offset + index] !== marker[index]) continue outer;
}
return offset;
}
return -1;
}
function patchARPC(body, config) {
if (body.length < 2) throw new Error("short ARPC");
let offset = 2;
for (let index = 0; index < 3; index += 1) {
if (offset + 2 > body.length) throw new Error("truncated ARPC string");
offset += 2 + uint16(body, offset);
}
const lengthOffset = offset + 4;
const payloadOffset = lengthOffset + 4;
if (payloadOffset > body.length) throw new Error("truncated ARPC header");
const length = uint32(body, lengthOffset);
if (!length || payloadOffset + length > body.length) throw new Error("invalid ARPC length");
const patched = patchPayload(body.slice(payloadOffset, payloadOffset + length), config);
if (equal(patched.data, body.slice(payloadOffset, payloadOffset + length))) throw new Error("unchanged ARPC");
return { data: concat(body.slice(0, lengthOffset), writeUint32(patched.data.length),
patched.data, body.slice(payloadOffset + length)), stats: patched.stats };
}
function patchMarker(body, config) {
const markerOffset = findBytes(body, WLOC_MARKER);
if (markerOffset < 0) throw new Error("marker not found");
const lengthOffset = markerOffset + WLOC_MARKER.length;
const payloadOffset = lengthOffset + 2;
const length = uint16(body, lengthOffset);
if (!length || payloadOffset + length > body.length) throw new Error("invalid marker length");
const patched = patchPayload(body.slice(payloadOffset, payloadOffset + length), config);
if (patched.data.length > 65535 || equal(patched.data, body.slice(payloadOffset, payloadOffset + length))) {
throw new Error("unchanged marker");
}
return { data: concat(body.slice(0, lengthOffset), writeUint16(patched.data.length),
patched.data, body.slice(payloadOffset + length)), stats: patched.stats };
}
function patchSynthetic(body, offset, config) {
if (offset + 10 > body.length) throw new Error("short frame");
const length = uint16(body, offset + 8);
if (!length || offset + 10 + length > body.length) throw new Error("invalid frame");
const patched = patchPayload(body.slice(offset + 10, offset + 10 + length), config);
if (patched.data.length > 65535 || equal(patched.data, body.slice(offset + 10, offset + 10 + length))) {
throw new Error("unchanged frame");
}
return { data: concat(body.slice(0, offset + 8), writeUint16(patched.data.length),
patched.data, body.slice(offset + 10 + length)), stats: patched.stats };
}
export function patchWlocBody(body, config) {
for (const patcher of [patchARPC, patchMarker]) {
try { return patcher(body, config); } catch {}
}
const offsets = [...new Set([0, 2, 4, 6, 8, 10, 12, 14, 16,
...Array.from({ length: Math.min(96, Math.max(0, body.length - 10)) + 1 }, (_, index) => index)])];
for (const offset of offsets) {
try { return patchSynthetic(body, offset, config); } catch {}
}
for (let offset = 0; offset <= Math.min(256, body.length); offset += 1) {
try {
const patched = patchPayload(body.slice(offset), config);
if (!equal(patched.data, body.slice(offset))) {
return { data: concat(body.slice(0, offset), patched.data), stats: patched.stats };
}
} catch {}
}
throw new Error("no patchable wloc payload found");
}
export const internals = { concat, writeVarint, writeTag, writeLengthDelimited, equal };
+26
View File
@@ -0,0 +1,26 @@
import { ungzip } from "pako";
import { patchWlocBody } from "./core.js";
import {
STORAGE_KEY, finishBinary, finishPassthrough, readPersistent, responseBytes
} from "./runtime.js";
try {
const settings = readPersistent(STORAGE_KEY);
const input = responseBytes();
if (!settings || !settings.enabled || !input.length) {
finishPassthrough();
} else {
const isGzip = input.length >= 2 && input[0] === 0x1f && input[1] === 0x8b;
const body = isGzip ? ungzip(input) : input;
const patched = patchWlocBody(body, {
latitude: Number(settings.latitude),
longitude: Number(settings.longitude),
accuracy: Number(settings.accuracy != null ? settings.accuracy : 25),
motionSimulationEnabled: settings.motionSimulationEnabled === true
});
finishBinary(patched.data);
}
} catch (error) {
console.log(`[Location Spoofer] ${error && error.message ? error.message : error}`);
finishPassthrough();
}
+109
View File
@@ -0,0 +1,109 @@
export const MODULE_VERSION = "1.0.0";
export const PROTOCOL_VERSION = 1;
export const CAPABILITIES = [
"wifi", "cellTower", "arpc", "marker", "synthetic", "bare", "motionSimulation"
];
export const STORAGE_KEY = "locationSpoofer.settings.v1";
export function environment() {
if (typeof $task !== "undefined") return "quantumultX";
if (typeof $loon !== "undefined") return "loon";
if (typeof $rocket !== "undefined") return "shadowrocket";
if (typeof Egern !== "undefined") return "egern";
if (typeof $environment !== "undefined" && $environment["stash-version"]) return "stash";
if (typeof $environment !== "undefined" && $environment["surge-version"]) return "surge";
return "unknown";
}
export function readPersistent(key) {
const raw = environment() === "quantumultX"
? $prefs.valueForKey(key)
: $persistentStore.read(key);
if (!raw) return null;
try { return JSON.parse(raw); } catch { return null; }
}
export function writePersistent(key, value) {
const raw = value == null ? "" : JSON.stringify(value);
return environment() === "quantumultX"
? $prefs.setValueForKey(raw, key)
: $persistentStore.write(raw, key);
}
export function responseBytes() {
const response = typeof $response === "undefined" ? null : $response;
const value = response && response.bodyBytes != null ? response.bodyBytes : response && response.body;
if (value instanceof ArrayBuffer) return new Uint8Array(value);
if (ArrayBuffer.isView(value)) return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
if (typeof value === "string") {
return Uint8Array.from(value, (character) => character.charCodeAt(0) & 0xff);
}
return new Uint8Array();
}
function cleanHeaders(headers, length) {
const out = Object.assign({}, headers || {});
for (const name of ["Content-Encoding", "content-encoding", "Transfer-Encoding", "transfer-encoding"]) {
delete out[name];
}
out["Content-Length"] = String(length);
return out;
}
export function finishBinary(bytes) {
const response = typeof $response === "undefined" ? {} : $response;
const headers = cleanHeaders(response.headers, bytes.length);
const env = environment();
if (env === "quantumultX") {
delete headers["Content-Length"];
$done({ status: "HTTP/1.1 200 OK", headers, bodyBytes: bytes.buffer });
} else if (env === "stash") {
$done(Object.assign({}, response, { status: 200, headers, body: bytes }));
} else {
$done({ response: Object.assign({}, response, { status: 200, headers, body: bytes }) });
}
}
export function finishPassthrough() {
$done({});
}
export function finishJSON(value) {
const response = {
status: 200,
headers: { "Content-Type": "application/json; charset=utf-8" },
body: JSON.stringify(value)
};
if (environment() === "quantumultX") {
$done(Object.assign({}, response, { status: "HTTP/1.1 200 OK" }));
} else if (environment() === "stash") {
$done(response);
} else {
$done({ response });
}
}
export function queryParameters(url) {
const query = url.split("?")[1] || "";
const values = {};
query.split("&").forEach((item) => {
if (!item) return;
const separator = item.indexOf("=");
const rawKey = separator < 0 ? item : item.slice(0, separator);
const rawValue = separator < 0 ? "" : item.slice(separator + 1);
let key = rawKey;
let value = rawValue;
try { key = decodeURIComponent(rawKey.replace(/\+/g, " ")); } catch {}
try { value = decodeURIComponent(rawValue.replace(/\+/g, " ")); } catch {}
if (!Object.prototype.hasOwnProperty.call(values, key)) values[key] = value;
});
return values;
}
export function requestPath(url) {
const withoutQuery = url.split("?")[0];
const scheme = withoutQuery.indexOf("://");
if (scheme < 0) return withoutQuery;
const path = withoutQuery.indexOf("/", scheme + 3);
return path < 0 ? "/" : withoutQuery.slice(path);
}
+48
View File
@@ -0,0 +1,48 @@
import {
CAPABILITIES, MODULE_VERSION, PROTOCOL_VERSION, STORAGE_KEY,
finishJSON, queryParameters, readPersistent, requestPath, writePersistent
} from "./runtime.js";
const url = typeof $request === "undefined" ? "" : ($request.url || "");
const path = requestPath(url);
const parameters = queryParameters(url);
if (path === "/wloc-settings/version") {
finishJSON({
success: true,
moduleVersion: MODULE_VERSION,
protocolVersion: PROTOCOL_VERSION,
capabilities: CAPABILITIES
});
} else if (parameters.action === "query") {
const settings = readPersistent(STORAGE_KEY);
finishJSON(settings && settings.enabled
? { success: true, longitude: settings.longitude, latitude: settings.latitude,
accuracy: settings.accuracy, motionSimulationEnabled: settings.motionSimulationEnabled === true }
: { success: false, error: "无已保存的坐标" });
} else if (parameters.action === "clear") {
writePersistent(STORAGE_KEY, null);
finishJSON({ success: true });
} else {
const longitude = Number(parameters.lon != null ? parameters.lon : parameters.longitude);
const latitude = Number(parameters.lat != null ? parameters.lat : parameters.latitude);
const accuracy = Number(parameters.acc != null
? parameters.acc
: (parameters.accuracy != null ? parameters.accuracy : 25));
if (!Number.isFinite(longitude) || !Number.isFinite(latitude)) {
finishJSON({ success: false, error: "缺少 lon/lat 参数" });
} else {
const settings = {
enabled: true,
longitude,
latitude,
accuracy,
motionSimulationEnabled: parameters.motion === "1"
};
const success = writePersistent(STORAGE_KEY, settings);
finishJSON(success
? { success: true, longitude, latitude, accuracy,
motionSimulationEnabled: settings.motionSimulationEnabled }
: { success: false, error: "保存配置失败" });
}
}
@@ -0,0 +1,52 @@
import test from "node:test";
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import vm from "node:vm";
const bundles = [
new URL("../dist/v1/wloc.js", import.meta.url),
new URL("../dist/v1/wloc-settings.js", import.meta.url)
];
test("generated bundles avoid newer JavaScriptCore runtime requirements", async () => {
for (const bundle of bundles) {
const source = await readFile(bundle, "utf8");
for (const unsupported of [
/\bBigInt\b/,
/\bglobalThis\b/,
/\bURLSearchParams\b/,
/\bObject\.fromEntries\b/,
/\?\./,
/\?\?/
]) {
assert.equal(unsupported.test(source), false, `${bundle.pathname} contains ${unsupported}`);
}
}
});
test("settings bundle runs without modern URL and text globals", async () => {
const source = await readFile(bundles[1], "utf8");
let result;
const storage = new Map();
vm.runInNewContext(source, {
$rocket: {},
$request: {
url: "https://gs-loc.apple.com/wloc-settings/save?lon=121.1&lat=31.2&acc=25"
},
$persistentStore: {
read: (key) => storage.get(key) || null,
write: (value, key) => {
storage.set(key, value);
return true;
}
},
$done: (value) => { result = value; },
JSON,
Number,
Object,
String,
decodeURIComponent
});
assert.equal(JSON.parse(result.response.body).success, true);
});
+85
View File
@@ -0,0 +1,85 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
MOTION_ACTIVITY_CONFIDENCE, MOTION_ACTIVITY_TYPE, internals, parseFields, patchWlocBody
} from "../src/core.js";
const { concat, writeVarint, writeTag, writeLengthDelimited } = internals;
function location(withMotion = false) {
const fields = [
concat(writeTag(1, 0), writeVarint(100)),
concat(writeTag(2, 0), writeVarint(200)),
concat(writeTag(3, 0), writeVarint(25))
];
if (withMotion) {
fields.push(concat(writeTag(11, 0), writeVarint(7)));
fields.push(concat(writeTag(12, 0), writeVarint(88)));
}
return concat(...fields);
}
function wifiPayload(value = location()) {
const device = concat(
writeLengthDelimited(1, new TextEncoder().encode("aa:bb:cc:dd:ee:ff")),
writeLengthDelimited(2, value)
);
return writeLengthDelimited(2, device);
}
function frame(payload) {
return concat(Uint8Array.from([0, 1, 0, 0, 0, 1, 0, 0]),
Uint8Array.from([payload.length >> 8, payload.length & 0xff]), payload);
}
const config = {
latitude: 31.230416,
longitude: 121.473701,
accuracy: 50,
motionSimulationEnabled: false
};
test("patches synthetic Wi-Fi response", () => {
const result = patchWlocBody(frame(wifiPayload()), config);
assert.equal(result.stats.wifi, 1);
assert.equal(result.stats.locations, 1);
});
test("preserves motion fields while disabled", () => {
const result = patchWlocBody(frame(wifiPayload(location(true))), config);
const payload = result.data.slice(10);
assert.ok(payload.includes(7));
assert.ok(payload.includes(88));
});
test("replaces motion fields while enabled", () => {
const result = patchWlocBody(frame(wifiPayload(location(true))), {
...config, motionSimulationEnabled: true
});
const root = parseFields(result.data.slice(10));
const device = parseFields(root[0].value);
const fields = parseFields(device.find((field) => field.number === 2).value);
const motionType = fields.find((field) => field.number === 11);
const motionConfidence = fields.find((field) => field.number === 12);
assert.deepEqual(motionType.value, writeVarint(MOTION_ACTIVITY_TYPE));
assert.deepEqual(motionConfidence.value, writeVarint(MOTION_ACTIVITY_CONFIDENCE));
});
test("patches CellTower fields 22 and 24", () => {
for (const number of [22, 24]) {
const cell = writeLengthDelimited(5, location());
const result = patchWlocBody(frame(writeLengthDelimited(number, cell)), config);
assert.equal(result.stats.cell, 1);
}
});
test("encodes signed int64 coordinates without BigInt", () => {
assert.deepEqual(
Array.from(writeVarint(-18_000_000_000)),
[128, 152, 247, 248, 188, 255, 255, 255, 255, 1]
);
assert.deepEqual(
Array.from(writeVarint(18_000_000_000)),
[128, 232, 136, 135, 67]
);
});
+46 -21
View File
@@ -1,20 +1,26 @@
# Third-party proxy modules # Third-party proxy modules
Third-party proxy mode (Surge / Quantumult X / Loon / Shadowrocket / Stash / The third-party proxy modules and WLOC scripts are maintained in this
Egern) now relies entirely on the upstream repository. The original `Yu9191/wloc` repository was deleted, and its Raw
[Yu9191/wloc](https://github.com/Yu9191/wloc) modules. The App no longer URLs now return HTTP 404, so it is retained only as provenance and is not a
maintains or ships project-owned module/script copies, so it never drifts runtime dependency.
from the upstream protocol.
## Subscription addresses ## Hosted files
The App builds each client's module subscription URL directly from the - Mirrored module subscriptions: `Resources/ThirdPartyProxyModules/`
upstream repository: - Direct module subscriptions: `ThirdParty/WlocScripts/modules/direct/`
- Versioned generated scripts: `ThirdParty/WlocScripts/dist/v1/`
- Script source and tests: `ThirdParty/WlocScripts/src/` and
`ThirdParty/WlocScripts/test/`
- default mirror (gh-proxy): The App appends `?v=1.0.7` to module subscription URLs, and every module uses
`https://gh-proxy.org/https://raw.githubusercontent.com/Yu9191/wloc/refs/heads/main/modules/<file>` the same query parameter for its generated script URLs, to invalidate client
caches:
- default mirror:
`https://gh-proxy.org/https://raw.githubusercontent.com/xweiba/location-spoofer/main/Resources/ThirdPartyProxyModules/<file>?v=1.0.7`
- direct: - direct:
`https://raw.githubusercontent.com/Yu9191/wloc/refs/heads/main/modules/<file>` `https://raw.githubusercontent.com/xweiba/location-spoofer/main/ThirdParty/WlocScripts/modules/direct/<file>?v=1.0.7`
| Module file | Client | | Module file | Client |
|---|---| |---|---|
@@ -24,17 +30,36 @@ upstream repository:
| `wloc.lpx` | Loon | | `wloc.lpx` | Loon |
| `wloc.stoverride` | Stash | | `wloc.stoverride` | Stash |
No `?v=` cache-bust is appended: the URL points at upstream's latest content, The hosted files become downloadable from these URLs after this change is
and re-importing the subscription in the proxy client re-fetches it. merged into `main`. Until then, validate them from the checked-out paths.
## Script protocol ## Script protocol
The upstream `wloc.js` patches Apple WLOC responses and reads coordinates from `wloc.js` patches Apple WLOC responses and reads coordinates from the
the `wloc_settings` persistent key or the module `argument` config. The `wloc_settings` persistent key or the module `argument` config.
upstream `wloc-settings.js` implements `wloc-settings/save` (query/clear/save) `wloc-settings.js` implements `wloc-settings/save` (query/clear/save) using
using `lon`/`lat`/`acc`/`randomRadius` parameters. `lon`/`lat`/`acc`/`randomRadius` parameters.
The App's third-party save sends `lon`/`lat`/`acc`, matching the upstream The App's third-party save sends `lon`/`lat`/`acc`. Motion-state simulation
script. Motion-state simulation (fields 11/12) is **not** implemented by the (fields 11/12) is unavailable in third-party mode; it remains available in
upstream scripts and is unavailable in third-party mode; it remains available App Mode through the built-in proxy.
in APP mode (built-in proxy).
## Provenance and maintenance
The response-rewrite approach and original module structure were derived from
`Yu9191/wloc`. The repository is no longer available, but its attribution is
preserved in module metadata and project acknowledgements. Bundled dependency
notices are recorded in `ThirdParty/WlocScripts/THIRD_PARTY_NOTICES.md`.
Build and test the scripts with:
```bash
cd ThirdParty/WlocScripts
npm ci
npm test
npm run build
```
If an authoritative, maintained upstream returns, compare its license,
protocol compatibility, tests, and release guarantees before considering a
switch away from the project-owned copies.
+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: