diff --git a/mond/helpers/L10n.swift b/mond/helpers/L10n.swift new file mode 100644 index 0000000..355a71a --- /dev/null +++ b/mond/helpers/L10n.swift @@ -0,0 +1,303 @@ +// +// L10n.swift +// mond +// +// In-app language switching (English / 中文) for mond-CN. +// + +import SwiftUI +import Combine + +/// Supported interface languages. +enum AppLanguage: String, CaseIterable, Identifiable { + case english = "en" + case chinese = "zh-Hans" + + var id: String { rawValue } + + /// Shown inside the language picker, always written in its own language. + var display_name: String { + switch self { + case .english: return "English" + case .chinese: return "中文" + } + } +} + +/// Provides localized strings and drives UI updates when the language changes. +/// +/// English strings in the codebase are the source of truth: in English mode +/// `t(_:)` returns the key unchanged, while in Chinese mode it looks up the +/// Chinese translation and falls back to English when a string is missing. +final class L10n: ObservableObject { + static let shared = L10n() + + @AppStorage("app_language") private var stored_language = AppLanguage.english.rawValue + + @Published var language: AppLanguage = .english + + private init() { + language = AppLanguage(rawValue: stored_language) ?? .english + } + + /// Applies and persists a language change. + func set(_ new_language: AppLanguage) { + guard new_language != language else { return } + language = new_language + stored_language = new_language.rawValue + } + + /// A SwiftUI binding that keeps pickers and the persisted value in sync. + var language_binding: Binding { + Binding( + get: { self.language }, + set: { self.set($0) } + ) + } + + /// Returns the localized string for `key`, substituting `args`. + func t(_ key: String, _ args: CVarArg...) -> String { + let value = language == .chinese ? (translations[key] ?? key) : key + if args.isEmpty { return value } + return String(format: value, arguments: args) + } + + private let translations: [String: String] = [ + // MARK: Home (ContentView) + "Language": "语言", + "Choose the language used by the app.": "选择本应用使用的语言。", + "Logs": "日志", + "Tweaks": "插件", + "Only MobileGestalt is available when method is set to cmg.": "当方法设置为 cmg 时,只有 MobileGestalt 可用。", + "Advanced": "高级", + "CacheExtra Fields": "CacheExtra 字段", + "Only use these Tweaks if you know what you're doing.\nYou could break something irreversibly.": "只有在你明确知道自己在做什么时,才使用这些插件。\n某些操作可能造成不可逆的损坏。", + + // MARK: Settings (SettingsView) + "Method": "方法", + "Run Exploit": "运行 Exploit", + "**CMG:** Supports iOS 27.0 b1 - b4. Only MobileGestalt will work with this method. Only use this when bad_query isnt working for you.": "**CMG:** 支持 iOS 27.0 b1 - b4。使用此方法时只有 MobileGestalt 可用,仅在 bad_query 无法正常工作时使用。", + "**bad_query:** Supports iOS 27.0 b1 - b4. By [forcequit](https://github.com/forcequitOS).": "**bad_query:** 支持 iOS 27.0 b1 - b4。由 [forcequit](https://github.com/forcequitOS) 开发。", + "Sandbox Extension Token.": "沙盒扩展 Token。", + "Copy Token": "复制 Token", + "Generate Token": "生成 Token", + "Failed to get token.": "获取 Token 失败。", + "Your sandbox token is valid.": "你的沙盒 Token 有效。", + "Your sandbox token is invalid.": "你的沙盒 Token 无效。", + "Disabled because the exploit failed. Is your iOS version supported?": "因 exploit 失败而禁用。你的 iOS 版本受支持吗?", + "Keep Alive": "后台保活", + "Keeps the app running in the background after you close it, allowing it to remain active.": "关闭应用后仍在后台保持运行,以便它能继续正常工作。", + "Dismiss after importing": "导入后自动关闭", + "When enabled, the Tendies explorer will close automatically after importing a .tendies file into PosterBoard.": "启用后,将 .tendies 文件导入 PosterBoard 时,Tendies 资源管理器会自动关闭。", + "Persist after reboot": "重启后保留", + "When enabled, MobileGestalt is written in-place on the same inode, which (hopefully) prevents iOS from regenerating the cache on reboot. Disable to use atomic file replacement instead.": "启用后,MobileGestalt 会在同一 inode 上原地写入,希望能防止 iOS 重启后重新生成缓存。关闭此选项则改用原子文件替换。", + "Ignore exploit failure": "忽略 exploit 失败", + "When enabled, All Tweaks will be accessible even if the exploit failed.": "启用后,即使 exploit 失败,所有插件仍可使用。", + "Information": "说明", + "Settings": "设置", + "Tools": "工具", + "Respring": "注销", + "Respring method by [neon](https://github.com/neonmodder123), swift implementation by [skadz](https://github.com/skadz108).": "注销方法由 [neon](https://github.com/neonmodder123) 提供,Swift 实现由 [skadz](https://github.com/skadz108) 完成。", + "Credits": "致谢", + "Main developer": "主要开发者", + "The bad_query exploit": "bad_query 漏洞利用", + "His work on the MCM bug class": "MCM 漏洞类别的研究", + "Tendies repository": "Tendies 资源库", + "Done": "完成", + "Are you sure?": "确定吗?", + "Cancel": "取消", + "Confirm": "确定", + "Confirm that you want to respring.": "确认要注销吗?", + + // MARK: Logs (LogView) + "Copy Output": "复制输出", + + // MARK: File browser (SantanderView) + "Go to Root": "前往根目录", + "Go to Home": "前往主目录", + "Display hidden files": "显示隐藏文件", + "Sort A-Z": "按 A-Z 排序", + "Sort Z-A": "按 Z-A 排序", + "No matching items.": "没有匹配的项目。", + "No visible items. Enable \"Display hidden files\" to show dotfiles.": "没有可见项目。启用“显示隐藏文件”可查看点文件。", + "Directory is empty.": "目录为空。", + "No app containers found.": "未找到应用容器。", + "Cannot list directory (missing permissions).": "无法列出目录(权限不足)。", + "Not a directory.": "不是目录。", + "Save": "保存", + "Save Failed": "保存失败", + "OK": "好", + "Failed to render image": "无法渲染图像", + "Error: %@": "错误:%@", + "Missing write permission.": "缺少写入权限。", + "Could not encode text.": "无法编码文本。", + "Edited text is not a valid plist/JSON document.": "编辑后的内容不是有效的 plist/JSON 文档。", + "(empty file)": "(空文件)", + "Failed to read file:": "读取文件失败:", + "Binary data (%d bytes). Showing first %d bytes:": "二进制数据(%d 字节)。正在显示前 %d 字节:", + "Exists: %@": "存在:%@", + "yes": "是", + "no": "否", + "Kind: %@": "类型:%@", + "directory": "目录", + "regular item": "常规项目", + "UTType: %@": "UTType:%@", + "Size: %d bytes": "大小:%d 字节", + "Symlink: %@": "符号链接:%@", + "Symlink target: %@": "符号链接目标:%@", + "Alias file: %@": "别名文件:%@", + "File attribute type: %@": "文件属性类型:%@", + "Owner: %@ (%d)": "所有者:%@(%d)", + "Owner: %@": "所有者:%@", + "Owner ID: %d": "所有者 ID:%d", + "Group: %@ (%d)": "组:%@(%d)", + "Group: %@": "组:%@", + "Group ID: %d": "组 ID:%d", + "POSIX perms: %04o": "POSIX 权限:%04o", + "Readable: %@": "可读:%@", + "Writable: %@": "可写:%@", + "Executable: %@": "可执行:%@", + + // MARK: MobileGestalt (GestaltView) + "Do not reboot!": "不要重启!", + "Your MobileGestalt.plist seems to be empty.": "你的 MobileGestalt.plist 似乎是空的。", + "Your MobileGestalt.plist seems to be invalid.": "你的 MobileGestalt.plist 似乎无效。", + "Warning": "警告", + "Rebooting now might cause a bootloop. Try pressing 'Revert Tweaks'. If the warnings dont go away after that, you're fucked.": "此时重启可能会导致引导循环(bootloop)。请尝试点击“还原插件”。如果警告依然存在,那你就自求多福吧。", + "Apply Tweaks": "应用插件", + "Revert Tweaks": "还原插件", + "**WARNING:** These tweaks have the capability to break features on your device or softbrick it if misused!": "**警告:** 使用不当,这些插件可能会导致设备功能异常甚至软变砖!", + "Original (%@)": "原始(%@)", + "Disable Dynamic Island": "关闭灵动岛", + "iPhone X Gestures": "iPhone X 手势", + "Subtype": "子类型", + "Custom Device Name": "自定义设备名称", + "Device Name": "设备名称", + "Device Artwork": "设备机型", + "Software-Oriented Features": "软件相关功能", + "Hardware-Oriented Features": "硬件相关功能", + "Spoofing": "伪装", + "Default": "默认", + "Eligibility": "资格", + "Device Spoofing Info": "设备伪装说明", + "Only spoof your device model if you want to download Apple Intelligence. This may break Face ID. If you decide to unspoof and want to keep Apple Intelligence, do NOT re-enter the Apple Intelligence & Siri menu in Settings.": "仅在你想下载 Apple Intelligence 时才伪装设备型号。这可能会导致 Face ID 失效。如果你决定取消伪装并想保留 Apple Intelligence,请勿再次进入“设置”中的“Apple Intelligence 与 Siri”菜单。", + "iPadOS Features": "iPadOS 功能", + "Internal": "内部功能", + + // MARK: MobileGestalt tweaks (mg.swift) + "Warning!": "警告!", + "Please do not use this feature to bypass region restrictions that would equate to breaking regional laws (e.g. disabling the camera shutter sound). We will NOT be held responsible for enabling any illegal activites!": "请不要使用此功能绕过等同于违反当地法律的地域限制(例如关闭相机快门声音)。对于由此产生的任何违法行为,我们概不负责!", + "Failed to load current MobileGestalt!": "无法加载当前的 MobileGestalt!", + "Restart the app and try again. Check logs for more detailed information.": "请重启应用后重试。查看日志可获取更详细的信息。", + "Successfully applied Gestalt tweaks!": "已成功应用 Gestalt 插件!", + "Respring your device for changes to take effect. Note that some tweaks may require a reboot for them to apply properly.": "注销设备后修改才会生效。注意:部分插件可能需要重启设备才能正确生效。", + "Failed to apply MobileGestalt!": "无法应用 MobileGestalt!", + "Successfully reverted Gestalt tweaks!": "已成功还原 Gestalt 插件!", + "Reboot your device for changes to take effect.": "重启设备后修改才会生效。", + "Failed to revert MobileGestalt!": "无法还原 MobileGestalt!", + "Check logs for error information.": "请查看日志中的错误信息。", + "Apple Intelligence Spoof": "Apple Intelligence 伪装", + "Failed to get ArtworkDeviceSubType!": "无法获取 ArtworkDeviceSubType!", + "Failed to get ArtworkDeviceProductDescription!": "无法获取 ArtworkDeviceProductDescription!", + + "Enable Dynamic Island Capability": "启用灵动岛功能", + "An alternate way to enable the Dynamic Island, as used by Nugget.": "启用灵动岛的另一种方式,与 Nugget 相同。", + "Always-On Display": "全天候显示", + "Can increase screen burn-in risk on devices that don't officially support it.": "在不官方支持该功能的设备上,可能会增加烧屏风险。", + "AOD Vibrancy": "AOD 鲜艳度", + "Turn this on if your Always-On Display renders incorrectly.": "如果全天候显示渲染异常,请开启此项。", + "Disable Wallpaper Parallax": "关闭壁纸视差效果", + "Prevents the wallpaper from moving when you tilt or move the device.": "防止倾斜或移动设备时壁纸随之移动。", + "Enable Liquid Glass Low-Performance Mode": "启用 Liquid Glass 低性能模式", + "Intended for iOS 26 and newer.": "适用于 iOS 26 及更新版本。", + "Disable Liquid Glass Low-Performance Mode": "关闭 Liquid Glass 低性能模式", + "Don't use this at the same time as the option above.": "请勿与上面的选项同时开启。", + "Boot & Shutdown Chime": "开机与关机提示音", + "Adds the chime sound when the device powers on or shuts down.": "在设备开机或关机时播放提示音。", + "Charge Limit Menu": "充电上限菜单", + "Reveals the Settings menu; whether it actually limits charging depends on your hardware.": "显示“设置”中的对应菜单;是否真的能限制充电取决于你的硬件。", + "Tap to Wake": "轻点唤醒", + "Meant for devices like the iPhone SE where tapping to wake isn't otherwise available.": "适用于 iPhone SE 这类原本不支持轻点唤醒的设备。", + "iPhone 16 Camera Control Settings": "iPhone 16 相机控制设置", + "Exposes the Camera Control settings and its related capabilities.": "显示相机控制设置及其相关功能。", + "Apple Pencil Settings": "Apple Pencil 设置", + "Reveals the Apple Pencil settings page.": "显示 Apple Pencil 设置页面。", + "Action Button Settings": "操作按钮设置", + "Reveals the Action Button settings page.": "显示操作按钮设置页面。", + "Collision SOS": "碰撞检测 SOS", + "Reveals the collision detection options in the SOS settings.": "显示 SOS 设置中的碰撞检测选项。", + "Pulse Width Modulation": "PWM 调光", + "Security Research Device Mode": "安全研究设备模式", + "Flags the device as an Apple Security Research Device.": "将设备标记为 Apple 安全研究设备。", + "Allow iPad Apps": "允许 iPad 应用", + "Allows iPad apps to be installed on iPhone.": "允许在 iPhone 上安装 iPad 应用。", + "Stage Manager Support": "台前调度支持", + "Flags the device as capable of Stage Manager.": "将设备标记为支持台前调度。", + "Apple Internal Install": "Apple 内部安装", + "Turns on internal features like the Metal HUD; some services may act up.": "开启 Metal HUD 等内部功能;部分服务可能会出现异常。", + "Internal Storage View": "内部存储查看", + "Displays internal files in the Storage settings; can be dangerous on some iPads.": "在“存储空间”设置中显示内部文件;在某些 iPad 上可能有风险。", + "Disable Region Restrictions": "关闭地区限制", + "This tweak may be broken or have no effect on some iOS versions or devices.": "此插件在某些 iOS 版本或设备上可能失效或无效。", + "How to use this tweak:\n1. Spoof to the model next to the first one supported by Apple Intelligence.\n2. Spoof back to your model.\n3. Spoof to your final model and you should see the Apple Intelligence icon in Settings.\n4. Connect iPhone to power and leave the Settings > Storage tab open for ~1 hour.\n\nNOTE: Do not spoof back.": "使用方法:\n1. 先伪装到 Apple Intelligence 支持的首个机型的下一个机型。\n2. 再伪装回你自己的机型。\n3. 伪装到最终机型后,你应该能在“设置”中看到 Apple Intelligence 图标。\n4. 连接电源,并让“设置”>“存储空间”页面保持打开约 1 小时。\n\n注意:不要伪装回去。", + "Enable iPadOS Mode": "启用 iPadOS 模式", + "Experimental and high risk; This is a very dangerous tweak to use! If you use an alphanumeric passcode, DO NOT USE THIS TWEAK AT ALL! Please do not turn off \"Show Dock In Stage Manager\" or your device will BOOTLOOP when rotating to landscape! Some users have also reported that enabling the iPadOS UI and then tapping Stage Manager can cause the device to enter Recovery Mode, even when the UI itself appears unchanged. The Settings search bar may move to the top before this happens. With these three things in mind, you may experience general instability, or other major issues such as app data randomly disappearing. But I guess some funny multitasking features that still make the device relatively unusable are cool? Whatever dude, I'm not here to tell you how to use your own device.": "实验性功能,风险极高!此插件非常危险!如果你使用的是字母数字混合密码,请完全不要使用此插件!请勿关闭“台前调度中显示程序坞”,否则设备在旋转到横屏时会进入引导循环(BOOTLOOP)!部分用户反馈,启用 iPadOS 界面后点击“台前调度”,即使界面看起来没有变化,也可能导致设备进入恢复模式。发生这种情况前,“设置”的搜索栏可能会移动到顶部。考虑到以上三点,你可能会遇到系统不稳定等问题,例如应用数据无故消失。不过话说回来,那些花里胡哨但让设备几乎没法正常用的多任务功能确实挺酷?随你吧,我不是来教你怎么用你自己的设备的。", + "Internal Features": "内部功能", + + // MARK: CacheExtra editor (CEView) + "No CacheExtra fields": "没有 CacheExtra 字段", + "No results": "没有结果", + "Search for keys or values": "搜索键或值", + "Saved": "已保存", + "Field changes written to MobileGestalt. Respring for changes to take effect.": "字段修改已写入 MobileGestalt。注销后修改才会生效。", + "Save failed": "保存失败", + "Type": "类型", + "Value": "值", + "Key": "键", + "Key name": "键名", + "Add Field": "添加字段", + "Add": "添加", + "Key cannot be empty": "键不能为空", + "Key already exists": "键已存在", + "String": "字符串", + "Integer": "整数", + "Boolean": "布尔值", + "Data (Base64)": "数据(Base64)", + "Invalid Integer": "无效的整数", + "Enter true/false": "请输入 true 或 false", + "Invalid base64": "无效的 base64", + "(empty str)": "(空字符串)", + "Data (%d bytes)": "数据(%d 字节)", + + // MARK: PosterBoard (PosterView) + "Apply": "应用", + "Reset": "重置", + "Import Tendies": "导入 Tendies", + "Explore Tendies": "浏览 Tendies", + "Import up to 5 wallpaper packs.\n**NOTE:** Importing more than 5 tendies at once is not a good idea and importing more than 15 is a TERRIBLE idea. Dont say I didnt warn you.": "最多导入 5 个壁纸包。\n**注意:** 一次导入超过 5 个 tendies 就不是什么好主意,超过 15 个更是糟糕透顶。别说我没提醒你。", + "Imported": "已导入", + "Successfully applied PosterBoard!": "已成功应用 PosterBoard!", + "For changes to take effect:\n1. Click 'Open' to launch Posterboard\n2. Close it from the App Switcher": "为使修改生效:\n1. 点击“打开”启动 PosterBoard\n2. 从应用切换器中将其关闭", + "Open": "打开", + "Failed to apply PosterBoard!": "无法应用 PosterBoard!", + "Successfully reverted PosterBoard!": "已成功还原 PosterBoard!", + "Respring your device for changes to take effect.": "注销设备后修改才会生效。", + "Failed to revert PosterBoard!": "无法还原 PosterBoard!", + + // MARK: Tendies (TendiesView) + "Loading wallpapers…": "正在加载壁纸…", + "Couldn't Load tendies": "无法加载 Tendies", + "Try Again": "重试", + "Search wallpapers": "搜索壁纸", + "Import Failed": "导入失败", + "Add to Imported": "加入已导入", + "Downloading...": "正在下载…", + "Preview Unavailable": "无法预览", + "The wallpaper preview couldn't be loaded.": "壁纸预览加载失败。", + "Download Failed": "下载失败", + + // MARK: App (mond.swift) + "Not supported!": "不支持!", + "Your iOS version may not be supported by mond.\nMond only supports iOS 27.0 developer beta 1 - 4.": "你的 iOS 版本可能不受 mond 支持。\nmond 仅支持 iOS 27.0 开发者测试版 1 - 4。", + ] +} diff --git a/mond/helpers/mg.swift b/mond/helpers/mg.swift index a05d033..d3676a0 100644 --- a/mond/helpers/mg.swift +++ b/mond/helpers/mg.swift @@ -176,7 +176,7 @@ let all_tweaks: [mg_tweak] = [ }, w_on: { dict in guard let cache_extra = dict["CacheExtra"] as? NSMutableDictionary else { return } - Alertinator.shared.alert(title: "Warning!", body: "Please do not use this feature to bypass region restrictions that would equate to breaking regional laws (e.g. disabling the camera shutter sound). We will NOT be held responsible for enabling any illegal activites!") + Alertinator.shared.alert(title: L10n.shared.t("Warning!"), body: L10n.shared.t("Please do not use this feature to bypass region restrictions that would equate to breaking regional laws (e.g. disabling the camera shutter sound). We will NOT be held responsible for enabling any illegal activites!")) cache_extra[region_code_key] = "LL" cache_extra[region_info_key] = "LL/A" }, @@ -200,8 +200,8 @@ let all_tweaks: [mg_tweak] = [ guard let cache_extra = dict["CacheExtra"] as? NSMutableDictionary else { return } cache_extra["A62OafQ85EJAiiqKn4agtg"] = 1 Alertinator.shared.alert( - title: "Apple Intelligence Spoof", - body: "How to use this tweak:\n1. Spoof to the model next to the first one supported by Apple Intelligence.\n2. Spoof back to your model.\n3. Spoof to your final model and you should see the Apple Intelligence icon in Settings.\n4. Connect iPhone to power and leave the Settings > Storage tab open for ~1 hour.\n\nNOTE: Do not spoof back." + title: L10n.shared.t("Apple Intelligence Spoof"), + body: L10n.shared.t("How to use this tweak:\n1. Spoof to the model next to the first one supported by Apple Intelligence.\n2. Spoof back to your model.\n3. Spoof to your final model and you should see the Apple Intelligence icon in Settings.\n4. Connect iPhone to power and leave the Settings > Storage tab open for ~1 hour.\n\nNOTE: Do not spoof back.") ) }, w_off: { dict in @@ -503,7 +503,7 @@ func mg_load() { is_valid = false is_empty = (try? FileManager.default.attributesOfItem(atPath: mg_url_now.path))?[.size] as? UInt64 == 0 is_loading = false - Alertinator.shared.alert(title: "Failed to load current MobileGestalt!", body: "Restart the app and try again. Check logs for more detailed information.") + Alertinator.shared.alert(title: L10n.shared.t("Failed to load current MobileGestalt!"), body: L10n.shared.t("Restart the app and try again. Check logs for more detailed information.")) } } } @@ -529,12 +529,12 @@ func mg_apply() { enable_device_name = false print("(mg) successfully overwrote mobilegestalt!") - Alertinator.shared.alert(title: "Successfully applied Gestalt tweaks!", body: "Respring your device for changes to take effect. Note that some tweaks may require a reboot for them to apply properly.", actionLabel: "Respring", action: { + Alertinator.shared.alert(title: L10n.shared.t("Successfully applied Gestalt tweaks!"), body: L10n.shared.t("Respring your device for changes to take effect. Note that some tweaks may require a reboot for them to apply properly."), actionLabel: L10n.shared.t("Respring"), action: { state.respring() }) } catch { print("(mg) failed to apply mobilegestalt: \(error)") - Alertinator.shared.alert(title: "Failed to apply MobileGestalt!", body: "Restart the app and try again. Check logs for more detailed information.") + Alertinator.shared.alert(title: L10n.shared.t("Failed to apply MobileGestalt!"), body: L10n.shared.t("Restart the app and try again. Check logs for more detailed information.")) } } @@ -545,10 +545,10 @@ func mg_revert() { try mg_write(backup_data) print("(mg) successfully reverted mobilegestalt!)") - Alertinator.shared.alert(title: "Successfully reverted Gestalt tweaks!", body: "Reboot your device for changes to take effect.") + Alertinator.shared.alert(title: L10n.shared.t("Successfully reverted Gestalt tweaks!"), body: L10n.shared.t("Reboot your device for changes to take effect.")) } catch { print("(mg) failed to revert mobilegestalt: \(error)") - Alertinator.shared.alert(title: "Failed to revert MobileGestalt!", body: "Check logs for error information.") + Alertinator.shared.alert(title: L10n.shared.t("Failed to revert MobileGestalt!"), body: L10n.shared.t("Check logs for error information.")) } } @@ -710,9 +710,9 @@ enum mg_view_err: Error, LocalizedError { var errorDescription: String? { switch self { case .missing_artwork_st: - return "Failed to get ArtworkDeviceSubType!" + return L10n.shared.t("Failed to get ArtworkDeviceSubType!") case .missing_artwork_device_name: - return "Failed to get ArtworkDeviceProductDescription!" + return L10n.shared.t("Failed to get ArtworkDeviceProductDescription!") } } } diff --git a/mond/mond.swift b/mond/mond.swift index 58fd40b..b5ccba6 100644 --- a/mond/mond.swift +++ b/mond/mond.swift @@ -55,6 +55,7 @@ struct mond: App { WindowGroup { ContentView() .environmentObject(state) + .environmentObject(L10n.shared) .onOpenURL { url in guard is_pb_archive(url) else { print("(mond) ignoring unsupported URL: \(url.lastPathComponent)") @@ -65,7 +66,7 @@ struct mond: App { } .onAppear() { if !is_supported() { - Alertinator.shared.alert(title: "Not supported!", body: "Your iOS version may not be supported by mond.\nMond only supports iOS 27.0 developer beta 1 - 4.") + Alertinator.shared.alert(title: L10n.shared.t("Not supported!"), body: L10n.shared.t("Your iOS version may not be supported by mond.\nMond only supports iOS 27.0 developer beta 1 - 4.")) } grant_all(state: state) diff --git a/mond/views/app/ContentView.swift b/mond/views/app/ContentView.swift index 133fab0..ca8f370 100644 --- a/mond/views/app/ContentView.swift +++ b/mond/views/app/ContentView.swift @@ -10,6 +10,7 @@ import PartyUI struct ContentView: View { @EnvironmentObject var state: AppState + @EnvironmentObject private var l10n: L10n @AppStorage("method") private var method: String = "bad_query" @AppStorage("ignore_failure") private var ignore_failure = false @@ -19,11 +20,26 @@ struct ContentView: View { var body: some View { NavigationStack { List { + Section { + Picker(selection: l10n.language_binding) { + ForEach(AppLanguage.allCases) { language in + Text(language.display_name).tag(language) + } + } label: { + Label(l10n.t("Language"), systemImage: "globe") + } + .pickerStyle(.segmented) + } header: { + Label(l10n.t("Language"), systemImage: "globe") + } footer: { + Text(l10n.t("Choose the language used by the app.")) + } + Section { LogView() .modifier(TerminalPlatter()) } header: { - Label("Logs", systemImage: "apple.terminal") + Label(l10n.t("Logs"), systemImage: "apple.terminal") } Section { @@ -69,10 +85,10 @@ struct ContentView: View { } .disabled((method == "cmg" || state.apps_granted != true) && !ignore_failure) } header: { - Label("Tweaks", systemImage: "paintbrush") + Label(l10n.t("Tweaks"), systemImage: "paintbrush") } footer: { if method == "cmg" { - Text("Only MobileGestalt is available when method is set to cmg.") + Text(l10n.t("Only MobileGestalt is available when method is set to cmg.")) } } @@ -80,12 +96,12 @@ struct ContentView: View { NavigationLink { CEView() } label: { - Text("CacheExtra Fields") + Text(l10n.t("CacheExtra Fields")) } } header: { - Label("Advanced", systemImage: "wrench.and.screwdriver") + Label(l10n.t("Advanced"), systemImage: "wrench.and.screwdriver") } footer: { - Text("Only use these Tweaks if you know what you're doing.\nYou could break something irreversibly.") + Text(l10n.t("Only use these Tweaks if you know what you're doing.\nYou could break something irreversibly.")) } } .navigationTitle("mond") diff --git a/mond/views/app/LogView.swift b/mond/views/app/LogView.swift index 3deaada..9921037 100644 --- a/mond/views/app/LogView.swift +++ b/mond/views/app/LogView.swift @@ -9,6 +9,7 @@ import SwiftUI import PartyUI struct LogView: View { + @EnvironmentObject private var l10n: L10n @State private var log = "" var body: some View { @@ -47,11 +48,10 @@ struct LogView: View { Button { UIPasteboard.general.string = log } label: { - Label("Copy Output", systemImage: "doc.on.doc") + Label(l10n.t("Copy Output"), systemImage: "doc.on.doc") } } } } } } - diff --git a/mond/views/app/SettingsView.swift b/mond/views/app/SettingsView.swift index f6cf9e1..c7ab2bc 100644 --- a/mond/views/app/SettingsView.swift +++ b/mond/views/app/SettingsView.swift @@ -11,6 +11,7 @@ import PartyUI struct SettingsView: View { @Environment(\.dismiss) private var dismiss @EnvironmentObject var state: AppState + @EnvironmentObject private var l10n: L10n @AppStorage("method") private var method: String = "bad_query" @AppStorage("ka_on") private var ka_on = true @@ -70,7 +71,7 @@ struct SettingsView: View { } Section { - Picker("Method", selection: $method) { + Picker(l10n.t("Method"), selection: $method) { Text("bad_query").tag("bad_query") Text("cmg").tag("cmg") } @@ -79,16 +80,18 @@ struct SettingsView: View { Button { grant_all(state: state) } label: { - Text("Run Exploit") + Text(l10n.t("Run Exploit")) } } header: { Label("Exploit", systemImage: "wrench.and.screwdriver") } footer: { - Text(method == "cmg" ? "**CMG:** Supports iOS 27.0 b1 - b4. Only MobileGestalt will work with this method. Only use this when bad_query isnt working for you." : "**bad_query:** Supports iOS 27.0 b1 - b4. By [forcequit](https://github.com/forcequitOS).") + Text(LocalizedStringKey(l10n.t(method == "cmg" + ? "**CMG:** Supports iOS 27.0 b1 - b4. Only MobileGestalt will work with this method. Only use this when bad_query isnt working for you." + : "**bad_query:** Supports iOS 27.0 b1 - b4. By [forcequit](https://github.com/forcequitOS)."))) } Section { - TextField("Sandbox Extension Token.", text: $token) + TextField(l10n.t("Sandbox Extension Token."), text: $token) .contextMenu { Text("Class: \(token.split(separator: ";").first { $0.contains("com.apple") }.map(String.init) ?? "N/A")") Text("Path: \(token.split(separator: ";").last.map(String.init) ?? "N/A")") @@ -96,35 +99,35 @@ struct SettingsView: View { Button { UIPasteboard.general.string = token } label: { - Label("Copy Token", systemImage: "doc.on.doc") + Label(l10n.t("Copy Token"), systemImage: "doc.on.doc") } } .lineLimit(1) Button { - token = sandbox_extension_issue_file(path: TweakPaths.gestalt_dir) ?? "Failed to get token." + token = sandbox_extension_issue_file(path: TweakPaths.gestalt_dir) ?? l10n.t("Failed to get token.") } label: { - Text("Generate Token") + Text(l10n.t("Generate Token")) } .disabled(!state.exploit_succeeded) } header: { Label("Token", systemImage: "key") } footer: { - if !token.isEmpty && token != "Failed to get token." { + if !token.isEmpty && token != l10n.t("Failed to get token.") { if valid { - Text("Your sandbox token is valid.") + Text(l10n.t("Your sandbox token is valid.")) } else { - Text("Your sandbox token is invalid.") + Text(l10n.t("Your sandbox token is invalid.")) } } if !state.exploit_succeeded { - Text("Disabled because the exploit failed. Is your iOS version supported?") + Text(l10n.t("Disabled because the exploit failed. Is your iOS version supported?")) } } Section { - PlainToggle(text: "Keep Alive", infoType: .info, infoMessage: "Keeps the app running in the background after you close it, allowing it to remain active.", isOn: $ka_on) + PlainToggle(text: l10n.t("Keep Alive"), infoType: .info, infoTitle: l10n.t("Information"), infoMessage: l10n.t("Keeps the app running in the background after you close it, allowing it to remain active."), isOn: $ka_on) .onChange(of: ka_on) { _, enabled in if enabled { keep_alive() @@ -133,59 +136,59 @@ struct SettingsView: View { } } - PlainToggle(text: "Dismiss after importing", infoType: .info, infoMessage: "When enabled, the Tendies explorer will close automatically after importing a .tendies file into PosterBoard.", isOn: $dismiss_after_import) + PlainToggle(text: l10n.t("Dismiss after importing"), infoType: .info, infoTitle: l10n.t("Information"), infoMessage: l10n.t("When enabled, the Tendies explorer will close automatically after importing a .tendies file into PosterBoard."), isOn: $dismiss_after_import) - PlainToggle(text: "Persist after reboot", infoType: .info, infoMessage: "When enabled, MobileGestalt is written in-place on the same inode, which (hopefully) prevents iOS from regenerating the cache on reboot. Disable to use atomic file replacement instead.", isOn: $atomic_write) + PlainToggle(text: l10n.t("Persist after reboot"), infoType: .info, infoTitle: l10n.t("Information"), infoMessage: l10n.t("When enabled, MobileGestalt is written in-place on the same inode, which (hopefully) prevents iOS from regenerating the cache on reboot. Disable to use atomic file replacement instead."), isOn: $atomic_write) - PlainToggle(text: "Ignore exploit failure", infoType: .info, infoMessage: "When enabled, All Tweaks will be accessible even if the exploit failed.", isOn: $ignore_failure) + PlainToggle(text: l10n.t("Ignore exploit failure"), infoType: .info, infoTitle: l10n.t("Information"), infoMessage: l10n.t("When enabled, All Tweaks will be accessible even if the exploit failed."), isOn: $ignore_failure) } header: { - Label("Settings", systemImage: "gear") + Label(l10n.t("Settings"), systemImage: "gear") } Section { Button { show_confirm = true } label: { - Text("Respring") + Text(l10n.t("Respring")) } } header: { - Label("Tools", systemImage: "wrench.and.screwdriver") + Label(l10n.t("Tools"), systemImage: "wrench.and.screwdriver") } footer: { - Text("Respring method by [neon](https://github.com/neonmodder123), swift implementation by [skadz](https://github.com/skadz108).") + Text(LocalizedStringKey(l10n.t("Respring method by [neon](https://github.com/neonmodder123), swift implementation by [skadz](https://github.com/skadz108)."))) } Section { - CreditsRow(name: "roooot", role: "Main developer", profile: URL(string: "https://github.com/rooootdev")!) - CreditsRow(name: "forcequit", role: "The bad_query exploit", profile: URL(string: "https://github.com/forcequitOS")!) - CreditsRow(name: "johnny", role: "His work on the MCM bug class", profile: URL(string: "https://github.com/0xjohnnydev")!) + CreditsRow(name: "roooot", role: l10n.t("Main developer"), profile: URL(string: "https://github.com/rooootdev")!) + CreditsRow(name: "forcequit", role: l10n.t("The bad_query exploit"), profile: URL(string: "https://github.com/forcequitOS")!) + CreditsRow(name: "johnny", role: l10n.t("His work on the MCM bug class"), profile: URL(string: "https://github.com/0xjohnnydev")!) CreditsRow(name: "jailbreak.party", role: "PartyUI, GestaltView", profile: URL(string: "https://github.com/jailbreakdotparty")!) - CreditsRow(name: "SerStars", role: "Tendies repository", profile: URL(string: "https://github.com/SerStars")!) + CreditsRow(name: "SerStars", role: l10n.t("Tendies repository"), profile: URL(string: "https://github.com/SerStars")!) } header: { - Label("Credits", systemImage: "person.3.fill") + Label(l10n.t("Credits"), systemImage: "person.3.fill") } } - .navigationTitle("Settings") + .navigationTitle(l10n.t("Settings")) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { HStack { Button { dismiss() } label: { - Text("Done") + Text(l10n.t("Done")) } } } } - .alert("Are you sure?", isPresented: $show_confirm) { - Button("Cancel") { + .alert(l10n.t("Are you sure?"), isPresented: $show_confirm) { + Button(l10n.t("Cancel")) { show_confirm = false } - Button("Confirm") { + Button(l10n.t("Confirm")) { state.respring() } } message: { - Text("Confirm that you want to respring.") + Text(l10n.t("Confirm that you want to respring.")) } } } diff --git a/mond/views/tweaks/SantanderView.swift b/mond/views/tweaks/SantanderView.swift index 6070335..83440ee 100644 --- a/mond/views/tweaks/SantanderView.swift +++ b/mond/views/tweaks/SantanderView.swift @@ -77,6 +77,7 @@ private struct SantanderDirectoryListing { struct SantanderDirectoryView: View { let path: SantanderPath + @EnvironmentObject private var l10n: L10n @State private var items: [SantanderPath] = [] @State private var empty_message: String? @State private var is_loading = true @@ -140,25 +141,25 @@ struct SantanderDirectoryView: View { NavigationLink { SantanderDirectoryView(path: root_path) } label: { - Label("Go to Root", systemImage: "externaldrive") + Label(l10n.t("Go to Root"), systemImage: "externaldrive") } NavigationLink { SantanderDirectoryView(path: home_path) } label: { - Label("Go to Home", systemImage: "house") + Label(l10n.t("Go to Home"), systemImage: "house") } Divider() - Toggle("Display hidden files", isOn: $display_hidden_files) + Toggle(l10n.t("Display hidden files"), isOn: $display_hidden_files) Divider() Button { sort_ascending = true } label: { - Label("Sort A-Z", systemImage: "textformat") + Label(l10n.t("Sort A-Z"), systemImage: "textformat") } Button { sort_ascending = false } label: { - Label("Sort Z-A", systemImage: "textformat") + Label(l10n.t("Sort Z-A"), systemImage: "textformat") } } label: { Image(systemName: "ellipsis.circle") @@ -194,11 +195,11 @@ struct SantanderDirectoryView: View { } private var empty_state_message: String { - if !search_text.isEmpty { return "No matching items." } + if !search_text.isEmpty { return l10n.t("No matching items.") } if !display_hidden_files && !items.isEmpty { - return "No visible items. Enable \"Display hidden files\" to show dotfiles." + return l10n.t("No visible items. Enable \"Display hidden files\" to show dotfiles.") } - return empty_message ?? "Directory is empty." + return l10n.t(empty_message ?? "Directory is empty.") } private func load() { @@ -299,6 +300,7 @@ struct SantanderDirectoryView: View { struct SantanderFileView: View { let path: SantanderPath + @EnvironmentObject private var l10n: L10n @State private var is_editing = false @State private var is_editable = false @State private var edit_text = "" @@ -319,10 +321,10 @@ struct SantanderFileView: View { .toolbar { ToolbarItemGroup(placement: .topBarTrailing) { if is_editing { - Button("Cancel") { + Button(l10n.t("Cancel")) { is_editing = false } - Button("Save") { + Button(l10n.t("Save")) { save() } } else { @@ -347,11 +349,11 @@ struct SantanderFileView: View { || Self.plist_editor_content(from: data) != nil } } - .alert("Save Failed", isPresented: Binding( + .alert(l10n.t("Save Failed"), isPresented: Binding( get: { save_error != nil }, set: { if !$0 { save_error = nil } } )) { - Button("OK", role: .cancel) {} + Button(l10n.t("OK"), role: .cancel) {} } message: { Text(save_error ?? "") } @@ -386,7 +388,7 @@ struct SantanderFileView: View { .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.black) } else { - ContentUnavailableView(failure_text("Failed to render image"), systemImage: "photo") + ContentUnavailableView(failure_text(l10n.t("Failed to render image")), systemImage: "photo") } } } @@ -424,7 +426,7 @@ struct SantanderFileView: View { private func save() { guard Self.grant_file_write(path.path) else { - save_error = "Missing write permission." + save_error = l10n.t("Missing write permission.") return } if let format = original_plist_format { @@ -432,7 +434,7 @@ struct SantanderFileView: View { return } guard let data = edit_text.data(using: original_encoding) else { - save_error = "Could not encode text." + save_error = l10n.t("Could not encode text.") return } do { @@ -445,7 +447,7 @@ struct SantanderFileView: View { private func save_plist(format: PropertyListSerialization.PropertyListFormat) { guard let edited_data = edit_text.data(using: .utf8) else { - save_error = "Could not encode text." + save_error = l10n.t("Could not encode text.") return } let object: Any @@ -454,7 +456,7 @@ struct SantanderFileView: View { } else if let plist = try? PropertyListSerialization.propertyList(from: edited_data, options: [], format: nil) { object = plist } else { - save_error = "Edited text is not a valid plist/JSON document." + save_error = l10n.t("Edited text is not a valid plist/JSON document.") return } do { @@ -537,9 +539,9 @@ struct SantanderFileView: View { guard grant_file_read(url.path), let granted = try? Data(contentsOf: url, options: .mappedIfSafe) else { return """ - Failed to read file: + \(L10n.shared.t("Failed to read file:")) \(url.path) - Error: \(error.localizedDescription) + \(L10n.shared.t("Error: %@", error.localizedDescription)) \(unreadable_file_details(for: url)) """ @@ -548,7 +550,7 @@ struct SantanderFileView: View { } if data.isEmpty { - return "(empty file)" + return L10n.shared.t("(empty file)") } if let plist = decode_property_list(from: data) { @@ -568,9 +570,9 @@ struct SantanderFileView: View { var is_dir = ObjCBool(false) let exists = fm.fileExists(atPath: url.path, isDirectory: &is_dir) - lines.append("Exists: \(exists ? "yes" : "no")") + lines.append(L10n.shared.t("Exists: %@", exists ? L10n.shared.t("yes") : L10n.shared.t("no"))) if exists { - lines.append("Kind: \(is_dir.boolValue ? "directory" : "regular item")") + lines.append(L10n.shared.t("Kind: %@", is_dir.boolValue ? L10n.shared.t("directory") : L10n.shared.t("regular item"))) } let keys: Set = [ @@ -581,36 +583,36 @@ struct SantanderFileView: View { ] if let values = try? url.resourceValues(forKeys: keys) { if let type = values.contentType { - lines.append("UTType: \(type.identifier)") + lines.append(L10n.shared.t("UTType: %@", type.identifier)) } if let size = values.fileSize { - lines.append("Size: \(size) bytes") + lines.append(L10n.shared.t("Size: %d bytes", size)) } if let is_symlink = values.isSymbolicLink { - lines.append("Symlink: \(is_symlink ? "yes" : "no")") + lines.append(L10n.shared.t("Symlink: %@", is_symlink ? L10n.shared.t("yes") : L10n.shared.t("no"))) } if values.isSymbolicLink == true, let target = try? fm.destinationOfSymbolicLink(atPath: url.path) { - lines.append("Symlink target: \(target)") + lines.append(L10n.shared.t("Symlink target: %@", target)) } if let is_alias = values.isAliasFile { - lines.append("Alias file: \(is_alias ? "yes" : "no")") + lines.append(L10n.shared.t("Alias file: %@", is_alias ? L10n.shared.t("yes") : L10n.shared.t("no"))) } } if let attrs = try? fm.attributesOfItem(atPath: url.path) { if let file_type = attrs[.type] as? FileAttributeType { - lines.append("File attribute type: \(file_type.rawValue)") + lines.append(L10n.shared.t("File attribute type: %@", file_type.rawValue)) } let owner_name = attrs[.ownerAccountName] as? String let owner_id = (attrs[.ownerAccountID] as? NSNumber)?.intValue switch (owner_name, owner_id) { case let (name?, id?): - lines.append("Owner: \(name) (\(id))") + lines.append(L10n.shared.t("Owner: %@ (%d)", name, id)) case let (name?, nil): - lines.append("Owner: \(name)") + lines.append(L10n.shared.t("Owner: %@", name)) case let (nil, id?): - lines.append("Owner ID: \(id)") + lines.append(L10n.shared.t("Owner ID: %d", id)) default: break } @@ -619,22 +621,22 @@ struct SantanderFileView: View { let group_id = (attrs[.groupOwnerAccountID] as? NSNumber)?.intValue switch (group_name, group_id) { case let (name?, id?): - lines.append("Group: \(name) (\(id))") + lines.append(L10n.shared.t("Group: %@ (%d)", name, id)) case let (name?, nil): - lines.append("Group: \(name)") + lines.append(L10n.shared.t("Group: %@", name)) case let (nil, id?): - lines.append("Group ID: \(id)") + lines.append(L10n.shared.t("Group ID: %d", id)) default: break } if let perms = attrs[.posixPermissions] as? NSNumber { - lines.append(String(format: "POSIX perms: %04o", perms.intValue)) + lines.append(L10n.shared.t("POSIX perms: %04o", perms.intValue)) } } - lines.append("Readable: \(fm.isReadableFile(atPath: url.path) ? "yes" : "no")") - lines.append("Writable: \(fm.isWritableFile(atPath: url.path) ? "yes" : "no")") - lines.append("Executable: \(fm.isExecutableFile(atPath: url.path) ? "yes" : "no")") + lines.append(L10n.shared.t("Readable: %@", fm.isReadableFile(atPath: url.path) ? L10n.shared.t("yes") : L10n.shared.t("no"))) + lines.append(L10n.shared.t("Writable: %@", fm.isWritableFile(atPath: url.path) ? L10n.shared.t("yes") : L10n.shared.t("no"))) + lines.append(L10n.shared.t("Executable: %@", fm.isExecutableFile(atPath: url.path) ? L10n.shared.t("yes") : L10n.shared.t("no"))) return lines.joined(separator: "\n") } @@ -713,7 +715,7 @@ struct SantanderFileView: View { let limit = min(data.count, 4096) let chunk = data.prefix(limit) var lines: [String] = [] - lines.append("Binary data (\(data.count) bytes). Showing first \(limit) bytes:") + lines.append(L10n.shared.t("Binary data (%d bytes). Showing first %d bytes:", data.count, limit)) lines.append("") var offset = 0 diff --git a/mond/views/tweaks/mobilegestalt/CEView.swift b/mond/views/tweaks/mobilegestalt/CEView.swift index bfe42c5..47d5b10 100644 --- a/mond/views/tweaks/mobilegestalt/CEView.swift +++ b/mond/views/tweaks/mobilegestalt/CEView.swift @@ -52,16 +52,16 @@ func ce_parse(_ text: String, as type: ce_type) throws -> Any { case .str: return text case .int: - guard let v = Int64(t) else { throw ce_err.bad_val("Invalid Integer") } + guard let v = Int64(t) else { throw ce_err.bad_val(L10n.shared.t("Invalid Integer")) } return NSNumber(value: v) case .bool: switch t.lowercased() { case "true", "1", "yes": return NSNumber(value: true) case "false", "0", "no": return NSNumber(value: false) - default: throw ce_err.bad_val("Enter true/false") + default: throw ce_err.bad_val(L10n.shared.t("Enter true/false")) } case .data: - guard let d = Data(base64Encoded: t) else { throw ce_err.bad_val("Invalid base64") } + guard let d = Data(base64Encoded: t) else { throw ce_err.bad_val(L10n.shared.t("Invalid base64")) } return d } } @@ -78,6 +78,7 @@ enum ce_err: LocalizedError { struct CEView: View { @Environment(\.dismiss) private var dismiss + @EnvironmentObject private var l10n: L10n @State private var search = "" @State private var editing: CEField? @@ -97,7 +98,7 @@ struct CEView: View { var body: some View { List { if keys.isEmpty { - Text(search.isEmpty ? "No CacheExtra fields" : "No results") + Text(l10n.t(search.isEmpty ? "No CacheExtra fields" : "No results")) .foregroundStyle(.secondary) } else { ForEach(keys, id: \.self) { key in @@ -135,7 +136,7 @@ struct CEView: View { } } } - .searchable(text: $search, prompt: "Search for keys or values") + .searchable(text: $search, prompt: l10n.t("Search for keys or values")) .navigationTitle("CacheExtra") .onAppear() { mg_load() @@ -179,15 +180,15 @@ struct CEView: View { try mg_write(data) Alertinator.shared.alert( - title: "Saved", - body: "Field changes written to MobileGestalt. Respring for changes to take effect.", - actionLabel: "Respring", + title: L10n.shared.t("Saved"), + body: L10n.shared.t("Field changes written to MobileGestalt. Respring for changes to take effect."), + actionLabel: L10n.shared.t("Respring"), action: { state.respring() } ) } catch { - Alertinator.shared.alert(title: "Save failed", body: error.localizedDescription) + Alertinator.shared.alert(title: L10n.shared.t("Save failed"), body: error.localizedDescription) } } } @@ -200,6 +201,7 @@ private struct CEField: Identifiable { private struct CEEditSheet: View { @Environment(\.dismiss) private var dismiss + @EnvironmentObject private var l10n: L10n let key: String let save: (String, Any) -> Void @@ -219,16 +221,16 @@ private struct CEEditSheet: View { var body: some View { NavigationStack { Form { - Section("Type") { - Picker("Type", selection: $type) { - ForEach(ce_type.allCases) { t in Text(t.label).tag(t) } + Section(l10n.t("Type")) { + Picker(l10n.t("Type"), selection: $type) { + ForEach(ce_type.allCases) { t in Text(l10n.t(t.label)).tag(t) } } .pickerStyle(.menu) .onChange(of: type) { _, _ in error = nil } } - Section("Value") { + Section(l10n.t("Value")) { TextEditor(text: $text) .font(.system(.body, design: .monospaced)) .frame(minHeight: 120) @@ -243,10 +245,10 @@ private struct CEEditSheet: View { .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarLeading) { - Button("Cancel") { dismiss() } + Button(l10n.t("Cancel")) { dismiss() } } ToolbarItem(placement: .navigationBarTrailing) { - Button("Done") { + Button(l10n.t("Done")) { do { save(key, try ce_parse(text, as: type)) dismiss() @@ -263,6 +265,7 @@ private struct CEEditSheet: View { private struct CEAddSheet: View { @Environment(\.dismiss) private var dismiss + @EnvironmentObject private var l10n: L10n let save: (String, Any) -> Void @@ -274,19 +277,19 @@ private struct CEAddSheet: View { var body: some View { NavigationStack { Form { - Section("Key") { - TextField("Key name", text: $key) + Section(l10n.t("Key")) { + TextField(l10n.t("Key name"), text: $key) .font(.system(.body, design: .monospaced)) .autocorrectionDisabled() .textInputAutocapitalization(.never) } - Section("Type") { - Picker("Type", selection: $type) { - ForEach(ce_type.allCases) { t in Text(t.label).tag(t) } + Section(l10n.t("Type")) { + Picker(l10n.t("Type"), selection: $type) { + ForEach(ce_type.allCases) { t in Text(l10n.t(t.label)).tag(t) } } .pickerStyle(.menu) } - Section("Value") { + Section(l10n.t("Value")) { TextEditor(text: $text) .font(.system(.body, design: .monospaced)) .frame(minHeight: 100) @@ -297,21 +300,21 @@ private struct CEAddSheet: View { Section { Text(error).foregroundStyle(.red) } } } - .navigationTitle("Add Field") + .navigationTitle(l10n.t("Add Field")) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarLeading) { - Button("Cancel") { dismiss() } + Button(l10n.t("Cancel")) { dismiss() } } ToolbarItem(placement: .navigationBarTrailing) { - Button("Add") { + Button(l10n.t("Add")) { let trimmed = key.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { - error = "Key cannot be empty" + error = L10n.shared.t("Key cannot be empty") return } guard (mg_dict_now["CacheExtra"] as? [String: Any])?[trimmed] == nil else { - error = "Key already exists" + error = L10n.shared.t("Key already exists") return } do { @@ -330,12 +333,12 @@ private struct CEAddSheet: View { private func ce_summary(_ value: Any?) -> String { switch value { - case let s as String: s.isEmpty ? "(empty str)" : s + case let s as String: s.isEmpty ? L10n.shared.t("(empty str)") : s case let n as NSNumber: CFGetTypeID(n) == CFBooleanGetTypeID() ? (n.boolValue ? "true" : "false") : n.stringValue - case let d as Data: "Data (\(d.count) bytes)" + case let d as Data: L10n.shared.t("Data (%d bytes)", d.count) default: String(describing: value ?? "nil") } } diff --git a/mond/views/tweaks/mobilegestalt/GestaltView.swift b/mond/views/tweaks/mobilegestalt/GestaltView.swift index e88ca92..f556367 100644 --- a/mond/views/tweaks/mobilegestalt/GestaltView.swift +++ b/mond/views/tweaks/mobilegestalt/GestaltView.swift @@ -18,14 +18,16 @@ private extension mg_tweak.InfoType { } struct TweakToggle: View { + @EnvironmentObject private var l10n: L10n let title: String var body: some View { if let tweak = tweak(title) { PlainToggle( - text: tweak.title, + text: l10n.t(tweak.title), infoType: tweak.info_t?.party_info_type ?? .none, - infoMessage: tweak.info_msg ?? "", + infoTitle: l10n.t("Information"), + infoMessage: l10n.t(tweak.info_msg ?? ""), minSupportedVersion: tweak.minv ?? 0.0, isOn: mg_tweak_binding(tweak) ) @@ -46,6 +48,7 @@ fileprivate func mg_apply_ui_state(_ selected: String, _ enableName: Bool, _ dev struct GestaltView: View { @EnvironmentObject var state: AppState + @EnvironmentObject private var l10n: L10n @State private var show_settings: Bool = false @@ -60,16 +63,16 @@ struct GestaltView: View { if !is_valid || is_empty { Section { if is_empty { - PlainAlert(title: "Do not reboot!", icon: "exclamationmark.triangle.fill", text: "Your MobileGestalt.plist seems to be empty.", color: Color.yellow) + PlainAlert(title: l10n.t("Do not reboot!"), icon: "exclamationmark.triangle.fill", text: l10n.t("Your MobileGestalt.plist seems to be empty."), color: Color.yellow) } if !is_valid { - PlainAlert(title: "Do not reboot!", icon: "exclamationmark.triangle.fill", text: "Your MobileGestalt.plist seems to be invalid.", color: Color.yellow) + PlainAlert(title: l10n.t("Do not reboot!"), icon: "exclamationmark.triangle.fill", text: l10n.t("Your MobileGestalt.plist seems to be invalid."), color: Color.yellow) } } header: { - Label("Warning", systemImage: "exclamationmark.triangle") + Label(l10n.t("Warning"), systemImage: "exclamationmark.triangle") } footer: { - Text("Rebooting now might cause a bootloop. Try pressing 'Revert Tweaks'. If the warnings dont go away after that, you're fucked.") + Text(l10n.t("Rebooting now might cause a bootloop. Try pressing 'Revert Tweaks'. If the warnings dont go away after that, you're fucked.")) } } @@ -78,30 +81,30 @@ struct GestaltView: View { mg_apply_ui_state(selected_st, enable_device_name, mg_device_name, product_type) mg_apply() } label: { - Text("Apply Tweaks") + Text(l10n.t("Apply Tweaks")) } Button { mg_revert() } label: { - Text("Revert Tweaks") + Text(l10n.t("Revert Tweaks")) } Button { state.respring() } label: { - Text("Respring") + Text(l10n.t("Respring")) } } footer: { - Text("**WARNING:** These tweaks have the capability to break features on your device or softbrick it if misused!") + Text(LocalizedStringKey(l10n.t("**WARNING:** These tweaks have the capability to break features on your device or softbrick it if misused!"))) } Section { Picker(selection: $selected_st) { - Text("Original (\(og_st))").tag("og") + Text(l10n.t("Original (%@)", String(og_st))).tag("og") if is_device_good() { - Text("Disable Dynamic Island").tag("no_dynamic_island") + Text(l10n.t("Disable Dynamic Island")).tag("no_dynamic_island") } Text("iPhone 14 Pro").tag("14p") @@ -118,22 +121,22 @@ struct GestaltView: View { } if has_home_button() { - Text("iPhone X Gestures").tag("x") + Text(l10n.t("iPhone X Gestures")).tag("x") } } label: { HStack { - Text("Subtype") + Text(l10n.t("Subtype")) Spacer() } } - Toggle("Custom Device Name", isOn: $enable_device_name) + Toggle(l10n.t("Custom Device Name"), isOn: $enable_device_name) if enable_device_name { - TextField("Device Name", text: $mg_device_name) + TextField(l10n.t("Device Name"), text: $mg_device_name) } } header: { - Label("Device Artwork", systemImage: "paintbrush.pointed") + Label(l10n.t("Device Artwork"), systemImage: "paintbrush.pointed") } Section { @@ -146,7 +149,7 @@ struct GestaltView: View { TweakToggle(title: "Enable Liquid Glass Low-Performance Mode") TweakToggle(title: "Disable Liquid Glass Low-Performance Mode") } header: { - Label("Software-Oriented Features", systemImage: "gearshape") + Label(l10n.t("Software-Oriented Features"), systemImage: "gearshape") } Section { @@ -158,7 +161,7 @@ struct GestaltView: View { } TweakToggle(title: "Pulse Width Modulation") } header: { - Label("Hardware-Oriented Features", systemImage: "iphone") + Label(l10n.t("Hardware-Oriented Features"), systemImage: "iphone") } Section { @@ -167,8 +170,8 @@ struct GestaltView: View { TweakToggle(title: "Apple Intelligence") HStack(spacing: 10) { - Picker("Spoofing", selection: $product_type) { - Text("Default").tag(machine_name()) + Picker(l10n.t("Spoofing"), selection: $product_type) { + Text(l10n.t("Default")).tag(machine_name()) if UIDevice.current.userInterfaceIdiom == .pad { if doubleSystemVersion() >= 17.4 { Text("iPad Pro 11-inch (M4)").tag("iPad16,3") @@ -196,8 +199,8 @@ struct GestaltView: View { Button { Alertinator.shared.alert( - title: "Device Spoofing Info", - body: "Only spoof your device model if you want to download Apple Intelligence. This may break Face ID. If you decide to unspoof and want to keep Apple Intelligence, do NOT re-enter the Apple Intelligence & Siri menu in Settings." + title: L10n.shared.t("Device Spoofing Info"), + body: L10n.shared.t("Only spoof your device model if you want to download Apple Intelligence. This may break Face ID. If you decide to unspoof and want to keep Apple Intelligence, do NOT re-enter the Apple Intelligence & Siri menu in Settings.") ) } label: { Image(systemName: "info.circle") @@ -206,7 +209,7 @@ struct GestaltView: View { .buttonStyle(.plain) } } header: { - Label("Eligibility", systemImage: "checklist") + Label(l10n.t("Eligibility"), systemImage: "checklist") } Section { @@ -222,7 +225,7 @@ struct GestaltView: View { TweakToggle(title: "Enable iPadOS Mode") .disabled(cache_extra?["+3Uf0Pm5F8Xy7Onyvko0vA"] as? String != "iPhone") } header: { - Label("iPadOS Features", systemImage: "ipad") + Label(l10n.t("iPadOS Features"), systemImage: "ipad") } Section { @@ -230,7 +233,7 @@ struct GestaltView: View { TweakToggle(title: "Internal Features") TweakToggle(title: "Apple Internal Install") } header: { - Label("Internal", systemImage: "ant") + Label(l10n.t("Internal"), systemImage: "ant") } } .navigationTitle("MobileGestalt") diff --git a/mond/views/tweaks/posterboard/PosterView.swift b/mond/views/tweaks/posterboard/PosterView.swift index 7a95401..d3ee748 100644 --- a/mond/views/tweaks/posterboard/PosterView.swift +++ b/mond/views/tweaks/posterboard/PosterView.swift @@ -13,6 +13,7 @@ import PartyUI struct PosterView: View { @Environment(\.dismiss) private var dismiss @EnvironmentObject var state: AppState + @EnvironmentObject private var l10n: L10n @State private var show_settings: Bool = false @State private var show_importer: Bool = false @@ -31,7 +32,7 @@ struct PosterView: View { ProgressView() } - Text("Apply") + Text(l10n.t("Apply")) } } .disabled(state.poster_files.isEmpty || busy) @@ -40,7 +41,7 @@ struct PosterView: View { Button { reset() } label: { - Text("Reset") + Text(l10n.t("Reset")) } .disabled(busy) } @@ -50,18 +51,18 @@ struct PosterView: View { Button { show_importer = true } label: { - Text("Import Tendies") + Text(l10n.t("Import Tendies")) } .disabled(busy) Button { show_explorer = true } label: { - Text("Explore Tendies") + Text(l10n.t("Explore Tendies")) } .disabled(busy) } footer: { - Text("Import up to 5 wallpaper packs.\n**NOTE:** Importing more than 5 tendies at once is not a good idea and importing more than 15 is a TERRIBLE idea. Dont say I didnt warn you.") + Text(LocalizedStringKey(l10n.t("Import up to 5 wallpaper packs.\n**NOTE:** Importing more than 5 tendies at once is not a good idea and importing more than 15 is a TERRIBLE idea. Dont say I didnt warn you."))) } if !state.poster_files.isEmpty { @@ -73,7 +74,7 @@ struct PosterView: View { state.remove_poster_files(at: offsets) } } header: { - Label("Imported", systemImage: "document.on.document") + Label(l10n.t("Imported"), systemImage: "document.on.document") } } } @@ -113,9 +114,9 @@ struct PosterView: View { print("(pb) applied \(count) descriptor(s).") busy = false Alertinator.shared.alert( - title: "Successfully applied PosterBoard!", - body: "For changes to take effect:\n1. Click 'Open' to launch Posterboard\n2. Close it from the App Switcher", - actionLabel: "Open", + title: L10n.shared.t("Successfully applied PosterBoard!"), + body: L10n.shared.t("For changes to take effect:\n1. Click 'Open' to launch Posterboard\n2. Close it from the App Switcher"), + actionLabel: L10n.shared.t("Open"), action: { // state.respring() @@ -128,8 +129,8 @@ struct PosterView: View { print("(pb) failed: \(error.localizedDescription)\n") busy = false Alertinator.shared.alert( - title: "Failed to apply PosterBoard!", - body: "Restart the app and try again. Check logs for more detailed information." + title: L10n.shared.t("Failed to apply PosterBoard!"), + body: L10n.shared.t("Restart the app and try again. Check logs for more detailed information.") ) } } @@ -141,9 +142,9 @@ struct PosterView: View { print("(pb) reset done.") busy = false Alertinator.shared.alert( - title: "Successfully reverted PosterBoard!", - body: "Respring your device for changes to take effect.", - actionLabel: "Respring", + title: L10n.shared.t("Successfully reverted PosterBoard!"), + body: L10n.shared.t("Respring your device for changes to take effect."), + actionLabel: L10n.shared.t("Respring"), action: { state.respring() } @@ -152,8 +153,8 @@ struct PosterView: View { print("(pb) failed: \(error.localizedDescription)") busy = false Alertinator.shared.alert( - title: "Failed to revert PosterBoard!", - body: "Restart the app and try again. Check logs for more detailed information." + title: L10n.shared.t("Failed to revert PosterBoard!"), + body: L10n.shared.t("Restart the app and try again. Check logs for more detailed information.") ) } } diff --git a/mond/views/tweaks/posterboard/TendiesView.swift b/mond/views/tweaks/posterboard/TendiesView.swift index 0a6be6e..490574b 100644 --- a/mond/views/tweaks/posterboard/TendiesView.swift +++ b/mond/views/tweaks/posterboard/TendiesView.swift @@ -10,6 +10,7 @@ import UIKit struct TendiesView: View { @EnvironmentObject private var state: AppState + @EnvironmentObject private var l10n: L10n @Environment(\.dismiss) private var dismiss @AppStorage("dismiss_after_import") private var dismiss_after_import = false @State private var vm = TendiesVM() @@ -19,14 +20,14 @@ struct TendiesView: View { NavigationStack { Group { if vm.loading && vm.wallpapers.isEmpty { - ProgressView("Loading wallpapers…") + ProgressView(l10n.t("Loading wallpapers…")) } else if let error = vm.error_msg, vm.wallpapers.isEmpty { ContentUnavailableView { - Label("Couldn't Load tendies", systemImage: "wifi.exclamationmark") + Label(l10n.t("Couldn't Load tendies"), systemImage: "wifi.exclamationmark") } description: { Text(error) } actions: { - Button("Try Again") { + Button(l10n.t("Try Again")) { Task { await vm.retry() } @@ -39,7 +40,7 @@ struct TendiesView: View { .navigationTitle("Tendies") .searchable( text: $vm.query, - prompt: "Search wallpapers" + prompt: l10n.t("Search wallpapers") ) .toolbar { ToolbarItem(placement: .topBarTrailing) { @@ -57,11 +58,11 @@ struct TendiesView: View { await vm.load() } } - .alert("Import Failed", isPresented: Binding( + .alert(l10n.t("Import Failed"), isPresented: Binding( get: { import_error != nil }, set: { if !$0 { import_error = nil } } )) { - Button("OK", role: .cancel) {} + Button(l10n.t("OK"), role: .cancel) {} } message: { Text(import_error ?? "") } @@ -149,7 +150,7 @@ struct TendiesView: View { await add_to_imported(wallpaper) } } label: { - Label("Add to Imported", systemImage: "arrow.down.circle") + Label(l10n.t("Add to Imported"), systemImage: "arrow.down.circle") } } } @@ -218,6 +219,7 @@ struct TendiesDetail: View { let dismiss_explorer: () -> Void @EnvironmentObject private var state: AppState + @EnvironmentObject private var l10n: L10n @AppStorage("dismiss_after_import") private var dismiss_after_import = false @State private var importing = false @@ -241,7 +243,7 @@ struct TendiesDetail: View { .clipShape(RoundedRectangle(cornerRadius: 20)) case .failure: - ContentUnavailableView("Preview Unavailable", systemImage: "photo", description: Text("The wallpaper preview couldn't be loaded.")) + ContentUnavailableView(l10n.t("Preview Unavailable"), systemImage: "photo", description: Text(l10n.t("The wallpaper preview couldn't be loaded."))) @unknown default: EmptyView() @@ -291,10 +293,10 @@ struct TendiesDetail: View { if importing { HStack { ProgressView() - Text("Downloading...") + Text(l10n.t("Downloading...")) } } else { - Text("Add to Imported") + Text(l10n.t("Add to Imported")) } } .disabled(importing) @@ -302,11 +304,11 @@ struct TendiesDetail: View { } } .navigationTitle(wallpaper.name) - .alert("Download Failed", isPresented: Binding( + .alert(l10n.t("Download Failed"), isPresented: Binding( get: { import_error != nil }, set: { if !$0 { import_error = nil } } )) { - Button("OK", role: .cancel) {} + Button(l10n.t("OK"), role: .cancel) {} } message: { Text(import_error ?? "") }