Skip to content

feat: add Output Language setting for automatic translation#102

Merged
marcbodea merged 5 commits into
zachlatta:mainfrom
marwei:feat/output-language-translation
Apr 25, 2026
Merged

feat: add Output Language setting for automatic translation#102
marcbodea merged 5 commits into
zachlatta:mainfrom
marwei:feat/output-language-translation

Conversation

@marwei

@marwei marwei commented Apr 20, 2026

Copy link
Copy Markdown

Summary

  • Adds an Output Language picker in Settings > General (English, Chinese, Spanish, French, Japanese, Korean, German, Portuguese)
  • When set, the post-processing system prompt is modified to translate transcribed speech into the selected language
  • Works for both dictation mode and edit/command mode

Test plan

  • Open Settings > General, confirm "Output Language" card appears after API Key
  • Set to "Spanish", speak English, verify output is in Spanish
  • Set to "Same as spoken", speak English, verify output stays in English
  • Check Run Log to confirm the modified system prompt includes translation instructions
  • Test edit mode with output language set — verify transformed text is in target language

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added an "Output Language" option in General Settings with a "Same as spoken" choice plus multiple languages.
    • Selected preference is saved persistently and restored across launches.
    • The chosen output language is applied to transcription post-processing, including retry flows, and to command/edit transformations so results are produced in the selected language.

Add a new Output Language picker in Settings > General that translates
transcribed speech into a selected language during post-processing.
When set (e.g. English), the system prompt is modified to translate
instead of preserving the original language. Supports English, Chinese,
Spanish, French, Japanese, Korean, German, and Portuguese.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a persisted outputLanguage setting to AppState, threads it through transcript processing and retry flows, extends PostProcessingService APIs and prompt construction to accept/apply an output-language hint, and exposes an "Output Language" picker in settings.

Changes

Cohort / File(s) Summary
State Management
Sources/AppState.swift
Added @Published var outputLanguage persisted to UserDefaults (output_language). Extended processTranscript(...) signature to accept outputLanguage and propagated it through retry (retryTranscription) and main stop-and-transcribe call sites.
Post-processing & Prompt Logic
Sources/PostProcessingService.swift
Added outputLanguage: String = "" parameter to postProcess and commandTransform, propagated through helpers. Introduced static func applyOutputLanguage(_ prompt: String, language: String) -> String and integrated language-based instructions into system-prompt construction for both standard and command-transform flows.
Settings UI
Sources/SettingsView.swift
Added an "Output Language" SettingsCard/Picker in GeneralSettingsView, bound to $appState.outputLanguage with a "Same as spoken" ("") option and explanatory caption.

Sequence Diagram

