fix: use exact MyMemory matches when top translation is blank

This commit is contained in:
Valdemar Lorenzen
2026-10-15 10:52:17 +02:00
parent d0a66c29a8
commit c31424b217
3 changed files with 55 additions and 4 deletions
+2
View File
@@ -87,6 +87,8 @@ Choose **MyMemory (no key)** or **OpenAI · gpt-6-luna** from the provider dropd
[MyMemory](https://mymemory.translated.net/doc/spec.php) requires no account or key. Anonymous use allows 500 UTF-8 bytes per request and [5,000 characters per day](https://mymemory.translated.net/doc/usagelimits.php). Oversized input is rejected before sending; quota and service errors appear in the tool. Text is sent to a third-party translation service: avoid sensitive content. [MyMemory](https://mymemory.translated.net/doc/spec.php) requires no account or key. Anonymous use allows 500 UTF-8 bytes per request and [5,000 characters per day](https://mymemory.translated.net/doc/usagelimits.php). Oversized input is rejected before sending; quota and service errors appear in the tool. Text is sent to a third-party translation service: avoid sensitive content.
If MyMemory's top translation is blank, Conjure uses the first nonblank exact English→Japanese match in that same response. Approximate matches are not substituted, and service/quota failures still surface as errors.
For **OpenAI**, enter an API key and choose **Save Key** (or translate to save it first). Keys are stored in macOS Keychain, not preferences; **Forget Key** removes the saved key. This provider uses OpenAI's Responses API with exactly `gpt-6-luna`. Your account must have access to that model and sufficient API billing. Requests disable API response storage (`store: false`); OpenAI's own data-retention policies still apply. MyMemory never reads or sends the OpenAI key. For **OpenAI**, enter an API key and choose **Save Key** (or translate to save it first). Keys are stored in macOS Keychain, not preferences; **Forget Key** removes the saved key. This provider uses OpenAI's Responses API with exactly `gpt-6-luna`. Your account must have access to that model and sufficient API billing. Requests disable API response storage (`store: false`); OpenAI's own data-retention policies still apply. MyMemory never reads or sends the OpenAI key.
Translations appear as selectable Japanese text; use Command-C to copy a selection. Editing input or leaving the tool cancels the pending request and prevents stale results from appearing. Typing alone never sends text. Application-only actions and numbered launching are disabled inside the tool. Translations appear as selectable Japanese text; use Command-C to copy a selection. Editing input or leaving the tool cancels the pending request and prevents stale results from appearing. Typing alone never sends text. Application-only actions and numbered launching are disabled inside the tool.
+20 -3
View File
@@ -26,18 +26,35 @@ public enum MyMemoryAPI {
guard result.quotaFinished != true, result.responseStatus != 429 else { guard result.quotaFinished != true, result.responseStatus != 429 else {
throw Failure(message: "MyMemory's anonymous daily quota is exhausted. Try later or choose OpenAI.") throw Failure(message: "MyMemory's anonymous daily quota is exhausted. Try later or choose OpenAI.")
} }
guard result.responseStatus == 200, guard result.responseStatus == 200 else {
let translation = result.responseData?.translatedText,
!translation.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
throw Failure(message: "MyMemory returned no successful translation. Try again later or choose OpenAI.") throw Failure(message: "MyMemory returned no successful translation. Try again later or choose OpenAI.")
} }
if let translation = result.responseData?.translatedText,
!translation.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
return translation return translation
} }
// The service can rank an empty memory first; only use another exact en→ja match.
if let match = result.matches?.first(where: {
$0.segment == text && $0.source.split(separator: "-").first == "en"
&& $0.target.split(separator: "-").first == "ja"
&& !$0.translation.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}) {
return match.translation
}
throw Failure(message: "MyMemory returned no successful translation. Try again later or choose OpenAI.")
}
private struct Response: Decodable { private struct Response: Decodable {
let responseStatus: Int let responseStatus: Int
let quotaFinished: Bool? let quotaFinished: Bool?
let responseData: Translation? let responseData: Translation?
let matches: [Match]?
struct Match: Decodable {
let segment: String
let translation: String
let source: String
let target: String
}
struct Translation: Decodable { let translatedText: String? } struct Translation: Decodable { let translatedText: String? }
} }
private struct Failure: LocalizedError { private struct Failure: LocalizedError {
+32
View File
@@ -28,3 +28,35 @@ import Testing
} }
} }
} }
@Test func myMemoryUsesNonblankExactMatchWhenPrimaryTranslationIsEmpty() async throws {
// MyMemory's live response for "hello" has a blank top translation and a valid exact match.
let body = #"{"responseStatus":200,"quotaFinished":false,"responseData":{"translatedText":""},"matches":[{"segment":"hello","translation":"","source":"en-GB","target":"ja-JP"},{"segment":"hello there","translation":"近似","source":"en","target":"ja"},{"segment":"hello","translation":"bonjour","source":"en","target":"fr"},{"segment":"hello","translation":"こんにちは","source":"en-GB","target":"ja-JP"}]}"#
let translation = try await MyMemoryAPI.translate("hello") { request in
(Data(body.utf8), HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!)
}
#expect(translation == "こんにちは")
}
@Test func myMemoryNeverUsesMatchesToBypassFailureOrReplacePrimaryTranslation() async throws {
let matches = #""matches":[{"segment":"hello","translation":"こんにちは","source":"en","target":"ja"}]"#
for metadata in [#""responseStatus":403"#, #""responseStatus":200,"quotaFinished":true"#] {
await #expect(throws: (any Error).self) {
try await MyMemoryAPI.translate("hello") { request in
(Data("{\(metadata),\(matches)}".utf8),
HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!)
}
}
}
let primary = try await MyMemoryAPI.translate("hello") { request in
(Data("{\"responseStatus\":200,\"responseData\":{\"translatedText\":\"どうも\"},\(matches)}".utf8),
HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!)
}
#expect(primary == "どうも")
await #expect(throws: (any Error).self) {
try await MyMemoryAPI.translate("unrelated") { request in
(Data("{\"responseStatus\":200,\(matches)}".utf8),
HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!)
}
}
}