# 启动器 RESX 多语言适配实施计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 为 LanMountainDesktop.Launcher 引入 RESX 资源文件,实现启动器 UI 的多语言适配,消除所有硬编码中英文字符串。 **Architecture:** 在 Launcher 项目中创建 RESX 资源文件体系(默认 zh-CN + en-US/ja-JP/ko-KR),通过 .NET 内置 `ResourceManager` 机制实现本地化。启动时从主应用 `settings.json` 读取 `LanguageCode` 字段设置 `CultureInfo.CurrentUICulture`,AXAML 中使用 `x:Static` 引用资源,C# 代码中通过 `Strings.ResourceName` 强类型访问。 **Tech Stack:** .NET RESX 资源文件、Avalonia `x:Static` 标记扩展、`System.Globalization.CultureInfo` --- ## 现状分析 ### 问题概述 1. **启动器完全没有本地化支持**:所有 UI 字符串硬编码,中英文混杂严重 2. **纯英文窗口**:SplashWindow、ErrorWindow、MultiInstancePromptWindow、DataLocationPromptWindow、LoadingDetailsWindow 3. **纯中文窗口**:OobeWindow、MigrationPromptWindow、UpdateWindow、ErrorDebugWindow、DevDebugWindow、PrivacyPolicyWindow 4. **启动器不读取主应用语言设置**:没有 `LanguageCode` 相关代码 5. **硬编码字符串总量约 180+ 条**,分布在 11 个 AXAML 视图和 11 个 C# code-behind 文件中 ### 方案选择:RESX vs JSON | 维度 | RESX(本方案) | JSON(主项目模式) | |------|---------------|-------------------| | 编译时安全 | ✅ 强类型 `Strings.KeyName` | ❌ 字符串键值 `L("key", "fallback")` | | AXAML 集成 | ✅ `x:Static` 直接引用 | ❌ 需 code-behind 赋值 | | 回退机制 | ✅ 内置(默认资源 → 特定文化) | ✅ 自定义 `fallback` 参数 | | 新增语言 | 需添加 RESX 文件并重新编译 | 仅添加 JSON 文件 | | AOT 兼容性 | ⚠️ 需额外配置 | ✅ 已验证 | | 与主项目一致性 | ❌ 不同模式 | ✅ 一致 | **选择 RESX 的理由**:启动器是独立轻量进程,不需要运行时语言切换;强类型访问减少拼写错误;`x:Static` 比 code-behind 赋值更清晰;RESX 的内置回退机制足够满足启动器需求。 ### AOT 兼容性说明 Launcher 项目支持 Native AOT 发布。RESX 的 `ResourceManager` 依赖反射,需要: 1. 在 `.csproj` 中添加 `` 确保资源不被修剪 2. 在 AOT props 中添加 `TrimmerRootAssembly` 保留资源程序集 3. 发布后进行 AOT 冒烟测试验证 --- ## 文件结构规划 ### 新增文件 | 文件 | 职责 | |------|------| | `Resources/Strings.resx` | 默认资源文件(zh-CN,回退资源) | | `Resources/Strings.en-US.resx` | 英语资源 | | `Resources/Strings.ja-JP.resx` | 日语资源 | | `Resources/Strings.ko-KR.resx` | 韩语资源 | | `Services/LanguagePreferenceService.cs` | 从 settings.json 读取 LanguageCode 并设置 CultureInfo | ### 修改文件 | 文件 | 改动内容 | |------|---------| | `LanMountainDesktop.Launcher.csproj` | 添加 RESX 嵌入资源配置 | | `LanMountainDesktop.Launcher.AOT.props` | 添加资源程序集修剪保留 | | `Program.cs` | 启动时调用语言偏好初始化 | | `Views/SplashWindow.axaml` | 替换硬编码字符串为 `x:Static` | | `Views/SplashWindow.axaml.cs` | 替换 C# 硬编码字符串为 `Strings.XXX` | | `Views/ErrorWindow.axaml` | 同上 | | `Views/ErrorWindow.axaml.cs` | 同上 | | `Views/MultiInstancePromptWindow.axaml` | 同上 | | `Views/MultiInstancePromptWindow.axaml.cs` | 同上 | | `Views/DataLocationPromptWindow.axaml` | 同上 | | `Views/DataLocationPromptWindow.axaml.cs` | 同上 | | `Views/LoadingDetailsWindow.axaml` | 同上 | | `Views/LoadingDetailsWindow.axaml.cs` | 同上 | | `Views/UpdateWindow.axaml` | 同上 | | `Views/UpdateWindow.axaml.cs` | 同上 | | `Views/ErrorDebugWindow.axaml` | 同上 | | `Views/ErrorDebugWindow.axaml.cs` | 同上 | | `Views/OobeWindow.axaml` | 同上 | | `Views/OobeWindow.axaml.cs` | 同上 | | `Views/MigrationPromptWindow.axaml` | 同上 | | `Views/MigrationPromptWindow.axaml.cs` | 同上 | | `Views/PrivacyPolicyWindow.axaml` | 同上 | | `Views/PrivacyPolicyWindow.axaml.cs` | 同上 | | `Views/DevDebugWindow.axaml` | 同上 | | `Views/DevDebugWindow.axaml.cs` | 同上 | | `Services/LauncherFlowCoordinator.cs` | 替换硬编码字符串 | | `App.axaml.cs` | 替换预览模式硬编码字符串 | --- ## RESX 键命名规范 采用 `ViewName_ElementDescription` 模式,PascalCase 分隔: - 窗口标题:`Splash_Title`、`Error_Title`、`MultiInstance_Title` - 按钮文本:`Error_ButtonOpenLogs`、`Error_ButtonCopy`、`Error_ButtonRetry` - 状态文本:`Splash_StatusInitializing`、`Loading_StatusPreparing` - 描述文本:`DataLocation_DescSystemProfile`、`DataLocation_DescPortable` - OOBE 步骤:`Oobe_StepWelcomeTitle`、`Oobe_StepAppearanceTitle` --- ## 实施任务 ### Task 1: 创建 RESX 基础设施 **Files:** - Create: `LanMountainDesktop.Launcher/Resources/Strings.resx` - Create: `LanMountainDesktop.Launcher/Resources/Strings.en-US.resx` - Create: `LanMountainDesktop.Launcher/Resources/Strings.ja-JP.resx` - Create: `LanMountainDesktop.Launcher/Resources/Strings.ko-KR.resx` - Modify: `LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj` - Modify: `LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.AOT.props` - [ ] **Step 1: 创建默认 RESX 文件(zh-CN 回退资源)** 创建 `Resources/Strings.resx`,包含所有 180+ 条字符串的中文翻译。此文件同时作为回退资源和中文资源。 ```xml text/microsoft-resx 2.0 System.Resources.ResXResourceReader, System.Windows.Forms System.Resources.ResXResourceWriter, System.Windows.Forms 阑山桌面 阑山桌面 正在初始化... [调试模式] 启动画面预览 阑山桌面 启动器无法确认启动状态 阑山桌面未达到预期的启动状态。 启动恢复 您可以检查日志、等待当前进程或激活正在运行的桌面实例。 诊断详情 打开日志 复制 等待 退出 重试 激活 [调试] 启动器错误 启动器找不到桌面可执行文件 在调试模式下选择另一个可执行文件、检查日志,或在修复部署路径后重试。 检查日志后重试,等待上一次启动尝试完全结束。 检查日志或退出。旧进程仍在运行时,启动器不会创建新的桌面进程。 启动仍在进行中 桌面进程仍在运行,启动器不会启动第二个实例。 阑山桌面 阑山桌面已在运行 启动器检测到已存在的桌面实例,未启动新进程。 重复启动 您当前的设置为显示此提示而不自动打开桌面。 未创建第二个主进程。 复制 关闭 打开桌面 现有主进程 PID: {0}\nShell 状态: {1}\n未创建第二个主进程。 选择数据保存位置 选择数据保存位置 选择启动器和桌面数据的存储位置。您可以稍后在设置中更改。 应用目录不可写入 当前安装目录需要管理员权限才能写入。数据将存储在系统用户目录中。 保存在系统用户目录(推荐) 数据与当前 Windows 用户绑定,在应用重新安装和更新后保持完整。 保存在应用安装目录(便携模式) 适用于便携安装。整个应用文件夹可以连同数据一起移动到另一台机器。 取消 确认 检测到已有的系统数据。选择便携模式将自动迁移当前数据。 阑山桌面 - 加载详情 正在启动阑山桌面 正在初始化... 正在准备组件 加载项目 完成 加载时发生错误。 详情 取消 准备就绪 正在加载插件... 正在加载组件... 正在加载资源... 正在加载数据... 正在下载... 正在处理... 完成 插件 组件 资源 数据 网络 设置 系统 其他 阑山桌面 - 更新 阑山桌面 更新 正在更新,请稍候... 更新完成 更新失败 更新过程中发生错误 [调试模式] 更新页面 预览更新进度界面 调试模式 调试设置 开发模式 启用后自动扫描开发目录 应用路径 未选择 浏览... 此功能仅供开发人员使用 取消 确定 选择阑山桌面主程序可执行文件 欢迎使用阑山桌面 欢迎使用阑山桌面 你的桌面,不止一面 开始使用 个性化你的桌面 选择你喜欢的主题样式,可随时在设置中更改 外观模式 浅色模式 深色模式 主题色 莫奈取色来源 从桌面壁纸取色 自定义图片取色 不使用莫奈取色 选择数据保存位置 保存在系统用户目录(推荐) 数据与当前 Windows 用户绑定,在应用重新安装和更新后保持完整。 保存在应用安装目录(便携模式) 适用于便携安装。整个应用文件夹可以连同数据一起移动到另一台机器。 无法保存到应用目录 当前安装目录需要管理员权限才能写入。数据将存储在系统用户目录中。 启动与展示 在任务栏显示主桌面窗口 以滑动方式显示主窗口 启动时使用淡入过渡 融合桌面与弹入手势 登录 Windows 时自动启动阑山桌面 信息与隐私 发送匿名崩溃报告 发送匿名使用统计 隐私追踪 ID 同意 《阑山桌面遥测隐私数据收集协议》 返回 下一步 欢迎使用阑山桌面 你的桌面,不止一面 阑山桌面 - 版本迁移 检测到旧版本 检测到您的系统中安装了旧版本的阑山桌面(0.8.4)... 版本: 位置: 类型: 安装版 卸载旧版本不会影响新版本的使用,您的个人数据将保留。 查看位置 暂不处理 卸载旧版本 阑山桌面遥测隐私数据收集协议 阑山桌面遥测隐私数据收集协议 请仔细阅读以下协议内容,了解我们如何收集、使用和保护您的数据 关闭 开发调试窗口 启动画面 错误页面 更新页面 OOBE页面 数据位置选择 启用功能 打开 全部设为查看模式 全部设为功能模式 关闭 设备较慢,仍在启动,请稍候。 桌面主进程仍在运行,Launcher 会继续等待,不会重复启动。 正在初始化... 正在检查更新... 正在检查插件... 正在启动主程序... 准备就绪 [预览] 这是启动器错误窗口预览。 正在处理 {0}... 正在连接到活跃的启动器... ``` - [ ] **Step 2: 创建 en-US RESX 文件** 创建 `Resources/Strings.en-US.resx`,包含所有字符串的英文翻译。结构与默认文件相同,仅 `` 内容为英文。 ```xml LanMountain Desktop LanMountain Desktop Initializing... Launcher could not confirm startup LanMountain Desktop did not reach the expected startup state. ``` - [ ] **Step 3: 创建 ja-JP RESX 文件** 创建 `Resources/Strings.ja-JP.resx`,包含所有字符串的日语翻译。 - [ ] **Step 4: 创建 ko-KR RESX 文件** 创建 `Resources/Strings.ko-KR.resx`,包含所有字符串的韩语翻译。 - [ ] **Step 5: 修改 .csproj 添加 RESX 配置** 在 `LanMountainDesktop.Launcher.csproj` 的 `` 中添加: ```xml PublicResXFileCodeGenerator Strings.Designer.cs ``` 注意:使用 `PublicResXFileCodeGenerator` 而非 `ResXFileCodeGenerator`,生成 `public` 类以便 AXAML 的 `x:Static` 可以访问。 - [ ] **Step 6: 修改 AOT props 添加资源程序集保留** 在 `LanMountainDesktop.Launcher.AOT.props` 的 AOT 修剪配置 `` 中添加: ```xml ``` - [ ] **Step 7: 运行构建验证 RESX 生成** Run: `dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Debug` Expected: 构建成功,`Resources/Strings.Designer.cs` 自动生成 --- ### Task 2: 创建语言偏好服务 **Files:** - Create: `LanMountainDesktop.Launcher/Services/LanguagePreferenceService.cs` - Modify: `LanMountainDesktop.Launcher/Program.cs` - [ ] **Step 1: 创建 LanguagePreferenceService** ```csharp using System.Globalization; using System.Text.Json.Nodes; namespace LanMountainDesktop.Launcher.Services; internal static class LanguagePreferenceService { public static string ResolveLanguageCode(string appRoot) { try { var dataLocationResolver = new DataLocationResolver(appRoot); var settingsPath = HostAppSettingsOobeMerger.GetSettingsFilePath(dataLocationResolver.ResolveDataRoot()); if (!File.Exists(settingsPath)) { return "zh-CN"; } var root = JsonNode.Parse(File.ReadAllText(settingsPath))?.AsObject(); if (root is not null && root.TryGetPropertyValue("LanguageCode", out var node) && node is JsonValue value && value.TryGetValue(out var code) && !string.IsNullOrWhiteSpace(code)) { return NormalizeLanguageCode(code); } } catch { } return "zh-CN"; } public static void ApplyLanguage(string languageCode) { var normalized = NormalizeLanguageCode(languageCode); var culture = CultureInfo.GetCultureInfo(normalized); CultureInfo.DefaultThreadCurrentCulture = culture; CultureInfo.DefaultThreadCurrentUICulture = culture; Thread.CurrentThread.CurrentCulture = culture; Thread.CurrentThread.CurrentUICulture = culture; } private static string NormalizeLanguageCode(string code) { return code.ToLowerInvariant() switch { "en-us" or "en" => "en-US", "ja-jp" or "ja" => "ja-JP", "ko-kr" or "ko" => "ko-KR", _ => "zh-CN" }; } } ``` - [ ] **Step 2: 在 Program.cs 中调用语言初始化** 在 `Program.Main` 方法中,`BuildAvaloniaApp().StartWithClassicDesktopLifetime(args)` 之前添加语言初始化: ```csharp var appRoot = Commands.ResolveAppRoot(commandContext); var languageCode = LanguagePreferenceService.ResolveLanguageCode(appRoot); LanguagePreferenceService.ApplyLanguage(languageCode); ``` - [ ] **Step 3: 构建验证** Run: `dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Debug` Expected: 构建成功 --- ### Task 3: 替换 SplashWindow 硬编码字符串 **Files:** - Modify: `LanMountainDesktop.Launcher/Views/SplashWindow.axaml` - Modify: `LanMountainDesktop.Launcher/Views/SplashWindow.axaml.cs` - [ ] **Step 1: 在 SplashWindow.axaml 中添加 RESX 命名空间并替换字符串** 在 `` 标签添加命名空间: ```xml xmlns:res="clr-namespace:LanMountainDesktop.Launcher.Resources" ``` 替换硬编码字符串: - `Title="LanMountain Desktop"` → `Title="{x:Static res:Strings.Splash_Title}"` - `Text="LanMountain Desktop"` (AppNameText) → `Text="{x:Static res:Strings.Splash_AppName}"` - `Text="Initializing..."` (StatusText) → `Text="{x:Static res:Strings.Splash_StatusInitializing}"` 注意:`VersionText` 的 `Text="0.0.0-dev (Administrate)"` 是动态设置的占位文本,保留原样(由 code-behind `SetVersionInfo` 方法设置)。 - [ ] **Step 2: 在 SplashWindow.axaml.cs 中替换 C# 硬编码字符串** 将 `"[Debug Mode] Splash Preview"` 替换为 `Strings.Splash_DebugPreview`。 - [ ] **Step 3: 构建验证** Run: `dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Debug` Expected: 构建成功 --- ### Task 4: 替换 ErrorWindow 硬编码字符串 **Files:** - Modify: `LanMountainDesktop.Launcher/Views/ErrorWindow.axaml` - Modify: `LanMountainDesktop.Launcher/Views/ErrorWindow.axaml.cs` - [ ] **Step 1: 在 ErrorWindow.axaml 中添加 RESX 命名空间并替换字符串** 添加命名空间 `xmlns:res="clr-namespace:LanMountainDesktop.Launcher.Resources"` AXAML 替换: - `Title="LanMountain Desktop"` → `Title="{x:Static res:Strings.Error_Title}"` - `Text="Launcher could not confirm startup"` → `Text="{x:Static res:Strings.Error_TitleCannotConfirm}"` - `Text="LanMountain Desktop did not reach..."` → `Text="{x:Static res:Strings.Error_MessageNotReached}"` - `Title="Startup recovery"` → `Title="{x:Static res:Strings.Error_SuggestionTitle}"` - `Message="You can inspect logs..."` → `Message="{x:Static res:Strings.Error_SuggestionMessage}"` - `Header="Diagnostic details"` → `Header="{x:Static res:Strings.Error_DiagnosticHeader}"` - `Text="Open Logs"` → `Text="{x:Static res:Strings.Error_ButtonOpenLogs}"` - `Text="Copy"` → `Text="{x:Static res:Strings.Error_ButtonCopy}"` - `Content="Wait"` → `Content="{x:Static res:Strings.Error_ButtonWait}"` - `Text="Exit"` → `Text="{x:Static res:Strings.Error_ButtonExit}"` - `Content="Retry"` → `Content="{x:Static res:Strings.Error_ButtonRetry}"` - [ ] **Step 2: 在 ErrorWindow.axaml.cs 中替换 C# 硬编码字符串** 将所有硬编码字符串替换为 `Strings.XXX` 调用: - `"LanMountain Desktop did not reach..."` → `Strings.Error_MessageNotReached` - `"[Debug] Launcher error"` → `Strings.Error_DebugTitle` - `"Launcher could not find the desktop executable"` → `Strings.Error_HostNotFoundTitle` - `"Pick another executable..."` → `Strings.Error_HostNotFoundMessage` - `"Launcher could not confirm startup"` → `Strings.Error_TitleCannotConfirm` - `"Inspect logs, then retry..."` → `Strings.Error_GenericMessage` - `"Inspect logs or exit..."` → `Strings.Error_RunningHostMessage` - `"Retry"` → `Strings.Error_ButtonRetry` - `"Activate"` → `Strings.Error_ButtonActivate` - `"Wait"` → `Strings.Error_ButtonWait` - `"Startup is still pending"` → `Strings.Error_PendingTitle` - `"The desktop process is still running..."` → `Strings.Error_PendingMessage` - [ ] **Step 3: 构建验证** Run: `dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Debug` Expected: 构建成功 --- ### Task 5: 替换 MultiInstancePromptWindow 硬编码字符串 **Files:** - Modify: `LanMountainDesktop.Launcher/Views/MultiInstancePromptWindow.axaml` - Modify: `LanMountainDesktop.Launcher/Views/MultiInstancePromptWindow.axaml.cs` - [ ] **Step 1: 在 MultiInstancePromptWindow.axaml 中替换字符串** 添加命名空间,替换: - `Title="LanMountain Desktop"` → `Title="{x:Static res:Strings.MultiInstance_Title}"` - `Text="LanMountain Desktop is already running"` → `Text="{x:Static res:Strings.MultiInstance_AlreadyRunning}"` - `Text="Launcher found an existing..."` → `Text="{x:Static res:Strings.MultiInstance_AlreadyRunningMessage}"` - `Title="Repeated launch"` → `Title="{x:Static res:Strings.MultiInstance_RepeatedLaunchTitle}"` - `Message="Your current setting..."` → `Message="{x:Static res:Strings.MultiInstance_RepeatedLaunchMessage}"` - `Text="No second Host process..."` → `Text="{x:Static res:Strings.MultiInstance_NoSecondProcess}"` - `Text="Copy"` → `Text="{x:Static res:Strings.MultiInstance_ButtonCopy}"` - `Text="Close"` → `Text="{x:Static res:Strings.MultiInstance_ButtonClose}"` - `Text="Open desktop"` → `Text="{x:Static res:Strings.MultiInstance_ButtonOpenDesktop}"` - [ ] **Step 2: 在 MultiInstancePromptWindow.axaml.cs 中替换 C# 硬编码字符串** 将格式化字符串替换为 `string.Format(Strings.MultiInstance_DetailsFormat, processId, shellState)` 等。 - [ ] **Step 3: 构建验证** Run: `dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Debug` Expected: 构建成功 --- ### Task 6: 替换 DataLocationPromptWindow 硬编码字符串 **Files:** - Modify: `LanMountainDesktop.Launcher/Views/DataLocationPromptWindow.axaml` - Modify: `LanMountainDesktop.Launcher/Views/DataLocationPromptWindow.axaml.cs` - [ ] **Step 1: 在 DataLocationPromptWindow.axaml 中替换字符串** 替换所有 12 个硬编码字符串为 `x:Static` 引用。 - [ ] **Step 2: 在 DataLocationPromptWindow.axaml.cs 中替换 C# 硬编码字符串** 将 `"Existing system data was detected..."` 替换为 `Strings.DataLocation_MigrateWarning`。 - [ ] **Step 3: 构建验证** Run: `dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Debug` Expected: 构建成功 --- ### Task 7: 替换 LoadingDetailsWindow 硬编码字符串 **Files:** - Modify: `LanMountainDesktop.Launcher/Views/LoadingDetailsWindow.axaml` - Modify: `LanMountainDesktop.Launcher/Views/LoadingDetailsWindow.axaml.cs` - [ ] **Step 1: 在 LoadingDetailsWindow.axaml 中替换字符串** 替换所有硬编码字符串为 `x:Static` 引用。 - [ ] **Step 2: 在 LoadingDetailsWindow.axaml.cs 中替换 C# 硬编码字符串** 替换 `GetStageDescription`、`GetItemDescription`、`GetTypeLabel` 方法中的硬编码字符串为 `Strings.XXX` 调用。 - [ ] **Step 3: 构建验证** Run: `dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Debug` Expected: 构建成功 --- ### Task 8: 替换 UpdateWindow 硬编码字符串 **Files:** - Modify: `LanMountainDesktop.Launcher/Views/UpdateWindow.axaml` - Modify: `LanMountainDesktop.Launcher/Views/UpdateWindow.axaml.cs` - [ ] **Step 1: 在 UpdateWindow.axaml 中替换字符串** 替换 `"Update"` 为 `x:Static res:Strings.Update_StatusUpdate`。 - [ ] **Step 2: 在 UpdateWindow.axaml.cs 中替换 C# 硬编码字符串** 替换 `"更新完成"`、`"更新失败"`、`"更新过程中发生错误"`、`"[调试模式] 更新页面"`、`"预览更新进度界面"` 为 `Strings.XXX` 调用。 - [ ] **Step 3: 构建验证** Run: `dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Debug` Expected: 构建成功 --- ### Task 9: 替换 ErrorDebugWindow 硬编码字符串 **Files:** - Modify: `LanMountainDesktop.Launcher/Views/ErrorDebugWindow.axaml` - Modify: `LanMountainDesktop.Launcher/Views/ErrorDebugWindow.axaml.cs` - [ ] **Step 1: 在 ErrorDebugWindow.axaml 中替换字符串** 该窗口已使用中文,替换所有硬编码中文字符串为 `x:Static` 引用。 - [ ] **Step 2: 在 ErrorDebugWindow.axaml.cs 中替换 C# 硬编码字符串** 替换 `"Select LanMountainDesktop host executable"` 和 `"Not selected"` 为 `Strings.DebugDebug_SelectExeDialog` 和 `Strings.DebugDebug_NotSelected`。 - [ ] **Step 3: 构建验证** Run: `dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Debug` Expected: 构建成功 --- ### Task 10: 替换 OobeWindow 硬编码字符串 **Files:** - Modify: `LanMountainDesktop.Launcher/Views/OobeWindow.axaml` - Modify: `LanMountainDesktop.Launcher/Views/OobeWindow.axaml.cs` 这是最大的单个任务,OobeWindow 有约 42 个硬编码字符串。 - [ ] **Step 1: 在 OobeWindow.axaml 中替换字符串** 添加命名空间,逐个替换所有硬编码中文字符串为 `x:Static` 引用。包括: - 窗口标题、欢迎页文本 - 外观设置页文本 - 数据位置页文本 - 启动展示页文本 - 隐私页文本 - 完成页文本 - 导航按钮文本 - [ ] **Step 2: 在 OobeWindow.axaml.cs 中替换 C# 硬编码字符串(如有)** 检查 code-behind 中是否有动态设置的硬编码字符串并替换。 - [ ] **Step 3: 构建验证** Run: `dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Debug` Expected: 构建成功 --- ### Task 11: 替换 MigrationPromptWindow 硬编码字符串 **Files:** - Modify: `LanMountainDesktop.Launcher/Views/MigrationPromptWindow.axaml` - Modify: `LanMountainDesktop.Launcher/Views/MigrationPromptWindow.axaml.cs` - [ ] **Step 1: 在 MigrationPromptWindow.axaml 中替换字符串** 替换所有硬编码中文字符串为 `x:Static` 引用。 - [ ] **Step 2: 在 MigrationPromptWindow.axaml.cs 中替换 C# 硬编码字符串(如有)** - [ ] **Step 3: 构建验证** Run: `dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Debug` Expected: 构建成功 --- ### Task 12: 替换 PrivacyPolicyWindow 硬编码字符串 **Files:** - Modify: `LanMountainDesktop.Launcher/Views/PrivacyPolicyWindow.axaml` - Modify: `LanMountainDesktop.Launcher/Views/PrivacyPolicyWindow.axaml.cs` - [ ] **Step 1: 在 PrivacyPolicyWindow.axaml 中替换字符串** 替换标题、描述、关闭按钮等硬编码字符串。 - [ ] **Step 2: 在 PrivacyPolicyWindow.axaml.cs 中处理隐私政策正文** 隐私政策正文(约 80 行 Markdown)目前硬编码在 C# 中。考虑: - 方案 A:将 Markdown 正文也放入 RESX(支持多语言隐私政策) - 方案 B:保留 Markdown 正文在 C# 中,仅替换窗口标题和按钮 推荐方案 A,将隐私政策 Markdown 正文放入 RESX 的 `Privacy_PolicyContent` 键中。 - [ ] **Step 3: 构建验证** Run: `dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Debug` Expected: 构建成功 --- ### Task 13: 替换 DevDebugWindow 硬编码字符串 **Files:** - Modify: `LanMountainDesktop.Launcher/Views/DevDebugWindow.axaml` - Modify: `LanMountainDesktop.Launcher/Views/DevDebugWindow.axaml.cs` - [ ] **Step 1: 在 DevDebugWindow.axaml 中替换字符串** 替换所有硬编码中文字符串为 `x:Static` 引用。 - [ ] **Step 2: 在 DevDebugWindow.axaml.cs 中替换 C# 硬编码字符串(如有)** - [ ] **Step 3: 构建验证** Run: `dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Debug` Expected: 构建成功 --- ### Task 14: 替换 LauncherFlowCoordinator 和 App.axaml.cs 硬编码字符串 **Files:** - Modify: `LanMountainDesktop.Launcher/Services/LauncherFlowCoordinator.cs` - Modify: `LanMountainDesktop.Launcher/App.axaml.cs` - [ ] **Step 1: 在 LauncherFlowCoordinator.cs 中替换字符串** 替换: - `"设备较慢,仍在启动,请稍候。"` → `Strings.Coordinator_SlowDeviceMessage` - `"桌面主进程仍在运行..."` → `Strings.Coordinator_RunningHostMessage` - [ ] **Step 2: 在 App.axaml.cs 中替换预览模式字符串** 替换 `SimulateSplashPreviewAsync` 中的硬编码消息数组: ```csharp var messages = new[] { Strings.Preview_SplashInitializing, Strings.Preview_SplashCheckingUpdates, Strings.Preview_SplashCheckingPlugins, Strings.Preview_SplashLaunchingHost, Strings.Preview_SplashReady }; ``` 替换 `HandlePreviewCommand` 中的 `"[Preview] This is the launcher error window preview."` → `Strings.Preview_ErrorMessage` 替换 `RunApplyUpdateWithWindowAsync` 中的硬编码字符串: - `"Verifying update..."` → 使用 RESX 键 - `"Applying plugin upgrades..."` → 使用 RESX 键 - `"Cleaning up old deployments..."` → 使用 RESX 键 替换 `SimulateUpdatePreviewAsync` 中的 `$"Processing {stages[i]}..."` → `string.Format(Strings.Preview_UpdateProcessing, stages[i])` 替换 `AttachToExistingCoordinatorAsync` 中的 `"Connecting to the active launcher..."` → `Strings.Preview_ActivationConnecting` - [ ] **Step 3: 构建验证** Run: `dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Debug` Expected: 构建成功 --- ### Task 15: 完整构建和运行验证 **Files:** 无新增/修改 - [ ] **Step 1: 完整解决方案构建** Run: `dotnet build LanMountainDesktop.slnx -c Debug` Expected: 构建成功,无错误 - [ ] **Step 2: 运行启动器预览命令验证中文** Run: `dotnet run --project LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -- preview-splash` Expected: 启动画面显示中文 - [ ] **Step 3: 验证英文模式** 临时将 `LanguagePreferenceService.ResolveLanguageCode` 返回 `"en-US"` 后运行预览命令,验证英文显示。 - [ ] **Step 4: 运行测试** Run: `dotnet test LanMountainDesktop.slnx -c Debug` Expected: 所有测试通过 --- ### Task 16: AOT 发布冒烟测试 **Files:** 无新增/修改 - [ ] **Step 1: AOT 发布测试** Run: `dotnet publish LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Release -r win-x64 /p:PublishAot=true` Expected: 发布成功 - [ ] **Step 2: 运行 AOT 发布产物验证** 运行发布后的可执行文件,验证 RESX 资源正确加载。 --- ## 实施顺序建议 1. **Task 1** (RESX 基础设施) → **Task 2** (语言偏好服务) — 必须首先完成 2. **Task 3-9** (英文窗口) — 优先处理,解决用户提出的"只有英文"问题 3. **Task 10-13** (中文窗口) — 次优先,完成完整 i18n 覆盖 4. **Task 14** (服务层和 App) — 与 Task 3-13 并行或随后 5. **Task 15-16** (验证) — 最后执行 ## 风险与注意事项 1. **AOT 兼容性**:`ResourceManager` 在 Native AOT 下可能需要额外配置。如果 AOT 发布失败,需要添加 `DynamicDependency` 属性或使用 `System.Resources.Extensions` 包的源生成器。 2. **OOBE 首次运行**:OOBE 在首次运行时 `settings.json` 不存在,此时 `LanguagePreferenceService` 会回退到 `zh-CN`。这是合理的行为。 3. **`x:Static` 与 Avalonia CompiledBindings**:项目启用了 `AvaloniaUseCompiledBindingsByDefault`,需要确认 `x:Static` 在编译绑定模式下正常工作。如有问题,可在特定 AXAML 文件中添加 `x:CompileBindings="False"`。 4. **RESX Designer.cs 生成**:确保 `.csproj` 中使用 `PublicResXFileCodeGenerator` 生成 `public` 类,否则 `x:Static` 无法访问。 5. **隐私政策多语言**:隐私政策 Markdown 正文较长,放入 RESX 可能影响可读性。可考虑保留在 C# 中或使用独立资源文件。