sequenceDiagram
    actor User
    participant SettingsView
    participant AppState
    participant SessionController
    participant PostProcessingService
    participant LLM

    User->>SettingsView: choose output language
    SettingsView->>AppState: set outputLanguage
    AppState->>AppState: persist to UserDefaults

    User->>SessionController: submit/stop transcript
    SessionController->>AppState: read outputLanguage
    AppState-->>SessionController: return outputLanguage
    SessionController->>PostProcessingService: postProcess(transcript,..., outputLanguage)
    PostProcessingService->>PostProcessingService: applyOutputLanguage(systemPrompt, language)
    PostProcessingService->>LLM: send modified prompt + transcript
    LLM-->>PostProcessingService: return processed transcript
    PostProcessingService-->>SessionController: return final transcript
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I nibble at prompts and learn a tune,
Hopting between languages by afternoon.
A setting picked, the transcript bends,
From spoken hops to chosen ends.
✨📝

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the main feature: adding an output language setting for automatic translation. It directly reflects the core changes across all three modified files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/PostProcessingService.swift`:
- Around line 555-569: applyOutputLanguage currently assumes three exact bullet
strings and fails when customSystemPrompt changes; update applyOutputLanguage to
be robust by (1) searching for and removing or replacing any existing lines or
bullets that mention translation/preservation using pattern matching (e.g.,
regex matches for lines containing "translate", "preserve", "translation", or
"mixed-language") rather than exact literals, and then (2) appending a clear
fallback instruction like "Translate the final cleaned text into {language}.
Output ONLY in {language}." if nothing was replaced. Ensure you modify the
applyOutputLanguage(_ prompt: String, language: String) implementation so it
operates idempotently on prompts passed from process() (including user-supplied
customSystemPrompt) and always enforces the desired translation directive.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b605e8c0-c843-4dcb-876c-67529ca89036

📥 Commits

Reviewing files that changed from the base of the PR and between f8c43ad and bd6f4ad.

📒 Files selected for processing (3)
  • Sources/AppState.swift
  • Sources/PostProcessingService.swift
  • Sources/SettingsView.swift

Comment thread Sources/PostProcessingService.swift
wei and others added 2 commits April 20, 2026 17:01
When the user has a custom system prompt, the exact string replacements
may not match. Now appends a translation directive as fallback when none
of the default prompt lines were found.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Instead of fragile string replacements that break with custom prompts,
just append the translation directive at the end. Simpler and works
universally.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
Sources/PostProcessingService.swift (1)

472-479: Consider using append approach for consistency with dictation mode.

Command mode uses string replacement while dictation mode (line 372) uses the append-based applyOutputLanguage. Although the replacement targets a constant defined in this file and works correctly, a future edit to commandModeSystemPrompt could silently break this without compile-time detection.

For consistency and resilience, consider appending the language directive instead:

♻️ Suggested refactor
         var systemPrompt = Self.commandModeSystemPrompt
         let trimmedOutputLanguage = outputLanguage.trimmingCharacters(in: .whitespacesAndNewlines)
         if !trimmedOutputLanguage.isEmpty {
-            systemPrompt = systemPrompt.replacingOccurrences(
-                of: "- Preserve the original language unless VOICE_COMMAND explicitly requests translation.",
-                with: "- Output the result in \(trimmedOutputLanguage)."
-            )
+            systemPrompt += "\n\nIMPORTANT: Output the result in \(trimmedOutputLanguage), regardless of the original language of SELECTED_TEXT."
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/PostProcessingService.swift` around lines 472 - 479, The current code
mutates systemPrompt using replacingOccurrences which can break silently if
Self.commandModeSystemPrompt changes; instead, use the same append-based helper
used in dictation mode: call the existing applyOutputLanguage helper (the same
one referenced around line 372) to append the output language directive to
systemPrompt when trimmedOutputLanguage is non-empty, referencing systemPrompt
and Self.commandModeSystemPrompt so the logic matches dictation mode and avoids
fragile string replacements.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@Sources/PostProcessingService.swift`:
- Around line 472-479: The current code mutates systemPrompt using
replacingOccurrences which can break silently if Self.commandModeSystemPrompt
changes; instead, use the same append-based helper used in dictation mode: call
the existing applyOutputLanguage helper (the same one referenced around line
372) to append the output language directive to systemPrompt when
trimmedOutputLanguage is non-empty, referencing systemPrompt and
Self.commandModeSystemPrompt so the logic matches dictation mode and avoids
fragile string replacements.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9026557f-5347-4c2d-a84e-e966cd99bad6

📥 Commits

Reviewing files that changed from the base of the PR and between 9cf9261 and fe32efe.

📒 Files selected for processing (1)
  • Sources/PostProcessingService.swift

@marcbodea

Copy link
Copy Markdown
Collaborator

Hi, the idea of having translation built in is great. I would like to discuss the implementation in more detail:

For edit mode, not sure this is needed as the speaker can just say "translate this to x language" and it seems to work well from my testing. Have you encountered any issues with that?

I am hesitant to add the persistent settings toggle as I am not sure what the use case is for having all dictations translated. What do you think about adding a "trigger translation" phrase that if found in the transcription will add the "translate this to x language" to the prompt? I will test a couple of runs to see if having it in the user prompt is enough.

In general I think it's better to not dynamically modify the system prompt between runs as that would bust the cache on Groq and cause extra usage.

@marwei

marwei commented Apr 21, 2026

Copy link
Copy Markdown
Author

Thanks for taking the time to dig into this — these are fair concerns.

I see this primarily as a UX improvement for non-native English speakers, a user persona underserved by the trigger-phrase alternative.

Consider a non-native English speaker who uses Flow throughout their workday — Slack messages, emails, PR descriptions, Linear tickets, docs. Their native language might be Mandarin, Spanish, Portuguese, German, Hindi, whatever. Their output language, however, is English, essentially 100% of the time in a work context. For this user, translation isn't an occasional intent they opt into per-utterance; it's the stable default of their entire workflow. You're right that "translate this to X" works well when someone consciously wants a translation for a specific utterance — but asking this user to prepend it to every single dictation, or to remember a trigger phrase each time, turns what should be a fluent dictation experience into a constant cognitive tax, and worse, a constant reminder that the tool wasn't really built with them in mind. A trigger phrase solves the mechanics but not the ergonomics.

This is actually one of the higher-leverage UX improvements Flow could make for non-English-speaking users, and a real lever for adoption beyond the native-English audience. The population of knowledge workers worldwide who think in one language and write professionally in another is enormous, and right now Flow implicitly assumes input language == output language. A persistent setting is the simplest, most honest way to say "your output language is a preference, not a per-utterance decision."

On the Groq caching concern specifically: because this is a setting rather than a dynamic per-run value, the system prompt stays stable until the user explicitly changes it in settings. Cache invalidation would only happen on setting changes, which should be rare in practice. Happy to verify this empirically if helpful, but I don't think it meaningfully degrades cache hit rate for any given user.

…translation

# Conflicts:
#	Sources/AppState.swift
@marcbodea

Copy link
Copy Markdown
Collaborator

Makes sense. My only worry left is that the models that can realistically be used for post-processing are not that great at translation. Do you have experience using this feature in practice and have seen good results? I couldn't really find some good benchmarks around but would love seeing some.

Re: groq caching should be OK with static setting

Thanks!

ojhurst added a commit to ojhurst/freeflow that referenced this pull request Apr 24, 2026
Adds a Transcription Language picker in Settings → Advanced Provider
Settings (also visible in the initial Setup flow since the component
is shared). The selection is passed as the `language` form field on
non-realtime transcription requests and as the `language` field on
the realtime streaming Configuration.

The non-realtime TranscriptionService previously sent no language
parameter at all. The realtime RealtimeTranscriptionService already
had `language: String?` plumbing, but the call site in AppState
hardcoded `language: nil`, so the feature was wired but inert.

Users report wrong-script characters (for example CJK) leaking into
English dictation — a known Whisper failure mode on short utterances
when no language hint is supplied. Providing the ISO 639-1 code
resolves this.

Related: zachlatta#102 (marwei) takes a complementary approach by modifying
the post-processing system prompt to translate output into a chosen
language, operating at the LLM cleanup layer. This change operates
at the transcription layer and is independent of post-processing.

Note on provider behavior: empirically, Groq's Whisper endpoint
treats this parameter closer to "target language" — audio spoken in
one language is translated into the selected language. OpenAI
documents it as a pure input-audio hint. Default is Auto-detect,
which sends no field and preserves current behavior for existing
users.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@marwei

marwei commented Apr 24, 2026

Copy link
Copy Markdown
Author

Thanks — ran an eval in both directions to check. 280 samples across 14 language pairs (20 each), using a script that mirrors PostProcessingService.postProcess() + applyOutputLanguage verbatim and hits the real Groq API. Inputs are synthesized but representative of typical Flow-user messages (Slack/email/PR/ticket/meeting-note/code-review).

Headline: 12 of 14 pairs are very strong, and the remaining two (English → CJK) are still solid on translation quality itself.

The issues I saw on en-ja and en-zh weren't translation-quality problems — they were cases where llama-4-scout ignored the directive entirely and echoed the English back (5/20 en-ja, 3/20 en-zh). The same model has directive-compliance slips on plain English→English dictation too, sometimes returning "Here's the cleaned transcript:"-style preamble. So this is really a llama-scout reliability issue with adhering to system-prompt directives in general, not a translation-quality issue. When it does translate, the output is clean and idiomatic.

Couldn't get a full run on gpt-oss-20b — it hit Groq's daily token quota within the first few requests on two separate keys. Given how early that burns, most real user calls end up on llama-4-scout anyway, so these results probably reflect what most users actually see.

Caveats: N=20/pair, synthesized inputs, single LLM judge.

Script + raw 280-sample results: https://gist.github.com/marwei/3d7efb7126bea19831d22465d8bf936d

@marcbodea

Copy link
Copy Markdown
Collaborator

Look good! Thank you for putting this together.

Will solve the conflicts and merge shortly.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
Sources/SettingsView.swift (1)

966-985: Consider replacing the empty-string sentinel list with explicit value/label options.

Using "" in outputLanguageOptions plus dropFirst() works, but it’s brittle and ties storage values to display text. A small value/label model keeps persistence stable and simplifies rendering.

♻️ Proposed refactor
-    private static let outputLanguageOptions = [
-        "",
-        "English",
-        "Chinese (Simplified)",
-        "Chinese (Traditional)",
-        "Spanish",
-        "French",
-        "Japanese",
-        "Korean",
-        "German",
-        "Portuguese",
-    ]
+    private struct OutputLanguageOption: Identifiable {
+        let id: String      // persisted value
+        let label: String   // UI label
+    }
+
+    private static let outputLanguageOptions: [OutputLanguageOption] = [
+        .init(id: "", label: "Same as spoken"),
+        .init(id: "English", label: "English"),
+        .init(id: "Chinese (Simplified)", label: "Chinese (Simplified)"),
+        .init(id: "Chinese (Traditional)", label: "Chinese (Traditional)"),
+        .init(id: "Spanish", label: "Spanish"),
+        .init(id: "French", label: "French"),
+        .init(id: "Japanese", label: "Japanese"),
+        .init(id: "Korean", label: "Korean"),
+        .init(id: "German", label: "German"),
+        .init(id: "Portuguese", label: "Portuguese"),
+    ]
@@
-            Picker("Language", selection: $appState.outputLanguage) {
-                Text("Same as spoken").tag("")
-                ForEach(Self.outputLanguageOptions.dropFirst(), id: \.self) { lang in
-                    Text(lang).tag(lang)
-                }
-            }
+            Picker("Language", selection: $appState.outputLanguage) {
+                ForEach(Self.outputLanguageOptions) { option in
+                    Text(option.label).tag(option.id)
+                }
+            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/SettingsView.swift` around lines 966 - 985, Replace the empty-string
sentinel in outputLanguageOptions with an explicit value/label model and update
the Picker to use that model: create a small struct or tuple array (e.g.,
DisplayOption { let id/value: String; let label: String }) and replace
Self.outputLanguageOptions with an array of those options, ensuring the first
option represents "Same as spoken" with a stable value (not ""), then update
outputLanguageSection's Picker to iterate over the new option collection and
bind tags to option.id/value while showing option.label; keep
appState.outputLanguage as the persisted value used for selection.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/SettingsView.swift`:
- Around line 989-991: Update the helper Text that currently reads "When set,
FreeFlow translates your speech into the selected language." to explicitly
mention that translation also applies during Edit Mode transforms; locate the
Text literal in SettingsView (the Text(...) line in Sources/SettingsView.swift)
and replace the string with wording like "When set, FreeFlow translates your
speech — and Edit Mode transforms — into the selected language." to make the
scope clear to users.

---

Nitpick comments:
In `@Sources/SettingsView.swift`:
- Around line 966-985: Replace the empty-string sentinel in
outputLanguageOptions with an explicit value/label model and update the Picker
to use that model: create a small struct or tuple array (e.g., DisplayOption {
let id/value: String; let label: String }) and replace
Self.outputLanguageOptions with an array of those options, ensuring the first
option represents "Same as spoken" with a stable value (not ""), then update
outputLanguageSection's Picker to iterate over the new option collection and
bind tags to option.id/value while showing option.label; keep
appState.outputLanguage as the persisted value used for selection.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 430406c4-c834-4092-8bbb-b23ea9236eb1

📥 Commits

Reviewing files that changed from the base of the PR and between 45bd07e and 6dc141d.

📒 Files selected for processing (2)
  • Sources/AppState.swift
  • Sources/SettingsView.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • Sources/AppState.swift

Comment on lines +989 to +991
Text("When set, FreeFlow translates your speech into the selected language.")
.font(.caption)
.foregroundStyle(.secondary)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Update the helper text to include Edit Mode behavior.

Line 989 currently says “translates your speech,” but translation also applies during Edit Mode transforms. This copy may mislead users about scope.

✏️ Suggested copy tweak
-            Text("When set, FreeFlow translates your speech into the selected language.")
+            Text("When set, FreeFlow translates post-processed output into the selected language (including dictation and Edit Mode).")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/SettingsView.swift` around lines 989 - 991, Update the helper Text
that currently reads "When set, FreeFlow translates your speech into the
selected language." to explicitly mention that translation also applies during
Edit Mode transforms; locate the Text literal in SettingsView (the Text(...)
line in Sources/SettingsView.swift) and replace the string with wording like
"When set, FreeFlow translates your speech — and Edit Mode transforms — into the
selected language." to make the scope clear to users.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants