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.
|
[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.
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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