From 7b32548a4f404ed32f8f9e3de5d11e52c911dcac Mon Sep 17 00:00:00 2001 From: alexanderschmidt2 <67632384+alexanderschmidt2@users.noreply.github.com> Date: Sat, 10 Oct 2026 10:18:29 +0200 Subject: [PATCH] Use the app language for Apple coach replies and briefs --- Strand/AI/AICoach.swift | 16 ++++++++++++++-- StrandTests/AICoachPromptAndStressTests.swift | 19 +++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/Strand/AI/AICoach.swift b/Strand/AI/AICoach.swift index 612cca5a22..6e505666af 100644 --- a/Strand/AI/AICoach.swift +++ b/Strand/AI/AICoach.swift @@ -298,6 +298,18 @@ final class AICoachEngine: ObservableObject { return Self.defaultSystemPrompt } + /// The running bundle's language also applies to briefs and replies to English context/history. + var requestSystemPrompt: String { + Self.localizedSystemPrompt(systemPrompt, languageTag: Bundle.main.preferredLocalizations.first ?? "en") + } + + static func localizedSystemPrompt(_ prompt: String, languageTag: String) -> String { + let tag = languageTag.trimmingCharacters(in: .whitespacesAndNewlines) + return prompt + "\n\nReply in the app's language (BCP-47: \(tag.isEmpty ? "en" : tag)). " + + "Use this language even if the context or earlier messages are in another language, " + + "unless the user explicitly requests a different language." + } + /// The user's stored prompt override, or the default when nothing custom is set. The UI binds its /// editor to this: writing persists the override; writing a blank string clears it (back to default). var customSystemPrompt: String { @@ -1048,7 +1060,7 @@ final class AICoachEngine: ObservableObject { try await provider.client.send( key: key, model: model, - systemPrompt: systemPrompt, + systemPrompt: requestSystemPrompt, messages: messages, session: session ) @@ -1065,7 +1077,7 @@ final class AICoachEngine: ObservableObject { try await provider.client.streamWithImage( key: key, model: model, - systemPrompt: systemPrompt, + systemPrompt: requestSystemPrompt, messages: messages, inlineImage: inlineImage, session: session, diff --git a/StrandTests/AICoachPromptAndStressTests.swift b/StrandTests/AICoachPromptAndStressTests.swift index 11b17950f9..abab0a0798 100644 --- a/StrandTests/AICoachPromptAndStressTests.swift +++ b/StrandTests/AICoachPromptAndStressTests.swift @@ -69,6 +69,25 @@ final class AICoachPromptAndStressTests: XCTestCase { XCTAssertFalse(engine.hasCustomSystemPrompt) } + func testReplyLanguageFollowsAppLanguageWhilePreservingEditedPrompt() { + let engine = makeEngine() + engine.customSystemPrompt = "Coach in two sentences." + XCTAssertEqual(engine.systemPrompt, "Coach in two sentences.") + XCTAssertEqual(engine.requestSystemPrompt, AICoachEngine.localizedSystemPrompt( + engine.systemPrompt, languageTag: Bundle.main.preferredLocalizations.first ?? "en" + )) + for tag in ["de", "en", "es", "fr", "it", "pt-PT", "pl", "zh", "de-DE"] { + let prompt = AICoachEngine.localizedSystemPrompt(engine.systemPrompt, languageTag: tag) + XCTAssertEqual(prompt, "Coach in two sentences.\n\nReply in the app's language (BCP-47: \(tag)). " + + "Use this language even if the context or earlier messages are in another language, " + + "unless the user explicitly requests a different language.") + } + XCTAssertEqual( + AICoachEngine.localizedSystemPrompt("Coach", languageTag: " \n "), + AICoachEngine.localizedSystemPrompt("Coach", languageTag: "en") + ) + } + // MARK: - Feature 2: derived stress line func testStressIndexSummaryFormatsOneRoundedNumber() {