Skip to content

fix: remove redundant permission dialogs and fix language code handling#70

Merged
hubo1989 merged 4 commits into
mainfrom
fixlanguage_error
Mar 25, 2026
Merged

fix: remove redundant permission dialogs and fix language code handling#70
hubo1989 merged 4 commits into
mainfrom
fixlanguage_error

Conversation

@hubo1989
Copy link
Copy Markdown
Owner

@hubo1989 hubo1989 commented Mar 24, 2026

Summary

  • 移除自定义权限解释对话框,直接使用系统权限提示,避免双弹窗问题
  • 修复语言代码处理:为 zh/zh-CN/zh-TW 等添加模糊匹配到 zh-Hans/zh-Hant
  • 修复 localeLanguage 属性保留脚本子标签(zh-Hans 而非 zh),确保 Apple Translation 正确识别
  • 移除不可靠的 LanguageAvailability 预检查,直接依赖 TranslationSession 的错误反馈
  • 改进翻译错误展示,使用专用 showTranslationError 方法

Test plan

  • 快捷键触发未授权场景,确认只弹出系统权限提示
  • 英文→简体中文翻译正常工作
  • 其他语言对翻译正常工作
  • 权限授予后功能正常

Summary by CodeRabbit

发布说明

  • 新功能

    • 多引擎翻译结果展示:翻译弹窗显示每个引擎的独立结果、耗时与成功/失败状态。
  • 改进

    • 并行/主备运行仅使用已启用引擎,采纳首个成功结果并保留多引擎明细。
    • 设置变更立即持久化(包括快捷键、颜色、引擎配置与场景绑定)。
  • 修复

    • 简化权限请求流程,移除额外说明对话框。
    • 统一并本地化翻译/插入失败的错误提示弹窗。
  • 本地化

    • 添加“插入失败”错误提示的多语言翻译。

- Remove custom permission explanation dialogs, use system prompts directly
- Fix double dialog issue when requesting accessibility permission
- Add fuzzy matching for language codes (zh → zh-Hans, zh-TW → zh-Hant)
- Fix localeLanguage to preserve script subtags for Apple Translation
- Remove unreliable LanguageAvailability pre-check, rely on TranslationSession errors
- Use full BCP 47 identifiers in error messages instead of minimalIdentifier
- Improve translation error display with dedicated showTranslationError method
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 24, 2026

Warning

Rate limit exceeded

@hubo1989 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 4 minutes and 52 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5e459561-3a2c-482c-a316-192eeb6430e6

📥 Commits

Reviewing files that changed from the base of the PR and between cf984b0 and d4f9aab.

📒 Files selected for processing (1)
  • ScreenTranslate/App/Coordinators/TextTranslationCoordinator.swift
📝 Walkthrough

Walkthrough

该 PR 引入多引擎翻译结果汇总与展示(engine-level result bundle),改造翻译协议/服务以返回每引擎结果并在弹窗中展示多引擎卡片;同时调整多处设置持久化调用、权限请求简化与翻译/插入错误呈现,本地化新增插入失败文案。

Changes

