From 64e0860112d5e352dd6ac752567172e4e80042c8 Mon Sep 17 00:00:00 2001 From: 1395490241lhj-code <1395490241lhj-code@users.noreply.github.com> Date: Fri, 23 Oct 2026 13:22:00 -0400 Subject: [PATCH] fix(ios): trim repeated and self-evident explanatory copy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each trust or data fact now appears once, where the user makes the decision; lines that restated a title, a row or the next screen are gone. - Settings: drop the guest footer's lead sentence (the row already says 本机功能已全部可用), the About line's login/upload repeat, the permission-prompt and AI troubleshooting footers, and the clear-data footer line its alert already states. - Guest merge: drop the entry footer and the 稍后处理 line the preview already covers, the back-navigation hint, and 仅供查看,不在此修改, which was wrong: the review page lets you change choices before confirming. - Special plan, day rhythm, prepared dates, account deletion, photo tips: remove repeats and shorten long footers. Kept on purpose: the restore preview's replacement warning and the account page's sign-out/delete footers (test- and contract-pinned). Tests: update the pinned guest-footer strings; scroll the merge supporting copy and the clear-data footer into view before asserting, since the shorter pages now stop scrolling at a different spot. Co-Authored-By: Claude Opus 5.5 --- .../Authentication/AccountDeletionView.swift | 2 +- .../KitchenManager/DayRhythmViews.swift | 4 +-- .../KitchenManager/GuestMergeViews.swift | 7 ---- .../KitchenManager/MainFeatureViews.swift | 35 ++++++++----------- .../PreparedComponentViews.swift | 2 -- .../KitchenManager/ReceiptImport.swift | 2 +- .../KitchenManager/RecipeImageImport.swift | 2 +- .../SpecialPlanDetailView.swift | 5 +-- .../GuestMergeSummaryUI5B2BB2AUITests.swift | 22 ++++++++---- .../GuestMergeUIPhase2B3UITests.swift | 9 ++--- .../PR31VisualAcceptanceUITests.swift | 4 +++ .../SettingsExperienceUITests.swift | 2 +- test/ios-native-auth-phase1.test.mjs | 2 +- 13 files changed, 47 insertions(+), 51 deletions(-) diff --git a/ios-native/Kitchen Manager/KitchenManager/Authentication/AccountDeletionView.swift b/ios-native/Kitchen Manager/KitchenManager/Authentication/AccountDeletionView.swift index d24963a5..7a59142c 100644 --- a/ios-native/Kitchen Manager/KitchenManager/Authentication/AccountDeletionView.swift +++ b/ios-native/Kitchen Manager/KitchenManager/Authentication/AccountDeletionView.swift @@ -26,7 +26,7 @@ struct AccountDeletionView: View { var body: some View { Form { Section { - Text("删除账号会永久移除你的登录身份,并清除这台设备上的厨房数据,与「退出登录」不同——退出登录不会删除任何数据,仍可随时重新登录。") + Text("删除账号会永久移除你的登录身份,并清除这台设备上的厨房数据。退出登录则不会删除任何数据,仍可随时重新登录。") } header: { Text("这与退出登录不同") } diff --git a/ios-native/Kitchen Manager/KitchenManager/DayRhythmViews.swift b/ios-native/Kitchen Manager/KitchenManager/DayRhythmViews.swift index 32390858..9d1419c4 100644 --- a/ios-native/Kitchen Manager/KitchenManager/DayRhythmViews.swift +++ b/ios-native/Kitchen Manager/KitchenManager/DayRhythmViews.swift @@ -143,7 +143,7 @@ struct TodayRhythmSheet: View { .frame(minHeight: ChromeMetrics.minimumRowHeight) .accessibilityIdentifier("today.rhythm.reset.button") } footer: { - Text("恢复后,今天回到本周默认节奏,两餐回到照常,今晚的份量安排也一并清除。昨晚留给今天的份数不受影响。") + Text("今天的节奏和两餐会回到默认,今晚的份量安排会清除;昨晚留给今天的份数不受影响。") } } } @@ -246,7 +246,7 @@ struct WeeklyRhythmSettingsView: View { .accessibilityIdentifier("settings.weeklyRhythm.\(weekday.rawValue).picker") } } footer: { - Text("这是每周的默认安排。今天临时改动只影响当天,不会修改这里。") + Text("今天的临时改动只影响当天,不会修改这里。") } } .navigationTitle("每周用餐节奏") diff --git a/ios-native/Kitchen Manager/KitchenManager/GuestMergeViews.swift b/ios-native/Kitchen Manager/KitchenManager/GuestMergeViews.swift index d2ca82aa..7e33a43b 100644 --- a/ios-native/Kitchen Manager/KitchenManager/GuestMergeViews.swift +++ b/ios-native/Kitchen Manager/KitchenManager/GuestMergeViews.swift @@ -40,8 +40,6 @@ struct GuestMergePromptView: View { .foregroundStyle(AppTheme.onManagementAction) .accessibilityIdentifier("guestMergePromptButton") } - } footer: { - Text("购物清单、计划和菜谱不受影响,本次只涉及库存。") } .sheet(isPresented: $isShowingSheet) { NavigationStack { @@ -478,14 +476,11 @@ struct InventoryMergePreviewView: View { LabeledContent("查看处理结果", value: summary.resolvedSummaryText) } .accessibilityIdentifier("guestMergeResolvedReviewLink") - } footer: { - Text("仅供查看,不在此修改。") } } Section { Text("只合并库存,不会上传购物清单、计划或菜谱。") Text("本步骤不会写入云端;确认后如有冲突不会自动覆盖,需要逐条选择。") - Text("可以随时取消,也可以稍后处理。") Text("完成后的新增记录可在限定时间内回滚。") } .font(.footnote) @@ -675,8 +670,6 @@ struct InventoryMergeConflictView: View { Text("待处理冲突已全部选择") .font(.body.weight(.semibold)) .accessibilityIdentifier("guestMergePreConfirmAllResolved") - } footer: { - Text("返回上一页即可查看处理结果,或继续确认合并。") } } Section { diff --git a/ios-native/Kitchen Manager/KitchenManager/MainFeatureViews.swift b/ios-native/Kitchen Manager/KitchenManager/MainFeatureViews.swift index f589039f..44cdf91d 100644 --- a/ios-native/Kitchen Manager/KitchenManager/MainFeatureViews.swift +++ b/ios-native/Kitchen Manager/KitchenManager/MainFeatureViews.swift @@ -1750,7 +1750,7 @@ private struct AddShoppingItemView: View { Text("买什么") } footer: { if let lastAddedName { - Text("已添加「\(lastAddedName)」,可以继续添加下一项。") + Text("已添加「\(lastAddedName)」。") .accessibilityIdentifier("shopping.add.continue.confirmation") } } @@ -1873,13 +1873,12 @@ struct SettingsView: View { /// Kept as a named constant so the guest-mode UI test asserts the same string /// the screen renders, instead of a hand-copied duplicate. /// - /// Wording is deliberately byte-identical to the pre-UI-5A copy. UI-5A's - /// improvement is *hierarchy* — the mode, local-usability status, and action - /// moved into the row, and this qualification moved into the section footer — - /// not rewording. Two static contracts pin this exact string - /// (`test/ios-native-auth-phase1.test.mjs` and `GuestMergeUIPhase2B3UITests`), - /// and there is no user-facing gain in churning them for one word. - static let guestAccountFooter = "无需登录即可继续使用全部本机功能。登录后可为未来跨设备同步做准备,并可选择将本机库存合并到家庭云端;购物清单、计划和菜谱仍只保存在本机。" + /// UI-5A moved the mode, local-usability status, and action into the row and + /// this qualification into the section footer. The footer's old leading + /// "无需登录即可继续使用全部本机功能。" repeated the row's 本机功能已全部可用, + /// so it was dropped; the sync and inventory-merge wording is unchanged. + /// `GuestMergeUIPhase2B3UITests` pins this exact string. + static let guestAccountFooter = "登录后可为未来跨设备同步做准备,并可选择将本机库存合并到家庭云端;购物清单、计划和菜谱仍只保存在本机。" @AppStorage("appearance") private var appearanceRawValue = AppAppearance.system.rawValue @AppStorage("expiryNotificationsEnabled") private var notificationsEnabled = false @@ -2034,7 +2033,7 @@ struct SettingsView: View { Text("系统通知已关闭,提醒不会送达。在 iOS“设置”中允许 Kitchen Manager 发送通知后,这里会恢复你之前的选择。") .accessibilityIdentifier("settings.notifications.denied") } else { - Text("首次开启提醒时,会请求系统通知权限。常备食材补货提醒会在早上 9 点送达。") + Text("常备食材补货提醒会在早上 9 点送达。") } } .task { await refreshNotificationAuthorization() } @@ -2092,8 +2091,6 @@ struct SettingsView: View { .accessibilityIdentifier("settings.aiDiagnostics.link") } header: { Text("故障排查") - } footer: { - Text("AI 无法使用时,可检查网络、登录状态和模型服务。") } } .navigationTitle("AI 设置") @@ -2109,7 +2106,7 @@ struct SettingsView: View { Section("关于") { LabeledContent("版本", value: appVersion) .accessibilityIdentifier("settings.about.version") - Text("联网功能会发送完成操作所需的数据。登录不会自动上传本机厨房数据。") + Text("联网功能会发送完成操作所需的数据。") .font(.footnote) .foregroundStyle(.secondary) } @@ -2124,14 +2121,12 @@ struct SettingsView: View { .frame(minHeight: ChromeMetrics.minimumRowHeight) .accessibilityIdentifier("settings.cleardata.button") } footer: { - VStack(alignment: .leading, spacing: 4) { - // Backups leave user recipes out, so this is the one place - // they can be lost for good. Said here, before the tap. - Label("厨房备份不包含用户菜谱、收藏和常做记录,清除后无法从备份恢复。", systemImage: "exclamationmark.triangle.fill") - .foregroundStyle(AppTheme.warningInk) - .accessibilityIdentifier("settings.cleardata.recipeWarning") - Text("此操作无法撤销,且不会影响远端菜谱库。") - } + // Backups leave user recipes out, so this is the one place + // they can be lost for good. Said here, before the tap; the + // confirmation alert carries the rest of the consequences. + Label("厨房备份不包含用户菜谱、收藏和常做记录,清除后无法从备份恢复。", systemImage: "exclamationmark.triangle.fill") + .foregroundStyle(AppTheme.warningInk) + .accessibilityIdentifier("settings.cleardata.recipeWarning") } #if DEBUG diff --git a/ios-native/Kitchen Manager/KitchenManager/PreparedComponentViews.swift b/ios-native/Kitchen Manager/KitchenManager/PreparedComponentViews.swift index ba12611a..e4600c4e 100644 --- a/ios-native/Kitchen Manager/KitchenManager/PreparedComponentViews.swift +++ b/ios-native/Kitchen Manager/KitchenManager/PreparedComponentViews.swift @@ -261,8 +261,6 @@ private struct PreparedComponentEditor: View { DatePicker("做好时间", selection: $preparedAt, displayedComponents: .date) DatePicker("建议吃完", selection: $expiryDate, displayedComponents: .date) .accessibilityIdentifier("prepared.editor.expiry") - } footer: { - Text("默认日期只是根据状态和存放方式给的起点,可以按自己的判断修改。") } } .navigationTitle(existing == nil ? "添加备餐" : "编辑备餐") diff --git a/ios-native/Kitchen Manager/KitchenManager/ReceiptImport.swift b/ios-native/Kitchen Manager/KitchenManager/ReceiptImport.swift index 2f5db821..2deda7a7 100644 --- a/ios-native/Kitchen Manager/KitchenManager/ReceiptImport.swift +++ b/ios-native/Kitchen Manager/KitchenManager/ReceiptImport.swift @@ -452,7 +452,7 @@ struct RecordFoodSheet: View { ContentUnavailableView( "拍照识别小票", systemImage: "camera.viewfinder", - description: Text("保持小票平整、文字清晰,避免反光和阴影,并拍下完整内容。过长的小票可分段拍摄后分别导入。") + description: Text("拍下完整、清晰的小票,避免反光;过长的小票可分段拍摄后分别导入。") ) HStack { Button("拍照", systemImage: "camera") { requestCamera() } diff --git a/ios-native/Kitchen Manager/KitchenManager/RecipeImageImport.swift b/ios-native/Kitchen Manager/KitchenManager/RecipeImageImport.swift index 810aed39..e1d59ee0 100644 --- a/ios-native/Kitchen Manager/KitchenManager/RecipeImageImport.swift +++ b/ios-native/Kitchen Manager/KitchenManager/RecipeImageImport.swift @@ -498,7 +498,7 @@ struct RecipeImageImportView: View { ContentUnavailableView( "从图片导入菜谱", systemImage: "text.viewfinder", - description: Text("保证文字清晰,避免反光和阴影,尽量包含完整菜名、食材和步骤。长菜谱可以分段拍摄,截图不要裁掉关键内容。") + description: Text("拍清楚完整的菜名、食材和步骤,避免反光;长菜谱可分段拍摄。") ) HStack { Button("拍照", systemImage: "camera") { requestCamera() } diff --git a/ios-native/Kitchen Manager/KitchenManager/SpecialPlanDetailView.swift b/ios-native/Kitchen Manager/KitchenManager/SpecialPlanDetailView.swift index 0a47e9a8..09ad8d8e 100644 --- a/ios-native/Kitchen Manager/KitchenManager/SpecialPlanDetailView.swift +++ b/ios-native/Kitchen Manager/KitchenManager/SpecialPlanDetailView.swift @@ -73,7 +73,7 @@ struct SpecialPlanDetailView: View { planSummary(plan) .plannerRow() } footer: { - Text("时间、人数和要求由 AI 从你的描述里读出,没写到的会按常见情况补上;想改就重新描述一次。") + Text("由 AI 从你的描述里读出,没写到的按常见情况补上;想改就重新描述。") .plannerFootnote() } @@ -287,9 +287,6 @@ struct SpecialPlanDetailView: View { .plannerRow() } header: { KitchenSectionLabel(title: "菜单", count: plan.dishes.count, tint: KitchenTheme.textSecondary).plannerSectionHeader() - } footer: { - Text("菜品引用菜谱库,删除计划不会删除菜谱。") - .plannerFootnote() } } diff --git a/ios-native/Kitchen Manager/KitchenManagerUITests/GuestMergeSummaryUI5B2BB2AUITests.swift b/ios-native/Kitchen Manager/KitchenManagerUITests/GuestMergeSummaryUI5B2BB2AUITests.swift index fdf95b5b..6eef437d 100644 --- a/ios-native/Kitchen Manager/KitchenManagerUITests/GuestMergeSummaryUI5B2BB2AUITests.swift +++ b/ios-native/Kitchen Manager/KitchenManagerUITests/GuestMergeSummaryUI5B2BB2AUITests.swift @@ -63,6 +63,14 @@ final class GuestMergeSummaryUI5B2BB2AUITests: XCTestCase { return target.label } + /// The supporting line sits under the confirm button, and a lazy Form only + /// builds it once it scrolls on screen, so reaching the button is not enough. + private func supportingCopy(_ app: XCUIApplication) -> String? { + let copy = element(app, "guestMergeConfirmSupportingCopy") + _ = scrollTo(app, copy, maxSwipes: 6) + return copy.exists ? copy.label : nil + } + private func group(_ app: XCUIApplication, _ name: String) -> XCUIElement { element(app, "guestMergeReviewGroup-\(name)") } @@ -287,7 +295,7 @@ final class GuestMergeSummaryUI5B2BB2AUITests: XCTestCase { let app = launchSummary("UITEST_MERGE_SUMMARY_ALL_SKIP") XCTAssertEqual(confirmButton(app).label, "完成,不上传任何条目") XCTAssertEqual( - value(app, "guestMergeConfirmSupportingCopy"), + supportingCopy(app), "本次不会上传任何库存;家庭和本机数据保持现在的样子。" ) XCTAssertFalse(confirmButton(app).label.contains("确认合并库存")) @@ -311,7 +319,7 @@ final class GuestMergeSummaryUI5B2BB2AUITests: XCTestCase { XCTAssertEqual(value(app, "guestMergeSummaryStillNeedsDecision"), "仍待处理, 2 条") XCTAssertEqual(confirmButton(app).label, "先合并其余 3 条") XCTAssertEqual( - value(app, "guestMergeConfirmSupportingCopy"), + supportingCopy(app), "还有 2 条待处理,本次不会上传这些条目。" ) attach(app, named: "summary-unresolved") @@ -323,7 +331,7 @@ final class GuestMergeSummaryUI5B2BB2AUITests: XCTestCase { XCTAssertEqual(title, "确认当前处理结果") XCTAssertFalse(title.contains("0 条"), "不得出现“先合并其余 0 条”:\(title)") XCTAssertEqual( - value(app, "guestMergeConfirmSupportingCopy"), + supportingCopy(app), "目前没有可上传条目;确认后仍有 2 条冲突需要处理。" ) attach(app, named: "summary-zero-upload-unresolved") @@ -332,10 +340,10 @@ final class GuestMergeSummaryUI5B2BB2AUITests: XCTestCase { func testOrdinaryMergeCopyDoesNotRegress() throws { let app = launchSummary("UITEST_MERGE_SUMMARY_NO_CONFLICT") XCTAssertEqual(confirmButton(app).label, "确认合并库存") - XCTAssertEqual(value(app, "guestMergeConfirmSupportingCopy"), "计划新增 2 条、计划更新 1 条。") - // The long-standing preview footnotes are untouched. + XCTAssertEqual(supportingCopy(app), "计划新增 2 条、计划更新 1 条。") + // The preview's trust footnotes stay in place. XCTAssertTrue(app.staticTexts["只合并库存,不会上传购物清单、计划或菜谱。"].exists) - XCTAssertTrue(app.staticTexts["可以随时取消,也可以稍后处理。"].exists) + XCTAssertTrue(app.staticTexts["本步骤不会写入云端;确认后如有冲突不会自动覆盖,需要逐条选择。"].exists) // No unresolved conflicts, so no 仍待处理 row. XCTAssertFalse(element(app, "guestMergeSummaryStillNeedsDecision").exists) } @@ -402,7 +410,7 @@ final class GuestMergeSummaryUI5B2BB2AUITests: XCTestCase { // must not reuse the definite first-pass wording nor invent a remaining // count. XCTAssertEqual(confirmButton(app).label, "确认当前处理计划") - let copy = try XCTUnwrap(value(app, "guestMergeConfirmSupportingCopy")) + let copy = try XCTUnwrap(supportingCopy(app)) XCTAssertEqual(copy, "当前页面汇总本次会话的整体计划,系统会根据当前同步状态继续处理。") for banned in ["计划新增", "先合并其余", "尚未上传", "已经上传", "已上传", "已经合并", "已合并", "不会上传任何库存", "文案", "重新定义"] { diff --git a/ios-native/Kitchen Manager/KitchenManagerUITests/GuestMergeUIPhase2B3UITests.swift b/ios-native/Kitchen Manager/KitchenManagerUITests/GuestMergeUIPhase2B3UITests.swift index 4c7f06f9..b61e0ec9 100644 --- a/ios-native/Kitchen Manager/KitchenManagerUITests/GuestMergeUIPhase2B3UITests.swift +++ b/ios-native/Kitchen Manager/KitchenManagerUITests/GuestMergeUIPhase2B3UITests.swift @@ -25,11 +25,12 @@ final class GuestMergeUIPhase2B3UITests: XCTestCase { XCTAssertTrue(guestRow.label.contains("游客模式"), "账号入口应仍标明游客模式") // The explanatory copy must mention future sync prep AND inventory backup, - // matching the exact product copy — unchanged by UI-5A, which moved this - // text into the section footer without rewording it. No merge-specific UI - // (which requires a signed-in account) may ever appear here. + // matching the exact product copy. Its old leading "无需登录即可继续使用 + // 全部本机功能。" was dropped because the row above already says + // 本机功能已全部可用. No merge-specific UI (which requires a signed-in + // account) may ever appear here. let explanation = app.staticTexts[ - "无需登录即可继续使用全部本机功能。登录后可为未来跨设备同步做准备,并可选择将本机库存合并到家庭云端;购物清单、计划和菜谱仍只保存在本机。" + "登录后可为未来跨设备同步做准备,并可选择将本机库存合并到家庭云端;购物清单、计划和菜谱仍只保存在本机。" ] XCTAssertTrue(explanation.waitForExistence(timeout: 3)) diff --git a/ios-native/Kitchen Manager/KitchenManagerUITests/PR31VisualAcceptanceUITests.swift b/ios-native/Kitchen Manager/KitchenManagerUITests/PR31VisualAcceptanceUITests.swift index a4bc7c56..452a022e 100644 --- a/ios-native/Kitchen Manager/KitchenManagerUITests/PR31VisualAcceptanceUITests.swift +++ b/ios-native/Kitchen Manager/KitchenManagerUITests/PR31VisualAcceptanceUITests.swift @@ -216,6 +216,10 @@ final class PR31VisualAcceptanceUITests: XCTestCase { assertFullText(footer, contains: "清除后无法从备份恢复", variant, "clear-data footer") let tabBar = app.tabBars.firstMatch if tabBar.exists { + // `reveal` stops 40pt above the window edge, which can still be + // behind the taller tab bar; keep scrolling so the page end has + // to carry the footer clear of it. + for _ in 0..<3 where footer.frame.maxY > tabBar.frame.minY + 1 { app.swipeUp() } XCTAssertLessThanOrEqual(footer.frame.maxY, tabBar.frame.minY + 1, "\(variant.name): footer \(footer.frame) under tab bar \(tabBar.frame)") } diff --git a/ios-native/Kitchen Manager/KitchenManagerUITests/SettingsExperienceUITests.swift b/ios-native/Kitchen Manager/KitchenManagerUITests/SettingsExperienceUITests.swift index 1dd12ab7..f07aa971 100644 --- a/ios-native/Kitchen Manager/KitchenManagerUITests/SettingsExperienceUITests.swift +++ b/ios-native/Kitchen Manager/KitchenManagerUITests/SettingsExperienceUITests.swift @@ -5,7 +5,7 @@ import XCTest /// Destructive confirmation is opened and cancelled; AI checks are never run. final class SettingsExperienceUITests: XCTestCase { /// Must stay identical to `SettingsView.guestAccountFooter`. - private static let guestFooter = "无需登录即可继续使用全部本机功能。登录后可为未来跨设备同步做准备,并可选择将本机库存合并到家庭云端;购物清单、计划和菜谱仍只保存在本机。" + private static let guestFooter = "登录后可为未来跨设备同步做准备,并可选择将本机库存合并到家庭云端;购物清单、计划和菜谱仍只保存在本机。" override func setUpWithError() throws { continueAfterFailure = false diff --git a/test/ios-native-auth-phase1.test.mjs b/test/ios-native-auth-phase1.test.mjs index 4042921b..a332872a 100644 --- a/test/ios-native-auth-phase1.test.mjs +++ b/test/ios-native-auth-phase1.test.mjs @@ -36,7 +36,7 @@ test('Guest-first auth is app scoped without creating or switching kitchen persi assert.match(content, /AuthenticationAssembly\.make\(\)/); assert.match(content, /await authStore\.start\(\)/); assert.match(settings, /游客模式/); - assert.match(settings, /无需登录即可继续使用全部本机功能/); + assert.match(settings, /本机功能已全部可用/); assert.doesNotMatch(authStore, /KitchenStore|KitchenPersistenceFactory|ModelContainer/); });