//! 实例页的**文案入口**:页面上一处可见英文字面量都不留,全部从目录取。 //! //! ## 为什么要有这一层(而不是在控件树里直接 `catalog.text(..)`) //! //! 页面要的文案有 20 多条,直接写的话每个调用点都是「key 名 + 这一处画什么」的重复劳动, //! 而且**守门测试没法取样**:iced 0.14 的 `Element` 没有内省接口(`instances/tests.rs` 里 //! 「读源码」那条注释解释过同一件事),所以「上屏文本」只能从**页面取词的那一个入口**拿。 //! 于是所有取词收进 [`Texts`],页面用 `texts.xxx()` 画,测试用同一个 `Texts` 断言—— //! 一处定义、两处消费,不存在「测试抄了一份平行实现」。 //! //! ## 它抓得住什么 / 抓不住什么 //! //! - **抓得住**:目录里某条中文忘了翻(「无 ASCII 英文单词」)、某条中英写成一样(「切换语言文本真的变」)、 //! `Running · pid {pid}` 的数字与分隔符被翻译走样、页面里写死**英文**字面量 //! (`every_english_literal_in_the_page_sources_is_whitelisted`,白名单判定); //! - **抓不住**:页面里写死的**中文**字面量(没有 ASCII 词,扫不到)、`PAGE_SOURCES` 五个文件 //! 之外的东西(如 `data.rs` 夹具数据)、译文翻得顺不顺——这些只能靠出图人工看 //! (`DevDocs/artifacts/m120/`)。 //! //! ## 口径 //! //! 品牌名不翻译(`Java` / `Fabric` / `Forge` 原样);`pid` 是进程号的技术标识,两套语言都写 //! `pid`(改成「进程」会丢掉「拿它去任务管理器里搜」这条可操作性)。 //! //! **启动阶段(下载中 / 校验中 / …)不在这张表里**:它在 `app/view/instances.rs` 拼进 //! `Row.status`(「阶段 · 百分比」是一整句),与元信息卡同一层——页面只画模型给的字符串。 use unlucky_i18n::{Catalog, Key}; use unlucky_presenter::dto::{ModFailureKind, SavesFailureKind}; /// 实例页 + 详情列的文案(一个 `Catalog` 的薄包装,`Copy`)。 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) struct Texts(Catalog); impl Texts { /// 从目录取一套(调用方给 `model.lang.catalog()`)。 pub(super) fn of(catalog: Catalog) -> Self { Self(catalog) } // ------------------------------------------------------------- 页头 pub(super) fn title(self) -> &'static str { self.text(Key::InstancesTitle) } pub(super) fn subtitle(self) -> &'static str { self.text(Key::InstancesSubtitle) } pub(super) fn new_instance(self) -> &'static str { self.text(Key::InstancesNew) } pub(super) fn import(self) -> &'static str { self.text(Key::InstancesImport) } // --------------------------------------------------------------- 列表列 pub(super) fn all_title(self) -> &'static str { self.text(Key::InstancesAllTitle) } // --------------------------------------------------------------- 详情列 pub(super) fn back(self) -> &'static str { self.text(Key::InstancesBack) } pub(super) fn play(self) -> &'static str { self.text(Key::InstancesPlay) } pub(super) fn stop(self) -> &'static str { self.text(Key::InstancesStop) } pub(super) fn duplicate(self) -> &'static str { self.text(Key::InstancesDuplicate) } pub(super) fn open_folder(self) -> &'static str { self.text(Key::InstancesOpenFolder) } pub(super) fn delete(self) -> &'static str { self.text(Key::InstancesDelete) } pub(super) fn empty_title(self) -> &'static str { self.text(Key::InstancesEmptyTitle) } pub(super) fn empty_body(self) -> &'static str { self.text(Key::InstancesEmptyBody) } pub(super) fn narrow_note(self) -> &'static str { self.text(Key::InstancesNarrowNote) } // ------------------------------------------------------------- 启动参数(M157) pub(super) fn launch_title(self) -> &'static str { self.text(Key::InstancesLaunchTitle) } pub(super) fn launch_jvm(self) -> &'static str { self.text(Key::InstancesLaunchJvm) } pub(super) fn launch_jvm_hint(self) -> &'static str { self.text(Key::InstancesLaunchJvmHint) } pub(super) fn launch_game(self) -> &'static str { self.text(Key::InstancesLaunchGame) } pub(super) fn launch_game_hint(self) -> &'static str { self.text(Key::InstancesLaunchGameHint) } pub(super) fn launch_memory(self) -> &'static str { self.text(Key::InstancesLaunchMemory) } /// 「留空 = 跟随全局默认({default} MiB)」,`default` 是生效值(三级回落之后)。 pub(super) fn launch_memory_hint(self, default_mb: u32) -> String { self.0.render( Key::InstancesLaunchMemoryHint, &[("default", &default_mb.to_string())], ) } pub(super) fn launch_java(self) -> &'static str { self.text(Key::InstancesLaunchJava) } pub(super) fn launch_java_hint(self) -> &'static str { self.text(Key::InstancesLaunchJavaHint) } pub(super) fn launch_backend(self) -> &'static str { self.text(Key::InstancesLaunchBackend) } /// 「需游戏版本 {min} 及以上(本实例 {version})」。 /// /// `min` 来自 DTO(`LaunchFacts::min_version` ← core 的 `GRAPHICS_BACKEND_MIN_VERSION`), /// **不在目录里硬编码版本号**——否则 core 一改门槛,文案会继续骗人(r4 复审 P3-c)。 pub(super) fn launch_backend_gated(self, version: &str, min: &str) -> String { self.0.render( Key::InstancesLaunchBackendGated, &[("version", version), ("min", min)], ) } /// 「生效:vulkan」。 pub(super) fn launch_effective(self, value: &str) -> String { self.0 .render(Key::InstancesLaunchEffective, &[("value", value)]) } /// 三态后端的显示名(稳定 id → 文案);**未知 id 返回 `None`**(调用方原样上屏,不猜)。 pub(super) fn backend_label(self, id: &str) -> Option<&'static str> { Some(self.text(match id { "default" => Key::InstancesBackendDefault, "opengl" => Key::InstancesBackendOpengl, "vulkan" => Key::InstancesBackendVulkan, _ => return None, })) } pub(super) fn launch_save(self) -> &'static str { self.text(Key::InstancesLaunchSave) } pub(super) fn launch_reset(self) -> &'static str { self.text(Key::InstancesLaunchReset) } // ------------------------------------------------------------- MODS 区(M224) pub(super) fn mods_title(self) -> &'static str { self.text(Key::InstancesModsTitle) } /// 「12 启用 · 3 禁用」。 pub(super) fn mods_count(self, enabled: usize, disabled: usize) -> String { self.0.render( Key::InstancesModsCount, &[ ("enabled", &enabled.to_string()), ("disabled", &disabled.to_string()), ], ) } pub(super) fn mods_loading(self) -> &'static str { self.text(Key::InstancesModsLoading) } pub(super) fn mods_empty(self) -> &'static str { self.text(Key::InstancesModsEmpty) } pub(super) fn mods_missing_dir(self) -> &'static str { self.text(Key::InstancesModsMissingDir) } pub(super) fn mods_running(self) -> &'static str { self.text(Key::InstancesModsRunning) } pub(super) fn mods_old_badge(self) -> &'static str { self.text(Key::InstancesModsOldBadge) } /// 「跳过 3 项」。 pub(super) fn mods_skipped(self, count: usize) -> String { self.0 .render(Key::InstancesModsSkipped, &[("count", &count.to_string())]) } /// 读目录失败的主句:`读取 mods/ 失败:没有可用的 mods/ 目录`。 pub(super) fn mods_failed(self, reason: &str) -> String { self.0 .render(Key::InstancesModsFailed, &[("reason", reason)]) } /// 启停失败的主句:`启停 x.jar 失败:同名文件已存在`。 pub(super) fn mods_toggle_failed(self, file: &str, reason: &str) -> String { self.0.render( Key::InstancesModsToggleFailed, &[("file", file), ("reason", reason)], ) } /// 启停失败大类的短句(`ModFailureKind` → 一句话;未知取值不猜,原样上屏)。 pub(super) fn mods_reason(self, kind: ModFailureKind) -> &'static str { self.text(match kind { ModFailureKind::Running => Key::InstancesModsReasonRunning, ModFailureKind::InstanceUnavailable => Key::InstancesModsReasonInstance, ModFailureKind::UnsafeName => Key::InstancesModsReasonName, ModFailureKind::ModsDirUnusable => Key::InstancesModsReasonDirectory, ModFailureKind::NotFound => Key::InstancesModsReasonMissing, ModFailureKind::NotARegularFile => Key::InstancesModsReasonNotFile, ModFailureKind::Collision => Key::InstancesModsReasonCollision, ModFailureKind::Rename => Key::InstancesModsReasonRename, }) } // ------------------------------------------------------------- 存档区(M235) pub(super) fn saves_title(self) -> &'static str { self.text(Key::InstancesSavesTitle) } /// 「3 个世界 · 12 张截图」。 pub(super) fn saves_count(self, worlds: usize, shots: usize) -> String { self.0.render( Key::InstancesSavesCount, &[ ("worlds", &worlds.to_string()), ("shots", &shots.to_string()), ], ) } pub(super) fn saves_loading(self) -> &'static str { self.text(Key::InstancesSavesLoading) } pub(super) fn saves_empty(self) -> &'static str { self.text(Key::InstancesSavesEmpty) } pub(super) fn saves_missing_dir(self) -> &'static str { self.text(Key::InstancesSavesMissingDir) } pub(super) fn saves_worlds(self) -> &'static str { self.text(Key::InstancesSavesWorlds) } pub(super) fn saves_screenshots(self) -> &'static str { self.text(Key::InstancesSavesScreenshots) } pub(super) fn saves_no_worlds(self) -> &'static str { self.text(Key::InstancesSavesNoWorlds) } pub(super) fn saves_no_screenshots(self) -> &'static str { self.text(Key::InstancesSavesNoScreenshots) } /// 「有图标」:只判 `icon.png` 存不存在(不解码,缩略图墙不在 U3 范围内)。 pub(super) fn saves_world_icon(self) -> &'static str { self.text(Key::InstancesSavesWorldIcon) } pub(super) fn saves_time_unknown(self) -> &'static str { self.text(Key::InstancesSavesTimeUnknown) } /// 「跳过 3 项」。 pub(super) fn saves_skipped(self, count: usize) -> String { self.0 .render(Key::InstancesSavesSkipped, &[("count", &count.to_string())]) } pub(super) fn saves_open_folder(self) -> &'static str { self.text(Key::InstancesSavesOpenFolder) } pub(super) fn saves_retry(self) -> &'static str { self.text(Key::InstancesSavesRetry) } /// 读目录失败的主句:`读取 saves/ 失败:目录不可用`。 pub(super) fn saves_failed(self, reason: &str) -> String { self.0 .render(Key::InstancesSavesFailed, &[("reason", reason)]) } /// 打开文件夹失败的主句:`打开文件夹失败:文件管理器没起来`。 pub(super) fn saves_open_failed(self, reason: &str) -> String { self.0 .render(Key::InstancesSavesOpenFailed, &[("reason", reason)]) } /// 失败大类的短句(`SavesFailureKind` → 一句话;写操作那八类各有自己的话)。 pub(super) fn saves_reason(self, kind: SavesFailureKind) -> &'static str { self.text(match kind { SavesFailureKind::DirUnusable => Key::InstancesSavesReasonDir, SavesFailureKind::InstanceUnavailable => Key::InstancesSavesReasonInstance, SavesFailureKind::OpenFailed => Key::InstancesSavesReasonOpen, SavesFailureKind::WriteUnsafeName => Key::InstancesSavesReasonUnsafeName, SavesFailureKind::WriteSymlink => Key::InstancesSavesReasonSymlink, SavesFailureKind::WriteMissing => Key::InstancesSavesReasonMissing, SavesFailureKind::WriteNotADirectory => Key::InstancesSavesReasonNotDir, SavesFailureKind::WriteConflict => Key::InstancesSavesReasonConflict, SavesFailureKind::WriteBoundary => Key::InstancesSavesReasonBoundary, SavesFailureKind::WriteTooLarge => Key::InstancesSavesReasonTooLarge, SavesFailureKind::GameRunning => Key::InstancesSavesReasonRunning, SavesFailureKind::WriteFailed => Key::InstancesSavesReasonWrite, }) } // ------------------------------------------------- 存档写操作(M247 · SAVESOPS) pub(super) fn saves_rename(self) -> &'static str { self.text(Key::InstancesSavesRename) } pub(super) fn saves_backup(self) -> &'static str { self.text(Key::InstancesSavesBackup) } pub(super) fn saves_delete(self) -> &'static str { self.text(Key::InstancesSavesDelete) } /// 行内输入框的占位符(不是按钮文案:它说「该填什么」)。 pub(super) fn saves_rename_placeholder(self) -> &'static str { self.text(Key::InstancesSavesRenamePlaceholder) } pub(super) fn saves_rename_confirm(self) -> &'static str { self.text(Key::InstancesSavesRenameConfirm) } pub(super) fn saves_cancel(self) -> &'static str { self.text(Key::InstancesSavesCancel) } /// 「正在处理…」:写请求在飞时顶替那一行的操作区。 pub(super) fn saves_busy(self) -> &'static str { self.text(Key::InstancesSavesBusy) } pub(super) fn saves_delete_title(self) -> &'static str { self.text(Key::InstancesSavesDeleteTitle) } /// 删除确认框的正文:`{dir} 和它里面的全部内容都会被删除,且不能撤销。` pub(super) fn saves_delete_body(self, dir: &str) -> String { self.0 .render(Key::InstancesSavesDeleteBody, &[("dir", dir)]) } pub(super) fn saves_delete_confirm(self) -> &'static str { self.text(Key::InstancesSavesDeleteConfirm) } /// 改名成功的回执:`已把 A 改名为 B。` pub(super) fn saves_renamed(self, from: &str, to: &str) -> String { self.0 .render(Key::InstancesSavesRenamed, &[("from", from), ("to", to)]) } /// 删除成功的回执:`已删除 A。` pub(super) fn saves_deleted(self, dir: &str) -> String { self.0.render(Key::InstancesSavesDeleted, &[("dir", dir)]) } /// 备份成功的回执:`已把 A 备份到 D:/…/A-…zip(1.2 MiB)。` pub(super) fn saves_backed_up(self, dir: &str, path: &str, size: &str) -> String { self.0.render( Key::InstancesSavesBackedUp, &[("dir", dir), ("path", path), ("size", size)], ) } /// 「跳过 N 个符号链接」(备份时没跟随的那些)。 pub(super) fn saves_backup_skipped(self, count: usize) -> String { self.0.render( Key::InstancesSavesBackupSkipped, &[("count", &count.to_string())], ) } /// 重命名失败的主句:`重命名 A 失败:同名世界已存在`。 pub(super) fn saves_rename_failed(self, file: &str, reason: &str) -> String { self.0.render( Key::InstancesSavesRenameFailed, &[("file", file), ("reason", reason)], ) } /// 删除失败的主句。 pub(super) fn saves_delete_failed(self, file: &str, reason: &str) -> String { self.0.render( Key::InstancesSavesDeleteFailed, &[("file", file), ("reason", reason)], ) } /// 备份失败的主句。 pub(super) fn saves_backup_failed(self, file: &str, reason: &str) -> String { self.0.render( Key::InstancesSavesBackupFailed, &[("file", file), ("reason", reason)], ) } fn text(self, key: Key) -> &'static str { self.0.text(key) } /// 这一页的全部 key(守门测试的取样口:**从 `Key::ALL` 过滤出来**,不是手抄一份清单)。 /// /// 域判据是前缀 `Instances`——本族的 key 名是契约(`catalog/instances.rs` 里一条一条写着), /// 新加的 key 只要还在这个族里就自动进守门范围,不需要有人记得来登记。 #[cfg(test)] pub(super) fn keys() -> impl Iterator { Key::ALL .iter() .copied() .filter(|key| key.name().starts_with("Instances")) } }