diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index 42667ab..bad87b9 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -7048,177 +7048,282 @@ } } }, - "Ready using another installed translation mode.": { + "Source language to check": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Ready using another installed translation mode." + "value": "Source language to check" } }, "ko": { "stringUnit": { "state": "translated", - "value": "설치된 다른 번역 방식으로 번역할 수 있어요." + "value": "준비 상태를 확인할 원문 언어" } }, "ja": { "stringUnit": { "state": "translated", - "value": "インストール済みの別の翻訳方式で翻訳できます。" + "value": "準備状況を確認する原文の言語" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "可使用另一种已安装的翻译模式进行翻译。" + "value": "要检查准备状态的源语言" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "可使用另一種已安裝的翻譯模式進行翻譯。" + "value": "要檢查準備狀態的來源語言" } } } }, - "Source language to check": { + "This checks only this language pair. Your source-language setting stays unchanged.": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Source language to check" + "value": "This checks only this language pair. Your source-language setting stays unchanged." } }, "ko": { "stringUnit": { "state": "translated", - "value": "준비 상태를 확인할 원문 언어" + "value": "선택한 두 언어의 준비 상태만 확인해요. 앱의 원문 언어 설정은 바뀌지 않아요." } }, "ja": { "stringUnit": { "state": "translated", - "value": "準備状況を確認する原文の言語" + "value": "選んだ言語の組み合わせだけを確認します。アプリの原文言語の設定は変わりません。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "要检查准备状态的源语言" + "value": "仅检查所选语言组合,应用的源语言设置不会更改。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "要檢查準備狀態的來源語言" + "value": "僅檢查所選語言組合,App 的來源語言設定不會變更。" } } } }, - "This checks only this language pair. Your source-language setting stays unchanged.": { + "This language pair is not supported. Choose another language.": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "This checks only this language pair. Your source-language setting stays unchanged." + "value": "This language pair is not supported. Choose another language." } }, "ko": { "stringUnit": { "state": "translated", - "value": "선택한 두 언어의 준비 상태만 확인해요. 앱의 원문 언어 설정은 바뀌지 않아요." + "value": "지원하지 않는 언어 조합이에요. 다른 언어를 선택해 주세요." } }, "ja": { "stringUnit": { "state": "translated", - "value": "選んだ言語の組み合わせだけを確認します。アプリの原文言語の設定は変わりません。" + "value": "この言語の組み合わせには対応していません。別の言語を選んでください。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "仅检查所选语言组合,应用的源语言设置不会更改。" + "value": "不支持此语言组合,请选择其他语言。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "僅檢查所選語言組合,App 的來源語言設定不會變更。" + "value": "不支援此語言組合,請選擇其他語言。" } } } }, - "This language pair is not supported. Choose another language.": { + "Translations that follow the page": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "This language pair is not supported. Choose another language." + "value": "Translations that follow the page" } }, "ko": { "stringUnit": { "state": "translated", - "value": "지원하지 않는 언어 조합이에요. 다른 언어를 선택해 주세요." + "value": "원문의 흐름을 살리는 번역" } }, "ja": { "stringUnit": { "state": "translated", - "value": "この言語の組み合わせには対応していません。別の言語を選んでください。" + "value": "原文の流れに沿った翻訳" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "不支持此语言组合,请选择其他语言。" + "value": "保留原文结构的翻译" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "不支援此語言組合,請選擇其他語言。" + "value": "保留原文結構的翻譯" } } } }, - "Translations that follow the page": { + "Download languages for the selected translation mode.": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Translations that follow the page" + "value": "Download languages for the selected translation mode." } }, "ko": { "stringUnit": { "state": "translated", - "value": "원문의 흐름을 살리는 번역" + "value": "선택한 번역 방식의 언어를 다운로드해 주세요." } }, "ja": { "stringUnit": { "state": "translated", - "value": "原文の流れに沿った翻訳" + "value": "選択した翻訳方式の言語をダウンロードしてください。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "保留原文结构的翻译" + "value": "请下载所选翻译模式的语言。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "保留原文結構的翻譯" + "value": "請下載所選翻譯模式的語言。" + } + } + } + }, + "You can translate now using another installed mode.": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "You can translate now using another installed mode." + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "지금은 설치된 다른 방식으로 번역할 수 있어요." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "今はインストール済みの別の方式で翻訳できます。" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "目前可使用另一种已安装的模式进行翻译。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "目前可使用另一種已安裝的模式進行翻譯。" + } + } + } + }, + "The selected translation mode does not support this language pair.": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The selected translation mode does not support this language pair." + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "선택한 번역 방식은 이 언어 조합을 지원하지 않아요." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "選択した翻訳方式はこの言語の組み合わせに対応していません。" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "所选翻译模式不支持此语言组合。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "所選翻譯模式不支援此語言組合。" + } + } + } + }, + "Download the languages to use another translation mode.": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Download the languages to use another translation mode." + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "다른 번역 방식을 사용하려면 언어를 다운로드해 주세요." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "別の翻訳方式を使うには、言語をダウンロードしてください。" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "请下载语言以使用另一种翻译模式。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "請下載語言以使用另一種翻譯模式。" } } } diff --git a/Sources/Dependencies/LanguageCatalogClient.swift b/Sources/Dependencies/LanguageCatalogClient.swift index 03ced71..2718811 100644 --- a/Sources/Dependencies/LanguageCatalogClient.swift +++ b/Sources/Dependencies/LanguageCatalogClient.swift @@ -37,8 +37,7 @@ extension LanguageCatalogClient: DependencyKey { let alternative: TranslationStrategy = strategy == .lowLatency ? .highFidelity : .lowLatency let other = await LanguageAvailability(preferredStrategy: alternative.sessionStrategy) .status(from: source.localeLanguage, to: target.localeLanguage) - if other == .installed { return .alternativeInstalled } - return status == .unsupported && other == .unsupported ? .unsupported : .downloadRequired + return LanguageReadiness.resolve(preferred: status, alternative: other) } let status = await LanguageAvailability().status(from: source.localeLanguage, to: target.localeLanguage) switch status { @@ -66,7 +65,35 @@ enum LanguageReadiness: Equatable, Sendable { case checking case installed case alternativeInstalled + case preferredUnsupported + case alternativeDownloadRequired case downloadRequired case unsupported case sameLanguage + + // MARK: Internal + + var needsLanguageDownload: Bool { + switch self { + case .downloadRequired, + .alternativeInstalled, + .alternativeDownloadRequired: true + default: false + } + } + + /// An installed fallback is usable but is not readiness of the selected mode. + static func resolve(preferred: LanguageAvailability.Status, alternative: LanguageAvailability.Status) -> Self { + switch (preferred, alternative) { + case (.installed, _): .installed + case (.supported, .installed): .alternativeInstalled + case (.unsupported, .installed): .preferredUnsupported + case (.unsupported, .supported): .alternativeDownloadRequired + case (.supported, .supported), + (.supported, .unsupported): .downloadRequired + case (.unsupported, .unsupported): .unsupported + default: .unchecked + } + } + } diff --git a/Sources/Onboarding/OnboardingView.swift b/Sources/Onboarding/OnboardingView.swift index 5dee5a6..1624e1f 100644 --- a/Sources/Onboarding/OnboardingView.swift +++ b/Sources/Onboarding/OnboardingView.swift @@ -129,7 +129,7 @@ struct OnboardingView: View { languageReadiness Text("This checks only this language pair. Your source-language setting stays unchanged.") .font(.caption).foregroundStyle(.secondary) - if store.modelReadiness == .downloadRequired { + if store.modelReadiness.needsLanguageDownload { Text( "Download the languages you want to translate from and into. For English → Korean, download both English and Korean." ) @@ -168,7 +168,25 @@ struct OnboardingView: View { Label("Ready to translate this language pair.", systemImage: "checkmark.circle.fill").foregroundStyle(.green) case .alternativeInstalled: - Label("Ready using another installed translation mode.", systemImage: "checkmark.circle.fill").foregroundStyle(.green) + VStack(alignment: .leading, spacing: 6) { + Label("Download languages for the selected translation mode.", systemImage: "exclamationmark.triangle.fill") + .foregroundStyle(.orange) + Text("You can translate now using another installed mode.").font(.callout).foregroundStyle(.secondary) + } + + case .preferredUnsupported: + VStack(alignment: .leading, spacing: 6) { + Label("The selected translation mode does not support this language pair.", systemImage: "exclamationmark.triangle.fill") + .foregroundStyle(.orange) + Text("You can translate now using another installed mode.").font(.callout).foregroundStyle(.secondary) + } + + case .alternativeDownloadRequired: + VStack(alignment: .leading, spacing: 6) { + Label("The selected translation mode does not support this language pair.", systemImage: "exclamationmark.triangle.fill") + .foregroundStyle(.orange) + Text("Download the languages to use another translation mode.").font(.callout).foregroundStyle(.secondary) + } case .downloadRequired: Label("Download these languages to start translating.", systemImage: "arrow.down.circle").foregroundStyle(.orange) diff --git a/Tests/OnboardingTests.swift b/Tests/OnboardingTests.swift index 6818478..48c5d77 100644 --- a/Tests/OnboardingTests.swift +++ b/Tests/OnboardingTests.swift @@ -7,6 +7,7 @@ import Foundation import Sharing import Testing import TOML +import Translation @testable import SwiftyCrow @MainActor @@ -389,7 +390,15 @@ struct OnboardingTests { #expect(field.hotKey == value) } - @Test(arguments: [LanguageReadiness.installed, .alternativeInstalled, .downloadRequired, .unsupported, .sameLanguage]) + @Test(arguments: [ + LanguageReadiness.installed, + .alternativeInstalled, + .preferredUnsupported, + .alternativeDownloadRequired, + .downloadRequired, + .unsupported, + .sameLanguage, + ]) func setupReportsReadinessForTheSelectedPair(_ readiness: LanguageReadiness) async { var initial = state() initial.isPresented = true @@ -475,6 +484,20 @@ struct OnboardingTests { #expect(store.state.modelReadiness == .downloadRequired) } + @Test + func alternativeModesDistinguishMissingDownloadsFromUnsupportedPairs() { + #expect(LanguageReadiness.resolve(preferred: .installed, alternative: .installed) == .installed) + #expect(LanguageReadiness.resolve(preferred: .supported, alternative: .installed) == .alternativeInstalled) + #expect(LanguageReadiness.alternativeInstalled.needsLanguageDownload) + #expect(LanguageReadiness.resolve(preferred: .unsupported, alternative: .installed) == .preferredUnsupported) + #expect(!LanguageReadiness.preferredUnsupported.needsLanguageDownload) + #expect(LanguageReadiness.resolve(preferred: .unsupported, alternative: .supported) == .alternativeDownloadRequired) + #expect(LanguageReadiness.alternativeDownloadRequired.needsLanguageDownload) + #expect(LanguageReadiness.resolve(preferred: .unsupported, alternative: .unsupported) == .unsupported) + #expect(!LanguageReadiness.unsupported.needsLanguageDownload) + #expect(LanguageReadiness.resolve(preferred: .supported, alternative: .unsupported) == .downloadRequired) + } + // MARK: Private private func shortcutEvent(_ keyCode: UInt16, modifiers: NSEvent.ModifierFlags = []) throws -> NSEvent {