mirror of
https://github.com/xweiba/location-spoofer.git
synced 2026-09-24 23:51:58 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4aa4c6aa6c | ||
|
|
ca239411ee | ||
|
|
5b9fedfe9e | ||
|
|
228b67b210 | ||
|
|
f4712f7fb2 | ||
|
|
393c7cc215 |
@@ -0,0 +1,23 @@
|
|||||||
|
name: Contract Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
contract-tests:
|
||||||
|
runs-on: macos-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Run contract tests
|
||||||
|
run: ./Tests/run_all_contract_tests.sh
|
||||||
@@ -28,6 +28,9 @@ jobs:
|
|||||||
- name: Install XcodeGen
|
- name: Install XcodeGen
|
||||||
run: brew install xcodegen
|
run: brew install xcodegen
|
||||||
|
|
||||||
|
- name: Run contract tests
|
||||||
|
run: ./Tests/run_all_contract_tests.sh
|
||||||
|
|
||||||
- name: Build unsigned IPA
|
- name: Build unsigned IPA
|
||||||
run: ./build.sh
|
run: ./build.sh
|
||||||
|
|
||||||
@@ -37,16 +40,26 @@ jobs:
|
|||||||
- name: Validate archived release notes
|
- name: Validate archived release notes
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ github.ref_name }}"
|
VERSION="${{ github.ref_name }}"
|
||||||
NOTES_FILE="docs/releases/${VERSION}.md"
|
for NOTES_FILE in \
|
||||||
if [ ! -s "$NOTES_FILE" ]; then
|
"docs/releases/${VERSION}.en.md" \
|
||||||
echo "::error::缺少版本归档 $NOTES_FILE。请在打 tag 前运行 ./Scripts/generate-release-notes.sh $VERSION 并提交。"
|
"docs/releases/${VERSION}.md" \
|
||||||
exit 1
|
"docs/releases/${VERSION}.zh-Hant.md"; do
|
||||||
fi
|
if [ ! -s "$NOTES_FILE" ]; then
|
||||||
if ! grep -Fq "# ${VERSION}" "$NOTES_FILE"; then
|
echo "::error::Missing localized release notes: $NOTES_FILE"
|
||||||
echo "::error::$NOTES_FILE 的标题必须包含 # ${VERSION}"
|
exit 1
|
||||||
exit 1
|
fi
|
||||||
fi
|
if ! grep -Fq "# ${VERSION}" "$NOTES_FILE"; then
|
||||||
cp "$NOTES_FILE" /tmp/release_notes.md
|
echo "::error::$NOTES_FILE must contain the # ${VERSION} heading"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
{
|
||||||
|
cat "docs/releases/${VERSION}.en.md"
|
||||||
|
printf '\n---\n\n'
|
||||||
|
cat "docs/releases/${VERSION}.md"
|
||||||
|
printf '\n---\n\n'
|
||||||
|
cat "docs/releases/${VERSION}.zh-Hant.md"
|
||||||
|
} > /tmp/release_notes.md
|
||||||
|
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
|
|||||||
+14
-14
@@ -95,10 +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
|
||||||
let savedCoordinate = active ? String(localized: "是") : String(localized: "否")
|
let savedCoordinate = active ? AppLocalization.string("是") : AppLocalization.string("否")
|
||||||
testLog = String(localized: "第三方代理测试模式:模块连接成功;已保存坐标=\(savedCoordinate)")
|
testLog = AppLocalization.string("第三方代理测试模式:模块连接成功;已保存坐标=\(savedCoordinate)")
|
||||||
} catch {
|
} catch {
|
||||||
testLog = String(localized: "第三方代理测试模式:模块连接失败;\(error.localizedDescription)")
|
testLog = AppLocalization.string("第三方代理测试模式:模块连接失败;\(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_ = await setup.runVerificationTest()
|
_ = await setup.runVerificationTest()
|
||||||
@@ -132,21 +132,21 @@ struct BugReportView: View {
|
|||||||
private func bugReport(appVersion: String, systemVersion: String, testLog: String) -> String {
|
private func bugReport(appVersion: String, systemVersion: String, testLog: String) -> String {
|
||||||
let client = runtimeMode.mode == .thirdParty
|
let client = runtimeMode.mode == .thirdParty
|
||||||
? thirdPartyClient.selectedClient.name
|
? thirdPartyClient.selectedClient.name
|
||||||
: String(localized: "不适用")
|
: AppLocalization.string("不适用")
|
||||||
let reproducible = isReproducible ? String(localized: "是") : String(localized: "否")
|
let reproducible = isReproducible ? AppLocalization.string("是") : AppLocalization.string("否")
|
||||||
let diagnostics = testLog.isEmpty ? String(localized: "(无诊断数据)") : testLog
|
let diagnostics = testLog.isEmpty ? AppLocalization.string("(无诊断数据)") : testLog
|
||||||
return """
|
return """
|
||||||
### \(String(localized: "环境信息"))
|
### \(AppLocalization.string("环境信息"))
|
||||||
\(String(localized: "App 版本")): \(appVersion)
|
\(AppLocalization.string("App 版本")): \(appVersion)
|
||||||
\(String(localized: "系统版本")): iOS \(systemVersion)
|
\(AppLocalization.string("系统版本")): iOS \(systemVersion)
|
||||||
\(String(localized: "运行模式")): \(runtimeMode.mode.displayName)
|
\(AppLocalization.string("运行模式")): \(runtimeMode.mode.displayName)
|
||||||
\(String(localized: "第三方客户端")): \(client)
|
\(AppLocalization.string("第三方客户端")): \(client)
|
||||||
\(String(localized: "可复现环境")): \(reproducible)
|
\(AppLocalization.string("可复现环境")): \(reproducible)
|
||||||
|
|
||||||
### \(String(localized: "问题描述"))
|
### \(AppLocalization.string("问题描述"))
|
||||||
\(description.trimmingCharacters(in: .whitespacesAndNewlines))
|
\(description.trimmingCharacters(in: .whitespacesAndNewlines))
|
||||||
|
|
||||||
### \(String(localized: "诊断日志"))
|
### \(AppLocalization.string("诊断日志"))
|
||||||
```
|
```
|
||||||
\(diagnostics)
|
\(diagnostics)
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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
|
||||||
?? String(localized: "更新说明暂时无法加载,请前往最新 Release 页面查看。")
|
?? AppLocalization.string("更新说明暂时无法加载,请前往最新 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
|
||||||
?? String(localized: "更新说明暂时无法加载,请前往最新 Release 页面查看。")
|
?? AppLocalization.string("更新说明暂时无法加载,请前往最新 Release 页面查看。")
|
||||||
return Alert(
|
return Alert(
|
||||||
title: Text("发现新版本"),
|
title: Text("发现新版本"),
|
||||||
message: Text(
|
message: Text(
|
||||||
|
|||||||
+11
-11
@@ -109,9 +109,9 @@ struct RuntimeLogsView: View {
|
|||||||
let result = await setup.runVerificationTest()
|
let result = await setup.runVerificationTest()
|
||||||
testSucceeded = result.isSuccess
|
testSucceeded = result.isSuccess
|
||||||
testResult = result.isSuccess
|
testResult = result.isSuccess
|
||||||
? String(localized: "环境检测通过")
|
? AppLocalization.string("环境检测通过")
|
||||||
: String(localized: "环境检测失败: \(result.localizedTitle)")
|
: AppLocalization.string("环境检测失败: \(result.localizedTitle)")
|
||||||
if !result.isSuccess { testResult += String(localized: ",查看下方日志") }
|
if !result.isSuccess { testResult += AppLocalization.string(",查看下方日志") }
|
||||||
testMessage = setup.testLog
|
testMessage = setup.testLog
|
||||||
}
|
}
|
||||||
isTesting = false; refresh()
|
isTesting = false; refresh()
|
||||||
@@ -248,26 +248,26 @@ struct RuntimeLogsView: View {
|
|||||||
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
|
||||||
testSucceeded = true
|
testSucceeded = true
|
||||||
testResult = active ? String(localized: "第三方模块连接通过,已有坐标") : String(localized: "第三方模块连接通过,暂无坐标")
|
testResult = active ? AppLocalization.string("第三方模块连接通过,已有坐标") : AppLocalization.string("第三方模块连接通过,暂无坐标")
|
||||||
testMessage = thirdPartyTestLog(active: active)
|
testMessage = thirdPartyTestLog(active: active)
|
||||||
} catch {
|
} catch {
|
||||||
testSucceeded = false
|
testSucceeded = false
|
||||||
testResult = String(localized: "第三方模块连接失败")
|
testResult = AppLocalization.string("第三方模块连接失败")
|
||||||
testMessage = thirdPartyTestLog(error: error)
|
testMessage = thirdPartyTestLog(error: error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func thirdPartyTestLog(active: Bool? = nil, error: Error? = nil) -> String {
|
private func thirdPartyTestLog(active: Bool? = nil, error: Error? = nil) -> String {
|
||||||
var lines = [
|
var lines = [
|
||||||
String(localized: "======== 第三方代理连接检测 ========"),
|
AppLocalization.string("======== 第三方代理连接检测 ========"),
|
||||||
String(localized: "模式: 测试模式"),
|
AppLocalization.string("模式: 测试模式"),
|
||||||
String(localized: "请求: wloc-settings/save?action=query")
|
AppLocalization.string("请求: wloc-settings/save?action=query")
|
||||||
]
|
]
|
||||||
if let active {
|
if let active {
|
||||||
lines.append(String(localized: "拦截响应: 有效 JSON"))
|
lines.append(AppLocalization.string("拦截响应: 有效 JSON"))
|
||||||
lines.append(String(localized: "已保存坐标: \(active ? String(localized: "是") : String(localized: "否"))"))
|
lines.append(AppLocalization.string("已保存坐标: \(active ? AppLocalization.string("是") : AppLocalization.string("否"))"))
|
||||||
} else if let error {
|
} else if let error {
|
||||||
lines.append(String(localized: "结果: \(error.localizedDescription)"))
|
lines.append(AppLocalization.string("结果: \(error.localizedDescription)"))
|
||||||
}
|
}
|
||||||
return lines.joined(separator: "\n")
|
return lines.joined(separator: "\n")
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-34
@@ -221,13 +221,13 @@ struct FirstSetupView: View {
|
|||||||
}
|
}
|
||||||
guard !setup.message.isEmpty else { return nil }
|
guard !setup.message.isEmpty else { return nil }
|
||||||
return [
|
return [
|
||||||
String(localized: "======== 第三方代理运行检测 ========"),
|
AppLocalization.string("======== 第三方代理运行检测 ========"),
|
||||||
String(localized: "当前客户端:\(thirdPartyClient.selectedClient.name)"),
|
AppLocalization.string("当前客户端:\(thirdPartyClient.selectedClient.name)"),
|
||||||
String(localized: "触发来源:地图或设置中的第三方代理操作"),
|
AppLocalization.string("触发来源:地图或设置中的第三方代理操作"),
|
||||||
String(localized: "请求动作:WLOC 配置接口"),
|
AppLocalization.string("请求动作:WLOC 配置接口"),
|
||||||
String(localized: "检测结果:失败"),
|
AppLocalization.string("检测结果:失败"),
|
||||||
String(localized: "错误详情:\(setup.message)"),
|
AppLocalization.string("错误详情:\(setup.message)"),
|
||||||
String(localized: "处理建议:确认模块已启用,并检查 MITM、证书和代理/VPN 连接。")
|
AppLocalization.string("处理建议:确认模块已启用,并检查 MITM、证书和代理/VPN 连接。")
|
||||||
].joined(separator: "\n")
|
].joined(separator: "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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 ?? String(localized: "无法准备证书下载页面,请查看诊断日志")
|
setupActionError = setup.proxy.error ?? AppLocalization.string("无法准备证书下载页面,请查看诊断日志")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -468,7 +468,7 @@ struct FirstSetupView: View {
|
|||||||
|
|
||||||
Text("返回格式")
|
Text("返回格式")
|
||||||
.font(.subheadline.bold())
|
.font(.subheadline.bold())
|
||||||
Text(String(localized: """
|
Text(AppLocalization.string("""
|
||||||
成功:{"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":"错误说明"}
|
||||||
"""))
|
"""))
|
||||||
@@ -565,7 +565,7 @@ struct FirstSetupView: View {
|
|||||||
if let thirdPartyFailureLog {
|
if let thirdPartyFailureLog {
|
||||||
testResultView(
|
testResultView(
|
||||||
success: false,
|
success: false,
|
||||||
title: String(localized: "接口连接失败"),
|
title: AppLocalization.string("接口连接失败"),
|
||||||
log: thirdPartyFailureLog
|
log: thirdPartyFailureLog
|
||||||
)
|
)
|
||||||
.id("thirdPartyFailureLog")
|
.id("thirdPartyFailureLog")
|
||||||
@@ -698,7 +698,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 = String(localized: "无法打开 \(client.name),请确认客户端已安装后手动打开。")
|
manualHint = AppLocalization.string("无法打开 \(client.name),请确认客户端已安装后手动打开。")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -760,7 +760,7 @@ struct FirstSetupView: View {
|
|||||||
let success = result.isSuccess
|
let success = result.isSuccess
|
||||||
testResultView(
|
testResultView(
|
||||||
success: success,
|
success: success,
|
||||||
title: success ? String(localized: "环境检测通过") : failureSummary(result),
|
title: success ? AppLocalization.string("环境检测通过") : failureSummary(result),
|
||||||
log: setup.testLog
|
log: setup.testLog
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -889,17 +889,17 @@ struct FirstSetupView: View {
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
thirdPartyTestFailure = ThirdPartyConnectionTestFailure(message: [
|
thirdPartyTestFailure = ThirdPartyConnectionTestFailure(message: [
|
||||||
String(localized: "======== 第三方代理连接检测 ========"),
|
AppLocalization.string("======== 第三方代理连接检测 ========"),
|
||||||
String(localized: "当前客户端:\(client.name)"),
|
AppLocalization.string("当前客户端:\(client.name)"),
|
||||||
String(localized: "配置接口:/wloc-settings/save"),
|
AppLocalization.string("配置接口:/wloc-settings/save"),
|
||||||
String(localized: "请求动作:WLOC query"),
|
AppLocalization.string("请求动作:WLOC query"),
|
||||||
String(localized: "检查范围:模块拦截、MITM、证书、代理/VPN 连接"),
|
AppLocalization.string("检查范围:模块拦截、MITM、证书、代理/VPN 连接"),
|
||||||
String(localized: "连接状态:\(connectionState)"),
|
AppLocalization.string("连接状态:\(connectionState)"),
|
||||||
String(localized: "检测结果:失败"),
|
AppLocalization.string("检测结果:失败"),
|
||||||
String(localized: "耗时:\(elapsedMilliseconds) ms"),
|
AppLocalization.string("耗时:\(elapsedMilliseconds) ms"),
|
||||||
String(localized: "错误类型:\(errorType)"),
|
AppLocalization.string("错误类型:\(errorType)"),
|
||||||
String(localized: "错误详情:\(error.localizedDescription)"),
|
AppLocalization.string("错误详情:\(error.localizedDescription)"),
|
||||||
String(localized: "处理建议:\(ThirdPartyProxyError.recoverySuggestion(for: error))。")
|
AppLocalization.string("处理建议:\(ThirdPartyProxyError.recoverySuggestion(for: error))。")
|
||||||
].joined(separator: "\n"))
|
].joined(separator: "\n"))
|
||||||
showsThirdPartyFailureLog = true
|
showsThirdPartyFailureLog = true
|
||||||
}
|
}
|
||||||
@@ -909,11 +909,11 @@ struct FirstSetupView: View {
|
|||||||
private var thirdPartyConnectionStateDescription: String {
|
private var thirdPartyConnectionStateDescription: String {
|
||||||
switch thirdPartyProxy.connectionState {
|
switch thirdPartyProxy.connectionState {
|
||||||
case .unknown:
|
case .unknown:
|
||||||
return String(localized: "未检测")
|
return AppLocalization.string("未检测")
|
||||||
case .connected(let active):
|
case .connected(let active):
|
||||||
return active ? String(localized: "已连接,有保存坐标") : String(localized: "已连接,无保存坐标")
|
return active ? AppLocalization.string("已连接,有保存坐标") : AppLocalization.string("已连接,无保存坐标")
|
||||||
case .failed(let message):
|
case .failed(let message):
|
||||||
return String(localized: "连接失败(\(message))")
|
return AppLocalization.string("连接失败(\(message))")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -966,14 +966,14 @@ struct FirstSetupView: View {
|
|||||||
|
|
||||||
private func failureSummary(_ result: VerificationResult) -> String {
|
private func failureSummary(_ result: VerificationResult) -> String {
|
||||||
switch result {
|
switch result {
|
||||||
case .certNotTrusted: return String(localized: "证书尚未安装或信任")
|
case .certNotTrusted: return AppLocalization.string("证书尚未安装或信任")
|
||||||
case .wifiProxyNotConfigured: return String(localized: "Wi-Fi 代理未正确设置")
|
case .wifiProxyNotConfigured: return AppLocalization.string("Wi-Fi 代理未正确设置")
|
||||||
case .proxyNotRunning: return String(localized: "本地代理未能启动")
|
case .proxyNotRunning: return AppLocalization.string("本地代理未能启动")
|
||||||
case .verificationInProgress: return String(localized: "检测仍在进行")
|
case .verificationInProgress: return AppLocalization.string("检测仍在进行")
|
||||||
case .verificationSuperseded: return String(localized: "检测结果已过期")
|
case .verificationSuperseded: return AppLocalization.string("检测结果已过期")
|
||||||
case .coordinateWriteFailed: return String(localized: "坐标写入失败")
|
case .coordinateWriteFailed: return AppLocalization.string("坐标写入失败")
|
||||||
case .patchFailed: return String(localized: "定位改写检测失败")
|
case .patchFailed: return AppLocalization.string("定位改写检测失败")
|
||||||
case .success: return String(localized: "环境检测通过")
|
case .success: return AppLocalization.string("环境检测通过")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-12
@@ -301,7 +301,7 @@ struct MapHomeView: View {
|
|||||||
}
|
}
|
||||||
} message: {
|
} message: {
|
||||||
Text(
|
Text(
|
||||||
"你正在使用 \(communityContributionClient?.name ?? String(localized: "第三方客户端"))。点击“去提交”会先复制投稿模板,并在 App 内打开社区页面。采纳后将收录到 README,可选择是否匿名署名。"
|
"你正在使用 \(communityContributionClient?.name ?? AppLocalization.string("第三方客户端"))。点击“去提交”会先复制投稿模板,并在 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 ?? String(localized: "当前选点")).font(.subheadline.weight(.semibold)).lineLimit(1)
|
Text(mapState.displayName ?? AppLocalization.string("当前选点")).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)
|
||||||
}
|
}
|
||||||
@@ -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 String(localized: "同步到第三方代理")
|
case .idle: return AppLocalization.string("同步到第三方代理")
|
||||||
case .verifying: return String(localized: "检测并同步中…")
|
case .verifying: return AppLocalization.string("检测并同步中…")
|
||||||
case .active: return String(localized: "停止第三方虚拟定位")
|
case .active: return AppLocalization.string("停止第三方虚拟定位")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch spoofState {
|
switch spoofState {
|
||||||
case .idle: return String(localized: "开始虚拟定位")
|
case .idle: return AppLocalization.string("开始虚拟定位")
|
||||||
case .verifying: return String(localized: "验证环境中…")
|
case .verifying: return AppLocalization.string("验证环境中…")
|
||||||
case .active: return String(localized: "停止虚拟定位")
|
case .active: return AppLocalization.string("停止虚拟定位")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1092,7 +1092,7 @@ struct MapHomeView: View {
|
|||||||
"请求动作": "WLOC query",
|
"请求动作": "WLOC query",
|
||||||
"错误": response.error ?? "未知错误"
|
"错误": response.error ?? "未知错误"
|
||||||
])
|
])
|
||||||
setup.requestThirdPartySetup(message: response.error ?? String(localized: "第三方代理查询失败"))
|
setup.requestThirdPartySetup(message: response.error ?? AppLocalization.string("第三方代理查询失败"))
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
spoofState = .idle
|
spoofState = .idle
|
||||||
@@ -1145,7 +1145,7 @@ struct MapHomeView: View {
|
|||||||
"Wi-Fi接口": String(net.isWiFiEnabled)
|
"Wi-Fi接口": String(net.isWiFiEnabled)
|
||||||
])
|
])
|
||||||
activeTip = nil
|
activeTip = nil
|
||||||
setup.requestSetup(message: String(localized: "当前未连接可用的 Wi-Fi,请连接 Wi-Fi 后配置 127.0.0.1:8888 手动代理。"))
|
setup.requestSetup(message: AppLocalization.string("当前未连接可用的 Wi-Fi,请连接 Wi-Fi 后配置 127.0.0.1:8888 手动代理。"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1520,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 ?? String(localized: "未命名"),
|
name: item.name ?? AppLocalization.string("未命名"),
|
||||||
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 }
|
||||||
@@ -1532,7 +1532,7 @@ struct MapHomeView: View {
|
|||||||
])
|
])
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
if searchResults.isEmpty { searchError = String(localized: "没有找到相关地点") }
|
if searchResults.isEmpty { searchError = AppLocalization.string("没有找到相关地点") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ struct PaopaoLocationSpooferApp: App {
|
|||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
ContentView()
|
ContentView()
|
||||||
|
.environment(\.locale, AppLocalization.locale)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = String(localized: "证书下载地址无效")
|
error = AppLocalization.string("证书下载地址无效")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
error = nil
|
error = nil
|
||||||
return url
|
return url
|
||||||
} catch {
|
} catch {
|
||||||
self.error = String(localized: "启动代理失败: \(error.localizedDescription)")
|
self.error = AppLocalization.string("启动代理失败: \(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? { String(localized: "Go proxy 启动失败") }
|
var errorDescription: String? { AppLocalization.string("Go proxy 启动失败") }
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-21
@@ -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 = String(localized: "代理操作失败")
|
@State private var proxyOperationAlertTitle = AppLocalization.string("代理操作失败")
|
||||||
@State private var modeOperationRunning = false
|
@State private var modeOperationRunning = false
|
||||||
@State private var copiedClient: ThirdPartyProxyClient?
|
@State private var copiedClient: ThirdPartyProxyClient?
|
||||||
@State private var copiedMITMHostname: String?
|
@State private var copiedMITMHostname: String?
|
||||||
@@ -319,12 +319,12 @@ struct SettingsView: View {
|
|||||||
)
|
)
|
||||||
case .available(let prompt):
|
case .available(let prompt):
|
||||||
let details = prompt.releaseNotes
|
let details = prompt.releaseNotes
|
||||||
?? String(localized: "更新说明暂时无法加载,请前往最新 Release 页面查看。")
|
?? AppLocalization.string("更新说明暂时无法加载,请前往最新 Release 页面查看。")
|
||||||
let message: String
|
let message: String
|
||||||
if prompt.requirement == .required {
|
if prompt.requirement == .required {
|
||||||
message = String(localized: "当前版本 \(prompt.currentVersion) 已停止支持,请更新到 \(prompt.latestVersion) 后继续使用。\n\n\(details)")
|
message = AppLocalization.string("当前版本 \(prompt.currentVersion) 已停止支持,请更新到 \(prompt.latestVersion) 后继续使用。\n\n\(details)")
|
||||||
} else {
|
} else {
|
||||||
message = String(localized: "当前版本 \(prompt.currentVersion),最新版本 \(prompt.latestVersion)。\n\n\(details)")
|
message = AppLocalization.string("当前版本 \(prompt.currentVersion),最新版本 \(prompt.latestVersion)。\n\n\(details)")
|
||||||
}
|
}
|
||||||
return Alert(
|
return Alert(
|
||||||
title: prompt.requirement == .required ? Text("需要更新") : Text("发现新版本"),
|
title: prompt.requirement == .required ? Text("需要更新") : Text("发现新版本"),
|
||||||
@@ -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 = String(localized: "代理操作失败")
|
proxyOperationAlertTitle = AppLocalization.string("代理操作失败")
|
||||||
proxyOperationError = error.localizedDescription
|
proxyOperationError = error.localizedDescription
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -482,27 +482,27 @@ struct SettingsView: View {
|
|||||||
|
|
||||||
private var thirdPartyStatusText: String {
|
private var thirdPartyStatusText: String {
|
||||||
switch thirdPartyProxy.connectionState {
|
switch thirdPartyProxy.connectionState {
|
||||||
case .unknown: return String(localized: "未检测")
|
case .unknown: return AppLocalization.string("未检测")
|
||||||
case .connected(let active): return active ? String(localized: "已连接,有坐标") : String(localized: "已连接,无坐标")
|
case .connected(let active): return active ? AppLocalization.string("已连接,有坐标") : AppLocalization.string("已连接,无坐标")
|
||||||
case .failed: return String(localized: "连接失败")
|
case .failed: return AppLocalization.string("连接失败")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var virtualLocationStatusText: String {
|
private var virtualLocationStatusText: String {
|
||||||
if runtimeMode.mode == .localWiFi {
|
if runtimeMode.mode == .localWiFi {
|
||||||
return actions.virtualLocationEnabled ? String(localized: "已开启") : String(localized: "已关闭")
|
return actions.virtualLocationEnabled ? AppLocalization.string("已开启") : AppLocalization.string("已关闭")
|
||||||
}
|
}
|
||||||
if case .connected(let active) = thirdPartyProxy.connectionState {
|
if case .connected(let active) = thirdPartyProxy.connectionState {
|
||||||
return active ? String(localized: "第三方已保存") : String(localized: "未保存")
|
return active ? AppLocalization.string("第三方已保存") : AppLocalization.string("未保存")
|
||||||
}
|
}
|
||||||
return String(localized: "未知")
|
return AppLocalization.string("未知")
|
||||||
}
|
}
|
||||||
|
|
||||||
private var workflowDescription: String {
|
private var workflowDescription: String {
|
||||||
if runtimeMode.mode == .thirdParty {
|
if runtimeMode.mode == .thirdParty {
|
||||||
return String(localized: "App 只负责地图选点、收藏和发送 WGS-84 坐标。第三方代理客户端通过模块拦截 Apple WLOC 请求并持久化当前坐标;本模式不启动本机代理,不使用 App 的 CA,也不需要配置 127.0.0.1:8888。")
|
return AppLocalization.string("App 只负责地图选点、收藏和发送 WGS-84 坐标。第三方代理客户端通过模块拦截 Apple WLOC 请求并持久化当前坐标;本模式不启动本机代理,不使用 App 的 CA,也不需要配置 127.0.0.1:8888。")
|
||||||
}
|
}
|
||||||
return String(localized: """
|
return AppLocalization.string("""
|
||||||
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 返回的定位坐标改写为你设置的虚拟坐标,再加密返回给系统,从而实现虚拟定位。
|
||||||
@@ -523,8 +523,8 @@ struct SettingsView: View {
|
|||||||
if runtimeMode.isInitialized(.thirdParty) {
|
if runtimeMode.isInitialized(.thirdParty) {
|
||||||
do {
|
do {
|
||||||
_ = try await thirdPartyProxy.query()
|
_ = try await thirdPartyProxy.query()
|
||||||
proxyOperationAlertTitle = String(localized: "模式已切换")
|
proxyOperationAlertTitle = AppLocalization.string("模式已切换")
|
||||||
proxyOperationError = String(localized: "第三方代理模式检测通过。请关闭 Wi-Fi 中的 127.0.0.1:8888 手动代理,避免双重拦截。")
|
proxyOperationError = AppLocalization.string("第三方代理模式检测通过。请关闭 Wi-Fi 中的 127.0.0.1:8888 手动代理,避免双重拦截。")
|
||||||
} catch {
|
} catch {
|
||||||
openThirdPartySetup(for: error)
|
openThirdPartySetup(for: error)
|
||||||
}
|
}
|
||||||
@@ -546,8 +546,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 = String(localized: "模式已切换")
|
proxyOperationAlertTitle = AppLocalization.string("模式已切换")
|
||||||
proxyOperationError = String(localized: "APP 模式环境检测通过。请停用第三方 WLOC 模块或代理连接,避免双重拦截。")
|
proxyOperationError = AppLocalization.string("APP 模式环境检测通过。请停用第三方 WLOC 模块或代理连接,避免双重拦截。")
|
||||||
} else {
|
} else {
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
@@ -608,14 +608,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 = String(localized: "证书重置失败")
|
proxyOperationAlertTitle = AppLocalization.string("证书重置失败")
|
||||||
proxyOperationError = setup.message
|
proxyOperationError = setup.message
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setup.requestCertificateSetup()
|
setup.requestCertificateSetup()
|
||||||
dismiss()
|
dismiss()
|
||||||
} catch {
|
} catch {
|
||||||
proxyOperationAlertTitle = String(localized: "证书重置失败")
|
proxyOperationAlertTitle = AppLocalization.string("证书重置失败")
|
||||||
proxyOperationError = error.localizedDescription
|
proxyOperationError = error.localizedDescription
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -626,8 +626,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 = String(localized: "无法打开客户端")
|
proxyOperationAlertTitle = AppLocalization.string("无法打开客户端")
|
||||||
proxyOperationError = String(localized: "无法打开 \(client.name),请确认客户端已安装后手动打开。")
|
proxyOperationError = AppLocalization.string("无法打开 \(client.name),请确认客户端已安装后手动打开。")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-23
@@ -36,7 +36,7 @@ final class SetupCoordinator: ObservableObject {
|
|||||||
message = ""
|
message = ""
|
||||||
return true
|
return true
|
||||||
} catch {
|
} catch {
|
||||||
message = String(localized: "本地代理初始化失败:\(error.localizedDescription)")
|
message = AppLocalization.string("本地代理初始化失败:\(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 = String(localized: "✓ 定位环境正常")
|
message = AppLocalization.string("✓ 定位环境正常")
|
||||||
case .certNotTrusted:
|
case .certNotTrusted:
|
||||||
trustState = .unavailable
|
trustState = .unavailable
|
||||||
setupStep = .cert
|
setupStep = .cert
|
||||||
needsSetup = true
|
needsSetup = true
|
||||||
message = String(localized: "CA 证书未安装或未信任")
|
message = AppLocalization.string("CA 证书未安装或未信任")
|
||||||
case .verificationInProgress, .verificationSuperseded:
|
case .verificationInProgress, .verificationSuperseded:
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
trustState = .unavailable
|
trustState = .unavailable
|
||||||
setupStep = .proxy
|
setupStep = .proxy
|
||||||
needsSetup = true
|
needsSetup = true
|
||||||
message = String(localized: "Wi-Fi 代理未正确设置,请检查 127.0.0.1:8888")
|
message = AppLocalization.string("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(String(localized: "======== 代理验证测试 ========"))
|
log(AppLocalization.string("======== 代理验证测试 ========"))
|
||||||
log(String(localized: "App 版本: \(appVersion)"))
|
log(AppLocalization.string("App 版本: \(appVersion)"))
|
||||||
log(String(localized: "系统版本: iOS \(UIDevice.current.systemVersion)"))
|
log(AppLocalization.string("系统版本: iOS \(UIDevice.current.systemVersion)"))
|
||||||
log("")
|
log("")
|
||||||
|
|
||||||
// Step A: Proxy running
|
// Step A: Proxy running
|
||||||
log(String(localized: "[步骤 A] 检查代理是否运行…"))
|
log(AppLocalization.string("[步骤 A] 检查代理是否运行…"))
|
||||||
log(String(localized: " 端口: 127.0.0.1:8888"))
|
log(AppLocalization.string(" 端口: 127.0.0.1:8888"))
|
||||||
let stepAStart = Date()
|
let stepAStart = Date()
|
||||||
if !proxy.isRunning {
|
if !proxy.isRunning {
|
||||||
log(String(localized: " ⚠ 代理未运行,尝试启动…"))
|
log(AppLocalization.string(" ⚠ 代理未运行,尝试启动…"))
|
||||||
do { try await proxy.start() } catch {
|
do { try await proxy.start() } catch {
|
||||||
log(String(localized: " ✗ 启动失败: \(error.localizedDescription)"))
|
log(AppLocalization.string(" ✗ 启动失败: \(error.localizedDescription)"))
|
||||||
return .proxyNotRunning
|
return .proxyNotRunning
|
||||||
}
|
}
|
||||||
log(String(localized: " ✓ 代理启动成功"))
|
log(AppLocalization.string(" ✓ 代理启动成功"))
|
||||||
} else {
|
} else {
|
||||||
log(String(localized: " ✓ 代理已在运行中"))
|
log(AppLocalization.string(" ✓ 代理已在运行中"))
|
||||||
}
|
}
|
||||||
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(String(localized: "[步骤 B] 检测证书与 WiFi 代理…"))
|
log(AppLocalization.string("[步骤 B] 检测证书与 WiFi 代理…"))
|
||||||
log(String(localized: " 方式: 请求 baidu.com/paopao-verify-<token>"))
|
log(AppLocalization.string(" 方式: 请求 baidu.com/paopao-verify-<token>"))
|
||||||
log(String(localized: " 结果判定: TLS 错误=证书问题 / 响应不匹配=代理未配置 / 匹配=通过"))
|
log(AppLocalization.string(" 结果判定: TLS 错误=证书问题 / 响应不匹配=代理未配置 / 匹配=通过"))
|
||||||
let stepBStart = Date()
|
let stepBStart = Date()
|
||||||
let verifyToken = CoreBridge.refreshVerifyToken()
|
let verifyToken = CoreBridge.refreshVerifyToken()
|
||||||
guard !verifyToken.isEmpty else {
|
guard !verifyToken.isEmpty else {
|
||||||
log(String(localized: " ✗ 无法生成验证 token"))
|
log(AppLocalization.string(" ✗ 无法生成验证 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(String(localized: " ✓ 证书已信任,WiFi 代理已配置"))
|
log(AppLocalization.string(" ✓ 证书已信任,WiFi 代理已配置"))
|
||||||
} else {
|
} else {
|
||||||
log(String(localized: " ✗ 响应不匹配: HTTP \(statusCode), \(data.count) bytes,WiFi 代理未配置"))
|
log(AppLocalization.string(" ✗ 响应不匹配: HTTP \(statusCode), \(data.count) bytes,WiFi 代理未配置"))
|
||||||
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(String(localized: " ✗ 请求失败 [\(ns.domain) code=\(ns.code)]: \(msg)"))
|
log(AppLocalization.string(" ✗ 请求失败 [\(ns.domain) code=\(ns.code)]: \(msg)"))
|
||||||
if isCertificateTrustError(nsError: ns, message: msg) {
|
if isCertificateTrustError(nsError: ns, message: msg) {
|
||||||
log(String(localized: " TLS/证书校验失败,CA 证书未信任"))
|
log(AppLocalization.string(" 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(String(localized: "======== 环境检测通过 ✓ ========"))
|
log(AppLocalization.string("======== 环境检测通过 ✓ ========"))
|
||||||
return .success
|
return .success
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,7 +202,7 @@ 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(String(localized: " --- 代理日志 ---"))
|
log(AppLocalization.string(" --- 代理日志 ---"))
|
||||||
for e in proxyEntries {
|
for e in proxyEntries {
|
||||||
log(" " + e.localizedMessage)
|
log(" " + e.localizedMessage)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,13 +25,13 @@ enum SystemSettingsDestination {
|
|||||||
var manualPath: String {
|
var manualPath: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .appPermissions:
|
case .appPermissions:
|
||||||
return String(localized: "请手动打开「设置」,找到本 App 后检查定位权限。")
|
return AppLocalization.string("请手动打开「设置」,找到本 App 后检查定位权限。")
|
||||||
case .general:
|
case .general:
|
||||||
return String(localized: "请手动打开「设置 → 通用」。")
|
return AppLocalization.string("请手动打开「设置 → 通用」。")
|
||||||
case .wifi:
|
case .wifi:
|
||||||
return String(localized: "请手动打开「设置 → 无线局域网」,进入当前 Wi-Fi 的详情页。")
|
return AppLocalization.string("请手动打开「设置 → 无线局域网」,进入当前 Wi-Fi 的详情页。")
|
||||||
case .locationServices:
|
case .locationServices:
|
||||||
return String(localized: "请手动打开「设置 → 隐私与安全性 → 定位服务」。")
|
return AppLocalization.string("请手动打开「设置 → 隐私与安全性 → 定位服务」。")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,661 @@
|
|||||||
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
our General Public Licenses are intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
Developers that use our General Public Licenses protect your rights
|
||||||
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
|
you this License which gives you legal permission to copy, distribute
|
||||||
|
and/or modify the software.
|
||||||
|
|
||||||
|
A secondary benefit of defending all users' freedom is that
|
||||||
|
improvements made in alternate versions of the program, if they
|
||||||
|
receive widespread use, become available for other developers to
|
||||||
|
incorporate. Many developers of free software are heartened and
|
||||||
|
encouraged by the resulting cooperation. However, in the case of
|
||||||
|
software used on network servers, this result may fail to come about.
|
||||||
|
The GNU General Public License permits making a modified version and
|
||||||
|
letting the public access it on a server without ever releasing its
|
||||||
|
source code to the public.
|
||||||
|
|
||||||
|
The GNU Affero General Public License is designed specifically to
|
||||||
|
ensure that, in such cases, the modified source code becomes available
|
||||||
|
to the community. It requires the operator of a network server to
|
||||||
|
provide the source code of the modified version running there to the
|
||||||
|
users of that server. Therefore, public use of a modified version, on
|
||||||
|
a publicly accessible server, gives the public access to the source
|
||||||
|
code of the modified version.
|
||||||
|
|
||||||
|
An older license, called the Affero General Public License and
|
||||||
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
|
released a new version of the Affero GPL which permits relicensing under
|
||||||
|
this license.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, if you modify the
|
||||||
|
Program, your modified version must prominently offer all users
|
||||||
|
interacting with it remotely through a computer network (if your version
|
||||||
|
supports such interaction) an opportunity to receive the Corresponding
|
||||||
|
Source of your version by providing access to the Corresponding Source
|
||||||
|
from a network server at no charge, through some standard or customary
|
||||||
|
means of facilitating copying of software. This Corresponding Source
|
||||||
|
shall include the Corresponding Source for any work covered by version 3
|
||||||
|
of the GNU General Public License that is incorporated pursuant to the
|
||||||
|
following paragraph.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the work with which it is combined will remain governed by version
|
||||||
|
3 of the GNU General Public License.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU Affero General Public License from time to time. Such new versions
|
||||||
|
will be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU Affero General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU Affero General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU Affero General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If your software can interact with users remotely through a computer
|
||||||
|
network, you should also make sure that it provides a way for users to
|
||||||
|
get its source. For example, if your program is a web application, its
|
||||||
|
interface could display a "Source" link that leads users to an archive
|
||||||
|
of the code. There are many ways you could offer source, and different
|
||||||
|
solutions will be better for different programs; see section 13 for the
|
||||||
|
specific requirements.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
.PHONY: setup core ipa-unsigned verify-ipa
|
.PHONY: setup core ipa-unsigned verify-ipa test
|
||||||
|
|
||||||
setup:
|
setup:
|
||||||
./Scripts/setup.sh
|
./Scripts/setup.sh
|
||||||
@@ -12,3 +12,6 @@ ipa-unsigned: core
|
|||||||
verify-ipa:
|
verify-ipa:
|
||||||
@test -n "$(IPA)" || (echo "Usage: make verify-ipa IPA=/absolute/path/to/signed.ipa" >&2; exit 2)
|
@test -n "$(IPA)" || (echo "Usage: make verify-ipa IPA=/absolute/path/to/signed.ipa" >&2; exit 2)
|
||||||
./Scripts/verify-ipa.sh --signed "$(IPA)"
|
./Scripts/verify-ipa.sh --signed "$(IPA)"
|
||||||
|
|
||||||
|
test:
|
||||||
|
./Tests/run_all_contract_tests.sh
|
||||||
|
|||||||
-463
@@ -1,463 +0,0 @@
|
|||||||
<div align="center">
|
|
||||||
|
|
||||||
# 📍 Location Spoofer
|
|
||||||
|
|
||||||
### iOS Location Service Research & Testing Framework
|
|
||||||
|
|
||||||
An open-source project for **iOS location-service research, software development testing, and QA validation**.
|
|
||||||
|
|
||||||
The project uses either an on-device proxy or a third-party proxy client to simulate selected Apple location-service
|
|
||||||
responses in a controlled test environment.
|
|
||||||
|
|
||||||
> ⚠️ **Starting with iOS 27 beta 6, the system blocks MITM interception of `gs-loc.apple.com`.** This project is
|
|
||||||
> temporarily unusable on that version and later betas until a workaround is found.
|
|
||||||
|
|
||||||
[](project.yml)
|
|
||||||
[](project.yml)
|
|
||||||
[](Core/go.mod)
|
|
||||||
[](docs/CHANGELOG.md)
|
|
||||||
|
|
||||||
[Features](#feature-overview) ·
|
|
||||||
[How It Works](#how-it-works) ·
|
|
||||||
[Quick Start](#quick-start) ·
|
|
||||||
[Build](#building-the-project) ·
|
|
||||||
[中文](README.md)
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> Use this project only for education, research, testing on devices you own, software development, and QA validation.
|
|
||||||
>
|
|
||||||
> Use it only on devices, networks, and software environments that you own or are authorized to test. Follow applicable
|
|
||||||
> laws, network policies, and service terms.
|
|
||||||
>
|
|
||||||
> The project does not guarantee compatibility with every iOS version or third-party app, and it does not promise to
|
|
||||||
> bypass third-party security controls, business restrictions, or service rules.
|
|
||||||
|
|
||||||
## Project Scope
|
|
||||||
|
|
||||||
Location Spoofer is a tool for studying iOS location-service behavior and testing location-dependent software.
|
|
||||||
|
|
||||||
It provides:
|
|
||||||
|
|
||||||
- Native map selection and location-scenario switching;
|
|
||||||
- Controlled simulation of selected Apple location-service responses;
|
|
||||||
- App Mode and Third-party Proxy Mode;
|
|
||||||
- Map coordinate detection with paired WGS-84 and GCJ-02 values;
|
|
||||||
- Environment checks, runtime logs, and diagnostics;
|
|
||||||
- Favorite locations and restoration of the previous map state.
|
|
||||||
|
|
||||||
The project does not modify the target app's source code and does not provide telemetry, remote control, or data
|
|
||||||
collection services.
|
|
||||||
|
|
||||||
## Feature Overview
|
|
||||||
|
|
||||||
- **Native map interaction**
|
|
||||||
- Uses MapKit for the map and system blue dot;
|
|
||||||
- Supports search, map taps, center-point dragging, and zooming;
|
|
||||||
- Supports favorites and restoration of the latest selection;
|
|
||||||
- Shows both domestic and international coordinate representations with separate copy actions.
|
|
||||||
|
|
||||||
- **Location-service response simulation**
|
|
||||||
- Processes only the Apple location-service requests defined by the project;
|
|
||||||
- Returns the selected coordinates in a controlled test environment;
|
|
||||||
- Does not require changes to the target app.
|
|
||||||
|
|
||||||
- **Two runtime modes**
|
|
||||||
- App Mode: runs the Go proxy on-device and covers only the current Wi-Fi network;
|
|
||||||
- Third-party Proxy Mode: uses a supported proxy client and may cover Wi-Fi, 4G, or 5G depending on that client.
|
|
||||||
|
|
||||||
- **Environment checks**
|
|
||||||
- App Mode checks the local proxy, CA trust, and request path;
|
|
||||||
- Third-party Proxy Mode checks the WLOC configuration API and module response;
|
|
||||||
- Failures route to the relevant setup or diagnostics screen.
|
|
||||||
|
|
||||||
- **Development diagnostics**
|
|
||||||
- Runtime logs;
|
|
||||||
- Log copy and cleanup;
|
|
||||||
- Map coordinate-system change records;
|
|
||||||
- Sanitized issue-report generation.
|
|
||||||
|
|
||||||
## How It Works
|
|
||||||
|
|
||||||
### App Mode
|
|
||||||
|
|
||||||
App Mode runs the local Go proxy inside the app. A manual HTTP proxy on the current Wi-Fi routes the selected requests
|
|
||||||
through that on-device proxy.
|
|
||||||
|
|
||||||
```text
|
|
||||||
iOS location request
|
|
||||||
│
|
|
||||||
│ Manual HTTP proxy on the current Wi-Fi
|
|
||||||
▼
|
|
||||||
On-device wloccore Go proxy
|
|
||||||
│
|
|
||||||
│ Handle selected Apple location-service requests
|
|
||||||
▼
|
|
||||||
Apple location-service response
|
|
||||||
│
|
|
||||||
│ Test coordinate response
|
|
||||||
▼
|
|
||||||
The system and apps read the location result
|
|
||||||
```
|
|
||||||
|
|
||||||
App Mode:
|
|
||||||
|
|
||||||
- Does not create a Network Extension;
|
|
||||||
- Does not display or occupy the system VPN slot;
|
|
||||||
- Covers only the current Wi-Fi network;
|
|
||||||
- Requires a manual HTTP proxy on that Wi-Fi network;
|
|
||||||
- Requires installation and trust of the CA generated by the app;
|
|
||||||
- Handles only the Apple location-service and environment-verification traffic defined by the project. It is not a
|
|
||||||
general-purpose packet capture tool.
|
|
||||||
|
|
||||||
### Third-party Proxy Mode
|
|
||||||
|
|
||||||
Third-party Proxy Mode does not start the app's Go proxy and does not use the CA generated by the app.
|
|
||||||
|
|
||||||
```text
|
|
||||||
Map selection
|
|
||||||
│
|
|
||||||
│ WGS-84 coordinates
|
|
||||||
▼
|
|
||||||
WLOC configuration API
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
Third-party proxy client stores the configuration
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
Third-party client processes location-service requests
|
|
||||||
```
|
|
||||||
|
|
||||||
In this mode:
|
|
||||||
|
|
||||||
- The app owns map selection, favorites, coordinate synchronization, and coordinate clearing;
|
|
||||||
- The third-party client owns proxy/VPN, MITM, certificates, and rule execution;
|
|
||||||
- The third-party client owns coordinate persistence;
|
|
||||||
- Wi-Fi, 4G, and 5G support depends on the client;
|
|
||||||
- The configuration may remain active after Location Spoofer closes.
|
|
||||||
|
|
||||||
#### Configuration API and Client Integration
|
|
||||||
|
|
||||||
The app does not upload coordinates to a project server. It sends the following GET request, which the third-party
|
|
||||||
client must intercept locally on the device:
|
|
||||||
|
|
||||||
```text
|
|
||||||
https://gs-loc.apple.com/wloc-settings/save
|
|
||||||
```
|
|
||||||
|
|
||||||
| Action | Query parameters | Purpose |
|
|
||||||
|---|---|---|
|
|
||||||
| Query | `action=query` | Verify module connectivity and read the stored coordinate |
|
|
||||||
| Save | `lon=<WGS-84 longitude>&lat=<WGS-84 latitude>&acc=<accuracy>` | Store the selected coordinate |
|
|
||||||
| Clear | `action=clear` | Remove the stored test coordinate |
|
|
||||||
|
|
||||||
The interception script must return HTTP 200 with JSON:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"longitude": 113.0,
|
|
||||||
"latitude": 22.0,
|
|
||||||
"accuracy": 25
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Failures use:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"error": "Error description"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
When no coordinate is stored, a query may return `{"success":false,"error":"无已保存的坐标"}`. The app interprets
|
|
||||||
that response as “module connected, virtual location inactive.” A successful save response must echo the requested
|
|
||||||
WGS-84 longitude and latitude.
|
|
||||||
|
|
||||||
To integrate another third-party client:
|
|
||||||
|
|
||||||
1. Add an HTTP request script for `gs-loc.apple.com/wloc-settings/save` that parses the parameters and returns the
|
|
||||||
specified JSON;
|
|
||||||
2. Store coordinates, accuracy, and optional state in the client's persistent storage;
|
|
||||||
3. Enable HTTPS decryption for `gs-loc.apple.com` and `gs-loc-cn.apple.com`;
|
|
||||||
4. Intercept `gs-loc(-cn).apple.com/clls/wloc` responses, read the same persistent data, and modify the WLOC response;
|
|
||||||
5. Publish an importable module and verify query, save, clear, and location restoration on a real device.
|
|
||||||
|
|
||||||
The app validates only the configuration endpoint's HTTP status, JSON shape, and coordinate round trip. It does not
|
|
||||||
manage the third-party client's certificate, MITM, VPN, or proxy state.
|
|
||||||
|
|
||||||
Do not enable App Mode interception and Third-party Proxy Mode interception at the same time.
|
|
||||||
|
|
||||||
## Runtime Modes
|
|
||||||
|
|
||||||
### App Mode
|
|
||||||
|
|
||||||
Suitable for:
|
|
||||||
|
|
||||||
- Wi-Fi-only device testing;
|
|
||||||
- Local testing without a third-party proxy client;
|
|
||||||
- Workflows that need in-app proxy, certificate, and environment guidance.
|
|
||||||
|
|
||||||
Requirements:
|
|
||||||
|
|
||||||
- An iOS device;
|
|
||||||
- A Wi-Fi network that permits manual HTTP proxy configuration;
|
|
||||||
- Installation and trust of the local CA;
|
|
||||||
- Completion of the in-app proxy and environment checks.
|
|
||||||
|
|
||||||
### Third-party Proxy Mode
|
|
||||||
|
|
||||||
Suitable for:
|
|
||||||
|
|
||||||
- Tests that need Wi-Fi, 4G, or 5G coverage;
|
|
||||||
- Existing supported proxy-client workflows;
|
|
||||||
- Cases where the third-party client should keep the proxy configuration active.
|
|
||||||
|
|
||||||
Current client status:
|
|
||||||
|
|
||||||
| Client | Status | Community configuration |
|
|
||||||
|---|---|---|
|
|
||||||
| Shadowrocket | Currently used for on-device testing | Built-in App guide |
|
|
||||||
| Surge | Configuration provided, not fully verified | Contributions wanted |
|
|
||||||
| Quantumult X | Configuration provided, not fully verified | Contributions wanted |
|
|
||||||
| Loon | Configuration provided, not fully verified | Contributions wanted |
|
|
||||||
| Stash | Configuration provided, not fully verified | Contributions wanted |
|
|
||||||
| Egern | Uses the Surge module, not fully verified | Contributions wanted |
|
|
||||||
|
|
||||||
Community configurations are reviewed by client. Accepted submissions are linked in this table with attribution unless
|
|
||||||
the contributor requests anonymous inclusion.
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
The selected client owns its certificates, MITM configuration, and proxy switches. Review third-party modules and
|
|
||||||
scripts before importing them.
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
### 1. Install the App
|
|
||||||
|
|
||||||
You can use:
|
|
||||||
|
|
||||||
- Your own Apple Developer signing environment;
|
|
||||||
- A self-signing tool suitable for personal testing;
|
|
||||||
- The unsigned IPA published in Releases;
|
|
||||||
- A source build produced on macOS using the [build guide](docs/BUILD.md).
|
|
||||||
|
|
||||||
Free self-signing environments may not provide Network Extension capabilities. App Mode therefore uses an on-device
|
|
||||||
proxy plus a manual Wi-Fi HTTP proxy and does not depend on a VPN component.
|
|
||||||
|
|
||||||
#### Self-Signing Instructions
|
|
||||||
|
|
||||||
Release assets are unsigned IPA files and must be installed on an iPhone with a self-signing tool:
|
|
||||||
|
|
||||||
1. **Enable sideloading support**: On iOS 16 or newer, open Settings → Privacy & Security → Developer Mode, enable it,
|
|
||||||
then restart and confirm when prompted. iOS 15 does not have this switch, so skip this step.
|
|
||||||
2. **Download the IPA**: Open this project's [Releases](https://github.com/xweiba/location-spoofer/releases) and download
|
|
||||||
the latest `PaopaoLocationSpoofer-unsigned.ipa`.
|
|
||||||
3. **Prepare signing software**: Download the appropriate Impactor build from
|
|
||||||
[Impactor Releases](https://github.com/claration/Impactor/releases). Other tools that support self-signing and installing
|
|
||||||
IPA files, such as Aisi Assistant, may also be used.
|
|
||||||
4. **Connect and install**: Connect the iPhone to the computer with a USB cable, choose “Trust This Computer” on the
|
|
||||||
phone, select the downloaded IPA in the signing software, and follow that tool's instructions to sign and install it.
|
|
||||||
|
|
||||||
Impactor supports Windows, macOS, and Linux. If Windows cannot detect the device, install the Apple device drivers supplied
|
|
||||||
with iTunes first. Aisi Assistant is third-party software; obtain it from its official channel and evaluate its account,
|
|
||||||
certificate, and privacy risks yourself.
|
|
||||||
|
|
||||||
After installation, if iOS blocks the app from opening, go to Settings → General → VPN & Device Management and trust the
|
|
||||||
corresponding developer app. A free Apple ID signature normally expires after seven days and must then be renewed.
|
|
||||||
|
|
||||||
### 2. First Launch
|
|
||||||
|
|
||||||
1. Choose App Mode or Third-party Proxy Mode;
|
|
||||||
2. Complete the corresponding in-app setup;
|
|
||||||
3. Run the environment check;
|
|
||||||
4. Search, tap, or drag on the map to select a test location;
|
|
||||||
5. Enable the test location and verify the result in the authorized test environment.
|
|
||||||
|
|
||||||
### 3. Restore the Real Location
|
|
||||||
|
|
||||||
App Mode:
|
|
||||||
|
|
||||||
1. Stop the test location;
|
|
||||||
2. Disable the manual HTTP proxy on the current Wi-Fi network;
|
|
||||||
3. Follow the in-app instructions to refresh the location environment.
|
|
||||||
|
|
||||||
Third-party Proxy Mode:
|
|
||||||
|
|
||||||
1. Clear the WLOC coordinates from the app;
|
|
||||||
2. Disable the corresponding module or proxy in the third-party client;
|
|
||||||
3. Restore HTTPS decryption and proxy settings according to the client documentation.
|
|
||||||
|
|
||||||
Location caches may take time to refresh. Restart the device if the system or target app continues to show an old
|
|
||||||
location.
|
|
||||||
|
|
||||||
## Coordinate Handling
|
|
||||||
|
|
||||||
The project stores two coordinate representations:
|
|
||||||
|
|
||||||
- WGS-84: the international standard used for WLOC writes;
|
|
||||||
- GCJ-02: the domestic map representation used where required.
|
|
||||||
|
|
||||||
MapKit does not expose a public API that reports whether its current runtime output uses GCJ-02 or WGS-84. The project
|
|
||||||
uses a fixed-anchor query to resolve the active representation and performs controlled refreshes after blue-dot changes
|
|
||||||
and explicit user actions.
|
|
||||||
|
|
||||||
Each write boundary stores a complete WGS-84/GCJ-02 pair. Rendering selects the field matching the confirmed map
|
|
||||||
representation instead of repeatedly converting an already typed value.
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```text
|
|
||||||
App/ SwiftUI interface, MapKit, location, and runtime flow
|
|
||||||
Core/ Go proxy, certificate server, and location-response handling
|
|
||||||
Shared/ Coordinates, favorites, logs, configuration, and shared models
|
|
||||||
Resources/ Info.plist, Entitlements, and resources
|
|
||||||
Config/ Build configuration
|
|
||||||
Scripts/ Build, packaging, and validation scripts
|
|
||||||
Tests/ XCTest and Shell contract tests
|
|
||||||
docs/ Build, module, and release documentation
|
|
||||||
```
|
|
||||||
|
|
||||||
## Building the Project
|
|
||||||
|
|
||||||
Source builds require:
|
|
||||||
|
|
||||||
- macOS;
|
|
||||||
- Xcode;
|
|
||||||
- Xcode Command Line Tools;
|
|
||||||
- XcodeGen;
|
|
||||||
- Go 1.23 or newer.
|
|
||||||
|
|
||||||
Building the iOS app directly on Windows is not supported.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/xweiba/location-spoofer.git
|
|
||||||
cd location-spoofer
|
|
||||||
|
|
||||||
./build.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Build and run Simulator tests:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./build.sh --test
|
|
||||||
```
|
|
||||||
|
|
||||||
The build script generates an unsigned IPA:
|
|
||||||
|
|
||||||
```text
|
|
||||||
dist/PaopaoLocationSpoofer-unsigned.ipa
|
|
||||||
```
|
|
||||||
|
|
||||||
Deploy it to a test device using your own signing and installation process.
|
|
||||||
|
|
||||||
## Privacy and Security Boundaries
|
|
||||||
|
|
||||||
- The project contains no telemetry or remote-control service;
|
|
||||||
- The project does not automatically upload location data;
|
|
||||||
- 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;
|
|
||||||
- App Mode accesses the local proxy and the environment-verification URL;
|
|
||||||
- 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;
|
|
||||||
- Third-party MITM, certificates, and proxy behavior are owned by the selected client.
|
|
||||||
|
|
||||||
Do not post real locations, authentication information, CA private keys, or complete sensitive logs in public issues.
|
|
||||||
|
|
||||||
## Limitations
|
|
||||||
|
|
||||||
- iOS updates may change location-service behavior;
|
|
||||||
- MapKit coordinate output can vary with the system, region, and location environment;
|
|
||||||
- System location caches may delay visible changes;
|
|
||||||
- Each third-party proxy client requires separate compatibility testing;
|
|
||||||
- Not every app uses the same location API;
|
|
||||||
- Not every app or service accepts test coordinates;
|
|
||||||
- Behavior is not guaranteed across every network, device model, or iOS version.
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
Contributions are welcome for:
|
|
||||||
|
|
||||||
- [Bug reports](https://github.com/xweiba/location-spoofer/issues/new?template=bug-report.yml);
|
|
||||||
- [Feature requests](https://github.com/xweiba/location-spoofer/discussions/categories/%E5%8A%9F%E8%83%BD%E5%BB%BA%E8%AE%AE);
|
|
||||||
- [Usage help and compatibility results](https://github.com/xweiba/location-spoofer/discussions/categories/%E4%BD%BF%E7%94%A8%E5%B8%AE%E5%8A%A9);
|
|
||||||
- [Third-party client configurations and sanitized screenshots](https://github.com/xweiba/location-spoofer/discussions/categories/%E7%AC%AC%E4%B8%89%E6%96%B9%E9%85%8D%E7%BD%AE%E5%88%86%E4%BA%AB);
|
|
||||||
- Performance improvements;
|
|
||||||
- Documentation improvements;
|
|
||||||
- Additional tests.
|
|
||||||
|
|
||||||
For bugs, generate a report from **Settings → Support → Report Bug** in the App whenever possible. The report includes:
|
|
||||||
|
|
||||||
- iOS version;
|
|
||||||
- App version;
|
|
||||||
- Runtime mode;
|
|
||||||
- Selected third-party client;
|
|
||||||
- Whether the issue is reproducible;
|
|
||||||
- Problem description;
|
|
||||||
- Sanitized runtime logs;
|
|
||||||
|
|
||||||
Paste it into the **App-generated diagnostic report** field in the GitHub Issue Form.
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
- [Build guide](docs/BUILD.md)
|
|
||||||
- [Third-party module documentation](docs/THIRD_PARTY_MODULES.md)
|
|
||||||
- [Changelog](docs/CHANGELOG.md)
|
|
||||||
- [中文文档](README.md)
|
|
||||||
- [GitHub Issues](https://github.com/xweiba/location-spoofer/issues)
|
|
||||||
|
|
||||||
## Feature Preview
|
|
||||||
|
|
||||||
These screenshots show the main interface and selected on-device test scenarios. Actual results depend on the iOS
|
|
||||||
version, network environment, system caches, and the target app's location strategy; they are not a compatibility
|
|
||||||
guarantee for every app or release.
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<th>Main interface</th>
|
|
||||||
<th>Apple Maps test</th>
|
|
||||||
<th>Amap test</th>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><img src="images/主界面.jpg" alt="Location Spoofer map selection interface" width="220"></td>
|
|
||||||
<td><img src="images/Apple%20Map.jpg" alt="Apple Maps location test scenario" width="220"></td>
|
|
||||||
<td><img src="images/高德地图.jpg" alt="Amap location test scenario" width="220"></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>WeChat test</th>
|
|
||||||
<th>DingTalk test</th>
|
|
||||||
<th>Apple Watch scenario test</th>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><img src="images/微信.jpg" alt="WeChat location test scenario" width="220"></td>
|
|
||||||
<td><img src="images/钉钉.jpg" alt="DingTalk location test scenario" width="220"></td>
|
|
||||||
<td><img src="images/高血压.jpg" alt="Apple Watch region-feature test scenario" width="220"></td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
## Acknowledgements and Links
|
|
||||||
|
|
||||||
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)
|
|
||||||
- [ios-location-spoofer](https://github.com/mekos2772/ios-location-spoofer)
|
|
||||||
|
|
||||||
Thanks to the following LINUX DO users for their contributions:
|
|
||||||
|
|
||||||
- Bug fixes: [Chen Ze](https://linux.do/u/lixiaobaivv)
|
|
||||||
- Ideas and suggestions: [Alex](https://linux.do/u/_alex), [ye4241](https://linux.do/u/ye4241)
|
|
||||||
|
|
||||||
Links:
|
|
||||||
|
|
||||||
- [LINUX DO](https://linux.do/)
|
|
||||||
- [iOS-Location-Spoofer-Web](https://github.com/akudamatata/iOS-Location-Spoofer-Web)
|
|
||||||
|
|
||||||
Thanks to the open-source contributors working on iOS location-service research, network proxies, and mobile testing
|
|
||||||
tools.
|
|
||||||
@@ -4,149 +4,155 @@
|
|||||||
|
|
||||||
### iOS Location Service Research & Testing Framework
|
### iOS Location Service Research & Testing Framework
|
||||||
|
|
||||||
一个用于 **iOS 定位服务研究、软件开发测试和 QA 验证** 的开源项目。
|
An open-source project for **iOS location-service research, software development testing, and QA validation**.
|
||||||
|
|
||||||
项目通过本机代理或第三方代理客户端,对 Apple 定位服务的指定响应进行测试环境模拟,帮助开发者验证应用在
|
The project uses either an on-device proxy or a third-party proxy client to simulate selected Apple location-service
|
||||||
不同地理位置和定位场景下的行为。
|
responses in a controlled test environment.
|
||||||
|
|
||||||
> ⚠️ **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 version and later betas until a workaround is found.
|
||||||
|
|
||||||
[](project.yml)
|
[](project.yml)
|
||||||
[](project.yml)
|
[](project.yml)
|
||||||
[](Core/go.mod)
|
[](Core/go.mod)
|
||||||
[](docs/CHANGELOG.md)
|
[](docs/CHANGELOG.en.md)
|
||||||
|
|
||||||
[功能概览](#功能概览) ·
|
[Features](#feature-overview) ·
|
||||||
[工作原理](#工作原理) ·
|
[How It Works](#how-it-works) ·
|
||||||
[快速开始](#快速开始) ·
|
[Quick Start](#quick-start) ·
|
||||||
[构建项目](#构建项目) ·
|
[Build](#building-the-project) ·
|
||||||
[English](README.en.md)
|
[中文](README.zh-CN.md)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> 本项目用于学习研究、自有设备测试、软件开发和 QA 验证。
|
> Use this project only for education, research, testing on devices you own, software development, and QA validation.
|
||||||
>
|
>
|
||||||
> 请仅在你拥有或获得授权的设备、网络和软件环境中使用,并遵守当地法律法规、网络管理规定以及相关服务条
|
> Use it only on devices, networks, and software environments that you own or are authorized to test. Follow applicable
|
||||||
> 款。
|
> laws, network policies, and service terms.
|
||||||
>
|
>
|
||||||
> 本项目不保证兼容所有 iOS 版本或第三方应用,也不承诺绕过第三方应用的安全策略、业务限制或服务规则。
|
> The project does not guarantee compatibility with every iOS version or third-party app, and it does not promise to
|
||||||
|
> bypass third-party security controls, business restrictions, or service rules.
|
||||||
|
|
||||||
## 项目定位
|
## Project Scope
|
||||||
|
|
||||||
Location Spoofer 是一个面向 iOS 定位服务行为研究和开发测试的工具。
|
Location Spoofer is a tool for studying iOS location-service behavior and testing location-dependent software.
|
||||||
|
|
||||||
它提供:
|
It provides:
|
||||||
|
|
||||||
- 原生地图选点和位置场景切换;
|
- Native map selection and location-scenario switching;
|
||||||
- Apple 定位服务响应的测试环境模拟;
|
- Controlled simulation of selected Apple location-service responses;
|
||||||
- 本地代理和第三方代理两种运行模式;
|
- App Mode and Third-party Proxy Mode;
|
||||||
- 坐标标准识别与 WGS-84 / GCJ-02 双坐标管理;
|
- Map coordinate detection with paired WGS-84 and GCJ-02 values;
|
||||||
- 环境检测、运行日志和问题诊断;
|
- Environment checks, runtime logs, and diagnostics;
|
||||||
- 收藏位置和上次地图状态恢复。
|
- Favorite locations and restoration of the previous map state.
|
||||||
|
|
||||||
项目不修改目标 App 的源代码,也不提供远程控制或数据采集服务。
|
The project does not modify the target app's source code and does not provide telemetry, remote control, or data
|
||||||
|
collection services.
|
||||||
|
|
||||||
## 功能概览
|
## Feature Overview
|
||||||
|
|
||||||
- **原生地图交互**
|
- **Native map interaction**
|
||||||
- 使用 MapKit 显示地图和系统蓝点;
|
- Uses MapKit for the map and system blue dot;
|
||||||
- 支持搜索、点击选点、拖动地图中心和缩放;
|
- Supports search, map taps, center-point dragging, and zooming;
|
||||||
- 支持收藏位置和恢复上次选点;
|
- Supports favorites and restoration of the latest selection;
|
||||||
- 当前选点同时显示国内坐标和国际坐标,可分别复制。
|
- Shows both domestic and international coordinate representations with separate copy actions.
|
||||||
|
|
||||||
- **定位服务响应模拟**
|
- **Location-service response simulation**
|
||||||
- 通过代理层处理指定的 Apple 定位服务请求;
|
- Processes only the Apple location-service requests defined by the project;
|
||||||
- 在测试环境中返回选定的坐标数据;
|
- Returns the selected coordinates in a controlled test environment;
|
||||||
- 不需要修改目标 App 代码。
|
- Does not require changes to the target app.
|
||||||
|
|
||||||
- **双运行模式**
|
- **Two runtime modes**
|
||||||
- APP 模式:在设备内运行 Go 代理,仅支持当前 Wi-Fi 网络;
|
- App Mode: runs the Go proxy on-device and covers only the current Wi-Fi network;
|
||||||
- 第三方代理模式:通过支持的代理客户端覆盖 Wi-Fi、4G 或 5G,具体能力取决于客户端。
|
- Third-party Proxy Mode: uses a supported proxy client and may cover Wi-Fi, 4G, or 5G depending on that client.
|
||||||
|
|
||||||
- **环境检测**
|
- **Environment checks**
|
||||||
- APP 模式检测本地代理、证书信任和请求链路;
|
- App Mode checks the local proxy, CA trust, and request path;
|
||||||
- 第三方代理模式检测 WLOC 配置接口和模块响应;
|
- Third-party Proxy Mode checks the WLOC configuration API and module response;
|
||||||
- 失败时提供对应的配置或诊断入口。
|
- Failures route to the relevant setup or diagnostics screen.
|
||||||
|
|
||||||
- **开发调试**
|
- **Development diagnostics**
|
||||||
- 运行日志;
|
- Runtime logs;
|
||||||
- 日志复制和清理;
|
- Log copy and cleanup;
|
||||||
- 坐标标准变化记录;
|
- Map coordinate-system change records;
|
||||||
- 脱敏问题报告生成。
|
- Sanitized issue-report generation.
|
||||||
|
|
||||||
## 工作原理
|
## How It Works
|
||||||
|
|
||||||
### APP 模式
|
### App Mode
|
||||||
|
|
||||||
APP 模式在设备内运行本地 Go 代理,并通过当前 Wi-Fi 的手动 HTTP 代理让指定请求经过本地代理。
|
App Mode runs the local Go proxy inside the app. A manual HTTP proxy on the current Wi-Fi routes the selected requests
|
||||||
|
through that on-device proxy.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
iOS 定位请求
|
iOS location request
|
||||||
│
|
│
|
||||||
│ 当前 Wi-Fi 手动 HTTP 代理
|
│ Manual HTTP proxy on the current Wi-Fi
|
||||||
▼
|
▼
|
||||||
设备内 wloccore Go 代理
|
On-device wloccore Go proxy
|
||||||
│
|
│
|
||||||
│ 处理指定 Apple 定位服务请求
|
│ Handle selected Apple location-service requests
|
||||||
▼
|
▼
|
||||||
Apple 定位服务响应
|
Apple location-service response
|
||||||
│
|
│
|
||||||
│ 测试坐标响应
|
│ Test coordinate response
|
||||||
▼
|
▼
|
||||||
系统和应用读取定位结果
|
The system and apps read the location result
|
||||||
```
|
```
|
||||||
|
|
||||||
APP 模式:
|
App Mode:
|
||||||
|
|
||||||
- 不创建 Network Extension;
|
- Does not create a Network Extension;
|
||||||
- 不显示或占用系统 VPN;
|
- Does not display or occupy the system VPN slot;
|
||||||
- 只覆盖当前 Wi-Fi 网络;
|
- Covers only the current Wi-Fi network;
|
||||||
- 需要配置当前 Wi-Fi 的手动 HTTP 代理;
|
- Requires a manual HTTP proxy on that Wi-Fi network;
|
||||||
- 需要安装并信任 App 生成的本机 CA;
|
- Requires installation and trust of the CA generated by the app;
|
||||||
- 代理只处理项目定义的 Apple 定位服务和环境验证请求,不是通用网络抓包工具。
|
- Handles only the Apple location-service and environment-verification traffic defined by the project. It is not a
|
||||||
|
general-purpose packet capture tool.
|
||||||
|
|
||||||
### 第三方代理模式
|
### Third-party Proxy Mode
|
||||||
|
|
||||||
第三方代理模式不启动 App 内置 Go 代理,也不使用 App 生成的 CA。
|
Third-party Proxy Mode does not start the app's Go proxy and does not use the CA generated by the app.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
地图选点
|
Map selection
|
||||||
│
|
│
|
||||||
│ WGS-84 坐标
|
│ WGS-84 coordinates
|
||||||
▼
|
▼
|
||||||
WLOC 配置接口
|
WLOC configuration API
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
第三方代理客户端保存配置
|
Third-party proxy client stores the configuration
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
第三方代理客户端处理定位服务请求
|
Third-party client processes location-service requests
|
||||||
```
|
```
|
||||||
|
|
||||||
在该模式下:
|
In this mode:
|
||||||
|
|
||||||
- App 负责地图选点、收藏、坐标同步和清除;
|
- The app owns map selection, favorites, coordinate synchronization, and coordinate clearing;
|
||||||
- 第三方客户端负责代理/VPN、MITM、证书和规则执行;
|
- The third-party client owns proxy/VPN, MITM, certificates, and rule execution;
|
||||||
- 坐标持久化由第三方客户端负责;
|
- The third-party client owns coordinate persistence;
|
||||||
- 是否支持 Wi-Fi、4G 或 5G 取决于客户端;
|
- Wi-Fi, 4G, and 5G support depends on the client;
|
||||||
- App 关闭后,第三方客户端中的配置可能继续生效。
|
- The configuration may remain active after Location Spoofer closes.
|
||||||
|
|
||||||
#### 配置接口与客户端适配
|
#### Configuration API and Client Integration
|
||||||
|
|
||||||
App 不会把坐标上传到项目服务器。它会发起以下 GET 请求,第三方客户端必须在设备本地拦截:
|
The app does not upload coordinates to a project server. It sends the following GET request, which the third-party
|
||||||
|
client must intercept locally on the device:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
https://gs-loc.apple.com/wloc-settings/save
|
https://gs-loc.apple.com/wloc-settings/save
|
||||||
```
|
```
|
||||||
|
|
||||||
| 操作 | 查询参数 | 用途 |
|
| Action | Query parameters | Purpose |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 查询 | `action=query` | 检查模块是否连接,并读取当前保存的坐标 |
|
| Query | `action=query` | Verify module connectivity and read the stored coordinate |
|
||||||
| 保存 | `lon=<WGS-84 经度>&lat=<WGS-84 纬度>&acc=<精度>` | 保存当前选点 |
|
| Save | `lon=<WGS-84 longitude>&lat=<WGS-84 latitude>&acc=<accuracy>` | Store the selected coordinate |
|
||||||
| 清除 | `action=clear` | 删除已保存的测试坐标 |
|
| Clear | `action=clear` | Remove the stored test coordinate |
|
||||||
|
|
||||||
拦截脚本必须返回 HTTP 200 和 JSON:
|
The interception script must return HTTP 200 with JSON:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -157,170 +163,178 @@ https://gs-loc.apple.com/wloc-settings/save
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
失败时返回:
|
Failures use:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"success": false,
|
"success": false,
|
||||||
"error": "错误说明"
|
"error": "Error description"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
查询时没有已保存坐标,可以返回 `{"success":false,"error":"无已保存的坐标"}`。App 会把它识别为
|
When no coordinate is stored, a query may return `{"success":false,"error":"无已保存的坐标"}`. The app interprets
|
||||||
“模块已连接,但虚拟定位未开启”。保存成功时,响应中的经纬度必须与请求中的 WGS-84 坐标一致。
|
that response as “module connected, virtual location inactive.” A successful save response must echo the requested
|
||||||
|
WGS-84 longitude and latitude.
|
||||||
|
|
||||||
要适配新的第三方客户端,需要:
|
To integrate another third-party client:
|
||||||
|
|
||||||
1. 为 `gs-loc.apple.com/wloc-settings/save` 添加 HTTP 请求脚本,解析上述参数并返回约定 JSON;
|
1. Add an HTTP request script for `gs-loc.apple.com/wloc-settings/save` that parses the parameters and returns the
|
||||||
2. 使用客户端的持久化存储保存坐标、精度和可选状态;
|
specified JSON;
|
||||||
3. 为 `gs-loc.apple.com` 和 `gs-loc-cn.apple.com` 配置 HTTPS 解密;
|
2. Store coordinates, accuracy, and optional state in the client's persistent storage;
|
||||||
4. 拦截 `gs-loc(-cn).apple.com/clls/wloc` 响应,读取同一份持久化数据并修改 WLOC 响应;
|
3. Enable HTTPS decryption for `gs-loc.apple.com` and `gs-loc-cn.apple.com`;
|
||||||
5. 提供可订阅的模块文件,并确认查询、保存、清除和定位恢复都能在真机完成。
|
4. Intercept `gs-loc(-cn).apple.com/clls/wloc` responses, read the same persistent data, and modify the WLOC response;
|
||||||
|
5. Publish an importable module and verify query, save, clear, and location restoration on a real device.
|
||||||
|
|
||||||
App 只验证配置接口的 HTTP 状态、JSON 格式和坐标回读,不管理第三方客户端的证书、MITM、VPN 或代理状态。
|
The app validates only the configuration endpoint's HTTP status, JSON shape, and coordinate round trip. It does not
|
||||||
|
manage the third-party client's certificate, MITM, VPN, or proxy state.
|
||||||
|
|
||||||
不要同时启用 APP 模式代理和第三方代理模式,避免两个代理链路互相干扰。
|
Do not enable App Mode interception and Third-party Proxy Mode interception at the same time.
|
||||||
|
|
||||||
## 运行模式
|
## Runtime Modes
|
||||||
|
|
||||||
### APP 模式
|
### App Mode
|
||||||
|
|
||||||
适用于:
|
Suitable for:
|
||||||
|
|
||||||
- 只使用 Wi-Fi 的设备测试;
|
- Wi-Fi-only device testing;
|
||||||
- 不依赖第三方代理客户端的本地验证;
|
- Local testing without a third-party proxy client;
|
||||||
- 需要在 App 内完成代理、证书和环境检测的场景。
|
- Workflows that need in-app proxy, certificate, and environment guidance.
|
||||||
|
|
||||||
使用条件:
|
Requirements:
|
||||||
|
|
||||||
- iOS 设备;
|
- An iOS device;
|
||||||
- 当前 Wi-Fi 支持手动 HTTP 代理;
|
- A Wi-Fi network that permits manual HTTP proxy configuration;
|
||||||
- 安装并信任本机 CA;
|
- Installation and trust of the local CA;
|
||||||
- 在 App 内完成代理配置和环境检测。
|
- Completion of the in-app proxy and environment checks.
|
||||||
|
|
||||||
### 第三方代理模式
|
### Third-party Proxy Mode
|
||||||
|
|
||||||
适用于:
|
Suitable for:
|
||||||
|
|
||||||
- 需要 Wi-Fi、4G 或 5G 网络覆盖的测试;
|
- Tests that need Wi-Fi, 4G, or 5G coverage;
|
||||||
- 已经使用支持模块或脚本的代理客户端;
|
- Existing supported proxy-client workflows;
|
||||||
- 希望由第三方客户端继续保持代理配置的场景。
|
- Cases where the third-party client should keep the proxy configuration active.
|
||||||
|
|
||||||
当前客户端状态:
|
Current client status:
|
||||||
|
|
||||||
| 客户端 | 状态 | 社区配置 |
|
| Client | Status | Community configuration |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Shadowrocket | 当前用于真机测试 | App 内置教程 |
|
| Shadowrocket | Currently used for on-device testing | Built-in App guide |
|
||||||
| Surge | 已提供配置,尚未完整验证 | 待征集 |
|
| Surge | Configuration provided, not fully verified | Contributions wanted |
|
||||||
| Quantumult X | 已提供配置,尚未完整验证 | 待征集 |
|
| Quantumult X | Configuration provided, not fully verified | Contributions wanted |
|
||||||
| Loon | 已提供配置,尚未完整验证 | 待征集 |
|
| Loon | Configuration provided, not fully verified | Contributions wanted |
|
||||||
| Stash | 已提供配置,尚未完整验证 | 待征集 |
|
| Stash | Configuration provided, not fully verified | Contributions wanted |
|
||||||
| Egern | 使用 Surge 模块,尚未完整验证 | 待征集 |
|
| Egern | Uses the Surge module, not fully verified | Contributions wanted |
|
||||||
|
|
||||||
社区配置按客户端分区审核;采纳后会在上表链接教程和投稿者,投稿者也可以选择匿名收录。
|
Community configurations are reviewed by client. Accepted submissions are linked in this table with attribution unless
|
||||||
|
the contributor requests anonymous inclusion.
|
||||||
|
|
||||||
模块与脚本现由本仓库托管,原 `Yu9191/wloc` 仓库已删除,仅保留来源致谢,不再作为运行时依赖:
|
Modules and scripts are now hosted by this repository. The original
|
||||||
|
`Yu9191/wloc` repository was deleted and remains attribution only, not a runtime dependency:
|
||||||
|
|
||||||
- [第三方模块说明](docs/THIRD_PARTY_MODULES.md)
|
- [Third-party module documentation](docs/THIRD_PARTY_MODULES.md)
|
||||||
|
|
||||||
第三方客户端、证书、MITM 和代理开关由客户端自身负责。导入任何第三方模块前,请先审查其配置和脚本内容。
|
The selected client owns its certificates, MITM configuration, and proxy switches. Review third-party modules and
|
||||||
|
scripts before importing them.
|
||||||
|
|
||||||
## 快速开始
|
## Quick Start
|
||||||
|
|
||||||
### 1. 安装应用
|
### 1. Install the App
|
||||||
|
|
||||||
你可以使用:
|
You can use:
|
||||||
|
|
||||||
- 自己的 Apple Developer 签名环境;
|
- Your own Apple Developer signing environment;
|
||||||
- 适合个人测试的自签工具;
|
- A self-signing tool suitable for personal testing;
|
||||||
- 项目 Releases 中的未签名 IPA;
|
- The unsigned IPA published in Releases;
|
||||||
- 在 macOS 上按[构建说明](docs/BUILD.md)自行构建。
|
- A source build produced on macOS using the [build guide](docs/BUILD.md).
|
||||||
|
|
||||||
免费自签环境可能无法使用 Network Extension,因此 APP 模式采用设备内本地代理和 Wi-Fi 手动代理,不依赖
|
Free self-signing environments may not provide Network Extension capabilities. App Mode therefore uses an on-device
|
||||||
VPN 组件。
|
proxy plus a manual Wi-Fi HTTP proxy and does not depend on a VPN component.
|
||||||
|
|
||||||
#### 自签安装说明
|
#### Self-Signing Instructions
|
||||||
|
|
||||||
Release 附件是未签名 IPA,需要使用自签工具安装到 iPhone:
|
Release assets are unsigned IPA files and must be installed on an iPhone with a self-signing tool:
|
||||||
|
|
||||||
1. **开启自签支持**:iOS 16 及以上版本前往“设置 → 隐私与安全性 → 开发者模式”,开启后按系统提示重启并
|
1. **Enable sideloading support**: On iOS 16 or newer, open Settings → Privacy & Security → Developer Mode, enable it,
|
||||||
确认;iOS 15 没有此开关,可跳过本步。
|
then restart and confirm when prompted. iOS 15 does not have this switch, so skip this step.
|
||||||
2. **下载 IPA**:前往本项目的 [Releases](https://github.com/xweiba/location-spoofer/releases),下载最新的
|
2. **Download the IPA**: Open this project's [Releases](https://github.com/xweiba/location-spoofer/releases) and download
|
||||||
`PaopaoLocationSpoofer-unsigned.ipa`。
|
the latest `PaopaoLocationSpoofer-unsigned.ipa`.
|
||||||
3. **准备自签软件**:前往 [Impactor Releases](https://github.com/claration/Impactor/releases) 下载对应系统
|
3. **Prepare signing software**: Download the appropriate Impactor build from
|
||||||
版本的 Impactor;也可以使用爱思助手等支持 IPA 自签安装的软件。
|
[Impactor Releases](https://github.com/claration/Impactor/releases). Other tools that support self-signing and installing
|
||||||
4. **连接并安装**:使用 USB 数据线连接 iPhone 与电脑,在手机上选择“信任此电脑”,然后在自签软件中选择
|
IPA files, such as Aisi Assistant, may also be used.
|
||||||
刚下载的 IPA,根据软件提示完成签名与安装。
|
4. **Connect and install**: Connect the iPhone to the computer with a USB cable, choose “Trust This Computer” on the
|
||||||
|
phone, select the downloaded IPA in the signing software, and follow that tool's instructions to sign and install it.
|
||||||
|
|
||||||
Impactor 支持 Windows、macOS 和 Linux;Windows 若无法识别设备,请先安装 iTunes 提供的 Apple 设备驱动。
|
Impactor supports Windows, macOS, and Linux. If Windows cannot detect the device, install the Apple device drivers supplied
|
||||||
爱思助手属于第三方软件,请从其官方渠道获取,并自行评估账号、证书和隐私风险。
|
with iTunes first. Aisi Assistant is third-party software; obtain it from its official channel and evaluate its account,
|
||||||
|
certificate, and privacy risks yourself.
|
||||||
|
|
||||||
安装完成后,若 iOS 阻止打开 App,请前往“设置 → 通用 → VPN 与设备管理”信任对应的开发者 App。免费
|
After installation, if iOS blocks the app from opening, go to Settings → General → VPN & Device Management and trust the
|
||||||
Apple ID 自签通常只有 7 天有效期,到期后需要重新签名安装。
|
corresponding developer app. A free Apple ID signature normally expires after seven days and must then be renewed.
|
||||||
|
|
||||||
### 2. 首次启动
|
### 2. First Launch
|
||||||
|
|
||||||
首次启动时:
|
1. Choose App Mode or Third-party Proxy Mode;
|
||||||
|
2. Complete the corresponding in-app setup;
|
||||||
|
3. Run the environment check;
|
||||||
|
4. Search, tap, or drag on the map to select a test location;
|
||||||
|
5. Enable the test location and verify the result in the authorized test environment.
|
||||||
|
|
||||||
1. 选择 APP 模式或第三方代理模式;
|
### 3. Restore the Real Location
|
||||||
2. 按照 App 内引导完成对应配置;
|
|
||||||
3. 执行环境检测;
|
|
||||||
4. 在地图中搜索、点击或拖动选择测试位置;
|
|
||||||
5. 启用测试位置并在目标测试环境中验证结果。
|
|
||||||
|
|
||||||
### 3. 恢复真实位置
|
App Mode:
|
||||||
|
|
||||||
APP 模式:
|
1. Stop the test location;
|
||||||
|
2. Disable the manual HTTP proxy on the current Wi-Fi network;
|
||||||
|
3. Follow the in-app instructions to refresh the location environment.
|
||||||
|
|
||||||
1. 停止测试位置;
|
Third-party Proxy Mode:
|
||||||
2. 关闭当前 Wi-Fi 的手动 HTTP 代理;
|
|
||||||
3. 按 App 内提示刷新定位环境。
|
|
||||||
|
|
||||||
第三方代理模式:
|
1. Clear the WLOC coordinates from the app;
|
||||||
|
2. Disable the corresponding module or proxy in the third-party client;
|
||||||
|
3. Restore HTTPS decryption and proxy settings according to the client documentation.
|
||||||
|
|
||||||
1. 使用 App 清除 WLOC 坐标;
|
Location caches may take time to refresh. Restart the device if the system or target app continues to show an old
|
||||||
2. 在第三方客户端中关闭对应模块或代理;
|
location.
|
||||||
3. 按客户端要求恢复 HTTPS 解密和代理设置。
|
|
||||||
|
|
||||||
如果系统或目标应用仍显示旧位置,可能需要等待定位缓存刷新,必要时重启设备后再次验证。
|
## Coordinate Handling
|
||||||
|
|
||||||
## 坐标处理
|
The project stores two coordinate representations:
|
||||||
|
|
||||||
项目内部保存两种坐标表示:
|
- WGS-84: the international standard used for WLOC writes;
|
||||||
|
- GCJ-02: the domestic map representation used where required.
|
||||||
|
|
||||||
- WGS-84:国际标准,WLOC 写入使用此坐标;
|
MapKit does not expose a public API that reports whether its current runtime output uses GCJ-02 or WGS-84. The project
|
||||||
- GCJ-02:国内地图标准,用于需要国内地图坐标的场景。
|
uses a fixed-anchor query to resolve the active representation and performs controlled refreshes after blue-dot changes
|
||||||
|
and explicit user actions.
|
||||||
|
|
||||||
MapKit 不提供公开 API 直接返回当前是否使用 GCJ-02 或 WGS-84。项目通过固定锚点查询判断 MapKit 当前返回
|
Each write boundary stores a complete WGS-84/GCJ-02 pair. Rendering selects the field matching the confirmed map
|
||||||
标准,并在运行期间根据蓝点变化和用户操作进行受控刷新。
|
representation instead of repeatedly converting an already typed value.
|
||||||
|
|
||||||
坐标写入边界会保存完整的 WGS-84 / GCJ-02 坐标对,使用时根据当前地图标准选择对应字段,避免重复转换造成
|
## Project Structure
|
||||||
位置偏移。
|
|
||||||
|
|
||||||
## 项目结构
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
App/ SwiftUI 界面、MapKit、定位和运行流程
|
App/ SwiftUI interface, MapKit, location, and runtime flow
|
||||||
Core/ Go 代理、证书服务和定位响应处理
|
Core/ Go proxy, certificate server, and location-response handling
|
||||||
Shared/ 坐标、收藏、日志、配置和共享模型
|
Shared/ Coordinates, favorites, logs, configuration, and shared models
|
||||||
Resources/ Info.plist、Entitlements 和资源文件
|
Resources/ Info.plist, Entitlements, and resources
|
||||||
Config/ 构建配置
|
Config/ Build configuration
|
||||||
Scripts/ 构建、打包和验证脚本
|
Scripts/ Build, packaging, and validation scripts
|
||||||
Tests/ XCTest 和 Shell contract tests
|
Tests/ XCTest and Shell contract tests
|
||||||
docs/ 构建、模块和版本文档
|
docs/ Build, module, and release documentation
|
||||||
```
|
```
|
||||||
|
|
||||||
## 构建项目
|
## Building the Project
|
||||||
|
|
||||||
源码构建需要 macOS 环境:
|
Source builds require:
|
||||||
|
|
||||||
- macOS;
|
- macOS;
|
||||||
- Xcode;
|
- Xcode;
|
||||||
- Xcode Command Line Tools;
|
- Xcode Command Line Tools;
|
||||||
- XcodeGen;
|
- XcodeGen;
|
||||||
- Go 1.23 或更高版本。
|
- Go 1.23 or newer.
|
||||||
|
|
||||||
当前项目不支持在 Windows 上直接构建 iOS 应用。
|
Building the iOS app directly on Windows is not supported.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/xweiba/location-spoofer.git
|
git clone https://github.com/xweiba/location-spoofer.git
|
||||||
@@ -329,119 +343,128 @@ cd location-spoofer
|
|||||||
./build.sh
|
./build.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
运行构建并执行 Simulator 测试:
|
Build and run Simulator tests:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./build.sh --test
|
./build.sh --test
|
||||||
```
|
```
|
||||||
|
|
||||||
构建脚本会生成未签名 IPA:
|
The build script generates an unsigned IPA:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
dist/PaopaoLocationSpoofer-unsigned.ipa
|
dist/PaopaoLocationSpoofer-unsigned.ipa
|
||||||
```
|
```
|
||||||
|
|
||||||
之后需要使用你自己的签名和安装流程部署到测试设备。
|
Deploy it to a test device using your own signing and installation process.
|
||||||
|
|
||||||
## 隐私与安全边界
|
## Privacy and Security Boundaries
|
||||||
|
|
||||||
- 项目不包含遥测或远程控制服务;
|
- The project contains no telemetry or remote-control service;
|
||||||
- 项目不会自动上传用户位置数据;
|
- The project does not automatically upload location data;
|
||||||
- 运行日志保存在设备 App Group 容器中,并自动保留近三天;
|
- Runtime logs remain in the device App Group container and retain only the latest three days;
|
||||||
- 问题报告需要用户主动复制后提交到 GitHub;
|
- Issue reports are copied by the user before being submitted to GitHub;
|
||||||
- APP 模式会访问本机代理和环境验证地址;
|
- App Mode accesses the local proxy and the environment-verification URL;
|
||||||
- 第三方代理模式可能访问本仓库托管的模块地址和 WLOC 配置接口;
|
- Third-party Proxy Mode may access module URLs hosted by this repository and the WLOC configuration endpoint;
|
||||||
- App 生成的 CA 私钥保存在设备 Keychain 中;
|
- The CA private key generated by the app is stored in the device Keychain;
|
||||||
- 第三方客户端模块、MITM 和证书链路由用户选择的客户端负责。
|
- Third-party MITM, certificates, and proxy behavior are owned by the selected client.
|
||||||
|
|
||||||
请不要把真实位置、认证信息、证书私钥或完整敏感日志提交到公开 Issue。
|
Do not post real locations, authentication information, CA private keys, or complete sensitive logs in public issues.
|
||||||
|
|
||||||
## 限制
|
## Limitations
|
||||||
|
|
||||||
- iOS 系统版本变化可能影响定位服务行为;
|
- iOS updates may change location-service behavior;
|
||||||
- MapKit 的坐标返回标准可能随系统、地区和定位环境变化;
|
- MapKit coordinate output can vary with the system, region, and location environment;
|
||||||
- 系统定位存在缓存,切换位置后不一定立即生效;
|
- System location caches may delay visible changes;
|
||||||
- 第三方代理客户端的兼容性和规则行为需要分别验证;
|
- Each third-party proxy client requires separate compatibility testing;
|
||||||
- 不保证所有应用都使用同一种定位 API;
|
- Not every app uses the same location API;
|
||||||
- 不保证所有应用或服务都接受测试坐标;
|
- Not every app or service accepts test coordinates;
|
||||||
- 不保证在所有网络环境、设备型号和 iOS 版本上表现一致。
|
- Behavior is not guaranteed across every network, device model, or iOS version.
|
||||||
|
|
||||||
## 贡献
|
## License
|
||||||
|
|
||||||
欢迎提交:
|
The project code is distributed under the [GNU Affero General Public License v3.0](LICENSE).
|
||||||
|
Third-party WLOC scripts retain their original copyright notices and AGPL-3.0 license in
|
||||||
|
[`ThirdParty/WlocScripts/`](ThirdParty/WlocScripts/THIRD_PARTY_NOTICES.md).
|
||||||
|
Other third-party dependencies remain subject to their respective licenses.
|
||||||
|
|
||||||
- [Bug Report](https://github.com/xweiba/location-spoofer/issues/new?template=bug-report.yml);
|
## Contributing
|
||||||
- [功能建议](https://github.com/xweiba/location-spoofer/discussions/categories/%E5%8A%9F%E8%83%BD%E5%BB%BA%E8%AE%AE);
|
|
||||||
- [使用帮助与兼容性测试](https://github.com/xweiba/location-spoofer/discussions/categories/%E4%BD%BF%E7%94%A8%E5%B8%AE%E5%8A%A9);
|
|
||||||
- [第三方客户端配置和脱敏原始截图](https://github.com/xweiba/location-spoofer/discussions/categories/%E7%AC%AC%E4%B8%89%E6%96%B9%E9%85%8D%E7%BD%AE%E5%88%86%E4%BA%AB);
|
|
||||||
- 性能改进;
|
|
||||||
- 文档改进;
|
|
||||||
- 测试补充。
|
|
||||||
|
|
||||||
提交 Bug 时建议优先通过 App 的“设置 → 支持 → 报告 Bug”生成报告。报告包含:
|
Contributions are welcome for:
|
||||||
|
|
||||||
- iOS 版本;
|
- [Bug reports](https://github.com/xweiba/location-spoofer/issues/new?template=bug-report.yml);
|
||||||
- App 版本;
|
- [Feature requests](https://github.com/xweiba/location-spoofer/discussions/categories/%E5%8A%9F%E8%83%BD%E5%BB%BA%E8%AE%AE);
|
||||||
- 使用的运行模式;
|
- [Usage help and compatibility results](https://github.com/xweiba/location-spoofer/discussions/categories/%E4%BD%BF%E7%94%A8%E5%B8%AE%E5%8A%A9);
|
||||||
- 当前第三方客户端;
|
- [Third-party client configurations and sanitized screenshots](https://github.com/xweiba/location-spoofer/discussions/categories/%E7%AC%AC%E4%B8%89%E6%96%B9%E9%85%8D%E7%BD%AE%E5%88%86%E4%BA%AB);
|
||||||
- 是否可以稳定复现;
|
- Performance improvements;
|
||||||
- 问题描述;
|
- Documentation improvements;
|
||||||
- 脱敏后的运行日志;
|
- Additional tests.
|
||||||
|
|
||||||
GitHub Issue Form 中的“App 生成的诊断报告”字段与 App 复制内容一一对应。
|
For bugs, generate a report from **Settings → Support → Report Bug** in the App whenever possible. The report includes:
|
||||||
|
|
||||||
## 文档
|
- iOS version;
|
||||||
|
- App version;
|
||||||
|
- Runtime mode;
|
||||||
|
- Selected third-party client;
|
||||||
|
- Whether the issue is reproducible;
|
||||||
|
- Problem description;
|
||||||
|
- Sanitized runtime logs;
|
||||||
|
|
||||||
- [构建说明](docs/BUILD.md)
|
Paste it into the **App-generated diagnostic report** field in the GitHub Issue Form.
|
||||||
- [第三方模块说明](docs/THIRD_PARTY_MODULES.md)
|
|
||||||
- [社区客户端教程与截图提交](docs/COMMUNITY_TUTORIALS.md)
|
## Documentation
|
||||||
- [更新日志](docs/CHANGELOG.md)
|
|
||||||
- [英文文档](README.en.md)
|
- [Build guide](docs/BUILD.md)
|
||||||
|
- [Third-party module documentation](docs/THIRD_PARTY_MODULES.md)
|
||||||
|
- [Changelog](docs/CHANGELOG.en.md)
|
||||||
|
- [中文文档](README.zh-CN.md)
|
||||||
- [GitHub Issues](https://github.com/xweiba/location-spoofer/issues)
|
- [GitHub Issues](https://github.com/xweiba/location-spoofer/issues)
|
||||||
|
|
||||||
## 功能预览
|
## Feature Preview
|
||||||
|
|
||||||
以下截图用于展示主界面和部分真机测试场景。实际结果会受到 iOS 版本、网络环境、系统缓存和目标应用定位策略
|
These screenshots show the main interface and selected on-device test scenarios. Actual results depend on the iOS
|
||||||
影响,不代表对所有应用或版本作出兼容性保证。
|
version, network environment, system caches, and the target app's location strategy; they are not a compatibility
|
||||||
|
guarantee for every app or release.
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<th>应用主界面</th>
|
<th>Main interface</th>
|
||||||
<th>Apple 地图测试</th>
|
<th>Apple Maps test</th>
|
||||||
<th>高德地图测试</th>
|
<th>Amap test</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><img src="images/主界面.jpg" alt="Location Spoofer 地图选点主界面" width="220"></td>
|
<td><img src="images/主界面.jpg" alt="Location Spoofer map selection interface" width="220"></td>
|
||||||
<td><img src="images/Apple%20Map.jpg" alt="Apple 地图定位测试场景" width="220"></td>
|
<td><img src="images/Apple%20Map.jpg" alt="Apple Maps location test scenario" width="220"></td>
|
||||||
<td><img src="images/高德地图.jpg" alt="高德地图定位测试场景" width="220"></td>
|
<td><img src="images/高德地图.jpg" alt="Amap location test scenario" width="220"></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>微信测试</th>
|
<th>WeChat test</th>
|
||||||
<th>钉钉测试</th>
|
<th>DingTalk test</th>
|
||||||
<th>Apple Watch 场景测试</th>
|
<th>Apple Watch scenario test</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><img src="images/微信.jpg" alt="微信定位测试场景" width="220"></td>
|
<td><img src="images/微信.jpg" alt="WeChat location test scenario" width="220"></td>
|
||||||
<td><img src="images/钉钉.jpg" alt="钉钉定位测试场景" width="220"></td>
|
<td><img src="images/钉钉.jpg" alt="DingTalk location test scenario" width="220"></td>
|
||||||
<td><img src="images/高血压.jpg" alt="Apple Watch 地区功能测试场景" width="220"></td>
|
<td><img src="images/高血压.jpg" alt="Apple Watch region-feature test scenario" width="220"></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
## 致谢与友链
|
## Acknowledgements and Links
|
||||||
|
|
||||||
核心定位响应处理思路、Go 实现和第三方模块参考自(`Yu9191/wloc` 原仓库已删除):
|
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)
|
||||||
|
|
||||||
感谢以下 LINUX DO 用户对项目的贡献:
|
Thanks to the following LINUX DO users for their contributions:
|
||||||
|
|
||||||
- 功能修复:[陈泽](https://linux.do/u/lixiaobaivv)
|
- Bug fixes: [Chen Ze](https://linux.do/u/lixiaobaivv)
|
||||||
- 思路及建议:[Alex](https://linux.do/u/_alex)、[ye4241](https://linux.do/u/ye4241)
|
- Ideas and suggestions: [Alex](https://linux.do/u/_alex), [ye4241](https://linux.do/u/ye4241)
|
||||||
|
|
||||||
友链:
|
Links:
|
||||||
|
|
||||||
- [LINUX DO](https://linux.do/)
|
- [LINUX DO](https://linux.do/)
|
||||||
- [iOS-Location-Spoofer-Web](https://github.com/akudamatata/iOS-Location-Spoofer-Web)
|
- [iOS-Location-Spoofer-Web](https://github.com/akudamatata/iOS-Location-Spoofer-Web)
|
||||||
|
|
||||||
感谢开源社区中参与 iOS 定位服务研究、网络代理和移动端测试工具建设的贡献者。
|
Thanks to the open-source contributors working on iOS location-service research, network proxies, and mobile testing
|
||||||
|
tools.
|
||||||
|
|||||||
+454
@@ -0,0 +1,454 @@
|
|||||||
|
<div align="center">
|
||||||
|
|
||||||
|
# 📍 Location Spoofer
|
||||||
|
|
||||||
|
### iOS Location Service Research & Testing Framework
|
||||||
|
|
||||||
|
一个用于 **iOS 定位服务研究、软件开发测试和 QA 验证** 的开源项目。
|
||||||
|
|
||||||
|
项目通过本机代理或第三方代理客户端,对 Apple 定位服务的指定响应进行测试环境模拟,帮助开发者验证应用在
|
||||||
|
不同地理位置和定位场景下的行为。
|
||||||
|
|
||||||
|
> ⚠️ **iOS 27 beta 6 起,系统已禁止对 `gs-loc.apple.com` 进行 MITM 拦截。** 目前该版本及之后的 beta 版本暂时无法使用本项目,等待后续适配方案。
|
||||||
|
|
||||||
|
[](project.yml)
|
||||||
|
[](project.yml)
|
||||||
|
[](Core/go.mod)
|
||||||
|
[](docs/CHANGELOG.md)
|
||||||
|
|
||||||
|
[功能概览](#功能概览) ·
|
||||||
|
[工作原理](#工作原理) ·
|
||||||
|
[快速开始](#快速开始) ·
|
||||||
|
[构建项目](#构建项目) ·
|
||||||
|
[English](README.md)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> 本项目用于学习研究、自有设备测试、软件开发和 QA 验证。
|
||||||
|
>
|
||||||
|
> 请仅在你拥有或获得授权的设备、网络和软件环境中使用,并遵守当地法律法规、网络管理规定以及相关服务条
|
||||||
|
> 款。
|
||||||
|
>
|
||||||
|
> 本项目不保证兼容所有 iOS 版本或第三方应用,也不承诺绕过第三方应用的安全策略、业务限制或服务规则。
|
||||||
|
|
||||||
|
## 项目定位
|
||||||
|
|
||||||
|
Location Spoofer 是一个面向 iOS 定位服务行为研究和开发测试的工具。
|
||||||
|
|
||||||
|
它提供:
|
||||||
|
|
||||||
|
- 原生地图选点和位置场景切换;
|
||||||
|
- Apple 定位服务响应的测试环境模拟;
|
||||||
|
- 本地代理和第三方代理两种运行模式;
|
||||||
|
- 坐标标准识别与 WGS-84 / GCJ-02 双坐标管理;
|
||||||
|
- 环境检测、运行日志和问题诊断;
|
||||||
|
- 收藏位置和上次地图状态恢复。
|
||||||
|
|
||||||
|
项目不修改目标 App 的源代码,也不提供远程控制或数据采集服务。
|
||||||
|
|
||||||
|
## 功能概览
|
||||||
|
|
||||||
|
- **原生地图交互**
|
||||||
|
- 使用 MapKit 显示地图和系统蓝点;
|
||||||
|
- 支持搜索、点击选点、拖动地图中心和缩放;
|
||||||
|
- 支持收藏位置和恢复上次选点;
|
||||||
|
- 当前选点同时显示国内坐标和国际坐标,可分别复制。
|
||||||
|
|
||||||
|
- **定位服务响应模拟**
|
||||||
|
- 通过代理层处理指定的 Apple 定位服务请求;
|
||||||
|
- 在测试环境中返回选定的坐标数据;
|
||||||
|
- 不需要修改目标 App 代码。
|
||||||
|
|
||||||
|
- **双运行模式**
|
||||||
|
- APP 模式:在设备内运行 Go 代理,仅支持当前 Wi-Fi 网络;
|
||||||
|
- 第三方代理模式:通过支持的代理客户端覆盖 Wi-Fi、4G 或 5G,具体能力取决于客户端。
|
||||||
|
|
||||||
|
- **环境检测**
|
||||||
|
- APP 模式检测本地代理、证书信任和请求链路;
|
||||||
|
- 第三方代理模式检测 WLOC 配置接口和模块响应;
|
||||||
|
- 失败时提供对应的配置或诊断入口。
|
||||||
|
|
||||||
|
- **开发调试**
|
||||||
|
- 运行日志;
|
||||||
|
- 日志复制和清理;
|
||||||
|
- 坐标标准变化记录;
|
||||||
|
- 脱敏问题报告生成。
|
||||||
|
|
||||||
|
## 工作原理
|
||||||
|
|
||||||
|
### APP 模式
|
||||||
|
|
||||||
|
APP 模式在设备内运行本地 Go 代理,并通过当前 Wi-Fi 的手动 HTTP 代理让指定请求经过本地代理。
|
||||||
|
|
||||||
|
```text
|
||||||
|
iOS 定位请求
|
||||||
|
│
|
||||||
|
│ 当前 Wi-Fi 手动 HTTP 代理
|
||||||
|
▼
|
||||||
|
设备内 wloccore Go 代理
|
||||||
|
│
|
||||||
|
│ 处理指定 Apple 定位服务请求
|
||||||
|
▼
|
||||||
|
Apple 定位服务响应
|
||||||
|
│
|
||||||
|
│ 测试坐标响应
|
||||||
|
▼
|
||||||
|
系统和应用读取定位结果
|
||||||
|
```
|
||||||
|
|
||||||
|
APP 模式:
|
||||||
|
|
||||||
|
- 不创建 Network Extension;
|
||||||
|
- 不显示或占用系统 VPN;
|
||||||
|
- 只覆盖当前 Wi-Fi 网络;
|
||||||
|
- 需要配置当前 Wi-Fi 的手动 HTTP 代理;
|
||||||
|
- 需要安装并信任 App 生成的本机 CA;
|
||||||
|
- 代理只处理项目定义的 Apple 定位服务和环境验证请求,不是通用网络抓包工具。
|
||||||
|
|
||||||
|
### 第三方代理模式
|
||||||
|
|
||||||
|
第三方代理模式不启动 App 内置 Go 代理,也不使用 App 生成的 CA。
|
||||||
|
|
||||||
|
```text
|
||||||
|
地图选点
|
||||||
|
│
|
||||||
|
│ WGS-84 坐标
|
||||||
|
▼
|
||||||
|
WLOC 配置接口
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
第三方代理客户端保存配置
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
第三方代理客户端处理定位服务请求
|
||||||
|
```
|
||||||
|
|
||||||
|
在该模式下:
|
||||||
|
|
||||||
|
- App 负责地图选点、收藏、坐标同步和清除;
|
||||||
|
- 第三方客户端负责代理/VPN、MITM、证书和规则执行;
|
||||||
|
- 坐标持久化由第三方客户端负责;
|
||||||
|
- 是否支持 Wi-Fi、4G 或 5G 取决于客户端;
|
||||||
|
- App 关闭后,第三方客户端中的配置可能继续生效。
|
||||||
|
|
||||||
|
#### 配置接口与客户端适配
|
||||||
|
|
||||||
|
App 不会把坐标上传到项目服务器。它会发起以下 GET 请求,第三方客户端必须在设备本地拦截:
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://gs-loc.apple.com/wloc-settings/save
|
||||||
|
```
|
||||||
|
|
||||||
|
| 操作 | 查询参数 | 用途 |
|
||||||
|
|---|---|---|
|
||||||
|
| 查询 | `action=query` | 检查模块是否连接,并读取当前保存的坐标 |
|
||||||
|
| 保存 | `lon=<WGS-84 经度>&lat=<WGS-84 纬度>&acc=<精度>` | 保存当前选点 |
|
||||||
|
| 清除 | `action=clear` | 删除已保存的测试坐标 |
|
||||||
|
|
||||||
|
拦截脚本必须返回 HTTP 200 和 JSON:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"longitude": 113.0,
|
||||||
|
"latitude": 22.0,
|
||||||
|
"accuracy": 25
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
失败时返回:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": "错误说明"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
查询时没有已保存坐标,可以返回 `{"success":false,"error":"无已保存的坐标"}`。App 会把它识别为
|
||||||
|
“模块已连接,但虚拟定位未开启”。保存成功时,响应中的经纬度必须与请求中的 WGS-84 坐标一致。
|
||||||
|
|
||||||
|
要适配新的第三方客户端,需要:
|
||||||
|
|
||||||
|
1. 为 `gs-loc.apple.com/wloc-settings/save` 添加 HTTP 请求脚本,解析上述参数并返回约定 JSON;
|
||||||
|
2. 使用客户端的持久化存储保存坐标、精度和可选状态;
|
||||||
|
3. 为 `gs-loc.apple.com` 和 `gs-loc-cn.apple.com` 配置 HTTPS 解密;
|
||||||
|
4. 拦截 `gs-loc(-cn).apple.com/clls/wloc` 响应,读取同一份持久化数据并修改 WLOC 响应;
|
||||||
|
5. 提供可订阅的模块文件,并确认查询、保存、清除和定位恢复都能在真机完成。
|
||||||
|
|
||||||
|
App 只验证配置接口的 HTTP 状态、JSON 格式和坐标回读,不管理第三方客户端的证书、MITM、VPN 或代理状态。
|
||||||
|
|
||||||
|
不要同时启用 APP 模式代理和第三方代理模式,避免两个代理链路互相干扰。
|
||||||
|
|
||||||
|
## 运行模式
|
||||||
|
|
||||||
|
### APP 模式
|
||||||
|
|
||||||
|
适用于:
|
||||||
|
|
||||||
|
- 只使用 Wi-Fi 的设备测试;
|
||||||
|
- 不依赖第三方代理客户端的本地验证;
|
||||||
|
- 需要在 App 内完成代理、证书和环境检测的场景。
|
||||||
|
|
||||||
|
使用条件:
|
||||||
|
|
||||||
|
- iOS 设备;
|
||||||
|
- 当前 Wi-Fi 支持手动 HTTP 代理;
|
||||||
|
- 安装并信任本机 CA;
|
||||||
|
- 在 App 内完成代理配置和环境检测。
|
||||||
|
|
||||||
|
### 第三方代理模式
|
||||||
|
|
||||||
|
适用于:
|
||||||
|
|
||||||
|
- 需要 Wi-Fi、4G 或 5G 网络覆盖的测试;
|
||||||
|
- 已经使用支持模块或脚本的代理客户端;
|
||||||
|
- 希望由第三方客户端继续保持代理配置的场景。
|
||||||
|
|
||||||
|
当前客户端状态:
|
||||||
|
|
||||||
|
| 客户端 | 状态 | 社区配置 |
|
||||||
|
|---|---|---|
|
||||||
|
| Shadowrocket | 当前用于真机测试 | App 内置教程 |
|
||||||
|
| Surge | 已提供配置,尚未完整验证 | 待征集 |
|
||||||
|
| Quantumult X | 已提供配置,尚未完整验证 | 待征集 |
|
||||||
|
| Loon | 已提供配置,尚未完整验证 | 待征集 |
|
||||||
|
| Stash | 已提供配置,尚未完整验证 | 待征集 |
|
||||||
|
| Egern | 使用 Surge 模块,尚未完整验证 | 待征集 |
|
||||||
|
|
||||||
|
社区配置按客户端分区审核;采纳后会在上表链接教程和投稿者,投稿者也可以选择匿名收录。
|
||||||
|
|
||||||
|
模块与脚本现由本仓库托管,原 `Yu9191/wloc` 仓库已删除,仅保留来源致谢,不再作为运行时依赖:
|
||||||
|
|
||||||
|
- [第三方模块说明](docs/THIRD_PARTY_MODULES.md)
|
||||||
|
|
||||||
|
第三方客户端、证书、MITM 和代理开关由客户端自身负责。导入任何第三方模块前,请先审查其配置和脚本内容。
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 1. 安装应用
|
||||||
|
|
||||||
|
你可以使用:
|
||||||
|
|
||||||
|
- 自己的 Apple Developer 签名环境;
|
||||||
|
- 适合个人测试的自签工具;
|
||||||
|
- 项目 Releases 中的未签名 IPA;
|
||||||
|
- 在 macOS 上按[构建说明](docs/BUILD.md)自行构建。
|
||||||
|
|
||||||
|
免费自签环境可能无法使用 Network Extension,因此 APP 模式采用设备内本地代理和 Wi-Fi 手动代理,不依赖
|
||||||
|
VPN 组件。
|
||||||
|
|
||||||
|
#### 自签安装说明
|
||||||
|
|
||||||
|
Release 附件是未签名 IPA,需要使用自签工具安装到 iPhone:
|
||||||
|
|
||||||
|
1. **开启自签支持**:iOS 16 及以上版本前往“设置 → 隐私与安全性 → 开发者模式”,开启后按系统提示重启并
|
||||||
|
确认;iOS 15 没有此开关,可跳过本步。
|
||||||
|
2. **下载 IPA**:前往本项目的 [Releases](https://github.com/xweiba/location-spoofer/releases),下载最新的
|
||||||
|
`PaopaoLocationSpoofer-unsigned.ipa`。
|
||||||
|
3. **准备自签软件**:前往 [Impactor Releases](https://github.com/claration/Impactor/releases) 下载对应系统
|
||||||
|
版本的 Impactor;也可以使用爱思助手等支持 IPA 自签安装的软件。
|
||||||
|
4. **连接并安装**:使用 USB 数据线连接 iPhone 与电脑,在手机上选择“信任此电脑”,然后在自签软件中选择
|
||||||
|
刚下载的 IPA,根据软件提示完成签名与安装。
|
||||||
|
|
||||||
|
Impactor 支持 Windows、macOS 和 Linux;Windows 若无法识别设备,请先安装 iTunes 提供的 Apple 设备驱动。
|
||||||
|
爱思助手属于第三方软件,请从其官方渠道获取,并自行评估账号、证书和隐私风险。
|
||||||
|
|
||||||
|
安装完成后,若 iOS 阻止打开 App,请前往“设置 → 通用 → VPN 与设备管理”信任对应的开发者 App。免费
|
||||||
|
Apple ID 自签通常只有 7 天有效期,到期后需要重新签名安装。
|
||||||
|
|
||||||
|
### 2. 首次启动
|
||||||
|
|
||||||
|
首次启动时:
|
||||||
|
|
||||||
|
1. 选择 APP 模式或第三方代理模式;
|
||||||
|
2. 按照 App 内引导完成对应配置;
|
||||||
|
3. 执行环境检测;
|
||||||
|
4. 在地图中搜索、点击或拖动选择测试位置;
|
||||||
|
5. 启用测试位置并在目标测试环境中验证结果。
|
||||||
|
|
||||||
|
### 3. 恢复真实位置
|
||||||
|
|
||||||
|
APP 模式:
|
||||||
|
|
||||||
|
1. 停止测试位置;
|
||||||
|
2. 关闭当前 Wi-Fi 的手动 HTTP 代理;
|
||||||
|
3. 按 App 内提示刷新定位环境。
|
||||||
|
|
||||||
|
第三方代理模式:
|
||||||
|
|
||||||
|
1. 使用 App 清除 WLOC 坐标;
|
||||||
|
2. 在第三方客户端中关闭对应模块或代理;
|
||||||
|
3. 按客户端要求恢复 HTTPS 解密和代理设置。
|
||||||
|
|
||||||
|
如果系统或目标应用仍显示旧位置,可能需要等待定位缓存刷新,必要时重启设备后再次验证。
|
||||||
|
|
||||||
|
## 坐标处理
|
||||||
|
|
||||||
|
项目内部保存两种坐标表示:
|
||||||
|
|
||||||
|
- WGS-84:国际标准,WLOC 写入使用此坐标;
|
||||||
|
- GCJ-02:国内地图标准,用于需要国内地图坐标的场景。
|
||||||
|
|
||||||
|
MapKit 不提供公开 API 直接返回当前是否使用 GCJ-02 或 WGS-84。项目通过固定锚点查询判断 MapKit 当前返回
|
||||||
|
标准,并在运行期间根据蓝点变化和用户操作进行受控刷新。
|
||||||
|
|
||||||
|
坐标写入边界会保存完整的 WGS-84 / GCJ-02 坐标对,使用时根据当前地图标准选择对应字段,避免重复转换造成
|
||||||
|
位置偏移。
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```text
|
||||||
|
App/ SwiftUI 界面、MapKit、定位和运行流程
|
||||||
|
Core/ Go 代理、证书服务和定位响应处理
|
||||||
|
Shared/ 坐标、收藏、日志、配置和共享模型
|
||||||
|
Resources/ Info.plist、Entitlements 和资源文件
|
||||||
|
Config/ 构建配置
|
||||||
|
Scripts/ 构建、打包和验证脚本
|
||||||
|
Tests/ XCTest 和 Shell contract tests
|
||||||
|
docs/ 构建、模块和版本文档
|
||||||
|
```
|
||||||
|
|
||||||
|
## 构建项目
|
||||||
|
|
||||||
|
源码构建需要 macOS 环境:
|
||||||
|
|
||||||
|
- macOS;
|
||||||
|
- Xcode;
|
||||||
|
- Xcode Command Line Tools;
|
||||||
|
- XcodeGen;
|
||||||
|
- Go 1.23 或更高版本。
|
||||||
|
|
||||||
|
当前项目不支持在 Windows 上直接构建 iOS 应用。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/xweiba/location-spoofer.git
|
||||||
|
cd location-spoofer
|
||||||
|
|
||||||
|
./build.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
运行构建并执行 Simulator 测试:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./build.sh --test
|
||||||
|
```
|
||||||
|
|
||||||
|
构建脚本会生成未签名 IPA:
|
||||||
|
|
||||||
|
```text
|
||||||
|
dist/PaopaoLocationSpoofer-unsigned.ipa
|
||||||
|
```
|
||||||
|
|
||||||
|
之后需要使用你自己的签名和安装流程部署到测试设备。
|
||||||
|
|
||||||
|
## 隐私与安全边界
|
||||||
|
|
||||||
|
- 项目不包含遥测或远程控制服务;
|
||||||
|
- 项目不会自动上传用户位置数据;
|
||||||
|
- 运行日志保存在设备 App Group 容器中,并自动保留近三天;
|
||||||
|
- 问题报告需要用户主动复制后提交到 GitHub;
|
||||||
|
- APP 模式会访问本机代理和环境验证地址;
|
||||||
|
- 第三方代理模式可能访问本仓库托管的模块地址和 WLOC 配置接口;
|
||||||
|
- App 生成的 CA 私钥保存在设备 Keychain 中;
|
||||||
|
- 第三方客户端模块、MITM 和证书链路由用户选择的客户端负责。
|
||||||
|
|
||||||
|
请不要把真实位置、认证信息、证书私钥或完整敏感日志提交到公开 Issue。
|
||||||
|
|
||||||
|
## 限制
|
||||||
|
|
||||||
|
- iOS 系统版本变化可能影响定位服务行为;
|
||||||
|
- MapKit 的坐标返回标准可能随系统、地区和定位环境变化;
|
||||||
|
- 系统定位存在缓存,切换位置后不一定立即生效;
|
||||||
|
- 第三方代理客户端的兼容性和规则行为需要分别验证;
|
||||||
|
- 不保证所有应用都使用同一种定位 API;
|
||||||
|
- 不保证所有应用或服务都接受测试坐标;
|
||||||
|
- 不保证在所有网络环境、设备型号和 iOS 版本上表现一致。
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
项目代码采用 [GNU Affero General Public License v3.0](LICENSE) 授权。
|
||||||
|
第三方 WLOC 脚本保留原作者版权声明及其 AGPL-3.0 许可,详情见
|
||||||
|
[`ThirdParty/WlocScripts/`](ThirdParty/WlocScripts/THIRD_PARTY_NOTICES.md)。
|
||||||
|
其他第三方依赖仍遵循各自的许可证。
|
||||||
|
|
||||||
|
## 贡献
|
||||||
|
|
||||||
|
欢迎提交:
|
||||||
|
|
||||||
|
- [Bug Report](https://github.com/xweiba/location-spoofer/issues/new?template=bug-report.yml);
|
||||||
|
- [功能建议](https://github.com/xweiba/location-spoofer/discussions/categories/%E5%8A%9F%E8%83%BD%E5%BB%BA%E8%AE%AE);
|
||||||
|
- [使用帮助与兼容性测试](https://github.com/xweiba/location-spoofer/discussions/categories/%E4%BD%BF%E7%94%A8%E5%B8%AE%E5%8A%A9);
|
||||||
|
- [第三方客户端配置和脱敏原始截图](https://github.com/xweiba/location-spoofer/discussions/categories/%E7%AC%AC%E4%B8%89%E6%96%B9%E9%85%8D%E7%BD%AE%E5%88%86%E4%BA%AB);
|
||||||
|
- 性能改进;
|
||||||
|
- 文档改进;
|
||||||
|
- 测试补充。
|
||||||
|
|
||||||
|
提交 Bug 时建议优先通过 App 的“设置 → 支持 → 报告 Bug”生成报告。报告包含:
|
||||||
|
|
||||||
|
- iOS 版本;
|
||||||
|
- App 版本;
|
||||||
|
- 使用的运行模式;
|
||||||
|
- 当前第三方客户端;
|
||||||
|
- 是否可以稳定复现;
|
||||||
|
- 问题描述;
|
||||||
|
- 脱敏后的运行日志;
|
||||||
|
|
||||||
|
GitHub Issue Form 中的“App 生成的诊断报告”字段与 App 复制内容一一对应。
|
||||||
|
|
||||||
|
## 文档
|
||||||
|
|
||||||
|
- [构建说明](docs/BUILD.md)
|
||||||
|
- [第三方模块说明](docs/THIRD_PARTY_MODULES.md)
|
||||||
|
- [社区客户端教程与截图提交](docs/COMMUNITY_TUTORIALS.md)
|
||||||
|
- [更新日志](docs/CHANGELOG.md)
|
||||||
|
- [英文文档](README.md)
|
||||||
|
- [GitHub Issues](https://github.com/xweiba/location-spoofer/issues)
|
||||||
|
|
||||||
|
## 功能预览
|
||||||
|
|
||||||
|
以下截图用于展示主界面和部分真机测试场景。实际结果会受到 iOS 版本、网络环境、系统缓存和目标应用定位策略
|
||||||
|
影响,不代表对所有应用或版本作出兼容性保证。
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>应用主界面</th>
|
||||||
|
<th>Apple 地图测试</th>
|
||||||
|
<th>高德地图测试</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="images/主界面.jpg" alt="Location Spoofer 地图选点主界面" width="220"></td>
|
||||||
|
<td><img src="images/Apple%20Map.jpg" alt="Apple 地图定位测试场景" width="220"></td>
|
||||||
|
<td><img src="images/高德地图.jpg" alt="高德地图定位测试场景" width="220"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>微信测试</th>
|
||||||
|
<th>钉钉测试</th>
|
||||||
|
<th>Apple Watch 场景测试</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="images/微信.jpg" alt="微信定位测试场景" width="220"></td>
|
||||||
|
<td><img src="images/钉钉.jpg" alt="钉钉定位测试场景" width="220"></td>
|
||||||
|
<td><img src="images/高血压.jpg" alt="Apple Watch 地区功能测试场景" width="220"></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
## 致谢与友链
|
||||||
|
|
||||||
|
核心定位响应处理思路、Go 实现和第三方模块参考自(`Yu9191/wloc` 原仓库已删除):
|
||||||
|
|
||||||
|
- [Yu9191/wloc](https://github.com/Yu9191/wloc)
|
||||||
|
- [ios-location-spoofer](https://github.com/mekos2772/ios-location-spoofer)
|
||||||
|
|
||||||
|
感谢以下 LINUX DO 用户对项目的贡献:
|
||||||
|
|
||||||
|
- 功能修复:[陈泽](https://linux.do/u/lixiaobaivv)
|
||||||
|
- 思路及建议:[Alex](https://linux.do/u/_alex)、[ye4241](https://linux.do/u/ye4241)
|
||||||
|
|
||||||
|
友链:
|
||||||
|
|
||||||
|
- [LINUX DO](https://linux.do/)
|
||||||
|
- [iOS-Location-Spoofer-Web](https://github.com/akudamatata/iOS-Location-Spoofer-Web)
|
||||||
|
|
||||||
|
感谢开源社区中参与 iOS 定位服务研究、网络代理和移动端测试工具建设的贡献者。
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
/* ============================================================
|
/* ============================================================
|
||||||
English localization overlay.
|
English localization and development-language fallback.
|
||||||
Keys are the original Simplified-Chinese source strings
|
Keys are the original Simplified-Chinese source strings
|
||||||
(development language = zh-Hans). Any key not found here
|
All localized keys are mirrored in zh-Hans and zh-Hant tables.
|
||||||
falls back to the Chinese key text automatically.
|
Untranslated keys still display their Chinese source text.
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
/* MARK: - Shared models (ProxyRuntimeMode / VerificationResult) */
|
/* MARK: - Shared models (ProxyRuntimeMode / VerificationResult) */
|
||||||
@@ -60,6 +60,8 @@
|
|||||||
"验证状态" = "Verification Status";
|
"验证状态" = "Verification Status";
|
||||||
"已复制模块订阅地址" = "Subscription URL copied";
|
"已复制模块订阅地址" = "Subscription URL copied";
|
||||||
"复制模块订阅地址" = "Copy module subscription URL";
|
"复制模块订阅地址" = "Copy module subscription URL";
|
||||||
|
"复制 %@" = "Copy %@";
|
||||||
|
"已复制 %@" = "Copied %@";
|
||||||
"已复制解密域名" = "Decryption hostnames copied";
|
"已复制解密域名" = "Decryption hostnames copied";
|
||||||
"复制解密域名" = "Copy decryption hostnames";
|
"复制解密域名" = "Copy decryption hostnames";
|
||||||
"打开 %@" = "Open %@";
|
"打开 %@" = "Open %@";
|
||||||
|
|||||||
@@ -0,0 +1,668 @@
|
|||||||
|
/* Generated from en.lproj/Localizable.strings by Scripts/generate-chinese-localizations.swift. */
|
||||||
|
"APP模式" = "APP模式";
|
||||||
|
"第三方代理模式" = "第三方代理模式";
|
||||||
|
"成功" = "成功";
|
||||||
|
"已有验证正在进行" = "已有验证正在进行";
|
||||||
|
"验证已被新位置取代" = "验证已被新位置取代";
|
||||||
|
"证书未信任" = "证书未信任";
|
||||||
|
"WiFi代理未配置" = "WiFi代理未配置";
|
||||||
|
"坐标写入失败" = "坐标写入失败";
|
||||||
|
"改写验证失败" = "改写验证失败";
|
||||||
|
"运行模式" = "运行模式";
|
||||||
|
"模式" = "模式";
|
||||||
|
"状态" = "状态";
|
||||||
|
"本机代理" = "本机代理";
|
||||||
|
"第三方模块" = "第三方模块";
|
||||||
|
"检测连接" = "检测连接";
|
||||||
|
"虚拟定位" = "虚拟定位";
|
||||||
|
"定位模拟" = "定位模拟";
|
||||||
|
"运动状态模拟" = "运动状态模拟";
|
||||||
|
"实验性功能,默认关闭。开启后会同时模拟定位响应中的运动状态。" = "实验性功能,默认关闭。开启后会同时模拟定位响应中的运动状态。";
|
||||||
|
"随机扰动" = "随机扰动";
|
||||||
|
"开启后,下次同步坐标时会给目标点添加随机偏移(默认半径 50 米),避免位置固定在同一点。" = "开启后,下次同步坐标时会给目标点添加随机偏移(默认半径 50 米),避免位置固定在同一点。";
|
||||||
|
"说明" = "说明";
|
||||||
|
"工作原理" = "工作原理";
|
||||||
|
"应用" = "应用";
|
||||||
|
"进入引导页" = "进入引导页";
|
||||||
|
"正在检查…" = "正在检查…";
|
||||||
|
"检查更新" = "检查更新";
|
||||||
|
"版本" = "版本";
|
||||||
|
"证书" = "证书";
|
||||||
|
"重置证书" = "重置证书";
|
||||||
|
"仅删除 App 钥匙串中的设备 CA。iOS 中已经安装的旧证书需要在系统设置里手动移除。" = "仅删除 App 钥匙串中的设备 CA。iOS 中已经安装的旧证书需要在系统设置里手动移除。";
|
||||||
|
"支持" = "支持";
|
||||||
|
"使用帮助" = "使用帮助";
|
||||||
|
"功能建议" = "功能建议";
|
||||||
|
"分享第三方配置" = "分享第三方配置";
|
||||||
|
"关于" = "关于";
|
||||||
|
"如果觉得好用,欢迎去 GitHub 给项目点个 Star" = "如果觉得好用,欢迎去 GitHub 给项目点个 Star";
|
||||||
|
"致谢" = "致谢";
|
||||||
|
"核心定位改写逻辑移植自 Yu9191/wloc" = "核心定位改写逻辑移植自 Yu9191/wloc";
|
||||||
|
"设置" = "设置";
|
||||||
|
"完成" = "完成";
|
||||||
|
"代理操作失败" = "代理操作失败";
|
||||||
|
"重置证书?" = "重置证书?";
|
||||||
|
"重置并生成新证书" = "重置并生成新证书";
|
||||||
|
"当前虚拟定位和本地代理将停止。App 会删除钥匙串中的设备 CA、立即生成新证书,并打开安装与信任引导。你还需要前往 iOS「设置 → 通用 → VPN 与设备管理」手动删除旧证书,然后重新下载安装并完全信任新证书。" = "当前虚拟定位和本地代理将停止。App 会删除钥匙串中的设备 CA、立即生成新证书,并打开安装与信任引导。你还需要前往 iOS「设置 → 通用 → VPN 与设备管理」手动删除旧证书,然后重新下载安装并完全信任新证书。";
|
||||||
|
"第三方代理配置" = "第三方代理配置";
|
||||||
|
"客户端" = "客户端";
|
||||||
|
"使用国内镜像下载模块" = "使用国内镜像下载模块";
|
||||||
|
"仅影响之后复制和重新导入的模块地址;已安装模块需要重新导入后切换来源。" = "仅影响之后复制和重新导入的模块地址;已安装模块需要重新导入后切换来源。";
|
||||||
|
"验证状态" = "验证状态";
|
||||||
|
"已复制模块订阅地址" = "已复制模块订阅地址";
|
||||||
|
"复制模块订阅地址" = "复制模块订阅地址";
|
||||||
|
"复制 %@" = "复制 %@";
|
||||||
|
"已复制 %@" = "已复制 %@";
|
||||||
|
"已复制解密域名" = "已复制解密域名";
|
||||||
|
"复制解密域名" = "复制解密域名";
|
||||||
|
"打开 %@" = "打开 %@";
|
||||||
|
"重新打开配置引导" = "重新打开配置引导";
|
||||||
|
"Egern 直接使用 Surge 的 .sgmodule 模块。" = "Egern 直接使用 Surge 的 .sgmodule 模块。";
|
||||||
|
"Stash 直接订阅 .stoverride,不要通过 Script Hub 转换。" = "Stash 直接订阅 .stoverride,不要通过 Script Hub 转换。";
|
||||||
|
"复制模块订阅地址后,在对应代理客户端中添加模块/重写订阅,并为复制的全部域名(含 gsp-ssl.ls.apple.com、bluedot.is.autonavi.com)启用 MITM。第三方客户端保存坐标后,即使关闭本 App,坐标仍由代理客户端持久化并继续生效。" = "复制模块订阅地址后,在对应代理客户端中添加模块/重写订阅,并为复制的全部域名(含 gsp-ssl.ls.apple.com、bluedot.is.autonavi.com)启用 MITM。第三方客户端保存坐标后,即使关闭本 App,坐标仍由代理客户端持久化并继续生效。";
|
||||||
|
"已是最新版本" = "已是最新版本";
|
||||||
|
"当前版本 %@,远程最新版本 %@。" = "当前版本 %@,远程最新版本 %@。";
|
||||||
|
"检查更新失败" = "检查更新失败";
|
||||||
|
"无法获取远程版本信息,请检查网络后重试。" = "无法获取远程版本信息,请检查网络后重试。";
|
||||||
|
"未检测" = "未检测";
|
||||||
|
"已连接,有坐标" = "已连接,有坐标";
|
||||||
|
"已连接,无坐标" = "已连接,无坐标";
|
||||||
|
"连接失败" = "连接失败";
|
||||||
|
"已开启" = "已开启";
|
||||||
|
"已关闭" = "已关闭";
|
||||||
|
"第三方已保存" = "第三方已保存";
|
||||||
|
"未保存" = "未保存";
|
||||||
|
"未知" = "未知";
|
||||||
|
"App 只负责地图选点、收藏和发送 WGS-84 坐标。第三方代理客户端通过模块拦截 Apple WLOC 请求并持久化当前坐标;本模式不启动本机代理,不使用 App 的 CA,也不需要配置 127.0.0.1:8888。" = "App 只负责地图选点、收藏和发送 WGS-84 坐标。第三方代理客户端通过模块拦截 Apple WLOC 请求并持久化当前坐标;本模式不启动本机代理,不使用 App 的 CA,也不需要配置 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 返回的定位坐标改写为你设置的虚拟坐标,再加密返回给系统,从而实现虚拟定位。" = "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 返回的定位坐标改写为你设置的虚拟坐标,再加密返回给系统,从而实现虚拟定位。";
|
||||||
|
"模式已切换" = "模式已切换";
|
||||||
|
"第三方代理模式检测通过。请关闭 Wi-Fi 中的 127.0.0.1:8888 手动代理,避免双重拦截。" = "第三方代理模式检测通过。请关闭 Wi-Fi 中的 127.0.0.1:8888 手动代理,避免双重拦截。";
|
||||||
|
"APP 模式环境检测通过。请停用第三方 WLOC 模块或代理连接,避免双重拦截。" = "APP 模式环境检测通过。请停用第三方 WLOC 模块或代理连接,避免双重拦截。";
|
||||||
|
"证书重置失败" = "证书重置失败";
|
||||||
|
"无法打开客户端" = "无法打开客户端";
|
||||||
|
"无法打开 %@,请确认客户端已安装后手动打开。" = "无法打开 %@,请确认客户端已安装后手动打开。";
|
||||||
|
"正在初始化地图与本地代理…" = "正在初始化地图与本地代理…";
|
||||||
|
"正在初始化地图…" = "正在初始化地图…";
|
||||||
|
"更新说明暂时无法加载,请前往最新 Release 页面查看。" = "更新说明暂时无法加载,请前往最新 Release 页面查看。";
|
||||||
|
"需要更新" = "需要更新";
|
||||||
|
"当前版本 %@ 已停止支持,请更新到 %@ 后继续使用。\n\n%@" = "当前版本 %@ 已停止支持,请更新到 %@ 后继续使用。\n\n%@";
|
||||||
|
"立即更新" = "立即更新";
|
||||||
|
"发现新版本" = "发现新版本";
|
||||||
|
"当前版本 %@,最新版本 %@。\n\n%@" = "当前版本 %@,最新版本 %@。\n\n%@";
|
||||||
|
"前往更新" = "前往更新";
|
||||||
|
"稍后" = "稍后";
|
||||||
|
"遇到问题时,在这里生成 Bug 报告。系统会运行一次诊断测试,并将完整报告复制到剪切板。跳转到 GitHub 后,请粘贴到“App 生成的诊断报告”字段。" = "遇到问题时,在这里生成 Bug 报告。系统会运行一次诊断测试,并将完整报告复制到剪切板。跳转到 GitHub 后,请粘贴到“App 生成的诊断报告”字段。";
|
||||||
|
"可复现环境" = "可复现环境";
|
||||||
|
"当前设备上问题稳定复现,非偶发性。" = "当前设备上问题稳定复现,非偶发性。";
|
||||||
|
"问题描述" = "问题描述";
|
||||||
|
"正在生成报告…" = "正在生成报告…";
|
||||||
|
"生成 Bug 报告" = "生成 Bug 报告";
|
||||||
|
"报告 Bug" = "报告 Bug";
|
||||||
|
"已生成" = "已生成";
|
||||||
|
"打开 GitHub 表单" = "打开 GitHub 表单";
|
||||||
|
"稍后再说" = "稍后再说";
|
||||||
|
"Bug 报告已复制到剪切板。请在 GitHub 表单的“App 生成的诊断报告”字段中粘贴并提交。" = "Bug 报告已复制到剪切板。请在 GitHub 表单的“App 生成的诊断报告”字段中粘贴并提交。";
|
||||||
|
"过滤日志" = "过滤日志";
|
||||||
|
"暂无运行日志" = "暂无运行日志";
|
||||||
|
"无匹配日志" = "无匹配日志";
|
||||||
|
"运行日志" = "运行日志";
|
||||||
|
"日志自动清理,仅保留近 3 天" = "日志自动清理,仅保留近 3 天";
|
||||||
|
"关闭" = "关闭";
|
||||||
|
"复制全部日志" = "复制全部日志";
|
||||||
|
"清空全部日志" = "清空全部日志";
|
||||||
|
"清空所有运行日志?" = "清空所有运行日志?";
|
||||||
|
"清空日志" = "清空日志";
|
||||||
|
"取消" = "取消";
|
||||||
|
"正在检测…" = "正在检测…";
|
||||||
|
"环境检测" = "环境检测";
|
||||||
|
"检查第三方模块能否拦截并响应 query 请求;不会写入测试坐标。" = "检查第三方模块能否拦截并响应 query 请求;不会写入测试坐标。";
|
||||||
|
"依次检查:本地代理 → CA 证书信任 → Wi-Fi 代理链路。" = "依次检查:本地代理 → CA 证书信任 → Wi-Fi 代理链路。";
|
||||||
|
"测试日志" = "测试日志";
|
||||||
|
"已复制" = "已复制";
|
||||||
|
"代理运行中" = "代理运行中";
|
||||||
|
"代理未运行" = "代理未运行";
|
||||||
|
"可修改" = "可修改";
|
||||||
|
"不可修改" = "不可修改";
|
||||||
|
"环境检测通过" = "环境检测通过";
|
||||||
|
"环境检测失败: %@" = "环境检测失败: %@";
|
||||||
|
",查看下方日志" = ",查看下方日志";
|
||||||
|
"第三方模块连接通过,已有坐标" = "第三方模块连接通过,已有坐标";
|
||||||
|
"第三方模块连接通过,暂无坐标" = "第三方模块连接通过,暂无坐标";
|
||||||
|
"第三方模块连接失败" = "第三方模块连接失败";
|
||||||
|
"======== 第三方代理连接检测 ========" = "======== 第三方代理连接检测 ========";
|
||||||
|
"模式: 测试模式" = "模式: 测试模式";
|
||||||
|
"请求: wloc-settings/save?action=query" = "请求: wloc-settings/save?action=query";
|
||||||
|
"拦截响应: 有效 JSON" = "拦截响应: 有效 JSON";
|
||||||
|
"已保存坐标: %@" = "已保存坐标: %@";
|
||||||
|
"结果: %@" = "结果: %@";
|
||||||
|
"生效说明" = "生效说明";
|
||||||
|
"失效说明" = "失效说明";
|
||||||
|
"关闭 WiFi 代理" = "关闭 WiFi 代理";
|
||||||
|
"知道了" = "知道了";
|
||||||
|
"让虚拟定位生效" = "让虚拟定位生效";
|
||||||
|
"确认第三方代理已开启" = "确认第三方代理已开启";
|
||||||
|
"保持已导入的 WLOC 模块、HTTPS 解密和第三方代理/VPN 连接开启。" = "保持已导入的 WLOC 模块、HTTPS 解密和第三方代理/VPN 连接开启。";
|
||||||
|
"开启飞行模式" = "开启飞行模式";
|
||||||
|
"从控制中心打开飞行模式(点飞机图标),Wi‑Fi 会自动断开。这是为了清除 iOS 的定位缓存。等待 2 秒。" = "从控制中心打开飞行模式(点飞机图标),Wi‑Fi 会自动断开。这是为了清除 iOS 的定位缓存。等待 2 秒。";
|
||||||
|
"关闭 Wi‑Fi" = "关闭 Wi‑Fi";
|
||||||
|
"从控制中心再点一下 Wi‑Fi 图标,确认 Wi‑Fi 已关闭。等待 2 秒。" = "从控制中心再点一下 Wi‑Fi 图标,确认 Wi‑Fi 已关闭。等待 2 秒。";
|
||||||
|
"关闭系统定位服务" = "关闭系统定位服务";
|
||||||
|
"打开系统「设置 → 隐私与安全性 → 定位服务」,关闭顶部的总开关。等待 2 秒。" = "打开系统「设置 → 隐私与安全性 → 定位服务」,关闭顶部的总开关。等待 2 秒。";
|
||||||
|
"打开 Wi‑Fi,启动虚拟定位" = "打开 Wi‑Fi,启动虚拟定位";
|
||||||
|
"从控制中心打开 Wi‑Fi(飞行模式保持开启),确认第三方代理已连接。坐标已经同步到第三方代理。等待 2 秒。" = "从控制中心打开 Wi‑Fi(飞行模式保持开启),确认第三方代理已连接。坐标已经同步到第三方代理。等待 2 秒。";
|
||||||
|
"从控制中心打开 Wi‑Fi(飞行模式保持开启),进入 App 点底部「开始虚拟定位」。等待 2 秒。" = "从控制中心打开 Wi‑Fi(飞行模式保持开启),进入 App 点底部「开始虚拟定位」。等待 2 秒。";
|
||||||
|
"关闭飞行模式" = "关闭飞行模式";
|
||||||
|
"从控制中心关闭飞行模式。等待 2 秒。" = "从控制中心关闭飞行模式。等待 2 秒。";
|
||||||
|
"重新开启定位服务" = "重新开启定位服务";
|
||||||
|
"再次进入「设置 → 隐私与安全性 → 定位服务」,打开总开关。完成后打开地图验证定位是否已变化。" = "再次进入「设置 → 隐私与安全性 → 定位服务」,打开总开关。完成后打开地图验证定位是否已变化。";
|
||||||
|
"还是无法生效?" = "还是无法生效?";
|
||||||
|
"操作到第 3 步时关机重启,开机后从第 4 步继续。这样能彻底清除系统缓存的定位数据。" = "操作到第 3 步时关机重启,开机后从第 4 步继续。这样能彻底清除系统缓存的定位数据。";
|
||||||
|
"去设置" = "去设置";
|
||||||
|
"取消虚拟定位" = "取消虚拟定位";
|
||||||
|
"从控制中心打开飞行模式,Wi‑Fi 会自动断开。等待 2 秒。" = "从控制中心打开飞行模式,Wi‑Fi 会自动断开。等待 2 秒。";
|
||||||
|
"从控制中心确认 Wi‑Fi 已关闭。等待 2 秒。" = "从控制中心确认 Wi‑Fi 已关闭。等待 2 秒。";
|
||||||
|
"打开「设置 → 隐私与安全性 → 定位服务」,关闭总开关。等待 2 秒。" = "打开「设置 → 隐私与安全性 → 定位服务」,关闭总开关。等待 2 秒。";
|
||||||
|
"确认坐标已清除" = "确认坐标已清除";
|
||||||
|
"App 已通知第三方代理清除虚拟坐标。保持网络可用并等待 2 秒,让系统重新获取真实定位。" = "App 已通知第三方代理清除虚拟坐标。保持网络可用并等待 2 秒,让系统重新获取真实定位。";
|
||||||
|
"打开 Wi‑Fi,移除代理" = "打开 Wi‑Fi,移除代理";
|
||||||
|
"从控制中心打开 Wi‑Fi。然后进入「设置 → 无线局域网 → 点 WiFi 右侧 (i) → HTTP 代理」,选择「关闭」后存储。等待 2 秒。" = "从控制中心打开 Wi‑Fi。然后进入「设置 → 无线局域网 → 点 WiFi 右侧 (i) → HTTP 代理」,选择「关闭」后存储。等待 2 秒。";
|
||||||
|
"再次进入「设置 → 隐私与安全性 → 定位服务」打开总开关。打开地图验证定位是否恢复。" = "再次进入「设置 → 隐私与安全性 → 定位服务」打开总开关。打开地图验证定位是否恢复。";
|
||||||
|
"还是无法取消?" = "还是无法取消?";
|
||||||
|
"操作到第 3 步时关机重启,开机后从第 4 步继续。" = "操作到第 3 步时关机重启,开机后从第 4 步继续。";
|
||||||
|
"移除代理配置" = "移除代理配置";
|
||||||
|
"停止虚拟定位后,需要手动移除 WiFi 代理配置,否则可能无法上网。\n\n1. 打开「设置 → 无线局域网」\n2. 点击当前 WiFi 右侧 (i) 图标\n3. 找到「HTTP 代理」\n4. 选择「关闭」\n5. 点右上角「存储」" = "停止虚拟定位后,需要手动移除 WiFi 代理配置,否则可能无法上网。\n\n1. 打开「设置 → 无线局域网」\n2. 点击当前 WiFi 右侧 (i) 图标\n3. 找到「HTTP 代理」\n4. 选择「关闭」\n5. 点右上角「存储」";
|
||||||
|
"社区分享成功配置?" = "社区分享成功配置?";
|
||||||
|
"去提交" = "去提交";
|
||||||
|
"复制模板" = "复制模板";
|
||||||
|
"不再提示" = "不再提示";
|
||||||
|
"第三方客户端" = "第三方客户端";
|
||||||
|
"你正在使用 %@。点击“去提交”会先复制投稿模板,并在 App 内打开社区页面。采纳后将收录到 README,可选择是否匿名署名。" = "你正在使用 %@。点击“去提交”会先复制投稿模板,并在 App 内打开社区页面。采纳后将收录到 README,可选择是否匿名署名。";
|
||||||
|
"已复制投稿模板" = "已复制投稿模板";
|
||||||
|
"如果 GitHub 登录或浏览器跳转后模板没有自动填充,可以直接粘贴。" = "如果 GitHub 登录或浏览器跳转后模板没有自动填充,可以直接粘贴。";
|
||||||
|
"无法直接跳转" = "无法直接跳转";
|
||||||
|
"定位失败" = "定位失败";
|
||||||
|
"打开设置" = "打开设置";
|
||||||
|
"无法获取当前定位,请检查定位服务是否已开启" = "无法获取当前定位,请检查定位服务是否已开启";
|
||||||
|
"编辑收藏名称" = "编辑收藏名称";
|
||||||
|
"名称" = "名称";
|
||||||
|
"保存" = "保存";
|
||||||
|
"修改收藏地点名称" = "修改收藏地点名称";
|
||||||
|
"搜索地点或坐标" = "搜索地点或坐标";
|
||||||
|
"日志" = "日志";
|
||||||
|
"更多" = "更多";
|
||||||
|
"当前选点" = "当前选点";
|
||||||
|
"GCJ-02(国内)" = "GCJ-02(国内)";
|
||||||
|
"WGS-84(国际)" = "WGS-84(国际)";
|
||||||
|
"无法生效?" = "无法生效?";
|
||||||
|
"无法取消?" = "无法取消?";
|
||||||
|
"已收藏,点击取消收藏" = "已收藏,点击取消收藏";
|
||||||
|
"收藏当前选点" = "收藏当前选点";
|
||||||
|
"搜索或点击地图选点后,保存为收藏。" = "搜索或点击地图选点后,保存为收藏。";
|
||||||
|
"切换到此处" = "切换到此处";
|
||||||
|
"同步到第三方代理" = "同步到第三方代理";
|
||||||
|
"检测并同步中…" = "检测并同步中…";
|
||||||
|
"停止第三方虚拟定位" = "停止第三方虚拟定位";
|
||||||
|
"开始虚拟定位" = "开始虚拟定位";
|
||||||
|
"验证环境中…" = "验证环境中…";
|
||||||
|
"停止虚拟定位" = "停止虚拟定位";
|
||||||
|
"当前未连接可用的 Wi-Fi,请连接 Wi-Fi 后配置 127.0.0.1:8888 手动代理。" = "当前未连接可用的 Wi-Fi,请连接 Wi-Fi 后配置 127.0.0.1:8888 手动代理。";
|
||||||
|
"第三方代理查询失败" = "第三方代理查询失败";
|
||||||
|
"未命名" = "未命名";
|
||||||
|
"没有找到相关地点" = "没有找到相关地点";
|
||||||
|
"虚拟定位已开启" = "虚拟定位已开启";
|
||||||
|
"不再提醒" = "不再提醒";
|
||||||
|
"虚拟定位已关闭" = "虚拟定位已关闭";
|
||||||
|
"选择模式" = "选择模式";
|
||||||
|
"配置 Wi-Fi 代理" = "配置 Wi-Fi 代理";
|
||||||
|
"初始化 CA 证书" = "初始化 CA 证书";
|
||||||
|
"选择客户端" = "选择客户端";
|
||||||
|
"导入并检测" = "导入并检测";
|
||||||
|
"上一步" = "上一步";
|
||||||
|
"开始使用" = "开始使用";
|
||||||
|
"操作失败" = "操作失败";
|
||||||
|
"选择运行模式" = "选择运行模式";
|
||||||
|
"后续可在“设置 → 运行模式”中切换。两种模式不要同时拦截 WLOC 请求。" = "后续可在“设置 → 运行模式”中切换。两种模式不要同时拦截 WLOC 请求。";
|
||||||
|
"仅 Wi-Fi" = "仅 Wi-Fi";
|
||||||
|
"无外部依赖" = "无外部依赖";
|
||||||
|
"App 在设备本地启动代理,通过当前 Wi-Fi 的手动 HTTP 代理改写定位响应。免费自签应用无法使用系统 VPN 的 Network Extension 能力,因此 APP模式不支持蜂窝网络,需要配置 Wi-Fi 代理并安装 App 生成的 CA。" = "App 在设备本地启动代理,通过当前 Wi-Fi 的手动 HTTP 代理改写定位响应。免费自签应用无法使用系统 VPN 的 Network Extension 能力,因此 APP模式不支持蜂窝网络,需要配置 Wi-Fi 代理并安装 App 生成的 CA。";
|
||||||
|
"测试模式" = "测试模式";
|
||||||
|
"App 负责选点,并通过 WLOC 配置接口查询和同步坐标;第三方代理客户端负责网络代理、模块拦截、MITM 和持久化。证书、VPN 与代理连接均由第三方客户端处理。" = "App 负责选点,并通过 WLOC 配置接口查询和同步坐标;第三方代理客户端负责网络代理、模块拦截、MITM 和持久化。证书、VPN 与代理连接均由第三方客户端处理。";
|
||||||
|
"正在准备 APP模式本地服务…" = "正在准备 APP模式本地服务…";
|
||||||
|
"先配置 Wi-Fi 系统代理" = "先配置 Wi-Fi 系统代理";
|
||||||
|
"在当前 Wi-Fi 的详情页,将「HTTP 代理」设为「手动」:服务器填 127.0.0.1,端口填 8888。配置后点击下方「完成」。检测会自动判断是 Wi-Fi 代理还是证书信任有问题。" = "在当前 Wi-Fi 的详情页,将「HTTP 代理」设为「手动」:服务器填 127.0.0.1,端口填 8888。配置后点击下方「完成」。检测会自动判断是 Wi-Fi 代理还是证书信任有问题。";
|
||||||
|
"Wi-Fi 代理设置" = "Wi-Fi 代理设置";
|
||||||
|
"1 选择手动,2 填写服务器 127.0.0.1,3 填写端口 8888。" = "1 选择手动,2 填写服务器 127.0.0.1,3 填写端口 8888。";
|
||||||
|
"复制地址" = "复制地址";
|
||||||
|
"打开 Wi-Fi 设置" = "打开 Wi-Fi 设置";
|
||||||
|
"第 1 步:下载证书" = "第 1 步:下载证书";
|
||||||
|
"下载本机随机生成的 CA 根证书。私钥仅保存在此设备的钥匙串中,不会随证书文件导出。App 会弹出 Safari 下载页;出现配置描述文件下载提示时,选择「允许」。" = "下载本机随机生成的 CA 根证书。私钥仅保存在此设备的钥匙串中,不会随证书文件导出。App 会弹出 Safari 下载页;出现配置描述文件下载提示时,选择「允许」。";
|
||||||
|
"打开下载页" = "打开下载页";
|
||||||
|
"无法准备证书下载页面,请查看诊断日志" = "无法准备证书下载页面,请查看诊断日志";
|
||||||
|
"第 2 步:安装证书" = "第 2 步:安装证书";
|
||||||
|
"下载完成后打开系统「设置」。如果顶部显示「已下载描述文件」,点进去安装;否则进入「通用 → VPN 与设备管理」,找到 Location Spoofer CA 并完成安装。" = "下载完成后打开系统「设置」。如果顶部显示「已下载描述文件」,点进去安装;否则进入「通用 → VPN 与设备管理」,找到 Location Spoofer CA 并完成安装。";
|
||||||
|
"去安装" = "去安装";
|
||||||
|
"安装证书" = "安装证书";
|
||||||
|
"1 在「VPN 与设备管理」中打开 Location Spoofer CA 描述文件并完成安装。" = "1 在「VPN 与设备管理」中打开 Location Spoofer CA 描述文件并完成安装。";
|
||||||
|
"第 3 步:信任证书" = "第 3 步:信任证书";
|
||||||
|
"安装后进入「设置 → 通用 → 关于本机 → 证书信任设置」,找到 Location Spoofer CA 并开启完全信任。iOS 保留钥匙串数据时,重装 App 会继续复用同一证书。" = "安装后进入「设置 → 通用 → 关于本机 → 证书信任设置」,找到 Location Spoofer CA 并开启完全信任。iOS 保留钥匙串数据时,重装 App 会继续复用同一证书。";
|
||||||
|
"去信任" = "去信任";
|
||||||
|
"信任证书" = "信任证书";
|
||||||
|
"1 在「证书信任设置」中为 Location Spoofer CA 开启完全信任。" = "1 在「证书信任设置」中为 Location Spoofer CA 开启完全信任。";
|
||||||
|
"已完成" = "已完成";
|
||||||
|
"已完成 ✓" = "已完成 ✓";
|
||||||
|
"iOS 27 beta 6 起,系统已禁止对 gs-loc.apple.com 进行 MITM 拦截。该版本及之后的 beta 版本暂时无法使用本项目,等待后续适配方案。" = "iOS 27 beta 6 起,系统已禁止对 gs-loc.apple.com 进行 MITM 拦截。该版本及之后的 beta 版本暂时无法使用本项目,等待后续适配方案。";
|
||||||
|
"选择第三方代理客户端" = "选择第三方代理客户端";
|
||||||
|
"除 Shadowrocket 外,当前客户端配置尚未完成真机验证,页面只提供模块导入入口和通用配置提醒。" = "除 Shadowrocket 外,当前客户端配置尚未完成真机验证,页面只提供模块导入入口和通用配置提醒。";
|
||||||
|
"第三方客户端适配说明" = "第三方客户端适配说明";
|
||||||
|
"App 不连接远程坐标服务器,而是向 Apple 域名发起一个约定请求。第三方客户端需要在本机拦截该请求、保存 WGS-84 坐标并返回 JSON;定位模块再读取同一份数据,修改 Apple WLOC 响应。" = "App 不连接远程坐标服务器,而是向 Apple 域名发起一个约定请求。第三方客户端需要在本机拦截该请求、保存 WGS-84 坐标并返回 JSON;定位模块再读取同一份数据,修改 Apple WLOC 响应。";
|
||||||
|
"配置接口" = "配置接口";
|
||||||
|
"查询:GET ?action=query\n保存:GET ?lon=<经度>&lat=<纬度>&acc=<精度>\n清除:GET ?action=clear" = "查询:GET ?action=query\n保存:GET ?lon=<经度>&lat=<纬度>&acc=<精度>\n清除:GET ?action=clear";
|
||||||
|
"返回格式" = "返回格式";
|
||||||
|
"适配要求" = "适配要求";
|
||||||
|
"客户端需要支持请求脚本、持久化存储、HTTP 200 JSON 响应、Apple WLOC 响应脚本,以及 Apple 定位域名(gs-loc.apple.com、gsp-ssl.ls.apple.com、bluedot.is.autonavi.com 等)的 HTTPS 解密。保存接口和 WLOC 响应脚本必须读取同一份持久化数据。" = "客户端需要支持请求脚本、持久化存储、HTTP 200 JSON 响应、Apple WLOC 响应脚本,以及 Apple 定位域名(gs-loc.apple.com、gsp-ssl.ls.apple.com、bluedot.is.autonavi.com 等)的 HTTPS 解密。保存接口和 WLOC 响应脚本必须读取同一份持久化数据。";
|
||||||
|
"检测到第三方代理连接异常,请检查模块、MITM 和代理连接后重新检测。" = "检测到第三方代理连接异常,请检查模块、MITM 和代理连接后重新检测。";
|
||||||
|
"第 1 步:导入 %@ 模块" = "第 1 步:导入 %@ 模块";
|
||||||
|
"复制 %@ 的模块订阅地址。" = "复制 %@ 的模块订阅地址。";
|
||||||
|
"打开 Shadowrocket,进入“配置 → 模块”。" = "打开 Shadowrocket,进入“配置 → 模块”。";
|
||||||
|
"打开 %@。" = "打开 %@。";
|
||||||
|
"进入 Shadowrocket 配置" = "进入 Shadowrocket 配置";
|
||||||
|
"1 点击「模块」进入模块列表,2 可打开当前本地配置详情。" = "1 点击「模块」进入模块列表,2 可打开当前本地配置详情。";
|
||||||
|
"点击右上角“+”,粘贴模块订阅地址并导入,然后确认模块已启用。" = "点击右上角“+”,粘贴模块订阅地址并导入,然后确认模块已启用。";
|
||||||
|
"导入 Shadowrocket 模块" = "导入 Shadowrocket 模块";
|
||||||
|
"1 点击右上角加号导入模块,2 确认模块已启用。" = "1 点击右上角加号导入模块,2 确认模块已启用。";
|
||||||
|
"在 %@ 中导入刚才复制的模块订阅地址。" = "在 %@ 中导入刚才复制的模块订阅地址。";
|
||||||
|
"第 2 步:完成 %@ 配置" = "第 2 步:完成 %@ 配置";
|
||||||
|
"请在 %@ 中完成相应配置。" = "请在 %@ 中完成相应配置。";
|
||||||
|
"配置时请复制下方全部解密域名(含 gsp-ssl.ls.apple.com、bluedot.is.autonavi.com)。" = "配置时请复制下方全部解密域名(含 gsp-ssl.ls.apple.com、bluedot.is.autonavi.com)。";
|
||||||
|
"第 2 步:配置 HTTPS 解密" = "第 2 步:配置 HTTPS 解密";
|
||||||
|
"进入“配置 → 本地文件”,找到带黄点的配置,点击右侧 i 图标。" = "进入“配置 → 本地文件”,找到带黄点的配置,点击右侧 i 图标。";
|
||||||
|
"进入“HTTPS 解密”,开启解密开关。" = "进入“HTTPS 解密”,开启解密开关。";
|
||||||
|
"在域名列表中添加下方复制的全部解密域名。" = "在域名列表中添加下方复制的全部解密域名。";
|
||||||
|
"配置 HTTPS 解密" = "配置 HTTPS 解密";
|
||||||
|
"1 开启 HTTPS 解密,2 添加复制的全部解密域名,3 打开证书设置。" = "1 开启 HTTPS 解密,2 添加复制的全部解密域名,3 打开证书设置。";
|
||||||
|
"按 Shadowrocket 提示生成并完成证书授权。" = "按 Shadowrocket 提示生成并完成证书授权。";
|
||||||
|
"授权 Shadowrocket 证书" = "授权 Shadowrocket 证书";
|
||||||
|
"1 打开 Shadowrocket 证书项并按提示安装、授权。" = "1 打开 Shadowrocket 证书项并按提示安装、授权。";
|
||||||
|
"返回 HTTPS 解密页面,点击右上角勾号保存,然后开启代理。" = "返回 HTTPS 解密页面,点击右上角勾号保存,然后开启代理。";
|
||||||
|
"打开 Shadowrocket 继续配置" = "打开 Shadowrocket 继续配置";
|
||||||
|
"App 只能唤起 Shadowrocket,无法通过公开接口直接跳转到“模块”或“HTTPS 解密”页面。" = "App 只能唤起 Shadowrocket,无法通过公开接口直接跳转到“模块”或“HTTPS 解密”页面。";
|
||||||
|
"轻点查看大图" = "轻点查看大图";
|
||||||
|
"证书尚未安装或信任" = "证书尚未安装或信任";
|
||||||
|
"Wi-Fi 代理未正确设置" = "Wi-Fi 代理未正确设置";
|
||||||
|
"本地代理未能启动" = "本地代理未能启动";
|
||||||
|
"检测仍在进行" = "检测仍在进行";
|
||||||
|
"检测结果已过期" = "检测结果已过期";
|
||||||
|
"定位改写检测失败" = "定位改写检测失败";
|
||||||
|
"接口连接失败" = "接口连接失败";
|
||||||
|
"本地代理初始化失败:%@" = "本地代理初始化失败:%@";
|
||||||
|
"✓ 定位环境正常" = "✓ 定位环境正常";
|
||||||
|
"CA 证书未安装或未信任" = "CA 证书未安装或未信任";
|
||||||
|
"Wi-Fi 代理未正确设置,请检查 127.0.0.1:8888" = "Wi-Fi 代理未正确设置,请检查 127.0.0.1:8888";
|
||||||
|
"请手动打开「设置」,找到本 App 后检查定位权限。" = "请手动打开「设置」,找到本 App 后检查定位权限。";
|
||||||
|
"请手动打开「设置 → 通用」。" = "请手动打开「设置 → 通用」。";
|
||||||
|
"请手动打开「设置 → 无线局域网」,进入当前 Wi-Fi 的详情页。" = "请手动打开「设置 → 无线局域网」,进入当前 Wi-Fi 的详情页。";
|
||||||
|
"请手动打开「设置 → 隐私与安全性 → 定位服务」。" = "请手动打开「设置 → 隐私与安全性 → 定位服务」。";
|
||||||
|
"第三方代理返回了无法识别的数据" = "第三方代理返回了无法识别的数据";
|
||||||
|
"请求未被第三方代理模块拦截,请检查模块、MITM 和代理连接" = "请求未被第三方代理模块拦截,请检查模块、MITM 和代理连接";
|
||||||
|
"第三方代理保存的坐标与当前选点不一致" = "第三方代理保存的坐标与当前选点不一致";
|
||||||
|
"第三方代理请求失败:%@" = "第三方代理请求失败:%@";
|
||||||
|
"检查模块、MITM、证书和代理/VPN连接" = "检查模块、MITM、证书和代理/VPN连接";
|
||||||
|
"第三方代理拒绝保存坐标" = "第三方代理拒绝保存坐标";
|
||||||
|
"第三方代理清除坐标失败" = "第三方代理清除坐标失败";
|
||||||
|
"已有第三方代理请求正在执行" = "已有第三方代理请求正在执行";
|
||||||
|
"配置已提供,尚未验证" = "配置已提供,尚未验证";
|
||||||
|
"已连接,有保存坐标" = "已连接,有保存坐标";
|
||||||
|
"已连接,无保存坐标" = "已连接,无保存坐标";
|
||||||
|
"连接失败(%@)" = "连接失败(%@)";
|
||||||
|
"当前客户端:%@" = "当前客户端:%@";
|
||||||
|
"配置接口:/wloc-settings/save" = "配置接口:/wloc-settings/save";
|
||||||
|
"请求动作:WLOC query" = "请求动作:WLOC query";
|
||||||
|
"检查范围:模块拦截、MITM、证书、代理/VPN 连接" = "检查范围:模块拦截、MITM、证书、代理/VPN 连接";
|
||||||
|
"连接状态:%@" = "连接状态:%@";
|
||||||
|
"检测结果:失败" = "检测结果:失败";
|
||||||
|
"耗时:%lld ms" = "耗时:%lld ms";
|
||||||
|
"错误类型:%@" = "错误类型:%@";
|
||||||
|
"错误详情:%@" = "错误详情:%@";
|
||||||
|
"处理建议:%@。" = "处理建议:%@。";
|
||||||
|
"======== 第三方代理运行检测 ========" = "======== 第三方代理运行检测 ========";
|
||||||
|
"触发来源:地图或设置中的第三方代理操作" = "触发来源:地图或设置中的第三方代理操作";
|
||||||
|
"请求动作:WLOC 配置接口" = "请求动作:WLOC 配置接口";
|
||||||
|
"处理建议:确认模块已启用,并检查 MITM、证书和代理/VPN 连接。" = "处理建议:确认模块已启用,并检查 MITM、证书和代理/VPN 连接。";
|
||||||
|
"环境信息" = "环境信息";
|
||||||
|
"App 版本" = "App 版本";
|
||||||
|
"系统版本" = "系统版本";
|
||||||
|
"诊断日志" = "诊断日志";
|
||||||
|
"不适用" = "不适用";
|
||||||
|
"是" = "是";
|
||||||
|
"否" = "否";
|
||||||
|
"(无诊断数据)" = "(无诊断数据)";
|
||||||
|
"第三方代理测试模式:模块连接成功;已保存坐标=%@" = "第三方代理测试模式:模块连接成功;已保存坐标=%@";
|
||||||
|
"第三方代理测试模式:模块连接失败;%@" = "第三方代理测试模式:模块连接失败;%@";
|
||||||
|
"======== 代理验证测试 ========" = "======== 代理验证测试 ========";
|
||||||
|
"App 版本: %@" = "App 版本: %@";
|
||||||
|
"系统版本: iOS %@" = "系统版本: iOS %@";
|
||||||
|
"[步骤 A] 检查代理是否运行…" = "[步骤 A] 检查代理是否运行…";
|
||||||
|
" 端口: 127.0.0.1:8888" = " 端口: 127.0.0.1:8888";
|
||||||
|
" ⚠ 代理未运行,尝试启动…" = " ⚠ 代理未运行,尝试启动…";
|
||||||
|
" ✗ 启动失败: %@" = " ✗ 启动失败: %@";
|
||||||
|
" ✓ 代理启动成功" = " ✓ 代理启动成功";
|
||||||
|
" ✓ 代理已在运行中" = " ✓ 代理已在运行中";
|
||||||
|
"[步骤 B] 检测证书与 WiFi 代理…" = "[步骤 B] 检测证书与 WiFi 代理…";
|
||||||
|
" 方式: 请求 baidu.com/paopao-verify-<token>" = " 方式: 请求 baidu.com/paopao-verify-<token>";
|
||||||
|
" 结果判定: TLS 错误=证书问题 / 响应不匹配=代理未配置 / 匹配=通过" = " 结果判定: TLS 错误=证书问题 / 响应不匹配=代理未配置 / 匹配=通过";
|
||||||
|
" ✗ 无法生成验证 token" = " ✗ 无法生成验证 token";
|
||||||
|
" ✓ 证书已信任,WiFi 代理已配置" = " ✓ 证书已信任,WiFi 代理已配置";
|
||||||
|
" ✗ 响应不匹配: HTTP %lld, %lld bytes,WiFi 代理未配置" = " ✗ 响应不匹配: HTTP %lld, %lld bytes,WiFi 代理未配置";
|
||||||
|
" ✗ 请求失败 [%@ code=%lld]: %@" = " ✗ 请求失败 [%@ code=%lld]: %@";
|
||||||
|
" TLS/证书校验失败,CA 证书未信任" = " TLS/证书校验失败,CA 证书未信任";
|
||||||
|
"======== 环境检测通过 ✓ ========" = "======== 环境检测通过 ✓ ========";
|
||||||
|
" --- 代理日志 ---" = " --- 代理日志 ---";
|
||||||
|
"成功:{\"success\":true,\"longitude\":113.0,\"latitude\":22.0,\"accuracy\":25}\n失败:{\"success\":false,\"error\":\"错误说明\"}" = "成功:{\"success\":true,\"longitude\":113.0,\"latitude\":22.0,\"accuracy\":25}\n失败:{\"success\":false,\"error\":\"错误说明\"}";
|
||||||
|
"定位改写脚本源自 Yu9191/wloc,现由本项目维护" = "定位改写脚本源自 Yu9191/wloc,现由本项目维护";
|
||||||
|
"地图" = "地图";
|
||||||
|
"坐标转换" = "坐标转换";
|
||||||
|
"定位" = "定位";
|
||||||
|
"实时定位" = "实时定位";
|
||||||
|
"提醒" = "提醒";
|
||||||
|
"搜索" = "搜索";
|
||||||
|
"收藏" = "收藏";
|
||||||
|
"缩放" = "缩放";
|
||||||
|
"========== App 启动 ==========" = "========== App 启动 ==========";
|
||||||
|
"尚未选择运行模式,跳过本地 CA 和代理初始化" = "尚未选择运行模式,跳过本地 CA 和代理初始化";
|
||||||
|
"第三方代理测试模式:跳过本地 CA、代理和环境检测" = "第三方代理测试模式:跳过本地 CA、代理和环境检测";
|
||||||
|
"旧坐标数据迁移失败,将在下次启动重试" = "旧坐标数据迁移失败,将在下次启动重试";
|
||||||
|
"地图坐标标准初始化完成,开始后续启动流程" = "地图坐标标准初始化完成,开始后续启动流程";
|
||||||
|
"没有持久化图钉,地图创建前请求实时定位" = "没有持久化图钉,地图创建前请求实时定位";
|
||||||
|
"已使用实时定位准备唯一初始地图状态" = "已使用实时定位准备唯一初始地图状态";
|
||||||
|
"地图创建前取得的初始实时位置(WGS-84)" = "地图创建前取得的初始实时位置(WGS-84)";
|
||||||
|
"地图创建前无法取得实时定位,唯一初始位置使用深圳" = "地图创建前无法取得实时定位,唯一初始位置使用深圳";
|
||||||
|
"已找到持久化图钉,直接准备唯一初始地图状态" = "已找到持久化图钉,直接准备唯一初始地图状态";
|
||||||
|
"启动门禁全部完成,现在创建 MapHomeView" = "启动门禁全部完成,现在创建 MapHomeView";
|
||||||
|
"初始化" = "初始化";
|
||||||
|
"启动代理 127.0.0.1:8888" = "启动代理 127.0.0.1:8888";
|
||||||
|
"启动代理: 恢复上次 WGS-84 定位" = "启动代理: 恢复上次 WGS-84 定位";
|
||||||
|
"启动成功" = "启动成功";
|
||||||
|
"启动失败" = "启动失败";
|
||||||
|
"写入坐标" = "写入坐标";
|
||||||
|
"跳过过期坐标写入" = "跳过过期坐标写入";
|
||||||
|
"跳过旧验证坐标恢复" = "跳过旧验证坐标恢复";
|
||||||
|
"运动状态模拟设置已更新" = "运动状态模拟设置已更新";
|
||||||
|
"准备证书下载失败" = "准备证书下载失败";
|
||||||
|
"设置虚拟定位坐标" = "设置虚拟定位坐标";
|
||||||
|
"apply失败" = "apply失败";
|
||||||
|
"保存收藏失败" = "保存收藏失败";
|
||||||
|
"本地服务初始化失败" = "本地服务初始化失败";
|
||||||
|
"代理运行模式已切换" = "代理运行模式已切换";
|
||||||
|
"已迁移旧版模式初始化状态" = "已迁移旧版模式初始化状态";
|
||||||
|
"地图坐标标准检测已有请求进行中" = "地图坐标标准检测已有请求进行中";
|
||||||
|
"地图坐标标准检测开始" = "地图坐标标准检测开始";
|
||||||
|
"地图坐标标准检测获得明确结果" = "地图坐标标准检测获得明确结果";
|
||||||
|
"地图坐标标准检测不可用,开始实时定位兜底" = "地图坐标标准检测不可用,开始实时定位兜底";
|
||||||
|
"地图坐标标准检测使用兜底结果" = "地图坐标标准检测使用兜底结果";
|
||||||
|
"地图坐标标准检测被取消,保留默认国内标准" = "地图坐标标准检测被取消,保留默认国内标准";
|
||||||
|
"地图坐标标准已确定,允许创建地图" = "地图坐标标准已确定,允许创建地图";
|
||||||
|
"地图坐标标准运行期检测合并到进行中请求" = "地图坐标标准运行期检测合并到进行中请求";
|
||||||
|
"地图坐标标准运行期检测开始" = "地图坐标标准运行期检测开始";
|
||||||
|
"地图坐标标准运行期检测结果已过期,取消写入" = "地图坐标标准运行期检测结果已过期,取消写入";
|
||||||
|
"地图坐标标准运行期检测完成,标准未变化" = "地图坐标标准运行期检测完成,标准未变化";
|
||||||
|
"地图坐标标准运行期检测发现切换" = "地图坐标标准运行期检测发现切换";
|
||||||
|
"地图坐标标准运行期检测失败,保留当前标准" = "地图坐标标准运行期检测失败,保留当前标准";
|
||||||
|
"地图坐标标准运行期检测已取消" = "地图坐标标准运行期检测已取消";
|
||||||
|
"实时定位不覆盖固定锚点的明确检测结果" = "实时定位不覆盖固定锚点的明确检测结果";
|
||||||
|
"实时定位确认兜底地图坐标标准无需修正" = "实时定位确认兜底地图坐标标准无需修正";
|
||||||
|
"实时定位修正启动兜底地图坐标标准" = "实时定位修正启动兜底地图坐标标准";
|
||||||
|
"第三方代理已保存 WGS-84 坐标" = "第三方代理已保存 WGS-84 坐标";
|
||||||
|
"第三方代理坐标已清除" = "第三方代理坐标已清除";
|
||||||
|
"第三方代理请求失败" = "第三方代理请求失败";
|
||||||
|
"第三方代理坐标同步成功" = "第三方代理坐标同步成功";
|
||||||
|
"同步坐标到第三方客户端失败" = "同步坐标到第三方客户端失败";
|
||||||
|
"验证结果" = "验证结果";
|
||||||
|
"验证失败" = "验证失败";
|
||||||
|
"开启前检测失败,进入对应环境引导" = "开启前检测失败,进入对应环境引导";
|
||||||
|
"清除第三方客户端坐标失败" = "清除第三方客户端坐标失败";
|
||||||
|
"累计虚拟定位操作次数" = "累计虚拟定位操作次数";
|
||||||
|
"已复制坐标" = "已复制坐标";
|
||||||
|
"地图坐标标准切换时未找到当前选点缓存" = "地图坐标标准切换时未找到当前选点缓存";
|
||||||
|
"地图坐标标准切换后已使用缓存坐标对回显当前选点" = "地图坐标标准切换后已使用缓存坐标对回显当前选点";
|
||||||
|
"地图坐标类型已变化" = "地图坐标类型已变化";
|
||||||
|
"取消保存收藏:检测期间当前选点已变化" = "取消保存收藏:检测期间当前选点已变化";
|
||||||
|
"保存当前选点为收藏" = "保存当前选点为收藏";
|
||||||
|
"第三方代理查询返回失败" = "第三方代理查询返回失败";
|
||||||
|
"启动后第三方代理状态查询失败" = "启动后第三方代理状态查询失败";
|
||||||
|
"检测到 Wi-Fi 网络变化" = "检测到 Wi-Fi 网络变化";
|
||||||
|
"网络仍在变化,重新计算环境检测等待时间" = "网络仍在变化,重新计算环境检测等待时间";
|
||||||
|
"等待 Wi-Fi 连接稳定后检测" = "等待 Wi-Fi 连接稳定后检测";
|
||||||
|
"延时检测已被更新的网络事件取消" = "延时检测已被更新的网络事件取消";
|
||||||
|
"稳定等待结束后仍未连接 Wi-Fi,提示检查代理" = "稳定等待结束后仍未连接 Wi-Fi,提示检查代理";
|
||||||
|
"开始后台环境检测" = "开始后台环境检测";
|
||||||
|
"已有环境检测运行,1 秒后重试" = "已有环境检测运行,1 秒后重试";
|
||||||
|
"后台环境检测完成" = "后台环境检测完成";
|
||||||
|
"环境检测连续被占用,本次不重复弹窗" = "环境检测连续被占用,本次不重复弹窗";
|
||||||
|
"用户点击实时定位" = "用户点击实时定位";
|
||||||
|
"MapKit 蓝点尚不可用,启动 CLLocationManager 兜底" = "MapKit 蓝点尚不可用,启动 CLLocationManager 兜底";
|
||||||
|
"MapKit 蓝点抢先完成请求,取消 CLLocationManager 兜底" = "MapKit 蓝点抢先完成请求,取消 CLLocationManager 兜底";
|
||||||
|
"虚拟定位开启后的 MapKit 蓝点标准判定" = "虚拟定位开启后的 MapKit 蓝点标准判定";
|
||||||
|
"登记实时定位请求上下文" = "登记实时定位请求上下文";
|
||||||
|
"复用进行中的 CLLocationManager 请求并更新意图上下文" = "复用进行中的 CLLocationManager 请求并更新意图上下文";
|
||||||
|
"CLLocationManager 兜底未返回坐标" = "CLLocationManager 兜底未返回坐标";
|
||||||
|
"丢弃 CLLocationManager 结果:任务已取消或蓝点已抢先完成" = "丢弃 CLLocationManager 结果:任务已取消或蓝点已抢先完成";
|
||||||
|
"实时定位坐标完成标准判断并提交到地图" = "实时定位坐标完成标准判断并提交到地图";
|
||||||
|
"MKLocalSearch 返回地点结果" = "MKLocalSearch 返回地点结果";
|
||||||
|
"反向地理编码网络失败,准备重试" = "反向地理编码网络失败,准备重试";
|
||||||
|
"反向地理编码失败" = "反向地理编码失败";
|
||||||
|
"获得搜索结果" = "获得搜索结果";
|
||||||
|
"点击收藏点并回显到地图" = "点击收藏点并回显到地图";
|
||||||
|
"忽略重复 CLLocationManager 请求" = "忽略重复 CLLocationManager 请求";
|
||||||
|
"CLLocationManager 请求入口" = "CLLocationManager 请求入口";
|
||||||
|
"授权状态不允许定位" = "授权状态不允许定位";
|
||||||
|
"创建 CLLocationManager 请求" = "创建 CLLocationManager 请求";
|
||||||
|
"请求前台定位授权" = "请求前台定位授权";
|
||||||
|
"定位授权状态变化" = "定位授权状态变化";
|
||||||
|
"CLLocationManager 返回样本批次" = "CLLocationManager 返回样本批次";
|
||||||
|
"本批次没有可完成当前请求的样本" = "本批次没有可完成当前请求的样本";
|
||||||
|
"接受 CLLocationManager 样本并完成请求" = "接受 CLLocationManager 样本并完成请求";
|
||||||
|
"定位回调失败" = "定位回调失败";
|
||||||
|
"跳过 CLLocationManager 缓存样本" = "跳过 CLLocationManager 缓存样本";
|
||||||
|
"等待定位授权超时" = "等待定位授权超时";
|
||||||
|
"单次定位超时,切换持续定位" = "单次定位超时,切换持续定位";
|
||||||
|
"持续定位超时" = "持续定位超时";
|
||||||
|
"开始 CLLocationManager 单次定位" = "开始 CLLocationManager 单次定位";
|
||||||
|
"开始 CLLocationManager 持续定位兜底" = "开始 CLLocationManager 持续定位兜底";
|
||||||
|
"CLLocationManager 请求完成" = "CLLocationManager 请求完成";
|
||||||
|
"CLLocationManager 请求结束但没有坐标" = "CLLocationManager 请求结束但没有坐标";
|
||||||
|
"使用 CLLocationManager 新鲜缓存" = "使用 CLLocationManager 新鲜缓存";
|
||||||
|
"实时定位按钮直接使用 MapKit 蓝点缓存" = "实时定位按钮直接使用 MapKit 蓝点缓存";
|
||||||
|
"主页收到待处理请求所需的 MapKit 蓝点回调" = "主页收到待处理请求所需的 MapKit 蓝点回调";
|
||||||
|
"主页收到 CLLocationManager 兜底坐标" = "主页收到 CLLocationManager 兜底坐标";
|
||||||
|
"原始实时定位坐标" = "原始实时定位坐标";
|
||||||
|
"检查 CLLocationManager 样本" = "检查 CLLocationManager 样本";
|
||||||
|
"拒绝 CLLocationManager 样本:坐标或精度无效" = "拒绝 CLLocationManager 样本:坐标或精度无效";
|
||||||
|
"MapKit 蓝点更新但 location 为空" = "MapKit 蓝点更新但 location 为空";
|
||||||
|
"拒绝 MapKit 蓝点样本:坐标无效" = "拒绝 MapKit 蓝点样本:坐标无效";
|
||||||
|
"拒绝 MapKit 蓝点样本:水平精度无效" = "拒绝 MapKit 蓝点样本:水平精度无效";
|
||||||
|
"音频中断恢复" = "音频中断恢复";
|
||||||
|
"音频会话失败" = "音频会话失败";
|
||||||
|
"无法创建静音音频缓冲区" = "无法创建静音音频缓冲区";
|
||||||
|
"引擎启动失败" = "引擎启动失败";
|
||||||
|
"后台保活已启动(静音音频)" = "后台保活已启动(静音音频)";
|
||||||
|
"后台保活已停止" = "后台保活已停止";
|
||||||
|
"保存当前图钉失败" = "保存当前图钉失败";
|
||||||
|
"存储缩放" = "存储缩放";
|
||||||
|
"旧坐标数据迁移完成" = "旧坐标数据迁移完成";
|
||||||
|
"远程版本配置加载成功" = "远程版本配置加载成功";
|
||||||
|
"版本配置源不可用,尝试下一地址" = "版本配置源不可用,尝试下一地址";
|
||||||
|
"版本配置加载失败,继续使用内置配置" = "版本配置加载失败,继续使用内置配置";
|
||||||
|
"版本说明源不可用,尝试下一地址" = "版本说明源不可用,尝试下一地址";
|
||||||
|
"版本说明加载失败,将使用最新 Release 页面" = "版本说明加载失败,将使用最新 Release 页面";
|
||||||
|
"复用设备钥匙串中的 CA" = "复用设备钥匙串中的 CA";
|
||||||
|
"旧 CA 已迁移到设备钥匙串" = "旧 CA 已迁移到设备钥匙串";
|
||||||
|
"已生成并保存设备专属 CA" = "已生成并保存设备专属 CA";
|
||||||
|
"已删除设备 CA,等待重新生成" = "已删除设备 CA,等待重新生成";
|
||||||
|
"钥匙串中的 CA 无效,准备回退" = "钥匙串中的 CA 无效,准备回退";
|
||||||
|
"删除旧 CA 文件失败,将在下次启动重试" = "删除旧 CA 文件失败,将在下次启动重试";
|
||||||
|
"无法解析 CA 证书 PEM" = "无法解析 CA 证书 PEM";
|
||||||
|
"无法创建 SecTrust" = "无法创建 SecTrust";
|
||||||
|
"SecTrust 评估返回错误" = "SecTrust 评估返回错误";
|
||||||
|
"CA 证书已被系统信任" = "CA 证书已被系统信任";
|
||||||
|
"CA 证书未被系统信任" = "CA 证书未被系统信任";
|
||||||
|
"调用 Go Core 生成 CA" = "调用 Go Core 生成 CA";
|
||||||
|
"Go Core CA 生成成功" = "Go Core CA 生成成功";
|
||||||
|
"本地证书服务已在运行" = "本地证书服务已在运行";
|
||||||
|
"调用 Go Core 启动本地证书服务" = "调用 Go Core 启动本地证书服务";
|
||||||
|
"本地证书服务启动成功" = "本地证书服务启动成功";
|
||||||
|
"停止本地证书服务" = "停止本地证书服务";
|
||||||
|
"开始第三方代理连接检测" = "开始第三方代理连接检测";
|
||||||
|
"第三方代理连接检测通过" = "第三方代理连接检测通过";
|
||||||
|
"第三方代理连接检测失败" = "第三方代理连接检测失败";
|
||||||
|
"关闭代理前已同步关闭虚拟定位" = "关闭代理前已同步关闭虚拟定位";
|
||||||
|
"切换 APP 模式前无法清除第三方坐标" = "切换 APP 模式前无法清除第三方坐标";
|
||||||
|
"设置页第三方连接检测通过" = "设置页第三方连接检测通过";
|
||||||
|
"设置页第三方连接检测失败" = "设置页第三方连接检测失败";
|
||||||
|
"地图实际显示坐标" = "地图实际显示坐标";
|
||||||
|
"App回到前台" = "App回到前台";
|
||||||
|
"App已确认地图标准" = "App已确认地图标准";
|
||||||
|
"CLLocationManager请求中" = "CLLocationManager请求中";
|
||||||
|
"Go proxy 启动失败" = "Go proxy 启动失败";
|
||||||
|
"MapKit地图蓝点" = "MapKit地图蓝点";
|
||||||
|
"MapKit蓝点新样本" = "MapKit蓝点新样本";
|
||||||
|
"MapKit蓝点缓存" = "MapKit蓝点缓存";
|
||||||
|
"SSID可读取" = "SSID可读取";
|
||||||
|
"WLOC写入标准" = "WLOC写入标准";
|
||||||
|
"WLOC目标标准" = "WLOC目标标准";
|
||||||
|
"Wi-Fi接口" = "Wi-Fi接口";
|
||||||
|
"intent选点revision" = "intent选点revision";
|
||||||
|
"manager请求中" = "manager请求中";
|
||||||
|
"上下文存在" = "上下文存在";
|
||||||
|
"事件原因" = "事件原因";
|
||||||
|
"任务已取消" = "任务已取消";
|
||||||
|
"任务已存在" = "任务已存在";
|
||||||
|
"使用兜底" = "使用兜底";
|
||||||
|
"保存收藏" = "保存收藏";
|
||||||
|
"保存数据包含" = "保存数据包含";
|
||||||
|
"保持未启用" = "保持未启用";
|
||||||
|
"保留原第三方坐标" = "保留原第三方坐标";
|
||||||
|
"保留已启用状态" = "保留已启用状态";
|
||||||
|
"保留标准" = "保留标准";
|
||||||
|
"修正兜底标准" = "修正兜底标准";
|
||||||
|
"内存缓存存在" = "内存缓存存在";
|
||||||
|
"初始坐标来源" = "初始坐标来源";
|
||||||
|
"初始来源" = "初始来源";
|
||||||
|
"初始阶段" = "初始阶段";
|
||||||
|
"判定规则" = "判定规则";
|
||||||
|
"原兜底来源" = "原兜底来源";
|
||||||
|
"原因" = "原因";
|
||||||
|
"取值字段" = "取值字段";
|
||||||
|
"可显示不再提醒" = "可显示不再提醒";
|
||||||
|
"启动代理…" = "启动代理…";
|
||||||
|
"命中林士街" = "命中林士街";
|
||||||
|
"固定锚点" = "固定锚点";
|
||||||
|
"固定锚点查询超过5秒" = "固定锚点查询超过5秒";
|
||||||
|
"固定锚点查询返回空结果" = "固定锚点查询返回空结果";
|
||||||
|
"国内标准(GCJ-02)" = "国内标准(GCJ-02)";
|
||||||
|
"国内转换区域" = "国内转换区域";
|
||||||
|
"国外非转换区域" = "国外非转换区域";
|
||||||
|
"国际标准(WGS-84)" = "国际标准(WGS-84)";
|
||||||
|
"国际标准(WGS-84)+国内标准(GCJ-02)" = "国际标准(WGS-84)+国内标准(GCJ-02)";
|
||||||
|
"图钉已按新类型重设" = "图钉已按新类型重设";
|
||||||
|
"地图取值字段" = "地图取值字段";
|
||||||
|
"地图标准" = "地图标准";
|
||||||
|
"坐标有效" = "坐标有效";
|
||||||
|
"坐标标准" = "坐标标准";
|
||||||
|
"垂直精度米" = "垂直精度米";
|
||||||
|
"基础校验通过" = "基础校验通过";
|
||||||
|
"处理建议" = "处理建议";
|
||||||
|
"失败时提示" = "失败时提示";
|
||||||
|
"实时定位存在" = "实时定位存在";
|
||||||
|
"实时定位服务区域" = "实时定位服务区域";
|
||||||
|
"客户端模式" = "客户端模式";
|
||||||
|
"尝试" = "尝试";
|
||||||
|
"已取消位置更新" = "已取消位置更新";
|
||||||
|
"已恢复真实定位" = "已恢复真实定位";
|
||||||
|
"已清理" = "已清理";
|
||||||
|
"已重置" = "已重置";
|
||||||
|
"开启" = "开启";
|
||||||
|
"异步地理编码" = "异步地理编码";
|
||||||
|
"当前地图标准" = "当前地图标准";
|
||||||
|
"当前客户端" = "当前客户端";
|
||||||
|
"当前标准" = "当前标准";
|
||||||
|
"当前选点revision" = "当前选点revision";
|
||||||
|
"恢复状态" = "恢复状态";
|
||||||
|
"批次索引" = "批次索引";
|
||||||
|
"持久化字段" = "持久化字段";
|
||||||
|
"授权状态" = "授权状态";
|
||||||
|
"授权状态rawValue" = "授权状态rawValue";
|
||||||
|
"探测失败原因" = "探测失败原因";
|
||||||
|
"搜索结果" = "搜索结果";
|
||||||
|
"操作" = "操作";
|
||||||
|
"新类型" = "新类型";
|
||||||
|
"无已保存" = "无已保存";
|
||||||
|
"无法判定" = "无法判定";
|
||||||
|
"无法启动本地证书服务" = "无法启动本地证书服务";
|
||||||
|
"无法生成本地证书" = "无法生成本地证书";
|
||||||
|
"日志策略" = "日志策略";
|
||||||
|
"旧类型" = "旧类型";
|
||||||
|
"显示坐标字段" = "显示坐标字段";
|
||||||
|
"最低版本" = "最低版本";
|
||||||
|
"最新版本" = "最新版本";
|
||||||
|
"最终标准" = "最终标准";
|
||||||
|
"最终阶段" = "最终阶段";
|
||||||
|
"有坐标" = "有坐标";
|
||||||
|
"有效样本数" = "有效样本数";
|
||||||
|
"有缓存" = "有缓存";
|
||||||
|
"未知错误" = "未知错误";
|
||||||
|
"本地 CA 证书或私钥无效" = "本地 CA 证书或私钥无效";
|
||||||
|
"无法写入设备钥匙串(%d)" = "无法写入设备钥匙串(%d)";
|
||||||
|
"来源" = "来源";
|
||||||
|
"来源类型" = "来源类型";
|
||||||
|
"林士街" = "林士街";
|
||||||
|
"查看诊断日志" = "查看诊断日志";
|
||||||
|
"样本年龄秒" = "样本年龄秒";
|
||||||
|
"样本数" = "样本数";
|
||||||
|
"样本时间" = "样本时间";
|
||||||
|
"检查范围" = "检查范围";
|
||||||
|
"检测前标准" = "检测前标准";
|
||||||
|
"模块拦截、MITM、代理/VPN连接" = "模块拦截、MITM、代理/VPN连接";
|
||||||
|
"次数" = "次数";
|
||||||
|
"每次开启首次或判定变化" = "每次开启首次或判定变化";
|
||||||
|
"水平精度米" = "水平精度米";
|
||||||
|
"活动requestID" = "活动requestID";
|
||||||
|
"活动阶段" = "活动阶段";
|
||||||
|
"海拔米" = "海拔米";
|
||||||
|
"满足当前请求时间窗" = "满足当前请求时间窗";
|
||||||
|
"点击实时定位" = "点击实时定位";
|
||||||
|
"目标所在区域" = "目标所在区域";
|
||||||
|
"确认标准" = "确认标准";
|
||||||
|
"社区征集客户端数" = "社区征集客户端数";
|
||||||
|
"等待秒数" = "等待秒数";
|
||||||
|
"系统缓存存在" = "系统缓存存在";
|
||||||
|
"结果" = "结果";
|
||||||
|
"结果数" = "结果数";
|
||||||
|
"结果来源" = "结果来源";
|
||||||
|
"缓存" = "缓存";
|
||||||
|
"缓存上限秒" = "缓存上限秒";
|
||||||
|
"缓存时效通过" = "缓存时效通过";
|
||||||
|
"缩放米" = "缩放米";
|
||||||
|
"网络可用" = "网络可用";
|
||||||
|
"耗时毫秒" = "耗时毫秒";
|
||||||
|
"蓝点回调更接近" = "蓝点回调更接近";
|
||||||
|
"蓝点缓存" = "蓝点缓存";
|
||||||
|
"蓝点缓存存在" = "蓝点缓存存在";
|
||||||
|
"蓝点距GCJ目标米" = "蓝点距GCJ目标米";
|
||||||
|
"蓝点距WGS目标米" = "蓝点距WGS目标米";
|
||||||
|
"触发原因" = "触发原因";
|
||||||
|
"诊断位置" = "诊断位置";
|
||||||
|
"请求动作" = "请求动作";
|
||||||
|
"超时毫秒" = "超时毫秒";
|
||||||
|
"输入坐标标准" = "输入坐标标准";
|
||||||
|
"连接状态" = "连接状态";
|
||||||
|
"选点revision" = "选点revision";
|
||||||
|
"选点期间发生变化" = "选点期间发生变化";
|
||||||
|
"错误" = "错误";
|
||||||
|
"错误类型" = "错误类型";
|
||||||
|
"锚点" = "锚点";
|
||||||
|
"阶段" = "阶段";
|
||||||
|
"首条名称" = "首条名称";
|
||||||
|
"首条名称=林士街→GCJ-02,否则→WGS-84" = "首条名称=林士街→GCJ-02,否则→WGS-84";
|
||||||
|
"默认国内标准" = "默认国内标准";
|
||||||
|
"证书下载地址无效" = "证书下载地址无效";
|
||||||
|
"启动代理失败: %@" = "启动代理失败: %@";
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
/* App display name */
|
||||||
|
"CFBundleDisplayName" = "Location Spoofer";
|
||||||
|
|
||||||
|
/* Location permission prompts */
|
||||||
|
"NSLocationWhenInUseUsageDescription" = "用於顯示和驗證虛擬定位是否生效";
|
||||||
|
"NSLocationAlwaysAndWhenInUseUsageDescription" = "用於顯示和驗證虛擬定位是否生效";
|
||||||
@@ -0,0 +1,668 @@
|
|||||||
|
/* Generated from en.lproj/Localizable.strings by Scripts/generate-chinese-localizations.swift. */
|
||||||
|
"APP模式" = "APP模式";
|
||||||
|
"第三方代理模式" = "第三方代理模式";
|
||||||
|
"成功" = "成功";
|
||||||
|
"已有验证正在进行" = "已有驗證正在進行";
|
||||||
|
"验证已被新位置取代" = "驗證已被新位置取代";
|
||||||
|
"证书未信任" = "證書未信任";
|
||||||
|
"WiFi代理未配置" = "WiFi代理未配置";
|
||||||
|
"坐标写入失败" = "坐標寫入失敗";
|
||||||
|
"改写验证失败" = "改寫驗證失敗";
|
||||||
|
"运行模式" = "運行模式";
|
||||||
|
"模式" = "模式";
|
||||||
|
"状态" = "狀態";
|
||||||
|
"本机代理" = "本機代理";
|
||||||
|
"第三方模块" = "第三方模塊";
|
||||||
|
"检测连接" = "檢測連接";
|
||||||
|
"虚拟定位" = "虛擬定位";
|
||||||
|
"定位模拟" = "定位模擬";
|
||||||
|
"运动状态模拟" = "運動狀態模擬";
|
||||||
|
"实验性功能,默认关闭。开启后会同时模拟定位响应中的运动状态。" = "實驗性功能,默認關閉。開啓後會同時模擬定位響應中的運動狀態。";
|
||||||
|
"随机扰动" = "隨機擾動";
|
||||||
|
"开启后,下次同步坐标时会给目标点添加随机偏移(默认半径 50 米),避免位置固定在同一点。" = "開啓後,下次同步坐標時會給目標點添加隨機偏移(默認半徑 50 米),避免位置固定在同一點。";
|
||||||
|
"说明" = "說明";
|
||||||
|
"工作原理" = "工作原理";
|
||||||
|
"应用" = "應用";
|
||||||
|
"进入引导页" = "進入引導頁";
|
||||||
|
"正在检查…" = "正在檢查…";
|
||||||
|
"检查更新" = "檢查更新";
|
||||||
|
"版本" = "版本";
|
||||||
|
"证书" = "證書";
|
||||||
|
"重置证书" = "重置證書";
|
||||||
|
"仅删除 App 钥匙串中的设备 CA。iOS 中已经安装的旧证书需要在系统设置里手动移除。" = "僅刪除 App 鑰匙串中的設備 CA。iOS 中已經安裝的舊證書需要在系統設置裡手動移除。";
|
||||||
|
"支持" = "支持";
|
||||||
|
"使用帮助" = "使用幫助";
|
||||||
|
"功能建议" = "功能建議";
|
||||||
|
"分享第三方配置" = "分享第三方配置";
|
||||||
|
"关于" = "關於";
|
||||||
|
"如果觉得好用,欢迎去 GitHub 给项目点个 Star" = "如果覺得好用,歡迎去 GitHub 給項目點個 Star";
|
||||||
|
"致谢" = "致謝";
|
||||||
|
"核心定位改写逻辑移植自 Yu9191/wloc" = "核心定位改寫邏輯移植自 Yu9191/wloc";
|
||||||
|
"设置" = "設置";
|
||||||
|
"完成" = "完成";
|
||||||
|
"代理操作失败" = "代理操作失敗";
|
||||||
|
"重置证书?" = "重置證書?";
|
||||||
|
"重置并生成新证书" = "重置並生成新證書";
|
||||||
|
"当前虚拟定位和本地代理将停止。App 会删除钥匙串中的设备 CA、立即生成新证书,并打开安装与信任引导。你还需要前往 iOS「设置 → 通用 → VPN 与设备管理」手动删除旧证书,然后重新下载安装并完全信任新证书。" = "當前虛擬定位和本地代理將停止。App 會刪除鑰匙串中的設備 CA、立即生成新證書,並打開安裝與信任引導。你還需要前往 iOS「設置 → 通用 → VPN 與設備管理」手動刪除舊證書,然後重新下載安裝並完全信任新證書。";
|
||||||
|
"第三方代理配置" = "第三方代理配置";
|
||||||
|
"客户端" = "客戶端";
|
||||||
|
"使用国内镜像下载模块" = "使用國內鏡像下載模塊";
|
||||||
|
"仅影响之后复制和重新导入的模块地址;已安装模块需要重新导入后切换来源。" = "僅影響之後複製和重新導入的模塊地址;已安裝模塊需要重新導入後切換來源。";
|
||||||
|
"验证状态" = "驗證狀態";
|
||||||
|
"已复制模块订阅地址" = "已複製模塊訂閱地址";
|
||||||
|
"复制模块订阅地址" = "複製模塊訂閱地址";
|
||||||
|
"复制 %@" = "複製 %@";
|
||||||
|
"已复制 %@" = "已複製 %@";
|
||||||
|
"已复制解密域名" = "已複製解密域名";
|
||||||
|
"复制解密域名" = "複製解密域名";
|
||||||
|
"打开 %@" = "打開 %@";
|
||||||
|
"重新打开配置引导" = "重新打開配置引導";
|
||||||
|
"Egern 直接使用 Surge 的 .sgmodule 模块。" = "Egern 直接使用 Surge 的 .sgmodule 模塊。";
|
||||||
|
"Stash 直接订阅 .stoverride,不要通过 Script Hub 转换。" = "Stash 直接訂閱 .stoverride,不要通過 Script Hub 轉換。";
|
||||||
|
"复制模块订阅地址后,在对应代理客户端中添加模块/重写订阅,并为复制的全部域名(含 gsp-ssl.ls.apple.com、bluedot.is.autonavi.com)启用 MITM。第三方客户端保存坐标后,即使关闭本 App,坐标仍由代理客户端持久化并继续生效。" = "複製模塊訂閱地址後,在對應代理客戶端中添加模塊/重寫訂閱,並為複製的全部域名(含 gsp-ssl.ls.apple.com、bluedot.is.autonavi.com)啓用 MITM。第三方客戶端保存坐標後,即使關閉本 App,坐標仍由代理客戶端持久化並繼續生效。";
|
||||||
|
"已是最新版本" = "已是最新版本";
|
||||||
|
"当前版本 %@,远程最新版本 %@。" = "當前版本 %@,遠程最新版本 %@。";
|
||||||
|
"检查更新失败" = "檢查更新失敗";
|
||||||
|
"无法获取远程版本信息,请检查网络后重试。" = "無法獲取遠程版本信息,請檢查網絡後重試。";
|
||||||
|
"未检测" = "未檢測";
|
||||||
|
"已连接,有坐标" = "已連接,有坐標";
|
||||||
|
"已连接,无坐标" = "已連接,無坐標";
|
||||||
|
"连接失败" = "連接失敗";
|
||||||
|
"已开启" = "已開啓";
|
||||||
|
"已关闭" = "已關閉";
|
||||||
|
"第三方已保存" = "第三方已保存";
|
||||||
|
"未保存" = "未保存";
|
||||||
|
"未知" = "未知";
|
||||||
|
"App 只负责地图选点、收藏和发送 WGS-84 坐标。第三方代理客户端通过模块拦截 Apple WLOC 请求并持久化当前坐标;本模式不启动本机代理,不使用 App 的 CA,也不需要配置 127.0.0.1:8888。" = "App 只負責地圖選點、收藏和發送 WGS-84 坐標。第三方代理客戶端通過模塊攔截 Apple WLOC 請求並持久化當前坐標;本模式不啓動本機代理,不使用 App 的 CA,也不需要配置 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 返回的定位坐标改写为你设置的虚拟坐标,再加密返回给系统,从而实现虚拟定位。" = "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 返回的定位坐標改寫為你設置的虛擬坐標,再加密返回給系統,從而實現虛擬定位。";
|
||||||
|
"模式已切换" = "模式已切換";
|
||||||
|
"第三方代理模式检测通过。请关闭 Wi-Fi 中的 127.0.0.1:8888 手动代理,避免双重拦截。" = "第三方代理模式檢測通過。請關閉 Wi-Fi 中的 127.0.0.1:8888 手動代理,避免雙重攔截。";
|
||||||
|
"APP 模式环境检测通过。请停用第三方 WLOC 模块或代理连接,避免双重拦截。" = "APP 模式環境檢測通過。請停用第三方 WLOC 模塊或代理連接,避免雙重攔截。";
|
||||||
|
"证书重置失败" = "證書重置失敗";
|
||||||
|
"无法打开客户端" = "無法打開客戶端";
|
||||||
|
"无法打开 %@,请确认客户端已安装后手动打开。" = "無法打開 %@,請確認客戶端已安裝後手動打開。";
|
||||||
|
"正在初始化地图与本地代理…" = "正在初始化地圖與本地代理…";
|
||||||
|
"正在初始化地图…" = "正在初始化地圖…";
|
||||||
|
"更新说明暂时无法加载,请前往最新 Release 页面查看。" = "更新說明暫時無法加載,請前往最新 Release 頁面查看。";
|
||||||
|
"需要更新" = "需要更新";
|
||||||
|
"当前版本 %@ 已停止支持,请更新到 %@ 后继续使用。\n\n%@" = "當前版本 %@ 已停止支持,請更新到 %@ 後繼續使用。\n\n%@";
|
||||||
|
"立即更新" = "立即更新";
|
||||||
|
"发现新版本" = "發現新版本";
|
||||||
|
"当前版本 %@,最新版本 %@。\n\n%@" = "當前版本 %@,最新版本 %@。\n\n%@";
|
||||||
|
"前往更新" = "前往更新";
|
||||||
|
"稍后" = "稍後";
|
||||||
|
"遇到问题时,在这里生成 Bug 报告。系统会运行一次诊断测试,并将完整报告复制到剪切板。跳转到 GitHub 后,请粘贴到“App 生成的诊断报告”字段。" = "遇到問題時,在這裡生成 Bug 報告。系統會運行一次診斷測試,並將完整報告複製到剪切板。跳轉到 GitHub 後,請粘貼到“App 生成的診斷報告”字段。";
|
||||||
|
"可复现环境" = "可復現環境";
|
||||||
|
"当前设备上问题稳定复现,非偶发性。" = "當前設備上問題穩定復現,非偶發性。";
|
||||||
|
"问题描述" = "問題描述";
|
||||||
|
"正在生成报告…" = "正在生成報告…";
|
||||||
|
"生成 Bug 报告" = "生成 Bug 報告";
|
||||||
|
"报告 Bug" = "報告 Bug";
|
||||||
|
"已生成" = "已生成";
|
||||||
|
"打开 GitHub 表单" = "打開 GitHub 表單";
|
||||||
|
"稍后再说" = "稍後再說";
|
||||||
|
"Bug 报告已复制到剪切板。请在 GitHub 表单的“App 生成的诊断报告”字段中粘贴并提交。" = "Bug 報告已複製到剪切板。請在 GitHub 表單的“App 生成的診斷報告”字段中粘貼並提交。";
|
||||||
|
"过滤日志" = "過濾日誌";
|
||||||
|
"暂无运行日志" = "暫無運行日誌";
|
||||||
|
"无匹配日志" = "無匹配日誌";
|
||||||
|
"运行日志" = "運行日誌";
|
||||||
|
"日志自动清理,仅保留近 3 天" = "日誌自動清理,僅保留近 3 天";
|
||||||
|
"关闭" = "關閉";
|
||||||
|
"复制全部日志" = "複製全部日誌";
|
||||||
|
"清空全部日志" = "清空全部日誌";
|
||||||
|
"清空所有运行日志?" = "清空所有運行日誌?";
|
||||||
|
"清空日志" = "清空日誌";
|
||||||
|
"取消" = "取消";
|
||||||
|
"正在检测…" = "正在檢測…";
|
||||||
|
"环境检测" = "環境檢測";
|
||||||
|
"检查第三方模块能否拦截并响应 query 请求;不会写入测试坐标。" = "檢查第三方模塊能否攔截並響應 query 請求;不會寫入測試坐標。";
|
||||||
|
"依次检查:本地代理 → CA 证书信任 → Wi-Fi 代理链路。" = "依次檢查:本地代理 → CA 證書信任 → Wi-Fi 代理鏈路。";
|
||||||
|
"测试日志" = "測試日誌";
|
||||||
|
"已复制" = "已複製";
|
||||||
|
"代理运行中" = "代理運行中";
|
||||||
|
"代理未运行" = "代理未運行";
|
||||||
|
"可修改" = "可修改";
|
||||||
|
"不可修改" = "不可修改";
|
||||||
|
"环境检测通过" = "環境檢測通過";
|
||||||
|
"环境检测失败: %@" = "環境檢測失敗: %@";
|
||||||
|
",查看下方日志" = ",查看下方日誌";
|
||||||
|
"第三方模块连接通过,已有坐标" = "第三方模塊連接通過,已有坐標";
|
||||||
|
"第三方模块连接通过,暂无坐标" = "第三方模塊連接通過,暫無坐標";
|
||||||
|
"第三方模块连接失败" = "第三方模塊連接失敗";
|
||||||
|
"======== 第三方代理连接检测 ========" = "======== 第三方代理連接檢測 ========";
|
||||||
|
"模式: 测试模式" = "模式: 測試模式";
|
||||||
|
"请求: wloc-settings/save?action=query" = "請求: wloc-settings/save?action=query";
|
||||||
|
"拦截响应: 有效 JSON" = "攔截響應: 有效 JSON";
|
||||||
|
"已保存坐标: %@" = "已保存坐標: %@";
|
||||||
|
"结果: %@" = "結果: %@";
|
||||||
|
"生效说明" = "生效說明";
|
||||||
|
"失效说明" = "失效說明";
|
||||||
|
"关闭 WiFi 代理" = "關閉 WiFi 代理";
|
||||||
|
"知道了" = "知道了";
|
||||||
|
"让虚拟定位生效" = "讓虛擬定位生效";
|
||||||
|
"确认第三方代理已开启" = "確認第三方代理已開啓";
|
||||||
|
"保持已导入的 WLOC 模块、HTTPS 解密和第三方代理/VPN 连接开启。" = "保持已導入的 WLOC 模塊、HTTPS 解密和第三方代理/VPN 連接開啓。";
|
||||||
|
"开启飞行模式" = "開啓飛行模式";
|
||||||
|
"从控制中心打开飞行模式(点飞机图标),Wi‑Fi 会自动断开。这是为了清除 iOS 的定位缓存。等待 2 秒。" = "從控制中心打開飛行模式(點飛機圖標),Wi‑Fi 會自動斷開。這是為了清除 iOS 的定位緩存。等待 2 秒。";
|
||||||
|
"关闭 Wi‑Fi" = "關閉 Wi‑Fi";
|
||||||
|
"从控制中心再点一下 Wi‑Fi 图标,确认 Wi‑Fi 已关闭。等待 2 秒。" = "從控制中心再點一下 Wi‑Fi 圖標,確認 Wi‑Fi 已關閉。等待 2 秒。";
|
||||||
|
"关闭系统定位服务" = "關閉系統定位服務";
|
||||||
|
"打开系统「设置 → 隐私与安全性 → 定位服务」,关闭顶部的总开关。等待 2 秒。" = "打開系統「設置 → 隱私與安全性 → 定位服務」,關閉頂部的總開關。等待 2 秒。";
|
||||||
|
"打开 Wi‑Fi,启动虚拟定位" = "打開 Wi‑Fi,啓動虛擬定位";
|
||||||
|
"从控制中心打开 Wi‑Fi(飞行模式保持开启),确认第三方代理已连接。坐标已经同步到第三方代理。等待 2 秒。" = "從控制中心打開 Wi‑Fi(飛行模式保持開啓),確認第三方代理已連接。坐標已經同步到第三方代理。等待 2 秒。";
|
||||||
|
"从控制中心打开 Wi‑Fi(飞行模式保持开启),进入 App 点底部「开始虚拟定位」。等待 2 秒。" = "從控制中心打開 Wi‑Fi(飛行模式保持開啓),進入 App 點底部「開始虛擬定位」。等待 2 秒。";
|
||||||
|
"关闭飞行模式" = "關閉飛行模式";
|
||||||
|
"从控制中心关闭飞行模式。等待 2 秒。" = "從控制中心關閉飛行模式。等待 2 秒。";
|
||||||
|
"重新开启定位服务" = "重新開啓定位服務";
|
||||||
|
"再次进入「设置 → 隐私与安全性 → 定位服务」,打开总开关。完成后打开地图验证定位是否已变化。" = "再次進入「設置 → 隱私與安全性 → 定位服務」,打開總開關。完成後打開地圖驗證定位是否已變化。";
|
||||||
|
"还是无法生效?" = "還是無法生效?";
|
||||||
|
"操作到第 3 步时关机重启,开机后从第 4 步继续。这样能彻底清除系统缓存的定位数据。" = "操作到第 3 步時關機重啓,開機後從第 4 步繼續。這樣能徹底清除系統緩存的定位數據。";
|
||||||
|
"去设置" = "去設置";
|
||||||
|
"取消虚拟定位" = "取消虛擬定位";
|
||||||
|
"从控制中心打开飞行模式,Wi‑Fi 会自动断开。等待 2 秒。" = "從控制中心打開飛行模式,Wi‑Fi 會自動斷開。等待 2 秒。";
|
||||||
|
"从控制中心确认 Wi‑Fi 已关闭。等待 2 秒。" = "從控制中心確認 Wi‑Fi 已關閉。等待 2 秒。";
|
||||||
|
"打开「设置 → 隐私与安全性 → 定位服务」,关闭总开关。等待 2 秒。" = "打開「設置 → 隱私與安全性 → 定位服務」,關閉總開關。等待 2 秒。";
|
||||||
|
"确认坐标已清除" = "確認坐標已清除";
|
||||||
|
"App 已通知第三方代理清除虚拟坐标。保持网络可用并等待 2 秒,让系统重新获取真实定位。" = "App 已通知第三方代理清除虛擬坐標。保持網絡可用並等待 2 秒,讓系統重新獲取真實定位。";
|
||||||
|
"打开 Wi‑Fi,移除代理" = "打開 Wi‑Fi,移除代理";
|
||||||
|
"从控制中心打开 Wi‑Fi。然后进入「设置 → 无线局域网 → 点 WiFi 右侧 (i) → HTTP 代理」,选择「关闭」后存储。等待 2 秒。" = "從控制中心打開 Wi‑Fi。然後進入「設置 → 無線局域網 → 點 WiFi 右側 (i) → HTTP 代理」,選擇「關閉」後存儲。等待 2 秒。";
|
||||||
|
"再次进入「设置 → 隐私与安全性 → 定位服务」打开总开关。打开地图验证定位是否恢复。" = "再次進入「設置 → 隱私與安全性 → 定位服務」打開總開關。打開地圖驗證定位是否恢復。";
|
||||||
|
"还是无法取消?" = "還是無法取消?";
|
||||||
|
"操作到第 3 步时关机重启,开机后从第 4 步继续。" = "操作到第 3 步時關機重啓,開機後從第 4 步繼續。";
|
||||||
|
"移除代理配置" = "移除代理配置";
|
||||||
|
"停止虚拟定位后,需要手动移除 WiFi 代理配置,否则可能无法上网。\n\n1. 打开「设置 → 无线局域网」\n2. 点击当前 WiFi 右侧 (i) 图标\n3. 找到「HTTP 代理」\n4. 选择「关闭」\n5. 点右上角「存储」" = "停止虛擬定位後,需要手動移除 WiFi 代理配置,否則可能無法上網。\n\n1. 打開「設置 → 無線局域網」\n2. 點擊當前 WiFi 右側 (i) 圖標\n3. 找到「HTTP 代理」\n4. 選擇「關閉」\n5. 點右上角「存儲」";
|
||||||
|
"社区分享成功配置?" = "社區分享成功配置?";
|
||||||
|
"去提交" = "去提交";
|
||||||
|
"复制模板" = "複製模板";
|
||||||
|
"不再提示" = "不再提示";
|
||||||
|
"第三方客户端" = "第三方客戶端";
|
||||||
|
"你正在使用 %@。点击“去提交”会先复制投稿模板,并在 App 内打开社区页面。采纳后将收录到 README,可选择是否匿名署名。" = "你正在使用 %@。點擊“去提交”會先複製投稿模板,並在 App 內打開社區頁面。採納後將收錄到 README,可選擇是否匿名署名。";
|
||||||
|
"已复制投稿模板" = "已複製投稿模板";
|
||||||
|
"如果 GitHub 登录或浏览器跳转后模板没有自动填充,可以直接粘贴。" = "如果 GitHub 登錄或瀏覽器跳轉後模板沒有自動填充,可以直接粘貼。";
|
||||||
|
"无法直接跳转" = "無法直接跳轉";
|
||||||
|
"定位失败" = "定位失敗";
|
||||||
|
"打开设置" = "打開設置";
|
||||||
|
"无法获取当前定位,请检查定位服务是否已开启" = "無法獲取當前定位,請檢查定位服務是否已開啓";
|
||||||
|
"编辑收藏名称" = "編輯收藏名稱";
|
||||||
|
"名称" = "名稱";
|
||||||
|
"保存" = "保存";
|
||||||
|
"修改收藏地点名称" = "修改收藏地點名稱";
|
||||||
|
"搜索地点或坐标" = "搜索地點或坐標";
|
||||||
|
"日志" = "日誌";
|
||||||
|
"更多" = "更多";
|
||||||
|
"当前选点" = "當前選點";
|
||||||
|
"GCJ-02(国内)" = "GCJ-02(國內)";
|
||||||
|
"WGS-84(国际)" = "WGS-84(國際)";
|
||||||
|
"无法生效?" = "無法生效?";
|
||||||
|
"无法取消?" = "無法取消?";
|
||||||
|
"已收藏,点击取消收藏" = "已收藏,點擊取消收藏";
|
||||||
|
"收藏当前选点" = "收藏當前選點";
|
||||||
|
"搜索或点击地图选点后,保存为收藏。" = "搜索或點擊地圖選點後,保存為收藏。";
|
||||||
|
"切换到此处" = "切換到此處";
|
||||||
|
"同步到第三方代理" = "同步到第三方代理";
|
||||||
|
"检测并同步中…" = "檢測並同步中…";
|
||||||
|
"停止第三方虚拟定位" = "停止第三方虛擬定位";
|
||||||
|
"开始虚拟定位" = "開始虛擬定位";
|
||||||
|
"验证环境中…" = "驗證環境中…";
|
||||||
|
"停止虚拟定位" = "停止虛擬定位";
|
||||||
|
"当前未连接可用的 Wi-Fi,请连接 Wi-Fi 后配置 127.0.0.1:8888 手动代理。" = "當前未連接可用的 Wi-Fi,請連接 Wi-Fi 後配置 127.0.0.1:8888 手動代理。";
|
||||||
|
"第三方代理查询失败" = "第三方代理查詢失敗";
|
||||||
|
"未命名" = "未命名";
|
||||||
|
"没有找到相关地点" = "沒有找到相關地點";
|
||||||
|
"虚拟定位已开启" = "虛擬定位已開啓";
|
||||||
|
"不再提醒" = "不再提醒";
|
||||||
|
"虚拟定位已关闭" = "虛擬定位已關閉";
|
||||||
|
"选择模式" = "選擇模式";
|
||||||
|
"配置 Wi-Fi 代理" = "配置 Wi-Fi 代理";
|
||||||
|
"初始化 CA 证书" = "初始化 CA 證書";
|
||||||
|
"选择客户端" = "選擇客戶端";
|
||||||
|
"导入并检测" = "導入並檢測";
|
||||||
|
"上一步" = "上一步";
|
||||||
|
"开始使用" = "開始使用";
|
||||||
|
"操作失败" = "操作失敗";
|
||||||
|
"选择运行模式" = "選擇運行模式";
|
||||||
|
"后续可在“设置 → 运行模式”中切换。两种模式不要同时拦截 WLOC 请求。" = "後續可在“設置 → 運行模式”中切換。兩種模式不要同時攔截 WLOC 請求。";
|
||||||
|
"仅 Wi-Fi" = "僅 Wi-Fi";
|
||||||
|
"无外部依赖" = "無外部依賴";
|
||||||
|
"App 在设备本地启动代理,通过当前 Wi-Fi 的手动 HTTP 代理改写定位响应。免费自签应用无法使用系统 VPN 的 Network Extension 能力,因此 APP模式不支持蜂窝网络,需要配置 Wi-Fi 代理并安装 App 生成的 CA。" = "App 在設備本地啓動代理,通過當前 Wi-Fi 的手動 HTTP 代理改寫定位響應。免費自簽應用無法使用系統 VPN 的 Network Extension 能力,因此 APP模式不支持蜂窩網絡,需要配置 Wi-Fi 代理並安裝 App 生成的 CA。";
|
||||||
|
"测试模式" = "測試模式";
|
||||||
|
"App 负责选点,并通过 WLOC 配置接口查询和同步坐标;第三方代理客户端负责网络代理、模块拦截、MITM 和持久化。证书、VPN 与代理连接均由第三方客户端处理。" = "App 負責選點,並通過 WLOC 配置接口查詢和同步坐標;第三方代理客戶端負責網絡代理、模塊攔截、MITM 和持久化。證書、VPN 與代理連接均由第三方客戶端處理。";
|
||||||
|
"正在准备 APP模式本地服务…" = "正在準備 APP模式本地服務…";
|
||||||
|
"先配置 Wi-Fi 系统代理" = "先配置 Wi-Fi 系統代理";
|
||||||
|
"在当前 Wi-Fi 的详情页,将「HTTP 代理」设为「手动」:服务器填 127.0.0.1,端口填 8888。配置后点击下方「完成」。检测会自动判断是 Wi-Fi 代理还是证书信任有问题。" = "在當前 Wi-Fi 的詳情頁,將「HTTP 代理」設為「手動」:服務器填 127.0.0.1,端口填 8888。配置後點擊下方「完成」。檢測會自動判斷是 Wi-Fi 代理還是證書信任有問題。";
|
||||||
|
"Wi-Fi 代理设置" = "Wi-Fi 代理設置";
|
||||||
|
"1 选择手动,2 填写服务器 127.0.0.1,3 填写端口 8888。" = "1 選擇手動,2 填寫服務器 127.0.0.1,3 填寫端口 8888。";
|
||||||
|
"复制地址" = "複製地址";
|
||||||
|
"打开 Wi-Fi 设置" = "打開 Wi-Fi 設置";
|
||||||
|
"第 1 步:下载证书" = "第 1 步:下載證書";
|
||||||
|
"下载本机随机生成的 CA 根证书。私钥仅保存在此设备的钥匙串中,不会随证书文件导出。App 会弹出 Safari 下载页;出现配置描述文件下载提示时,选择「允许」。" = "下載本機隨機生成的 CA 根證書。私鑰僅保存在此設備的鑰匙串中,不會隨證書文件導出。App 會彈出 Safari 下載頁;出現配置描述文件下載提示時,選擇「允許」。";
|
||||||
|
"打开下载页" = "打開下載頁";
|
||||||
|
"无法准备证书下载页面,请查看诊断日志" = "無法準備證書下載頁面,請查看診斷日誌";
|
||||||
|
"第 2 步:安装证书" = "第 2 步:安裝證書";
|
||||||
|
"下载完成后打开系统「设置」。如果顶部显示「已下载描述文件」,点进去安装;否则进入「通用 → VPN 与设备管理」,找到 Location Spoofer CA 并完成安装。" = "下載完成後打開系統「設置」。如果頂部顯示「已下載描述文件」,點進去安裝;否則進入「通用 → VPN 與設備管理」,找到 Location Spoofer CA 並完成安裝。";
|
||||||
|
"去安装" = "去安裝";
|
||||||
|
"安装证书" = "安裝證書";
|
||||||
|
"1 在「VPN 与设备管理」中打开 Location Spoofer CA 描述文件并完成安装。" = "1 在「VPN 與設備管理」中打開 Location Spoofer CA 描述文件並完成安裝。";
|
||||||
|
"第 3 步:信任证书" = "第 3 步:信任證書";
|
||||||
|
"安装后进入「设置 → 通用 → 关于本机 → 证书信任设置」,找到 Location Spoofer CA 并开启完全信任。iOS 保留钥匙串数据时,重装 App 会继续复用同一证书。" = "安裝後進入「設置 → 通用 → 關於本機 → 證書信任設置」,找到 Location Spoofer CA 並開啓完全信任。iOS 保留鑰匙串數據時,重裝 App 會繼續復用同一證書。";
|
||||||
|
"去信任" = "去信任";
|
||||||
|
"信任证书" = "信任證書";
|
||||||
|
"1 在「证书信任设置」中为 Location Spoofer CA 开启完全信任。" = "1 在「證書信任設置」中為 Location Spoofer CA 開啓完全信任。";
|
||||||
|
"已完成" = "已完成";
|
||||||
|
"已完成 ✓" = "已完成 ✓";
|
||||||
|
"iOS 27 beta 6 起,系统已禁止对 gs-loc.apple.com 进行 MITM 拦截。该版本及之后的 beta 版本暂时无法使用本项目,等待后续适配方案。" = "iOS 27 beta 6 起,系統已禁止對 gs-loc.apple.com 進行 MITM 攔截。該版本及之後的 beta 版本暫時無法使用本項目,等待後續適配方案。";
|
||||||
|
"选择第三方代理客户端" = "選擇第三方代理客戶端";
|
||||||
|
"除 Shadowrocket 外,当前客户端配置尚未完成真机验证,页面只提供模块导入入口和通用配置提醒。" = "除 Shadowrocket 外,當前客戶端配置尚未完成真機驗證,頁面只提供模塊導入入口和通用配置提醒。";
|
||||||
|
"第三方客户端适配说明" = "第三方客戶端適配說明";
|
||||||
|
"App 不连接远程坐标服务器,而是向 Apple 域名发起一个约定请求。第三方客户端需要在本机拦截该请求、保存 WGS-84 坐标并返回 JSON;定位模块再读取同一份数据,修改 Apple WLOC 响应。" = "App 不連接遠程坐標服務器,而是向 Apple 域名發起一個約定請求。第三方客戶端需要在本機攔截該請求、保存 WGS-84 坐標並返回 JSON;定位模塊再讀取同一份數據,修改 Apple WLOC 響應。";
|
||||||
|
"配置接口" = "配置接口";
|
||||||
|
"查询:GET ?action=query\n保存:GET ?lon=<经度>&lat=<纬度>&acc=<精度>\n清除:GET ?action=clear" = "查詢:GET ?action=query\n保存:GET ?lon=<經度>&lat=<緯度>&acc=<精度>\n清除:GET ?action=clear";
|
||||||
|
"返回格式" = "返回格式";
|
||||||
|
"适配要求" = "適配要求";
|
||||||
|
"客户端需要支持请求脚本、持久化存储、HTTP 200 JSON 响应、Apple WLOC 响应脚本,以及 Apple 定位域名(gs-loc.apple.com、gsp-ssl.ls.apple.com、bluedot.is.autonavi.com 等)的 HTTPS 解密。保存接口和 WLOC 响应脚本必须读取同一份持久化数据。" = "客戶端需要支持請求腳本、持久化存儲、HTTP 200 JSON 響應、Apple WLOC 響應腳本,以及 Apple 定位域名(gs-loc.apple.com、gsp-ssl.ls.apple.com、bluedot.is.autonavi.com 等)的 HTTPS 解密。保存接口和 WLOC 響應腳本必須讀取同一份持久化數據。";
|
||||||
|
"检测到第三方代理连接异常,请检查模块、MITM 和代理连接后重新检测。" = "檢測到第三方代理連接異常,請檢查模塊、MITM 和代理連接後重新檢測。";
|
||||||
|
"第 1 步:导入 %@ 模块" = "第 1 步:導入 %@ 模塊";
|
||||||
|
"复制 %@ 的模块订阅地址。" = "複製 %@ 的模塊訂閱地址。";
|
||||||
|
"打开 Shadowrocket,进入“配置 → 模块”。" = "打開 Shadowrocket,進入“配置 → 模塊”。";
|
||||||
|
"打开 %@。" = "打開 %@。";
|
||||||
|
"进入 Shadowrocket 配置" = "進入 Shadowrocket 配置";
|
||||||
|
"1 点击「模块」进入模块列表,2 可打开当前本地配置详情。" = "1 點擊「模塊」進入模塊列表,2 可打開當前本地配置詳情。";
|
||||||
|
"点击右上角“+”,粘贴模块订阅地址并导入,然后确认模块已启用。" = "點擊右上角“+”,粘貼模塊訂閱地址並導入,然後確認模塊已啓用。";
|
||||||
|
"导入 Shadowrocket 模块" = "導入 Shadowrocket 模塊";
|
||||||
|
"1 点击右上角加号导入模块,2 确认模块已启用。" = "1 點擊右上角加號導入模塊,2 確認模塊已啓用。";
|
||||||
|
"在 %@ 中导入刚才复制的模块订阅地址。" = "在 %@ 中導入剛才複製的模塊訂閱地址。";
|
||||||
|
"第 2 步:完成 %@ 配置" = "第 2 步:完成 %@ 配置";
|
||||||
|
"请在 %@ 中完成相应配置。" = "請在 %@ 中完成相應配置。";
|
||||||
|
"配置时请复制下方全部解密域名(含 gsp-ssl.ls.apple.com、bluedot.is.autonavi.com)。" = "配置時請複製下方全部解密域名(含 gsp-ssl.ls.apple.com、bluedot.is.autonavi.com)。";
|
||||||
|
"第 2 步:配置 HTTPS 解密" = "第 2 步:配置 HTTPS 解密";
|
||||||
|
"进入“配置 → 本地文件”,找到带黄点的配置,点击右侧 i 图标。" = "進入“配置 → 本地文件”,找到帶黃點的配置,點擊右側 i 圖標。";
|
||||||
|
"进入“HTTPS 解密”,开启解密开关。" = "進入“HTTPS 解密”,開啓解密開關。";
|
||||||
|
"在域名列表中添加下方复制的全部解密域名。" = "在域名列表中添加下方複製的全部解密域名。";
|
||||||
|
"配置 HTTPS 解密" = "配置 HTTPS 解密";
|
||||||
|
"1 开启 HTTPS 解密,2 添加复制的全部解密域名,3 打开证书设置。" = "1 開啓 HTTPS 解密,2 添加複製的全部解密域名,3 打開證書設置。";
|
||||||
|
"按 Shadowrocket 提示生成并完成证书授权。" = "按 Shadowrocket 提示生成並完成證書授權。";
|
||||||
|
"授权 Shadowrocket 证书" = "授權 Shadowrocket 證書";
|
||||||
|
"1 打开 Shadowrocket 证书项并按提示安装、授权。" = "1 打開 Shadowrocket 證書項並按提示安裝、授權。";
|
||||||
|
"返回 HTTPS 解密页面,点击右上角勾号保存,然后开启代理。" = "返回 HTTPS 解密頁面,點擊右上角勾號保存,然後開啓代理。";
|
||||||
|
"打开 Shadowrocket 继续配置" = "打開 Shadowrocket 繼續配置";
|
||||||
|
"App 只能唤起 Shadowrocket,无法通过公开接口直接跳转到“模块”或“HTTPS 解密”页面。" = "App 只能喚起 Shadowrocket,無法通過公開接口直接跳轉到“模塊”或“HTTPS 解密”頁面。";
|
||||||
|
"轻点查看大图" = "輕點查看大圖";
|
||||||
|
"证书尚未安装或信任" = "證書尚未安裝或信任";
|
||||||
|
"Wi-Fi 代理未正确设置" = "Wi-Fi 代理未正確設置";
|
||||||
|
"本地代理未能启动" = "本地代理未能啓動";
|
||||||
|
"检测仍在进行" = "檢測仍在進行";
|
||||||
|
"检测结果已过期" = "檢測結果已過期";
|
||||||
|
"定位改写检测失败" = "定位改寫檢測失敗";
|
||||||
|
"接口连接失败" = "接口連接失敗";
|
||||||
|
"本地代理初始化失败:%@" = "本地代理初始化失敗:%@";
|
||||||
|
"✓ 定位环境正常" = "✓ 定位環境正常";
|
||||||
|
"CA 证书未安装或未信任" = "CA 證書未安裝或未信任";
|
||||||
|
"Wi-Fi 代理未正确设置,请检查 127.0.0.1:8888" = "Wi-Fi 代理未正確設置,請檢查 127.0.0.1:8888";
|
||||||
|
"请手动打开「设置」,找到本 App 后检查定位权限。" = "請手動打開「設置」,找到本 App 後檢查定位權限。";
|
||||||
|
"请手动打开「设置 → 通用」。" = "請手動打開「設置 → 通用」。";
|
||||||
|
"请手动打开「设置 → 无线局域网」,进入当前 Wi-Fi 的详情页。" = "請手動打開「設置 → 無線局域網」,進入當前 Wi-Fi 的詳情頁。";
|
||||||
|
"请手动打开「设置 → 隐私与安全性 → 定位服务」。" = "請手動打開「設置 → 隱私與安全性 → 定位服務」。";
|
||||||
|
"第三方代理返回了无法识别的数据" = "第三方代理返回了無法識別的數據";
|
||||||
|
"请求未被第三方代理模块拦截,请检查模块、MITM 和代理连接" = "請求未被第三方代理模塊攔截,請檢查模塊、MITM 和代理連接";
|
||||||
|
"第三方代理保存的坐标与当前选点不一致" = "第三方代理保存的坐標與當前選點不一致";
|
||||||
|
"第三方代理请求失败:%@" = "第三方代理請求失敗:%@";
|
||||||
|
"检查模块、MITM、证书和代理/VPN连接" = "檢查模塊、MITM、證書和代理/VPN連接";
|
||||||
|
"第三方代理拒绝保存坐标" = "第三方代理拒絕保存坐標";
|
||||||
|
"第三方代理清除坐标失败" = "第三方代理清除坐標失敗";
|
||||||
|
"已有第三方代理请求正在执行" = "已有第三方代理請求正在執行";
|
||||||
|
"配置已提供,尚未验证" = "配置已提供,尚未驗證";
|
||||||
|
"已连接,有保存坐标" = "已連接,有保存坐標";
|
||||||
|
"已连接,无保存坐标" = "已連接,無保存坐標";
|
||||||
|
"连接失败(%@)" = "連接失敗(%@)";
|
||||||
|
"当前客户端:%@" = "當前客戶端:%@";
|
||||||
|
"配置接口:/wloc-settings/save" = "配置接口:/wloc-settings/save";
|
||||||
|
"请求动作:WLOC query" = "請求動作:WLOC query";
|
||||||
|
"检查范围:模块拦截、MITM、证书、代理/VPN 连接" = "檢查範圍:模塊攔截、MITM、證書、代理/VPN 連接";
|
||||||
|
"连接状态:%@" = "連接狀態:%@";
|
||||||
|
"检测结果:失败" = "檢測結果:失敗";
|
||||||
|
"耗时:%lld ms" = "耗時:%lld ms";
|
||||||
|
"错误类型:%@" = "錯誤類型:%@";
|
||||||
|
"错误详情:%@" = "錯誤詳情:%@";
|
||||||
|
"处理建议:%@。" = "處理建議:%@。";
|
||||||
|
"======== 第三方代理运行检测 ========" = "======== 第三方代理運行檢測 ========";
|
||||||
|
"触发来源:地图或设置中的第三方代理操作" = "觸發來源:地圖或設置中的第三方代理操作";
|
||||||
|
"请求动作:WLOC 配置接口" = "請求動作:WLOC 配置接口";
|
||||||
|
"处理建议:确认模块已启用,并检查 MITM、证书和代理/VPN 连接。" = "處理建議:確認模塊已啓用,並檢查 MITM、證書和代理/VPN 連接。";
|
||||||
|
"环境信息" = "環境信息";
|
||||||
|
"App 版本" = "App 版本";
|
||||||
|
"系统版本" = "系統版本";
|
||||||
|
"诊断日志" = "診斷日誌";
|
||||||
|
"不适用" = "不適用";
|
||||||
|
"是" = "是";
|
||||||
|
"否" = "否";
|
||||||
|
"(无诊断数据)" = "(無診斷數據)";
|
||||||
|
"第三方代理测试模式:模块连接成功;已保存坐标=%@" = "第三方代理測試模式:模塊連接成功;已保存坐標=%@";
|
||||||
|
"第三方代理测试模式:模块连接失败;%@" = "第三方代理測試模式:模塊連接失敗;%@";
|
||||||
|
"======== 代理验证测试 ========" = "======== 代理驗證測試 ========";
|
||||||
|
"App 版本: %@" = "App 版本: %@";
|
||||||
|
"系统版本: iOS %@" = "系統版本: iOS %@";
|
||||||
|
"[步骤 A] 检查代理是否运行…" = "[步驟 A] 檢查代理是否運行…";
|
||||||
|
" 端口: 127.0.0.1:8888" = " 端口: 127.0.0.1:8888";
|
||||||
|
" ⚠ 代理未运行,尝试启动…" = " ⚠ 代理未運行,嘗試啓動…";
|
||||||
|
" ✗ 启动失败: %@" = " ✗ 啓動失敗: %@";
|
||||||
|
" ✓ 代理启动成功" = " ✓ 代理啓動成功";
|
||||||
|
" ✓ 代理已在运行中" = " ✓ 代理已在運行中";
|
||||||
|
"[步骤 B] 检测证书与 WiFi 代理…" = "[步驟 B] 檢測證書與 WiFi 代理…";
|
||||||
|
" 方式: 请求 baidu.com/paopao-verify-<token>" = " 方式: 請求 baidu.com/paopao-verify-<token>";
|
||||||
|
" 结果判定: TLS 错误=证书问题 / 响应不匹配=代理未配置 / 匹配=通过" = " 結果判定: TLS 錯誤=證書問題 / 響應不匹配=代理未配置 / 匹配=通過";
|
||||||
|
" ✗ 无法生成验证 token" = " ✗ 無法生成驗證 token";
|
||||||
|
" ✓ 证书已信任,WiFi 代理已配置" = " ✓ 證書已信任,WiFi 代理已配置";
|
||||||
|
" ✗ 响应不匹配: HTTP %lld, %lld bytes,WiFi 代理未配置" = " ✗ 響應不匹配: HTTP %lld, %lld bytes,WiFi 代理未配置";
|
||||||
|
" ✗ 请求失败 [%@ code=%lld]: %@" = " ✗ 請求失敗 [%@ code=%lld]: %@";
|
||||||
|
" TLS/证书校验失败,CA 证书未信任" = " TLS/證書校驗失敗,CA 證書未信任";
|
||||||
|
"======== 环境检测通过 ✓ ========" = "======== 環境檢測通過 ✓ ========";
|
||||||
|
" --- 代理日志 ---" = " --- 代理日誌 ---";
|
||||||
|
"成功:{\"success\":true,\"longitude\":113.0,\"latitude\":22.0,\"accuracy\":25}\n失败:{\"success\":false,\"error\":\"错误说明\"}" = "成功:{\"success\":true,\"longitude\":113.0,\"latitude\":22.0,\"accuracy\":25}\n失敗:{\"success\":false,\"error\":\"錯誤說明\"}";
|
||||||
|
"定位改写脚本源自 Yu9191/wloc,现由本项目维护" = "定位改寫腳本源自 Yu9191/wloc,現由本項目維護";
|
||||||
|
"地图" = "地圖";
|
||||||
|
"坐标转换" = "坐標轉換";
|
||||||
|
"定位" = "定位";
|
||||||
|
"实时定位" = "實時定位";
|
||||||
|
"提醒" = "提醒";
|
||||||
|
"搜索" = "搜索";
|
||||||
|
"收藏" = "收藏";
|
||||||
|
"缩放" = "縮放";
|
||||||
|
"========== App 启动 ==========" = "========== App 啓動 ==========";
|
||||||
|
"尚未选择运行模式,跳过本地 CA 和代理初始化" = "尚未選擇運行模式,跳過本地 CA 和代理初始化";
|
||||||
|
"第三方代理测试模式:跳过本地 CA、代理和环境检测" = "第三方代理測試模式:跳過本地 CA、代理和環境檢測";
|
||||||
|
"旧坐标数据迁移失败,将在下次启动重试" = "舊坐標數據遷移失敗,將在下次啓動重試";
|
||||||
|
"地图坐标标准初始化完成,开始后续启动流程" = "地圖坐標標準初始化完成,開始後續啓動流程";
|
||||||
|
"没有持久化图钉,地图创建前请求实时定位" = "沒有持久化圖釘,地圖創建前請求實時定位";
|
||||||
|
"已使用实时定位准备唯一初始地图状态" = "已使用實時定位準備唯一初始地圖狀態";
|
||||||
|
"地图创建前取得的初始实时位置(WGS-84)" = "地圖創建前取得的初始實時位置(WGS-84)";
|
||||||
|
"地图创建前无法取得实时定位,唯一初始位置使用深圳" = "地圖創建前無法取得實時定位,唯一初始位置使用深圳";
|
||||||
|
"已找到持久化图钉,直接准备唯一初始地图状态" = "已找到持久化圖釘,直接準備唯一初始地圖狀態";
|
||||||
|
"启动门禁全部完成,现在创建 MapHomeView" = "啓動門禁全部完成,現在創建 MapHomeView";
|
||||||
|
"初始化" = "初始化";
|
||||||
|
"启动代理 127.0.0.1:8888" = "啓動代理 127.0.0.1:8888";
|
||||||
|
"启动代理: 恢复上次 WGS-84 定位" = "啓動代理: 恢復上次 WGS-84 定位";
|
||||||
|
"启动成功" = "啓動成功";
|
||||||
|
"启动失败" = "啓動失敗";
|
||||||
|
"写入坐标" = "寫入坐標";
|
||||||
|
"跳过过期坐标写入" = "跳過過期坐標寫入";
|
||||||
|
"跳过旧验证坐标恢复" = "跳過舊驗證坐標恢復";
|
||||||
|
"运动状态模拟设置已更新" = "運動狀態模擬設置已更新";
|
||||||
|
"准备证书下载失败" = "準備證書下載失敗";
|
||||||
|
"设置虚拟定位坐标" = "設置虛擬定位坐標";
|
||||||
|
"apply失败" = "apply失敗";
|
||||||
|
"保存收藏失败" = "保存收藏失敗";
|
||||||
|
"本地服务初始化失败" = "本地服務初始化失敗";
|
||||||
|
"代理运行模式已切换" = "代理運行模式已切換";
|
||||||
|
"已迁移旧版模式初始化状态" = "已遷移舊版模式初始化狀態";
|
||||||
|
"地图坐标标准检测已有请求进行中" = "地圖坐標標準檢測已有請求進行中";
|
||||||
|
"地图坐标标准检测开始" = "地圖坐標標準檢測開始";
|
||||||
|
"地图坐标标准检测获得明确结果" = "地圖坐標標準檢測獲得明確結果";
|
||||||
|
"地图坐标标准检测不可用,开始实时定位兜底" = "地圖坐標標準檢測不可用,開始實時定位兜底";
|
||||||
|
"地图坐标标准检测使用兜底结果" = "地圖坐標標準檢測使用兜底結果";
|
||||||
|
"地图坐标标准检测被取消,保留默认国内标准" = "地圖坐標標準檢測被取消,保留默認國內標準";
|
||||||
|
"地图坐标标准已确定,允许创建地图" = "地圖坐標標準已確定,允許創建地圖";
|
||||||
|
"地图坐标标准运行期检测合并到进行中请求" = "地圖坐標標準運行期檢測合併到進行中請求";
|
||||||
|
"地图坐标标准运行期检测开始" = "地圖坐標標準運行期檢測開始";
|
||||||
|
"地图坐标标准运行期检测结果已过期,取消写入" = "地圖坐標標準運行期檢測結果已過期,取消寫入";
|
||||||
|
"地图坐标标准运行期检测完成,标准未变化" = "地圖坐標標準運行期檢測完成,標準未變化";
|
||||||
|
"地图坐标标准运行期检测发现切换" = "地圖坐標標準運行期檢測發現切換";
|
||||||
|
"地图坐标标准运行期检测失败,保留当前标准" = "地圖坐標標準運行期檢測失敗,保留當前標準";
|
||||||
|
"地图坐标标准运行期检测已取消" = "地圖坐標標準運行期檢測已取消";
|
||||||
|
"实时定位不覆盖固定锚点的明确检测结果" = "實時定位不覆蓋固定錨點的明確檢測結果";
|
||||||
|
"实时定位确认兜底地图坐标标准无需修正" = "實時定位確認兜底地圖坐標標準無需修正";
|
||||||
|
"实时定位修正启动兜底地图坐标标准" = "實時定位修正啓動兜底地圖坐標標準";
|
||||||
|
"第三方代理已保存 WGS-84 坐标" = "第三方代理已保存 WGS-84 坐標";
|
||||||
|
"第三方代理坐标已清除" = "第三方代理坐標已清除";
|
||||||
|
"第三方代理请求失败" = "第三方代理請求失敗";
|
||||||
|
"第三方代理坐标同步成功" = "第三方代理坐標同步成功";
|
||||||
|
"同步坐标到第三方客户端失败" = "同步坐標到第三方客戶端失敗";
|
||||||
|
"验证结果" = "驗證結果";
|
||||||
|
"验证失败" = "驗證失敗";
|
||||||
|
"开启前检测失败,进入对应环境引导" = "開啓前檢測失敗,進入對應環境引導";
|
||||||
|
"清除第三方客户端坐标失败" = "清除第三方客戶端坐標失敗";
|
||||||
|
"累计虚拟定位操作次数" = "累計虛擬定位操作次數";
|
||||||
|
"已复制坐标" = "已複製坐標";
|
||||||
|
"地图坐标标准切换时未找到当前选点缓存" = "地圖坐標標準切換時未找到當前選點緩存";
|
||||||
|
"地图坐标标准切换后已使用缓存坐标对回显当前选点" = "地圖坐標標準切換後已使用緩存坐標對回顯當前選點";
|
||||||
|
"地图坐标类型已变化" = "地圖坐標類型已變化";
|
||||||
|
"取消保存收藏:检测期间当前选点已变化" = "取消保存收藏:檢測期間當前選點已變化";
|
||||||
|
"保存当前选点为收藏" = "保存當前選點為收藏";
|
||||||
|
"第三方代理查询返回失败" = "第三方代理查詢返回失敗";
|
||||||
|
"启动后第三方代理状态查询失败" = "啓動後第三方代理狀態查詢失敗";
|
||||||
|
"检测到 Wi-Fi 网络变化" = "檢測到 Wi-Fi 網絡變化";
|
||||||
|
"网络仍在变化,重新计算环境检测等待时间" = "網絡仍在變化,重新計算環境檢測等待時間";
|
||||||
|
"等待 Wi-Fi 连接稳定后检测" = "等待 Wi-Fi 連接穩定後檢測";
|
||||||
|
"延时检测已被更新的网络事件取消" = "延時檢測已被更新的網絡事件取消";
|
||||||
|
"稳定等待结束后仍未连接 Wi-Fi,提示检查代理" = "穩定等待結束後仍未連接 Wi-Fi,提示檢查代理";
|
||||||
|
"开始后台环境检测" = "開始後台環境檢測";
|
||||||
|
"已有环境检测运行,1 秒后重试" = "已有環境檢測運行,1 秒後重試";
|
||||||
|
"后台环境检测完成" = "後台環境檢測完成";
|
||||||
|
"环境检测连续被占用,本次不重复弹窗" = "環境檢測連續被佔用,本次不重復彈窗";
|
||||||
|
"用户点击实时定位" = "用戶點擊實時定位";
|
||||||
|
"MapKit 蓝点尚不可用,启动 CLLocationManager 兜底" = "MapKit 藍點尚不可用,啓動 CLLocationManager 兜底";
|
||||||
|
"MapKit 蓝点抢先完成请求,取消 CLLocationManager 兜底" = "MapKit 藍點搶先完成請求,取消 CLLocationManager 兜底";
|
||||||
|
"虚拟定位开启后的 MapKit 蓝点标准判定" = "虛擬定位開啓後的 MapKit 藍點標準判定";
|
||||||
|
"登记实时定位请求上下文" = "登記實時定位請求上下文";
|
||||||
|
"复用进行中的 CLLocationManager 请求并更新意图上下文" = "復用進行中的 CLLocationManager 請求並更新意圖上下文";
|
||||||
|
"CLLocationManager 兜底未返回坐标" = "CLLocationManager 兜底未返回坐標";
|
||||||
|
"丢弃 CLLocationManager 结果:任务已取消或蓝点已抢先完成" = "丟棄 CLLocationManager 結果:任務已取消或藍點已搶先完成";
|
||||||
|
"实时定位坐标完成标准判断并提交到地图" = "實時定位坐標完成標準判斷並提交到地圖";
|
||||||
|
"MKLocalSearch 返回地点结果" = "MKLocalSearch 返回地點結果";
|
||||||
|
"反向地理编码网络失败,准备重试" = "反向地理編碼網絡失敗,準備重試";
|
||||||
|
"反向地理编码失败" = "反向地理編碼失敗";
|
||||||
|
"获得搜索结果" = "獲得搜索結果";
|
||||||
|
"点击收藏点并回显到地图" = "點擊收藏點並回顯到地圖";
|
||||||
|
"忽略重复 CLLocationManager 请求" = "忽略重復 CLLocationManager 請求";
|
||||||
|
"CLLocationManager 请求入口" = "CLLocationManager 請求入口";
|
||||||
|
"授权状态不允许定位" = "授權狀態不允許定位";
|
||||||
|
"创建 CLLocationManager 请求" = "創建 CLLocationManager 請求";
|
||||||
|
"请求前台定位授权" = "請求前台定位授權";
|
||||||
|
"定位授权状态变化" = "定位授權狀態變化";
|
||||||
|
"CLLocationManager 返回样本批次" = "CLLocationManager 返回樣本批次";
|
||||||
|
"本批次没有可完成当前请求的样本" = "本批次沒有可完成當前請求的樣本";
|
||||||
|
"接受 CLLocationManager 样本并完成请求" = "接受 CLLocationManager 樣本並完成請求";
|
||||||
|
"定位回调失败" = "定位回調失敗";
|
||||||
|
"跳过 CLLocationManager 缓存样本" = "跳過 CLLocationManager 緩存樣本";
|
||||||
|
"等待定位授权超时" = "等待定位授權超時";
|
||||||
|
"单次定位超时,切换持续定位" = "單次定位超時,切換持續定位";
|
||||||
|
"持续定位超时" = "持續定位超時";
|
||||||
|
"开始 CLLocationManager 单次定位" = "開始 CLLocationManager 單次定位";
|
||||||
|
"开始 CLLocationManager 持续定位兜底" = "開始 CLLocationManager 持續定位兜底";
|
||||||
|
"CLLocationManager 请求完成" = "CLLocationManager 請求完成";
|
||||||
|
"CLLocationManager 请求结束但没有坐标" = "CLLocationManager 請求結束但沒有坐標";
|
||||||
|
"使用 CLLocationManager 新鲜缓存" = "使用 CLLocationManager 新鮮緩存";
|
||||||
|
"实时定位按钮直接使用 MapKit 蓝点缓存" = "實時定位按鈕直接使用 MapKit 藍點緩存";
|
||||||
|
"主页收到待处理请求所需的 MapKit 蓝点回调" = "主頁收到待處理請求所需的 MapKit 藍點回調";
|
||||||
|
"主页收到 CLLocationManager 兜底坐标" = "主頁收到 CLLocationManager 兜底坐標";
|
||||||
|
"原始实时定位坐标" = "原始實時定位坐標";
|
||||||
|
"检查 CLLocationManager 样本" = "檢查 CLLocationManager 樣本";
|
||||||
|
"拒绝 CLLocationManager 样本:坐标或精度无效" = "拒絕 CLLocationManager 樣本:坐標或精度無效";
|
||||||
|
"MapKit 蓝点更新但 location 为空" = "MapKit 藍點更新但 location 為空";
|
||||||
|
"拒绝 MapKit 蓝点样本:坐标无效" = "拒絕 MapKit 藍點樣本:坐標無效";
|
||||||
|
"拒绝 MapKit 蓝点样本:水平精度无效" = "拒絕 MapKit 藍點樣本:水平精度無效";
|
||||||
|
"音频中断恢复" = "音頻中斷恢復";
|
||||||
|
"音频会话失败" = "音頻會話失敗";
|
||||||
|
"无法创建静音音频缓冲区" = "無法創建靜音音頻緩衝區";
|
||||||
|
"引擎启动失败" = "引擎啓動失敗";
|
||||||
|
"后台保活已启动(静音音频)" = "後台保活已啓動(靜音音頻)";
|
||||||
|
"后台保活已停止" = "後台保活已停止";
|
||||||
|
"保存当前图钉失败" = "保存當前圖釘失敗";
|
||||||
|
"存储缩放" = "存儲縮放";
|
||||||
|
"旧坐标数据迁移完成" = "舊坐標數據遷移完成";
|
||||||
|
"远程版本配置加载成功" = "遠程版本配置加載成功";
|
||||||
|
"版本配置源不可用,尝试下一地址" = "版本配置源不可用,嘗試下一地址";
|
||||||
|
"版本配置加载失败,继续使用内置配置" = "版本配置加載失敗,繼續使用內置配置";
|
||||||
|
"版本说明源不可用,尝试下一地址" = "版本說明源不可用,嘗試下一地址";
|
||||||
|
"版本说明加载失败,将使用最新 Release 页面" = "版本說明加載失敗,將使用最新 Release 頁面";
|
||||||
|
"复用设备钥匙串中的 CA" = "復用設備鑰匙串中的 CA";
|
||||||
|
"旧 CA 已迁移到设备钥匙串" = "舊 CA 已遷移到設備鑰匙串";
|
||||||
|
"已生成并保存设备专属 CA" = "已生成並保存設備專屬 CA";
|
||||||
|
"已删除设备 CA,等待重新生成" = "已刪除設備 CA,等待重新生成";
|
||||||
|
"钥匙串中的 CA 无效,准备回退" = "鑰匙串中的 CA 無效,準備回退";
|
||||||
|
"删除旧 CA 文件失败,将在下次启动重试" = "刪除舊 CA 文件失敗,將在下次啓動重試";
|
||||||
|
"无法解析 CA 证书 PEM" = "無法解析 CA 證書 PEM";
|
||||||
|
"无法创建 SecTrust" = "無法創建 SecTrust";
|
||||||
|
"SecTrust 评估返回错误" = "SecTrust 評估返回錯誤";
|
||||||
|
"CA 证书已被系统信任" = "CA 證書已被系統信任";
|
||||||
|
"CA 证书未被系统信任" = "CA 證書未被系統信任";
|
||||||
|
"调用 Go Core 生成 CA" = "調用 Go Core 生成 CA";
|
||||||
|
"Go Core CA 生成成功" = "Go Core CA 生成成功";
|
||||||
|
"本地证书服务已在运行" = "本地證書服務已在運行";
|
||||||
|
"调用 Go Core 启动本地证书服务" = "調用 Go Core 啓動本地證書服務";
|
||||||
|
"本地证书服务启动成功" = "本地證書服務啓動成功";
|
||||||
|
"停止本地证书服务" = "停止本地證書服務";
|
||||||
|
"开始第三方代理连接检测" = "開始第三方代理連接檢測";
|
||||||
|
"第三方代理连接检测通过" = "第三方代理連接檢測通過";
|
||||||
|
"第三方代理连接检测失败" = "第三方代理連接檢測失敗";
|
||||||
|
"关闭代理前已同步关闭虚拟定位" = "關閉代理前已同步關閉虛擬定位";
|
||||||
|
"切换 APP 模式前无法清除第三方坐标" = "切換 APP 模式前無法清除第三方坐標";
|
||||||
|
"设置页第三方连接检测通过" = "設置頁第三方連接檢測通過";
|
||||||
|
"设置页第三方连接检测失败" = "設置頁第三方連接檢測失敗";
|
||||||
|
"地图实际显示坐标" = "地圖實際顯示坐標";
|
||||||
|
"App回到前台" = "App回到前台";
|
||||||
|
"App已确认地图标准" = "App已確認地圖標準";
|
||||||
|
"CLLocationManager请求中" = "CLLocationManager請求中";
|
||||||
|
"Go proxy 启动失败" = "Go proxy 啓動失敗";
|
||||||
|
"MapKit地图蓝点" = "MapKit地圖藍點";
|
||||||
|
"MapKit蓝点新样本" = "MapKit藍點新樣本";
|
||||||
|
"MapKit蓝点缓存" = "MapKit藍點緩存";
|
||||||
|
"SSID可读取" = "SSID可讀取";
|
||||||
|
"WLOC写入标准" = "WLOC寫入標準";
|
||||||
|
"WLOC目标标准" = "WLOC目標標準";
|
||||||
|
"Wi-Fi接口" = "Wi-Fi接口";
|
||||||
|
"intent选点revision" = "intent選點revision";
|
||||||
|
"manager请求中" = "manager請求中";
|
||||||
|
"上下文存在" = "上下文存在";
|
||||||
|
"事件原因" = "事件原因";
|
||||||
|
"任务已取消" = "任務已取消";
|
||||||
|
"任务已存在" = "任務已存在";
|
||||||
|
"使用兜底" = "使用兜底";
|
||||||
|
"保存收藏" = "保存收藏";
|
||||||
|
"保存数据包含" = "保存數據包含";
|
||||||
|
"保持未启用" = "保持未啓用";
|
||||||
|
"保留原第三方坐标" = "保留原第三方坐標";
|
||||||
|
"保留已启用状态" = "保留已啓用狀態";
|
||||||
|
"保留标准" = "保留標準";
|
||||||
|
"修正兜底标准" = "修正兜底標準";
|
||||||
|
"内存缓存存在" = "內存緩存存在";
|
||||||
|
"初始坐标来源" = "初始坐標來源";
|
||||||
|
"初始来源" = "初始來源";
|
||||||
|
"初始阶段" = "初始階段";
|
||||||
|
"判定规则" = "判定規則";
|
||||||
|
"原兜底来源" = "原兜底來源";
|
||||||
|
"原因" = "原因";
|
||||||
|
"取值字段" = "取值字段";
|
||||||
|
"可显示不再提醒" = "可顯示不再提醒";
|
||||||
|
"启动代理…" = "啓動代理…";
|
||||||
|
"命中林士街" = "命中林士街";
|
||||||
|
"固定锚点" = "固定錨點";
|
||||||
|
"固定锚点查询超过5秒" = "固定錨點查詢超過5秒";
|
||||||
|
"固定锚点查询返回空结果" = "固定錨點查詢返回空結果";
|
||||||
|
"国内标准(GCJ-02)" = "國內標準(GCJ-02)";
|
||||||
|
"国内转换区域" = "國內轉換區域";
|
||||||
|
"国外非转换区域" = "國外非轉換區域";
|
||||||
|
"国际标准(WGS-84)" = "國際標準(WGS-84)";
|
||||||
|
"国际标准(WGS-84)+国内标准(GCJ-02)" = "國際標準(WGS-84)+國內標準(GCJ-02)";
|
||||||
|
"图钉已按新类型重设" = "圖釘已按新類型重設";
|
||||||
|
"地图取值字段" = "地圖取值字段";
|
||||||
|
"地图标准" = "地圖標準";
|
||||||
|
"坐标有效" = "坐標有效";
|
||||||
|
"坐标标准" = "坐標標準";
|
||||||
|
"垂直精度米" = "垂直精度米";
|
||||||
|
"基础校验通过" = "基礎校驗通過";
|
||||||
|
"处理建议" = "處理建議";
|
||||||
|
"失败时提示" = "失敗時提示";
|
||||||
|
"实时定位存在" = "實時定位存在";
|
||||||
|
"实时定位服务区域" = "實時定位服務區域";
|
||||||
|
"客户端模式" = "客戶端模式";
|
||||||
|
"尝试" = "嘗試";
|
||||||
|
"已取消位置更新" = "已取消位置更新";
|
||||||
|
"已恢复真实定位" = "已恢復真實定位";
|
||||||
|
"已清理" = "已清理";
|
||||||
|
"已重置" = "已重置";
|
||||||
|
"开启" = "開啓";
|
||||||
|
"异步地理编码" = "異步地理編碼";
|
||||||
|
"当前地图标准" = "當前地圖標準";
|
||||||
|
"当前客户端" = "當前客戶端";
|
||||||
|
"当前标准" = "當前標準";
|
||||||
|
"当前选点revision" = "當前選點revision";
|
||||||
|
"恢复状态" = "恢復狀態";
|
||||||
|
"批次索引" = "批次索引";
|
||||||
|
"持久化字段" = "持久化字段";
|
||||||
|
"授权状态" = "授權狀態";
|
||||||
|
"授权状态rawValue" = "授權狀態rawValue";
|
||||||
|
"探测失败原因" = "探測失敗原因";
|
||||||
|
"搜索结果" = "搜索結果";
|
||||||
|
"操作" = "操作";
|
||||||
|
"新类型" = "新類型";
|
||||||
|
"无已保存" = "無已保存";
|
||||||
|
"无法判定" = "無法判定";
|
||||||
|
"无法启动本地证书服务" = "無法啓動本地證書服務";
|
||||||
|
"无法生成本地证书" = "無法生成本地證書";
|
||||||
|
"日志策略" = "日誌策略";
|
||||||
|
"旧类型" = "舊類型";
|
||||||
|
"显示坐标字段" = "顯示坐標字段";
|
||||||
|
"最低版本" = "最低版本";
|
||||||
|
"最新版本" = "最新版本";
|
||||||
|
"最终标准" = "最終標準";
|
||||||
|
"最终阶段" = "最終階段";
|
||||||
|
"有坐标" = "有坐標";
|
||||||
|
"有效样本数" = "有效樣本數";
|
||||||
|
"有缓存" = "有緩存";
|
||||||
|
"未知错误" = "未知錯誤";
|
||||||
|
"本地 CA 证书或私钥无效" = "本地 CA 證書或私鑰無效";
|
||||||
|
"无法写入设备钥匙串(%d)" = "無法寫入設備鑰匙串(%d)";
|
||||||
|
"来源" = "來源";
|
||||||
|
"来源类型" = "來源類型";
|
||||||
|
"林士街" = "林士街";
|
||||||
|
"查看诊断日志" = "查看診斷日誌";
|
||||||
|
"样本年龄秒" = "樣本年齡秒";
|
||||||
|
"样本数" = "樣本數";
|
||||||
|
"样本时间" = "樣本時間";
|
||||||
|
"检查范围" = "檢查範圍";
|
||||||
|
"检测前标准" = "檢測前標準";
|
||||||
|
"模块拦截、MITM、代理/VPN连接" = "模塊攔截、MITM、代理/VPN連接";
|
||||||
|
"次数" = "次數";
|
||||||
|
"每次开启首次或判定变化" = "每次開啓首次或判定變化";
|
||||||
|
"水平精度米" = "水平精度米";
|
||||||
|
"活动requestID" = "活動requestID";
|
||||||
|
"活动阶段" = "活動階段";
|
||||||
|
"海拔米" = "海拔米";
|
||||||
|
"满足当前请求时间窗" = "滿足當前請求時間窗";
|
||||||
|
"点击实时定位" = "點擊實時定位";
|
||||||
|
"目标所在区域" = "目標所在區域";
|
||||||
|
"确认标准" = "確認標準";
|
||||||
|
"社区征集客户端数" = "社區徵集客戶端數";
|
||||||
|
"等待秒数" = "等待秒數";
|
||||||
|
"系统缓存存在" = "系統緩存存在";
|
||||||
|
"结果" = "結果";
|
||||||
|
"结果数" = "結果數";
|
||||||
|
"结果来源" = "結果來源";
|
||||||
|
"缓存" = "緩存";
|
||||||
|
"缓存上限秒" = "緩存上限秒";
|
||||||
|
"缓存时效通过" = "緩存時效通過";
|
||||||
|
"缩放米" = "縮放米";
|
||||||
|
"网络可用" = "網絡可用";
|
||||||
|
"耗时毫秒" = "耗時毫秒";
|
||||||
|
"蓝点回调更接近" = "藍點回調更接近";
|
||||||
|
"蓝点缓存" = "藍點緩存";
|
||||||
|
"蓝点缓存存在" = "藍點緩存存在";
|
||||||
|
"蓝点距GCJ目标米" = "藍點距GCJ目標米";
|
||||||
|
"蓝点距WGS目标米" = "藍點距WGS目標米";
|
||||||
|
"触发原因" = "觸發原因";
|
||||||
|
"诊断位置" = "診斷位置";
|
||||||
|
"请求动作" = "請求動作";
|
||||||
|
"超时毫秒" = "超時毫秒";
|
||||||
|
"输入坐标标准" = "輸入坐標標準";
|
||||||
|
"连接状态" = "連接狀態";
|
||||||
|
"选点revision" = "選點revision";
|
||||||
|
"选点期间发生变化" = "選點期間發生變化";
|
||||||
|
"错误" = "錯誤";
|
||||||
|
"错误类型" = "錯誤類型";
|
||||||
|
"锚点" = "錨點";
|
||||||
|
"阶段" = "階段";
|
||||||
|
"首条名称" = "首條名稱";
|
||||||
|
"首条名称=林士街→GCJ-02,否则→WGS-84" = "首條名稱=林士街→GCJ-02,否則→WGS-84";
|
||||||
|
"默认国内标准" = "默認國內標準";
|
||||||
|
"证书下载地址无效" = "證書下載地址無效";
|
||||||
|
"启动代理失败: %@" = "啓動代理失敗: %@";
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
// Chinese source keys are extracted from the maintained English table so all
|
||||||
|
// shipped languages resolve the same keys. Keep both Chinese tables explicit:
|
||||||
|
// an absent table falls back to the development language (English), not the key.
|
||||||
|
let root = URL(fileURLWithPath: #filePath).deletingLastPathComponent().deletingLastPathComponent()
|
||||||
|
let resources = root.appendingPathComponent("Resources")
|
||||||
|
let english = try String(
|
||||||
|
contentsOf: resources.appendingPathComponent("en.lproj/Localizable.strings"),
|
||||||
|
encoding: .utf8
|
||||||
|
)
|
||||||
|
let pattern = try NSRegularExpression(
|
||||||
|
pattern: #"^"((?:\\.|[^"\\])*)"\s*=\s*"(?:\\.|[^"\\])*";"#,
|
||||||
|
options: .anchorsMatchLines
|
||||||
|
)
|
||||||
|
let range = NSRange(english.startIndex..<english.endIndex, in: english)
|
||||||
|
let keys = pattern.matches(in: english, range: range).compactMap { match -> String? in
|
||||||
|
guard let keyRange = Range(match.range(at: 1), in: english) else { return nil }
|
||||||
|
return String(english[keyRange])
|
||||||
|
}
|
||||||
|
precondition(!keys.isEmpty && Set(keys).count == keys.count, "English keys must be nonempty and unique")
|
||||||
|
|
||||||
|
for (locale, transform) in [("zh-Hans", false), ("zh-Hant", true)] {
|
||||||
|
var lines = ["/* Generated from en.lproj/Localizable.strings by Scripts/generate-chinese-localizations.swift. */"]
|
||||||
|
lines += keys.map { key in
|
||||||
|
let value = transform
|
||||||
|
? (key.applyingTransform(StringTransform(rawValue: "Hans-Hant"), reverse: false) ?? key)
|
||||||
|
: key
|
||||||
|
return "\"\(key)\" = \"\(value)\";"
|
||||||
|
}
|
||||||
|
let path = resources.appendingPathComponent("\(locale).lproj/Localizable.strings")
|
||||||
|
try FileManager.default.createDirectory(at: path.deletingLastPathComponent(), withIntermediateDirectories: true)
|
||||||
|
try (lines.joined(separator: "\n") + "\n").write(to: path, atomically: true, encoding: .utf8)
|
||||||
|
}
|
||||||
@@ -49,3 +49,4 @@ ${COMMITS}
|
|||||||
EOF
|
EOF
|
||||||
|
|
||||||
echo "Generated $OUTPUT from $RANGE"
|
echo "Generated $OUTPUT from $RANGE"
|
||||||
|
echo "Before tagging, add docs/releases/${VERSION}.en.md and docs/releases/${VERSION}.zh-Hant.md."
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Chooses the app language from the first preferred language only. iOS normally
|
||||||
|
/// scans the whole preference list, which can select Chinese when Italian is first
|
||||||
|
/// and Chinese is second; this app intentionally falls back to English instead.
|
||||||
|
enum AppLocalization {
|
||||||
|
static func identifier(for preferredLanguages: [String]) -> String {
|
||||||
|
guard let first = preferredLanguages.first,
|
||||||
|
Locale(identifier: first).languageCode == "zh" else {
|
||||||
|
return "en"
|
||||||
|
}
|
||||||
|
return Bundle.preferredLocalizations(
|
||||||
|
from: ["zh-Hans", "zh-Hant"], forPreferences: [first]
|
||||||
|
).first ?? "zh-Hans"
|
||||||
|
}
|
||||||
|
|
||||||
|
static let identifier = identifier(for: Locale.preferredLanguages)
|
||||||
|
static let locale = Locale(identifier: identifier)
|
||||||
|
|
||||||
|
// An explicit .lproj bundle also covers String(localized:) calls made outside
|
||||||
|
// SwiftUI views, where the view's locale environment is not available.
|
||||||
|
private static let localizedBundle: Bundle = {
|
||||||
|
guard let path = Bundle.main.path(forResource: identifier, ofType: "lproj"),
|
||||||
|
let bundle = Bundle(path: path) else {
|
||||||
|
return .main
|
||||||
|
}
|
||||||
|
return bundle
|
||||||
|
}()
|
||||||
|
|
||||||
|
static func string(_ value: String.LocalizationValue) -> String {
|
||||||
|
String(localized: value, bundle: localizedBundle)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,7 +23,7 @@ struct AppRemoteConfiguration: Decodable, Equatable {
|
|||||||
let communityPromptClients: [String]
|
let communityPromptClients: [String]
|
||||||
|
|
||||||
static let fallback = AppRemoteConfiguration(
|
static let fallback = AppRemoteConfiguration(
|
||||||
latestVersion: "1.0.5",
|
latestVersion: "1.0.8",
|
||||||
minimumSupportedVersion: "1.0.0",
|
minimumSupportedVersion: "1.0.0",
|
||||||
communityPromptClients: [
|
communityPromptClients: [
|
||||||
ThirdPartyProxyClient.surge.rawValue,
|
ThirdPartyProxyClient.surge.rawValue,
|
||||||
@@ -153,11 +153,13 @@ enum AppRemoteConfigurationService {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
static func fetchReleaseNotes(version: String) async -> String? {
|
static func fetchReleaseNotes(
|
||||||
|
version: String, language: String = AppLocalization.identifier
|
||||||
|
) async -> String? {
|
||||||
let session = makeSession()
|
let session = makeSession()
|
||||||
defer { session.finishTasksAndInvalidate() }
|
defer { session.finishTasksAndInvalidate() }
|
||||||
|
|
||||||
for url in releaseNotesURLs(version: version) {
|
for url in releaseNotesURLs(version: version, language: language) {
|
||||||
var request = URLRequest(url: url)
|
var request = URLRequest(url: url)
|
||||||
request.timeoutInterval = 1.5
|
request.timeoutInterval = 1.5
|
||||||
|
|
||||||
@@ -166,7 +168,7 @@ enum AppRemoteConfigurationService {
|
|||||||
guard let httpResponse = response as? HTTPURLResponse,
|
guard let httpResponse = response as? HTTPURLResponse,
|
||||||
httpResponse.statusCode == 200,
|
httpResponse.statusCode == 200,
|
||||||
let markdown = String(data: data, encoding: .utf8),
|
let markdown = String(data: data, encoding: .utf8),
|
||||||
let summary = releaseNotesSummary(markdown) else {
|
let summary = releaseNotesSummary(markdown, language: language) else {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return summary
|
return summary
|
||||||
@@ -185,8 +187,17 @@ enum AppRemoteConfigurationService {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
static func releaseNotesURLs(version: String) -> [URL] {
|
static func releaseNotesURLs(
|
||||||
let path = "https://raw.githubusercontent.com/xweiba/location-spoofer/main/docs/releases/v\(version).md"
|
version: String, language: String = AppLocalization.identifier
|
||||||
|
) -> [URL] {
|
||||||
|
// The unsuffixed archive remains Simplified Chinese for older clients.
|
||||||
|
let suffix: String
|
||||||
|
switch language {
|
||||||
|
case "zh-Hans": suffix = ""
|
||||||
|
case "zh-Hant": suffix = ".zh-Hant"
|
||||||
|
default: suffix = ".en"
|
||||||
|
}
|
||||||
|
let path = "https://raw.githubusercontent.com/xweiba/location-spoofer/main/docs/releases/v\(version)\(suffix).md"
|
||||||
return [
|
return [
|
||||||
URL(string: "https://gh-proxy.org/\(path)")!,
|
URL(string: "https://gh-proxy.org/\(path)")!,
|
||||||
URL(string: path)!
|
URL(string: path)!
|
||||||
@@ -202,12 +213,14 @@ enum AppRemoteConfigurationService {
|
|||||||
return URLSession(configuration: sessionConfiguration)
|
return URLSession(configuration: sessionConfiguration)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func releaseNotesSummary(_ markdown: String) -> String? {
|
static func releaseNotesSummary(_ markdown: String, language: String) -> String? {
|
||||||
var items: [String] = []
|
var items: [String] = []
|
||||||
var inMainUpdates = false
|
var inMainUpdates = false
|
||||||
for rawLine in markdown.components(separatedBy: .newlines) {
|
for rawLine in markdown.components(separatedBy: .newlines) {
|
||||||
let line = rawLine.trimmingCharacters(in: .whitespacesAndNewlines)
|
let line = rawLine.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
if line == "## 主要更新" {
|
let heading = language == "zh-Hans" || language == "zh-Hant"
|
||||||
|
? "## 主要更新" : "## Highlights"
|
||||||
|
if line == heading {
|
||||||
inMainUpdates = true
|
inMainUpdates = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ enum CertificateAuthorityStoreError: LocalizedError {
|
|||||||
|
|
||||||
var errorDescription: String? {
|
var errorDescription: String? {
|
||||||
switch self {
|
switch self {
|
||||||
case .invalidAuthority: return String(localized: "本地 CA 证书或私钥无效")
|
case .invalidAuthority: return AppLocalization.string("本地 CA 证书或私钥无效")
|
||||||
case let .keychain(status): return String(localized: "无法写入设备钥匙串(\(status))")
|
case let .keychain(status): return AppLocalization.string("无法写入设备钥匙串(\(status))")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ enum CoreBridgeError: LocalizedError {
|
|||||||
|
|
||||||
var errorDescription: String? {
|
var errorDescription: String? {
|
||||||
switch self {
|
switch self {
|
||||||
case .generationFailed: return String(localized: "无法生成本地证书")
|
case .generationFailed: return AppLocalization.string("无法生成本地证书")
|
||||||
case .serverStartFailed: return String(localized: "无法启动本地证书服务")
|
case .serverStartFailed: return AppLocalization.string("无法启动本地证书服务")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ enum ProxyRuntimeMode: String, CaseIterable, Codable, Identifiable {
|
|||||||
|
|
||||||
var displayName: String {
|
var displayName: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .localWiFi: return String(localized: "APP模式")
|
case .localWiFi: return AppLocalization.string("APP模式")
|
||||||
case .thirdParty: return String(localized: "第三方代理模式")
|
case .thirdParty: return AppLocalization.string("第三方代理模式")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,13 +58,13 @@ struct RuntimeLogEntry: Codable, Identifiable, Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func localizedDiagnosticText(_ text: String) -> String {
|
private func localizedDiagnosticText(_ text: String) -> String {
|
||||||
String(localized: String.LocalizationValue(text))
|
AppLocalization.string(String.LocalizationValue(text))
|
||||||
}
|
}
|
||||||
|
|
||||||
private func localizedDiagnosticValue(_ value: String) -> String {
|
private func localizedDiagnosticValue(_ value: String) -> String {
|
||||||
switch value {
|
switch value {
|
||||||
case "true": return String(localized: "是")
|
case "true": return AppLocalization.string("是")
|
||||||
case "false": return String(localized: "否")
|
case "false": return AppLocalization.string("否")
|
||||||
default: return localizedDiagnosticText(value)
|
default: return localizedDiagnosticText(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,25 +25,25 @@ enum ThirdPartyProxyError: LocalizedError, Equatable {
|
|||||||
var errorDescription: String? {
|
var errorDescription: String? {
|
||||||
switch self {
|
switch self {
|
||||||
case .invalidResponse:
|
case .invalidResponse:
|
||||||
return String(localized: "第三方代理返回了无法识别的数据")
|
return AppLocalization.string("第三方代理返回了无法识别的数据")
|
||||||
case .moduleNotIntercepted:
|
case .moduleNotIntercepted:
|
||||||
return String(localized: "请求未被第三方代理模块拦截,请检查模块、MITM 和代理连接")
|
return AppLocalization.string("请求未被第三方代理模块拦截,请检查模块、MITM 和代理连接")
|
||||||
case .rejected(let message):
|
case .rejected(let message):
|
||||||
return message
|
return message
|
||||||
case .coordinateMismatch:
|
case .coordinateMismatch:
|
||||||
return String(localized: "第三方代理保存的坐标与当前选点不一致")
|
return AppLocalization.string("第三方代理保存的坐标与当前选点不一致")
|
||||||
case .network(let message):
|
case .network(let message):
|
||||||
return String(localized: "第三方代理请求失败:\(message)")
|
return AppLocalization.string("第三方代理请求失败:\(message)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var recoverySuggestion: String {
|
var recoverySuggestion: String {
|
||||||
return String(localized: "检查模块、MITM、证书和代理/VPN连接")
|
return AppLocalization.string("检查模块、MITM、证书和代理/VPN连接")
|
||||||
}
|
}
|
||||||
|
|
||||||
static func recoverySuggestion(for error: Error) -> String {
|
static func recoverySuggestion(for error: Error) -> String {
|
||||||
(error as? Self)?.recoverySuggestion
|
(error as? Self)?.recoverySuggestion
|
||||||
?? String(localized: "检查模块、MITM、证书和代理/VPN连接")
|
?? AppLocalization.string("检查模块、MITM、证书和代理/VPN连接")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +105,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 ?? String(localized: "第三方代理拒绝保存坐标"))
|
throw ThirdPartyProxyError.rejected(response.error ?? AppLocalization.string("第三方代理拒绝保存坐标"))
|
||||||
}
|
}
|
||||||
guard let latitude = response.latitude,
|
guard let latitude = response.latitude,
|
||||||
let longitude = response.longitude,
|
let longitude = response.longitude,
|
||||||
@@ -126,7 +126,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 ?? String(localized: "第三方代理清除坐标失败"))
|
throw ThirdPartyProxyError.rejected(response.error ?? AppLocalization.string("第三方代理清除坐标失败"))
|
||||||
}
|
}
|
||||||
activeSettings = nil
|
activeSettings = nil
|
||||||
connectionState = .connected(active: false)
|
connectionState = .connected(active: false)
|
||||||
@@ -142,7 +142,7 @@ final class ThirdPartyProxyManager: ObservableObject {
|
|||||||
if response.error?.contains("无已保存") == true {
|
if response.error?.contains("无已保存") == true {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
throw ThirdPartyProxyError.rejected(response.error ?? String(localized: "第三方代理查询失败"))
|
throw ThirdPartyProxyError.rejected(response.error ?? AppLocalization.string("第三方代理查询失败"))
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum Action {
|
private enum Action {
|
||||||
@@ -153,7 +153,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(String(localized: "已有第三方代理请求正在执行"))
|
throw ThirdPartyProxyError.rejected(AppLocalization.string("已有第三方代理请求正在执行"))
|
||||||
}
|
}
|
||||||
isRequesting = true
|
isRequesting = true
|
||||||
defer { isRequesting = false }
|
defer { isRequesting = false }
|
||||||
@@ -229,7 +229,7 @@ enum ThirdPartyProxyClient: String, CaseIterable, Identifiable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var verificationText: String? {
|
var verificationText: String? {
|
||||||
self == .shadowrocket ? nil : String(localized: "配置已提供,尚未验证")
|
self == .shadowrocket ? nil : AppLocalization.string("配置已提供,尚未验证")
|
||||||
}
|
}
|
||||||
|
|
||||||
var moduleFileName: String {
|
var moduleFileName: String {
|
||||||
|
|||||||
@@ -29,14 +29,14 @@ enum VerificationResult: Equatable, Identifiable {
|
|||||||
/// Localized, user-facing title. `id` stays stable for routing/equality.
|
/// Localized, user-facing title. `id` stays stable for routing/equality.
|
||||||
var localizedTitle: String {
|
var localizedTitle: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .success: return String(localized: "成功")
|
case .success: return AppLocalization.string("成功")
|
||||||
case .proxyNotRunning: return String(localized: "代理未运行")
|
case .proxyNotRunning: return AppLocalization.string("代理未运行")
|
||||||
case .verificationInProgress: return String(localized: "已有验证正在进行")
|
case .verificationInProgress: return AppLocalization.string("已有验证正在进行")
|
||||||
case .verificationSuperseded: return String(localized: "验证已被新位置取代")
|
case .verificationSuperseded: return AppLocalization.string("验证已被新位置取代")
|
||||||
case .certNotTrusted: return String(localized: "证书未信任")
|
case .certNotTrusted: return AppLocalization.string("证书未信任")
|
||||||
case .wifiProxyNotConfigured: return String(localized: "WiFi代理未配置")
|
case .wifiProxyNotConfigured: return AppLocalization.string("WiFi代理未配置")
|
||||||
case .coordinateWriteFailed: return String(localized: "坐标写入失败")
|
case .coordinateWriteFailed: return AppLocalization.string("坐标写入失败")
|
||||||
case .patchFailed: return String(localized: "改写验证失败")
|
case .patchFailed: return AppLocalization.string("改写验证失败")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import XCTest
|
||||||
|
@testable import PaopaoLocationSpoofer
|
||||||
|
|
||||||
|
final class AppLocalizationTests: XCTestCase {
|
||||||
|
func testOnlyFirstPreferredLanguageSelectsTheAppLanguage() {
|
||||||
|
XCTAssertEqual(AppLocalization.identifier(for: ["it-IT", "zh-Hans-CN"]), "en")
|
||||||
|
XCTAssertEqual(AppLocalization.identifier(for: ["fr-FR", "zh-Hant-TW"]), "en")
|
||||||
|
XCTAssertEqual(AppLocalization.identifier(for: ["zh-Hans-CN", "it-IT"]), "zh-Hans")
|
||||||
|
XCTAssertEqual(AppLocalization.identifier(for: ["zh-Hant-TW", "en-US"]), "zh-Hant")
|
||||||
|
XCTAssertEqual(AppLocalization.identifier(for: []), "en")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -62,7 +62,7 @@ final class AppRemoteConfigurationTests: XCTestCase {
|
|||||||
func testFallbackMatchesCurrentProjectPolicy() {
|
func testFallbackMatchesCurrentProjectPolicy() {
|
||||||
let configuration = AppRemoteConfiguration.fallback
|
let configuration = AppRemoteConfiguration.fallback
|
||||||
|
|
||||||
XCTAssertEqual(configuration.latestVersion, "1.0.5")
|
XCTAssertEqual(configuration.latestVersion, "1.0.8")
|
||||||
XCTAssertEqual(configuration.minimumSupportedVersion, "1.0.0")
|
XCTAssertEqual(configuration.minimumSupportedVersion, "1.0.0")
|
||||||
XCTAssertFalse(configuration.requestsCommunityPrompt(for: .shadowrocket))
|
XCTAssertFalse(configuration.requestsCommunityPrompt(for: .shadowrocket))
|
||||||
for client in ThirdPartyProxyClient.allCases where client != .shadowrocket {
|
for client in ThirdPartyProxyClient.allCases where client != .shadowrocket {
|
||||||
@@ -77,8 +77,44 @@ final class AppRemoteConfigurationTests: XCTestCase {
|
|||||||
"raw.githubusercontent.com"
|
"raw.githubusercontent.com"
|
||||||
)
|
)
|
||||||
|
|
||||||
let releaseNotesURLs = AppRemoteConfigurationService.releaseNotesURLs(version: "1.0.5")
|
let releaseNotesURLs = AppRemoteConfigurationService.releaseNotesURLs(version: "1.0.8", language: "en")
|
||||||
XCTAssertEqual(releaseNotesURLs.first?.host, "gh-proxy.org")
|
XCTAssertEqual(releaseNotesURLs.first?.host, "gh-proxy.org")
|
||||||
XCTAssertEqual(releaseNotesURLs.last?.host, "raw.githubusercontent.com")
|
XCTAssertEqual(releaseNotesURLs.last?.host, "raw.githubusercontent.com")
|
||||||
|
XCTAssertTrue(releaseNotesURLs.last?.path.hasSuffix("/docs/releases/v1.0.8.en.md") == true)
|
||||||
|
XCTAssertTrue(AppRemoteConfigurationService.releaseNotesURLs(
|
||||||
|
version: "1.0.8", language: "zh-Hans"
|
||||||
|
).last?.path.hasSuffix("/docs/releases/v1.0.8.md") == true)
|
||||||
|
XCTAssertTrue(AppRemoteConfigurationService.releaseNotesURLs(
|
||||||
|
version: "1.0.8", language: "zh-Hant"
|
||||||
|
).last?.path.hasSuffix("/docs/releases/v1.0.8.zh-Hant.md") == true)
|
||||||
|
XCTAssertTrue(AppRemoteConfigurationService.releaseNotesURLs(
|
||||||
|
version: "1.0.8", language: "it"
|
||||||
|
).last?.path.hasSuffix("/docs/releases/v1.0.8.en.md") == true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testReleaseNotesSummaryUsesTheSelectedLanguageHeading() {
|
||||||
|
let english = "# v1.0.8\n\n## Highlights\n\n- English update\n\n## Installation"
|
||||||
|
let chinese = "# v1.0.8\n\n## 主要更新\n\n- 中文更新\n\n## 安装"
|
||||||
|
XCTAssertEqual(AppRemoteConfigurationService.releaseNotesSummary(english, language: "en"), "• English update")
|
||||||
|
XCTAssertEqual(AppRemoteConfigurationService.releaseNotesSummary(chinese, language: "zh-Hans"), "• 中文更新")
|
||||||
|
XCTAssertEqual(AppRemoteConfigurationService.releaseNotesSummary(chinese, language: "zh-Hant"), "• 中文更新")
|
||||||
|
XCTAssertNil(AppRemoteConfigurationService.releaseNotesSummary(chinese, language: "en"))
|
||||||
|
XCTAssertEqual(AppRemoteConfigurationService.releaseNotesSummary(english, language: "it"), "• English update")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testArchivedReleaseNotesParseInEachLanguage() throws {
|
||||||
|
let root = URL(fileURLWithPath: #filePath)
|
||||||
|
.deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent()
|
||||||
|
for (language, name) in [
|
||||||
|
("en", "v1.0.8.en.md"),
|
||||||
|
("zh-Hans", "v1.0.8.md"),
|
||||||
|
("zh-Hant", "v1.0.8.zh-Hant.md")
|
||||||
|
] {
|
||||||
|
let markdown = try String(
|
||||||
|
contentsOf: root.appendingPathComponent("docs/releases/\(name)"),
|
||||||
|
encoding: .utf8
|
||||||
|
)
|
||||||
|
XCTAssertNotNil(AppRemoteConfigurationService.releaseNotesSummary(markdown, language: language))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ final class ThirdPartyProxyManagerTests: XCTestCase {
|
|||||||
XCTAssertNil(ThirdPartyProxyClient.shadowrocket.verificationText)
|
XCTAssertNil(ThirdPartyProxyClient.shadowrocket.verificationText)
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
ThirdPartyProxyClient.surge.verificationText,
|
ThirdPartyProxyClient.surge.verificationText,
|
||||||
String(localized: "配置已提供,尚未验证")
|
AppLocalization.string("配置已提供,尚未验证")
|
||||||
)
|
)
|
||||||
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(
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ RUBY
|
|||||||
|
|
||||||
for key in \
|
for key in \
|
||||||
'环境信息' \
|
'环境信息' \
|
||||||
|
'复制 %@' \
|
||||||
|
'已复制 %@' \
|
||||||
'诊断日志' \
|
'诊断日志' \
|
||||||
'======== 代理验证测试 ========' \
|
'======== 代理验证测试 ========' \
|
||||||
'======== 第三方代理连接检测 ========' \
|
'======== 第三方代理连接检测 ========' \
|
||||||
@@ -44,7 +46,7 @@ grep -q 'localizedCategory' "$ROOT/Shared/RuntimeLog.swift" \
|
|||||||
|| fail "runtime log categories must be localized when rendered"
|
|| fail "runtime log categories must be localized when rendered"
|
||||||
grep -q 'localizedDetailsText' "$ROOT/App/DiagnosticsView.swift" \
|
grep -q 'localizedDetailsText' "$ROOT/App/DiagnosticsView.swift" \
|
||||||
|| fail "runtime log details must be localized in the diagnostics UI"
|
|| fail "runtime log details must be localized in the diagnostics UI"
|
||||||
grep -q 'String(localized: "诊断日志")' "$ROOT/App/BugReportView.swift" \
|
grep -q 'AppLocalization.string("诊断日志")' "$ROOT/App/BugReportView.swift" \
|
||||||
|| fail "generated bug reports must localize their diagnostic section"
|
|| fail "generated bug reports must localize their diagnostic section"
|
||||||
grep -q 'log(" " + e.localizedMessage)' "$ROOT/App/SetupCoordinator.swift" \
|
grep -q 'log(" " + e.localizedMessage)' "$ROOT/App/SetupCoordinator.swift" \
|
||||||
|| fail "bug-report verification logs must render stored messages in the active language"
|
|| fail "bug-report verification logs must render stored messages in the active language"
|
||||||
@@ -55,4 +57,43 @@ if grep -R -n 'raw.githubusercontent.com/Yu9191/wloc' \
|
|||||||
fail "deleted Yu9191/wloc repository must not remain a runtime dependency"
|
fail "deleted Yu9191/wloc repository must not remain a runtime dependency"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Check the real Foundation lookup path, not just the existence of locale files.
|
||||||
|
grep -q 'developmentLanguage: en' "$ROOT/project.yml" \
|
||||||
|
|| fail "English must be the development-language fallback"
|
||||||
|
grep -q ' - zh-Hant' "$ROOT/project.yml" \
|
||||||
|
|| fail "Traditional Chinese must be a supported region"
|
||||||
|
for locale in zh-Hans zh-Hant; do
|
||||||
|
plutil -lint "$ROOT/Resources/$locale.lproj/Localizable.strings" >/dev/null \
|
||||||
|
|| fail "$locale localization file is invalid"
|
||||||
|
plutil -lint "$ROOT/Resources/$locale.lproj/InfoPlist.strings" >/dev/null \
|
||||||
|
|| fail "$locale permission prompts are invalid"
|
||||||
|
done
|
||||||
|
|
||||||
|
ruby - "$STRINGS" "$ROOT/Resources/zh-Hans.lproj/Localizable.strings" \
|
||||||
|
"$ROOT/Resources/zh-Hant.lproj/Localizable.strings" <<'RUBY' || exit 1
|
||||||
|
paths = ARGV
|
||||||
|
keys = paths.map { |path| File.readlines(path).map { |line| line[/^"((?:\\.|[^"\\])*)"\s*=/, 1] }.compact }
|
||||||
|
abort "FAIL: Chinese tables must cover every English key" unless keys[1] == keys[0] && keys[2] == keys[0]
|
||||||
|
RUBY
|
||||||
|
|
||||||
|
grep -Fq '"虚拟定位" = "虚拟定位";' "$ROOT/Resources/zh-Hans.lproj/Localizable.strings" \
|
||||||
|
|| fail "Simplified Chinese must resolve the source key"
|
||||||
|
grep -Fq '"虚拟定位" = "虛擬定位";' "$ROOT/Resources/zh-Hant.lproj/Localizable.strings" \
|
||||||
|
|| fail "Traditional Chinese must use Traditional characters"
|
||||||
|
|
||||||
|
swift - <<'SWIFT' || fail "language selection differs from expected behavior"
|
||||||
|
import Foundation
|
||||||
|
let available = ["en", "zh-Hans", "zh-Hant"]
|
||||||
|
for (language, expected) in [
|
||||||
|
("zh-CN", "zh-Hans"), ("zh-TW", "zh-Hant"), ("zh-HK", "zh-Hant"),
|
||||||
|
("it-IT", "en"), ("fr-FR", "en")
|
||||||
|
] {
|
||||||
|
let selected = Bundle.preferredLocalizations(from: available, forPreferences: [language]).first
|
||||||
|
guard selected == expected else {
|
||||||
|
fputs("FAIL: \(language) selected \(selected ?? "nil")\n", stderr)
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SWIFT
|
||||||
|
|
||||||
echo "PASS: localization and diagnostic export contract"
|
echo "PASS: localization and diagnostic export contract"
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
ZH="$ROOT/README.md"
|
ZH="$ROOT/README.zh-CN.md"
|
||||||
EN="$ROOT/README.en.md"
|
EN="$ROOT/README.md"
|
||||||
fail() { echo "FAIL: $*" >&2; exit 1; }
|
fail() { echo "FAIL: $*" >&2; exit 1; }
|
||||||
|
|
||||||
grep -q 'iOS Location Service Research & Testing Framework' "$ZH" || fail "Chinese README positioning is missing"
|
grep -q 'iOS Location Service Research & Testing Framework' "$ZH" || fail "Chinese README positioning is missing"
|
||||||
@@ -49,7 +49,14 @@ done
|
|||||||
test "$(grep -c '^## ' "$ZH")" -eq "$(grep -c '^## ' "$EN")" \
|
test "$(grep -c '^## ' "$ZH")" -eq "$(grep -c '^## ' "$EN")" \
|
||||||
|| fail "Chinese and English README section counts must stay aligned"
|
|| fail "Chinese and English README section counts must stay aligned"
|
||||||
|
|
||||||
! grep -Eq '^## (许可证|License)$' "$ZH" "$EN" || fail "README must not claim a repository license"
|
test -s "$ROOT/LICENSE" || fail "root AGPL-3.0 license is missing"
|
||||||
|
grep -q "GNU AFFERO GENERAL PUBLIC LICENSE" "$ROOT/LICENSE" || fail "root license must be AGPL-3.0"
|
||||||
|
grep -q '^## 许可证$' "$ZH" || fail "Chinese README license section is missing"
|
||||||
|
grep -q '^## License$' "$EN" || fail "English README license section is missing"
|
||||||
|
grep -q 'ThirdParty/WlocScripts/THIRD_PARTY_NOTICES.md' "$ZH" || fail "Chinese README must retain third-party provenance"
|
||||||
|
grep -q 'ThirdParty/WlocScripts/THIRD_PARTY_NOTICES.md' "$EN" || fail "English README must retain third-party provenance"
|
||||||
|
grep -q 'README.zh-CN.md' "$EN" || fail "English README must link Chinese translation"
|
||||||
|
grep -q 'README.md' "$ZH" || fail "Chinese README must link English default"
|
||||||
grep -q '当前项目不支持在 Windows 上直接构建 iOS 应用' "$ZH" || fail "Chinese README must reject Windows source builds"
|
grep -q '当前项目不支持在 Windows 上直接构建 iOS 应用' "$ZH" || fail "Chinese README must reject Windows source builds"
|
||||||
grep -q 'Building the iOS app directly on Windows is not supported' "$EN" || fail "English README must reject Windows source builds"
|
grep -q 'Building the iOS app directly on Windows is not supported' "$EN" || fail "English README must reject Windows source builds"
|
||||||
grep -q 'docs/COMMUNITY_TUTORIALS.md' "$ZH" || fail "Chinese README must link the community tutorial submission guide"
|
grep -q 'docs/COMMUNITY_TUTORIALS.md' "$ZH" || fail "Chinese README must link the community tutorial submission guide"
|
||||||
@@ -70,7 +77,7 @@ grep -q '不得覆盖上述原图' "$ROOT/docs/COMMUNITY_TUTORIALS.md" \
|
|||||||
|| fail "annotated app assets must not replace categorized source screenshots"
|
|| fail "annotated app assets must not replace categorized source screenshots"
|
||||||
|
|
||||||
if grep -Rnw --include='*.md' --include='*.sh' \
|
if grep -Rnw --include='*.md' --include='*.sh' \
|
||||||
"$ROOT/build.sh" "$ROOT/README.md" "$ROOT/README.en.md" "$ROOT/docs" "$ROOT/Scripts" \
|
"$ROOT/build.sh" "$ROOT/README.md" "$ROOT/README.zh-CN.md" "$ROOT/docs" "$ROOT/Scripts" \
|
||||||
-e 'Impact'; then
|
-e 'Impact'; then
|
||||||
fail "documentation and build output must use the correct Impactor name"
|
fail "documentation and build output must use the correct Impactor name"
|
||||||
fi
|
fi
|
||||||
|
|||||||
Executable
+33
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
|
||||||
|
passed=0
|
||||||
|
failed=0
|
||||||
|
failed_scripts=()
|
||||||
|
|
||||||
|
for script in "$ROOT"/Tests/*_test.sh; do
|
||||||
|
name="$(basename "$script")"
|
||||||
|
echo "==> $name"
|
||||||
|
if bash "$script"; then
|
||||||
|
echo "PASS: $name"
|
||||||
|
passed=$((passed + 1))
|
||||||
|
else
|
||||||
|
echo "FAIL: $name"
|
||||||
|
failed_scripts+=("$name")
|
||||||
|
failed=$((failed + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "----------------------------------------"
|
||||||
|
echo "Contract tests: $passed passed / $failed failed"
|
||||||
|
if [ "$failed" -gt 0 ]; then
|
||||||
|
echo "Failed scripts:"
|
||||||
|
for name in "${failed_scripts[@]}"; do
|
||||||
|
echo " - $name"
|
||||||
|
done
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "PASS: all contract tests"
|
||||||
@@ -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 'String(localized: "APP模式")' "$MODE" || fail "localized APP mode display name is missing"
|
grep -q 'AppLocalization.string("APP模式")' "$MODE" || fail "localized APP mode display name is missing"
|
||||||
grep -q 'String(localized: "第三方代理模式")' "$MODE" || fail "localized third-party mode display name is missing"
|
grep -q 'AppLocalization.string("第三方代理模式")' "$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"
|
||||||
@@ -106,7 +106,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: String(localized: \"接口连接失败\")' "$SETUP")" -eq 1 \
|
test "$(grep -c 'title: AppLocalization.string(\"接口连接失败\")' "$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"
|
||||||
@@ -142,7 +142,7 @@ test -s "$ROOT/docs/onboarding-screenshots/shadowrocket/shadowrocket-module-impo
|
|||||||
grep -q 'presentSuccessfulOperationTip(.activation)' "$ROOT/App/MapHomeView.swift" || fail "third-party save must present the activation tip"
|
grep -q 'presentSuccessfulOperationTip(.activation)' "$ROOT/App/MapHomeView.swift" || fail "third-party save must present the activation tip"
|
||||||
grep -q 'presentSuccessfulOperationTip(.deactivation)' "$ROOT/App/MapHomeView.swift" || fail "third-party clear must present the deactivation tip"
|
grep -q 'presentSuccessfulOperationTip(.deactivation)' "$ROOT/App/MapHomeView.swift" || fail "third-party clear must present the deactivation tip"
|
||||||
grep -q 'if spoofState == .active' "$ROOT/App/MapHomeView.swift" || fail "manual help must follow the shared spoof state"
|
grep -q 'if spoofState == .active' "$ROOT/App/MapHomeView.swift" || fail "manual help must follow the shared spoof state"
|
||||||
grep -q 'MARKETING_VERSION: "1.0.7"' "$ROOT/project.yml" || fail "marketing version must be 1.0.7"
|
grep -q 'MARKETING_VERSION: "1.0.8"' "$ROOT/project.yml" || fail "marketing version must be 1.0.8"
|
||||||
grep -q 'CURRENT_PROJECT_VERSION: "8"' "$ROOT/project.yml" || fail "build version must be 8"
|
grep -q 'CURRENT_PROJECT_VERSION: "10"' "$ROOT/project.yml" || fail "build version must be 10"
|
||||||
|
|
||||||
echo "PASS: third-party proxy mode contract"
|
echo "PASS: third-party proxy mode contract"
|
||||||
|
|||||||
@@ -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.7"
|
assert config["latestVersion"] == "1.0.8"
|
||||||
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"]) == {
|
||||||
@@ -33,6 +33,15 @@ assert set(config["communityPromptClients"]) == {
|
|||||||
}
|
}
|
||||||
PY
|
PY
|
||||||
|
|
||||||
|
LATEST_VERSION="$(python3 -c 'import json, sys; print(json.load(open(sys.argv[1], encoding="utf-8"))["latestVersion"])' "$ROOT/version.txt")"
|
||||||
|
|
||||||
|
grep -Fq "latestVersion: \"$LATEST_VERSION\"" "$CONFIG" \
|
||||||
|
|| fail "Shared/AppRemoteConfiguration.swift fallback latestVersion must match version.txt ($LATEST_VERSION)"
|
||||||
|
grep -Fq "version-v$LATEST_VERSION" "$ROOT/README.md" \
|
||||||
|
|| fail "README.md version badge must match version.txt ($LATEST_VERSION)"
|
||||||
|
grep -Fq "version-v$LATEST_VERSION" "$ROOT/README.zh-CN.md" \
|
||||||
|
|| fail "README.zh-CN.md version badge must match version.txt ($LATEST_VERSION)"
|
||||||
|
|
||||||
grep -q 'static let fallback = AppRemoteConfiguration' "$CONFIG" \
|
grep -q 'static let fallback = AppRemoteConfiguration' "$CONFIG" \
|
||||||
|| fail "the app must ship a built-in remote-configuration fallback"
|
|| fail "the app must ship a built-in remote-configuration fallback"
|
||||||
grep -q 'timeoutIntervalForRequest = 1.5' "$CONFIG" \
|
grep -q 'timeoutIntervalForRequest = 1.5' "$CONFIG" \
|
||||||
@@ -45,8 +54,16 @@ grep -q 'static let configurationURLs = \[' "$CONFIG" \
|
|||||||
|| fail "update detection must retain multiple configuration sources"
|
|| fail "update detection must retain multiple configuration sources"
|
||||||
! grep -q 'data.count' "$CONFIG" \
|
! grep -q 'data.count' "$CONFIG" \
|
||||||
|| fail "the client must not impose a remote configuration file-size limit"
|
|| fail "the client must not impose a remote configuration file-size limit"
|
||||||
grep -q 'docs/releases/v\\(version).md' "$CONFIG" \
|
grep -Fq 'docs/releases/v\(version)\(suffix).md' "$CONFIG" \
|
||||||
|| fail "update notes must come from the archived release document"
|
|| fail "update notes must come from the language-specific archived release document"
|
||||||
|
for locale in '' .en .zh-Hant; do
|
||||||
|
test -s "$ROOT/docs/releases/v${LATEST_VERSION}${locale}.md" \
|
||||||
|
|| fail "missing localized v${LATEST_VERSION}${locale} release notes"
|
||||||
|
done
|
||||||
|
grep -Fq '## Highlights' "$ROOT/docs/releases/v${LATEST_VERSION}.en.md" \
|
||||||
|
|| fail "English update highlights are missing"
|
||||||
|
grep -Fq '## 主要更新' "$ROOT/docs/releases/v${LATEST_VERSION}.zh-Hant.md" \
|
||||||
|
|| fail "Traditional Chinese update highlights are missing"
|
||||||
grep -q '/releases/latest' "$CONFIG" \
|
grep -q '/releases/latest' "$CONFIG" \
|
||||||
|| fail "missing release notes must fall back to the latest Release page"
|
|| fail "missing release notes must fall back to the latest Release page"
|
||||||
grep -q '.task { await checkForUpdates() }' "$CONTENT" \
|
grep -q '.task { await checkForUpdates() }' "$CONTENT" \
|
||||||
@@ -121,7 +138,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 'String(localized: "第三方客户端")' "$BUG_REPORT" \
|
grep -Fq 'AppLocalization.string("第三方客户端")' "$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"
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
Release notes are archived under [`docs/releases/`](releases/).
|
||||||
|
|
||||||
|
## Releases
|
||||||
|
|
||||||
|
- [v1.0.8](releases/v1.0.8.en.md) — September 24, 2026 ([简体中文](releases/v1.0.8.md) · [繁體中文](releases/v1.0.8.zh-Hant.md))
|
||||||
|
- [v1.0.7](releases/v1.0.7.md) — September 11, 2026 (Chinese)
|
||||||
|
- [v1.0.6](releases/v1.0.6.md) — September 1, 2026 (Chinese)
|
||||||
|
|
||||||
|
Earlier releases: [GitHub Releases](https://github.com/xweiba/location-spoofer/releases).
|
||||||
@@ -2,8 +2,11 @@
|
|||||||
|
|
||||||
各版本发布说明见 `docs/releases/` 目录。
|
各版本发布说明见 `docs/releases/` 目录。
|
||||||
|
|
||||||
|
[English](CHANGELOG.en.md) · [繁體中文](CHANGELOG.zh-Hant.md)
|
||||||
|
|
||||||
## 已发布
|
## 已发布
|
||||||
|
|
||||||
|
- [v1.0.8](https://github.com/xweiba/location-spoofer/releases/tag/v1.0.8) — 2026-09-24
|
||||||
- [v1.0.7](https://github.com/xweiba/location-spoofer/releases/tag/v1.0.7) — 2026-09-11
|
- [v1.0.7](https://github.com/xweiba/location-spoofer/releases/tag/v1.0.7) — 2026-09-11
|
||||||
- [v1.0.6](https://github.com/xweiba/location-spoofer/releases/tag/v1.0.6) — 2026-09-01
|
- [v1.0.6](https://github.com/xweiba/location-spoofer/releases/tag/v1.0.6) — 2026-09-01
|
||||||
- [v1.0.5](https://github.com/xweiba/location-spoofer/releases/tag/v1.0.5) — 2026-08-10
|
- [v1.0.5](https://github.com/xweiba/location-spoofer/releases/tag/v1.0.5) — 2026-08-10
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# 更新紀錄
|
||||||
|
|
||||||
|
各版本的說明存放於 [`docs/releases/`](releases/) 目錄。
|
||||||
|
|
||||||
|
## 已發佈
|
||||||
|
|
||||||
|
- [v1.0.8](releases/v1.0.8.zh-Hant.md) — 2026-09-24([English](releases/v1.0.8.en.md) · [简体中文](releases/v1.0.8.md))
|
||||||
|
- [v1.0.7](releases/v1.0.7.md) — 2026-09-11(簡體中文)
|
||||||
|
- [v1.0.6](releases/v1.0.6.md) — 2026-09-01(簡體中文)
|
||||||
|
|
||||||
|
更早的版本:[GitHub Releases](https://github.com/xweiba/location-spoofer/releases)。
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# v1.0.8
|
||||||
|
|
||||||
|
Release date: September 24, 2026
|
||||||
|
|
||||||
|
[简体中文](https://github.com/xweiba/location-spoofer/blob/v1.0.8/docs/releases/v1.0.8.md) · [繁體中文](https://github.com/xweiba/location-spoofer/blob/v1.0.8/docs/releases/v1.0.8.zh-Hant.md)
|
||||||
|
|
||||||
|
This re-release replaces build 9 with **build 10** while keeping the v1.0.8 version.
|
||||||
|
|
||||||
|
## Highlights
|
||||||
|
|
||||||
|
- Fixed the app showing Chinese when a non-Chinese language is preferred: the app now uses only the **first** preferred language. Simplified and Traditional Chinese display Chinese; Italian and other non-Chinese languages display English even if Chinese is second in the preference list.
|
||||||
|
- Added explicit Simplified and Traditional Chinese resources and made English the development-language fallback. Diagnostics, prompts, and generated bug reports follow the same language choice.
|
||||||
|
- Added missing English translations for two hostname-copy messages and regression tests for language selection.
|
||||||
|
- Made English the default repository README, retained a separate Chinese README, added the project's AGPL-3.0 license, and preserved third-party script license and provenance information.
|
||||||
|
- Added contract tests to CI and synchronized the remote-version fallback with the release version.
|
||||||
|
|
||||||
|
## Changes since v1.0.7
|
||||||
|
|
||||||
|
- `5b9fedf` Fix first-preferred-language fallback and prepare v1.0.8
|
||||||
|
- `228b67b` Make English the default README and add AGPL licensing
|
||||||
|
- `f4712f7` Run contract tests in CI and via make test
|
||||||
|
- `393c7cc` Align the built-in fallback version and add consistency checks
|
||||||
|
|
||||||
|
## Self-signing and installation
|
||||||
|
|
||||||
|
- The Release attachment is an **unsigned IPA**. Sign it before installing it on an iPhone.
|
||||||
|
- A free Apple ID and Impactor can be used; a paid developer account is not required.
|
||||||
|
- Keep the Bundle ID `com.paopaolabs.location-spoofer`, App Group `group.com.paopaolabs.location-spoofer`, and existing entitlements when signing.
|
||||||
|
- Free Apple ID signing usually expires after seven days, after which the app needs to be signed and installed again.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# v1.0.8
|
||||||
|
|
||||||
|
发布日期:2026-09-24
|
||||||
|
|
||||||
|
[English](https://github.com/xweiba/location-spoofer/blob/v1.0.8/docs/releases/v1.0.8.en.md) · [繁體中文](https://github.com/xweiba/location-spoofer/blob/v1.0.8/docs/releases/v1.0.8.zh-Hant.md)
|
||||||
|
|
||||||
|
本次重新发布以**构建号 10** 替换原构建号 9,版本号仍为 v1.0.8。
|
||||||
|
|
||||||
|
## 主要更新
|
||||||
|
|
||||||
|
- 修复非中文系统语言下错误显示中文的问题:只根据首选语言的**第一项**选择 App 界面语言。简体中文和繁体中文分别显示中文;意大利语及其他非中文语言统一显示英文,即使中文排在首选语言列表的后面。
|
||||||
|
- 补齐简体、繁体中文本地化资源,并将英语设置为开发语言与默认回退语言;诊断、提示及 Bug 报告等由代码生成的文案也遵循同一语言规则。
|
||||||
|
- 修复两处主机名复制提示缺少英文翻译的问题,并增加语言回退测试。
|
||||||
|
- 仓库默认 README 改为英文,中文文档保留独立入口;添加项目 AGPL-3.0 许可证并保留第三方脚本的原有许可与来源说明。
|
||||||
|
- 将契约测试接入 CI,统一版本配置与内置兜底版本号。
|
||||||
|
|
||||||
|
## 提交变更总结
|
||||||
|
|
||||||
|
- `5b9fedf` fix(i18n): 修复首选语言回退并准备 v1.0.8
|
||||||
|
- `228b67b` docs: 英文 README 设为默认并添加 AGPL 许可
|
||||||
|
- `f4712f7` ci: 契约测试接入 CI 与 make test
|
||||||
|
- `393c7cc` fix(version): 修正内置兜底版本号漂移并补版本一致性校验
|
||||||
|
|
||||||
|
## 自签安装
|
||||||
|
|
||||||
|
- Release 附件为未签名 IPA,安装前需要自行签名。
|
||||||
|
- 可使用免费 Apple ID 和 Impactor 完成签名安装,无需付费开发者账号。
|
||||||
|
- 签名时请保留 Bundle ID `com.paopaolabs.location-spoofer`、App Group `group.com.paopaolabs.location-spoofer` 及原有 entitlements。
|
||||||
|
- 免费 Apple ID 签名通常只有 7 天有效期,到期后需要重新签名安装。
|
||||||
|
|
||||||
|
<!-- commit-range: v1.0.7..v1.0.8 -->
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# v1.0.8
|
||||||
|
|
||||||
|
發佈日期:2026-09-24
|
||||||
|
|
||||||
|
[English](https://github.com/xweiba/location-spoofer/blob/v1.0.8/docs/releases/v1.0.8.en.md) · [简体中文](https://github.com/xweiba/location-spoofer/blob/v1.0.8/docs/releases/v1.0.8.md)
|
||||||
|
|
||||||
|
本次重新發佈以**建置版本 10** 取代原有的建置版本 9,版本號仍為 v1.0.8。
|
||||||
|
|
||||||
|
## 主要更新
|
||||||
|
|
||||||
|
- 修正非中文系統語言下錯誤顯示中文的問題:App 現在只根據偏好語言的**第一項**選擇介面語言。簡體和繁體中文顯示中文;義大利文及其他非中文語言顯示英文,即使中文排在語言清單的第二項。
|
||||||
|
- 補齊簡體與繁體中文本地化資源,並將英文設為開發語言及預設回退語言。診斷資訊、提示與產生的 Bug 回報也遵循相同的語言選擇規則。
|
||||||
|
- 補上兩處主機名稱複製提示的英文翻譯,並加入語言選擇的迴歸測試。
|
||||||
|
- 將英文設為儲存庫的預設 README,保留獨立的中文文件;加入專案的 AGPL-3.0 授權,並保留第三方腳本原有的授權及來源說明。
|
||||||
|
- 將契約測試加入 CI,並統一遠端版本設定與內建回退版本號。
|
||||||
|
|
||||||
|
## 提交變更摘要
|
||||||
|
|
||||||
|
- `5b9fedf` 修正偏好語言回退邏輯並準備 v1.0.8
|
||||||
|
- `228b67b` 將英文 README 設為預設並加入 AGPL 授權
|
||||||
|
- `f4712f7` 在 CI 與 make test 執行契約測試
|
||||||
|
- `393c7cc` 修正內建回退版本號並加入一致性檢查
|
||||||
|
|
||||||
|
## 自行簽署與安裝
|
||||||
|
|
||||||
|
- Release 附件是**未簽署的 IPA**,安裝到 iPhone 前必須先簽署。
|
||||||
|
- 可以使用免費 Apple ID 和 Impactor 簽署安裝,無須付費開發者帳號。
|
||||||
|
- 簽署時請保留 Bundle ID `com.paopaolabs.location-spoofer`、App Group `group.com.paopaolabs.location-spoofer` 及原有 entitlements。
|
||||||
|
- 免費 Apple ID 的簽署通常只有七天有效期,到期後需要重新簽署並安裝。
|
||||||
+5
-4
@@ -4,18 +4,19 @@ options:
|
|||||||
deploymentTarget:
|
deploymentTarget:
|
||||||
iOS: "15.0"
|
iOS: "15.0"
|
||||||
createIntermediateGroups: true
|
createIntermediateGroups: true
|
||||||
developmentLanguage: zh-Hans
|
developmentLanguage: en
|
||||||
|
|
||||||
knownRegions:
|
knownRegions:
|
||||||
- zh-Hans
|
|
||||||
- en
|
- en
|
||||||
|
- zh-Hans
|
||||||
|
- zh-Hant
|
||||||
- Base
|
- Base
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
base:
|
base:
|
||||||
SWIFT_VERSION: "5.9"
|
SWIFT_VERSION: "5.9"
|
||||||
MARKETING_VERSION: "1.0.7"
|
MARKETING_VERSION: "1.0.8"
|
||||||
CURRENT_PROJECT_VERSION: "8"
|
CURRENT_PROJECT_VERSION: "10"
|
||||||
CODE_SIGN_STYLE: Manual
|
CODE_SIGN_STYLE: Manual
|
||||||
CODE_SIGNING_ALLOWED: "NO"
|
CODE_SIGNING_ALLOWED: "NO"
|
||||||
CODE_SIGNING_REQUIRED: "NO"
|
CODE_SIGNING_REQUIRED: "NO"
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"latestVersion": "1.0.7",
|
"latestVersion": "1.0.8",
|
||||||
"minimumSupportedVersion": "1.0.0",
|
"minimumSupportedVersion": "1.0.0",
|
||||||
"communityPromptClients": ["surge", "quantumultX", "loon", "stash", "egern"]
|
"communityPromptClients": ["surge", "quantumultX", "loon", "stash", "egern"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user