mirror of
https://github.com/StikDebug/StikDebug.git
synced 2026-10-16 04:20:30 +00:00
feat: add localization support (zh-Hans + en) with instant language switching
This commit is contained in:
parent
5f2ed6667b
commit
a18d8b5d5c
7 changed files with 1869 additions and 63 deletions
1774
StikJIT/Resources/Localizable.xcstrings
Normal file
1774
StikJIT/Resources/Localizable.xcstrings
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -159,6 +159,8 @@ struct HeartbeatApp: App {
|
|||
var body: some Scene {
|
||||
WindowGroup {
|
||||
MainTabView()
|
||||
.environment(LanguageManager.shared)
|
||||
.id(LanguageManager.shared.language)
|
||||
.onAppear {
|
||||
Task {
|
||||
let fileManager = FileManager.default
|
||||
|
|
|
|||
|
|
@ -24,57 +24,57 @@ enum AppFeature: String, CaseIterable, Identifiable {
|
|||
var title: String {
|
||||
switch self {
|
||||
case .home:
|
||||
return "Apps"
|
||||
return "Apps".localized
|
||||
case .scripts:
|
||||
return "Scripts"
|
||||
return "Scripts".localized
|
||||
case .tools:
|
||||
return "Tools"
|
||||
return "Tools".localized
|
||||
case .news:
|
||||
return "News"
|
||||
return "News".localized
|
||||
case .console:
|
||||
return "Console"
|
||||
return "Console".localized
|
||||
case .deviceInfo:
|
||||
return "Device Info"
|
||||
return "Device Info".localized
|
||||
case .profiles:
|
||||
return "App Expiry"
|
||||
return "App Expiry".localized
|
||||
case .processes:
|
||||
return "Processes"
|
||||
return "Processes".localized
|
||||
case .location:
|
||||
return "Location"
|
||||
return "Location".localized
|
||||
case .settings:
|
||||
return "Settings"
|
||||
return "Settings".localized
|
||||
}
|
||||
}
|
||||
|
||||
var detail: String {
|
||||
switch self {
|
||||
case .home:
|
||||
return "Manage installed apps"
|
||||
return "Manage installed apps".localized
|
||||
case .scripts:
|
||||
return "Manage and run JS scripts"
|
||||
return "Manage and run JS scripts".localized
|
||||
case .tools:
|
||||
return "Access additional tools"
|
||||
return "Access additional tools".localized
|
||||
case .news:
|
||||
return "Latest StikDebug updates"
|
||||
return "Latest StikDebug updates".localized
|
||||
case .console:
|
||||
return "Live device logs"
|
||||
return "Live device logs".localized
|
||||
case .deviceInfo:
|
||||
return "View detailed device metadata"
|
||||
return "View detailed device metadata".localized
|
||||
case .profiles:
|
||||
return "Check app expiration dates"
|
||||
return "Check app expiration dates".localized
|
||||
case .processes:
|
||||
return "Inspect running apps"
|
||||
return "Inspect running apps".localized
|
||||
case .location:
|
||||
return "Simulate GPS location"
|
||||
return "Simulate GPS location".localized
|
||||
case .settings:
|
||||
return "Configure StikDebug"
|
||||
return "Configure StikDebug".localized
|
||||
}
|
||||
}
|
||||
|
||||
var toolTitle: String {
|
||||
switch self {
|
||||
case .location:
|
||||
return "Location Simulation"
|
||||
return "Location Simulation".localized
|
||||
default:
|
||||
return title
|
||||
}
|
||||
|
|
|
|||
18
StikJIT/Utilities/LanguageManager.swift
Normal file
18
StikJIT/Utilities/LanguageManager.swift
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import SwiftUI
|
||||
|
||||
@Observable
|
||||
class LanguageManager {
|
||||
static let shared = LanguageManager()
|
||||
|
||||
var language: String {
|
||||
didSet {
|
||||
UserDefaults.standard.set([language], forKey: "AppleLanguages")
|
||||
UserDefaults.standard.synchronize()
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
let saved = UserDefaults.standard.stringArray(forKey: "AppleLanguages")?.first ?? "zh-Hans"
|
||||
self.language = saved
|
||||
}
|
||||
}
|
||||
|
|
@ -220,7 +220,7 @@ struct HomeView: View {
|
|||
ConsoleLogsView()
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Close") {
|
||||
Button("Close".localized) {
|
||||
isShowingConsole = false
|
||||
}
|
||||
}
|
||||
|
|
@ -233,7 +233,7 @@ struct HomeView: View {
|
|||
RunJSView(model: jsModel)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button("Done") { scriptViewShow = false }
|
||||
Button("Done".localized) { scriptViewShow = false }
|
||||
}
|
||||
}
|
||||
.navigationTitle(selectedScript)
|
||||
|
|
|
|||
|
|
@ -133,8 +133,8 @@ private enum AppListTab: Int, CaseIterable, Identifiable {
|
|||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .debuggable: return "JIT"
|
||||
case .launch: return "Other"
|
||||
case .debuggable: return "JIT".localized
|
||||
case .launch: return "Other".localized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -186,7 +186,7 @@ private enum AppListTab: Int, CaseIterable, Identifiable {
|
|||
}
|
||||
if showDoneButton {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Done") { dismiss() }.fontWeight(.semibold)
|
||||
Button("Done".localized) { dismiss() }.fontWeight(.semibold)
|
||||
}
|
||||
} else {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
|
|
@ -580,7 +580,7 @@ struct AppButton: View {
|
|||
.contextMenu {
|
||||
Button(action: toggleFavorite) {
|
||||
Label(
|
||||
favoriteApps.contains(bundleID) ? "Remove Favorite" : "Add to Favorites",
|
||||
favoriteApps.contains(bundleID) ? "Remove Favorite".localized : "Add to Favorites".localized,
|
||||
systemImage: favoriteApps.contains(bundleID) ? "star.slash" : "star"
|
||||
)
|
||||
.disabled(!favoriteApps.contains(bundleID) && favoriteApps.count >= 4)
|
||||
|
|
@ -588,17 +588,17 @@ struct AppButton: View {
|
|||
Button {
|
||||
copyBundleID()
|
||||
} label: {
|
||||
Label("Copy Bundle ID", systemImage: "doc.on.doc")
|
||||
Label("Copy Bundle ID".localized, systemImage: "doc.on.doc")
|
||||
}
|
||||
if enableAdvancedOptions {
|
||||
Button { showScriptPicker = true } label: {
|
||||
Label("Assign Script", systemImage: "chevron.left.slash.chevron.right")
|
||||
Label("Assign Script".localized, systemImage: "chevron.left.slash.chevron.right")
|
||||
}
|
||||
if assignedScriptName != nil {
|
||||
Button {
|
||||
resetScriptAssignment()
|
||||
} label: {
|
||||
Label("Reset Script", systemImage: "arrow.uturn.left")
|
||||
Label("Reset Script".localized, systemImage: "arrow.uturn.left")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -607,14 +607,14 @@ struct AppButton: View {
|
|||
Button {
|
||||
toggleFavorite()
|
||||
} label: {
|
||||
Label(favoriteApps.contains(bundleID) ? "Unfavorite" : "Favorite", systemImage: "star")
|
||||
Label(favoriteApps.contains(bundleID) ? "Unfavorite".localized : "Favorite".localized, systemImage: "star")
|
||||
}
|
||||
.tint(.yellow)
|
||||
|
||||
Button {
|
||||
copyBundleID()
|
||||
} label: {
|
||||
Label("Copy ID", systemImage: "doc.on.doc")
|
||||
Label("Copy ID".localized, systemImage: "doc.on.doc")
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showScriptPicker) {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ struct SettingsView: View {
|
|||
@AppStorage("keepAliveAudio") private var keepAliveAudio = true
|
||||
@AppStorage("keepAliveLocation") private var keepAliveLocation = true
|
||||
@AppStorage("customTargetIP") private var customTargetIP = ""
|
||||
@Environment(LanguageManager.self) private var langManager
|
||||
|
||||
@State private var isShowingPairingFilePicker = false
|
||||
@State private var isImportingFile = false
|
||||
|
|
@ -54,15 +55,26 @@ struct SettingsView: View {
|
|||
|
||||
Section {
|
||||
Link(destination: SettingsLinks.githubStars) {
|
||||
Label("Star on GitHub", systemImage: "star")
|
||||
Label("Star on GitHub".localized, systemImage: "star")
|
||||
}
|
||||
}
|
||||
|
||||
Section("Pairing File") {
|
||||
Section("Language".localized) {
|
||||
Picker("Language".localized, selection: Binding(
|
||||
get: { langManager.language },
|
||||
set: { langManager.language = $0 }
|
||||
)) {
|
||||
Text("简体中文").tag("zh-Hans")
|
||||
Text("English").tag("en")
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
}
|
||||
|
||||
Section("Pairing File".localized) {
|
||||
Button {
|
||||
isShowingPairingFilePicker = true
|
||||
} label: {
|
||||
Label("Import Pairing File", systemImage: "doc.badge.plus")
|
||||
Label("Import Pairing File".localized, systemImage: "doc.badge.plus")
|
||||
}
|
||||
.disabled(isImportingFile)
|
||||
|
||||
|
|
@ -70,7 +82,7 @@ struct SettingsView: View {
|
|||
HStack(spacing: 10) {
|
||||
ProgressView()
|
||||
.controlSize(.small)
|
||||
Text("Importing pairing file…")
|
||||
Text("Importing pairing file…".localized)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
|
@ -87,8 +99,8 @@ struct SettingsView: View {
|
|||
Section {
|
||||
Toggle(isOn: $keepAliveAudio) {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Silent Audio")
|
||||
Text("Plays inaudible audio so iOS keeps the app running.")
|
||||
Text("Silent Audio".localized)
|
||||
Text("Plays inaudible audio so iOS keeps the app running.".localized)
|
||||
.font(.caption).foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
|
@ -99,8 +111,8 @@ struct SettingsView: View {
|
|||
|
||||
Toggle(isOn: $keepAliveLocation) {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Background Location")
|
||||
Text("Uses low-accuracy location to stay alive when an activity needs it.")
|
||||
Text("Background Location".localized)
|
||||
Text("Uses low-accuracy location to stay alive when an activity needs it.".localized)
|
||||
.font(.caption).foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
|
@ -109,22 +121,22 @@ struct SettingsView: View {
|
|||
}
|
||||
|
||||
} header: {
|
||||
Text("Background Keep-Alive")
|
||||
Text("Background Keep-Alive".localized)
|
||||
}
|
||||
|
||||
Section("Behavior") {
|
||||
Section("Behavior".localized) {
|
||||
Toggle(isOn: $overrideTXMDetection) {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Always Run Scripts")
|
||||
Text("Treats device as TXM-capable to bypass hardware checks.")
|
||||
Text("Always Run Scripts".localized)
|
||||
Text("Treats device as TXM-capable to bypass hardware checks.".localized)
|
||||
.font(.caption).foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section("Advanced") {
|
||||
Section("Advanced".localized) {
|
||||
HStack {
|
||||
Text("Target Device IP")
|
||||
Text("Target Device IP".localized)
|
||||
Spacer()
|
||||
TextField("10.7.0.1", text: $customTargetIP)
|
||||
.multilineTextAlignment(.trailing)
|
||||
|
|
@ -132,10 +144,10 @@ struct SettingsView: View {
|
|||
.submitLabel(.done)
|
||||
}
|
||||
Button { openAppFolder() } label: {
|
||||
Label("App Folder", systemImage: "folder")
|
||||
Label("App Folder".localized, systemImage: "folder")
|
||||
}.foregroundStyle(.primary)
|
||||
Button { showDDIConfirmation = true } label: {
|
||||
Label("Redownload DDI", systemImage: "arrow.down.circle")
|
||||
Label("Redownload DDI".localized, systemImage: "arrow.down.circle")
|
||||
}.foregroundStyle(.primary).disabled(isRedownloadingDDI)
|
||||
if isRedownloadingDDI {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
|
|
@ -147,15 +159,15 @@ struct SettingsView: View {
|
|||
}
|
||||
}
|
||||
|
||||
Section("Help") {
|
||||
Section("Help".localized) {
|
||||
Link(destination: SettingsLinks.pairingFileGuide) {
|
||||
Label("Pairing File Guide", systemImage: "questionmark.circle")
|
||||
Label("Pairing File Guide".localized, systemImage: "questionmark.circle")
|
||||
}
|
||||
Link(destination: SettingsLinks.localDevVPN) {
|
||||
Label("Download LocalDevVPN", systemImage: "arrow.down.circle")
|
||||
Label("Download LocalDevVPN".localized, systemImage: "arrow.down.circle")
|
||||
}
|
||||
Link(destination: SettingsLinks.discord) {
|
||||
Label("Discord Support", systemImage: "bubble.left.and.bubble.right")
|
||||
Label("Discord Support".localized, systemImage: "bubble.left.and.bubble.right")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -166,7 +178,7 @@ struct SettingsView: View {
|
|||
.listRowBackground(Color.clear)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Settings")
|
||||
.navigationTitle("Settings".localized)
|
||||
}
|
||||
.fileImporter(
|
||||
isPresented: $isShowingPairingFilePicker,
|
||||
|
|
@ -184,27 +196,27 @@ struct SettingsView: View {
|
|||
do {
|
||||
try PairingFileStore.importFromPicker(url, fileManager: fileManager)
|
||||
isImportingFile = false
|
||||
pairingImportMessage = ("Imported successfully", false)
|
||||
pairingImportMessage = ("Imported successfully".localized, false)
|
||||
startTunnelInBackground()
|
||||
schedulePairingStatusDismiss()
|
||||
} catch {
|
||||
isImportingFile = false
|
||||
pairingImportMessage = ("Import failed: \(error.localizedDescription)", true)
|
||||
pairingImportMessage = (String(format: "Import failed: %@".localized, error.localizedDescription), true)
|
||||
schedulePairingStatusDismiss()
|
||||
}
|
||||
case .failure(let error):
|
||||
isImportingFile = false
|
||||
pairingImportMessage = ("Import failed: \(error.localizedDescription)", true)
|
||||
pairingImportMessage = (String(format: "Import failed: %@".localized, error.localizedDescription), true)
|
||||
schedulePairingStatusDismiss()
|
||||
}
|
||||
}
|
||||
.confirmationDialog("Redownload DDI Files?", isPresented: $showDDIConfirmation, titleVisibility: .visible) {
|
||||
Button("Redownload", role: .destructive) {
|
||||
.confirmationDialog("Redownload DDI Files?".localized, isPresented: $showDDIConfirmation, titleVisibility: .visible) {
|
||||
Button("Redownload".localized, role: .destructive) {
|
||||
redownloadDDIPressed()
|
||||
}
|
||||
Button("Cancel", role: .cancel) { }
|
||||
Button("Cancel".localized, role: .cancel) { }
|
||||
} message: {
|
||||
Text("Existing DDI files will be removed before downloading fresh copies.")
|
||||
Text("Existing DDI files will be removed before downloading fresh copies.".localized)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -235,7 +247,7 @@ struct SettingsView: View {
|
|||
await MainActor.run {
|
||||
isRedownloadingDDI = true
|
||||
ddiDownloadProgress = 0
|
||||
ddiStatusMessage = "Preparing download…"
|
||||
ddiStatusMessage = "Preparing download…".localized
|
||||
ddiResultMessage = nil
|
||||
}
|
||||
do {
|
||||
|
|
@ -247,12 +259,12 @@ struct SettingsView: View {
|
|||
}
|
||||
await MainActor.run {
|
||||
isRedownloadingDDI = false
|
||||
ddiResultMessage = ("DDI files refreshed successfully.", false)
|
||||
ddiResultMessage = ("DDI files refreshed successfully.".localized, false)
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
isRedownloadingDDI = false
|
||||
ddiResultMessage = ("Failed to redownload DDI files: \(error.localizedDescription)", true)
|
||||
ddiResultMessage = (String(format: "Failed to redownload DDI files: %@".localized, error.localizedDescription), true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue