merge(dev): 解 M196 与 dev(M179/M193)在 settings/mod.rs 的冲突

`origin/main` 是 D41 的孤儿快照(单提交、与开发史无共同祖先),M179/M193 这些提交在
`origin/dev` 上;按「跟上主线」的意图合的是 `origin/dev`。用 merge 不用 rebase:复审 clean 的
tip `e5a4b9b` 原样保留,本提交只负责解冲突。

唯一冲突:`settings/mod.rs` 里 `form` 的文档注释(M179 与 M195 重写过同一段)。两边都留:
- **M179/M193**:`type Body` 带 `Lang`、四条按下标分发(General / Update / Assistant / Java)、
  M193 的 download / logs 接线及其文档更新——只有一处措辞随之调整:「四组在下面各有 `else if`」
  → 「在 [`section_columns`] 里各有 `else if`」,因为 M195 把那层循环抽成了独立函数;
- **M195**:循环之后不再 push 那张「没有逐实例覆盖」的空态卡、`overrides_empty` 不存在、
  `section_columns` 与其行为断言(`frame.rs` 两条用例)。
合并后的注释同时说明两件事:`GROUP.body` 从形参拿生效语言(M179)+ 尾部不许再挂东西、
逐实例覆盖的真控件在实例详情页(M195)。

为守住 400 行硬线(冲突态 424 → 399),只压自己的重复叙述:M196 段压到 4 行、
`section_columns` 的文档改为指向 `form`(它原来重复了一遍分发点,且在 M179 之后已不准确)、
模块文档里一段被 M179 实现取代的历史「r3 订正」压成一句可执行的结论。

代码零语义改动(证据见汇报):`section_columns` 的循环体与 `origin/dev` 归一化后逐字节相同(46 行),
`GROUPS` / `GROUP_LABELS` 三方一致;相对 `origin/dev` 的差异只剩本 mission 的 4 个文件。
This commit is contained in:
AskaEth committed 2026-11-09 05:18:29 +08:00
commit fb0b1a5311
274 files changed
+24717 -1998

No files matched your search

