From 25cfafda1b25e4e6a81eb9bcf12e64218999a4fd Mon Sep 17 00:00:00 2001 From: pkssky Date: Tue, 27 Oct 2026 18:39:22 +0800 Subject: [PATCH 1/2] Add switchable zh-CN / en-US localization (WinUI + docs) - Add WinRT-free L10n provider (Core/L10n.cs) bridged to WinUI ResourceLoader by App/Localization.cs, persisted via ApplicationLanguages.PrimaryLanguageOverride - Add language selector in sidebar (System / en-US / zh-CN) with restart-to-apply - Tag all static XAML text with x:Uid and add Strings/zh-CN/Resources.resw (546 entries, generated by tools/gen_resw.py for consistent escaping) - Localize user-visible C# strings across Models, CleanupCatalog, Choices, OperationFlow and MainWindow with English fallbacks - Add Chinese docs: README.zh-CN.md, docs/usage.zh-CN.md, docs/support.zh-CN.md and cross-language links in the English docs --- README.md | 2 + README.zh-CN.md | 64 + docs/support.md | 2 + docs/support.zh-CN.md | 29 + docs/usage.md | 2 + docs/usage.zh-CN.md | 362 ++++ src/Tiny11Builder.App/App.xaml.cs | 1 + src/Tiny11Builder.App/Choices.cs | 37 +- src/Tiny11Builder.App/Localization.cs | 73 + src/Tiny11Builder.App/MainWindow.xaml | 306 +-- src/Tiny11Builder.App/MainWindow.xaml.cs | 327 ++-- src/Tiny11Builder.App/OperationFlow.cs | 96 +- .../Strings/zh-CN/Resources.resw | 1670 +++++++++++++++++ .../Tiny11Builder.App.csproj | 1 + src/Tiny11Builder.Core/CleanupCatalog.cs | 14 +- src/Tiny11Builder.Core/L10n.cs | 19 + src/Tiny11Builder.Core/Models.cs | 20 +- tools/gen_resw.py | 672 +++++++ 18 files changed, 3344 insertions(+), 353 deletions(-) create mode 100644 README.zh-CN.md create mode 100644 docs/support.zh-CN.md create mode 100644 docs/usage.zh-CN.md create mode 100644 src/Tiny11Builder.App/Localization.cs create mode 100644 src/Tiny11Builder.App/Strings/zh-CN/Resources.resw create mode 100644 src/Tiny11Builder.Core/L10n.cs create mode 100644 tools/gen_resw.py diff --git a/README.md b/README.md index e3f13795..c3713a80 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Tiny11 Builder +English | [简体中文](README.zh-CN.md) + Build a smaller Windows 11 ISO with a native WinUI app. Download an official Windows image or use your own, choose what to remove, and create your ISO. diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 00000000..5f0581dd --- /dev/null +++ b/README.zh-CN.md @@ -0,0 +1,64 @@ +# Tiny11 Builder(简体中文) + +[English](../README.md) | 简体中文 + +使用原生 WinUI 应用构建更小的 Windows 11 ISO。可下载官方 Windows 映像或使用本地映像,选择要移除的内容,然后创建你的 ISO。 + +## 下载 + +[**获取最新版本**](https://github.com/ntdevlabs/tiny11builder/releases/latest) + +Intel/AMD 电脑选择 **x64**,ARM 版 Windows 选择 **ARM64**。解压完整 ZIP 后运行 `Tiny11Builder.exe`。需要管理员权限。 + +## 选择构建模式 + +| 模式 | 作用 | +| --- | --- | +| Tiny11 | 移除预装应用和不需要的功能,同时保留 Windows 更新、Defender 和恢复环境。 | +| Tiny11 Core | 进一步精简:移除维护与恢复支持,并禁用 Defender。 | +| 自定义(Custom) | 逐项选择要移除或保留的应用、组件、功能和服务。 | + +Turbo 模式可加快映像处理速度。映像提交或压缩后中断的构建可从已保存的进度继续。Core 和组件存储移除会自动使用旧版 Windows 安装程序。 + +**Core 无法再更新或维护。自定义模式移除受保护项可能导致 Windows 无法使用。安装前请先在虚拟机中测试你的 ISO。** + +## 视频 + +[**在 YouTube 上观看**](https://youtu.be/LfEWIz5mQ8A) + +## 截图 + +### 欢迎页 + +![Tiny11 Builder 欢迎界面,包含 Tiny11、Tiny11 Core 与自定义模式](images/ui-welcome.png) + +### 组件 + +![组件选择与 Tiny11 Core 警告](images/ui-components.png) + +### 构建设置 + +![包含 Turbo 模式与可选映像清理的构建设置](images/ui-build-settings.png) + +## 更多信息 + +[用法、兼容性与构建说明(中文)](usage.zh-CN.md) · +[署名与分发说明](../NOTICE.md) + +原始 PowerShell 构建脚本仍保留在本仓库中。 + +## 支持本项目 + +Tiny11 Builder 完全免费。如果它为你节省了时间,你可以赞助开发、跨 Windows 版本的测试以及问题修复。 + +**[按月支持](https://github.com/sponsors/ntdevlabs)** · +**[一次性捐赠](https://ko-fi.com/ntdev)** + +偏好其他平台?[Patreon](https://www.patreon.com/ntdev) · +[PayPal](https://paypal.me/ntdev2) + +使用任何功能都无需捐赠。反馈可复现的 Bug、在虚拟机中测试构建、分享[演示视频](https://youtu.be/LfEWIz5mQ8A)同样是对项目的帮助。 + +## 界面语言 + +应用内置简体中文与英文界面,可在侧边栏“设置”中切换,重启应用后生效。本页与 [用法说明](usage.zh-CN.md)、[支持说明](support.zh-CN.md) 提供中文文档。 diff --git a/docs/support.md b/docs/support.md index c7fa3b9c..9065d5af 100644 --- a/docs/support.md +++ b/docs/support.md @@ -1,5 +1,7 @@ # Support Tiny11 Builder +English | [简体中文](support.zh-CN.md) + Tiny11 Builder is free, including all build modes and removal options. Optional support helps fund development time, testing across Windows releases and fixes for reported problems. diff --git a/docs/support.zh-CN.md b/docs/support.zh-CN.md new file mode 100644 index 00000000..37b3a68c --- /dev/null +++ b/docs/support.zh-CN.md @@ -0,0 +1,29 @@ +# 支持 Tiny11 Builder(简体中文) + +[English](support.md) | 简体中文 + +Tiny11 Builder 完全免费,包括所有构建模式与移除选项。可选的支持用于资助开发时间、跨 Windows 版本的测试以及已报告问题的修复。 + +**[GitHub Sponsors 按月支持](https://github.com/sponsors/ntdevlabs)** · +**[Ko-fi 一次性捐赠](https://ko-fi.com/ntdev)** + +其他平台:[Patreon](https://www.patreon.com/ntdev) · +[PayPal](https://paypal.me/ntdev2)。 + +你还可以通过报告可复现的 Bug、在虚拟机中测试输出以及分享[视频演示](https://youtu.be/LfEWIz5mQ8A)来帮助项目。提交 Bug 时请附上应用版本、映像 build 号、所选选项与相关日志;分享日志前请移除个人路径与私密信息。 + +## 视频简介或置顶评论文案 + +> Tiny11 Builder 完全免费。如果它为你节省了时间,你可以支持它的开发: +> +> 按月:https://github.com/sponsors/ntdevlabs +> +> 一次性:https://ko-fi.com/ntdev +> +> 下载:https://github.com/ntdevlabs/tiny11builder/releases/latest +> +> 提交 Bug 与分享项目同样有帮助。应用的所有功能始终保持免费。 + +## 项目动态 + +每个版本的说明都会链接到对应的源码,并描述修复内容、已知限制与剩余工作。进度更新请保持具体:修复了什么、测试了什么、还有什么需要做。只有当资金目标反映真实、有据可查的成本时才设定;本文不声称任何目标或支出数字。 diff --git a/docs/usage.md b/docs/usage.md index 7676c46a..33d8b762 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -1,5 +1,7 @@ # Tiny11 Builder: usage and development +English | [简体中文](usage.zh-CN.md) + Run the commands below from the repository root. For downloads, the video and screenshots, see the [project page](../README.md). diff --git a/docs/usage.zh-CN.md b/docs/usage.zh-CN.md new file mode 100644 index 00000000..e10babe0 --- /dev/null +++ b/docs/usage.zh-CN.md @@ -0,0 +1,362 @@ +# Tiny11 Builder:用法与开发(简体中文) + +[English](usage.md) | 简体中文 + +以下命令均从仓库根目录运行。下载、视频与截图请见[项目主页](../README.zh-CN.md)。 + +可通过侧边栏与构建成功页中的 **支持开发** 使用可选的[项目支持](support.zh-CN.md)。支持选项只有点击才会打开,绝不会中断构建,也绝不会解锁任何功能。支付发生在所选服务商的网站上,而非应用内部。 + +**WinUI 3 桌面应用,配备 C# 映像构建器。** 其分步布局参考了 [S9 WoA 安装器](https://github.com/ntdevlabs/exynos9810-woa)。它不封装、也不执行 PowerShell 构建脚本。为保持兼容,原始脚本原样保留在本文档下方。 + +## 应用功能 + +| 模式 | 行为 | +| --- | --- | +| Tiny11 | 固定的原始风格移除集:预装应用、Edge/WebView2 与 OneDrive 二进制文件、硬件检查设置、保留存储与自动加密设置。保留 Windows 更新、Defender、WinRE、应用商店和共享运行时。选择自定义模式可编辑移除项。 | +| Tiny11 Core | 在重置组件基础前应用更改。保留 common-controls/GDI+/CRT/维护文件,删除其他 WinSxS 组件文件夹,移除 WinRE 并禁用 Windows 更新与 Defender。这些更改不可逆。仅用于测试虚拟机。 | +| 自定义(Custom) | 初始不选择任何移除项。可选择单个移除组,或列出预装应用、受保护的系统应用、可选功能、语言功能、CBS 包、服务、驱动与 WinSxS 组件文件夹。功能与服务还提供仅禁用选项。Core 的不可逆移除是单独的未勾选选项,带有相同的警告与确认。 | + +预设采用原始风格,但并非脚本的逐命令复刻。Tiny11 在不使用 `/ResetBase` 的情况下清理被取代的组件,保留卸载现有更新的能力。Core 则会重置组件基础,保留额外检测到的运行时/维护文件族,并移除 WinRE 但不创建原始脚本中的空占位文件。这些兼容性选择不因可靠性更新而改变;进一步精简需要单独的 Setup/启动测试。 + +六步向导提供: + +- 全宽欢迎页与可点击的构建模式卡片。选择卡片即设定模式并打开 ISO 选择。窄窗口下卡片自动堆叠;页面向下滚动时,淡色的大号数字(1-6)会淡出。返回键位于左上角,支持 Alt+Left。所有前进操作(包括开始与构建)位于右下角。没有页脚条或多余的步骤计数。 +- 一键下载 Microsoft 最新发布的 Windows 11 ISO,使用主机架构与显示语言。不支持的语言会回退到英文并显示明确的状态提示。无需输入 URL 或保存对话框;下载保存到所选工作区的 `downloads` 目录。Microsoft 的可用性/地区限制会被如实报告而不是绕过。 + 瞬时连接重置与服务器繁忙响应会在可见状态下进行有界重试。安全握手重置后,应用可用 TLS 1.2 重试,仍使用 Windows 证书验证。证书、DNS、代理与权限错误不会被绕过。失败提示会链接到 Microsoft 下载页;本地 ISO 仍是可选方案。中断的 ISO 下载会保留部分文件以供重试/续传。 +- 官方 Microsoft ISO 下载带速度/进度显示、基于强 ETag 的可续传部分文件、ISO 结构检查,以及可选的发布 SHA-256 校验。只接受 Microsoft 的 ISO 主机,包括每一次重定向。过期的查询令牌不会被持久化;续传元数据只保存 Microsoft 资源的主机/路径、ETag 与长度。 +- 本地 ISO 与解压后的安装介质输入、WIM/ESD 检测、版本选择,以及自动只读原生 ISO 挂载。找到 Windows 11 Pro 时优先选择;否则选择索引 1。恢复的配方若其保存的版本仍存在,则保留该版本。 +- 三张构建模式卡片:Tiny11、Tiny11 Core 与自定义。单独的保留/移除控件仅在自定义模式显示。Core 需要先通过明确的同意对话框;取消则不批准 Core。 + 完整映像清单需要先显示专家警告,并在构建前进行单独的风险确认。 +- Core 与自定义的 WinSxS/组件负载移除始终使用旧版 Windows 安装程序,包括 24H2 及更新介质。私有 Setup 启动映像先运行 `wpeinit.exe`,再运行其匹配的 `sources\setup.exe`(如存在 `setup_old.exe` 则用它),而不是较新的 SetupHost 流程。 + 这是自动行为,不是绕过硬件检查的选项;不会下载其他 Windows 版本的安装程序。缺少经典可执行文件的介质会明确报错失败。恢复的 Core 作业同样会应用此更改,即使其最终安装映像已经压缩完成。 + 这通过 [Winpeshl.ini](https://learn.microsoft.com/zh-cn/windows-hardware/manufacture/desktop/winpeshlini-reference-launching-an-app-when-winpe-starts) 配置启动 ISO 的 WinPE shell;它不是就地升级的变通方案。 +- 可选的来自匹配 `sources\sxs` 的 .NET 3.5、紧凑安装,以及尽力而为的本地账户设置。不会注入任何账户、密码、产品密钥、自动登录或 EULA 接受。 +- 自动映像工具检测与兼容版本检查。oscdimg 在本地查找,或从 Microsoft 符号服务器以精确 SHA-256 固定值下载。没有工具选择控件,也不会更改脚本执行策略。 +- 带开关的图示化安装选项卡片、结构化评审,以及可展开的构建阶段(含待处理、运行中、已完成、已跳过与失败的操作)。应用、功能与能力移除会显示确切的标识。控制台风格的进度条只写入磁盘日志,不进入界面。 + 总进度跟随阶段边界,而非时间估算。条目按其所属阶段分组;失败的条目不会被重复计为其阶段的第二次失败。未开始的工作在停止后显示 **未运行**。 + 构建页将总进度、当前操作、输出详情与可展开的操作历史分区显示,区块之间留有更多空间。 +- 架构正确的 setup XML、x64 与 ARM64 的 UEFI 启动介质、源中包含时 x64 的 BIOS 启动项,以及恢复 ESD、最大 WIM 或快速 WIM 压缩。 +- 配方导入/导出、实时日志、安全边界取消、持久化作业状态、**继续构建** 的已验证保存进度、输出 SHA-256、JSON 报告,以及独立的中断工作区丢弃/恢复。 + +## Turbo 模式与更小的映像 + +设置页有独立的 **Turbo 模式**、**移除冗余映像数据** 与 **省略 .NET 3.5 安装负载** 开关。所有默认关闭,因此现有配方与固定的 Tiny11/Core 移除集保持原有行为。 + +| 设置 | 改变什么 | 取舍 | +| --- | --- | --- | +| Turbo 模式 | 序列化的功能/能力批次带状态验证、最多八个复制工作线程以及更少的日志 I/O。 | 更多 CPU/I/O 与临时空间占用;实际收益取决于映像与主机。 | +| Turbo + Core 或快速 WIM 输出 | 使用快速压缩的可写中间 WIM,而不是把大量 Core 更改重新压缩进 LZX 工作映像。 | 初始转换可能更慢且工作 WIM 更大。最终压缩仍按所选执行。 | +| 恢复 ESD | 最终版本使用实体恢复压缩。 | Microsoft 工具链下最小的输出,通常也是压缩最慢的阶段。Turbo 不会静默替换它。 | +| 最大 WIM | 强力且可挂载的 WIM 压缩。 | 更易于维护,通常大于恢复 ESD。 | +| 快速 WIM | 更快的最终压缩。 | 更大的 ISO;构建时间比存储空间更重要时使用。 | +| 移除冗余映像数据 | 移除已下载的更新缓存、临时文件、归档的 CBS/DISM 日志、崩溃转储与 Windows 错误报告归档/队列。 | 记录的是逻辑移除字节数;它们不是对压缩后 ISO 节省量的预测。 | +| 省略 .NET 3.5 安装负载 | 不把 `sources\sxs` 暂存进输出 ISO。构建期间仍可从原始源启用 .NET 3.5。 | 事后添加需要其他匹配介质或 Windows 更新;Core 无法事后添加功能。 | + +构建与清单默认使用常规挂载,Turbo 亦然。工具版本变化时不会自动尝试延迟挂载(`/Optimize`):在这台电脑上一次失败的生产探测曾耗时超过十二分钟。开发选优开关保留了已知错误回退与带版本的 `mount-optimization.json` 咨询缓存。无效或不可访问的咨询缓存会以诊断方式禁用延迟挂载,而不是中止构建。Turbo 不会针对单个已挂载映像并行化维护操作、不会禁用主机杀毒软件、不会更改 OS 电源设置,也不会移除完整性检查。批次会跟踪每个功能并回读其状态;失败的批次按失败报告,而不是按单个成功报告。 + +能力移除在标准与 Turbo 模式下都会验证。**卸载挂起(Uninstall Pending)** 表示 Windows 已接受卸载但仍需在首次启动时完成。可维护的构建会保留挂起的负载并在可见警告下继续;每能力报告与操作详情会标注 **deferred(延迟)** 而非缺失。这些移除挂起期间,被取代组件的清理与选定的 Edge/WebView2 文件移除会被跳过。**安装挂起(Install Pending)**、仍已安装、未知、缺失或移除后冲突的状态仍视为错误。Core 与自定义的组件存储精简仍拒绝挂起的移除。 + +这种情况可能出现在之前维护过或 UUP 转换的介质上,包括已集成更新与 .NET 3.5 的介质。它不需要删除 `pending.xml`、回滚维护操作或更改 UUP 应用列表。Core 构建需要没有挂起操作的稳定源;可维护的映像可以保留它们到首次启动。更改压缩不能解决这些问题。 + +清理器使用固定的允许清单。它保留缓存/日志根目录、当前 `CBS.log` 与 `dism.log`、更新数据库、注册表文件、驱动仓库、Windows Installer 缓存与维护清单。当 `pending.xml` 或 `reboot.xml` 表明存在挂起的维护,或脱机注册表带有维护/更新重启标记时,下载与临时负载会被保留。如果注册表配置单元缺失或损坏而无法验证,敏感目标会在警告下保留;这不会被报告为成功移除。注册表检查使用私有的配置单元/日志副本,因此加载它不会把事务重放进映像。其他 I/O/访问错误仍按失败处理。在删除清理目标之前会拒绝未知的符号链接与联接点。它不会扫描正在运行的电脑寻找可删除项。 + +组件清理在移除之后、启用 .NET Framework 3.5 之前运行:脱机功能更改可能留下只在 Windows 首次启动时才完成的工作。如果清理返回 `0x800F0806`(挂起操作),可维护的 Tiny11/自定义构建会保留该工作并将清理报告为 **已跳过** 且附带警告;映像可能更大。其他清理错误仍为致命错误。挂起的操作永远不会被删除或回滚以绕过此错误。当出现这一确切的挂起状态错误时,选定 Edge/WebView2 文件移除在该次运行中同样被跳过,因为其 WinSxS 文件可能被挂起的维护引用。报告会记录这些文件被保留,而不会声称所请求的移除已成功。这些警告在续传后依然有效。 + +当 Edge 的文件确实被移除时,匹配的 Microsoft Edge 条目也会从两个脱机已安装程序注册表位置中移除。确切的标识与发布者检查确保无关软件、Edge Update 与 WebView2 的注册不在此浏览器条目清理范围内。如果文件移除失败或被延迟,该条目会保留。绝不针对正在运行电脑的注册表。Core 与显式的自定义组件负载移除也会在删除组件文件之前立即检查挂起的文件、包状态与功能状态,即使清理已关闭。如果启用 .NET 3.5 留下挂起的工作,这些破坏性构建会带着解释停止,而不是移除首次启动所需的文件。此时请在不带 .NET 3.5 的情况下构建,或使用可维护的映像。 + +标准与 Turbo 都通过备份/还原句柄删除脱机条目,不更改其 ACL,也不清除与剩余硬链接共享的只读属性。两者都能处理长映像路径,并拒绝非映像的重分析点。每次构建都会记录每操作的耗时、`timings.json` 中每阶段的耗时、复制/省略的安装字节数,以及报告中的清理文件/字节计数。失败报告同样保留耗时。 + +两种模式共用常见优化:不使用快速工作映像时保留源 WIM 压缩;重复目标只处理一次;已移除的负载被跳过;预检只重新查询所选版本;省略不必要的功能清单;安装文件按从大到小复制;同卷 ISO 发布采用重命名而非复制。日志仅保留在作业内部,临时文件使用作业的 NTFS 工作区。进度计时器只在操作期间运行并处理有界批次,而不是在应用空闲时持续唤醒。 + +在采用共享删除路径之前,三次运行 4,000 个文件的基准测试中,旧的 ACL 重写路径耗时 3.55-4.12 秒,原生删除耗时 0.69-0.75 秒。这不是当前两种模式的对比。真实的 977 文件 ISO 暂存中,四个工作线程耗时 1.74-5.90 秒,八个工作线程耗时 1.68-1.85 秒;结果取决于缓存与主机,不是普遍的加速承诺。省略此映像的可选 .NET 负载避免了复制 71,656,916 字节(约 68.3 MiB)。 + +一次端到端的 26H2 x64 Core 构建(相同移除集、恢复 ESD 压缩)耗时 **31 分 44 秒**,此前在这台电脑上的运行为 **90 分 31 秒**。Core 精简耗时 37 秒而非约 4 分 38 秒;提交耗时 3 分 54 秒而非约 29 分 38 秒。初始转换为快速工作 WIM 更慢(3 分 8 秒而非 33 秒)。这些是缓存/系统负载不同的实测开发运行,不是保证的加速倍数。未使用优化的延迟挂载,因为此主机的提供程序拒绝了它。 + +得到的 Turbo ISO 为 **3,259,584,512 字节(3.04 GiB)**,此前为 3,331,207,168 字节。其中约 68.3 MiB 来自省略可选安装负载;这个干净的 Microsoft 源没有额外的缓存文件可移除。该构建不声称有缓存节省。ISO 的 Pro 版本、内部版本号、压缩与 SHA-256 已验证;虚拟机启动认证仍然独立进行。 + +**这是映像构建器,不是安装器,也不是运行中系统的精简工具。** 它绝不故意针对正在运行的 Windows 安装、格式化磁盘或修改源介质。只有新建的工作副本会被维护。已存在的输出 ISO 会被拒绝,而不会被覆盖。 + +脱机注册表编辑会把复制的映像配置单元临时挂载到唯一命名的 `HKLM\Tiny11Builder_Offline_` 键下并显式卸载。不编辑主机现有的注册表设置。配置单元加载/卸载失败会停止操作并报告确切的临时键,而不是隐藏错误。每作业的配置单元日志让工作区恢复能在应用于卸载前被中断时,只卸载该作业的临时键。DWORD 与字符串写入通过以受限备份/还原权限打开的句柄使用原生 `RegSetValueExW` API,并且每个写入的值都会回读。小组件使用 Microsoft 有文档记载的 `SOFTWARE\Policies\Microsoft\Dsh\AllowNewsAndInterests=0` 策略禁用。受保护的 `Explorer\Advanced\TaskbarDa` 首选项被刻意不写入:较新的主机即使通过原生 API 也可能拒绝它。主机的保护服务不会被禁用。该策略适用于小组件及其任务栏内容;支持程度仍因 Windows 版本而异。 + +搜索亮点使用计算机策略 `SOFTWARE\Policies\Microsoft\Windows\Windows Search\EnableDynamicContentInWSB=0`。受保护的默认用户 `SearchSettings\IsDynamicSearchBoxEnabled` 首选项不被编辑。必需的策略写入仍会验证;访问错误不会被静默忽略,也不会禁用任何主机注册表保护。 + +Core 负载删除使用超长 Windows 路径,因此较长的 WinSxS 文件名不会在 260 字符的旧限制处失败。符号链接与联接点限制保持不变。 + +加速使用 Microsoft 有文档记载的 [/Mount-Image /Optimize](https://learn.microsoft.com/windows-hardware/manufacture/desktop/dism-image-management-command-line-options-s14) 与 [多功能 /Disable-Feature](https://learn.microsoft.com/windows-hardware/manufacture/desktop/dism-operating-system-package-servicing-command-line-options) 选项。功能批次针对默认 Foundation 包;单个专家维护操作保持序列化。 + +脱机映像文件可以合法使用 WIM/WOF 重分析标记。这两个非名称代理的映像提供程序标记在所拥有的映像内部被接受;普通符号链接与联接点仍被拒绝。源、工作区与输出路径保留其更严格的重分析点检查。 + +## Windows 11 内部版本兼容性 + +构建器接受 **Windows 11 客户端内部版本 22621(22H2)及更新**,包括 23H2、24H2、25H2、26H2 与 Insider/Canary 介质,支持 **x64 与 ARM64**,没有硬编码的最大版本号。映像架构、版本与build号从 DISM 读取,而不是从 ISO 文件名推断。包/能力/功能标识来自挂载的映像,因此缺失项会被记录为 *not present(不存在)*,而不是猜测。版本名称使用 Windows 映像工具写入的 OEM 代码页解码,并显式识别 UTF 字节序标记。西里尔文本不会通过替换字体或猜测版本名来修复。无效的编码输出会明确失败,而不是静默替换字符。 + +接受并不代表认证**每个版本与每种自定义移除组合都能启动**。这一新实现需要针对每个发布/渠道的代表性介质进行虚拟机验证。单元测试覆盖构建元数据以及跨正式版与更新/Canary 风格版本号的模拟管线;可选的集成测试以只读方式使用真实介质。 + +Windows 10(包括 22H2)可以使用兼容的 Microsoft 部署工具构建 Windows 11 映像;要求不在主机的版本号。应用检查实际的维护提供程序,而不只是 `dism.exe` 启动器的版本。已知的启用包版本共享维护基础:23H2(22631)使用 22621;25H2(26200)与 26H2(26300)使用 26100。因此 26100 工具不会仅因为 26H2 映像报告 26300 而被拒绝。请保持 Windows/ADK 维护更新为最新。 + +Microsoft 的 [ADK 支持列表](https://learn.microsoft.com/windows-hardware/get-started/adk-install) 为 26100 ADK 列出了 24H2、25H2 与 26H2;[DISM 支持的平台](https://learn.microsoft.com/windows-hardware/manufacture/desktop/dism-supported-platforms) 记录了在 Windows 10 上使用 Windows 11 部署工具。未知的更新/Canary 版本不会被并入这些家族,可能仍需要预览版 ADK或更新的工具提供程序。当工具不满足该维护基础时,应用会在修改之前停止。它会自动检测已安装的工具;没有手动工具选择器。 + +### Windows 10 报告映像工具为 10.0.19041 + +对于映像 **26300**,工具 **10.0.19041.3636** 与早前误拒 26100 工具的情况不同。Windows 10 可以保持安装,但其内置维护工具比此映像的维护基础更旧。 + +1. 通过应用错误提示中的 **获取映像工具** 打开 [Microsoft ADK 页面](https://learn.microsoft.com/windows-hardware/get-started/adk-install)。 +2. 安装 Microsoft 为 Windows 11 24H2/25H2/26H2(26100 家族)列出的 ADK,选择 **Deployment Tools**,并应用其维护更新。修改完整 ISO 中已有的 `boot.wim` 不需要单独的 Windows PE 加载项。 +3. 关闭并重新打开 Tiny11 Builder,让它检测已安装的工具,然后用你的原始 ISO 再次开始构建。 + +不要替换 `Windows\System32` 中的文件、下载独立的 DISM EXE 或删除挂起操作文件。应用需要完整且兼容的工具集,而不是一个改名的启动器。 + +高级组件清单是只读的,它检查实际的清单操作,而不是强加可写构建的版本门槛或输出空间要求。如果已安装的工具无法查询更新的映像,应用会停止清单、丢弃其拥有的挂载并提供 **获取映像工具**,链接到 Microsoft 的 ADK 页面。成功列出映像并不等于允许用旧工具修改它:构建与续传预检仍要求兼容的维护提供程序。 + +保存的映像的哈希、版本、架构、安装类型与完整版本在续传时仍会验证。其本地化显示名称不被视为标识:修复解码错误的文本不会使未更改的已保存映像字节失效。Microsoft 同样决定哪些正式版/Insider ISO 可用:应用不声称能下载未发布的 Canary 版本,也不使用非官方 UUP 镜像。主“下载”按钮会自动解析当前的公开版本。可选的“其他官方下载”区仍接受其他语言或 Insider 介质的手动链接。Microsoft 可能要求你在其 Insider 页面登录;应用不处理你的凭据。 + +**策略依赖内部版本/版本 edition。** 脱机注册表写入成功会被验证,但这并不能证明 Home、Pro、Enterprise 或未来版本会遵循每个策略。必需的诊断仍保留在均衡电源配置中。Windows 功能更新可能恢复应用与设置。本地账户与硬件检查设置是尽力而为,不保证在新版本上绕过。任何修改都不会绕过 Windows 授权或激活。 + +**Core 与专家警告:** Core 没有可用的更新/组件维护或恢复映像,并禁用杀毒保护。运行时布局未知的未来版本会在 Core 精简开始前被拒绝。专家模式刻意允许移除受保护与启动关键项;没有依赖求解器。DISM 可以拒绝受保护 CBS 包的移除,此时构建明确失败。直接的系统应用/WinSxS 负载移除被单独标记为不支持的操作。移除服务会删除其脱机注册表注册;共享的 `svchost.exe` 与其他共享二进制文件永远不会被盲目删除。请在隔离的虚拟机中测试激进的映像,并保留已知良好的介质。 + +## 构建与运行 + +要求:Windows 10 2004 或更新作为主机、.NET 8 SDK 以及与项目目标兼容的 Windows SDK。应用会选择最新安装的 Microsoft 维护工具并自动准备 oscdimg。较新的映像版本仍需要足够新的 Windows 主机或 ADK。Windows App SDK 与 .NET 运行时在发布的应用文件夹中是自包含的。 + +已安装的工具要求受信任的 Microsoft 签名。Microsoft 符号服务器上的 oscdimg 副本省略了其内嵌证书,因此只接受确切的经批准的 2.56 二进制文件:SHA-256 `2000160B2C5044691B2F9A0AC308E5207F273D4880A572457AF16D05886BA861`。下载仅限于经批准的 Microsoft 符号服务器资源及其观察到的 Microsoft blob 重定向。不同的校验或重定向会在执行前停止获取。这不是通用的未签名工具例外。 + +```powershell +dotnet build .\Tiny11Builder.sln -c Release +dotnet publish .\src\Tiny11Builder.App\Tiny11Builder.App.csproj -c Release -r win-x64 -p:Platform=x64 -o .\artifacts\Tiny11Builder-x64 +.\artifacts\Tiny11Builder-x64\Tiny11Builder.exe +``` + +ARM64 主机: + +```powershell +dotnet publish .\src\Tiny11Builder.App\Tiny11Builder.App.csproj -c Release -r win-arm64 -p:Platform=ARM64 -o .\artifacts\Tiny11Builder-arm64 +``` + +应用**启动时始终要求管理员权限**。Windows 会在打开它之前要求 UAC 批准(或管理员账户凭据);拒绝则应用不会启动。这内嵌在可执行文件中,不是可选的重启按钮或 UAC 绕过。请运行 `Tiny11Builder.exe`,而不是托管 DLL。构建仍需要明确的确认与风险确认。提权应用中文件/文件夹选择器可以正常工作。 + +## GitHub 源码与发布包 + +提交源码、测试、解决方案、脚本、README、NOTICE 与 `.github` 配置。生成的 `bin`、`obj`、`artifacts`、测试结果与 Windows 映像会被忽略。不要在公开上传中包含你的工作介质、带个人路径的配方或构建日志。 + +GitHub Actions 工作流在 Windows 上运行测试并构建自包含的 x64 与 ARM64 ZIP。它只上传工作流工件;不发布 GitHub Release、不更改权限,也不打包 Windows ISO/ADK 工具。原生测试需要提权的 Windows 运行器。真实映像测试保持可选。 + +在提权的 PowerShell 中本地构建同样的压缩包: + +```powershell +.\scripts\package.ps1 -Architecture x64 +.\scripts\package.ps1 -Architecture arm64 +``` + +压缩包与 SHA-256 文件出现在 `artifacts\release`。脚本会验证可执行文件架构、内嵌管理员清单与捆绑运行时,并包含依赖项许可声明。它拒绝覆盖该版本的现有压缩包。将整个 ZIP 解压到文件夹后运行 `Tiny11Builder.exe`;只复制 EXE 是不够的。 + +当前应用版本为 **0.1.3**。包未签名,也不是 Microsoft 认证的 Windows 映像。公开再分发前请查阅 [NOTICE.md](../NOTICE.md):此上游检出没有独立的许可文件,且此重写不会为别人的源码授予许可。现有的 `origin` 指向上游项目;推送前请选择你自己的 GitHub 仓库或 fork。打包脚本不会推送任何代码。 + +默认工作区为 **`C:\tiny11`**,输出 ISO 也在那里。请使用 **NTFS 工作区**,通常需要 40-70 GiB 可用空间。由于 WIM 挂载限制,ReFS、FAT32 与网络工作区会被拒绝。输出不能是 FAT32。支持工作区与输出在同一驱动器。网络路径、备用数据流、源/工作区/输出中的重分析点,以及作为输出/工作区的系统目录都会被拒绝。输入必须包含 `boot.wim`、`install.wim` 或 `install.esd` 以及 UEFI 启动介质;不支持拆分的 `install.swm` 输入。导出单个版本可以避免先复制多版本安装映像再将其删除。 + +## 日志、配方与恢复 + +状态与缓存工具位于 `%LOCALAPPDATA%\Tiny11Builder`;工作介质默认为 `C:\tiny11` 且可更改。每次操作都会创建唯一的 `tiny11-` 子目录。构建会保留 `job.json`、`recipe.json`、`build.log`、`dism.log`、`operations.json`、`timings.json` 与 `report.json`。`operations.json` 也会记录失败的构建,包括确切的操作与错误详情。原始磁盘日志保留异常与控制台输出。成功时,除非选择 **保留工作介质**,工作介质会被移除;日志与报告保留。失败时,工作文件被保留,应用会给出其路径。配方包含本地路径与选择项,分享前请先检查。切换构建模式会替换之前的移除选择。自定义初始不选择任何移除组、高级操作或 Core 移除。单个组件操作叠加在已选组之上:要保留一个组件,其组与其单个条目都不要勾选。保存的配方保留所选模式与确切移除项。旧配方仍被接受;修改过的 Core 配方会加载为启用 Core 移除的自定义模式,绝不会静默丢弃。 + +取消**不会**杀死正在运行的 DISM 挂载/提交进程。它等待操作完成后,在安全边界丢弃此作业的挂载。操作运行时关闭应用会被阻止。ISO 创建与下载流可以取消。在下一边界取消之前,已完成的阶段会被记录。失败/取消时工作文件与日志会保留。失败提示提供 **打开日志**。红色 **安全取消** 按钮在工作运行期间保持于右上角。点击它首先要求确认;“继续构建/继续”是默认项,不发送取消。确认后,同一对话框保持打开并显示进度条,同时当前操作完成、拥有的挂载被丢弃、日志被保存。等待进度条是不确定模式,不是猜测的关闭百分比。运行完全稳定后,对话框显示其实际结果。失败的清理或必需的恢复显示为错误,绝不会伪装成安全完成。如果操作在确认打开期间已完成,则不会取消任何内容。取消清理期间 Esc 与关闭应用被阻止。 + +安装映像成功提交后,作业会保存 `checkpoint.json`,包含原始配方哈希、映像标识、映像哈希与已暂存安装文件的哈希清单。最终压缩后,第二个检查点允许重试 ISO 创建而无需重新压缩。检查点在持有作业独占锁的情况下原子地刷新与替换。安装映像编辑使用私有副本,在替换之前提交并验证。 + +使用 **继续构建** 并选择已停止作业的 `job.json`,包括在重启应用之后。引擎在继续之前验证检查点、哈希、工具与作业所有权。它拒绝更改过的移除选择、损坏或过期的进度、活动作业与不安全路径。有效提交后不再需要原始源。UI 会用新定位的工具恢复原始配方;如果输出已存在,请选择新的 ISO 名称。继续构建确认对话框默认为取消,并重复适用的 Core/自定义风险。 + +没有检查点的较早中断作业无法自动继续,即使它们恰好包含已提交的 WIM。选择它们继续时不会被收养或删除。如果需要,请保留其工作文件。 + +发布从不覆盖现有 ISO。跨卷复制在最终重命名前会刷新并校验哈希。输出竞争时,验证过的 ISO 保留在作业的 `output.iso`,构建保持可续传。成功报告只在发布之后写入。发布后的日志/报告/清理问题显示为警告,而不是对已发布 ISO 的虚假失败。 + +如果应用/电脑崩溃,使用 **恢复工作区**,选择该作业的 `job.json`,并明确确认丢弃其工作副本。恢复拒绝活动锁定作业与已完成作业。如果存在已保存进度,UI 会提供 **改为继续**,绝不会自动删除已提交的映像。恢复只处理该作业的挂载与暂存目录;它从不使用全局 DISM 挂载清理,并保留源、日志、配方与现有输出。 + +## 测试 + +从提权终端运行测试。原生文件与注册表测试需要 Windows 备份与还原权限。 + +```powershell +dotnet test .\tests\Tiny11Builder.Core.Tests\Tiny11Builder.Core.Tests.csproj +``` + +一个小型原生映像提供程序检查会把一个合成目录捕获进其自己的临时 WIM,删除一个脱机组件,提交,然后只读重新挂载以验证保留的硬链接的内容、权限与只读状态。它不使用你的 Windows ISO: + +```powershell +$env:TINY11_NATIVE_WIM_CHECK = '1' +dotnet test .\tests\Tiny11Builder.Core.Tests\Tiny11Builder.Core.Tests.csproj --filter Shared_deletion_survives_a_synthetic_WIM_commit_when_configured -- RunConfiguration.EnvironmentVariables.TINY11_NATIVE_WIM_CHECK=1 +Remove-Item Env:\TINY11_NATIVE_WIM_CHECK +``` + +旧版安装程序集成可以针对真实 ISO 检查。此测试只把 `boot.wim` 复制进一次性作业,应用启动配置,提交,只读重新挂载,并检查保存的配置与映像计数。它不会更改原始 ISO;它不认证虚拟机中的 Windows 安装程序: + +```powershell +dotnet test .\tests\Tiny11Builder.Core.Tests\Tiny11Builder.Core.Tests.csproj --filter Legacy_startup_survives_a_real_Setup_image_commit_when_configured -- RunConfiguration.EnvironmentVariables.TINY11_SETUP_ISO_CHECK=C:\media\Windows11.iso +``` + +真实映像测试是可选的,需要有效的 Windows 11 WIM。它以**只读**方式挂载 WIM,列出其组件,并在其注册表文件的单独副本上测试策略与服务更改。然后丢弃挂载并检查源时间戳未改变: + +```powershell +$env:TINY11_TEST_WIM = 'C:\media\sources\install.wim' +dotnet test .\tests\Tiny11Builder.Core.Tests\Tiny11Builder.Core.Tests.csproj --filter Read_only_real_Windows_image_integration_when_configured +``` + +应用是 `src\Tiny11Builder.Core` 之上的薄 WinUI 表现层;测试无需 GUI 运行。实时 Microsoft 目录与工具检查是可选的: + +```powershell +$env:TINY11_TEST_DOWNLOADS = '1' +dotnet test .\tests\Tiny11Builder.Core.Tests\Tiny11Builder.Core.Tests.csproj --filter Live_official_catalog_and_pinned_tool_when_configured +``` + +这会解析 x64 与 ARM64 ISO 链接并下载/验证 oscdimg,但不执行它,也不下载完整 Windows ISO。Windows 介质与部署可执行文件不由本仓库再分发。 + +可写配置单元回归测试也是可选的。它把完整的 Core 策略集应用到临时挂载,验证小组件策略,然后丢弃更改: + +```powershell +$env:TINY11_PERMISSION_WIM = 'C:\media\working.wim' +dotnet test .\tests\Tiny11Builder.Core.Tests\Tiny11Builder.Core.Tests.csproj --filter Writable_mount_permission_regression_when_configured +``` + +此测试请使用工作 WIM 副本。完整 Core ISO 验证读取显式保存的配方,在 `C:\tiny11` 下写入单独的 ISO,且不声称虚拟机启动验证: + +```powershell +$env:TINY11_BUILD_RECIPE = 'C:\tiny11\tiny11-job\recipe.json' +dotnet test .\tests\Tiny11Builder.Core.Tests\Tiny11Builder.Core.Tests.csproj --filter Full_real_Core_build_when_configured +``` + +一次 Windows 11 Pro 26H2 x64(build 26300.9457)的端到端构建已完成,使用原始 Core 移除集、紧凑安装与恢复 ESD 压缩。得到的 ISO 为 3,331,207,168 字节(3.10 GiB),在索引 1 包含一个安装版本。源介质为 8.31 GiB 的多版本介质,因此该大小对比包含只导出 Pro 以及移除与压缩。虚拟机启动验证仍与 ISO 创建分开。 + +可选的 Turbo 基准测试: + +```powershell +$env:TINY11_TURBO_BENCHMARK = '1' +dotnet test .\tests\Tiny11Builder.Core.Tests\Tiny11Builder.Core.Tests.csproj --filter Native_file_deletion_benchmark_when_configured + +$env:TINY11_TURBO_TEST_WIM = 'C:\media\working.wim' +dotnet test .\tests\Tiny11Builder.Core.Tests\Tiny11Builder.Core.Tests.csproj --filter Real_Turbo_mount_staging_and_cleanup_benchmark_when_configured + +$env:TINY11_BUILD_RECIPE = 'C:\tiny11\tiny11-job\recipe.json' +$env:TINY11_TEST_TURBO = '1' +dotnet test .\tests\Tiny11Builder.Core.Tests\Tiny11Builder.Core.Tests.csproj --filter Full_real_Core_build_when_configured +``` + +最后一条命令使用单独的 `C:\tiny11\turbo-verification` 工作区与 `C:\tiny11\Tiny11Core-26300-turbo.iso` 输出。它保留恢复压缩、启用 Turbo 与冗余数据清理,并省略可选安装负载。挂载基准测试请使用工作副本 WIM,并让每个测试在进程结束前丢弃其挂载。 + +--- + +# 原始 PowerShell tiny11builder +*构建精简 Windows 11 映像的脚本 —— 现已用 **PowerShell** 实现!* + +## 简介: +原始 PowerShell 脚本使用 DISM 和 oscdimg 创建修改过的 Windows 11 安装映像。`tiny11maker.ps1` 保留更新支持;`tiny11Coremaker.ps1` 移除它。 + +现在你可以在任何 Windows 11 版本(不只是特定 build)上使用它,也支持任何语言或架构。这要归功于 PowerShell 相比早期 Batch 版本大幅改进的脚本能力。 + +脚本使用 DISM 恢复压缩与 Windows ADK 中的 **oscdimg.exe** 创建 ISO。附带的无人值守应答文件配置紧凑安装与本地账户设置。本地账户绕过在较新的 Windows 版本上可能无效。 + +**tiny11 Core** 移除组件存储文件与 Windows 恢复支持,并禁用 Windows 更新与 Defender。之后无法添加语言、更新或功能。仅用于测试虚拟机。 + +--- + +## ⚠️ 脚本版本: +- **tiny11maker.ps1**:常规脚本,移除大量预装内容但保持系统可维护。创建后可以添加语言、更新与功能。这是日常使用的推荐脚本。 +- ⚠️ **tiny11coremaker.ps1**:核心脚本,移除更多内容但同时也移除维护映像的能力。之后无法添加语言、更新或功能。推荐用于快速测试或开发用途。 + +## 使用说明: +1. 从 [Microsoft 网站](https://www.microsoft.com/software-download/windows11) 或 [Rufus](https://github.com/pbatard/rufus) 下载 Windows 11 +2. 使用 Windows 资源管理器挂载下载的 ISO 映像。 +3. 以管理员身份打开 **PowerShell 5.1**。 +4. 更改脚本执行策略: +```powershell +Set-ExecutionPolicy Bypass -Scope Process +``` +> 使用 `-Scope Process` 可保持你原有的策略不变,因为此更改只在当前 PowerShell 会话中生效。 + +5. 启动脚本: +```powershell +C:/path/to/your/tiny11/script.ps1 -ISO -SCRATCH +``` +> 运行 `get-help` 命令可以查看脚本帮助。 + +6. 选择映像挂载的驱动器号(只输入字母,不带冒号(:)) +7. 选择你希望映像基于的 SKU。 +8. 然后休息等待 :) +9. 映像完成后,你会在脚本解压的文件夹中看到名为 tiny11.iso 的文件 + +--- + +## 移除的内容: + + + + + + + + + + + +
Tiny11makerTiny11coremaker
+
    +
  • Clipchamp
  • +
  • 新闻
  • +
  • 天气
  • +
  • Xbox
  • +
  • 获取帮助
  • +
  • 入门
  • +
  • Office Hub
  • +
  • 纸牌
  • +
  • 人脉
  • +
  • PowerAutomate
  • +
  • To Do
  • +
  • 闹钟
  • +
  • 邮件和日历
  • +
  • 反馈中心
  • +
  • 地图
  • +
  • 录音机
  • +
  • 你的手机
  • +
  • 媒体播放器
  • +
  • 快速助手
  • +
  • Internet Explorer
  • +
  • Tablet PC 数学
  • +
  • Edge
  • +
  • OneDrive
  • +