Cohort / File(s) Summary
多引擎翻译核心
ScreenTranslate/Features/TextTranslation/TextTranslationFlow.swift, ScreenTranslate/Services/Protocols/TranslationServicing.swift, ScreenTranslate/Services/TranslationService.swift
新增 EngineTranslationInfo,TextTranslationResult 扩展包含 per‑engine engineResults;协议新增/实现 translateBundle(...);TranslationService 调整并行/回退逻辑、空结果/全失败错误路径与已启用引擎过滤。
UI:翻译弹窗与窗口
ScreenTranslate/Features/TextTranslation/TextTranslationPopupView.swift, ScreenTranslate/Features/TextTranslation/TextTranslationPopupWindow.swift
PopupContent 接收 engineResults,当多于 1 个结果时展示每引擎卡片(成功/失败、引擎名、延迟、文本或错误);保留 0–1 结果兼容渲染,重构子视图与间距。
协调器与错误呈现
ScreenTranslate/App/Coordinators/TextTranslationCoordinator.swift
简化 ensureAccessibilityPermission 逻辑(不再捕获 request 返回值并直接重检),将若干错误展示从 appDelegate 路径替换为局部 NSAlert 演示 (showTranslationError / 插入失败提示)。
设置持久化改动
ScreenTranslate/Models/AppSettings.swift, ScreenTranslate/Features/Settings/SettingsViewModel.swift, ScreenTranslate/Features/Settings/MultiEngineSettingsSection.swift
提升 AppSettings.Keys 可见性,移除 engineConfigs 的 didSet 自动保存,新增/暴露显式保存方法;SettingsViewModel 在多个 setter 中立即调用对应 settings.save...();Settings UI 在多处变更后显式调用保存并在 EngineConfigSheet set 中改为整体赋值后保存并记录日志。
引擎/提供商语言解析与错误标识
ScreenTranslate/Services/TranslationEngine.swift, ScreenTranslate/Services/AppleTranslationProvider.swift
增强 TranslationLanguage.fromTranslationCode 支持脚本/模糊匹配(保留 zh‑Hans/Hant 等);Apple provider 使用该解析;改用完整 BCP47 标识符报告不支持语言;移除 translate 前的可用性预检。
权限管理简化
ScreenTranslate/Services/PermissionManager.swift
移除自定义解释对话框,requestAccessibilityPermission/requestInputMonitoringPermission 直接触发系统请求或打开设置并统一缓存结果,删除相关私有对话框 helper。
本地化:插入失败消息
ScreenTranslate/Resources/.../Localizable.strings
在多语言资源(en/de/es/fr/it/ja/ko/pt/ru/zh‑Hans 等)新增键 textTranslation.error.insertFailed 及对应翻译,用于插入失败提示。

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Coordinator as TextTranslationCoordinator
    participant PermissionMgr as PermissionManager
    participant Service as TranslationService
    participant Engine as TranslationEngine
    participant Provider as TranslationProvider
    participant UI as TextTranslationPopupView

    User->>Coordinator: 发起翻译请求
    rect rgba(100,150,200,0.5)
    Coordinator->>PermissionMgr: requestAccessibilityPermission()
    PermissionMgr->>PermissionMgr: 调用系统权限请求
    PermissionMgr-->>Coordinator: 返回权限状态
    end

    rect rgba(100,200,150,0.5)
    Coordinator->>Service: translateBundle(segments, target, preferredEngine, mode, ...)
    Service->>Service: filterEnabledEngines()
    par 并行/多引擎调用
        Service->>Engine: translateWithEngine(engine, ...)
        Engine->>Provider: 调用对应提供商
        Provider-->>Engine: 返回 segments / error
        Engine-->>Service: 返回 EngineResult
    and
        Service->>Service: 收集 EngineResult -> EngineTranslationInfo 列表
    end
    Service-->>Coordinator: 返回 TranslationResultBundle (engineResults...)
    end

    rect rgba(200,150,100,0.5)
    alt 有成功引擎
        Coordinator->>UI: 显示 engineResults
        UI->>User: 渲染多引擎卡片或单一翻译
    else 全部失败
        Coordinator->>Coordinator: showTranslationError(error)
        Coordinator->>User: NSAlert 显示错误
    end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • PR #69: 与本 PR 在多引擎翻译管道(TranslationServicing / TextTranslationFlow 等)上有重叠改动。
  • PR #19: 直接相关 — 修改相同多引擎翻译子系统、设置与 UI。
  • PR #17: 相关 — 涉及协调器、权限与插入流程,与本次改动存在代码级重叠。

Poem

🐰 我在胡萝卜堆里跳舞唱:多引擎齐心翻译忙,
卡片一列结果亮,设置存好不慌张,
权限简洁少彷徨,插入失败也有章,
兔子挥手把彩带撒,翻译世界更欢畅 ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.54% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately captures the main changes: removing redundant permission dialogs and fixing language code handling with fuzzy matching and script tag preservation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fixlanguage_error

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@hubo1989
Copy link
Copy Markdown
Owner Author

@coderabbitai

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 25, 2026

Tip

For best results, initiate chat on the files or code changes.

@hubo1989 它看起来您的评论只包含了对我的标记,但没有具体的请求或问题。请问您需要我做什么?例如:

  • 🔍 代码审查 — 审查此 PR 的代码变更
  • 解答问题 — 关于代码或实现的具体问题
  • 📝 生成摘要 — 重新生成 PR 摘要
  • 🛠️ 代码建议 — 针对某个具体问题提供代码修复建议

请告诉我您需要什么帮助!

@hubo1989
Copy link
Copy Markdown
Owner Author

@coderabbitai 代码审查 — 审查此 PR 的代码变更

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 25, 2026

@hubo1989 好的,我将立即对此 PR 的代码变更进行全面审查!

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
ScreenTranslate/Services/PermissionManager.swift (1)

104-116: ⚠️ Potential issue | 🟠 Major

Input Monitoring 权限检查使用了错误的 API。

hasInputMonitoringPermission 当前通过 AXIsProcessTrustedWithOptions 检查,但这个 API 只覆盖无障碍权限,而非 Input Monitoring 权限。当用户已授予无障碍权限但未授予 Input Monitoring 权限时,此函数会错误返回 true,导致输入监控功能实际失效。

