Files
Unlucky-Launcher/crates/i18n/src/catalog/content.rs
T
AskaEth b394316df4 fix(m162-r1): 叶子成因穿出引擎 + 安装路径切 core 在线源 + 详情迟到结果闸门
P1-1(p1-1items 的根因):
- download/error.rs:`DownloadError` 增 `leaf: Option<DownloadErrorKind>` 与
  `effective_kind()`;`leaf_of()` 汇总规则只此一处:全 404 ⇒ NotFound、有
  Network/Timeout ⇒ Network、全 5xx ⇒ ServerError、混合按「网络错 > 5xx > 404」
  取最可诊断的那个(**不从 suppressed 文本猜状态码**)。
- download/bytes.rs:候选源循环收集逐源 kind,收尾时把叶子挂到 AllSourcesFailed 上。
- online/transport.rs:`From<DownloadError>` 改用 `effective_kind()`,于是
  NotFound/Server/Integrity 在生产路径可达(项目被删/id 拼错/限速不再画成「未联网」)。
- import/fetch.rs:新增 `FetchErrorKind::Offline`(真正的「没联网」)并与 Network 分开;
  `From<DownloadError>`/`From<OnlineError>` 都按叶子/大类映射。
- import/complete.rs:补全只在 `Offline` 时早停;404(真实 mrpack 大多托管在 CF CDN,
  Modrinth 必然 404)与 5xx 只影响那一条,不再掐掉整轮;注记「因未联网提前停止」因此
  只会在真断网时出现。

P2-1:presenter 安装路径不再自己拼 Modrinth URL / 自己解析 JSON——改用
`core::addon::online::Modrinth::versions` + `primary_file()` + `VersionFile::sha1/sha512`;
`HostTask::InstallDone.result` 改成 `Result<String, ContentFailure>`,
`InstallProgressView` 增 `failure_kind`(离线 ≠ 查无此项)。

P2-2:`app/update/content.rs` 把被详情方法抢走的「装入闸门」注释还回
`content_install_request`,详情方法只留自己那一行。

P2-3:`ContentState::apply_detail/fail_detail` 带上 project(事件与 HostTask 都携带请求时的
项目 id),迟到结果一律丢弃;`request_detail` 记下在飞项目,`detail_pending` 不再被迟到
结果清掉。

顺手项:`usable_hash` 让形状非法的那一档摘要被**丢弃**(sha512 仍校验,不再整条跳过;
两档都不可用才拒绝);`digest_file` 的 IO 失败先删落盘文件再返回。i18n 的
ContentOfflineBody 改成不暗示「有缓存」的措辞(golden 同步)。

用例:download/error.rs 叶子四档 + effective_kind;online/tests/classify.rs 5 条
(AllSourcesFailed+leaf 404 ⇒ NotFound、无叶子 ⇒ NotConnected 保守、叶子一一映射、
单源自身 kind、导入侧 404≠Offline);import_online 增「404 不停」「真离线才停」
(共 7 条);download 增「sha1 形状非法仍用 sha512 校验成功」;presenter 增详情迟到闸门。
2026-11-07 17:28:36 +08:00