+62 -1
View File
@@ -57,7 +57,9 @@ macro_rules! account_entries {
AccountDeviceRetry => ("Try again", "重试"),
AccountDeviceCancel => ("Cancel", "取消"),
AccountDeviceBrowser => ("Sign in with browser", "用浏览器登录"),
AccountDeviceUseCode => ("Use a device code instead", "改用设备码"),
// M178 复审 r1 P2-5:这是**主按钮**(设备码流),不能读起来像备选。
AccountDeviceSignIn => ("Sign in with your Microsoft account", "使用 Microsoft 账户登录"),
AccountDeviceFallbackHint => ("No browser handy? This way shows a code you can type on any device.", "浏览器打不开就用这个:它会给一串用户码,在任何设备上输入即可。"),
// ── 第三方站表单
AccountServerLabel => ("Server address", "服务器地址"),
AccountServerHint => ("API root, e.g. https://littleskin.cn/api/yggdrasil/", "API 根,例:https://littleskin.cn/api/yggdrasil/"),
@@ -115,6 +117,23 @@ macro_rules! account_entries {
// ── 重新登录的引导
AccountHintAuthlibReauth => ("Enter the password again for {server}.", "请输入 {server} 的密码。"),
AccountHintOfflineNoRelogin => ("Local profiles have no server account to sign in again — create a new profile instead.", "本地档案没有可重新登录的服务器账户,请新建一个档案。"),
// ── 浏览器登录回退到设备码的说明(M178)
// 口径:这不是「登录失败」——设备码接着跑。第一句说清「已改用设备码」,
// 需要改配置的那一条**必须给可照抄的下一步**(哪个门户、加什么值、端口不参与匹配),
// 且**不许**出现 AAD 的原始报文(`invalid_request` / `AADSTS…` / JSON)——
// 用户 2026-11-08 实测撞到的就是那张原始错误页。
AccountBrowserFallbackNotRegistered => (
"This build's app registration has no loopback redirect URI, so browser sign-in cannot work; switched to a device code. To fix: add http://localhost/auth under Redirect URIs in the Azure app registration (the port is ignored), then restart the launcher.",
"这个构建的应用注册里没有登记回环重定向 URI,浏览器登录用不了,已自动改用设备码。怎么修:在 Azure 应用注册的「重定向 URI」里加上 http://localhost/auth(端口不参与匹配),然后重启启动器。"
),
AccountBrowserFallbackUnverified => (
"Could not confirm that browser sign-in is usable, so it switched to a device code.",
"没能确认浏览器登录是否可用(预检没成功),已自动改用设备码。"
),
AccountBrowserFallbackFailed => (
"Browser sign-in did not finish, so it switched to a device code. If the browser page said the redirect URI is not valid, add http://localhost/auth under Redirect URIs of the app registration.",
"浏览器登录没有完成,已自动改用设备码。如果浏览器里提示 redirect_uri 无效,请把 http://localhost/auth 加进应用注册的「重定向 URI」。"
),
} }
};
}
@@ -125,6 +144,48 @@ pub(crate) use account_entries;
mod tests {
use crate::{Catalog, Key, Lang};
/// **M178**:浏览器登录回退到设备码时那一句,必须「说清为什么 + 怎么修」,且**不许**把
/// AAD 的原始报文搬上屏(用户实测撞到的正是那张 `invalid_request ... redirect_uri` 错误页)。
///
/// 抓得住:文案退回「登录失败,完整原因在日志里」、删掉可照抄的重定向 URI、或有人把
/// AAD 原始错误码/JSON 塞进词条。
/// 抓不住:读起来顺不顺(那靠 review 与截图)。
#[test]
fn the_browser_fallback_copy_explains_itself_without_raw_aad_text() {
for &lang in Lang::ALL {
let catalog = Catalog::of(lang);
let not_registered = catalog.text(Key::AccountBrowserFallbackNotRegistered);
let unverified = catalog.text(Key::AccountBrowserFallbackUnverified);
let failed = catalog.text(Key::AccountBrowserFallbackFailed);
// 「已改用设备码」这条结论三句里都得有(用户要知道画面为什么变了)。
for copy in [not_registered, unverified, failed] {
assert!(
copy.contains("device code") || copy.contains("设备码"),
"{}: {copy}",
lang.tag()
);
// 原文不上屏:这两条是 AAD 的机器码,出现即语言/脱敏纪律失效。
assert!(!copy.contains("invalid_request"), "{}: {copy}", lang.tag());
assert!(!copy.contains("AADSTS"), "{}: {copy}", lang.tag());
assert!(!copy.contains('{'), "{}: {copy}", lang.tag());
}
// 「没登记回环」这一条必须给**可照抄**的下一步(门户 + 值 + 端口不参与匹配)。
assert!(
not_registered.contains("http://localhost/auth"),
"{not_registered}"
);
assert!(
not_registered.to_lowercase().contains("redirect uri")
|| not_registered.contains("重定向 URI"),
"{not_registered}"
);
assert!(
not_registered.to_lowercase().contains("port") || not_registered.contains("端口"),
"{not_registered}"
);
}
}
/// **M132**:缺 client id 这条是本页唯一「说清原因」的失败文案,它的纪律比别的正文更硬。
///
/// 抓得住:文案退回「登录失败 / 完整原因在日志里」、退化成「未支持」、或者只说缺 id 却
+34 -2
View File
@@ -95,6 +95,29 @@ macro_rules! content_entries {
"Installing from CurseForge is not available yet",
"CurseForge 来源的安装尚未开放"
),
// --- M191:安装失败的两档分组与「只重试这些」---
// 两档**各一条**:不兼容要换版本/加载器,取不到要检查网络,合并成一句会让用户
// 对着一条重试必然失败的项反复点重试。
ContentInstallFailedTitle => (
"Install did not finish — {count} items failed",
"安装未完成 —— {count} 项没装上"
),
ContentInstallRetryIncompatible => (
"Not available for this version/loader",
"不兼容所选版本/加载器"
),
ContentInstallRetryUnreachable => (
"Could not fetch (network or missing project)",
"取不到(网络 / 项目不存在)"
),
ContentInstallRetryOnly => (
"Retry just these {count}",
"只重试这 {count} 个"
),
ContentInstallRetryHint => (
"Retry asks again only for the items above; files that already landed stay put.",
"重试只重新解析上面这几项,已落盘的文件不动。"
),
// --- M113: pack import / export(PACKUI)---
PackTitle => ("Modpack import & export", "整合包导入 / 导出"),
PackImportTitle => ("Import a modpack", "导入整合包"),
@@ -156,8 +179,17 @@ macro_rules! content_entries {
),
PackReportRejectedNote => ("not supported by the client", "客户端不支持"),
PackCancelled => (
"Import keeps running in the background — this result is discarded. Files already written stay.",
"导入会在后台继续跑完 —— 本次结果作废;已经写下的文件会留下。"
"Cancel requested — the pipeline is stopping now; the real outcome follows.",
"已请求取消 —— 后台正在停手,真实结果随后给出。"
),
PackStatusCancelled => ("Cancelled — rolled back", "已取消 —— 已回滚"),
PackCancelIncomplete => (
"Cancelled — rollback incomplete: the target directory may differ from before the import.",
"已取消 —— 回滚没做完:目标目录可能与导入前不同。"
),
PackCancelTooLate => (
"The cancel arrived too late: this import already finished. The instance is kept; same-name files may already be overwritten.",
"取消来得太晚:这次导入已经跑完,实例会保留,同名文件可能已被覆盖。"
),
PackOverwriteAction => ("Re-import, the pack wins", "以包为准重新导入"),
PackOverwriteNote => (
+136 -5
View File
@@ -24,11 +24,117 @@ pub(super) const ROWS: &[(&str, &str, &str)] = &[
("FontCjkReady", "Chinese font ready: {family}", "中文字体已就绪:{family}"),
("SettingsLanguage", "Language", "界面语言"),
("SettingsLanguageHint", "Switches the interface language. Chinese text needs a Chinese font.", "切换界面语言。显示中文需要中文字体。"),
("SettingsLogsFolder", "Log folder", "日志文件夹"),
("SettingsLogsFolderHint", "What explains a crash lives here: the launcher's own runtime log (launcher.log, rolled over at 1 MiB with one old copy kept) and one jsonl per game session under logs/game/. Sessions from older versions (data/agent/sessions) are still read, not moved.", "解释一次崩溃要看的东西都在这:启动器自己的运行日志(launcher.log,满 1 MiB 滚动一次、只留一份旧档)与每次游戏会话的 jsonl(logs/game/)。旧版本留下的会话(data/agent/sessions)仍然读得出来,不会被搬动。"),
("SettingsLogsCopy", "Copy path", "复制路径"),
("SettingsLogsUnknown", "Data directory not resolved — restart the launcher from its own folder", "数据根没解析出来——从启动器所在目录重新启动试试"),
("SettingsAppearanceCorner", "Window corners", "窗口圆角"),
("SettingsAppearanceCornerSystem", "Follow system", "跟随系统"),
("SettingsAppearanceCornerSquare", "Square", "直角"),
("SettingsAppearanceCornerHint", "On Windows 11 and Server 2025 we ask the system for rounded corners; Windows 10 keeps square corners. On macOS and Linux the desktop's own rules apply. We never draw corners ourselves.", "Windows 11 与 Windows Server 2025 上我们请求系统处理外框圆角;Windows 10 保持直角;macOS 与 Linux 交给桌面合成器自己的规则;我们不自己画圆角。"),
("SettingsAppearanceCornerSuspended", "Not in effect right now: window transparency is below 100%. Setting it back to 100% restores the rounded corners.", "当前因整窗透明度不生效:把整窗透明度调回 100% 圆角即恢复。"),
// M180:外观分组补齐(`catalog/settings/appearance.rs` 的第二批,M116 圆角那 5 条之后)。
// 位置就是装配链里那个域的位置——`settings_appearance_entries` 在 `settings_java_entries` 之前,
// 所以这 40 行必须落在 `SettingsAppearanceCornerSuspended` 与 `SettingsJavaScan` 之间
// (顺序 = `Key::ALL`;为排到文件尾而重排别人的族会让 `split_keeps_every_translation` 整片红)。
("SettingsAppearanceWindowOpacity", "Window transparency", "整窗透明度"),
("SettingsAppearanceWindowOpacityHint", "Applies to the whole window. Floor {floor}; 1.00 = fully opaque.", "作用于整个窗口。下限 {floor};1.00 = 完全不透明。"),
("SettingsAppearancePanelOpacity", "Panel transparency", "面板透明度"),
("SettingsAppearancePanelOpacityHint", "Only the panel/card layer. Floor {floor}; independent of window transparency.", "只作用于面板 / 卡片这一层。下限 {floor};与整窗透明度互不影响。"),
("SettingsAppearancePanelOpacityForcedValue", "100% (forced from {requested})", "100%(强制,原请求值 {requested})"),
("SettingsAppearancePanelOpacityForcedHint", "Software renderer: panels and glass stay fully opaque (window transparency still works).", "软件渲染:面板与玻璃效果恒为不透明(整窗透明度仍然有效)。"),
("SettingsAppearanceBackgroundImage", "Background image", "背景图"),
("SettingsAppearanceOff", "Off", "关"),
("SettingsAppearanceOn", "On", "开"),
("SettingsAppearanceImagePath", "Image path", "图片路径"),
("SettingsAppearancePaletteLevel", "Colours: {level}", "配色:{level}"),
("SettingsAppearanceBgFill", "Fill", "填充方式"),
("SettingsAppearanceBgFillCover", "Cover", "铺满"),
("SettingsAppearanceBgFillContain", "Contain", "完整显示"),
("SettingsAppearanceBgFillTile", "Tile", "平铺"),
("SettingsAppearanceBgDim", "Dim", "暗化"),
("SettingsAppearanceBgDimHint", "Blends the image toward the panel colour so text stays readable. Max {max}.", "把背景图往面板底色上退,让文字读得清。上限 {max}。"),
("SettingsAppearancePaletteSource", "Colour source", "配色来源"),
("SettingsAppearancePaletteBackground", "Background", "取自背景图"),
("SettingsAppearancePaletteWallpaper", "Wallpaper", "取自壁纸"),
("SettingsAppearancePalettePreset", "Preset", "内置色卡"),
("SettingsAppearanceBgReadFailed", "Background image could not be read: {reason}", "背景图读不出来:{reason}"),
("SettingsAppearanceBgDecodeFailed", "Background image could not be decoded ({reason}; this build decodes PNG, JPEG and BMP)", "背景图解不开({reason};这个构建只支持 PNG、JPEG 与 BMP)"),
("SettingsAppearanceBgTooLarge", "Background image is too large to decode: {width}×{height} (limit {limit} px per edge)", "背景图太大,解不开:{width}×{height}(每边上限 {limit} 像素)"),
("SettingsAppearanceBgSmallTile", "Small image ({width}×{height}): tiles are scaled up so the window stays covered (max {max} tiles, no gaps).", "图片很小({width}×{height}):图块会被放大,保证窗口铺满(最多 {max} 块,不留缝)。"),
("SettingsAppearancePaletteBgUndecodable", "Background image could not be read; colours fall back.", "背景图读不出来,配色回落到下一级。"),
("SettingsAppearancePaletteBgTooSmall", "Background image is too small to sample; colours fall back.", "背景图太小,取不了色,配色回落到下一级。"),
("SettingsAppearancePaletteBgBadDimensions", "Background image data looks inconsistent; colours fall back.", "背景图数据前后不一致,配色回落到下一级。"),
("SettingsAppearancePaletteBgNoOpaquePixels", "Background image is almost fully transparent; colours fall back.", "背景图几乎全透明,配色回落到下一级。"),
("SettingsAppearancePaletteBgLowChroma", "Background image is too monochrome to tint from; colours fall back.", "背景图太单调(彩度不足),取不出配色,回落到下一级。"),
("SettingsAppearancePaletteBgContrast", "Background image colours would be unreadable; colours fall back.", "按背景图取的颜色读不清,配色回落到下一级。"),
("SettingsAppearancePaletteWallpaperUndecodable", "Desktop wallpaper could not be read.", "桌面壁纸读不出来。"),
("SettingsAppearancePaletteWallpaperTooSmall", "Desktop wallpaper is too small to sample.", "桌面壁纸太小,取不了色。"),
("SettingsAppearancePaletteWallpaperBadDimensions", "Desktop wallpaper data looks inconsistent.", "桌面壁纸数据前后不一致。"),
("SettingsAppearancePaletteWallpaperNoOpaquePixels", "Desktop wallpaper is almost fully transparent.", "桌面壁纸几乎全透明。"),
("SettingsAppearancePaletteWallpaperLowChroma", "Desktop wallpaper is too monochrome to tint from.", "桌面壁纸太单调(彩度不足),取不出配色。"),
("SettingsAppearancePaletteWallpaperContrast", "Desktop wallpaper colours would be unreadable; colours fall back.", "按桌面壁纸取的颜色读不清,配色回落到下一级。"),
("SettingsAppearancePalettePresetContrast", "The built-in card needed a small contrast correction.", "内置色卡做了一点对比度修正。"),
("SettingsAppearancePaletteAdjusted", "A few colours were nudged for readability.", "有几个颜色为了可读性做了微调。"),
// M175:设置页「Java」分组(`catalog/settings/java.rs` 的 `settings_java_entries`)。
// 位置就是装配链里那个域的位置:`settings_java_entries` 在 `settings_appearance_entries` 之后、
// `settings_download_entries`(空)与 `settings_ai_entries`(M174)之前,所以这几行必须落在
// `SettingsAppearanceCornerSuspended` 与 `SettingsGroupAssistant` 之间(顺序 = `Key::ALL`)。
("SettingsJavaScan", "Scan now", "立即扫描"),
("SettingsJavaScanningStart", "Scanning…", "正在扫描…"),
("SettingsJavaScanning", "Scanning… {done}/{total}", "正在扫描… {done}/{total}"),
("SettingsJavaCancelling", "Cancelling…", "正在取消…"),
("SettingsJavaCancel", "Cancel", "取消"),
("SettingsJavaCancelled", "The scan was cancelled; the list below is from the previous run.", "扫描已取消;下面的列表还是上一次的结果。"),
("SettingsJavaScanDone", "{count} JDK found", "找到 {count} 个 JDK"),
("SettingsJavaNeverScanned", "Not scanned yet", "还没扫描过"),
("SettingsJavaNeverScannedBody", "Press Scan to look for installed JDKs (registry, common install folders, JAVA_HOME and PATH). Nothing runs until you do.", "点「立即扫描」去找已装的 JDK(注册表、常见安装目录、JAVA_HOME 与 PATH)。在那之前启动器不会自己去扫。"),
("SettingsJavaEmptyTitle", "No Java runtime found", "未检测到 Java 运行时"),
("SettingsJavaEmptyBody", "Nothing matched. Add the path of a JDK you already unpacked below, or install one with `ull java install 21` on the command line.", "没找到任何 JDK。可以在下面手动加上你自己解压的那份,或者用命令行 `ull java install 21` 装一个。"),
("SettingsJavaDefault", "Default", "默认"),
("SettingsJavaSetDefault", "Use as default", "设为默认"),
("SettingsJavaFits", "Meets the current instance requirement", "满足当前实例的要求"),
("SettingsJavaTooOld", "Below the requirement: this instance needs Java {minimum}+", "低于要求:当前实例需要 Java {minimum} 及以上"),
("SettingsJavaDefaultSet", "Default JDK is now {path}", "默认 JDK 已设为 {path}"),
("SettingsJavaSourceManaged", "installed by this launcher", "本启动器安装"),
("SettingsJavaSourceEnv", "JAVA_HOME (environment variable)", "JAVA_HOME(环境变量)"),
("SettingsJavaSourcePath", "on PATH", "PATH 上"),
("SettingsJavaSourceSystem", "system install", "系统安装"),
("SettingsJavaSourceManual", "added by you", "手动添加"),
("SettingsJavaAdd", "Add a JDK path", "手动添加 JDK 路径"),
("SettingsJavaAddPlaceholder", "C:\\Program Files\\Java\\jdk-21", "例:C:\\Program Files\\Java\\jdk-21"),
("SettingsJavaAddHint", "A JDK folder or its java executable. We run `java -version` on it; anything that cannot answer is refused.", "JDK 目录或它的 java 可执行文件。会真跑一次 `java -version`;答不出话的一律拒绝。"),
("SettingsJavaAddAction", "Add", "添加"),
("SettingsJavaAdded", "Added {path}", "已添加 {path}"),
("SettingsJavaMemory", "Default memory (MB)", "默认内存(MB)"),
("SettingsJavaMemoryHint", "Whole megabytes, {min}–{max}. Instances without their own override follow this value.", "整数 MB,范围 {min}~{max}。没有实例级覆盖的实例都用它。"),
("SettingsJavaLayerGlobal", "In effect: {mb} MB, from the global default.", "生效:{mb} MB,来自全局默认值。"),
("SettingsJavaLayerInstance", "In effect: {mb} MB, set by instance {instance}. Changing the global value below will not affect that instance.", "生效:{mb} MB,由实例 {instance} 覆盖。改下面的全局值不会影响它。"),
("SettingsJavaJvm", "Extra JVM arguments", "额外 JVM 参数"),
("SettingsJavaJvmPlaceholder", "-XX:+UseG1GC -Dfile.encoding=UTF-8", "例:-XX:+UseG1GC -Dfile.encoding=UTF-8"),
("SettingsJavaJvmHint", "Separated by spaces, quotes allowed. Appended to the command line of every launch; an instance's own JVM arguments come after it.", "按空白分隔,支持引号。会拼到每次启动的命令行上;实例自己的 JVM 参数排在它后面。"),
("SettingsJavaJvmSaved", "Saved value: {value}", "已保存的值:{value}"),
("SettingsJavaJvmSavedNone", "No extra JVM arguments are saved yet.", "还没有保存任何额外参数。"),
("SettingsJavaSave", "Save", "保存"),
("SettingsJavaSaved", "Saved and read back from disk. The next launch uses the new value.", "已保存,并从磁盘读回确认。下次启动就用新值。"),
("SettingsJavaUnsaved", "There are unsaved changes.", "有未保存的改动。"),
("SettingsJavaRefused", "Not saved: {reason}", "未保存:{reason}"),
("SettingsJavaSkipNotDirty", "there was nothing to change, so no file was written", "没有任何改动,因此没有写文件"),
("SettingsJavaSkipReadOnly", "the settings file is read-only: a newer launcher wrote it, or it is marked untrusted", "配置文件只读:它被更新版本的启动器写过,或被标记为不可信"),
("SettingsJavaSkipUnreadable", "the settings file is untrusted, so it is treated as read-only", "配置文件不可信,因此按只读处理"),
("SettingsJavaNoStorage", "No settings file is available in this run, so nothing can be scanned or saved.", "这一次运行没有可用的设置文件,扫描与保存都做不了。"),
("SettingsJavaReasonMemoryEmpty", "the memory field is empty", "内存没填"),
("SettingsJavaReasonMemoryNotNumber", "the memory must be a whole number of MB", "内存必须是整数 MB"),
("SettingsJavaReasonMemoryRange", "the memory is outside the allowed range", "内存超出允许范围"),
("SettingsJavaReasonJvmQuotes", "the JVM arguments have an unclosed quote", "JVM 参数里有没配对的引号"),
("SettingsJavaReasonAddEmpty", "the path is empty", "路径是空的"),
("SettingsJavaReasonAddMissing", "nothing exists at that path", "那个路径上什么都没有"),
("SettingsJavaReasonAddNotJava", "there is no java executable at that path", "那个路径里没有 java 可执行文件"),
("SettingsJavaReasonAddNoVersion", "java -version did not answer there, so it is not a usable JDK", "在那里跑不出 java -version,不是一份能用的 JDK"),
("SettingsJavaReasonAddTooLong", "the path is too long to be a JDK location", "路径太长,不像是一个 JDK 位置"),
("SettingsJavaReasonAddWriteFailed", "the JDK is fine, but the list file could not be written (disk or permission)", "这份 JDK 没问题,但名单文件写不进去(磁盘或权限)"),
("SettingsJavaReasonAddDuplicate", "that JDK is already in the list", "这份 JDK 已经在列表里了"),
("SettingsJavaReasonWriteFailed", "the settings file could not be written (disk full, no permission, or the value was rejected)", "设置写不进去(磁盘满、没权限,或这个值被拒绝)"),
// M174:设置页「助手(AI)」分组(`catalog/settings/ai.rs` 的 `settings_ai_entries`)。
// 位置就是装配链里那个域的位置:`settings_ai_entries` 在 `settings_download_entries` 之后、
// `core_text_entries` 之前,所以这几行必须落在 `SettingsAppearanceCornerSuspended` 与
@@ -142,6 +248,31 @@ pub(super) const ROWS: &[(&str, &str, &str)] = &[
("LogsEmptyTitle", "No output yet", "还没有输出"),
("LogsEmptyBody", "Logs stream here while the launcher or the game is running. Pick an instance and start it to see the first lines.", "启动器或游戏运行时,日志会实时出现在这里。选择一个实例并启动,就能看到第一批日志。"),
("LogsEmptyAction", "Choose an instance", "选择实例"),
("CrashDialogTitleFailed", "Launch failed", "启动失败"),
("CrashDialogTitleCrashed", "The game crashed", "游戏崩溃"),
("CrashDialogExitCode", "Exit code {code}", "退出码 {code}"),
("CrashDialogStartedThenDied", "The game process started, then exited abnormally.", "游戏进程起来了,随后异常退出。"),
("CrashDialogSilentFailure", "The game exited with code 0 but never really started.", "游戏以退出码 0 结束,但并没有真正起来。"),
("CrashDialogStageReason", "It failed before the game could start: {stage}", "游戏还没起来就失败了:{stage}"),
("CrashDialogCrashReportFound", "The game wrote a crash report — the details are below.", "游戏写出了崩溃报告,详见下方说明。"),
("CrashDialogNoGameLog", "No crash report this time: the game stopped before it could write one. The last lines the launcher captured are below.", "这次没有生成崩溃报告:游戏还没来得及写就停了。下面是启动器捕获的最后若干行。"),
("CrashDialogNoLogAtAll", "No crash report and no captured output. Most likely: missing Java, not enough memory, wrong launch arguments, or missing libraries.", "既没有崩溃报告,也没有捕获到输出。最可能的原因:缺少 Java、内存不足、启动参数错误、或库文件缺失。"),
("CrashDialogShowDetail", "Show crash details", "展开崩溃说明"),
("CrashDialogHideDetail", "Hide crash details", "收起崩溃说明"),
("CrashDialogLauncherLogs", "Launcher logs", "启动器日志"),
("CrashDialogSessionLog", "This session", "本次会话"),
("CrashDialogGameLog", "Game latest.log", "游戏 latest.log"),
("CrashDialogCrashReports", "Crash reports", "崩溃报告目录"),
("CrashDialogCapturedTail", "CAPTURED OUTPUT (last {count} lines)", "捕获输出(最后 {count} 行)"),
("CrashDialogExport", "Export report bundle", "导出诊断包"),
("CrashDialogOpenFolder", "Open log folder", "打开日志文件夹"),
("CrashDialogCopyPaths", "Copy file paths", "复制文件路径"),
("CrashDialogClose", "Close", "关闭"),
("CrashDialogCopied", "File paths copied to the clipboard", "文件路径已复制到剪贴板"),
("CrashDialogExportDone", "Exported {bytes} bytes to {path}", "已导出 {bytes} 字节到 {path}"),
("CrashDialogExportFailed", "Export failed: {error}", "导出失败:{error}"),
("CrashDialogExportTruncated", "{count} file(s) were too large and are truncated (head + tail kept).", "有 {count} 个文件太大,已截断(保留头 + 尾)。"),
("CrashDialogOpenFailed", "Could not open the folder: {error}", "打不开文件夹:{error}"),
("AgentTitle", "Assistant", "助手"),
("AgentSubtitle", "Simulated assistant · no real model is connected", "模拟助手 · 还没接真实模型"),
("AgentCollapse", "Collapse", "收起"),
@@ -252,6 +383,11 @@ pub(super) const ROWS: &[(&str, &str, &str)] = &[
("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 来源的安装尚未开放"),
("ContentInstallFailedTitle", "Install did not finish — {count} items failed", "安装未完成 —— {count} 项没装上"),
("ContentInstallRetryIncompatible", "Not available for this version/loader", "不兼容所选版本/加载器"),
("ContentInstallRetryUnreachable", "Could not fetch (network or missing project)", "取不到(网络 / 项目不存在)"),
("ContentInstallRetryOnly", "Retry just these {count}", "只重试这 {count} 个"),
("ContentInstallRetryHint", "Retry asks again only for the items above; files that already landed stay put.", "重试只重新解析上面这几项,已落盘的文件不动。"),
("PackTitle", "Modpack import & export", "整合包导入 / 导出"),
("PackImportTitle", "Import a modpack", "导入整合包"),
("PackDropHint", "Drop a .mrpack, .zip or a MultiMC/Prism instance folder here", "把 .mrpack / .zip 或 MultiMC·Prism 实例目录拖到这里"),
@@ -259,9 +395,4 @@ pub(super) const ROWS: &[(&str, &str, &str)] = &[
("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", "新建实例"),
];
+14 -2
View File
@@ -4,6 +4,11 @@
//! 拼装见 `golden.rs` 的 [`super::rows`]。
pub(super) const ROWS: &[(&str, &str, &str)] = &[
("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", "读取清单"),
@@ -24,7 +29,10 @@ pub(super) const ROWS: &[(&str, &str, &str)] = &[
("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.", "导入会在后台继续跑完 —— 本次结果作废;已经写下的文件会留下。"),
("PackCancelled", "Cancel requested — the pipeline is stopping now; the real outcome follows.", "已请求取消 —— 后台正在停手,真实结果随后给出。"),
("PackStatusCancelled", "Cancelled — rolled back", "已取消 —— 已回滚"),
("PackCancelIncomplete", "Cancelled — rollback incomplete: the target directory may differ from before the import.", "已取消 —— 回滚没做完:目标目录可能与导入前不同。"),
("PackCancelTooLate", "The cancel arrived too late: this import already finished. The instance is kept; same-name files may already be overwritten.", "取消来得太晚:这次导入已经跑完,实例会保留,同名文件可能已被覆盖。"),
("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", "离线导入"),
@@ -135,7 +143,8 @@ pub(super) const ROWS: &[(&str, &str, &str)] = &[
("AccountDeviceRetry", "Try again", "重试"),
("AccountDeviceCancel", "Cancel", "取消"),
("AccountDeviceBrowser", "Sign in with browser", "用浏览器登录"),
("AccountDeviceUseCode", "Use a device code instead", "改用设备码"),
("AccountDeviceSignIn", "Sign in with your Microsoft account", "使用 Microsoft 账户登录"),
("AccountDeviceFallbackHint", "No browser handy? This way shows a code you can type on any device.", "浏览器打不开就用这个:它会给一串用户码,在任何设备上输入即可。"),
("AccountServerLabel", "Server address", "服务器地址"),
("AccountServerHint", "API root, e.g. https://littleskin.cn/api/yggdrasil/", "API 根,例:https://littleskin.cn/api/yggdrasil/"),
("AccountServerProbe", "Check server", "校验服务器"),
@@ -180,6 +189,9 @@ pub(super) const ROWS: &[(&str, &str, &str)] = &[
("AccountNoticeTitleDone", "Account updated", "账户已更新"),
("AccountHintAuthlibReauth", "Enter the password again for {server}.", "请输入 {server} 的密码。"),
("AccountHintOfflineNoRelogin", "Local profiles have no server account to sign in again — create a new profile instead.", "本地档案没有可重新登录的服务器账户,请新建一个档案。"),
("AccountBrowserFallbackNotRegistered", "This build's app registration has no loopback redirect URI, so browser sign-in cannot work; switched to a device code. To fix: add http://localhost/auth under Redirect URIs in the Azure app registration (the port is ignored), then restart the launcher.", "这个构建的应用注册里没有登记回环重定向 URI,浏览器登录用不了,已自动改用设备码。怎么修:在 Azure 应用注册的「重定向 URI」里加上 http://localhost/auth(端口不参与匹配),然后重启启动器。"),
("AccountBrowserFallbackUnverified", "Could not confirm that browser sign-in is usable, so it switched to a device code.", "没能确认浏览器登录是否可用(预检没成功),已自动改用设备码。"),
("AccountBrowserFallbackFailed", "Browser sign-in did not finish, so it switched to a device code. If the browser page said the redirect URI is not valid, add http://localhost/auth under Redirect URIs of the app registration.", "浏览器登录没有完成,已自动改用设备码。如果浏览器里提示 redirect_uri 无效,请把 http://localhost/auth 加进应用注册的「重定向 URI」。"),
("SettingsGroupUpdate", "Updates", "更新"),
("SettingsUpdateCurrent", "Current version", "当前版本"),
("SettingsUpdateChannel", "Channel", "更新渠道"),
+26
View File
@@ -33,6 +33,32 @@ macro_rules! logs_entries {
"启动器或游戏运行时,日志会实时出现在这里。选择一个实例并启动,就能看到第一批日志。"
),
LogsEmptyAction => ("Choose an instance", "选择实例"),
// --- 启动失败弹窗(M187)---
CrashDialogTitleFailed => ("Launch failed", "启动失败"),
CrashDialogTitleCrashed => ("The game crashed", "游戏崩溃"),
CrashDialogExitCode => ("Exit code {code}", "退出码 {code}"),
CrashDialogStartedThenDied => ("The game process started, then exited abnormally.", "游戏进程起来了,随后异常退出。"),
CrashDialogSilentFailure => ("The game exited with code 0 but never really started.", "游戏以退出码 0 结束,但并没有真正起来。"),
CrashDialogStageReason => ("It failed before the game could start: {stage}", "游戏还没起来就失败了:{stage}"),
CrashDialogCrashReportFound => ("The game wrote a crash report — the details are below.", "游戏写出了崩溃报告,详见下方说明。"),
CrashDialogNoGameLog => ("No crash report this time: the game stopped before it could write one. The last lines the launcher captured are below.", "这次没有生成崩溃报告:游戏还没来得及写就停了。下面是启动器捕获的最后若干行。"),
CrashDialogNoLogAtAll => ("No crash report and no captured output. Most likely: missing Java, not enough memory, wrong launch arguments, or missing libraries.", "既没有崩溃报告,也没有捕获到输出。最可能的原因:缺少 Java、内存不足、启动参数错误、或库文件缺失。"),
CrashDialogShowDetail => ("Show crash details", "展开崩溃说明"),
CrashDialogHideDetail => ("Hide crash details", "收起崩溃说明"),
CrashDialogLauncherLogs => ("Launcher logs", "启动器日志"),
CrashDialogSessionLog => ("This session", "本次会话"),
CrashDialogGameLog => ("Game latest.log", "游戏 latest.log"),
CrashDialogCrashReports => ("Crash reports", "崩溃报告目录"),
CrashDialogCapturedTail => ("CAPTURED OUTPUT (last {count} lines)", "捕获输出(最后 {count} 行)"),
CrashDialogExport => ("Export report bundle", "导出诊断包"),
CrashDialogOpenFolder => ("Open log folder", "打开日志文件夹"),
CrashDialogCopyPaths => ("Copy file paths", "复制文件路径"),
CrashDialogClose => ("Close", "关闭"),
CrashDialogCopied => ("File paths copied to the clipboard", "文件路径已复制到剪贴板"),
CrashDialogExportDone => ("Exported {bytes} bytes to {path}", "已导出 {bytes} 字节到 {path}"),
CrashDialogExportFailed => ("Export failed: {error}", "导出失败:{error}"),
CrashDialogExportTruncated => ("{count} file(s) were too large and are truncated (head + tail kept).", "有 {count} 个文件太大,已截断(保留头 + 尾)。"),
CrashDialogOpenFailed => ("Could not open the folder: {error}", "打不开文件夹:{error}"),
} }
};
}
+228 -5
View File
@@ -1,12 +1,38 @@
//! 设置页「Appearance」分组文案(M116 首次落地:窗口圆角一档)。
//! 设置页「Appearance」分组文案(M116 落了圆角一族;M180 `I18NAPPEAR` 补齐全组)。
//!
//! 加 key 时把条目写进 `$($acc)*` 后面即可(形状见 [`crate::catalog`] 的「域装配」一节)。
//!
//! ## 为什么只有圆角这一组的 key
//! ## 两批 key:圆角(M116)在前,其余(M180)在后
//!
//! 本分组的既有控件(整窗/面板透明度、背景图、配色来源……)是 M93 落的,文案是英文基线字面量
//! (`pages/appearance/section.rs`)。M116 新增的圆角档位**从一开始就走词表**(tower 口径:
//! 设置项文案不许硬编码),所以这里先只有它这一组;老控件接词表归 i18n 接线 mission。
//! M116 只落了圆角那 5 条;本分组其余控件(整窗/面板透明度、背景图、填充、暗化、配色来源)
//! 是 M93 落的,文案一直是英文基线字面量。M172 审计量出这一页 27 处文案里 22 处硬编码、中文界面
//! 本地化 0/27。M180 把**控件文案 + 运行时诊断 + 配色回落提示**全部搬进本文件:
//!
//! | 批次 | 条目 | 来源 |
//! |---|---|---|
//! | M116 | `Corner` … `CornerSuspended` | `pages/appearance/section/window.rs` 的圆角一段 |
//! | M180 | `WindowOpacity` … `PaletteAdjusted` | 卡片各段标签/说明 + `state.rs` / `background.rs` / `palette.rs` 的诊断 prose |
//!
//! ## 为什么诊断也在这里(而不是留在 `state.rs`)
//!
//! `pages/appearance/state.rs` 已经 390 行、贴着 400 行硬线,放不下任何一张文案表。所以那一层只
//! 给 **key + 已格式化好的参数**(`background::Note`),句子一律在本文件里。
//!
//! ## 数值参数:模板里只有 `{name}`,格式在 Rust 侧做
//!
//! [`crate::catalog::Catalog::render`] 只做 `{name}` → 值的字面替换,**不认** `{:.2}` 这类格式说明。
//! 所以「位置/尺寸/上限」这些数字由调用方先 `format!("{:.2}")` 好再当参数传进来:
//!
//! | key | 原字面量 | 现在 |
//! |---|---|---|
//! | `SettingsAppearancePanelOpacityForcedValue` | `format!("100% (forced from {:.2})")` | `{requested}` = `format!("{:.2}")` |
//! | `SettingsAppearanceWindowOpacityHint` | `Floor {:.2}` = `WINDOW_ALPHA_FLOOR` | `{floor}` |
//! | `SettingsAppearancePanelOpacityHint` | `Floor 0.40` = `PANEL_ALPHA_FLOOR` | `{floor}` |
//! | `SettingsAppearanceBgDimHint` | `Max 0.95` = `BG_DIM_MAX` | `{max}` |
//! | `SettingsAppearanceBgTooLarge` | `(limit 8192 px per edge)` = `MAX_SOURCE_EDGE` | `{limit}` |
//! | `SettingsAppearanceBgSmallTile` | `(max 64 tiles …)` = `MAX_TILES` | `{max}` |
//!
//! 常量走占位符而不是写进译文,是为了「改常量不用改两套译文」(M180 任务口径)。
//!
//! ## 文案口径(不许暗示所有平台一致)
//!
@@ -28,8 +54,205 @@ macro_rules! settings_appearance_entries {
"Not in effect right now: window transparency is below 100%. Setting it back to 100% restores the rounded corners.",
"当前因整窗透明度不生效:把整窗透明度调回 100% 圆角即恢复。"
),
// ---- M180 `I18NAPPEAR`:卡片内文案(① 整窗 / ③ 面板 / ④~⑧ 背景图 / ⑨ 配色来源)
SettingsAppearanceWindowOpacity => ("Window transparency", "整窗透明度"),
SettingsAppearanceWindowOpacityHint => (
"Applies to the whole window. Floor {floor}; 1.00 = fully opaque.",
"作用于整个窗口。下限 {floor};1.00 = 完全不透明。"
),
SettingsAppearancePanelOpacity => ("Panel transparency", "面板透明度"),
SettingsAppearancePanelOpacityHint => (
"Only the panel/card layer. Floor {floor}; independent of window transparency.",
"只作用于面板 / 卡片这一层。下限 {floor};与整窗透明度互不影响。"
),
SettingsAppearancePanelOpacityForcedValue => (
"100% (forced from {requested})",
"100%(强制,原请求值 {requested})"
),
SettingsAppearancePanelOpacityForcedHint => (
"Software renderer: panels and glass stay fully opaque (window transparency still works).",
"软件渲染:面板与玻璃效果恒为不透明(整窗透明度仍然有效)。"
),
SettingsAppearanceBackgroundImage => ("Background image", "背景图"),
SettingsAppearanceOff => ("Off", "关"),
SettingsAppearanceOn => ("On", "开"),
SettingsAppearanceImagePath => ("Image path", "图片路径"),
SettingsAppearancePaletteLevel => ("Colours: {level}", "配色:{level}"),
SettingsAppearanceBgFill => ("Fill", "填充方式"),
SettingsAppearanceBgFillCover => ("Cover", "铺满"),
SettingsAppearanceBgFillContain => ("Contain", "完整显示"),
SettingsAppearanceBgFillTile => ("Tile", "平铺"),
SettingsAppearanceBgDim => ("Dim", "暗化"),
SettingsAppearanceBgDimHint => (
"Blends the image toward the panel colour so text stays readable. Max {max}.",
"把背景图往面板底色上退,让文字读得清。上限 {max}。"
),
SettingsAppearancePaletteSource => ("Colour source", "配色来源"),
SettingsAppearancePaletteBackground => ("Background", "取自背景图"),
SettingsAppearancePaletteWallpaper => ("Wallpaper", "取自壁纸"),
SettingsAppearancePalettePreset => ("Preset", "内置色卡"),
// ---- M180:运行时诊断(`background.rs` 的三种读盘失败 + 小图平铺)。
//
// M180 r1 复审 P2-1:软件渲染那句**只有一条**,就挂在面板那一行(`PanelOpacityForcedHint`)
// 上——它挨着被禁用的滑块,正好回答「为什么拖不动」;这里不再有第二句。
SettingsAppearanceBgReadFailed => (
"Background image could not be read: {reason}",
"背景图读不出来:{reason}"
),
SettingsAppearanceBgDecodeFailed => (
"Background image could not be decoded ({reason}; this build decodes PNG, JPEG and BMP)",
"背景图解不开({reason};这个构建只支持 PNG、JPEG 与 BMP)"
),
SettingsAppearanceBgTooLarge => (
"Background image is too large to decode: {width}×{height} (limit {limit} px per edge)",
"背景图太大,解不开:{width}×{height}(每边上限 {limit} 像素)"
),
SettingsAppearanceBgSmallTile => (
"Small image ({width}×{height}): tiles are scaled up so the window stays covered (max {max} tiles, no gaps).",
"图片很小({width}×{height}):图块会被放大,保证窗口铺满(最多 {max} 块,不留缝)。"
),
// ---- M180:配色回落提示(`palette.rs` 把 design 的 `hint_key()` 翻成下面这些)
SettingsAppearancePaletteBgUndecodable => (
"Background image could not be read; colours fall back.",
"背景图读不出来,配色回落到下一级。"
),
SettingsAppearancePaletteBgTooSmall => (
"Background image is too small to sample; colours fall back.",
"背景图太小,取不了色,配色回落到下一级。"
),
SettingsAppearancePaletteBgBadDimensions => (
"Background image data looks inconsistent; colours fall back.",
"背景图数据前后不一致,配色回落到下一级。"
),
SettingsAppearancePaletteBgNoOpaquePixels => (
"Background image is almost fully transparent; colours fall back.",
"背景图几乎全透明,配色回落到下一级。"
),
SettingsAppearancePaletteBgLowChroma => (
"Background image is too monochrome to tint from; colours fall back.",
"背景图太单调(彩度不足),取不出配色,回落到下一级。"
),
SettingsAppearancePaletteBgContrast => (
"Background image colours would be unreadable; colours fall back.",
"按背景图取的颜色读不清,配色回落到下一级。"
),
SettingsAppearancePaletteWallpaperUndecodable => (
"Desktop wallpaper could not be read.",
"桌面壁纸读不出来。"
),
SettingsAppearancePaletteWallpaperTooSmall => (
"Desktop wallpaper is too small to sample.",
"桌面壁纸太小,取不了色。"
),
SettingsAppearancePaletteWallpaperBadDimensions => (
"Desktop wallpaper data looks inconsistent.",
"桌面壁纸数据前后不一致。"
),
SettingsAppearancePaletteWallpaperNoOpaquePixels => (
"Desktop wallpaper is almost fully transparent.",
"桌面壁纸几乎全透明。"
),
SettingsAppearancePaletteWallpaperLowChroma => (
"Desktop wallpaper is too monochrome to tint from.",
"桌面壁纸太单调(彩度不足),取不出配色。"
),
SettingsAppearancePaletteWallpaperContrast => (
"Desktop wallpaper colours would be unreadable; colours fall back.",
"按桌面壁纸取的颜色读不清,配色回落到下一级。"
),
SettingsAppearancePalettePresetContrast => (
"The built-in card needed a small contrast correction.",
"内置色卡做了一点对比度修正。"
),
SettingsAppearancePaletteAdjusted => (
"A few colours were nudged for readability.",
"有几个颜色为了可读性做了微调。"
),
} }
};
}
pub(crate) use settings_appearance_entries;
#[cfg(test)]
mod tests {
use crate::catalog::{placeholders, Catalog, Key};
use crate::Lang;
/// 本域的 key(`Key::ALL` 里按名字前缀筛;不手工列表,漏一条就少一条守卫)。
fn appearance_keys() -> Vec<Key> {
Key::ALL
.iter()
.copied()
.filter(|key| key.name().starts_with("SettingsAppearance"))
.collect()
}
/// 有没有汉字(与 `pages/settings/tests/appearance_section.rs` 的 `is_han` 同一段码位)。
fn is_han(ch: char) -> bool {
('\u{4e00}'..='\u{9fff}').contains(&ch)
}
/// **M180 守门 ②**:外观分组的每条 key 都必须真翻过——英文照抄进中文表就地判红。
///
/// 抓得住:① 新加 key 时中文栏直接抄英文(`chinese_catalog_is_actually_chinese` 也抓这条,
/// 但那条在 `catalog/tests.rs`、不属于本 mission 的 scope,而且不查「中文字里有没有汉字」);
/// ② 中文译文变成纯 ASCII(比如只写了 `PNG` / `Preset` 这种专名);③ 占位符集合两边不一致
/// (`{floor}` 只在一边有 ⇒ `render()` 吐出半截句子)。
/// 抓不住:译文翻得对不对(语义只能人审)。
///
/// 变异证明:把任一条的第二个字符串换成第一个(`("Cover", "Cover")`)⇒ 本条立刻红(`assert_ne!`
/// 与 `is_han` 命中同一行)。
#[test]
fn every_appearance_key_is_translated_into_chinese() {
let keys = appearance_keys();
assert!(
keys.len() >= 40,
"外观分组至少 40 条 key(M180 落地时是 45 条),实际 {}:\
key 被搬走或前缀改名了?",
keys.len()
);
let en = Catalog::of(Lang::En);
let zh = Catalog::of(Lang::ZhCn);
for key in keys {
let (english, chinese) = (en.text(key), zh.text(key));
assert_ne!(english, chinese, "{} 的中文是英文照抄", key.name());
assert!(
chinese.chars().any(is_han),
"{} 的中文里一个汉字都没有:{chinese}",
key.name()
);
let mut left = placeholders(english);
let mut right = placeholders(chinese);
left.sort_unstable();
right.sort_unstable();
assert_eq!(left, right, "{} 的占位符两套译文不一致", key.name());
}
}
/// 数值参数必须走占位符,不许把常量写进译文(改常量时不该还要改两套译文)。
#[test]
fn numeric_limits_are_placeholders_not_text() {
let en = Catalog::of(Lang::En);
for (key, needle, placeholder) in [
(Key::SettingsAppearanceWindowOpacityHint, "0.08", "floor"),
(Key::SettingsAppearancePanelOpacityHint, "0.40", "floor"),
(Key::SettingsAppearanceBgDimHint, "0.95", "max"),
(Key::SettingsAppearanceBgTooLarge, "8192", "limit"),
(Key::SettingsAppearanceBgSmallTile, "64", "max"),
] {
let text = en.text(key);
assert!(
!text.contains(needle),
"{} 把常量 {needle} 写死在译文里了(应当走 {{{placeholder}}})",
key.name()
);
assert!(
placeholders(text).contains(&placeholder),
"{} 缺 {{{placeholder}}} 占位符:{text}",
key.name()
);
}
}
}
@@ -12,6 +12,17 @@ macro_rules! settings_general_entries {
"Switches the interface language. Chinese text needs a Chinese font.",
"切换界面语言。显示中文需要中文字体。"
),
// ── 日志文件夹(M188:用户投诉「找不到崩溃日志」的直接落点)
SettingsLogsFolder => ("Log folder", "日志文件夹"),
SettingsLogsFolderHint => (
"What explains a crash lives here: the launcher's own runtime log (launcher.log, rolled over at 1 MiB with one old copy kept) and one jsonl per game session under logs/game/. Sessions from older versions (data/agent/sessions) are still read, not moved.",
"解释一次崩溃要看的东西都在这:启动器自己的运行日志(launcher.log,满 1 MiB 滚动一次、只留一份旧档)与每次游戏会话的 jsonl(logs/game/)。旧版本留下的会话(data/agent/sessions)仍然读得出来,不会被搬动。"
),
SettingsLogsCopy => ("Copy path", "复制路径"),
SettingsLogsUnknown => (
"Data directory not resolved — restart the launcher from its own folder",
"数据根没解析出来——从启动器所在目录重新启动试试"
),
} }
};
}
+147 -4
View File
@@ -1,11 +1,154 @@
//! 设置页「Java」分组文案:**预留**(页面侧的真控件还没接)。
//! 设置页「Java」分组文案(M175):扫描 / 运行时列表 / 默认 JDK / 内存 / JVM 参数。
//!
//! 加 key 时把条目写进 `$($acc)*` 后面即可(形状见 [`crate::catalog`] 的「域装配」一节)。
//! 现在没有条目,所以本宏是**空转移器**:只把累积表原样交给下一个域。
//! 域文件的形状见 [`crate::catalog`] 的「域装配」一节。本域在装配链里排在
//! `settings_appearance_entries` 之后、`settings_download_entries` 之前,所以金表那批行要插在
//! `SettingsAppearanceCornerSuspended` 之后、`SettingsGroupAssistant`(M174 的助手域)之前
//! ——**顺序即 `Key::ALL`**。
//!
//! 三条口径:
//!
//! 1. 技术名词不翻:`JDK` / `JVM` / `-version` / `bin/java` / `java.selected_path` 原样;
//! 2. **不许出现写死的版本号或数字**:本组以前那三行(`Temurin 21.0.4` / `4 GiB`)正是被用户
//! 当成假数据的那三条,现在所有数值都由页面从真读盘的结果里格式化进来;
//! 3. 拒绝原因一句一条(空 / 不存在 / 不是 java / 跑不出 -version / 重复),合成一句
//! 「路径无效」等于让用户自己猜。
macro_rules! settings_java_entries {
($next:ident, $($rest:ident,)* { $($acc:tt)* }) => {
$next! { $($rest,)* { $($acc)* } }
$next! { $($rest,)* {
$($acc)*
// ── 扫描(状态 / 进度 / 取消)
SettingsJavaScan => ("Scan now", "立即扫描"),
SettingsJavaScanningStart => ("Scanning…", "正在扫描…"),
SettingsJavaScanning => ("Scanning… {done}/{total}", "正在扫描… {done}/{total}"),
SettingsJavaCancelling => ("Cancelling…", "正在取消…"),
SettingsJavaCancel => ("Cancel", "取消"),
SettingsJavaCancelled => (
"The scan was cancelled; the list below is from the previous run.",
"扫描已取消;下面的列表还是上一次的结果。"
),
SettingsJavaScanDone => ("{count} JDK found", "找到 {count} 个 JDK"),
SettingsJavaNeverScanned => ("Not scanned yet", "还没扫描过"),
SettingsJavaNeverScannedBody => (
"Press Scan to look for installed JDKs (registry, common install folders, JAVA_HOME and PATH). Nothing runs until you do.",
"点「立即扫描」去找已装的 JDK(注册表、常见安装目录、JAVA_HOME 与 PATH)。在那之前启动器不会自己去扫。"
),
SettingsJavaEmptyTitle => ("No Java runtime found", "未检测到 Java 运行时"),
SettingsJavaEmptyBody => (
"Nothing matched. Add the path of a JDK you already unpacked below, or install one with `ull java install 21` on the command line.",
"没找到任何 JDK。可以在下面手动加上你自己解压的那份,或者用命令行 `ull java install 21` 装一个。"
),
// ── 运行时列表
SettingsJavaDefault => ("Default", "默认"),
SettingsJavaSetDefault => ("Use as default", "设为默认"),
SettingsJavaFits => ("Meets the current instance requirement", "满足当前实例的要求"),
SettingsJavaTooOld => (
"Below the requirement: this instance needs Java {minimum}+",
"低于要求:当前实例需要 Java {minimum} 及以上"
),
SettingsJavaDefaultSet => ("Default JDK is now {path}", "默认 JDK 已设为 {path}"),
SettingsJavaSourceManaged => ("installed by this launcher", "本启动器安装"),
SettingsJavaSourceEnv => ("JAVA_HOME (environment variable)", "JAVA_HOME(环境变量)"),
SettingsJavaSourcePath => ("on PATH", "PATH 上"),
SettingsJavaSourceSystem => ("system install", "系统安装"),
SettingsJavaSourceManual => ("added by you", "手动添加"),
// ── 手动添加
SettingsJavaAdd => ("Add a JDK path", "手动添加 JDK 路径"),
SettingsJavaAddPlaceholder => (
"C:\\Program Files\\Java\\jdk-21",
"例:C:\\Program Files\\Java\\jdk-21"
),
SettingsJavaAddHint => (
"A JDK folder or its java executable. We run `java -version` on it; anything that cannot answer is refused.",
"JDK 目录或它的 java 可执行文件。会真跑一次 `java -version`;答不出话的一律拒绝。"
),
SettingsJavaAddAction => ("Add", "添加"),
SettingsJavaAdded => ("Added {path}", "已添加 {path}"),
// ── 内存
SettingsJavaMemory => ("Default memory (MB)", "默认内存(MB)"),
SettingsJavaMemoryHint => (
"Whole megabytes, {min}–{max}. Instances without their own override follow this value.",
"整数 MB,范围 {min}~{max}。没有实例级覆盖的实例都用它。"
),
SettingsJavaLayerGlobal => (
"In effect: {mb} MB, from the global default.",
"生效:{mb} MB,来自全局默认值。"
),
SettingsJavaLayerInstance => (
"In effect: {mb} MB, set by instance {instance}. Changing the global value below will not affect that instance.",
"生效:{mb} MB,由实例 {instance} 覆盖。改下面的全局值不会影响它。"
),
// ── JVM 参数
SettingsJavaJvm => ("Extra JVM arguments", "额外 JVM 参数"),
SettingsJavaJvmPlaceholder => ("-XX:+UseG1GC -Dfile.encoding=UTF-8", "例:-XX:+UseG1GC -Dfile.encoding=UTF-8"),
SettingsJavaJvmHint => (
"Separated by spaces, quotes allowed. Appended to the command line of every launch; an instance's own JVM arguments come after it.",
"按空白分隔,支持引号。会拼到每次启动的命令行上;实例自己的 JVM 参数排在它后面。"
),
SettingsJavaJvmSaved => ("Saved value: {value}", "已保存的值:{value}"),
SettingsJavaJvmSavedNone => (
"No extra JVM arguments are saved yet.",
"还没有保存任何额外参数。"
),
// ── 保存与回执
SettingsJavaSave => ("Save", "保存"),
SettingsJavaSaved => (
"Saved and read back from disk. The next launch uses the new value.",
"已保存,并从磁盘读回确认。下次启动就用新值。"
),
SettingsJavaUnsaved => ("There are unsaved changes.", "有未保存的改动。"),
SettingsJavaRefused => ("Not saved: {reason}", "未保存:{reason}"),
SettingsJavaSkipNotDirty => (
"there was nothing to change, so no file was written",
"没有任何改动,因此没有写文件"
),
SettingsJavaSkipReadOnly => (
"the settings file is read-only: a newer launcher wrote it, or it is marked untrusted",
"配置文件只读:它被更新版本的启动器写过,或被标记为不可信"
),
SettingsJavaSkipUnreadable => (
"the settings file is untrusted, so it is treated as read-only",
"配置文件不可信,因此按只读处理"
),
SettingsJavaNoStorage => (
"No settings file is available in this run, so nothing can be scanned or saved.",
"这一次运行没有可用的设置文件,扫描与保存都做不了。"
),
// ── 拒绝原因(一条一句)
SettingsJavaReasonMemoryEmpty => ("the memory field is empty", "内存没填"),
SettingsJavaReasonMemoryNotNumber => ("the memory must be a whole number of MB", "内存必须是整数 MB"),
SettingsJavaReasonMemoryRange => (
"the memory is outside the allowed range",
"内存超出允许范围"
),
SettingsJavaReasonJvmQuotes => (
"the JVM arguments have an unclosed quote",
"JVM 参数里有没配对的引号"
),
SettingsJavaReasonAddEmpty => ("the path is empty", "路径是空的"),
SettingsJavaReasonAddMissing => ("nothing exists at that path", "那个路径上什么都没有"),
SettingsJavaReasonAddNotJava => (
"there is no java executable at that path",
"那个路径里没有 java 可执行文件"
),
SettingsJavaReasonAddNoVersion => (
"java -version did not answer there, so it is not a usable JDK",
"在那里跑不出 java -version,不是一份能用的 JDK"
),
SettingsJavaReasonAddTooLong => (
"the path is too long to be a JDK location",
"路径太长,不像是一个 JDK 位置"
),
SettingsJavaReasonAddWriteFailed => (
"the JDK is fine, but the list file could not be written (disk or permission)",
"这份 JDK 没问题,但名单文件写不进去(磁盘或权限)"
),
SettingsJavaReasonAddDuplicate => ("that JDK is already in the list", "这份 JDK 已经在列表里了"),
SettingsJavaReasonWriteFailed => (
"the settings file could not be written (disk full, no permission, or the value was rejected)",
"设置写不进去(磁盘满、没权限,或这个值被拒绝)"
),
} }
};
}