应使用 CGPreflightListenEventAccess() 检查实际的 Input Monitoring 权限状态,并在拒绝后才用 CGRequestListenEventAccess() 请求权限。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ScreenTranslate/Services/PermissionManager.swift` around lines 104 - 116, The
current hasInputMonitoringPermission check and request flow is using
AXIsProcessTrustedWithOptions (accessibility) which is wrong for Input
Monitoring; update the hasInputMonitoringPermission implementation to call
CGPreflightListenEventAccess() to determine actual Input Monitoring status, and
in requestInputMonitoringPermission replace the AX-based request with
CGRequestListenEventAccess() to prompt the system (falling back to
openInputMonitoringSettings() only if CGRequestListenEventAccess() fails or is
insufficient); ensure references to hasInputMonitoringPermission,
requestInputMonitoringPermission, and openInputMonitoringSettings are updated
accordingly so the permission flow uses CGPreflightListenEventAccess() for
checks and CGRequestListenEventAccess() for requests.
ScreenTranslate/Services/TranslationService.swift (1)

194-237: ⚠️ Potential issue | 🟠 Major

并行结果顺序现在是不稳定的。

TaskGroup 收集结果时按完成先后 append,bundle.results 的顺序会随网络延迟变化。下游在 TextTranslationFlow 中用 first(where: { $0.isSuccess }) 选择主显示文本,导致主译文在不同运行间可能随机切到不同引擎的结果。建议按 engines 的输入顺序重建结果数组。

建议修改
-        let results = await withTaskGroup(of: EngineResult.self, returning: [EngineResult].self) { group in
-            for engine in engines {
+        let results = await withTaskGroup(of: (Int, EngineResult).self, returning: [EngineResult].self) { group in
+            for (index, engine) in engines.enumerated() {
                 group.addTask {
                     do {
                         let start = Date()
                         let provider = try await self.resolvedProvider(for: engine)
@@
-                        guard !bilingualSegments.isEmpty else {
-                            return EngineResult.failed(
+                        guard !bilingualSegments.isEmpty else {
+                            return (index, EngineResult.failed(
                                 engine: engine,
                                 error: TranslationProviderError.translationFailed(
                                     "\(provider.name) returned no results"
                                 )
-                            )
+                            ))
                         }
 
-                        return EngineResult(
+                        return (index, EngineResult(
                             engine: engine,
                             segments: bilingualSegments,
                             latency: Date().timeIntervalSince(start)
-                        )
+                        ))
                     } catch {
-                        return EngineResult.failed(engine: engine, error: error)
+                        return (index, EngineResult.failed(engine: engine, error: error))
                     }
                 }
             }
 
-            var collectedResults: [EngineResult] = []
-            for await result in group {
-                collectedResults.append(result)
+            var collectedResults = Array<EngineResult?>(repeating: nil, count: engines.count)
+            for await (index, result) in group {
+                collectedResults[index] = result
             }
-            return collectedResults
+            return collectedResults.compactMap { $0 }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ScreenTranslate/Services/TranslationService.swift` around lines 194 - 237,
The concurrent TaskGroup collects EngineResult in completion order, making the
returned `results` unstable; rebuild the final array to match the input
`engines` order: inside the await `withTaskGroup` after collecting
`collectedResults`, create a lookup (e.g., [Engine: EngineResult]) from
`collectedResults` keyed by `engine` and then return engines.map { lookup[$0] ??
EngineResult.failed(engine: $0, error:
TranslationProviderError.translationFailed("no result")) } so the returned
[EngineResult] preserves the original `engines` order (refer to `engines`,
`EngineResult`, and the translateWithResolvedPrompt/resolvedProvider task
block).
🧹 Nitpick comments (2)
ScreenTranslate/Features/Settings/MultiEngineSettingsSection.swift (1)

641-641: 优化:移除冗余的 let 关键字。

根据 SwiftLint 提示,当丢弃函数返回值时,应使用 _ = foo() 而非 let _ = foo()