332 lines
18 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 内容库文案(M109 `CONTENTDL`)。
//!
//! 一句话一个 key:带变量的句子用 `{name}`,页面只做 [`crate::Catalog::render`],不拼片段。
//!
//! ## 三条口径
//!
//! 1. **只说能做的事**:整合包那条写的是「要等导入管线」,不是「稍后自动支持」;
//! 2. **站名也进词表**:`Modrinth` / `CurseForge` 的**中文译文必须与英文不同**
//! (`catalog/tests.rs` 的 `chinese_catalog_is_actually_chinese` 会红),所以中文写成「Modrinth 源」;
//! 3. **禁用态要能解释自己**:`ContentIncompatible*` / `ContentModpackNotReady` 就是禁用按钮旁边那几句话——
//! 没有它们,禁用就等于「点了没反应」。
//!
//! 域文件的形状见 `crate::catalog` 的「域装配」一节。
macro_rules! content_entries {
($next:ident, $($rest:ident,)* { $($acc:tt)* }) => {
$next! { $($rest,)* {
$($acc)*
// --- content library page (M109) ---
ContentTitle => ("Content", "内容库"),
ContentSubtitle => (
"Modpacks, mods, resource packs and shaders",
"整合包、Mod、资源包与光影"
),
ContentSectionVersions => ("Version management", "版本管理"),
ContentSectionContent => ("Content library", "内容库"),
ContentKindModpack => ("Modpacks", "整合包"),
ContentKindMod => ("Mods", "Mod"),
ContentKindResourcepack => ("Resource packs", "资源包"),
ContentKindShader => ("Shaders", "光影"),
ContentSourceModrinth => ("Modrinth", "Modrinth 源"),
ContentSourceCurseforge => ("CurseForge", "CurseForge 源"),
ContentSortRelevance => ("Relevance", "相关度"),
ContentSortDownloads => ("Downloads", "下载量"),
ContentSortNewest => ("Newest", "最新发布"),
ContentSortUpdated => ("Recently updated", "最近更新"),
ContentSearchPlaceholder => ("Search content…", "搜索内容…"),
ContentSearch => ("Search", "搜索"),
ContentFilterVersion => ("Game version", "游戏版本"),
ContentFilterLoader => ("Loader", "加载器"),
ContentFilterAny => ("Any", "不限"),
ContentCount => ("{count} results", "{count} 个结果"),
ContentLoading => ("Loading content…", "正在加载内容…"),
ContentLoadingHint => (
"Results come from the network; nothing is cached offline yet.",
"结果来自网络,暂时没有离线缓存。"
),
ContentEmptyTitle => ("Nothing matched", "没有匹配的内容"),
ContentEmptyBody => (
"Try another keyword, or clear a filter.",
"换个关键词,或清掉一个筛选条件。"
),
ContentFailedTitle => ("Could not load content", "内容加载失败"),
ContentFailedBody => ("Check the network, then retry.", "检查网络后重试。"),
ContentOfflineTitle => ("No connection", "未联网"),
ContentOfflineBody => (
"Content search needs the network. Check your connection, then retry.",
"内容检索需要联网。检查网络后重试。"
),
ContentRetry => ("Retry", "重试"),
ContentLoadMore => ("Load more", "加载更多"),
ContentNoMore => ("No more results", "没有更多了"),
ContentDetailEmpty => (
"Pick an item to see what it supports.",
"选一个项目看它支持什么。"
),
ContentDetailDownloads => ("{count} downloads", "下载量 {count}"),
ContentDetailUpdated => ("Updated {date}", "更新于 {date}"),
ContentDetailLoaders => ("Loaders: {list}", "加载器:{list}"),
ContentDetailVersions => ("Game versions: {list}", "游戏版本:{list}"),
ContentDetailFetching => ("Loading versions…", "正在取版本列表…"),
ContentDetailFetchFailed => (
"Could not load versions: {reason}",
"版本列表取不到:{reason}"
),
ContentDetailVersionCount => ("{count} versions", "{count} 个版本"),
ContentDetailVersionLine => ("{number} · {kind} · {date}", "{number}({kind})· {date}"),
ContentInstall => ("Install into instance", "装入实例"),
ContentTargetInstance => ("Target instance", "目标实例"),
ContentNoInstances => ("No instance to install into", "没有可装入的实例"),
ContentIncompatibleVersion => (
"Needs {version}; this instance runs {actual}",
"需要 {version},该实例运行的是 {actual}"
),
ContentIncompatibleLoader => ("Not built for {loader}", "不支持 {loader}"),
ContentModpackNotReady => (
"Installing modpacks from the library into an instance is not available yet",
"从内容库一键安装整合包到实例还没做(本地的包可以直接导入)"
),
ContentSourceUnavailable => (
"CurseForge needs an API key: add one in Settings",
"CurseForge 需要在设置里配置密钥才能搜索"
),
ContentSourceNotWired => (
"Installing from CurseForge is not available yet",
"CurseForge 来源的安装尚未开放"
),
// --- M113: pack import / export(PACKUI)---
PackTitle => ("Modpack import & export", "整合包导入 / 导出"),
PackImportTitle => ("Import a modpack", "导入整合包"),
PackDropHint => (
"Drop a .mrpack, .zip or a MultiMC/Prism instance folder here",
"把 .mrpack / .zip 或 MultiMC·Prism 实例目录拖到这里"
),
PackDropActive => ("Release to probe the pack", "松手开始探测"),
PackNoDialog => (
"This build has no system file picker: drag the file in, or paste its path below.",
"本版没有系统文件选择框:请把文件拖进来,或在下面粘贴路径。"
),
PackPathLabel => ("Pack path", "整合包路径"),
PackPathPlaceholder => (
"D:\\packs\\Better MC.mrpack",
"D:\\整合包\\更好的MC.mrpack"
),
PackImportAction => ("Import", "导入"),
PackCancel => ("Cancel import", "取消导入"),
PackDismiss => ("Dismiss", "关闭"),
PackTargetLabel => ("Install into", "装到实例"),
PackTargetNew => ("New instance", "新建实例"),
PackInstanceName => ("Instance name", "实例名"),
PackStageLine => ("Step {index}/{total} · {stage}", "步骤 {index}/{total} · {stage}"),
PackStageProbe => ("Reading the manifest", "读取清单"),
PackStageExtract => ("Unpacking overrides", "解包覆盖内容"),
PackStageFetch => ("Fetching missing files", "补下载文件"),
PackStageCommit => ("Writing files", "落地写盘"),
PackStageRegister => ("Registering the instance", "登记实例"),
PackProbeTitle => (
"This pack is almost a known format",
"这个包差一点就是已知格式"
),
PackProbeLooksLike => (
"Looks like {format}, but missing: {missing}",
"看起来像 {format},但缺:{missing}"
),
PackProbeUnknown => (
"No known format claims this pack",
"没有任何已知格式认领这个包"
),
PackProbeSaw => ("Found in the pack: {saw}", "包里看到:{saw}"),
PackProbeNothing => ("nothing recognisable", "没有可识别的东西"),
PackStatusSuccess => ("Imported", "已导入"),
PackStatusPartial => ("Imported with gaps", "导入完成,但有缺口"),
PackStatusFailed => ("Import failed — rolled back", "导入失败 —— 已整包回滚"),
PackReportWritten => ("Files written: {count} · {bytes}", "{count} 个文件已写盘 · {bytes}"),
PackReportMissing => (
"Could not fetch (tolerated): {count}",
"有 {count} 个文件没搞到(可容忍)"
),
PackReportConflicts => (
"Conflicts: {count}",
"冲突:{count} 个"
),
PackReportRejected => (
"The client does not support these: {count}",
"这些组件客户端不支持:{count} 个"
),
PackReportRejectedNote => ("not supported by the client", "客户端不支持"),
PackCancelled => (
"Import keeps running in the background — this result is discarded. Files already written stay.",
"导入会在后台继续跑完 —— 本次结果作废;已经写下的文件会留下。"
),
PackOverwriteAction => ("Re-import, the pack wins", "以包为准重新导入"),
PackOverwriteNote => (
"Keeping both (renamed) is not supported yet: for each existing file you pick either “keep” or “overwrite”.",
"「两者都留(改名)」暂不支持:每个已存在的文件只能选「保留」或「覆盖」。"
),
PackMissingOffline => ("offline import", "离线导入"),
PackMissingNoSource => (
"no download source available yet",
"暂时拿不到下载地址"
),
PackMissingFetchFailed => ("download failed: {detail}", "下载失败:{detail}"),
PackFatalTitle => (
"Rolled back — nothing was left behind",
"已回滚 —— 目标目录零残留"
),
PackFatalHint => ("What to try next", "下一步可以试"),
PackConflictUserModified => ("you changed it", "你改过"),
PackConflictUserDeleted => ("you deleted it", "你删过"),
PackConflictPreExisting => ("was already there", "本来就在"),
PackChoiceKeepUser => ("kept yours", "保留我的"),
PackChoiceOverwrite => ("pack version used", "用包里的"),
PackConflictHashes => (
"pack {pack} · base {base} · disk {disk}",
"包内 {pack} · 基线 {base} · 磁盘 {disk}"
),
PackConflictDetail => ("Hashes", "三方哈希"),
PackPickTitle => (
"Before importing: {count} files already exist with different content",
"导入前:有 {count} 个文件已存在且内容不同"
),
PackPickSummary => (
"Keep {keep} · overwrite {overwrite} · overwriting discards {bytes}",
"保留 {keep} · 覆盖 {overwrite} · 覆盖将丢弃 {bytes}"
),
PackPickKeepAll => ("Keep all mine", "全部保留我的"),
PackPickOverwriteAll => ("Overwrite all", "全部覆盖"),
PackPickRowKeep => ("Keep", "保留"),
PackPickRowOverwrite => ("Overwrite", "覆盖"),
PackPickPage => ("Items {from}-{to} of {total}", "第 {from}-{to} 项,共 {total} 项"),
PackPickPrev => ("Previous", "上一页"),
PackPickNext => ("Next", "下一页"),
PackPickConfirm => ("Import with these choices", "按这些选择导入"),
PackPickCancel => ("Cancel import", "取消导入"),
PackPickDefaultNote => (
"Anything you do not change stays as you have it: file content is only replaced where you pick “Overwrite”.",
"没有改动的一律按「保留」:只有你点了「覆盖」的文件才会被包内内容替换。"
),
PackPickUnknownRemote => (
"{count} remote entries only learn their path after download; they follow “keep”.",
"还有 {count} 条远端条目要下载后才知道路径,一律按「保留」处置。"
),
PackPickSourceOverrides => ("in the pack (overrides/)", "包内 overrides/"),
PackPickSourceClientOverrides => ("in the pack (client-overrides/)", "包内 client-overrides/"),
PackPickSourceManifest => ("manifest files[]", "清单 files[]"),
PackPickDisk => ("yours: {size}", "你的:{size}"),
PackPickIncoming => ("pack: {size}", "包内:{size}"),
PackFormatModrinth => ("Modrinth .mrpack", "Modrinth 的 .mrpack"),
PackFormatCurseforge => ("CurseForge manifest", "CurseForge 清单"),
PackFormatMultimc => ("MultiMC / Prism instance", "MultiMC·Prism 实例"),
PackExportTitle => ("Export this instance", "导出这个实例"),
PackExportOpen => ("Export…", "导出…"),
PackExportFormat => ("Format", "格式"),
PackExportName => ("Pack name", "包名"),
PackExportVersion => ("Version", "版本"),
PackExportOutput => ("Save to", "保存到"),
PackExportOutputPlaceholder => (
"empty = suggested path",
"留空 = 自动给一个路径"
),
PackExportPrecheck => ("Dry run", "预检"),
PackExportConfirm => ("Write the pack", "确认写盘"),
PackExportAbort => ("Back", "返回"),
PackExportMultimcNote => (
"MultiMC/Prism can only be imported, not exported (by design).",
"MultiMC·Prism 只能导入、不能导出(有意省略)。"
),
PackExportDone => ("Wrote {path} · {bytes}", "已写出 {path} · {bytes}"),
PackExportFailed => ("Export failed: {reason}", "导出失败:{reason}"),
PackExportPending => ("Scanning the instance…", "正在扫描实例…"),
PackPreviewHead => (
"Will inline {inline} files · cannot export: {skipped}",
"将内联 {inline} 个文件 · 无法导出 {skipped} 项"
),
PackPreviewRemote => (
"{count} files can be referenced remotely",
"{count} 个文件可走远程引用"
),
PackPreviewInlineBytes => (
"{bytes} copied into overrides/",
"{bytes} 会原样拷进 overrides/"
),
PackPreviewInlineTitle => (
"Copied into overrides/ (first {shown}):",
"原样拷进 overrides/(只列前 {shown} 个):"
),
PackPreviewSkippedTitle => (
"Not in the pack: {count}",
"没进包:{count} 项"
),
PackPreviewNotes => ("Known losses", "已知损失"),
PackPreviewTarget => ("Will be written to {path}", "将写到 {path}"),
PackPreviewMore => (
"…and {count} more files (not shown here)",
"…还有 {count} 个文件(这里不展开)"
),
PackPreviewComplete => ("Nothing is left out", "没有遗漏任何文件"),
PackPreviewIncomplete => (
"Some files will not be in the pack",
"有文件不会进包"
),
PackReasonModrinthSha1 => ("Modrinth match (SHA-1)", "Modrinth 命中(SHA-1)"),
PackReasonCurseFingerprint => (
"CurseForge match (fingerprint)",
"CurseForge 命中(文件指纹)"
),
PackReasonNoRemoteMatch => (
"unknown to both catalogs, inlined",
"两个目录都不认识,已内联"
),
PackReasonCurseOnly => (
"only CurseForge knows it, this format cannot use it, inlined",
"只有 CF 认识,目标格式用不上,已内联"
),
PackReasonModrinthOnly => (
"only Modrinth knows it, the CF manifest cannot use it, inlined",
"只有 Modrinth 认识,CF 清单用不上,已内联"
),
PackReasonMissingSha512 => (
"the catalogue entry has no sha512 hash, inlined",
"命中缺 sha512,已内联"
),
PackReasonMissingCurseIds => (
"the catalogue entry has no CurseForge ids, inlined",
"命中缺 projectID/fileID,已内联"
),
PackReasonDisabled => (
"disabled file, this format cannot express it (.disabled kept)",
"禁用文件,目标格式表达不了(保留 .disabled)"
),
PackReasonSymlink => ("symbolic link, skipped", "符号链接,已跳过"),
PackReasonUnsafePath => (
"path fails the pack path rules, skipped",
"路径过不了包内规整,已跳过"
),
PackReasonUnreadable => ("cannot read it, skipped", "读不出来,已跳过"),
// --- M161:导入结果的两种失败**不许共用一句话** ---
// 「预检没通过(一个字节都没写)」与「写进去之后回滚完成」是两件事:
// 前者用户去修包/修配置再试,后者说明已经动过目标目录。此前两者共用
// `PackStatusFailed`("Import failed — rolled back"),预检失败时那句话是假的。
PackStatusFailedNotStarted => (
"Import failed — nothing was written",
"导入失败 —— 还没写任何文件"
),
PackStatusFailedIncomplete => (
"Import failed — rollback did not finish",
"导入失败 —— 回滚没做完"
),
PackFatalTitleNotStarted => (
"Preflight failed — nothing was written",
"预检未通过 —— 一个字节都没写"
),
PackFatalTitleIncomplete => (
"Rollback did not finish — the target folder may differ",
"回滚没做完 —— 目标目录可能与导入前不同"
),
} }
};
}
pub(crate) use content_entries;