+
+
    +
  • 常规 tiny11 的全部内容,另加
  • +
  • Windows 组件存储(WinSxS)
  • +
  • Windows Defender(仅禁用,需要时可以重新启用)
  • +
  • Windows 更新(没有 WinSxS 无法工作,启用会使系统进入故障状态)
  • +
  • WinRE
  • +
+
+ +请记住 **tiny11 core 中无法添加回功能**!
+创建映像时会询问你是否启用 .NET 3.5 支持! + +--- + +## 已知问题: +- 虽然 Edge 已被移除,但设置中仍有一些残留,不过应用本身已被删除。 +- 你可能需要先更新 Winget 才能通过 Microsoft Store 安装应用。 +- Outlook 与 Dev Home 可能过段时间重新出现。这是一场持续的斗争,不过最新的脚本更新更积极地阻止这一点。 +- 如果你在 arm64 上使用此脚本,运行时可能看到一闪而过的错误。这是因为 arm64 映像的 System32 文件夹中没有包含 OneDriveSetup.exe。 + +--- + +## 计划实现的功能: +- ~~禁用遥测~~(已在 2024-04-29 版本中实现!) +- ~~更多广告抑制~~(已在 2025-09-06 版本中部分实现!) +- 改进的语言与架构检测 +- 更灵活的保留/删除选择 +- 也许一个 GUI??? + +目前就这些了! + +## ❤️ 支持本项目 + +如果这个项目对你有帮助,请考虑表达你的支持!小额捐赠能帮助我投入更多时间到这样的项目中。谢谢! + +**[Patreon](http://patreon.com/ntdev) | [PayPal](http://paypal.me/ntdev2) | [Ko-fi](http://ko-fi.com/ntdev)** + +感谢你的尝试,欢迎告诉我你的使用感受! + diff --git a/src/Tiny11Builder.App/App.xaml.cs b/src/Tiny11Builder.App/App.xaml.cs index 11af9e1c..be778181 100644 --- a/src/Tiny11Builder.App/App.xaml.cs +++ b/src/Tiny11Builder.App/App.xaml.cs @@ -8,6 +8,7 @@ public partial class App : Application public App() => InitializeComponent(); protected override void OnLaunched(LaunchActivatedEventArgs args) { + Localization.Initialize(); window = new MainWindow(); window.Activate(); } diff --git a/src/Tiny11Builder.App/Choices.cs b/src/Tiny11Builder.App/Choices.cs index 3dfe64ee..421d1b29 100644 --- a/src/Tiny11Builder.App/Choices.cs +++ b/src/Tiny11Builder.App/Choices.cs @@ -37,10 +37,10 @@ public sealed class InventoryChoice(ImageInventoryItem item) : ObservableChoice public ImageInventoryItem Item { get; } = item; public string Title => Item.Identity; public string Detail => $"{Item.Kind} · {Item.Detail}"; - public string Risk => Item.Protected ? "Protected / potentially boot-critical" : "Application"; - public string ModeLabel => $"Action for {Title}"; + public string Risk => Item.Protected ? L10n.T("RiskProtected", "Protected / potentially boot-critical") : L10n.T("RiskApp", "Application"); + public string ModeLabel => string.Format(L10n.T("ActionFor", "Action for {0}"), Title); public bool HasModes => Item.Kind is ImageItemKind.Service or ImageItemKind.Feature; - public string[] Modes { get; } = ["Disable", "Remove"]; + public string[] Modes { get; } = [L10n.T("ModeDisable", "Disable"), L10n.T("ModeRemove", "Remove")]; public bool Selected { get => selected; set => Set(ref selected, value); } public int ModeIndex { get => modeIndex; set => Set(ref modeIndex, value); } public ExpertAction Action => new(Item.Kind, Item.Identity, ModeIndex == 0 ? ExpertActionMode.Disable : ExpertActionMode.Remove); @@ -52,8 +52,8 @@ public sealed class BuildStageBoard { public BuildStageChoice[] Stages { get; } = [ - new("Prepare media", "\uE8B7"), new("Export and mount", "\uE7C5"), new("Remove components", "\uE74D"), - new("Commit image", "\uE74E"), new("Compress image", "\uE8B9"), new("Create ISO", "\uE958"), new("Verify output", "\uE73E") + new(L10n.T("StagePrepare", "Prepare media"), "\uE8B7"), new(L10n.T("StageExport", "Export and mount"), "\uE7C5"), new(L10n.T("StageRemove", "Remove components"), "\uE74D"), + new(L10n.T("StageCommit", "Commit image"), "\uE74E"), new(L10n.T("StageCompress", "Compress image"), "\uE8B9"), new(L10n.T("StageIso", "Create ISO"), "\uE958"), new(L10n.T("StageVerify", "Verify output"), "\uE73E") ]; // Returns the phase the event belongs to, or -1 when it is not a displayed operation. @@ -111,7 +111,7 @@ public void Reset() Operations.Clear(); halted = false; doneEarlier = false; - Status = "Waiting"; + Status = L10n.T("StatusWaiting", "Waiting"); Expanded = false; } @@ -171,13 +171,13 @@ public void Stop(bool cancelled) RefreshStatus(); } - public void Complete() => Status = doneEarlier ? "Done earlier" : "Complete"; + public void Complete() => Status = doneEarlier ? L10n.T("StatusDoneEarlier", "Done earlier") : L10n.T("StatusComplete", "Complete"); public void MarkDoneEarlier() { if (Operations.Count > 0) return; doneEarlier = true; - Status = "Done earlier"; + Status = L10n.T("StatusDoneEarlier", "Done earlier"); } private int InsertIndex(string stage) @@ -215,11 +215,16 @@ void Add(BuildOperationState state, int count) if (tally.Own is BuildOperationState.Failed && tally.Items[(int)BuildOperationState.Failed] == 0) failed++; if (tally.Own is BuildOperationState.Cancelled && tally.Items[(int)BuildOperationState.Cancelled] == 0) cancelled++; } - var rest = pending == 0 ? "" : halted ? $" · {pending} not run" : $" · {pending} next"; - Status = failed > 0 ? $"{(failed == 1 ? "Failed: " + ErrorText.Clip(FirstFailedTitle(), 48) : failed + " failed")} · {done} done{rest}" : - cancelled > 0 ? $"Cancelled · {done} done{rest}" : - running ? $"In progress · {done} done{rest}" : - pending > 0 ? (halted && done == 0 ? "Not run" : $"{done} done{rest}") : "Complete"; + var rest = pending == 0 ? "" : halted + ? string.Format(L10n.T("StageRestNotRun", " · {0} not run"), pending) + : string.Format(L10n.T("StageRestNext", " · {0} next"), pending); + Status = failed > 0 ? string.Format(L10n.T("StageFailed", "{0} failed · {1} done{2}"), + failed == 1 ? L10n.T("StageFailedOne", "Failed: ") + ErrorText.Clip(FirstFailedTitle(), 48) : failed + L10n.T("StageFailedCount", " failed"), + done, rest) : + cancelled > 0 ? string.Format(L10n.T("StageCancelled", "Cancelled · {0} done{1}"), done, rest) : + running ? string.Format(L10n.T("StageInProgress", "In progress · {0} done{1}"), done, rest) : + pending > 0 ? (halted && done == 0 ? L10n.T("StatusNotRun", "Not run") : string.Format(L10n.T("StageDone", "{0} done{1}"), done, rest)) : + L10n.T("StatusComplete", "Complete"); } private string FirstFailedTitle() => @@ -252,9 +257,9 @@ public BuildOperationState State } public string Status => State switch { - BuildOperationState.Running => "In progress", BuildOperationState.Completed => "Done", - BuildOperationState.Skipped => "Skipped", BuildOperationState.Failed => "Failed", - BuildOperationState.Cancelled => "Cancelled", _ => NotRun ? "Not run" : "Next" + BuildOperationState.Running => L10n.T("StatusInProgress", "In progress"), BuildOperationState.Completed => L10n.T("StatusDone", "Done"), + BuildOperationState.Skipped => L10n.T("StatusSkipped", "Skipped"), BuildOperationState.Failed => L10n.T("StatusFailed", "Failed"), + BuildOperationState.Cancelled => L10n.T("StatusCancelled", "Cancelled"), _ => NotRun ? L10n.T("StatusNotRun", "Not run") : L10n.T("StatusNext", "Next") }; public string Glyph => State switch { diff --git a/src/Tiny11Builder.App/Localization.cs b/src/Tiny11Builder.App/Localization.cs new file mode 100644 index 00000000..8cf12e8e --- /dev/null +++ b/src/Tiny11Builder.App/Localization.cs @@ -0,0 +1,73 @@ +using Microsoft.Windows.ApplicationModel.Resources; +using Windows.Globalization; +using Windows.Storage; + +namespace Tiny11Builder.App; + +/// +/// Bridges the WinRT resource system to the WinRT-free provider used across the +/// app and the shared logic, and remembers the user's language choice so it survives restarts. +/// +internal static class Localization +{ + private const string SettingsKey = "PreferredLanguage"; + + private static readonly ResourceLoader Loader = new(); + + /// The language choices offered in the sidebar, in display order. + public static string[] Choices { get; } = ["System", "en-US", "zh-CN"]; + + /// Currently active language override, or empty string for the system default. + public static string Current + { + get => ApplicationLanguages.PrimaryLanguageOverride; + set + { + ApplicationLanguages.PrimaryLanguageOverride = value; + try + { + if (ApplicationData.Current is { } data) + data.LocalSettings.Values[SettingsKey] = value; + } + catch (Exception) + { + // Settings may be unavailable in some contexts; the override still applies for this run. + } + } + } + + /// Call once at startup, before any window is created, so XAML x:Uid lookups use the saved language. + public static void Initialize() + { + ApplySaved(); + L10n.Provider = (key, english) => + { + try + { + var value = Loader.GetString(key); + return string.IsNullOrWhiteSpace(value) ? english : value; + } + catch (Exception) + { + return english; + } + }; + } + + private static void ApplySaved() + { + try + { + if (ApplicationData.Current is not { } data) return; + if (data.LocalSettings.Values.TryGetValue(SettingsKey, out var stored) && + stored is string language && language.Length > 0) + { + ApplicationLanguages.PrimaryLanguageOverride = language; + } + } + catch (Exception) + { + // Ignore: fall back to the system language for this run. + } + } +} diff --git a/src/Tiny11Builder.App/MainWindow.xaml b/src/Tiny11Builder.App/MainWindow.xaml index 05f2c765..d470d4a3 100644 --- a/src/Tiny11Builder.App/MainWindow.xaml +++ b/src/Tiny11Builder.App/MainWindow.xaml @@ -2,6 +2,7 @@ x:Class="Tiny11Builder.App.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + x:Uid="AppWindow" Title="Tiny11 Builder"> @@ -9,42 +10,47 @@ - + - - - - - - - + + + + + + - - - + + + - @@ -93,15 +99,15 @@ - - - + @@ -116,11 +122,11 @@ - - + - @@ -131,11 +137,11 @@ - - + - @@ -146,11 +152,11 @@ - - + - @@ -158,30 +164,30 @@ - - + - + - - - + + - - + - + - - - + + - - - - - - + - - + - - - + + + @@ -272,24 +278,24 @@ - - + - @@ -569,28 +575,28 @@ Visibility="Collapsed" AutomationProperties.Name="Working"/> - - - - + + + - - - + + + - + - - + + - + diff --git a/src/Tiny11Builder.App/MainWindow.xaml.cs b/src/Tiny11Builder.App/MainWindow.xaml.cs index 86ace557..a0716de1 100644 --- a/src/Tiny11Builder.App/MainWindow.xaml.cs +++ b/src/Tiny11Builder.App/MainWindow.xaml.cs @@ -35,6 +35,7 @@ public sealed partial class MainWindow : Window private bool completed; private bool coreAcknowledged; private bool modalOpen; + private int _appliedLanguageIndex; private enum NoticeAction { None, OpenLogs, CopyDetails, OpenTools, OpenOfficialDownload } private string officialDownloadPage = OfficialImageDownloader.DownloadPage; // The run RunOperation is executing and the cancel prompt open for it; both are null when nothing is happening. @@ -74,11 +75,11 @@ public MainWindow() { if (!busy) return; args.Cancel = true; - ShowNotice("An operation is still running.", cancelRequested ? "It is being cancelled. Wait for it to stop before closing." : - $"Choose {CancelPrompts.ButtonLabel} and wait for the image operation to finish before closing.", InfoBarSeverity.Warning); + ShowNotice(L10n.T("OpStillRunning", "An operation is still running."), cancelRequested ? L10n.T("OpCancellingWait", "It is being cancelled. Wait for it to stop before closing.") : + string.Format(L10n.T("OpFinishWait", "Choose {0} and wait for the image operation to finish before closing."), CancelPrompts.ButtonLabel), InfoBarSeverity.Warning); }; WorkspaceBox.Text = @"C:\tiny11"; - SupportPurpose.Text = ProjectSupport.Purpose; + SupportPurpose.Text = L10n.T("SupportPurpose", ProjectSupport.Purpose); OutputBox.Text = Path.Combine(WorkspaceBox.Text, $"tiny11-{DateTime.Now:yyyyMMdd-HHmmss}.iso"); BuildStages.ItemsSource = board.Stages; progressTimer.Tick += (_, _) => { FlushBuildProgress(); DrainDownload(); }; @@ -87,7 +88,7 @@ public MainWindow() Root.ActualThemeChanged += (_, _) => ApplyContrast(); Activated += (_, _) => ApplyContrast(); Closed += (_, _) => progressTimer.Stop(); - LatestDownloadDescription.Text = $"Official Microsoft ISO · {HostArchitecture.ToUpperInvariant()} · {CultureInfo.CurrentUICulture.NativeName}"; + LatestDownloadDescription.Text = string.Format(L10n.T("OfficialIsoFmt", "Official Microsoft ISO · {0} · {1}"), HostArchitecture.ToUpperInvariant(), CultureInfo.CurrentUICulture.NativeName); foreach (var choice in options) choice.PropertyChanged += (_, _) => UpdateCounts(); initialized = true; ApplyContrast(); @@ -103,11 +104,24 @@ public MainWindow() var recipe = JsonSerializer.Deserialize(File.ReadAllText(args[restoreIndex + 1]), JsonDefaults.Options) ?? throw new InvalidDataException("The saved session is empty."); RestoreRecipe(recipe); - ShowNotice("Session restored.", "Inspect the image again before continuing. No build starts automatically."); + ShowNotice(L10n.T("SessionRestored", "Session restored."), L10n.T("SessionRestoredMsg", "Inspect the image again before continuing. No build starts automatically.")); } - catch (Exception error) { ShowNotice("Session could not be restored.", ErrorText.Reason(error), InfoBarSeverity.Error); } + catch (Exception error) { ShowNotice(L10n.T("SessionRestoreFail", "Session could not be restored."), ErrorText.Reason(error), InfoBarSeverity.Error); } } UpdateCounts(); + ApplyLanguageSelection(); + } + + private void ApplyLanguageSelection() + { + _appliedLanguageIndex = Localization.Current switch + { + "zh-CN" => 2, + "en-US" => 1, + _ => 0 + }; + if (LanguageChoice.SelectedIndex != _appliedLanguageIndex) + LanguageChoice.SelectedIndex = _appliedLanguageIndex; } private FrameworkElement[] Panels => [WelcomePanel, SourcePanel, CleanupPanel, SettingsPanel, ReviewPanel, BuildPanel]; @@ -202,9 +216,9 @@ private void UpdateNavigation() NextButton.IsEnabled = !busy && canAdvance; // After a build stops, the primary action says what it will do: start over, or continue the resume. var retry = step == 5 && !completed && stoppedRun is not null; - ActionLabel.Text = completed ? "Build another" : retry ? (stoppedRun == OperationKind.Resume ? "Resume again" : "Build again") : step switch + ActionLabel.Text = completed ? L10n.T("ActionBuildAnother", "Build another") : retry ? (stoppedRun == OperationKind.Resume ? L10n.T("ActionResumeAgain", "Resume again") : L10n.T("ActionBuildAgain", "Build again")) : step switch { - 0 => "Start", 1 => "Next", 2 => "Next", 3 => "Review", 4 => "Continue", _ => "Build" + 0 => L10n.T("ActionStart", "Start"), 1 => L10n.T("ActionNext", "Next"), 2 => L10n.T("ActionNext", "Next"), 3 => L10n.T("ActionReview", "Review"), 4 => L10n.T("ActionContinue", "Continue"), _ => L10n.T("ActionBuild", "Build") }; ActionLabel.FontSize = 32; Grid.SetColumn(NextButton, 2); @@ -253,7 +267,7 @@ private void OnModeCardClick(object sender, RoutedEventArgs args) if (busy || modalOpen) return; if (sender is not Button { Tag: string tag } || !Enum.TryParse(tag, out var mode) || !Enum.IsDefined(mode)) { - ShowNotice("Build mode could not be selected.", "The selected card does not identify a valid build mode.", InfoBarSeverity.Error); + ShowNotice(L10n.T("ModeSelectFail", "Build mode could not be selected."), L10n.T("ModeSelectFailMsg", "The selected card does not identify a valid build mode."), InfoBarSeverity.Error); return; } ModeChoice.SelectedIndex = (int)mode; @@ -286,16 +300,16 @@ private async void OnNext(object sender, RoutedEventArgs args) } if (step == 2 && UsesCore && !coreAcknowledged) { - coreAcknowledged = await Confirm("Core is not serviceable", - "No Windows Update\nNo Microsoft Defender\nNo recovery environment\nNo added languages or features\n\n" + - "Use only in an isolated test VM.\nThe image may not boot.", - "I agree"); - CoreConsentStatus.Text = coreAcknowledged ? "Core limitations accepted for this build." : "Core limitations have not been accepted."; + coreAcknowledged = await Confirm(L10n.T("CoreConfirmTitle", "Core is not serviceable"), + L10n.T("CoreConfirmBody", "No Windows Update\nNo Microsoft Defender\nNo recovery environment\nNo added languages or features\n\n" + + "Use only in an isolated test VM.\nThe image may not boot."), + L10n.T("CoreConfirmButton", "I agree")); + CoreConsentStatus.Text = coreAcknowledged ? L10n.T("CoreConsentAccepted", "Core limitations accepted for this build.") : L10n.T("CoreConsentNotAccepted", "Core limitations have not been accepted."); if (!coreAcknowledged) return; } await RunOperation(OperationKind.Check, async token => { - Working("Checking your choices…"); + Working(L10n.T("CheckingChoices", "Checking your choices…")); if (step >= 1 && image is null) throw new InvalidOperationException("Inspect the selected Windows image first."); image?.Validate(); if (step >= 2) CreateRecipe().ValidateOptions(); @@ -373,7 +387,7 @@ private async Task RunOperation(OperationKind kind, Func "Open logs", NoticeAction.OpenTools => "Get image tools", - NoticeAction.OpenOfficialDownload => "Open Microsoft download page", _ => "Copy details" + NoticeAction.OpenLogs => L10n.T("NoticeOpenLogs", "Open logs"), NoticeAction.OpenTools => L10n.T("NoticeOpenTools", "Get image tools"), + NoticeAction.OpenOfficialDownload => L10n.T("NoticeOpenDownload", "Open Microsoft download page"), _ => L10n.T("NoticeCopyDetails", "Copy details") }; NoticeButton.Content = label; Microsoft.UI.Xaml.Automation.AutomationProperties.SetName(NoticeButton, label); @@ -503,13 +519,13 @@ private void OnNoticeAction(object sender, RoutedEventArgs args) } catch (Exception error) when (error is IOException or InvalidDataException or System.ComponentModel.Win32Exception or InvalidOperationException) { - ShowNotice("Could not open the download page.", page + "\n" + + ShowNotice(L10n.T("NoticeDownloadPageFailTitle", "Could not open the download page."), page + "\n" + ErrorText.Reason(error), InfoBarSeverity.Error, NoticeAction.CopyDetails, error.ToString()); } return; } if (noticeAction != NoticeAction.CopyDetails || noticeDetails.Length == 0) return; - NoticeButton.Content = CopyToClipboard(noticeDetails) ? "Copied" : "Could not copy"; + NoticeButton.Content = CopyToClipboard(noticeDetails) ? L10n.T("CopiedLabel", "Copied") : L10n.T("CopyFailLabel", "Could not copy"); } // A clipboard that is locked or unavailable is reported to the caller, never thrown into the page or a dialog. @@ -574,7 +590,7 @@ private async Task Confirm(string title, string content, string action) var dialog = new ContentDialog { XamlRoot = Root.XamlRoot, RequestedTheme = DialogTheme(), - Title = title, Content = content, PrimaryButtonText = action, CloseButtonText = "Cancel", + Title = title, Content = content, PrimaryButtonText = action, CloseButtonText = L10n.T("DialogCancel", "Cancel"), DefaultButton = ContentDialogButton.Close }; return await dialog.ShowAsync() == ContentDialogResult.Primary; @@ -600,7 +616,7 @@ private async void OnSupport(object sender, RoutedEventArgs args) } catch (Exception error) { - ShowNotice("Support options could not be opened.", ErrorText.Reason(error), + ShowNotice(L10n.T("SupportOptionsFailTitle", "Support options could not be opened."), ErrorText.Reason(error), InfoBarSeverity.Error, NoticeAction.CopyDetails, error.ToString()); } finally @@ -616,15 +632,15 @@ private async void OnSupportDestination(object sender, RoutedEventArgs args) { if (sender is not FrameworkElement { Tag: string tag } || !Enum.TryParse(tag, out var destination)) - throw new InvalidDataException("This support link has no valid destination."); + throw new InvalidDataException(L10n.T("SupportLinkInvalid", "This support link has no valid destination.")); var uri = ProjectSupport.Link(destination); if (!await Windows.System.Launcher.LaunchUriAsync(uri)) - throw new IOException($"Your browser could not open {uri}. No payment was made."); + throw new IOException(string.Format(L10n.T("BrowserOpenFailMsg", "Your browser could not open {0}. No payment was made."), uri)); SupportError.IsOpen = false; } catch (Exception error) { - SupportError.Title = "Could not open the support page"; + SupportError.Title = L10n.T("SupportOpenFailTitle", "Could not open the support page"); SupportError.Message = ErrorText.Reason(error); SupportError.IsOpen = true; } @@ -653,7 +669,7 @@ private async void OnSupportDestination(object sender, RoutedEventArgs args) private Task SaveFile(string filename, string extension) { var picker = new FileSavePicker(AppWindow.Id) { SuggestedFileName = filename }; - picker.FileTypeChoices.Add(extension == ".iso" ? "ISO image" : "Image recipe", new List { extension }); + picker.FileTypeChoices.Add(extension == ".iso" ? L10n.T("FileTypeIso", "ISO image") : L10n.T("FileTypeRecipe", "Image recipe"), new List { extension }); return Pick(async () => (await picker.PickSaveFileAsync())?.Path); } @@ -673,7 +689,7 @@ private void OnSourceChanged(object sender, TextChangedEventArgs args) pendingImageIndex = null; pendingSourcePath = ""; EditionChoice.ItemsSource = null; - ImageBadge.Text = "No image selected"; + ImageBadge.Text = L10n.T("ImageBadgeNone", "No image selected"); ClearInventory(); UpdateNavigation(); } @@ -686,9 +702,9 @@ private void OnEditionChanged(object sender, SelectionChangedEventArgs args) SourcePathBox.Text.Trim().Equals(pendingSourcePath, StringComparison.OrdinalIgnoreCase))) ClearInventory(); image = selected; - ImageBadge.Text = image is null ? "No image selected" : $"{image.Name}\n{image.Architecture} · build {image.Version.Build}"; + ImageBadge.Text = image is null ? L10n.T("ImageBadgeNone", "No image selected") : $"{image.Name}\n{image.Architecture} · build {image.Version.Build}"; if (image is not null && !image.IsSupported) - ShowNotice("This edition is not supported.", "Choose a Windows 11 client image, 22H2 or newer, in x64 or ARM64 format.", InfoBarSeverity.Warning); + ShowNotice(L10n.T("EditionNotSupported", "This edition is not supported."), L10n.T("EditionNotSupportedMsg", "Choose a Windows 11 client image, 22H2 or newer, in x64 or ARM64 format."), InfoBarSeverity.Warning); UpdateNavigation(); } @@ -696,7 +712,7 @@ private async void OnInspect(object sender, RoutedEventArgs args) { if (!ToolLocator.IsAdministrator) { - ShowNotice("Administrator privileges required.", "Close and reopen Tiny11Builder.exe, then approve the Windows administrator prompt.", InfoBarSeverity.Warning); + ShowNotice(L10n.T("AdminRequired", "Administrator privileges required."), L10n.T("AdminRequiredMsg", "Close and reopen Tiny11Builder.exe, then approve the Windows administrator prompt."), InfoBarSeverity.Warning); return; } await RunOperation(OperationKind.Inspect, InspectSourceAsync); @@ -706,10 +722,10 @@ private async Task InspectSourceAsync(CancellationToken token) { if (!ToolLocator.IsAdministrator) { - ShowNotice("Image selected.", "Close and reopen Tiny11Builder.exe with administrator approval to inspect its editions."); + ShowNotice(L10n.T("ImageSelectedNotice", "Image selected."), L10n.T("ImageSelectedMsg", "Close and reopen Tiny11Builder.exe with administrator approval to inspect its editions.")); return; } - Working("Inspecting the image…"); + Working(L10n.T("InspectingImage", "Inspecting the image…")); var source = SourcePathBox.Text.Trim(); servicingTool = ToolLocator.FindDism(); await Task.Run(() => ToolLocator.Validate(servicingTool, "dism.exe"), token); @@ -717,7 +733,7 @@ private async Task InspectSourceAsync(CancellationToken token) var restored = source.Equals(pendingSourcePath, StringComparison.OrdinalIgnoreCase) ? pendingImageIndex : null; EditionChoice.ItemsSource = images; EditionChoice.SelectedIndex = ImageDefaults.SelectPosition(images, restored); - ShowNotice("Image inspected.", $"{images.Count} editions found. {image?.Name} is selected."); + ShowNotice(L10n.T("ImageInspected", "Image inspected."), string.Format(L10n.T("ImageInspectedFmt", "{0} editions found. {1} is selected."), images.Count, image?.Name)); } private void BeginDownloadText(Func format) @@ -754,7 +770,7 @@ private async void OnDownload(object sender, RoutedEventArgs args) => await RunO var hash = OfficialImageDownloader.ValidateHash(PublishedHashBox.Text); var path = await SaveFile(Path.GetFileName(uri.AbsolutePath), ".iso"); if (path is null) return; - Working("Downloading from Microsoft…"); + Working(L10n.T("DownloadingMicrosoft", "Downloading from Microsoft…")); using var client = OfficialImageDownloader.CreateClient(); DownloadBar.IsIndeterminate = true; BeginDownloadText(p => FormatDownload(p, "")); @@ -765,16 +781,16 @@ private async void OnDownload(object sender, RoutedEventArgs args) => await RunO DrainDownload(); SourcePathBox.Text = downloaded.Path; DownloadStatus.Text = $"SHA-256: {downloaded.Sha256}"; - ShowNotice(downloaded.VerifiedAgainstPublishedHash ? "Official ISO downloaded and hash-verified." : "Microsoft-hosted ISO downloaded.", - downloaded.VerifiedAgainstPublishedHash ? "The file matches the supplied published SHA-256. Inspect it to select an edition." : - "No published hash was supplied. The computed hash is shown, but authenticity has not been independently verified.", InfoBarSeverity.Informational); + ShowNotice(downloaded.VerifiedAgainstPublishedHash ? L10n.T("DownloadVerified", "Official ISO downloaded and hash-verified.") : L10n.T("DownloadMicrosoft", "Microsoft-hosted ISO downloaded."), + downloaded.VerifiedAgainstPublishedHash ? L10n.T("DownloadVerifiedMsg", "The file matches the supplied published SHA-256. Inspect it to select an edition.") : + L10n.T("DownloadUnverifiedMsg", "No published hash was supplied. The computed hash is shown, but authenticity has not been independently verified."), InfoBarSeverity.Informational); } finally { EndDownloadText(); } }); private async void OnDownloadLatest(object sender, RoutedEventArgs args) => await RunOperation(OperationKind.Download, async token => { - Working("Finding the latest Windows 11…"); + Working(L10n.T("FindingLatest", "Finding the latest Windows 11…")); DownloadBar.IsIndeterminate = true; try { @@ -790,27 +806,27 @@ private async void OnDownloadLatest(object sender, RoutedEventArgs args) => awai var hashFile = path + ".sha256"; if (File.Exists(path) && File.Exists(hashFile)) { - Working("Checking the downloaded ISO…"); + Working(L10n.T("CheckingDownloaded", "Checking the downloaded ISO…")); await using var input = File.OpenRead(path); await IsoStructure.ValidateAsync(input, token); var actual = Convert.ToHexString(await SHA256.HashDataAsync(input, token)); if (actual != (await File.ReadAllTextAsync(hashFile, token)).Trim() || (media.Sha256 is not null && actual != media.Sha256)) - throw new InvalidDataException("The cached ISO has changed. It has not been accepted."); - DownloadStatus.Text = "Previously downloaded ISO verified."; + throw new InvalidDataException(L10n.T("CachedIsoChanged", "The cached ISO has changed. It has not been accepted.")); + DownloadStatus.Text = L10n.T("PreviouslyVerified", "Previously downloaded ISO verified."); } else { if (File.Exists(path)) path = Path.Combine(folder, Path.GetFileNameWithoutExtension(media.FileName) + "-" + Guid.NewGuid().ToString("N")[..8] + ".iso"); - Working("Downloading from Microsoft…"); + Working(L10n.T("DownloadingMicrosoft", "Downloading from Microsoft…")); using var downloadClient = OfficialImageDownloader.CreateClient(); BeginDownloadText(p => FormatDownload(p, $"{media.Language} · ")); var download = await new OfficialImageDownloader(downloadClient).DownloadAsync(media.Url.AbsoluteUri, path, media.Sha256, downloadUpdates, token, status: catalogProgress); DrainDownload(); await File.WriteAllTextAsync(path + ".sha256", download.Sha256, token); - DownloadStatus.Text = $"Downloaded {media.Description}. SHA-256: {download.Sha256}" + - (download.VerifiedAgainstPublishedHash ? " (published hash verified)" : " (computed hash; Microsoft supplied no published hash)"); + DownloadStatus.Text = string.Format(L10n.T("DownloadedFmt", "Downloaded {0}. SHA-256: {1}"), media.Description, download.Sha256) + + (download.VerifiedAgainstPublishedHash ? L10n.T("HashPublished", " (published hash verified)") : L10n.T("HashComputed", " (computed hash; Microsoft supplied no published hash)")); } SourcePathBox.Text = path; await InspectSourceAsync(token); @@ -825,7 +841,7 @@ private async Task EnsureToolsAsync(CancellationToken token) client.Timeout = TimeSpan.FromSeconds(60); isoTool = await new OscdimgAcquirer(client).EnsureAsync(Path.Combine(dataDirectory, "tools"), new UiProgress(text => DispatcherQueue.TryEnqueue(() => ToolStatus.Text = text)), token); - ToolStatus.Text = "Image and ISO tools ready."; + ToolStatus.Text = L10n.T("ToolsReady", "Image and ISO tools ready."); } private void OnModeChanged(object sender, SelectionChangedEventArgs args) @@ -852,14 +868,14 @@ private void UpdateModePresentation() CustomChoices.Visibility = SelectedMode == BuildMode.Custom ? Visibility.Visible : Visibility.Collapsed; CoreWarning.IsOpen = UsesCore; CoreConsentStatus.Visibility = UsesCore ? Visibility.Visible : Visibility.Collapsed; - CoreConsentStatus.Text = coreAcknowledged ? "Core limitations accepted for this build." : "You will be asked to accept Core's limitations before continuing."; + CoreConsentStatus.Text = coreAcknowledged ? L10n.T("CoreConsentAccepted", "Core limitations accepted for this build.") : L10n.T("CoreConsentPending", "You will be asked to accept Core's limitations before continuing."); ModeDescription.Text = SelectedMode switch { - BuildMode.Tiny11 => "Original Tiny11 removals, including Edge/WebView2 and OneDrive binaries. Windows Update, Defender and recovery are kept. Choose Custom to change individual removals.", - BuildMode.Tiny11Core => "Tiny11 Core removals, including irreversible component pruning. Choose Custom to change individual removals.", - _ => "Start with nothing selected, then choose what to remove or disable. You can also list individual components, including protected items." + BuildMode.Tiny11 => L10n.T("ModeDescTiny", "Original Tiny11 removals, including Edge/WebView2 and OneDrive binaries. Windows Update, Defender and recovery are kept. Choose Custom to change individual removals."), + BuildMode.Tiny11Core => L10n.T("ModeDescCore", "Tiny11 Core removals, including irreversible component pruning. Choose Custom to change individual removals."), + _ => L10n.T("ModeDescCustom", "Start with nothing selected, then choose what to remove or disable. You can also list individual components, including protected items.") }; - SourceMode.Text = $"Build mode: {(ModeChoice.SelectedItem as ComboBoxItem)?.Content}"; + SourceMode.Text = string.Format(L10n.T("ModeLine", "Build mode: {0}"), (ModeChoice.SelectedItem as ComboBoxItem)?.Content); MarkChoice(Tiny11Selection, SelectedMode == BuildMode.Tiny11); MarkChoice(CoreSelection, SelectedMode == BuildMode.Tiny11Core); MarkChoice(CustomSelection, SelectedMode == BuildMode.Custom); @@ -893,21 +909,22 @@ private void UpdateCounts() { if (!initialized) return; var custom = inventory.Count > 0 ? inventory.Count(i => i.Selected) : pendingActions.Length; - SelectionCount.Text = $"{options.Count(o => o.Selected)} removal groups selected · {custom} advanced actions"; - ExpertCount.Text = $"{inventory.Count:N0} components listed · {custom:N0} advanced actions selected."; + SelectionCount.Text = string.Format(L10n.T("SelCountFmt", "{0} removal groups selected · {1} advanced actions"), options.Count(o => o.Selected), custom); + ExpertCount.Text = string.Format(L10n.T("ExpCountFmt", "{0} components listed · {1} advanced actions selected."), + inventory.Count.ToString("N0", CultureInfo.InvariantCulture), custom.ToString("N0", CultureInfo.InvariantCulture)); UpdateNavigation(); } private async void OnOpenExpert(object sender, RoutedEventArgs args) { if (busy || modalOpen || SelectedMode != BuildMode.Custom) return; - if (image is null) { ShowNotice("Inspect an image first.", "Select an edition before listing its components.", InfoBarSeverity.Warning); return; } - if (!await Confirm("Protected components are included", - "This inventory includes boot drivers, security, licensing, system apps and component payloads. Removing them can make Windows unbootable, insecure or impossible to update. Protected packages may refuse removal. Direct payload removal is unsupported.\n\nContinue only for isolated testing, with backups and a known-good installer.", - "Inspect components")) return; + if (image is null) { ShowNotice(L10n.T("InspectFirstTitle", "Inspect an image first."), L10n.T("InspectFirstMsg", "Select an edition before listing its components."), InfoBarSeverity.Warning); return; } + if (!await Confirm(L10n.T("ProtectedConfirmTitle", "Protected components are included"), + L10n.T("ProtectedConfirmBody", "This inventory includes boot drivers, security, licensing, system apps and component payloads. Removing them can make Windows unbootable, insecure or impossible to update. Protected packages may refuse removal. Direct payload removal is unsupported.\n\nContinue only for isolated testing, with backups and a known-good installer."), + L10n.T("ProtectedConfirmAction", "Inspect components"))) return; await RunOperation(OperationKind.Inventory, async token => { - Working("Listing image components…"); + Working(L10n.T("ListingComponents", "Listing image components…")); var recipe = CreateRecipe() with { CoreAcknowledged = true, ExpertAcknowledged = true }; var items = await Task.Run(() => engine.InventoryAsync(recipe, BuildProgressReporter(), token), token); var previous = inventory.Count > 0 ? inventory.Where(i => i.Selected).Select(i => i.Action).ToArray() : pendingActions; @@ -925,11 +942,11 @@ await RunOperation(OperationKind.Inventory, async token => FilterInventory(); UpdateCounts(); if (!ToolLocator.SupportsServicing(recipe.DismPath, image!)) - ShowNotice("Component list loaded read-only.", "The installed tools could list this image, but building it requires newer " + - "Windows ADK Deployment Tools. Install them and reopen the app before building.", + ShowNotice(L10n.T("ComponentListReadOnly", "Component list loaded read-only."), + L10n.T("ComponentListReadOnlyMsg", "The installed tools could list this image, but building it requires newer Windows ADK Deployment Tools. Install them and reopen the app before building."), InfoBarSeverity.Warning, NoticeAction.OpenTools); else - ShowNotice("Component list loaded.", "Existing selections were preserved. Check the individual items you want to remove or disable."); + ShowNotice(L10n.T("ComponentListLoaded", "Component list loaded."), L10n.T("ComponentListLoadedMsg", "Existing selections were preserved. Check the individual items you want to remove or disable.")); }); } @@ -963,9 +980,9 @@ private string CreateReview() { var recipe = CreateRecipe(); var text = new StringBuilder(); - text.AppendLine($"IMAGE {image?.Summary ?? "Not inspected"}"); - text.AppendLine($"MODE {(ModeChoice.SelectedItem as ComboBoxItem)?.Content}"); - if (SelectedMode == BuildMode.Custom) text.AppendLine($"CORE REMOVALS {UsesCore}"); + text.AppendLine($"IMAGE {image?.Summary ?? L10n.T("ReviewNotInspected", "Not inspected")}"); + text.AppendLine(string.Format(L10n.T("ReviewModeLabelFmt", "MODE {0}"), (ModeChoice.SelectedItem as ComboBoxItem)?.Content)); + if (SelectedMode == BuildMode.Custom) text.AppendLine(string.Format(L10n.T("ReviewCoreRemovalsFmt", "CORE REMOVALS {0}"), UsesCore ? L10n.T("TurboOn", "On") : L10n.T("TurboOff", "Off"))); text.AppendLine($"SOURCE {recipe.SourcePath}"); text.AppendLine($"OUTPUT {recipe.OutputPath}"); text.AppendLine($"WORKSPACE {recipe.WorkspacePath}"); @@ -979,15 +996,15 @@ private string CreateReview() text.AppendLine($".NET 3.5 {recipe.EnableNetFx3}"); text.AppendLine($"LOCAL OOBE {recipe.PreferLocalAccount} (best effort)"); if (SetupLabels.LegacyReason(recipe) is { } legacyReason) text.AppendLine($"LEGACY SETUP automatic, needed for {legacyReason}"); - text.AppendLine("\nREMOVE / DISABLE"); + text.AppendLine("\n" + L10n.T("ReviewRemoveHeader", "REMOVE / DISABLE")); foreach (var option in options.Where(o => o.Selected)) text.AppendLine($" {option.Title}"); - foreach (var action in recipe.ExpertActions) text.AppendLine($" EXPERT {action.Mode} {action.Kind}: {action.Identity}"); - text.AppendLine("\nUNSELECTED ITEMS ARE KEPT."); + foreach (var action in recipe.ExpertActions) text.AppendLine(string.Format(L10n.T("ReviewExpertItemFmt", " EXPERT {0} {1}: {2}"), action.Mode, action.Kind, action.Identity)); + text.AppendLine("\n" + L10n.T("ReviewUnselectedKept", "UNSELECTED ITEMS ARE KEPT.")); if (recipe.Flavor == BuildFlavor.Core) - text.AppendLine("\nCORE: irreversible component pruning, WinRE removed, Update and Defender disabled. Install optional features now, not after creation."); + text.AppendLine("\n" + L10n.T("ReviewCoreNote", "CORE: irreversible component pruning, WinRE removed, Update and Defender disabled. Install optional features now, not after creation.")); if (recipe.ExpertActions.Length > 0) - text.AppendLine("\nEXPERT: dependencies are not resolved. Protected and boot-critical removals may produce an unbootable image."); - text.AppendLine("\nPolicies may be ignored by a particular Windows build/edition or changed by updates. VM boot validation is required."); + text.AppendLine("\n" + L10n.T("ReviewExpertNote", "EXPERT: dependencies are not resolved. Protected and boot-critical removals may produce an unbootable image.")); + text.AppendLine("\n" + L10n.T("ReviewPoliciesNote", "Policies may be ignored by a particular Windows build/edition or changed by updates. VM boot validation is required.")); return text.ToString(); } @@ -995,44 +1012,55 @@ private void UpdateReview() { var recipe = CreateRecipe(); ReviewText.Text = CreateReview(); - ReviewImage.Text = image?.Summary ?? "No image selected"; + ReviewImage.Text = image?.Summary ?? L10n.T("ImageBadgeNone", "No image selected"); ReviewMode.Text = (ModeChoice.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? ""; - ReviewRemovals.Text = $"{recipe.SelectedOptions.Length} removal groups · {recipe.ExpertActions.Length} individual actions\n" + - (UsesCore ? "No servicing, recovery or Defender." : SelectedMode == BuildMode.Custom ? - "Custom removals may affect servicing, recovery or security." : "Windows Update, recovery and Defender retained."); - ReviewPaths.Text = $"ISO\n{recipe.OutputPath}\n\nWorkspace\n{recipe.WorkspacePath}"; - var compression = recipe.Compression switch { Compression.Recovery => "Recovery ESD", Compression.Maximum => "Maximum WIM", _ => "Fast WIM" }; - ReviewOptions.Text = $"{compression} compression\nTurbo mode: {(recipe.TurboMode ? "On" : "Off")}\n" + - $"Redundant data: {(recipe.RemoveRedundantData ? "Remove" : "Keep")}\n.NET setup payload: {(recipe.OmitNetFx3SetupPayload ? "Omit" : "Keep")}\n" + - $"Compact installation: {(recipe.CompactInstallation ? "On" : "Off")}\n.NET 3.5: {(recipe.EnableNetFx3 ? "Included" : "Not included")}\n" + - $"Local-account setup: {(recipe.PreferLocalAccount ? "Preferred" : "Default")}\nWorking files: {(recipe.KeepWorkingFiles ? "Keep" : "Clean after build")}" + + ReviewRemovals.Text = string.Format(L10n.T("ReviewRemovalsFmt", "{0} removal groups · {1} individual actions"), + recipe.SelectedOptions.Length, recipe.ExpertActions.Length) + "\n" + + (UsesCore ? L10n.T("ReviewNoService", "No servicing, recovery or Defender.") : SelectedMode == BuildMode.Custom ? + L10n.T("ReviewCustomWarn", "Custom removals may affect servicing, recovery or security.") : L10n.T("ReviewRetained", "Windows Update, recovery and Defender retained.")); + ReviewPaths.Text = L10n.T("PathIso", "ISO") + "\n" + recipe.OutputPath + "\n\n" + L10n.T("PathWorkspace", "Workspace") + "\n" + recipe.WorkspacePath; + var compressionWord = recipe.Compression switch + { + Compression.Recovery => L10n.T("CompRecovery", "Recovery ESD"), + Compression.Maximum => L10n.T("CompMaximum", "Maximum WIM"), + _ => L10n.T("CompFast", "Fast WIM") + }; + ReviewOptions.Text = string.Format(L10n.T("ReviewOptionsCompFmt", "{0} compression"), compressionWord) + "\n" + + string.Format(L10n.T("ReviewTurboLine", "Turbo mode: {0}"), recipe.TurboMode ? L10n.T("TurboOn", "On") : L10n.T("TurboOff", "Off")) + "\n" + + string.Format(L10n.T("ReviewRedundantLine", "Redundant data: {0}"), recipe.RemoveRedundantData ? L10n.T("RedundantRemove", "Remove") : L10n.T("RedundantKeep", "Keep")) + "\n" + + string.Format(L10n.T("ReviewNetSetupLine", ".NET setup payload: {0}"), recipe.OmitNetFx3SetupPayload ? L10n.T("NetSetupOmit", "Omit") : L10n.T("NetSetupKeep", "Keep")) + "\n" + + string.Format(L10n.T("ReviewCompactLine", "Compact installation: {0}"), recipe.CompactInstallation ? L10n.T("TurboOn", "On") : L10n.T("TurboOff", "Off")) + "\n" + + string.Format(L10n.T("ReviewNetFxLine", ".NET 3.5: {0}"), recipe.EnableNetFx3 ? L10n.T("NetFxIncluded", "Included") : L10n.T("NetFxNotIncluded", "Not included")) + "\n" + + string.Format(L10n.T("ReviewLocalLine", "Local-account setup: {0}"), recipe.PreferLocalAccount ? L10n.T("LocalPreferred", "Preferred") : L10n.T("LocalDefault", "Default")) + "\n" + + string.Format(L10n.T("ReviewFilesLine", "Working files: {0}"), recipe.KeepWorkingFiles ? L10n.T("FilesKeep", "Keep") : L10n.T("FilesClean", "Clean after build")) + (SetupLabels.Legacy(recipe) is { } legacy ? "\n" + legacy : ""); } - private const string BuildDescriptionText = "The selected edition will be copied, modified, compressed and saved as a bootable ISO."; + private static string BuildDescriptionText => L10n.T("BuildDescText", "The selected edition will be copied, modified, compressed and saved as a bootable ISO."); // The legacy installer is derived from the removals, never chosen, so this only reports it. private void UpdateBuildDescription(BuildRecipe recipe) => BuildDescription.Text = BuildDescriptionText + - (SetupLabels.LegacyReason(recipe) is { } reason ? $" The legacy installer is turned on automatically for {reason}." : ""); + (SetupLabels.LegacyReason(recipe) is { } reason ? string.Format(L10n.T("BuildDescLegacyFmt", " The legacy installer is turned on automatically for {0}."), reason) : ""); private async Task StartBuildAsync() { BuildRecipe recipe; try { recipe = CreateRecipe(); recipe.ValidateOptions(); PathSafety.Validate(recipe); } - catch (Exception error) { ShowNotice("The recipe is not ready.", ErrorText.Reason(error), InfoBarSeverity.Error); return; } - if (!await Confirm("Create this offline Windows image?", - $"Output: {recipe.OutputPath}\n\nMode: {(ModeChoice.SelectedItem as ComboBoxItem)?.Content}\nRemoval groups: {recipe.SelectedOptions.Length}\nIndividual actions: {recipe.ExpertActions.Length}" + + catch (Exception error) { ShowNotice(L10n.T("RecipeNotReadyTitle", "The recipe is not ready."), ErrorText.Reason(error), InfoBarSeverity.Error); return; } + if (!await Confirm(L10n.T("ConfirmBuildTitle", "Create this offline Windows image?"), + string.Format(L10n.T("ConfirmBuildBodyFmt", "Output: {0}\n\nMode: {1}\nRemoval groups: {2}\nIndividual actions: {3}"), + recipe.OutputPath, (ModeChoice.SelectedItem as ComboBoxItem)?.Content, recipe.SelectedOptions.Length, recipe.ExpertActions.Length) + (SetupLabels.Legacy(recipe) is { } legacy ? "\n" + legacy : ""), - "Build")) return; + L10n.T("ActionBuild", "Build"))) return; await RunOperation(OperationKind.Build, async token => { ResetBuildPage(); BuildActivity.IsActive = true; BuildBar.IsIndeterminate = true; - CurrentMessage.Text = "Preparing image tools…"; + CurrentMessage.Text = L10n.T("PreparingTools", "Preparing image tools…"); await EnsureToolsAsync(token); recipe = CreateRecipe(); - BeginBuildRun("Building"); + BeginBuildRun(L10n.T("BuildingHeading", "Building")); var reporter = BuildProgressReporter(); var built = await Task.Run(() => engine.BuildAsync(recipe, reporter, token), token); ShowBuildResult(built, resumed: false, turbo: recipe.TurboMode); @@ -1054,9 +1082,9 @@ private void ResetBuildPage() ResultWarnings.IsOpen = false; liveLog.Clear(); RefreshLog(force: true); - BuildHeading.Text = "Build"; - CurrentStage.Text = "Waiting to start"; - CurrentMessage.Text = "No changes have been made."; + BuildHeading.Text = L10n.T("BuildNoun", "Build"); + CurrentStage.Text = L10n.T("WaitingToStart", "Waiting to start"); + CurrentMessage.Text = L10n.T("NoChanges", "No changes have been made."); BuildGlyph.Glyph = "\uE7C5"; BuildActivity.IsActive = false; BuildBar.IsIndeterminate = false; @@ -1083,40 +1111,45 @@ private void ShowBuildResult(BuildResult built, bool resumed, bool turbo) ResultPath.Text = built.IsoPath; ResultHash.Text = built.Sha256; var size = File.Exists(built.IsoPath) ? $" · {new FileInfo(built.IsoPath).Length / (1024d * 1024 * 1024):N2} GiB ISO" : ""; + var typeWord = resumed ? L10n.T("MetricResumedBuild", "Resumed build") + : turbo ? L10n.T("MetricTurboBuild", "Turbo build") : L10n.T("MetricStandardBuild", "Standard build"); + var minutes = string.Format(L10n.T("MetricMinutes", "{0} minutes"), built.Duration.TotalMinutes.ToString("N1", CultureInfo.InvariantCulture)); + var total = resumed ? L10n.T("MetricInTotal", " in total") : ""; var metrics = new List { - $"{(resumed ? "Resumed build" : turbo ? "Turbo build" : "Standard build")} · {built.Duration.TotalMinutes:N1} minutes{(resumed ? " in total" : "")}{size}" + $"{typeWord} · {minutes}{total}{size}" }; if (!resumed || built.RemovedLogicalBytes > 0 || built.OmittedSetupBytes > 0) - metrics.Add($"Redundant data removed: {built.RemovedLogicalBytes / (1024d * 1024):N1} MiB logical · " + - $"Setup payload omitted: {built.OmittedSetupBytes / (1024d * 1024):N1} MiB"); - metrics.Add("Detailed stage timings are saved with the build report."); + metrics.Add(string.Format(L10n.T("MetricRedundantFmt", "Redundant data removed: {0} logical · Setup payload omitted: {1}"), + (built.RemovedLogicalBytes / (1024d * 1024)).ToString("N1", CultureInfo.InvariantCulture), + (built.OmittedSetupBytes / (1024d * 1024)).ToString("N1", CultureInfo.InvariantCulture))); + metrics.Add(L10n.T("MetricTimingsSaved", "Detailed stage timings are saved with the build report.")); ResultMetrics.Text = string.Join("\n", metrics); var warnings = built.Warnings; // A cancel prompt open for this run reports the warning too, instead of calling the end clean. if (warnings.Count > 0) - active?.NoteWarning("The ISO was saved with warnings. See the warnings on the page."); + active?.NoteWarning(L10n.T("WarnNoteDetail", "The ISO was saved with warnings. See the warnings on the page.")); ResultWarnings.IsOpen = warnings.Count > 0; if (warnings.Count > 0) ResultWarnings.Message = string.Join("\n", warnings.Take(4).Select(w => "• " + ErrorText.Summary(w, 200))) + - (warnings.Count > 4 ? $"\n• {warnings.Count - 4} more in the build log" : ""); + (warnings.Count > 4 ? string.Format(L10n.T("WarnMoreFmt", "\n• {0} more in the build log"), warnings.Count - 4) : ""); ResultPanel.Visibility = Visibility.Visible; - BuildHeading.Text = "ISO created"; - CurrentStage.Text = "Complete"; + BuildHeading.Text = L10n.T("IsoCreated", "ISO created"); + CurrentStage.Text = L10n.T("Complete", "Complete"); BuildActivity.IsActive = false; BuildGlyph.Glyph = "\uE73E"; if (resumed) board.MarkEarlierPhasesDone(); board.Complete(); OverallBar.Value = 100; BuildPercent.Text = "100%"; - OverallStep.Text = "All stages done"; + OverallStep.Text = L10n.T("AllStagesDone", "All stages done"); CurrentPercent.Text = "100%"; HistoryCount.Text = StageSummary.Describe(board); BuildBar.IsIndeterminate = false; BuildBar.Value = 100; CurrentMessage.Text = warnings.Count > 0 ? - "The ISO was saved with warnings. See the skipped work and other details below." : - "The ISO and build report have been saved. Test the ISO in a VM before use."; + L10n.T("ResultWarnMsg", "The ISO was saved with warnings. See the skipped work and other details below.") : + L10n.T("ResultOkMsg", "The ISO and build report have been saved. Test the ISO in a VM before use."); stoppedRun = null; stoppedHasCheckpoint = false; completed = true; @@ -1139,7 +1172,7 @@ private void FlushBuildProgress(bool flushAll = false) // the build, so it must not move the page's stage, bars or history. if (!string.IsNullOrWhiteSpace(p.Message) && !BuildProgressText.IsConsoleProgress(p.Message)) { - if (running is OperationKind.Build or OperationKind.Resume) CurrentMessage.Text = $"Cancellation requested. {p.Message}…"; + if (running is OperationKind.Build or OperationKind.Resume) CurrentMessage.Text = string.Format(L10n.T("CancelRequestedFmt", "Cancellation requested. {0}…"), p.Message); liveLog.Add($"[{p.Stage}] {BuildProgressText.ForDisplay(p.Message)}"); } continue; @@ -1156,7 +1189,7 @@ private void FlushBuildProgress(bool flushAll = false) // Once cancellation has been requested, that status stays until the run actually stops. if (!cancelRequested) CurrentMessage.Text = p.State == BuildOperationState.Failed ? - p.Message + " failed. Expand this stage for error details." : p.Message; + string.Format(L10n.T("FailedDetailFmt", "{0} failed. Expand this stage for error details."), p.Message) : p.Message; } BuildBar.IsIndeterminate = p.Percent is null && p.State is not (BuildOperationState.Completed or BuildOperationState.Failed or BuildOperationState.Cancelled); if (p.Percent is double percent) BuildBar.Value = percent; @@ -1174,7 +1207,7 @@ private void FlushBuildProgress(bool flushAll = false) var overall = start + (end - start) * stage.Fraction(p); OverallBar.Value = Math.Max(OverallBar.Value, overall); BuildPercent.Text = $"{OverallBar.Value:0}%"; - OverallStep.Text = $"Stage {phase + 1} of {board.Stages.Length}"; + OverallStep.Text = string.Format(L10n.T("StageOfFmt", "Stage {0} of {1}"), phase + 1, board.Stages.Length); BuildGlyph.Glyph = stage.Glyph; } var line = p.Detail ?? (p.State is null ? null : p.Message); @@ -1287,8 +1320,8 @@ private async Task WatchSettled(CancellationFlow flow) private void AfterCancelRequested() { if (running is OperationKind.Build or OperationKind.Resume) - CurrentMessage.Text = "Cancellation requested. Waiting for the current operation to finish…"; - Working("Cancelling…"); + CurrentMessage.Text = L10n.T("CancelWaiting", "Cancellation requested. Waiting for the current operation to finish…"); + Working(L10n.T("Cancelling", "Cancelling…")); UpdateNavigation(); } @@ -1441,14 +1474,14 @@ private async void OnSaveRecipe(object sender, RoutedEventArgs args) => await Ru var recipe = CreateRecipe(); recipe.ValidateOptions(); var path = await SaveFile("tiny11-recipe", ".json"); - if (path is not null) { recipe.Write(path); ShowNotice("Recipe saved.", "It contains selections and local paths, not downloaded media or credentials."); } + if (path is not null) { recipe.Write(path); ShowNotice(L10n.T("RecipeSaved", "Recipe saved."), L10n.T("RecipeSavedMsg", "It contains selections and local paths, not downloaded media or credentials.")); } }); private async void OnLoadRecipe(object sender, RoutedEventArgs args) => await RunOperation(OperationKind.Recipe, async _ => { var path = await PickFile(".json"); if (path is null) return; RestoreRecipe(BuildRecipe.Read(path)); - ShowNotice("Recipe loaded.", "Inspect the source again. Core/expert risk acknowledgements must be renewed."); + ShowNotice(L10n.T("RecipeLoaded", "Recipe loaded."), L10n.T("RecipeLoadedMsg", "Inspect the source again. Core/expert risk acknowledgements must be renewed.")); }); private void RestoreRecipe(BuildRecipe recipe) @@ -1487,7 +1520,7 @@ private async void OnRecover(object sender, RoutedEventArgs args) if (busy || modalOpen) return; if (!ToolLocator.IsAdministrator) { - ShowNotice("Administrator privileges required.", "Close and reopen Tiny11Builder.exe with administrator approval to recover a workspace.", InfoBarSeverity.Warning); + ShowNotice(L10n.T("AdminRecoverTitle", "Administrator privileges required."), L10n.T("AdminRecoverMsg", "Close and reopen Tiny11Builder.exe with administrator approval to recover a workspace."), InfoBarSeverity.Warning); return; } var path = await PickFile(".json"); @@ -1495,24 +1528,24 @@ private async void OnRecover(object sender, RoutedEventArgs args) // Advisory only: the engine decides. This just avoids offering a discard it would refuse. if (JobCheckpoint.Exists(Path.GetDirectoryName(Path.GetFullPath(path)) ?? "")) { - if (await Confirm("This job has saved progress", - "Recover discards a job's working copy, and it does not do that for a job with saved progress. " + - "Resume continues the build from that progress instead.\n\nNothing has been changed.", "Resume instead")) + if (await Confirm(L10n.T("JobHasProgress", "This job has saved progress"), + L10n.T("JobHasProgressMsg", "Recover discards a job's working copy, and it does not do that for a job with saved progress. " + + "Resume continues the build from that progress instead.\n\nNothing has been changed."), L10n.T("ConfirmResumeInstead", "Resume instead"))) await ResumeJobAsync(path); return; } - if (!await Confirm("Discard this interrupted workspace?", - "Select only job.json from the interrupted tiny11 job. This discards that job's mounted working copy and removes its staged media. " + + if (!await Confirm(L10n.T("DiscardWorkspace", "Discard this interrupted workspace?"), + L10n.T("DiscardWorkspaceMsg", "Select only job.json from the interrupted tiny11 job. This discards that job's mounted working copy and removes its staged media. " + "Its logs, recipe, report and your source ISO are kept. A job whose image was already committed has saved progress and is never discarded here; " + - "use Resume build for it. Active builds cannot be recovered.", - "Discard working copy")) return; + "use Resume build for it. Active builds cannot be recovered."), + L10n.T("ConfirmDiscard", "Discard working copy"))) return; await RunOperation(OperationKind.Recover, async token => { - Working("Recovering workspace…"); + Working(L10n.T("RecoveringWorkspace", "Recovering workspace…")); var dism = ToolLocator.FindDism(); ToolLocator.Validate(dism, "dism.exe"); await Task.Run(() => engine.RecoverAsync(path, dism, token), token); - ShowNotice("Workspace recovered.", "The interrupted working copy was discarded. Your source and logs were preserved."); + ShowNotice(L10n.T("WorkspaceRecovered", "Workspace recovered."), L10n.T("WorkspaceRecoveredMsg", "The interrupted working copy was discarded. Your source and logs were preserved.")); }); } @@ -1537,22 +1570,22 @@ private async Task ResumeJobAsync(string jobFile) var candidate = ResumeSupport.Inspect(jobFile, JobCheckpoint.Exists, JobCheckpoint.RecipeFileName); if (candidate.Recipe is not { } original) { - ShowNotice("This build can't be resumed.", candidate.Problem ?? ResumeSupport.NoCheckpoint, InfoBarSeverity.Warning); + ShowNotice(L10n.T("ResumeNoProgress", "This build can't be resumed."), candidate.Problem ?? ResumeSupport.NoCheckpoint, InfoBarSeverity.Warning); return; } if (!ToolLocator.IsAdministrator) { - ShowNotice("Administrator privileges required.", "Close and reopen Tiny11Builder.exe with administrator approval, then choose Resume build. Nothing was changed.", InfoBarSeverity.Warning); + ShowNotice(L10n.T("AdminRequired", "Administrator privileges required."), L10n.T("ResumeAdminMsg", "Close and reopen Tiny11Builder.exe with administrator approval, then choose Resume build. Nothing was changed."), InfoBarSeverity.Warning); return; } - if (!await Confirm("Resume this build?", ResumeSupport.Describe(original), "Resume")) return; + if (!await Confirm(L10n.T("ResumeThisBuild", "Resume this build?"), ResumeSupport.Describe(original), L10n.T("ConfirmResume", "Resume"))) return; var recipe = original; if (File.Exists(recipe.OutputPath)) { var picked = await SaveFile(Path.GetFileNameWithoutExtension(recipe.OutputPath), ".iso"); if (picked is null) { - ShowNotice("Resume not started.", "The output file already exists. Choose a new file name to continue.", InfoBarSeverity.Warning); + ShowNotice(L10n.T("ResumeNotStarted", "Resume not started."), L10n.T("OutputExistsMsg", "The output file already exists. Choose a new file name to continue."), InfoBarSeverity.Warning); return; } recipe = recipe with { OutputPath = picked }; @@ -1567,11 +1600,11 @@ await RunOperation(OperationKind.Resume, async token => workDirectory = candidate.JobDirectory; BuildActivity.IsActive = true; BuildBar.IsIndeterminate = true; - CurrentMessage.Text = "Preparing image tools…"; + CurrentMessage.Text = L10n.T("PreparingTools", "Preparing image tools…"); await EnsureToolsAsync(token); recipe = recipe with { DismPath = servicingTool, OscdimgPath = isoTool }; new BuildHost().ValidateTools(recipe); - BeginBuildRun("Resuming"); + BeginBuildRun(L10n.T("ResumingHeading", "Resuming")); var reporter = BuildProgressReporter(); var built = await Task.Run(() => engine.ResumeAsync(candidate.JobFile, recipe, reporter, token), token); ShowBuildResult(built, resumed: true, turbo: recipe.TurboMode); @@ -1587,7 +1620,7 @@ private void OnOpenLogs(object sender, RoutedEventArgs args) if (workDirectory is null) return; if (!Directory.Exists(workDirectory)) { - ShowNotice("The job folder is gone.", $"{workDirectory} no longer exists.", InfoBarSeverity.Warning); + ShowNotice(L10n.T("JobFolderGoneTitle", "The job folder is gone."), string.Format(L10n.T("JobFolderGoneMsg", "{0} no longer exists."), workDirectory), InfoBarSeverity.Warning); return; } OpenFolder(workDirectory); @@ -1595,11 +1628,47 @@ private void OnOpenLogs(object sender, RoutedEventArgs args) private void OpenFolder(string path) { try { using var process = Process.Start(new ProcessStartInfo("explorer.exe") { UseShellExecute = true, Arguments = $"\"{path}\"" }); } - catch (Exception error) { ShowNotice("Folder could not be opened.", ErrorText.Reason(error), InfoBarSeverity.Error); } + catch (Exception error) { ShowNotice(L10n.T("FolderOpenFailTitle", "Folder could not be opened."), ErrorText.Reason(error), InfoBarSeverity.Error); } } private void OnThemeChanged(object sender, SelectionChangedEventArgs args) => ApplyTheme(); + // Language resources are resolved when each window is created, so the cleanest way to apply a new + // language everywhere is to persist the choice and restart the process. + private void OnLanguageChanged(object sender, SelectionChangedEventArgs args) + { + if (LanguageChoice.SelectedIndex == _appliedLanguageIndex) return; + _appliedLanguageIndex = LanguageChoice.SelectedIndex; + Localization.Current = _appliedLanguageIndex switch + { + 2 => "zh-CN", + 1 => "en-US", + _ => string.Empty + }; + RestartApplication(); + } + + private void RestartApplication() + { + string notice = L10n.T("LangAppliedNotice", "Language applied. Restart the app to see all text in the new language."); + try + { + var exe = Environment.ProcessPath; + if (!string.IsNullOrEmpty(exe)) + { + Process.Start(new ProcessStartInfo(exe) { UseShellExecute = true }); + Application.Current.Exit(); + return; + } + } + catch (Exception error) + { + ShowNotice(notice, ErrorText.Reason(error), InfoBarSeverity.Warning); + return; + } + ShowNotice(notice, L10n.T("LangRestartCloseMsg", "Close and reopen Tiny11 Builder."), InfoBarSeverity.Warning); + } + private bool HighContrast { get diff --git a/src/Tiny11Builder.App/OperationFlow.cs b/src/Tiny11Builder.App/OperationFlow.cs index 54f1f48a..b5fd5b56 100644 --- a/src/Tiny11Builder.App/OperationFlow.cs +++ b/src/Tiny11Builder.App/OperationFlow.cs @@ -50,7 +50,7 @@ public static OperationOutcome Classify(OperationKind kind, Exception error, boo var recovery = failure is not null && NeedsRecovery(failure); var (title, message) = cancelled ? Cancelled(kind, canResume) : Failed(kind, ErrorText.Reason(error), canResume); if (recovery) - message += " The job's mounted copy could not be discarded. Use Recover workspace and choose this job's job.json."; + message += " " + L10n.T("OutcomeRecoverySuffix", "The job's mounted copy could not be discarded. Use Recover workspace and choose this job's job.json."); return new(kind, cancelled, title, message, cancelled && !recovery ? NoticeLevel.Warning : NoticeLevel.Error, failure?.WorkDirectory, error.ToString(), recovery, failure is not null && !recovery ? CleanupProblems(failure) : ""); } @@ -77,27 +77,27 @@ private static string CleanupProblems(BuildFailureException failure) private static (string Title, string Message) Cancelled(OperationKind kind, bool canResume) => kind switch { - OperationKind.Build => ("Build cancelled.", "Your source was not changed. Working files and logs are kept." + - (canResume ? " Saved progress can be resumed." : "")), - OperationKind.Resume => ("Resume cancelled.", "Saved progress and logs are kept." + (canResume ? " You can resume again." : "")), - OperationKind.Inspect => ("Inspection cancelled.", "The image was not changed."), - OperationKind.Inventory => ("Component list cancelled.", "The image was not changed."), - OperationKind.Download => ("Download cancelled.", "Partial downloads are kept, so you can continue later. No source media was modified."), - OperationKind.Recover => ("Recovery cancelled.", "Run Recover workspace again to finish cleaning up this job."), - OperationKind.Elevate => ("Restart cancelled.", "The app is still running without administrator privileges."), - _ => ("Cancelled.", "Nothing was changed.") + OperationKind.Build => (L10n.T("CancelBuildTitle", "Build cancelled."), L10n.T("CancelBuildMsg", "Your source was not changed. Working files and logs are kept.") + + (canResume ? L10n.T("OutcomeResumeSuffix", " Saved progress can be resumed.") : "")), + OperationKind.Resume => (L10n.T("CancelResumeTitle", "Resume cancelled."), L10n.T("CancelResumeMsg", "Saved progress and logs are kept.") + (canResume ? L10n.T("OutcomeResumeAgain", " You can resume again.") : "")), + OperationKind.Inspect => (L10n.T("CancelInspectTitle", "Inspection cancelled."), L10n.T("CancelInspectMsg", "The image was not changed.")), + OperationKind.Inventory => (L10n.T("CancelInventoryTitle", "Component list cancelled."), L10n.T("CancelInventoryMsg", "The image was not changed.")), + OperationKind.Download => (L10n.T("CancelDownloadTitle", "Download cancelled."), L10n.T("CancelDownloadMsg", "Partial downloads are kept, so you can continue later. No source media was modified.")), + OperationKind.Recover => (L10n.T("CancelRecoverTitle", "Recovery cancelled."), L10n.T("CancelRecoverMsg", "Run Recover workspace again to finish cleaning up this job.")), + OperationKind.Elevate => (L10n.T("CancelElevateTitle", "Restart cancelled."), L10n.T("CancelElevateMsg", "The app is still running without administrator privileges.")), + _ => (L10n.T("CancelDefaultTitle", "Cancelled."), L10n.T("CancelDefaultMsg", "Nothing was changed.")) }; private static (string Title, string Message) Failed(OperationKind kind, string reason, bool canResume) => kind switch { - OperationKind.Build => ("Build failed.", reason + (canResume ? " Saved progress can be resumed." : "")), - OperationKind.Resume => ("Resume failed.", reason + (canResume ? " Saved progress is kept, so you can resume again." : "")), - OperationKind.Inspect => ("The image could not be inspected.", reason), - OperationKind.Inventory => ("The component list could not be loaded.", reason), - OperationKind.Download => ("The download did not complete.", reason), - OperationKind.Recover => ("Recovery did not complete.", reason), - OperationKind.Elevate => ("The app could not restart as administrator.", reason), - _ => ("The operation did not complete.", reason) + OperationKind.Build => (L10n.T("FailBuildTitle", "Build failed."), reason + (canResume ? L10n.T("OutcomeResumeSuffix", " Saved progress can be resumed.") : "")), + OperationKind.Resume => (L10n.T("FailResumeTitle", "Resume failed."), reason + (canResume ? L10n.T("OutcomeResumeAgain", " Saved progress is kept, so you can resume again.") : "")), + OperationKind.Inspect => (L10n.T("FailInspectTitle", "The image could not be inspected."), reason), + OperationKind.Inventory => (L10n.T("FailInventoryTitle", "The component list could not be loaded."), reason), + OperationKind.Download => (L10n.T("FailDownloadTitle", "The download did not complete."), reason), + OperationKind.Recover => (L10n.T("FailRecoverTitle", "Recovery did not complete."), reason), + OperationKind.Elevate => (L10n.T("FailElevateTitle", "The app could not restart as administrator."), reason), + _ => (L10n.T("FailDefaultTitle", "The operation did not complete."), reason) }; } @@ -213,6 +213,10 @@ internal static class ResumeSupport public const string NoCheckpoint = "This build has no saved progress to resume. It was started before progress was saved, or it stopped before its image was saved, " + "so it can't be continued. Nothing was changed, and its logs and files are kept."; + // Localized form of NoCheckpoint, used at the call site. + public static string NoCheckpointLocalized => L10n.T("ResumeNoCheckpoint", + "This build has no saved progress to resume. It was started before progress was saved, or it stopped before its image was saved, " + + "so it can't be continued. Nothing was changed, and its logs and files are kept."); // hasCheckpoint is advisory: it only decides what to offer. The engine verifies the saved work itself. public static ResumeCandidate Inspect(string jobFile, Func hasCheckpoint, string recipeFileName = "recipe.json") @@ -225,18 +229,18 @@ public static ResumeCandidate Inspect(string jobFile, Func hasChec } catch (Exception error) when (error is ArgumentException or NotSupportedException or PathTooLongException) { - return new(jobFile, "", null, "That is not a valid job file path."); + return new(jobFile, "", null, L10n.T("ResumeBadPath", "That is not a valid job file path.")); } if (!Path.GetFileName(file).Equals(JobFileName, StringComparison.OrdinalIgnoreCase)) - return new(file, directory, null, "Choose the job.json file inside the build's tiny11-… folder."); - if (!hasCheckpoint(directory)) return new(file, directory, null, NoCheckpoint); + return new(file, directory, null, L10n.T("ResumePickJob", "Choose the job.json file inside the build's tiny11-… folder.")); + if (!hasCheckpoint(directory)) return new(file, directory, null, NoCheckpointLocalized); var recipeFile = Path.Combine(directory, recipeFileName); if (!File.Exists(recipeFile)) - return new(file, directory, null, "This job's saved recipe is missing, so it can't be resumed."); + return new(file, directory, null, L10n.T("ResumeMissingRecipe", "This job's saved recipe is missing, so it can't be resumed.")); try { return new(file, directory, BuildRecipe.Read(recipeFile), null); } catch (Exception error) when (error is IOException or UnauthorizedAccessException or InvalidDataException or System.Text.Json.JsonException) { - return new(file, directory, null, "This job's saved recipe could not be read. " + ErrorText.Reason(error)); + return new(file, directory, null, L10n.T("ResumeUnreadableRecipe", "This job's saved recipe could not be read. ") + ErrorText.Reason(error)); } } @@ -244,26 +248,30 @@ public static string Describe(BuildRecipe recipe) { var mode = CleanupCatalog.ModeForRecipe(recipe) switch { - BuildMode.Tiny11 => "Tiny11", BuildMode.Tiny11Core => "Tiny11 Core", - _ => recipe.Flavor == BuildFlavor.Core ? "Custom with Core removals" : "Custom" + BuildMode.Tiny11 => L10n.T("ModeTiny11", "Tiny11"), + BuildMode.Tiny11Core => L10n.T("ModeTiny11Core", "Tiny11 Core"), + _ => recipe.Flavor == BuildFlavor.Core ? L10n.T("ModeCustomCore", "Custom with Core removals") : L10n.T("ModeCustom", "Custom") }; var compression = recipe.Compression switch { - Compression.Recovery => "Recovery ESD", Compression.Maximum => "Maximum WIM", _ => "Fast WIM" + Compression.Recovery => L10n.T("CompRecovery", "Recovery ESD"), + Compression.Maximum => L10n.T("CompMaximum", "Maximum WIM"), + _ => L10n.T("CompFast", "Fast WIM") }; var text = new StringBuilder(); - text.AppendLine($"Output: {recipe.OutputPath}"); - text.AppendLine($"Mode: {mode} · {recipe.SelectedOptions.Length} removal groups · {recipe.ExpertActions.Length} individual actions"); - text.AppendLine($"Compression: {compression}"); + text.AppendLine(string.Format(L10n.T("ResumeHeader", "Output: {0}"), recipe.OutputPath)); + text.AppendLine(string.Format(L10n.T("ResumeModeLine", "Mode: {0} · {1} removal groups · {2} individual actions"), + mode, recipe.SelectedOptions.Length, recipe.ExpertActions.Length)); + text.AppendLine(string.Format(L10n.T("ResumeCompressionLine", "Compression: {0}"), compression)); text.AppendLine(); - text.AppendLine("The removal choices from the original build stay as they were. They can't be changed when resuming."); + text.AppendLine(L10n.T("ResumeUnchangeable", "The removal choices from the original build stay as they were. They can't be changed when resuming.")); if (recipe.Flavor == BuildFlavor.Core) - text.AppendLine("Core: no Windows Update, no Microsoft Defender, no recovery environment. Use only in an isolated test VM."); + text.AppendLine(L10n.T("ResumeCoreNote", "Core: no Windows Update, no Microsoft Defender, no recovery environment. Use only in an isolated test VM.")); if (SetupLabels.Legacy(recipe) is { } legacy) text.AppendLine(legacy + "."); if (recipe.ExpertActions.Length > 0) - text.AppendLine("Individual removals include protected items. The image may not boot."); + text.AppendLine(L10n.T("ResumeProtectedNote", "Individual removals include protected items. The image may not boot.")); text.AppendLine(); - text.Append("The saved progress is verified first. If anything does not match, nothing is changed."); + text.Append(L10n.T("ResumeVerifyFirst", "The saved progress is verified first. If anything does not match, nothing is changed.")); return text.ToString(); } } @@ -278,12 +286,14 @@ internal static class SetupLabels if (!SetupInstaller.RequiresLegacy(recipe)) return null; var core = recipe.Flavor == BuildFlavor.Core; var payload = recipe.ExpertActions.Any(a => a.Kind == ImageItemKind.ComponentPayload && a.Mode == ExpertActionMode.Remove); - return core && payload ? "Core and component-payload removals" : core ? "Core removals" : - payload ? "component-payload removals" : "the selected removals"; + return core && payload ? L10n.T("LegacyReasonCorePayload", "Core and component-payload removals") + : core ? L10n.T("LegacyReasonCore", "Core removals") + : payload ? L10n.T("LegacyReasonPayload", "component-payload removals") + : L10n.T("LegacyReasonSelected", "the selected removals"); } public static string? Legacy(BuildRecipe recipe) => - LegacyReason(recipe) is { } reason ? $"Legacy installer: automatic (needed for {reason})" : null; + LegacyReason(recipe) is { } reason ? string.Format(L10n.T("LegacyInstaller", "Legacy installer: automatic (needed for {0})"), reason) : null; } // "3 of 7 stages done": the count shown over the stage history, read from the same rows the history draws. @@ -297,8 +307,12 @@ public static string Describe(BuildStageBoard board) var done = stages.Count(IsDone); var failed = stages.Count(stage => stage.Operations.Any(o => o.State == BuildOperationState.Failed)); var cancelled = stages.Any(stage => stage.Operations.Any(o => o.State == BuildOperationState.Cancelled)); - var text = done == stages.Length ? $"All {stages.Length} stages done" : $"{done} of {stages.Length} stages done"; - return failed > 0 ? $"{text} · {failed} failed" : cancelled ? text + " · cancelled" : text; + var text = done == stages.Length + ? string.Format(L10n.T("StagesAllDone", "All {0} stages done"), stages.Length) + : string.Format(L10n.T("StagesDone", "{0} of {1} stages done"), done, stages.Length); + return failed > 0 ? text + string.Format(L10n.T("StagesFailed", " · {0} failed"), failed) + : cancelled ? text + L10n.T("StagesCancelled", " · cancelled") + : text; } } @@ -320,15 +334,15 @@ internal sealed record OperationEnd(OperationKind Kind, RunResult Result, bool R public bool NeedsAttention => RecoveryRequired || Result == RunResult.Failed; public static OperationEnd Succeeded(OperationKind kind) => - new(kind, RunResult.Succeeded, false, "Finished.", "", null, ""); + new(kind, RunResult.Succeeded, false, L10n.T("OutcomeFinished", "Finished."), "", null, ""); public static OperationEnd From(OperationOutcome outcome) => new(outcome.Kind, outcome.Cancelled ? RunResult.Cancelled : RunResult.Failed, outcome.RecoveryRequired, outcome.Title, outcome.Message, outcome.WorkDirectory, outcome.Details, outcome.Warning); public static OperationEnd Unknown(OperationKind kind) => - new(kind, RunResult.Failed, false, "The operation did not complete.", - "It ended without saying how, so it is not shown as finished. Check the logs.", null, ""); + new(kind, RunResult.Failed, false, L10n.T("OutcomeUnknownTitle", "The operation did not complete."), + L10n.T("OutcomeUnknownMsg", "It ended without saying how, so it is not shown as finished. Check the logs."), null, ""); } // One call of RunOperation as seen by whoever may cancel it. A prompt is bound to the run it was opened for, so it can diff --git a/src/Tiny11Builder.App/Strings/zh-CN/Resources.resw b/src/Tiny11Builder.App/Strings/zh-CN/Resources.resw new file mode 100644 index 00000000..6b95c3c1 --- /dev/null +++ b/src/Tiny11Builder.App/Strings/zh-CN/Resources.resw @@ -0,0 +1,1670 @@ + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 构建 + + + 重新构建 + + + 再次构建 + + + 继续 + + + 对 {0} 的操作 + + + 开始 + + + 下一步 + + + 继续构建 + + + 检查 + + + 开始 + + + 请关闭并重新打开 Tiny11Builder.exe 并批准管理员权限,以恢复工作区。 + + + 需要管理员权限。 + + + 需要管理员权限。 + + + 请关闭并重新打开 Tiny11Builder.exe,然后批准 Windows 管理员提示。 + + + 全部阶段完成 + + + Tiny11 构建工具 + + + Tiny11 构建工具 + + + 返回 + + + 你的浏览器无法打开 {0}。尚未产生任何付款。 + + + 选择文件夹 + + + 选择 ISO + + + 选择输出 + + + 选择工作区 + + + 清除自定义选择 + + + 下载 ISO / 继续未完成的下载 + + + 检查映像 + + + 列出所有组件… + + + 加载配方 + + + 打开输出文件夹 + + + 打开日志与报告 + + + 保存配方 + + + 旧版安装程序因 {0} 而自动启用。 + + + 所选版本将被复制、修改、压缩并保存为可启动的 ISO。 + + + 构建详情 + + + 构建 + + + 构建模式 + + + 构建 + + + 构建中 + + + 缓存的 ISO 已更改。它未被接受。 + + + 你的源未被修改。工作文件与日志已保留。 + + + 构建已取消 + + + 没有任何更改。 + + + 已取消 + + + 部分下载已保留,可稍后继续。未修改任何源媒体。 + + + 下载已取消 + + + 应用仍以普通权限运行。 + + + 重启已取消 + + + 要停止,请使用右上角的“安全取消”。它会在下一个安全点停止。你的源不会被更改;工作文件、已保存进度与日志都会保留。 + + + 映像未被修改。 + + + 检查已取消 + + + 映像未被修改。 + + + 组件列表已取消 + + + 安全取消 + + + 再次运行“恢复工作区”以完成此作业的清理。 + + + 恢复已取消 + + + 已请求取消。{0}… + + + 已保存的进度与日志已保留。 + + + 恢复已取消 + + + 安全取消 + + + 已请求取消。正在等待当前操作完成… + + + 正在取消… + + + 正在检查你的选择… + + + 正在检查下载的 ISO… + + + 请关闭并重新打开 Tiny11 Builder。 + + + 快速 WIM + + + 快速 WIM(构建最快,ISO 更大) + + + 最大 WIM(便于日后维护) + + + 最大 WIM + + + 恢复 ESD(更小,压缩更慢) + + + 恢复 ESD + + + 关 + + + 开 + + + 使用压缩的系统文件安装 Windows,以减少磁盘占用。 + + + 紧凑安装 + + + 完成 + + + 组件列表已加载。 + + + 已保留现有选择。请勾选要移除或禁用的单个项。 + + + 组件列表以只读方式加载。 + + + 已安装的工具可以列举此映像,但构建它需要更新的 Windows ADK 部署工具。请在构建前安装并重新打开应用。 + + + 组件 + + + 选择 Tiny11、Tiny11 Core,或使用自定义自行选择要移除的内容。每次更改都会记录在构建日志中。 + + + 安装映像压缩 + + + 输出:{0} + +模式:{1} +移除组:{2} +单独操作:{3} + + + 创建此离线 Windows 映像? + + + 丢弃工作副本 + + + 继续 + + + 改为继续 + + + 已复制 + + + 无法复制 + + + Core 需要确认将失去更新、恢复与杀毒保护。 + + + 无 Windows 更新 +无 Microsoft Defender +无恢复环境 +无法添加语言或功能 + +仅在隔离的测试虚拟机中使用。 +映像可能无法启动。 + + + 我同意 + + + Core 不可维护 + + + 已接受 Core 的限制(本次构建)。 + + + 尚未接受 Core 的限制。 + + + 继续前会要求你接受 Core 的限制。 + + + 继续前会要求你接受 Core 的限制。 + + + 仅保留核心。用于测试虚拟机。 + + + Tiny11 Core + + + Core 删除组件文件与 WinRE,并禁用 Windows 更新和 Defender。之后无法添加更新、语言或功能。请在虚拟机中测试映像。 + + + Core 会移除更新与恢复支持。 + + + 当前操作 + + + 勾选的组将被移除或禁用,未勾选的组将保留。如需处理单个项,请使用下方的组件列表。 + + + 应用 Core 移除 + + + 保留服务、恢复与 Defender + + + 移除服务、恢复与 Defender + + + 选择单个移除项。 + + + 自定义 + + + 取消 + + + 丢弃此中断的工作区? + + + 仅选择来自中断 tiny11 作业的 job.json。这会丢弃该作业已挂载的工作副本,并移除其暂存媒体。其日志、配方、报告以及你的源 ISO 均会保留。映像已提交(有已保存进度)的作业不会在此丢弃,请使用“继续构建”。正在进行的构建无法恢复。 + + + 官方 Microsoft ISO。架构与语言自动选择。 + + + Microsoft 托管的 ISO 已下载。 + + + 或使用你已有的 ISO。 + + + 下载最新的 Windows 11 + + + 未提供发布哈希。显示的是计算得到的哈希,真实性尚未经过独立验证。 + + + Microsoft 直链 ISO + + + https://software.download.prss.microsoft.com/...iso?... + + + 官方 ISO 已下载并通过哈希校验。 + + + 文件与所提供的发布 SHA-256 一致。请检查它以选择版本。 + + + 已下载 {0}。SHA-256:{1} + + + 正在从 Microsoft 下载… + + + 重复的自定义映像操作。 + + + 要构建的版本 + + + 此版本不受支持。 + + + 请选择 Windows 11 客户端映像,22H2 或更新,x64 或 ARM64 格式。 + + + {0} 个组件已列出 · {1} 个高级操作已选。 + + + 自定义移除需要确认可能产生无法启动的映像。 + + + 此清单包含启动驱动、安全性、授权、系统应用与组件负载。移除它们可能导致 Windows 无法启动、不安全或无法更新。受保护包可能拒绝移除。直接移除负载不受支持。 + +仅用于隔离测试,并准备好备份与可用的安装介质。 + + + 列出组件 + + + 包含受保护组件 + + + 我接受自定义映像可能无法启动、无法维护或不安全的风险。 + + + 列出映像中的应用、包、功能、服务与组件文件,包括受保护项。移除服务会删除其注册表项,而非共享可执行文件。受保护包可能拒绝移除。 + + + 列出组件前请先选择一个版本。 + + + 请先检查映像。 + + + 单个移除项会添加到所选组中。要保留某个组件,请取消其所属组及单个项的勾选。 + + + 高级移除 + + + 移除受保护或启动关键项可能导致映像无法启动或不安全。直接移除负载会绕过服务机制。不提供依赖求解或启动保证。 + + + 不受支持的移除 + + + 构建失败 + + + 操作未完成 + + + 下载未完成 + + + 应用无法以管理员身份重启 + + + 无法检查映像 + + + 无法加载组件列表 + + + 恢复未完成 + + + 恢复失败 + + + {0} 失败。展开此阶段查看错误详情。 + + + ISO 映像 + + + 映像配方 + + + 构建后清理 + + + 保留 + + + 文件与工作区 + + + 高级 / 原版构建器 + + + 全部 + + + 捆绑应用 + + + 可选功能 + + + Windows 设置 + + + 正在查找最新的 Windows 11… + + + 无法打开文件夹。 + + + Windows 11 22H2 或更新版本 · x64 与 ARM64 · Insider / Canary + + + 在虚拟机中测试 Core 与自定义映像后再使用。 + + + (计算的哈希;Microsoft 未提供发布哈希) + + + (已验证发布哈希) + + + {0} 已取消 + + + {0} 已停止 + + + 展开某个阶段可查看每一步及错误详情。 + + + 操作历史 + + + 未选择映像 + + + 未选择映像 + + + 映像已检查。 + + + 找到 {0} 个版本。已选择 {1}。 + + + 请关闭并重新打开 Tiny11Builder.exe 并批准管理员权限,以检查其版本。 + + + 已选择映像。 + + + 请选择 Windows 11 客户端映像,版本为 22621(22H2)或更新,格式为 x64 或 ARM64。 + + + 请在列出组件之前选择一个版本。 + + + 请先检查映像。 + + + 正在检查映像… + + + 全部 + + + 功能包 + + + 组件负载 + + + 功能 + + + 包 + + + 预装应用 + + + 服务 + + + 系统应用 + + + 无效的自定义映像操作。 + + + 映像索引无效或存在重复的清理选项。 + + + 组件类型 + + + 搜索组件 + + + ISO 已创建 + + + {0} 已不存在。 + + + 作业文件夹已不存在。 + + + 此作业有已保存的进度 + + + 恢复会丢弃作业的工作副本,而它不会对有已保存进度的作业这样做。继续构建会从该进度继续,而非恢复。 + +未做任何更改。 + + + 关 + + + 开 + + + ISO 保存后保留修改后的安装媒体。占用更多磁盘空间。 + + + 保留工作文件 + + + 在 Ko-fi 上捐赠一次 + + + 语言已应用。重启应用后以新语言显示所有文本。 + + + 中文(简体) + + + English + + + 请关闭并重新打开 Tiny11 Builder。 + + + 跟随系统 + + + 语言 + + + 旧版安装程序:自动(因 {0} 需要) + + + Core 移除 + + + Core 与组件负载移除 + + + 组件负载移除 + + + 所选的移除项 + + + Microsoft ARM64 下载 ↗ + + + 官方 Insider / Canary ISO 选择 ↗ + + + Microsoft x64 下载 ↗ + + + 正在列出映像组件… + + + 关 + + + 开 + + + 在设置期间优先使用本地登录。部分较新版本仍可能要求 Microsoft 账户。 + + + 本地账户设置 + + + 默认 + + + 本地安装媒体 + + + 优先 + + + {0} 已不存在。 + + + 作业文件夹已消失。 + + + (总计) + + + {0} 分钟 + + + 已移除冗余数据:逻辑 {0} MiB · 已省略安装负载 {1} MiB + + + 继续构建 + + + 标准构建 + + + 详细的阶段耗时已随构建报告保存。 + + + Turbo 构建 + + + 移除更新与恢复支持,并禁用 Defender。 + + + Tiny11 Core + + + 仅用于测试虚拟机 + + + Tiny11 Core + + + 自定义 + + + 含 Core 移除的自定义 + + + 选择要移除的单个应用、组件与服务。 + + + 自定义 + + + 包含受保护项 + + + 自定义 + + + Tiny11 Core 移除项,包含不可逆的组件裁剪。选择“自定义”可更改单个移除项。 + + + 从空白开始,再选择要移除或禁用的项。你也可以列出单个组件,包括受保护项。 + + + 原版 Tiny11 移除项,包括 Edge/WebView2 与 OneDrive 二进制文件。保留 Windows 更新、Defender 与恢复。选择“自定义”可更改单个移除项。 + + + 禁用 + + + 构建模式:{0} + + + 移除 + + + 无法选择构建模式。 + + + 所选卡片未标识有效的构建模式。 + + + Tiny11 + + + Tiny11 Core + + + 使用原版 Tiny11 移除项,保留 Windows 更新与恢复。 + + + Tiny11 + + + 保留更新支持 + + + Tiny11 + + + 帮助项目长期维护。 + + + 每月支持 + + + 关 + + + 开 + + + 包含对旧版应用的支持。所需的源文件必须存在于 ISO 中。 + + + 包含 + + + 未包含 + + + .NET Framework 3.5 + + + 保留 + + + 省略 + + + 尚未进行任何更改。 + + + 复制详情 + + + 无法打开下载页。 + + + 打开 Microsoft 下载页 + + + 打开日志 + + + 获取映像工具 + + + 官方 Microsoft ISO · {0} · {1} + + + 保留 + + + 省略 + + + 将 sources\sxs 排除在 ISO 之外。你现在仍可包含 .NET 3.5;日后启用需要单独匹配的媒体或 Windows 更新。Core 日后无法添加。 + + + 省略 .NET 3.5 安装负载 + + + 一次性的感谢,无需承诺。 + + + 一次性捐赠 + + + 正在取消。请等待其停止后再关闭。 + + + 请选择 {0} 并等待映像操作完成后再关闭。 + + + 操作未完成。 + + + 操作仍在运行。 + + + 显示选项 + + + 其他官方下载 + + + 在 Microsoft 下载页选择语言,然后将 ISO 下载链接粘贴到这里。链接通常 24 小时后失效。 + + + 已完成。 + + + 该作业挂载的副本无法被丢弃。请使用“恢复工作区”并选择此作业的 job.json。 + + + 已保存的进度已保留,可再次恢复。 + + + 已保存的进度可继续。 + + + 它结束时未说明原因,因此不会显示为已完成。请查看日志。 + + + 操作未完成。 + + + 新的输出 ISO 文件 + + + 输出文件已存在。请选择新的文件名以继续。 + + + 输出 ISO + + + 总体进度 + + + 页面部分内容无法更新。 + + + ISO + + + 工作区 + + + Patreon + + + PayPal + + + 偏好其他服务? + + + 正在准备映像工具… + + + 已验证先前下载的 ISO。 + + + 检查组件 + + + 此清单包含启动驱动、安全、许可、系统应用与组件负载。移除它们可能导致 Windows 无法启动、不安全或无法更新。受保护的包可能拒绝移除。不支持直接负载移除。 + +仅在有备份与可靠安装介质的情况下,为隔离测试继续。 + + + 包含受保护的组件 + + + 已发布的 SHA-256(推荐) + + + 从 Microsoft 的“验证下载”部分复制 + + + 配方为空。 + + + 配方已加载。 + + + 请重新检查源。Core/专家风险确认需要重新进行。 + + + 配方尚未就绪。 + + + 配方已保存。 + + + 它包含选择项与本地路径,不包含已下载的媒体或凭据。 + + + 此配方版本或压缩模式不受支持。 + + + 恢复工作区 + + + 正在恢复工作区… + + + 保留 + + + 移除 + + + 移除下载的更新缓存、归档的服务日志、临时文件与崩溃报告。保留待处理的维护数据、驱动与安装程序。 + + + 保留 + + + 移除 + + + 移除冗余映像数据 + + + ISO 与构建报告已保存。使用前请在虚拟机中测试 ISO。 + + + ISO 已创建 + + + ISO 已带警告保存。请参阅下方跳过的操作与其他详情。 + + + 已完成,但存在警告 + + + 请关闭并重新打开 Tiny11Builder.exe 并批准管理员权限,然后选择“继续构建”。未做任何更改。 + + + 这不是有效的作业文件路径。 + + + 继续构建 + + + 压缩:{0} + + + Core:无 Windows 更新、无 Microsoft Defender、无恢复环境。仅在隔离的测试虚拟机中使用。 + + + 输出:{0} + + + 此作业已保存的配方缺失,因此无法继续。 + + + 模式:{0} · {1} 个移除组 · {2} 个单独操作 + + + 此构建没有可继续的已保存进度。它是在保存进度功能加入之前启动的,或在映像保存前就已停止,因此无法继续。没有任何更改,其日志与文件均已保留。 + + + 此构建无法继续。 + + + 未开始恢复。 + + + 恢复 + + + 请选择构建 tiny11-… 文件夹内的 job.json 文件。 + + + 单独移除项包含受保护项目,映像可能无法启动。 + + + 继续构建 + + + 继续此构建? + + + 原始构建的移除选项保持不变,恢复时无法更改。 + + + 此作业已保存的配方无法读取。 + + + 系统会先验证已保存的进度。如有任何不匹配,则不会进行任何更改。 + + + 继续中 + + + 紧凑安装:{0} + + + Core:不可逆的组件裁剪,已移除 WinRE,已禁用更新与 Defender。请在创建前而非创建后安装可选功能。 + + + CORE 移除 {0} + + + 自定义移除可能影响维护、恢复或安全性。 + + + 专家 {0} {1}: {2} + + + EXPERT:不解析依赖关系。受保护或启动关键项的移除可能导致映像无法启动。 + + + 工作文件:{0} + + + 检查 + + + Windows 映像 + + + 部分策略仅适用于特定版本或构建。功能更新可能重新安装应用。本地账户与硬件绕过为尽力而为。在依赖前请在虚拟机中测试生成的 ISO。 + + + 依赖于构建的设置 + + + 在构建前检查你的映像、移除集与输出。 + + + 本地账户设置:{0} + + + 模式 {0} + + + 构建模式 + + + .NET 3.5:{0} + + + .NET 安装负载:{0} + + + 无维护、恢复或 Defender。 + + + 未检查 + + + {0} 压缩 + + + 安装选项 + + + 输出与工作区 + + + 策略可能被特定 Windows 版本/版本忽略,或被更新更改。需要进行虚拟机启动验证。 + + + 所选移除项与完整配方 + + + 冗余数据:{0} + + + {0} 个移除组 · {1} 个单独操作 + + + 移除 / 禁用 + + + 保留 Windows 更新、恢复与 Defender。 + + + Turbo 模式:{0} + + + 未选中的项均予以保留。 + + + 应用 + + + 受保护 / 可能为启动关键 + + + {0} 个移除组已选 · {1} 个高级操作 + + + 已选映像 + + + 无法恢复会话。 + + + 会话已恢复。 + + + 请重新检查映像后再继续。不会自动开始构建。 + + + 构建设置 + + + 选择构建位置以及映像的压缩方式。 + + + SHA-256 + + + Windows 映像 + + + 从 Microsoft 下载最新的 Windows 11,或选择 ISO 或已解压的安装文件夹。检查映像以选择版本。 + + + 源文件不会被修改。 + + + Windows ISO 或安装媒体文件夹 + + + 选择 Windows 11 映像 + + + 在 GitHub Sponsors 上每月支持 + + + 已取消 · {0} 个完成{1} + + + 提交映像 + + + 压缩映像 + + + {0} 个完成{1} + + + 导出与挂载 + + + {0} 个失败 · {1} 个完成{2} + + + 个失败 + + + 失败: + + + 进行中 · {0} 个完成{1} + + + 创建 ISO + + + 阶段 {0}/{1} + + + 准备媒体 + + + 移除组件 + + + · {0} 个待运行 + + + · {0} 个未运行 + + + 验证输出 + + + 全部 {0} 个阶段已完成 + + + · 已取消 + + + {0} / {1} 个阶段已完成 + + + · {0} 个失败 + + + 已取消 + + + 已完成 + + + 完成 + + + 此前已完成 + + + 失败 + + + 进行中 + + + 下一步 + + + 未运行 + + + 已跳过 + + + 等待中 + + + 欢迎 + + + Windows 映像 + + + 移除组件 + + + 构建设置 + + + 检查 + + + 创建映像 + + + 步骤 + + + 支持开发 + + + 支持开发 + + + 关闭 + + + 支持 Tiny11 Builder + + + 如果 Tiny11 Builder 节省了你的时间,可以支持它的开发。 + + + 此支持链接没有有效的目标。 + + + 链接将在浏览器中打开。应用内不会扣费或订阅。 + + + 无法打开支持页面 + + + 无法打开支持选项。 + + + 帮助筹措开发时间、跨 Windows 版本的测试,以及修复已报告的问题。所有应用功能仍然免费。 + + + 在虚拟机中测试 ISO 后再安装 Windows。 + + + 外观 + + + 深色 + + + 浅色 + + + 系统 + + + 原版移除项。保留更新支持。 + + + Tiny11 + + + 映像与 ISO 工具会自动准备。 + + + 映像与 ISO 工具已就绪。 + + + 关 + + + 开 + + + 批量移除功能与功能包并减少日志开销。Core 与快速 WIM 构建使用更快的工作映像。占用更多 CPU 与临时空间;最终压缩方式不变。 + + + 关 + + + 开 + + + Turbo 模式 + + + 未知的清理选项:{0} + + + 上游项目 ↗ + + + 要更改 Tiny11 或 Tiny11 Core 的移除集,请使用自定义模式。 + + + 等待开始 + + + +• 构建日志中还有 {0} 条 + + + ISO 已带警告保存。请参阅页面上的警告。 + + + 创建自定义的 Windows 11 ISO。选择映像、决定要移除的内容,然后构建 ISO。 + + + 欢迎 + + + 工作区(NTFS,通常需 40–70 GiB 可用空间) + + + 工作区已恢复。 + + + 已丢弃中断的工作副本。你的源与日志均已保留。 + + + 抑制消费内容、设置建议、资源管理器推广与欢迎提示。 + + + 广告、推荐与推广应用 + + + 移除“相机”应用,不影响相机驱动。 + + + 相机 + + + 常规模式保留维护能力;Core 还会在不可逆的组件裁剪前重置更新基准。 + + + 组件清理 + + + 移除内置的视频编辑器。 + + + Clipchamp + + + 移除预装的 Copilot 应用,保留共享运行库与 shell 组件。 + + + Copilot 应用 + + + 应用策略以禁用 Copilot 与 Recall,支持程度因版本与版本而异。 + + + 禁用 Copilot 与 Recall + + + 移除 Dev Home 与 Power Automate Desktop。 + + + Dev Home 与 Power Automate + + + 阻止 BitLocker 自动设备加密;若你需要默认加密,请勿使用。 + + + 禁用自动设备加密 + + + 删除 Edge 与 WebView2 文件。依赖 WebView2 的应用可能失效,更新可能重新安装 Edge。 + + + 移除 Edge 与 WebView2 + + + 抑制首次运行、侧边栏与推荐,保留 Edge 与 WebView2 以保证应用兼容。 + + + Edge 首次运行与推广界面 + + + 移除反馈中心、获取帮助与入门应用。 + + + 反馈、提示与帮助 + + + 在 setup 与 Windows 中设置 CPU、内存、TPM、存储与 Secure Boot 绕过值,不受支持的硬件可能无法正常工作。 + + + 绕过安装硬件要求 + + + 移除各映像语言下的可选语言功能,保留基础语言支持。 + + + 语音、OCR 与手写负载 + + + 移除 WordPad、步骤记录器、数学输入、旧版媒体播放器及扩展壁纸(若已安装)。 + + + 已停用的附件与壁纸负载 + + + 在存在处禁用已停用的 Internet Explorer 可选功能。 + + + 旧版 Internet Explorer + + + 移除已停用的地图与混合现实客户端。 + + + 地图与混合现实 + + + 移除内置媒体播放应用,编解码器与共享框架保留。 + + + 媒体播放器与影片 + + + 移除打包的信息流应用,保留本地 Windows 搜索。 + + + 新闻、天气与必应应用 + + + 移除 Intel 管理状态与 Dolby 推广应用,不影响驱动。 + + + OEM 推广应用 + + + 移除 Office 中心与 Office 推送通知推广应用。 + + + Microsoft 365 推广 + + + 从离线映像中移除两套架构对应的 OneDrive 安装包。 + + + 移除 OneDrive 安装程序 + + + 通过策略禁用 OneDrive 同步;OneDrive 程序保留,仅在你不需要同步时使用。 + + + OneDrive 同步与备份提示 + + + 抑制隐私设置与登录后设置建议,不创建账户、不代为接受许可协议。 + + + 安装隐私与“完成设置”提示 + + + 移除 Outlook 与已停用的“邮件 / 日历”应用。 + + + 新版 Outlook 与邮件 + + + 移除画图、3D 查看器与 OneNote;取消勾选以保留。 + + + 画图、3D 查看器与 OneNote + + + 移除手机集成应用,跨设备功能可能失效。 + + + 手机连接与跨设备应用 + + + 保留必需的诊断数据,禁用广告 ID、个性化建议与输入采集。 + + + 可选诊断与个性化 + + + 移除微软的远程协助客户端。 + + + 快速助手 + + + 仅当本映像向 DISM 暴露 Recall 时,才禁用并移除其负载。 + + + Recall 可选功能 + + + 停止为更新预留磁盘空间,维护时可用的空间可能变少。 + + + 禁用保留存储 + + + 关闭网络建议、云搜索与动态搜索高亮,保留本地搜索。 + + + 网络搜索与搜索高亮 + + + 移除可选的日程与通讯应用。 + + + 人脉、待办、家庭与 Skype + + + 移除内置游戏。 + + + 纸牌游戏 + + + 移除个人版 Teams 客户端。 + + + Teams + + + 移除终端;PowerShell 与命令提示符保留。 + + + Windows 终端 + + + 移除可选的个人工具。 + + + 时钟、录音机与便笺 + + + 通过策略禁用小组件并隐藏其任务栏入口。 + + + 小组件与任务栏信息流 + + + 禁用企业工作文件夹同步并移除其负载。 + + + 工作文件夹 + + + 移除 Xbox 应用与游戏栏,游戏登录与录制可能不可用。 + + + Xbox 与游戏叠加层 + + + 禁用可选的 XPS 打印并移除其负载。 + + + XPS 打印 + + \ No newline at end of file diff --git a/src/Tiny11Builder.App/Tiny11Builder.App.csproj b/src/Tiny11Builder.App/Tiny11Builder.App.csproj index e37fcda4..f502fa23 100644 --- a/src/Tiny11Builder.App/Tiny11Builder.App.csproj +++ b/src/Tiny11Builder.App/Tiny11Builder.App.csproj @@ -5,6 +5,7 @@ 10.0.19041.0 Tiny11Builder 0.1.3 + en-US Tiny11Builder.App app.manifest x64;ARM64 diff --git a/src/Tiny11Builder.Core/CleanupCatalog.cs b/src/Tiny11Builder.Core/CleanupCatalog.cs index 2d5836c4..7ed7a567 100644 --- a/src/Tiny11Builder.Core/CleanupCatalog.cs +++ b/src/Tiny11Builder.Core/CleanupCatalog.cs @@ -39,14 +39,14 @@ public static class CleanupCatalog App("terminal", "Windows Terminal", "Remove Terminal. PowerShell and Command Prompt remain.", false, false, "Microsoft.WindowsTerminal"), App("oem", "OEM promotional apps", "Remove Intel management status and Dolby promotional applications, not drivers.", false, false, "AppUp.IntelManagementandSecurityStatus", "DolbyLaboratories.DolbyAccess", "DolbyLaboratories.DolbyDigitalPlusDecoderOEM"), - new("edge-files", "Remove Edge and WebView2", "Delete Edge and WebView2 files. Apps that require WebView2 may stop working. Updates may reinstall Edge.", + new("edge-files", L10n.T("edge-files.Title", "Remove Edge and WebView2"), L10n.T("edge-files.Description", "Delete Edge and WebView2 files. Apps that require WebView2 may stop working. Updates may reinstall Edge."), "Advanced / original builder", OptionKind.File, false, false, []), - new("onedrive-files", "Remove OneDrive setup binaries", "Remove both architecture-specific OneDrive installers from the offline image.", + new("onedrive-files", L10n.T("onedrive-files.Title", "Remove OneDrive setup binaries"), L10n.T("onedrive-files.Description", "Remove both architecture-specific OneDrive installers from the offline image."), "Advanced / original builder", OptionKind.File, false, false, []), - new("language-extras", "Speech, OCR and handwriting payloads", "Remove optional language capabilities in every image language; retain basic language support.", + new("language-extras", L10n.T("language-extras.Title", "Speech, OCR and handwriting payloads"), L10n.T("language-extras.Description", "Remove optional language capabilities in every image language; retain basic language support."), "Optional features", OptionKind.Capability, false, false, ["Language.Handwriting", "Language.OCR", "Language.Speech", "Language.TextToSpeech"]), - new("legacy-capabilities", "Retired accessories and wallpaper payloads", "Remove WordPad, Steps Recorder, Math Recognizer, legacy Media Player and extended wallpapers where installed.", + new("legacy-capabilities", L10n.T("legacy-capabilities.Title", "Retired accessories and wallpaper payloads"), L10n.T("legacy-capabilities.Description", "Remove WordPad, Steps Recorder, Math Recognizer, legacy Media Player and extended wallpapers where installed."), "Optional features", OptionKind.Capability, false, true, ["Microsoft.Windows.WordPad", "App.StepsRecorder", "MathRecognizer", "Media.WindowsMediaPlayer", "Microsoft.Windows.Wallpaper.Extended"]), Feature("xps", "XPS printing", "Disable optional XPS printing and remove its payload.", false, true, "Printing-XPSServices-Features"), @@ -69,11 +69,11 @@ public static class CleanupCatalog ]; private static CleanupOption App(string id, string title, string description, bool balanced, bool minimal, params string[] targets) - => new(id, title, description, "Bundled apps", OptionKind.App, balanced, minimal, targets); + => new(id, L10n.T(id + ".Title", title), L10n.T(id + ".Description", description), "Bundled apps", OptionKind.App, balanced, minimal, targets); private static CleanupOption Feature(string id, string title, string description, bool balanced, bool minimal, params string[] targets) - => new(id, title, description, "Optional features", OptionKind.Feature, balanced, minimal, targets); + => new(id, L10n.T(id + ".Title", title), L10n.T(id + ".Description", description), "Optional features", OptionKind.Feature, balanced, minimal, targets); private static CleanupOption Policy(string id, string title, string description, bool balanced, bool minimal) - => new(id, title, description, "Windows settings", OptionKind.Policy, balanced, minimal, []); + => new(id, L10n.T(id + ".Title", title), L10n.T(id + ".Description", description), "Windows settings", OptionKind.Policy, balanced, minimal, []); public static string[] ForPreset(CleanupPreset preset) => Options .Where(o => preset switch diff --git a/src/Tiny11Builder.Core/L10n.cs b/src/Tiny11Builder.Core/L10n.cs new file mode 100644 index 00000000..9b9710f0 --- /dev/null +++ b/src/Tiny11Builder.Core/L10n.cs @@ -0,0 +1,19 @@ +namespace Tiny11Builder.Core; + +/// +/// WinRT-free localization helper. The app supplies a provider that resolves a key through the +/// platform resource loader; when none is set (unit tests, or before startup) the English fallback +/// is returned so a string is never blank. Keeping this type free of Windows Runtime dependencies +/// lets the same source compile and run under the unit-test project. +/// +public static class L10n +{ + /// + /// Resolves to the active language, or when no + /// provider is registered or the key is not translated. Set once at app startup. + /// + public static Func? Provider; + + public static string T(string key, string english) => + Provider is null ? english : Provider(key, english) ?? english; +} diff --git a/src/Tiny11Builder.Core/Models.cs b/src/Tiny11Builder.Core/Models.cs index ada160ec..0b801b3a 100644 --- a/src/Tiny11Builder.Core/Models.cs +++ b/src/Tiny11Builder.Core/Models.cs @@ -33,7 +33,7 @@ public void Validate() { if (!IsSupported) throw new InvalidDataException( - "Choose a Windows 11 client image, build 22621 (22H2) or newer, in x64 or ARM64 format."); + L10n.T("ImageUnsupported", "Choose a Windows 11 client image, build 22621 (22H2) or newer, in x64 or ARM64 format.")); } } @@ -77,36 +77,36 @@ public void ValidateOptions() { if (SchemaVersion != 1 || !Enum.IsDefined(Compression) || !Enum.IsDefined(Flavor) || (Mode is { } mode && !Enum.IsDefined(mode))) - throw new InvalidDataException("This recipe version or compression mode is not supported."); + throw new InvalidDataException(L10n.T("RecipeUnsupported", "This recipe version or compression mode is not supported.")); if (Flavor == BuildFlavor.Core && !CoreAcknowledged) - throw new InvalidDataException("Core requires acknowledgement of the loss of updates, recovery and antivirus protection."); + throw new InvalidDataException(L10n.T("CoreAckRequired", "Core requires acknowledgement of the loss of updates, recovery and antivirus protection.")); if (ExpertActions is null || ExpertActions.Any(action => action is null) || (ExpertActions.Length > 0 && !ExpertAcknowledged)) - throw new InvalidDataException("Custom removals require acknowledgement of the risk of an unbootable image."); + throw new InvalidDataException(L10n.T("ExpertAckRequired", "Custom removals require acknowledgement of the risk of an unbootable image.")); if (ExpertActions.Select(a => (a.Kind, a.Identity)).Distinct().Count() != ExpertActions.Length) - throw new InvalidDataException("Duplicate custom image actions."); + throw new InvalidDataException(L10n.T("DuplicateActions", "Duplicate custom image actions.")); foreach (var action in ExpertActions) if (!Enum.IsDefined(action.Kind) || !Enum.IsDefined(action.Mode) || string.IsNullOrWhiteSpace(action.Identity) || action.Identity.Contains('\\') || action.Identity.Contains('/') || (action.Mode == ExpertActionMode.Disable && action.Kind is not (ImageItemKind.Feature or ImageItemKind.Service))) - throw new InvalidDataException("Invalid custom image action."); + throw new InvalidDataException(L10n.T("InvalidAction", "Invalid custom image action.")); if (ImageIndex < 1 || SelectedOptions is null || SelectedOptions.Length != SelectedOptions.Distinct(StringComparer.Ordinal).Count()) - throw new InvalidDataException("Invalid image index or duplicate cleanup options."); + throw new InvalidDataException(L10n.T("InvalidOptions", "Invalid image index or duplicate cleanup options.")); foreach (var id in SelectedOptions) if (!CleanupCatalog.Options.Any(o => o.Id == id)) - throw new InvalidDataException($"Unknown cleanup option: {id}"); + throw new InvalidDataException(string.Format(L10n.T("UnknownOption", "Unknown cleanup option: {0}"), id)); if (Mode is BuildMode.Tiny11 or BuildMode.Tiny11Core && (Flavor != (Mode == BuildMode.Tiny11Core ? BuildFlavor.Core : BuildFlavor.Regular) || ExpertActions.Length > 0 || !SelectedOptions.ToHashSet(StringComparer.Ordinal).SetEquals(CleanupCatalog.ForMode(Mode.Value)))) - throw new InvalidDataException("Use Custom for changes to the Tiny11 or Tiny11 Core removal sets."); + throw new InvalidDataException(L10n.T("UseCustomMode", "Use Custom for changes to the Tiny11 or Tiny11 Core removal sets.")); } public static BuildRecipe Read(string path) { var recipe = JsonSerializer.Deserialize(File.ReadAllText(path), JsonDefaults.Options) - ?? throw new InvalidDataException("The recipe is empty."); + ?? throw new InvalidDataException(L10n.T("RecipeEmpty", "The recipe is empty.")); recipe.ValidateOptions(); return recipe; } diff --git a/tools/gen_resw.py b/tools/gen_resw.py new file mode 100644 index 00000000..429d3df4 --- /dev/null +++ b/tools/gen_resw.py @@ -0,0 +1,672 @@ +#!/usr/bin/env python3 +"""Generate zh-CN/Resources.resw for Tiny11 Builder localization. + +English text is kept as the XAML/code fallback, so this file only needs the +translated values. Each key is the resource name used by x:Uid (e.g. +"Foo.Text") or by L10n.T (a plain key). Run: python tools/gen_resw.py +""" +import xml.etree.ElementTree as ET +from xml.dom import minidom + +# resw entry name -> Simplified Chinese translation +ZH = { + # ---- Sidebar ---- + "AppWindow.Title": "Tiny11 构建工具", + "AppTitle.Text": "Tiny11 构建工具", + "StepsHeader.Text": "步骤", + "Step0.Text": "欢迎", + "Step1.Text": "Windows 映像", + "Step2.Text": "移除组件", + "Step3.Text": "构建设置", + "Step4.Text": "检查", + "Step5.Text": "创建映像", + "SelectedImageHeader.Text": "已选映像", + "ImageBadge.Text": "未选择映像", + "SourceNote.Text": "源文件不会被修改。", + "ResumeButton.Content": "继续构建", + "RecoverButton.Content": "恢复工作区", + "ThemeChoice.Header": "外观", + "ThemeSystem.Content": "系统", + "ThemeLight.Content": "浅色", + "ThemeDark.Content": "深色", + "SupportButton.Content": "支持开发", + "UpstreamLink.Content": "上游项目 ↗", + "BackLabel.Text": "返回", + "CancelLabel.Text": "安全取消", + "LanguageChoice.Header": "语言", + "LangSystem.Content": "跟随系统", + "LangEnglish.Content": "English", + "LangChinese.Content": "中文(简体)", + + # ---- Welcome ---- + "WelcomeTitle.Text": "欢迎", + "WelcomeSubtitle.Text": "创建自定义的 Windows 11 ISO。选择映像、决定要移除的内容,然后构建 ISO。", + "BuildModesLabel.Text": "构建模式", + "ModeTinyTitle.Text": "Tiny11", + "ModeTinyDesc.Text": "使用原版 Tiny11 移除项,保留 Windows 更新与恢复。", + "ModeTinyTag.Text": "保留更新支持", + "ModeCoreTitle.Text": "Tiny11 Core", + "ModeCoreDesc.Text": "移除更新与恢复支持,并禁用 Defender。", + "ModeCoreTag.Text": "仅用于测试虚拟机", + "ModeCustomTitle.Text": "自定义", + "ModeCustomDesc.Text": "选择要移除的单个应用、组件与服务。", + "ModeCustomTag.Text": "包含受保护项", + "Footer1.Text": "Windows 11 22H2 或更新版本 · x64 与 ARM64 · Insider / Canary", + "Footer2.Text": "在虚拟机中测试 Core 与自定义映像后再使用。", + + # ---- Source ---- + "SourceHeading.Text": "Windows 映像", + "SourceIntro.Text": "从 Microsoft 下载最新的 Windows 11,或选择 ISO 或已解压的安装文件夹。检查映像以选择版本。", + "LocalMediaTitle.Text": "本地安装媒体", + "SourcePathBox.Header": "Windows ISO 或安装媒体文件夹", + "SourcePathBox.PlaceholderText": "选择 Windows 11 映像", + "BtnChooseIso.Content": "选择 ISO", + "BtnChooseFolder.Content": "选择文件夹", + "BtnInspect.Content": "检查映像", + "EditionChoice.Header": "要构建的版本", + "DownloadTitle.Text": "下载最新的 Windows 11", + "DownloadDesc.Text": "官方 Microsoft ISO。架构与语言自动选择。", + "DownloadStatus.Text": "或使用你已有的 ISO。", + "OtherDownloads.Header": "其他官方下载", + "OtherDownloadsDesc.Text": "在 Microsoft 下载页选择语言,然后将 ISO 下载链接粘贴到这里。链接通常 24 小时后失效。", + "LinkX64.Content": "Microsoft x64 下载 ↗", + "LinkARM64.Content": "Microsoft ARM64 下载 ↗", + "LinkInsider.Content": "官方 Insider / Canary ISO 选择 ↗", + "DownloadUrlBox.Header": "Microsoft 直链 ISO", + "DownloadUrlBox.PlaceholderText": "https://software.download.prss.microsoft.com/...iso?...", + "PublishedHashBox.Header": "已发布的 SHA-256(推荐)", + "PublishedHashBox.PlaceholderText": "从 Microsoft 的“验证下载”部分复制", + "BtnDownload.Content": "下载 ISO / 继续未完成的下载", + + # ---- Components ---- + "ComponentsHeading.Text": "组件", + "ComponentsIntro.Text": "选择 Tiny11、Tiny11 Core,或使用自定义自行选择要移除的内容。每次更改都会记录在构建日志中。", + "ModeTinyItem.Content": "Tiny11", + "ModeCoreItem.Content": "Tiny11 Core", + "ModeCustomItem.Content": "自定义", + "Tiny11SelTitle.Text": "Tiny11", + "Tiny11SelDesc.Text": "原版移除项。保留更新支持。", + "CoreSelTitle.Text": "Tiny11 Core", + "CoreSelDesc.Text": "仅保留核心。用于测试虚拟机。", + "CustomSelTitle.Text": "自定义", + "CustomSelDesc.Text": "选择单个移除项。", + "CoreWarning.Title": "Core 会移除更新与恢复支持。", + "CoreWarning.Message": "Core 删除组件文件与 WinRE,并禁用 Windows 更新和 Defender。之后无法添加更新、语言或功能。请在虚拟机中测试映像。", + "CoreConsentStatus.Text": "继续前会要求你接受 Core 的限制。", + "CustomCoreCheck.Header": "应用 Core 移除", + "CustomCoreCheck.OnContent": "移除服务、恢复与 Defender", + "CustomCoreCheck.OffContent": "保留服务、恢复与 Defender", + "OptionFilter.Header": "显示选项", + "FilterAll.Content": "全部", + "FilterApps.Content": "捆绑应用", + "FilterFeatures.Content": "可选功能", + "FilterSettings.Content": "Windows 设置", + "FilterAdvanced.Content": "高级 / 原版构建器", + "ExpertTitle.Text": "高级移除", + "ExpertIntro.Text": "列出映像中的应用、包、功能、服务与组件文件,包括受保护项。移除服务会删除其注册表项,而非共享可执行文件。受保护包可能拒绝移除。", + "BtnListComponents.Content": "列出所有组件…", + "ExpertWarning.Title": "不受支持的移除", + "ExpertWarning.Message": "移除受保护或启动关键项可能导致映像无法启动或不安全。直接移除负载会绕过服务机制。不提供依赖求解或启动保证。", + "ExpertConsent.Content": "我接受自定义映像可能无法启动、无法维护或不安全的风险。", + "InventorySearch.PlaceholderText": "搜索组件", + "InventoryFilter.Header": "组件类型", + "InvAll.Content": "全部", + "InvProvisionedApp.Content": "预装应用", + "InvSystemApp.Content": "系统应用", + "InvFeature.Content": "功能", + "InvCapability.Content": "功能包", + "InvPackage.Content": "包", + "InvService.Content": "服务", + "InvComponentPayload.Content": "组件负载", + "ExpertNote2.Text": "单个移除项会添加到所选组中。要保留某个组件,请取消其所属组及单个项的勾选。", + "BtnClearExpert.Content": "清除自定义选择", + + # ---- Settings ---- + "SettingsHeading.Text": "构建设置", + "SettingsIntro.Text": "选择构建位置以及映像的压缩方式。", + "FilesWorkspace.Text": "文件与工作区", + "WorkspaceBox.Header": "工作区(NTFS,通常需 40–70 GiB 可用空间)", + "BtnChooseWorkspace.Content": "选择工作区", + "OutputBox.Header": "新的输出 ISO 文件", + "BtnChooseOutput.Content": "选择输出", + "CompressionChoice.Header": "安装映像压缩", + "CompRecover.Content": "恢复 ESD(更小,压缩更慢)", + "CompMax.Content": "最大 WIM(便于日后维护)", + "CompFast.Content": "快速 WIM(构建最快,ISO 更大)", + "TurboTitle.Text": "Turbo 模式", + "TurboDesc.Text": "批量移除功能与功能包并减少日志开销。Core 与快速 WIM 构建使用更快的工作映像。占用更多 CPU 与临时空间;最终压缩方式不变。", + "TurboCheck.OnContent": "开", + "TurboCheck.OffContent": "关", + "RedundantTitle.Text": "移除冗余映像数据", + "RedundantDesc.Text": "移除下载的更新缓存、归档的服务日志、临时文件与崩溃报告。保留待处理的维护数据、驱动与安装程序。", + "RedundantCheck.OnContent": "移除", + "RedundantCheck.OffContent": "保留", + "OmitSetupTitle.Text": "省略 .NET 3.5 安装负载", + "OmitSetupDesc.Text": "将 sources\\sxs 排除在 ISO 之外。你现在仍可包含 .NET 3.5;日后启用需要单独匹配的媒体或 Windows 更新。Core 日后无法添加。", + "OmitSetupCheck.OnContent": "省略", + "OmitSetupCheck.OffContent": "保留", + "CompactTitle.Text": "紧凑安装", + "CompactDesc.Text": "使用压缩的系统文件安装 Windows,以减少磁盘占用。", + "CompactCheck.OnContent": "开", + "CompactCheck.OffContent": "关", + "NetFxTitle.Text": ".NET Framework 3.5", + "NetFxDesc.Text": "包含对旧版应用的支持。所需的源文件必须存在于 ISO 中。", + "NetFxCheck.OnContent": "开", + "NetFxCheck.OffContent": "关", + "LocalAccountTitle.Text": "本地账户设置", + "LocalAccountDesc.Text": "在设置期间优先使用本地登录。部分较新版本仍可能要求 Microsoft 账户。", + "LocalAccountCheck.OnContent": "开", + "LocalAccountCheck.OffContent": "关", + "KeepFilesTitle.Text": "保留工作文件", + "KeepFilesDesc.Text": "ISO 保存后保留修改后的安装媒体。占用更多磁盘空间。", + "KeepFilesCheck.OnContent": "开", + "KeepFilesCheck.OffContent": "关", + "ToolStatus.Text": "映像与 ISO 工具会自动准备。", + + # ---- Review ---- + "ReviewHeading.Text": "检查", + "ReviewIntro.Text": "在构建前检查你的映像、移除集与输出。", + "ReviewImageTitle.Text": "Windows 映像", + "ReviewPathsTitle.Text": "输出与工作区", + "ReviewModeTitle.Text": "构建模式", + "ReviewOptionsTitle.Text": "安装选项", + "ReviewRecipe.Header": "所选移除项与完整配方", + "ReviewInfo.Title": "依赖于构建的设置", + "ReviewInfo.Message": "部分策略仅适用于特定版本或构建。功能更新可能重新安装应用。本地账户与硬件绕过为尽力而为。在依赖前请在虚拟机中测试生成的 ISO。", + "BtnSaveRecipe.Content": "保存配方", + "BtnLoadRecipe.Content": "加载配方", + + # ---- Build ---- + "OverallLabel.Text": "总体进度", + "CurrentOpLabel.Text": "当前操作", + "CancelHint.Text": "要停止,请使用右上角的“安全取消”。它会在下一个安全点停止。你的源不会被更改;工作文件、已保存进度与日志都会保留。", + "ResultTitle.Text": "ISO 已创建", + "ResultWarnings.Title": "已完成,但存在警告", + "OutputIsoHeader.Header": "输出 ISO", + "ShaHeader.Header": "SHA-256", + "BuildHeading.Text": "构建", + "BtnOpenFolder.Content": "打开输出文件夹", + "BtnOpenLogs.Content": "打开日志与报告", + "TestVmHint.Text": "在虚拟机中测试 ISO 后再安装 Windows。", + "SupportHint.Text": "如果 Tiny11 Builder 节省了你的时间,可以支持它的开发。", + "SupportDev2.Content": "支持开发", + "HistoryTitle.Text": "操作历史", + "HistoryHint.Text": "展开某个阶段可查看每一步及错误详情。", + "BuildDetails.Header": "构建详情", + "ResumeRunButton.Content": "继续构建", + "ActionLabel.Text": "开始", + + # ---- Support dialog ---- + "SupportDialog.Title": "支持 Tiny11 Builder", + "SupportDialog.CloseButtonText": "关闭", + "MonthlyTitle.Text": "每月支持", + "MonthlyDesc.Text": "帮助项目长期维护。", + "SponsorsLink.Content": "在 GitHub Sponsors 上每月支持", + "OneTimeTitle.Text": "一次性捐赠", + "OneTimeDesc.Text": "一次性的感谢,无需承诺。", + "KofiLink.Content": "在 Ko-fi 上捐赠一次", + "PreferOther.Text": "偏好其他服务?", + "PatreonLink.Content": "Patreon", + "PayPalLink.Content": "PayPal", + "SupportLinksNote.Text": "链接将在浏览器中打开。应用内不会扣费或订阅。", + + # ---- Cleanup catalog options (id.Title / id.Description) ---- + "copilot-app.Title": "Copilot 应用", + "copilot-app.Description": "移除预装的 Copilot 应用,保留共享运行库与 shell 组件。", + "outlook.Title": "新版 Outlook 与邮件", + "outlook.Description": "移除 Outlook 与已停用的“邮件 / 日历”应用。", + "teams.Title": "Teams", + "teams.Description": "移除个人版 Teams 客户端。", + "xbox.Title": "Xbox 与游戏叠加层", + "xbox.Description": "移除 Xbox 应用与游戏栏,游戏登录与录制可能不可用。", + "news-weather.Title": "新闻、天气与必应应用", + "news-weather.Description": "移除打包的信息流应用,保留本地 Windows 搜索。", + "office-promos.Title": "Microsoft 365 推广", + "office-promos.Description": "移除 Office 中心与 Office 推送通知推广应用。", + "clipchamp.Title": "Clipchamp", + "clipchamp.Description": "移除内置的视频编辑器。", + "solitaire.Title": "纸牌游戏", + "solitaire.Description": "移除内置游戏。", + "phone.Title": "手机连接与跨设备应用", + "phone.Description": "移除手机集成应用,跨设备功能可能失效。", + "feedback.Title": "反馈、提示与帮助", + "feedback.Description": "移除反馈中心、获取帮助与入门应用。", + "devhome.Title": "Dev Home 与 Power Automate", + "devhome.Description": "移除 Dev Home 与 Power Automate Desktop。", + "social.Title": "人脉、待办、家庭与 Skype", + "social.Description": "移除可选的日程与通讯应用。", + "maps.Title": "地图与混合现实", + "maps.Description": "移除已停用的地图与混合现实客户端。", + "media.Title": "媒体播放器与影片", + "media.Description": "移除内置媒体播放应用,编解码器与共享框架保留。", + "utilities.Title": "时钟、录音机与便笺", + "utilities.Description": "移除可选的个人工具。", + "quickassist.Title": "快速助手", + "quickassist.Description": "移除微软的远程协助客户端。", + "paint.Title": "画图、3D 查看器与 OneNote", + "paint.Description": "移除画图、3D 查看器与 OneNote;取消勾选以保留。", + "camera.Title": "相机", + "camera.Description": "移除“相机”应用,不影响相机驱动。", + "terminal.Title": "Windows 终端", + "terminal.Description": "移除终端;PowerShell 与命令提示符保留。", + "oem.Title": "OEM 推广应用", + "oem.Description": "移除 Intel 管理状态与 Dolby 推广应用,不影响驱动。", + "edge-files.Title": "移除 Edge 与 WebView2", + "edge-files.Description": "删除 Edge 与 WebView2 文件。依赖 WebView2 的应用可能失效,更新可能重新安装 Edge。", + "onedrive-files.Title": "移除 OneDrive 安装程序", + "onedrive-files.Description": "从离线映像中移除两套架构对应的 OneDrive 安装包。", + "language-extras.Title": "语音、OCR 与手写负载", + "language-extras.Description": "移除各映像语言下的可选语言功能,保留基础语言支持。", + "legacy-capabilities.Title": "已停用的附件与壁纸负载", + "legacy-capabilities.Description": "移除 WordPad、步骤记录器、数学输入、旧版媒体播放器及扩展壁纸(若已安装)。", + "xps.Title": "XPS 打印", + "xps.Description": "禁用可选的 XPS 打印并移除其负载。", + "workfolders.Title": "工作文件夹", + "workfolders.Description": "禁用企业工作文件夹同步并移除其负载。", + "legacy.Title": "旧版 Internet Explorer", + "legacy.Description": "在存在处禁用已停用的 Internet Explorer 可选功能。", + "recall-feature.Title": "Recall 可选功能", + "recall-feature.Description": "仅当本映像向 DISM 暴露 Recall 时,才禁用并移除其负载。", + "ads.Title": "广告、推荐与推广应用", + "ads.Description": "抑制消费内容、设置建议、资源管理器推广与欢迎提示。", + "privacy.Title": "可选诊断与个性化", + "privacy.Description": "保留必需的诊断数据,禁用广告 ID、个性化建议与输入采集。", + "copilot.Title": "禁用 Copilot 与 Recall", + "copilot.Description": "应用策略以禁用 Copilot 与 Recall,支持程度因版本与版本而异。", + "widgets.Title": "小组件与任务栏信息流", + "widgets.Description": "通过策略禁用小组件并隐藏其任务栏入口。", + "search.Title": "网络搜索与搜索高亮", + "search.Description": "关闭网络建议、云搜索与动态搜索高亮,保留本地搜索。", + "onedrive.Title": "OneDrive 同步与备份提示", + "onedrive.Description": "通过策略禁用 OneDrive 同步;OneDrive 程序保留,仅在你不需要同步时使用。", + "edge.Title": "Edge 首次运行与推广界面", + "edge.Description": "抑制首次运行、侧边栏与推荐,保留 Edge 与 WebView2 以保证应用兼容。", + "oobe.Title": "安装隐私与“完成设置”提示", + "oobe.Description": "抑制隐私设置与登录后设置建议,不创建账户、不代为接受许可协议。", + "cleanup.Title": "组件清理", + "cleanup.Description": "常规模式保留维护能力;Core 还会在不可逆的组件裁剪前重置更新基准。", + "hardware-bypass.Title": "绕过安装硬件要求", + "hardware-bypass.Description": "在 setup 与 Windows 中设置 CPU、内存、TPM、存储与 Secure Boot 绕过值,不受支持的硬件可能无法正常工作。", + "reserved-storage.Title": "禁用保留存储", + "reserved-storage.Description": "停止为更新预留磁盘空间,维护时可用的空间可能变少。", + "device-encryption.Title": "禁用自动设备加密", + "device-encryption.Description": "阻止 BitLocker 自动设备加密;若你需要默认加密,请勿使用。", + + # ================= C# strings (L10n.T keys) ================= + # Choices.cs + "StagePrepare": "准备媒体", + "StageExport": "导出与挂载", + "StageRemove": "移除组件", + "StageCommit": "提交映像", + "StageCompress": "压缩映像", + "StageIso": "创建 ISO", + "StageVerify": "验证输出", + "StatusWaiting": "等待中", + "StatusInProgress": "进行中", + "StatusDone": "完成", + "StatusSkipped": "已跳过", + "StatusFailed": "失败", + "StatusCancelled": "已取消", + "StatusNotRun": "未运行", + "StatusNext": "下一步", + "StatusComplete": "已完成", + "StatusDoneEarlier": "此前已完成", + "ModeDisable": "禁用", + "ModeRemove": "移除", + "RiskProtected": "受保护 / 可能为启动关键", + "RiskApp": "应用", + "ActionFor": "对 {0} 的操作", + + # Models.cs validation + "ImageUnsupported": "请选择 Windows 11 客户端映像,版本为 22621(22H2)或更新,格式为 x64 或 ARM64。", + "RecipeUnsupported": "此配方版本或压缩模式不受支持。", + "CoreAckRequired": "Core 需要确认将失去更新、恢复与杀毒保护。", + "ExpertAckRequired": "自定义移除需要确认可能产生无法启动的映像。", + "DuplicateActions": "重复的自定义映像操作。", + "InvalidAction": "无效的自定义映像操作。", + "InvalidOptions": "映像索引无效或存在重复的清理选项。", + "UnknownOption": "未知的清理选项:{0}", + "UseCustomMode": "要更改 Tiny11 或 Tiny11 Core 的移除集,请使用自定义模式。", + "RecipeEmpty": "配方为空。", + + # ProjectSupport.cs + "SupportPurpose": "帮助筹措开发时间、跨 Windows 版本的测试,以及修复已报告的问题。所有应用功能仍然免费。", + + # Choices.cs stage status summaries + "StageRestNotRun": " · {0} 个未运行", + "StageRestNext": " · {0} 个待运行", + "StageFailed": "{0} 个失败 · {1} 个完成{2}", + "StageFailedOne": "失败:", + "StageFailedCount": " 个失败", + "StageCancelled": "已取消 · {0} 个完成{1}", + "StageInProgress": "进行中 · {0} 个完成{1}", + "StageDone": "{0} 个完成{1}", + + # StageSummary.Describe + "StagesAllDone": "全部 {0} 个阶段已完成", + "StagesDone": "{0} / {1} 个阶段已完成", + "StagesFailed": " · {0} 个失败", + "StagesCancelled": " · 已取消", + + # Cancel + "CancelSafely": "安全取消", + + # Operation outcomes (cancelled) + "CancelBuildTitle": "构建已取消", + "CancelBuildMsg": "你的源未被修改。工作文件与日志已保留。", + "CancelResumeTitle": "恢复已取消", + "CancelResumeMsg": "已保存的进度与日志已保留。", + "CancelInspectTitle": "检查已取消", + "CancelInspectMsg": "映像未被修改。", + "CancelInventoryTitle": "组件列表已取消", + "CancelInventoryMsg": "映像未被修改。", + "CancelDownloadTitle": "下载已取消", + "CancelDownloadMsg": "部分下载已保留,可稍后继续。未修改任何源媒体。", + "CancelRecoverTitle": "恢复已取消", + "CancelRecoverMsg": "再次运行“恢复工作区”以完成此作业的清理。", + "CancelElevateTitle": "重启已取消", + "CancelElevateMsg": "应用仍以普通权限运行。", + "CancelDefaultTitle": "已取消", + "CancelDefaultMsg": "没有任何更改。", + + # Operation outcomes (failed) — reason text stays in English (technical error detail) + "FailBuildTitle": "构建失败", + "FailResumeTitle": "恢复失败", + "FailInspectTitle": "无法检查映像", + "FailInventoryTitle": "无法加载组件列表", + "FailDownloadTitle": "下载未完成", + "FailRecoverTitle": "恢复未完成", + "FailElevateTitle": "应用无法以管理员身份重启", + "FailDefaultTitle": "操作未完成", + "OutcomeResumeSuffix": "已保存的进度可继续。", + "OutcomeResumeAgain": "已保存的进度已保留,可再次恢复。", + "OutcomeRecoverySuffix": "该作业挂载的副本无法被丢弃。请使用“恢复工作区”并选择此作业的 job.json。", + "OutcomeFinished": "已完成。", + "OutcomeUnknownTitle": "操作未完成。", + "OutcomeUnknownMsg": "它结束时未说明原因,因此不会显示为已完成。请查看日志。", + + # ResumeSupport + "ResumeNoCheckpoint": "此构建没有可继续的已保存进度。它是在保存进度功能加入之前启动的,或在映像保存前就已停止,因此无法继续。没有任何更改,其日志与文件均已保留。", + "ResumeBadPath": "这不是有效的作业文件路径。", + "ResumePickJob": "请选择构建 tiny11-… 文件夹内的 job.json 文件。", + "ResumeMissingRecipe": "此作业已保存的配方缺失,因此无法继续。", + "ResumeUnreadableRecipe": "此作业已保存的配方无法读取。", + "ResumeVerifyFirst": "系统会先验证已保存的进度。如有任何不匹配,则不会进行任何更改。", + "ResumeHeader": "输出:{0}", + "ResumeModeLine": "模式:{0} · {1} 个移除组 · {2} 个单独操作", + "ResumeCompressionLine": "压缩:{0}", + "ResumeUnchangeable": "原始构建的移除选项保持不变,恢复时无法更改。", + "ResumeCoreNote": "Core:无 Windows 更新、无 Microsoft Defender、无恢复环境。仅在隔离的测试虚拟机中使用。", + "ResumeProtectedNote": "单独移除项包含受保护项目,映像可能无法启动。", + + # ResumeSupport mode / compression labels + "ModeTiny11": "Tiny11", + "ModeTiny11Core": "Tiny11 Core", + "ModeCustomCore": "含 Core 移除的自定义", + "ModeCustom": "自定义", + "CompRecovery": "恢复 ESD", + "CompMaximum": "最大 WIM", + "CompFast": "快速 WIM", + + # SetupLabels (legacy installer note) + "LegacyInstaller": "旧版安装程序:自动(因 {0} 需要)", + "LegacyReasonCorePayload": "Core 与组件负载移除", + "LegacyReasonCore": "Core 移除", + "LegacyReasonPayload": "组件负载移除", + "LegacyReasonSelected": "所选的移除项", + + # ================= MainWindow.xaml.cs dynamic strings ================= + # Navigation / action button + "ActionBuildAnother": "再次构建", + "ActionResumeAgain": "继续构建", + "ActionBuildAgain": "重新构建", + "ActionStart": "开始", + "ActionNext": "下一步", + "ActionReview": "检查", + "ActionContinue": "继续", + "ActionBuild": "构建", + + # Core consent status + "CoreConsentAccepted": "已接受 Core 的限制(本次构建)。", + "CoreConsentNotAccepted": "尚未接受 Core 的限制。", + "CoreConsentPending": "继续前会要求你接受 Core 的限制。", + + # Core confirm dialog + "CoreConfirmTitle": "Core 不可维护", + "CoreConfirmBody": "无 Windows 更新\n无 Microsoft Defender\n无恢复环境\n无法添加语言或功能\n\n仅在隔离的测试虚拟机中使用。\n映像可能无法启动。", + "CoreConfirmButton": "我同意", + + # ShowStopped heading + "ResumeNoun": "恢复", + "BuildNoun": "构建", + "HeadingCancelled": "{0} 已取消", + "HeadingStopped": "{0} 已停止", + + # Notice action labels + "NoticeOpenLogs": "打开日志", + "NoticeOpenTools": "获取映像工具", + "NoticeOpenDownload": "打开 Microsoft 下载页", + "NoticeCopyDetails": "复制详情", + "NoticeDownloadPageFailTitle": "无法打开下载页。", + "CopiedLabel": "已复制", + "CopyFailLabel": "无法复制", + + # Generic dialogs / errors + "DialogCancel": "取消", + "SupportOpenFailTitle": "无法打开支持页面", + "SupportOptionsFailTitle": "无法打开支持选项。", + "BrowserOpenFailMsg": "你的浏览器无法打开 {0}。尚未产生任何付款。", + "SupportLinkInvalid": "此支持链接没有有效的目标。", + "RecipeNotReadyTitle": "配方尚未就绪。", + + # Image badge + "ImageBadgeNone": "未选择映像", + + # Tool status + "ToolsReady": "映像与 ISO 工具已就绪。", + + # Mode descriptions + "ModeDescTiny": "原版 Tiny11 移除项,包括 Edge/WebView2 与 OneDrive 二进制文件。保留 Windows 更新、Defender 与恢复。选择“自定义”可更改单个移除项。", + "ModeDescCore": "Tiny11 Core 移除项,包含不可逆的组件裁剪。选择“自定义”可更改单个移除项。", + "ModeDescCustom": "从空白开始,再选择要移除或禁用的项。你也可以列出单个组件,包括受保护项。", + "ModeLine": "构建模式:{0}", + + # Counts + "SelCountFmt": "{0} 个移除组已选 · {1} 个高级操作", + "ExpCountFmt": "{0} 个组件已列出 · {1} 个高级操作已选。", + + # Review screen + "ReviewNotInspected": "未检查", + "ReviewRemoveHeader": "移除 / 禁用", + "ReviewUnselectedKept": "未选中的项均予以保留。", + "ReviewCoreNote": "Core:不可逆的组件裁剪,已移除 WinRE,已禁用更新与 Defender。请在创建前而非创建后安装可选功能。", + "ReviewExpertNote": "EXPERT:不解析依赖关系。受保护或启动关键项的移除可能导致映像无法启动。", + "ReviewPoliciesNote": "策略可能被特定 Windows 版本/版本忽略,或被更新更改。需要进行虚拟机启动验证。", + "ReviewRemovalsFmt": "{0} 个移除组 · {1} 个单独操作", + "ReviewNoService": "无维护、恢复或 Defender。", + "ReviewCustomWarn": "自定义移除可能影响维护、恢复或安全性。", + "ReviewRetained": "保留 Windows 更新、恢复与 Defender。", + "PathIso": "ISO", + "PathWorkspace": "工作区", + + # Review options lines + "ReviewOptionsCompFmt": "{0} 压缩", + "ReviewTurboLine": "Turbo 模式:{0}", + "ReviewRedundantLine": "冗余数据:{0}", + "ReviewNetSetupLine": ".NET 安装负载:{0}", + "ReviewCompactLine": "紧凑安装:{0}", + "ReviewNetFxLine": ".NET 3.5:{0}", + "ReviewLocalLine": "本地账户设置:{0}", + "ReviewFilesLine": "工作文件:{0}", + "TurboOn": "开", + "TurboOff": "关", + "RedundantRemove": "移除", + "RedundantKeep": "保留", + "NetSetupOmit": "省略", + "NetSetupKeep": "保留", + "NetFxIncluded": "包含", + "NetFxNotIncluded": "未包含", + "LocalPreferred": "优先", + "LocalDefault": "默认", + "FilesKeep": "保留", + "FilesClean": "构建后清理", + + # Build description + "BuildDescText": "所选版本将被复制、修改、压缩并保存为可启动的 ISO。", + "BuildDescLegacyFmt": " 旧版安装程序因 {0} 而自动启用。", + + # Confirm build + "ConfirmBuildTitle": "创建此离线 Windows 映像?", + "ConfirmBuildBodyFmt": "输出:{0}\n\n模式:{1}\n移除组:{2}\n单独操作:{3}", + + # Expert dialog + "ExpertNeedImageTitle": "请先检查映像。", + "ExpertNeedImageMsg": "列出组件前请先选择一个版本。", + "ExpertConfirmTitle": "包含受保护组件", + "ExpertConfirmBody": "此清单包含启动驱动、安全性、授权、系统应用与组件负载。移除它们可能导致 Windows 无法启动、不安全或无法更新。受保护包可能拒绝移除。直接移除负载不受支持。\n\n仅用于隔离测试,并准备好备份与可用的安装介质。", + "ExpertConfirmButton": "列出组件", + + # Build progress + "PreparingTools": "正在准备映像工具…", + "BuildingHeading": "构建中", + "ResumingHeading": "继续中", + "WaitingToStart": "等待开始", + "NoChanges": "尚未进行任何更改。", + "AllStagesDone": "全部阶段完成", + "IsoCreated": "ISO 已创建", + "Complete": "完成", + "MetricResumedBuild": "继续构建", + "MetricTurboBuild": "Turbo 构建", + "MetricStandardBuild": "标准构建", + "MetricMinutes": "{0} 分钟", + "MetricInTotal": "(总计)", + "MetricRedundantFmt": "已移除冗余数据:逻辑 {0} MiB · 已省略安装负载 {1} MiB", + "MetricTimingsSaved": "详细的阶段耗时已随构建报告保存。", + "WarnNoteDetail": "ISO 已带警告保存。请参阅页面上的警告。", + "WarnMoreFmt": "\n• 构建日志中还有 {0} 条", + "ResultWarnMsg": "ISO 已带警告保存。请参阅下方跳过的操作与其他详情。", + "ResultOkMsg": "ISO 与构建报告已保存。使用前请在虚拟机中测试 ISO。", + + # Busy text + "CheckingChoices": "正在检查你的选择…", + "ListingComponents": "正在列出映像组件…", + "InspectingImage": "正在检查映像…", + "DownloadingMicrosoft": "正在从 Microsoft 下载…", + "FindingLatest": "正在查找最新的 Windows 11…", + "CheckingDownloaded": "正在检查下载的 ISO…", + "Cancelling": "正在取消…", + "RecoveringWorkspace": "正在恢复工作区…", + + # Cancellation messages + "CancelRequestedFmt": "已请求取消。{0}…", + "CancelWaiting": "已请求取消。正在等待当前操作完成…", + + # Logs / folder / language + "LogsGoneTitle": "作业文件夹已消失。", + "LogsGoneMsgFmt": "{0} 已不存在。", + "FolderOpenFailTitle": "无法打开文件夹。", + "LangAppliedNotice": "语言已应用。重启应用后以新语言显示所有文本。", + "CloseReopen": "请关闭并重新打开 Tiny11 Builder。", + + # Misc notices + "OpStillRunning": "操作仍在运行。", + "OpCancellingWait": "正在取消。请等待其停止后再关闭。", + "OpFinishWait": "请选择 {0} 并等待映像操作完成后再关闭。", + "SessionRestored": "会话已恢复。", + "SessionRestoredMsg": "请重新检查映像后再继续。不会自动开始构建。", + "SessionRestoreFail": "无法恢复会话。", + "ModeSelectFail": "无法选择构建模式。", + "ModeSelectFailMsg": "所选卡片未标识有效的构建模式。", + "OpNotComplete": "操作未完成。", + "PageUpdateFail": "页面部分内容无法更新。", + "EditionNotSupported": "此版本不受支持。", + "EditionNotSupportedMsg": "请选择 Windows 11 客户端映像,22H2 或更新,x64 或 ARM64 格式。", + "AdminRequired": "需要管理员权限。", + "AdminRequiredMsg": "请关闭并重新打开 Tiny11Builder.exe,然后批准 Windows 管理员提示。", + "AdminRecoverMsg": "请关闭并重新打开 Tiny11Builder.exe 并批准管理员权限,以恢复工作区。", + "ImageSelectedNotice": "已选择映像。", + "ImageSelectedMsg": "请关闭并重新打开 Tiny11Builder.exe 并批准管理员权限,以检查其版本。", + "ImageInspected": "映像已检查。", + "ImageInspectedFmt": "找到 {0} 个版本。已选择 {1}。", + "DownloadVerified": "官方 ISO 已下载并通过哈希校验。", + "DownloadMicrosoft": "Microsoft 托管的 ISO 已下载。", + "ComponentListReadOnly": "组件列表以只读方式加载。", + "ComponentListReadOnlyMsg": "已安装的工具可以列举此映像,但构建它需要更新的 Windows ADK 部署工具。请在构建前安装并重新打开应用。", + "ComponentListLoaded": "组件列表已加载。", + "ComponentListLoadedMsg": "已保留现有选择。请勾选要移除或禁用的单个项。", + "RecipeSaved": "配方已保存。", + "RecipeSavedMsg": "它包含选择项与本地路径,不包含已下载的媒体或凭据。", + "RecipeLoaded": "配方已加载。", + "RecipeLoadedMsg": "请重新检查源。Core/专家风险确认需要重新进行。", + "ResumeNoProgress": "此构建无法继续。", + "ResumeAdminMsg": "请关闭并重新打开 Tiny11Builder.exe 并批准管理员权限,然后选择“继续构建”。未做任何更改。", + "ResumeNotStarted": "未开始恢复。", + "OutputExistsMsg": "输出文件已存在。请选择新的文件名以继续。", + "JobHasProgress": "此作业有已保存的进度", + "JobHasProgressMsg": "恢复会丢弃作业的工作副本,而它不会对有已保存进度的作业这样做。继续构建会从该进度继续,而非恢复。\n\n未做任何更改。", + "ConfirmResumeInstead": "改为继续", + "DiscardWorkspace": "丢弃此中断的工作区?", + "DiscardWorkspaceMsg": "仅选择来自中断 tiny11 作业的 job.json。这会丢弃该作业已挂载的工作副本,并移除其暂存媒体。其日志、配方、报告以及你的源 ISO 均会保留。映像已提交(有已保存进度)的作业不会在此丢弃,请使用“继续构建”。正在进行的构建无法恢复。", + "ConfirmDiscard": "丢弃工作副本", + "WorkspaceRecovered": "工作区已恢复。", + "WorkspaceRecoveredMsg": "已丢弃中断的工作副本。你的源与日志均已保留。", + "ResumeThisBuild": "继续此构建?", + "ConfirmResume": "继续", + + # Remaining MainWindow dynamic strings + "OfficialIsoFmt": "官方 Microsoft ISO · {0} · {1}", + "FileTypeIso": "ISO 映像", + "FileTypeRecipe": "映像配方", + "CachedIsoChanged": "缓存的 ISO 已更改。它未被接受。", + "PreviouslyVerified": "已验证先前下载的 ISO。", + "DownloadedFmt": "已下载 {0}。SHA-256:{1}", + "HashPublished": "(已验证发布哈希)", + "HashComputed": "(计算的哈希;Microsoft 未提供发布哈希)", + "InspectFirstTitle": "请先检查映像。", + "InspectFirstMsg": "请在列出组件之前选择一个版本。", + "ProtectedConfirmTitle": "包含受保护的组件", + "ProtectedConfirmBody": "此清单包含启动驱动、安全、许可、系统应用与组件负载。移除它们可能导致 Windows 无法启动、不安全或无法更新。受保护的包可能拒绝移除。不支持直接负载移除。\n\n仅在有备份与可靠安装介质的情况下,为隔离测试继续。", + "ProtectedConfirmAction": "检查组件", + "FailedDetailFmt": "{0} 失败。展开此阶段查看错误详情。", + "StageOfFmt": "阶段 {0}/{1}", + "ReviewCoreRemovalsFmt": "CORE 移除 {0}", + "ReviewExpertItemFmt": " 专家 {0} {1}: {2}", + "AdminRecoverTitle": "需要管理员权限。", + "JobFolderGoneTitle": "作业文件夹已不存在。", + "JobFolderGoneMsg": "{0} 已不存在。", + "LangRestartCloseMsg": "请关闭并重新打开 Tiny11 Builder。", + "CustomChoicesNote.Text": "勾选的组将被移除或禁用,未勾选的组将保留。如需处理单个项,请使用下方的组件列表。", + "DownloadVerifiedMsg": "文件与所提供的发布 SHA-256 一致。请检查它以选择版本。", + "DownloadUnverifiedMsg": "未提供发布哈希。显示的是计算得到的哈希,真实性尚未经过独立验证。", + "ReviewModeLabelFmt": "模式 {0}", +} + + +def build_resw(items: dict[str, str]) -> str: + root = ET.Element("root") + for name in ("resmimetype", "version", "reader", "writer"): + header = ET.SubElement(root, "resheader", {"name": name}) + value = ET.SubElement(header, "value") + value.text = { + "resmimetype": "text/microsoft-resx", + "version": "2.0", + "reader": "System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", + "writer": "System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", + }[name] + for key in sorted(items): + data = ET.SubElement(root, "data", {"name": key, "xml:space": "preserve"}) + ET.SubElement(data, "value").text = items[key] + rough = ET.tostring(root, encoding="utf-8") + pretty = minidom.parseString(rough).toprettyxml(indent=" ") + # Drop the default XML declaration's standalone and keep a UTF-8 declaration. + return '\n' + "\n".join(pretty.splitlines()[1:]) + + +if __name__ == "__main__": + import os + out_dir = os.path.join(os.path.dirname(__file__), "..", "src", "Tiny11Builder.App", "Strings", "zh-CN") + os.makedirs(out_dir, exist_ok=True) + out_path = os.path.join(out_dir, "Resources.resw") + with open(out_path, "w", encoding="utf-8") as handle: + handle.write(build_resw(ZH)) + print(f"Wrote {len(ZH)} entries to {os.path.normpath(out_path)}") From 7cc48c47efd7bdd9fc49da44e10989de225db075 Mon Sep 17 00:00:00 2001 From: pkssky Date: Wed, 28 Oct 2026 08:40:29 +0800 Subject: [PATCH 2/2] Fix build issues found during first real compile - Localization.cs: add missing using Tiny11Builder.Core for L10n (CS0103) - Rename C# key CompFast to CompFastWim: it collided with the CompFast ComboBoxItem x:Uid in PRI generation (PRI175/PRI278) - Resw regenerated; verified build + package for x64 and arm64 --- src/Tiny11Builder.App/Localization.cs | 1 + src/Tiny11Builder.App/MainWindow.xaml.cs | 2 +- src/Tiny11Builder.App/OperationFlow.cs | 2 +- src/Tiny11Builder.App/Strings/zh-CN/Resources.resw | 6 +++--- tools/gen_resw.py | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Tiny11Builder.App/Localization.cs b/src/Tiny11Builder.App/Localization.cs index 8cf12e8e..dae5fbec 100644 --- a/src/Tiny11Builder.App/Localization.cs +++ b/src/Tiny11Builder.App/Localization.cs @@ -1,4 +1,5 @@ using Microsoft.Windows.ApplicationModel.Resources; +using Tiny11Builder.Core; using Windows.Globalization; using Windows.Storage; diff --git a/src/Tiny11Builder.App/MainWindow.xaml.cs b/src/Tiny11Builder.App/MainWindow.xaml.cs index a0716de1..706b4574 100644 --- a/src/Tiny11Builder.App/MainWindow.xaml.cs +++ b/src/Tiny11Builder.App/MainWindow.xaml.cs @@ -1023,7 +1023,7 @@ private void UpdateReview() { Compression.Recovery => L10n.T("CompRecovery", "Recovery ESD"), Compression.Maximum => L10n.T("CompMaximum", "Maximum WIM"), - _ => L10n.T("CompFast", "Fast WIM") + _ => L10n.T("CompFastWim", "Fast WIM") }; ReviewOptions.Text = string.Format(L10n.T("ReviewOptionsCompFmt", "{0} compression"), compressionWord) + "\n" + string.Format(L10n.T("ReviewTurboLine", "Turbo mode: {0}"), recipe.TurboMode ? L10n.T("TurboOn", "On") : L10n.T("TurboOff", "Off")) + "\n" + diff --git a/src/Tiny11Builder.App/OperationFlow.cs b/src/Tiny11Builder.App/OperationFlow.cs index b5fd5b56..4f80b1d1 100644 --- a/src/Tiny11Builder.App/OperationFlow.cs +++ b/src/Tiny11Builder.App/OperationFlow.cs @@ -256,7 +256,7 @@ public static string Describe(BuildRecipe recipe) { Compression.Recovery => L10n.T("CompRecovery", "Recovery ESD"), Compression.Maximum => L10n.T("CompMaximum", "Maximum WIM"), - _ => L10n.T("CompFast", "Fast WIM") + _ => L10n.T("CompFastWim", "Fast WIM") }; var text = new StringBuilder(); text.AppendLine(string.Format(L10n.T("ResumeHeader", "Output: {0}"), recipe.OutputPath)); diff --git a/src/Tiny11Builder.App/Strings/zh-CN/Resources.resw b/src/Tiny11Builder.App/Strings/zh-CN/Resources.resw index 6b95c3c1..f880e0a5 100644 --- a/src/Tiny11Builder.App/Strings/zh-CN/Resources.resw +++ b/src/Tiny11Builder.App/Strings/zh-CN/Resources.resw @@ -204,12 +204,12 @@ 请关闭并重新打开 Tiny11 Builder。 - - 快速 WIM - 快速 WIM(构建最快,ISO 更大) + + 快速 WIM + 最大 WIM(便于日后维护) diff --git a/tools/gen_resw.py b/tools/gen_resw.py index 429d3df4..fa32cdaf 100644 --- a/tools/gen_resw.py +++ b/tools/gen_resw.py @@ -406,7 +406,7 @@ "ModeCustom": "自定义", "CompRecovery": "恢复 ESD", "CompMaximum": "最大 WIM", - "CompFast": "快速 WIM", + "CompFastWim": "快速 WIM", # SetupLabels (legacy installer note) "LegacyInstaller": "旧版安装程序:自动(因 {0} 需要)",