♻️ 建议修复
-        let _ = Logger.settings.info("engineCard \(engine.rawValue): isEnabled=\(config.isEnabled), fromDefault=\(viewModel.settings.engineConfigs[engine] == nil)")
+        _ = Logger.settings.info("engineCard \(engine.rawValue): isEnabled=\(config.isEnabled), fromDefault=\(viewModel.settings.engineConfigs[engine] == nil)")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ScreenTranslate/Features/Settings/MultiEngineSettingsSection.swift` at line
641, The Logger call currently discards its return value using the redundant
`let _ =` pattern; update the invocation in MultiEngineSettingsSection (the
`Logger.settings.info("engineCard \(engine.rawValue):
isEnabled=\(config.isEnabled),
fromDefault=\(viewModel.settings.engineConfigs[engine] == nil)")` line) to
discard the result correctly by removing `let` (either call
`Logger.settings.info(...)` directly or use `_ = Logger.settings.info(...)`), so
the `let _ =` is eliminated.
ScreenTranslate/Features/Settings/SettingsViewModel.swift (1)

301-307: nil 分支不要直接绕过 AppSettings

这两个 setter 的写入路径都走 settings,但删除路径直接落到 UserDefaults.standardSettingsViewModel 本身允许注入自定义 AppSettings,所以测试或非默认 suite 下会出现 set/remove 走不同后端的问题。建议给 AppSettings 补一个 remove helper,并统一从同一抽象层处理。

Also applies to: 338-344

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ScreenTranslate/Features/Settings/SettingsViewModel.swift` around lines 301 -
307, The nil branch currently removes keys directly via UserDefaults.standard,
bypassing the injected AppSettings and causing inconsistent backends; add a
remove(forKey:) helper to AppSettings (or an equivalent method on the existing
protocol) and change the nil branches in SettingsViewModel setters (e.g., the
translationTargetLanguage setter and the similar setter at the 338-344 region)
to call settings.remove(forKey: AppSettings.Keys.translationTargetLanguage)
instead of UserDefaults.standard.removeObject(...), ensuring all get/set/remove
operations go through the same AppSettings abstraction.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ScreenTranslate/App/Coordinators/TextTranslationCoordinator.swift`:
- Around line 242-250: The fallback localization key used in
showTranslationError (alert.messageText) is a format string with a %@
placeholder but is being used without parameters; update showTranslationError to
either use a non‑formatted/localization key (e.g., a generic
"textTranslation.error.generic") or call the formatted localization with a
supplied argument (for example the error.localizedDescription or
error.errorDescription) so the %@ placeholder is satisfied; adjust
alert.messageText assignment accordingly and keep TextTranslationError handling
consistent.

In `@ScreenTranslate/Features/Settings/SettingsViewModel.swift`:
- Around line 368-370: 当前的 vlmAPIKey 属性在 setter 中调用 settings.save(..., forKey:
AppSettings.Keys.vlmAPIKey) 将敏感 API key 写入通用设置(可能为 UserDefaults/
plist);请改为使用项目现有的 KeychainService 持久化:在 SettingsViewModel 的 vlmAPIKey setter
中替换对 settings.save 的调用,改为通过 KeychainService 提供的存/取方法保存和读取 vlmAPIKey(保留 getter 对
settings.vlmAPIKey 的读取逻辑或改为从 Keychain 读取,确保使用相同的唯一键名并处理可能的失败/错误情况),并移除将 API key
写入通用设置的路径。

In `@ScreenTranslate/Features/TextTranslation/TextTranslationPopupView.swift`:
- Around line 274-278: Remove the trailing comma after the last element in the
engineResults array literal (the EngineTranslationInfo entries) so the array no
longer ends with an extraneous comma; update the array in
TextTranslationPopupView where engineResults is defined/initialized to ensure
the final EngineTranslationInfo item has no following comma to satisfy
SwiftLint's trailing_comma rule.
- Around line 133-169: The translatedText and errorMessage Text views in
engineResultSection currently hardcode .leading alignment; change
engineResultSection to read the environment layout direction (add
`@Environment`(\.layoutDirection) var layoutDirection) and compute an alignment
variable (let textAlignment: TextAlignment = layoutDirection == .rightToLeft ?
.trailing : .leading and let frameAlignment: Alignment = layoutDirection ==
.rightToLeft ? .trailing : .leading) then apply
multilineTextAlignment(textAlignment) and .frame(maxWidth: .infinity, alignment:
frameAlignment) to both the Text(result.translatedText) and Text(error) branches
so RTL languages render with trailing alignment; keep other UI behavior intact
and reuse the same alignment logic for both success and error text branches.

In `@ScreenTranslate/Models/AppSettings.swift`:
- Around line 874-880: 在 loadParallelEngines() 中修复“脏数据回退”不一致的问题:当前 guard 分支对
missing key 返回 [.apple],但当持久化数组存在但全部解析失败(engines.isEmpty)时又返回 [.apple,
.mtranServer]。将 engines.isEmpty 的回退值改为与 guard 分支一致(即只返回 [.apple]),确保
Keys.parallelEngines 的默认恢复行为与新的默认值一致,引用符号:loadParallelEngines(),
Keys.parallelEngines, TranslationEngineType, engines.

In `@ScreenTranslate/Services/TranslationEngine.swift`:
- Around line 76-93: The prefix-match in fromTranslationCode(_:) is reversed:
currently it checks candidate.rawValue.lowercased().hasPrefix(lowercased) which
fails for longer BCP47 inputs like "en-US" or "zh-Hans-CN"; change the predicate
to test whether the input (lowercased) hasPrefix the candidate raw value (e.g.
lowercased.hasPrefix(candidate.rawValue.lowercased())) so region/script variants
map to their base TranslationLanguage. Update the final return that uses
TranslationLanguage.allCases.first(where:) to use this corrected hasPrefix
direction and keep the existing normalization flow (use normalized/lowercased
variables already present).

In `@ScreenTranslate/Services/TranslationService.swift`:
- Around line 404-412: filterEnabledEngines currently lets a config explicitly
disable .apple because it only defaults to true when the config is missing;
change the predicate to short-circuit and always include .apple by first
checking if engine == .apple and returning true, otherwise consult
AppSettings.shared.engineConfigs[engine]?.isEnabled (treat missing entries as
false). Update the filter inside filterEnabledEngines (and reference
AppSettings.shared.engineConfigs and TranslationEngineType.isEnabled) so Apple
is unconditionally kept.

---

Outside diff comments:
In `@ScreenTranslate/Services/PermissionManager.swift`:
- Around line 104-116: The current hasInputMonitoringPermission check and
request flow is using AXIsProcessTrustedWithOptions (accessibility) which is
wrong for Input Monitoring; update the hasInputMonitoringPermission
implementation to call CGPreflightListenEventAccess() to determine actual Input
Monitoring status, and in requestInputMonitoringPermission replace the AX-based
request with CGRequestListenEventAccess() to prompt the system (falling back to
openInputMonitoringSettings() only if CGRequestListenEventAccess() fails or is
insufficient); ensure references to hasInputMonitoringPermission,
requestInputMonitoringPermission, and openInputMonitoringSettings are updated
accordingly so the permission flow uses CGPreflightListenEventAccess() for
checks and CGRequestListenEventAccess() for requests.

In `@ScreenTranslate/Services/TranslationService.swift`:
- Around line 194-237: The concurrent TaskGroup collects EngineResult in
completion order, making the returned `results` unstable; rebuild the final
array to match the input `engines` order: inside the await `withTaskGroup` after
collecting `collectedResults`, create a lookup (e.g., [Engine: EngineResult])
from `collectedResults` keyed by `engine` and then return engines.map {
lookup[$0] ?? EngineResult.failed(engine: $0, error:
TranslationProviderError.translationFailed("no result")) } so the returned
[EngineResult] preserves the original `engines` order (refer to `engines`,
`EngineResult`, and the translateWithResolvedPrompt/resolvedProvider task
block).

---

Nitpick comments:
In `@ScreenTranslate/Features/Settings/MultiEngineSettingsSection.swift`:
- Line 641: The Logger call currently discards its return value using the
redundant `let _ =` pattern; update the invocation in MultiEngineSettingsSection
(the `Logger.settings.info("engineCard \(engine.rawValue):
isEnabled=\(config.isEnabled),
fromDefault=\(viewModel.settings.engineConfigs[engine] == nil)")` line) to
discard the result correctly by removing `let` (either call
`Logger.settings.info(...)` directly or use `_ = Logger.settings.info(...)`), so
the `let _ =` is eliminated.

In `@ScreenTranslate/Features/Settings/SettingsViewModel.swift`:
- Around line 301-307: The nil branch currently removes keys directly via
UserDefaults.standard, bypassing the injected AppSettings and causing
inconsistent backends; add a remove(forKey:) helper to AppSettings (or an
equivalent method on the existing protocol) and change the nil branches in
SettingsViewModel setters (e.g., the translationTargetLanguage setter and the
similar setter at the 338-344 region) to call settings.remove(forKey:
AppSettings.Keys.translationTargetLanguage) instead of
UserDefaults.standard.removeObject(...), ensuring all get/set/remove operations
go through the same AppSettings abstraction.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ab41c3a9-ee8f-45ca-abb5-af85e2214503

📥 Commits

Reviewing files that changed from the base of the PR and between 5d43e54 and c1a81b9.

📒 Files selected for processing (22)
  • ScreenTranslate/App/Coordinators/TextTranslationCoordinator.swift
  • ScreenTranslate/Features/Settings/MultiEngineSettingsSection.swift
  • ScreenTranslate/Features/Settings/SettingsViewModel.swift
  • ScreenTranslate/Features/TextTranslation/TextTranslationFlow.swift
  • ScreenTranslate/Features/TextTranslation/TextTranslationPopupView.swift
  • ScreenTranslate/Features/TextTranslation/TextTranslationPopupWindow.swift
  • ScreenTranslate/Models/AppSettings.swift
  • ScreenTranslate/Resources/de.lproj/Localizable.strings
  • ScreenTranslate/Resources/en.lproj/Localizable.strings
  • ScreenTranslate/Resources/es.lproj/Localizable.strings
  • ScreenTranslate/Resources/fr.lproj/Localizable.strings
  • ScreenTranslate/Resources/it.lproj/Localizable.strings
  • ScreenTranslate/Resources/ja.lproj/Localizable.strings
  • ScreenTranslate/Resources/ko.lproj/Localizable.strings
  • ScreenTranslate/Resources/pt.lproj/Localizable.strings
  • ScreenTranslate/Resources/ru.lproj/Localizable.strings
  • ScreenTranslate/Resources/zh-Hans.lproj/Localizable.strings
  • ScreenTranslate/Services/AppleTranslationProvider.swift
  • ScreenTranslate/Services/PermissionManager.swift
  • ScreenTranslate/Services/Protocols/TranslationServicing.swift
  • ScreenTranslate/Services/TranslationEngine.swift
  • ScreenTranslate/Services/TranslationService.swift

Comment thread ScreenTranslate/App/Coordinators/TextTranslationCoordinator.swift
Comment on lines 368 to +370
var vlmAPIKey: String {
get { settings.vlmAPIKey }
set { settings.vlmAPIKey = newValue }
set { settings.vlmAPIKey = newValue; settings.save(newValue, forKey: AppSettings.Keys.vlmAPIKey) }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

不要把 vlmAPIKey 存到通用设置存储。

这里把敏感凭证走 settings.save(...) 落盘;如果底层是 UserDefaults / plist,这会把 API key 以普通偏好项保存。项目里已经有 KeychainService,这里应改成 Keychain 持久化。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ScreenTranslate/Features/Settings/SettingsViewModel.swift` around lines 368 -
370, 当前的 vlmAPIKey 属性在 setter 中调用 settings.save(..., forKey:
AppSettings.Keys.vlmAPIKey) 将敏感 API key 写入通用设置(可能为 UserDefaults/
plist);请改为使用项目现有的 KeychainService 持久化:在 SettingsViewModel 的 vlmAPIKey setter
中替换对 settings.save 的调用,改为通过 KeychainService 提供的存/取方法保存和读取 vlmAPIKey(保留 getter 对
settings.vlmAPIKey 的读取逻辑或改为从 Keychain 读取,确保使用相同的唯一键名并处理可能的失败/错误情况),并移除将 API key
写入通用设置的路径。

Comment on lines +133 to +169
private func engineResultSection(_ result: EngineTranslationInfo) -> some View {
VStack(alignment: .leading, spacing: 6) {
HStack(spacing: 6) {
if result.isSuccess {
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 11, weight: .medium))
.foregroundStyle(.green)
} else {
Image(systemName: "xmark.circle.fill")
.font(.system(size: 11, weight: .medium))
.foregroundStyle(.red)
}
Text(result.engine.localizedName)
.font(.system(size: 10, weight: .semibold))
.foregroundStyle(result.isSuccess ? .green : .red)
.tracking(0.3)
Spacer()
if result.isSuccess {
Text(String(format: "%.1fs", result.latency))
.font(.system(size: 9, weight: .regular, design: .monospaced))
.foregroundStyle(.tertiary)
}
}

if let text = result.translatedText {
Text(text)
.font(.system(size: 14, weight: .medium))
.foregroundStyle(.primary)
.multilineTextAlignment(.leading)
.textSelection(.enabled)
.frame(maxWidth: .infinity, alignment: .leading)
} else if let error = result.errorMessage {
Text(error)
.font(.system(size: 12, weight: .regular))
.foregroundStyle(.red)
.multilineTextAlignment(.leading)
.frame(maxWidth: .infinity, alignment: .leading)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

多引擎结果卡片丢了 RTL 对齐。

单结果分支会根据语言/文本切到 .trailing,但这里把成功和失败文案都固定成了 .leading。翻译到阿拉伯语、希伯来语这类 RTL 语言时,多引擎视图会退回成错误的阅读方向。

建议修改
     private func engineResultSection(_ result: EngineTranslationInfo) -> some View {
-        VStack(alignment: .leading, spacing: 6) {
+        let previewText = result.translatedText ?? result.errorMessage ?? ""
+        let isResultRTL = Self.isRTLLanguage(targetLanguage) || Self.containsRTLText(previewText)
+
+        VStack(alignment: isResultRTL ? .trailing : .leading, spacing: 6) {
             HStack(spacing: 6) {
                 if result.isSuccess {
                     Image(systemName: "checkmark.circle.fill")
                         .font(.system(size: 11, weight: .medium))
                         .foregroundStyle(.green)
@@
             if let text = result.translatedText {
                 Text(text)
                     .font(.system(size: 14, weight: .medium))
                     .foregroundStyle(.primary)
-                    .multilineTextAlignment(.leading)
+                    .multilineTextAlignment(isResultRTL ? .trailing : .leading)
                     .textSelection(.enabled)
-                    .frame(maxWidth: .infinity, alignment: .leading)
+                    .frame(maxWidth: .infinity, alignment: isResultRTL ? .trailing : .leading)
             } else if let error = result.errorMessage {
                 Text(error)
                     .font(.system(size: 12, weight: .regular))
                     .foregroundStyle(.red)
-                    .multilineTextAlignment(.leading)
-                    .frame(maxWidth: .infinity, alignment: .leading)
+                    .multilineTextAlignment(isResultRTL ? .trailing : .leading)
+                    .frame(maxWidth: .infinity, alignment: isResultRTL ? .trailing : .leading)
             }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ScreenTranslate/Features/TextTranslation/TextTranslationPopupView.swift`
around lines 133 - 169, The translatedText and errorMessage Text views in
engineResultSection currently hardcode .leading alignment; change
engineResultSection to read the environment layout direction (add
`@Environment`(\.layoutDirection) var layoutDirection) and compute an alignment
variable (let textAlignment: TextAlignment = layoutDirection == .rightToLeft ?
.trailing : .leading and let frameAlignment: Alignment = layoutDirection ==
.rightToLeft ? .trailing : .leading) then apply
multilineTextAlignment(textAlignment) and .frame(maxWidth: .infinity, alignment:
frameAlignment) to both the Text(result.translatedText) and Text(error) branches
so RTL languages render with trailing alignment; keep other UI behavior intact
and reuse the same alignment logic for both success and error text branches.

Comment on lines +274 to +278
engineResults: [
EngineTranslationInfo(engine: .mtranServer, translatedText: nil, errorMessage: "Connection refused", latency: 2.1),
EngineTranslationInfo(engine: .apple, translatedText: "你好,今天怎么样?", errorMessage: nil, latency: 0.5),
EngineTranslationInfo(engine: .google, translatedText: "你好,你今天好吗?", errorMessage: nil, latency: 1.0),
],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

删掉预览数组最后一项后的多余逗号。

这个会直接触发当前 SwiftLint 的 trailing_comma 警告。

🧰 Tools
🪛 SwiftLint (0.63.2)

[Warning] 277-277: Collection literals should not have trailing commas

(trailing_comma)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ScreenTranslate/Features/TextTranslation/TextTranslationPopupView.swift`
around lines 274 - 278, Remove the trailing comma after the last element in the
engineResults array literal (the EngineTranslationInfo entries) so the array no
longer ends with an extraneous comma; update the array in
TextTranslationPopupView where engineResults is defined/initialized to ensure
the final EngineTranslationInfo item has no following comma to satisfy
SwiftLint's trailing_comma rule.

Comment thread ScreenTranslate/Models/AppSettings.swift Outdated
Comment thread ScreenTranslate/Services/TranslationEngine.swift Outdated
Comment thread ScreenTranslate/Services/TranslationService.swift
- Fix prefix match direction in fromTranslationCode (en-US → .english now works)
- Fix dirty data fallback to return [.apple] consistently
- Fix filterEnabledEngines to truly always include Apple engine
- Fix showTranslationError fallback key to avoid %@ placeholder issue
@hubo1989
Copy link
Copy Markdown
Owner Author

CodeRabbit Review Replies

#1 TextTranslationCoordinator L250 — ✅ Fixed in 90f521a. Changed fallback key to error.translation.failed to avoid %@ placeholder issue.

#2 SettingsViewModel L370 — Acknowledged. Pre-existing issue (vlmAPIKey in UserDefaults). Will migrate to Keychain in a separate PR.

#3 TextTranslationPopupView L169 — Acknowledged. Pre-existing issue. Will add RTL alignment in a follow-up i18n PR.

#4 TextTranslationPopupView L278 — Acknowledged. Pre-existing trailing comma. Will fix separately.

#5 AppSettings — ✅ Fixed in 90f521a. Dirty data fallback now returns [.apple] consistently.

#6 TranslationEngine — ✅ Fixed in 90f521a. Prefix match direction corrected: input hasPrefix candidate, so "en-US" and "zh-Hans-CN" now match correctly.

#7 TranslationService L412 — ✅ Fixed in 90f521a. Changed to engine == .apple || configs[engine]?.isEnabled == true so Apple is always included regardless of config.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
ScreenTranslate/Models/AppSettings.swift (1)

99-99: 建议保持 Keys 枚举为 private

Keysprivate 改为 internal 会暴露 UserDefaults 的实现细节。外部调用方应通过公开的 save* 方法来持久化数据,而不需要直接访问键名。如果确实有外部需要访问键名的场景,建议明确记录在注释中。

建议保持封装
-    enum Keys {
+    private enum Keys {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ScreenTranslate/Models/AppSettings.swift` at line 99, The Keys enum currently
exposes UserDefaults key names; make it private to retain encapsulation by
changing the visibility of enum Keys to private and ensuring all internal
references (e.g., in saveFoo/saveBar and related load methods in AppSettings)
still access Keys directly; if external consumers really need key names, add a
documented public accessor or comment rather than leaving Keys non‑private.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ScreenTranslate/App/Coordinators/TextTranslationCoordinator.swift`:
- Around line 89-95: The current flow in TextTranslationCoordinator returns
false after calling permissionManager.requestAccessibilityPermission() without
informing the user if the permission was previously denied; update the logic in
the method that uses permissionManager.hasAccessibilityPermission to, after
calling requestAccessibilityPermission(), re-check
permissionManager.hasAccessibilityPermission and if it remains false call
showPermissionDeniedError(for: .accessibility) to present the "Open System
Settings" guidance to the user (this targets the block around
permissionManager.requestAccessibilityPermission() in TextTranslationCoordinator
and uses the existing showPermissionDeniedError(for:) helper).

---

Nitpick comments:
In `@ScreenTranslate/Models/AppSettings.swift`:
- Line 99: The Keys enum currently exposes UserDefaults key names; make it
private to retain encapsulation by changing the visibility of enum Keys to
private and ensuring all internal references (e.g., in saveFoo/saveBar and
related load methods in AppSettings) still access Keys directly; if external
consumers really need key names, add a documented public accessor or comment
rather than leaving Keys non‑private.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 159aed24-f84b-431c-aeca-e02c81ef53b6

📥 Commits

Reviewing files that changed from the base of the PR and between c1a81b9 and 90f521a.

📒 Files selected for processing (4)
  • ScreenTranslate/App/Coordinators/TextTranslationCoordinator.swift
  • ScreenTranslate/Models/AppSettings.swift
  • ScreenTranslate/Services/TranslationEngine.swift
  • ScreenTranslate/Services/TranslationService.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • ScreenTranslate/Services/TranslationService.swift

Comment thread ScreenTranslate/App/Coordinators/TextTranslationCoordinator.swift
System prompt is shown first (no custom dialog). If user denies,
show "Open System Settings" guidance to help them grant permission later.
@hubo1989
Copy link
Copy Markdown
Owner Author

Updated reply to inline review

TextTranslationCoordinator L89-95 — ✅ Fixed in cf984b0. Now re-checks hasAccessibilityPermission after the system prompt. If still denied (user clicked Deny), shows showPermissionDeniedError to guide user to System Settings. This preserves the fix for double dialogs while ensuring denied users get actionable guidance.

AppSettings Keys enum visibility — Acknowledged as nitpick. Pre-existing design choice, not in scope for this PR. Will consider in a future settings refactor.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ScreenTranslate/App/Coordinators/TextTranslationCoordinator.swift`:
- Around line 89-97: After calling
permissionManager.requestAccessibilityPermission(), re-check
permissionManager.hasAccessibilityPermission and return true if it is now
granted; only call permissionManager.showPermissionDeniedError(for:
.accessibility) and return false when the updated hasAccessibilityPermission
remains false. Update the logic in TextTranslationCoordinator (the block using
permissionManager.hasAccessibilityPermission,
permissionManager.requestAccessibilityPermission(), and
permissionManager.showPermissionDeniedError(for:)) so the post-request decision
is based on the refreshed hasAccessibilityPermission state instead of
unconditionally returning false.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 15c8cd99-1f06-4ed7-8fc2-6541952e1c6b

📥 Commits

Reviewing files that changed from the base of the PR and between 90f521a and cf984b0.

📒 Files selected for processing (1)
  • ScreenTranslate/App/Coordinators/TextTranslationCoordinator.swift

Comment thread ScreenTranslate/App/Coordinators/TextTranslationCoordinator.swift
Re-check hasAccessibilityPermission after requestAccessibilityPermission().
If user granted in the system prompt, return true and continue the flow.
Only show System Settings guidance when still denied after refresh.
@hubo1989
Copy link
Copy Markdown
Owner Author

TextTranslationCoordinator L89-97 — ✅ Fixed in cf984b0. Now refreshes permission status after the system prompt via permissionManager.refreshPermissionStatus(). If user granted permission, returns true and proceeds with the operation. Only shows System Settings guidance when the refreshed state remains false.

@hubo1989 hubo1989 merged commit 40e49a5 into main Mar 25, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant