fix: use exact MyMemory matches when top translation is blank
This commit is contained in:
@@ -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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -26,18 +26,35 @@ public enum MyMemoryAPI {
|
||||
guard result.quotaFinished != true, result.responseStatus != 429 else {
|
||||
throw Failure(message: "MyMemory's anonymous daily quota is exhausted. Try later or choose OpenAI.")
|
||||
}
|
||||
guard result.responseStatus == 200,
|
||||
let translation = result.responseData?.translatedText,
|
||||
!translation.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
||||
guard result.responseStatus == 200 else {
|
||||
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
|
||||
}
|
||||
// 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 {
|
||||
let responseStatus: Int
|
||||
let quotaFinished: Bool?
|
||||
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? }
|
||||
}
|
||||
private struct Failure: LocalizedError {
|
||||
|
||||
@@ -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)!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user