Apple's Translation Framework: Free, On-Device, and Sharper Than It Looks

Apple’s Translation framework translates text on-device, for free, with no API key and no network call once the language is installed1. It is built on Core ML models, ships with the system, and gives an app the same translation engine the Translate app uses. For multilingual features that used to mean a cloud translation bill and a privacy question, the cost again rounds to zero. And as with the Foundation Models framework, the interesting part is not the happy path; it is the edges the demos skip: the language download that blocks your first translation, the simulator that silently refuses to work, and the SwiftUI-only surface that shapes how you adopt it.

TL;DR

  • Two surfaces, two iOS versions. translationPresentation shows Apple’s built-in translation popover (iOS 17.4+). TranslationSession, reached through the translationTask modifier, does programmatic translation in your own UI (iOS 18+)2.
  • Programmatic translation is async. Inside the translationTask closure you get a TranslationSession; call it to translate one string or a batch3.
  • Batch is first-class. Translate a list in one request and keep each result matched to its input, instead of looping and awaiting one at a time3.
  • The translation UI is SwiftUI-only, and it does not run in the iOS Simulator. Both are easy to discover the hard way; design and test accordingly4.
  • Offline means a download first. The first translation for a language pair downloads bundles, which is a real UX moment you handle, not a detail5.
  • The pairing worth knowing: translate user input with Translation, then reason over it with Foundation Models, and a monolingual on-device agent feature works in any language the device can install.

Two surfaces: the system popover and your own UI

The framework gives you two distinct ways to translate, and choosing the right one is most of the decision.

The light touch is translationPresentation, available since iOS 17.4. You attach it to a view, bind an isPresented flag, and pass the text; when the flag flips true, the system slides up its own translation popover over your content2:

.translationPresentation(isPresented: $showTranslation, text: selectedText)

You write no translation logic and you do not see the result; Apple owns the UI and the interaction. For “let the user translate this passage,” this is the whole feature, and reaching for anything heavier is wasted work.

The programmatic surface is TranslationSession, available since iOS 18, and you reach it through the translationTask modifier. The modifier runs an async closure and hands you a session you call yourself, so the translated text comes back to your code and you render it your way2:

.translationTask(configuration) { session in
    let response = try await session.translate("Good morning")
    await MainActor.run { translated = response.targetText }
}

The split is clean. translationPresentation is for showing the user a translation in Apple’s UI. TranslationSession is for getting translated text into your data and your views. Most apps that do more than a one-off “translate this” reach for the session.

Batch translation: translate the list, not the loop

The detail that separates a smooth feature from a janky one is batching. If you need to translate a list (chat messages, catalog entries, a set of labels), do not loop and await each translation in turn. TranslationSession takes a batch of requests and returns the responses, each matched to its request, in one pass3:

.translationTask(configuration) { session in
    let requests = items.map { TranslationSession.Request(sourceText: $0.text, clientIdentifier: $0.id) }
    for try await response in session.translate(batch: requests) {
        store[response.clientIdentifier] = response.targetText
    }
}

The clientIdentifier is the part that matters: it comes back on the response so you can match each translation to the row it belongs to without relying on order. Batching also lets the framework schedule the work efficiently instead of paying per-call overhead across a loop. For anything beyond a single string, batch.

The offline reality nobody screenshots

Here is the edge that turns a clean demo into a support ticket. The translation runs on-device, but the language has to be on the device first. The first time your app translates a given source-to-target pair, the system downloads the language bundles, and that download takes time and a network connection5. If you fire a translation and render the result with no handling for the download, your feature appears to hang on first use for every new language.

Handle it deliberately. The framework lets you check language availability and prepare (download) a pair ahead of the moment you need it, so you can surface a “preparing translation” state or pre-download during a calmer moment instead of stalling mid-interaction5. The mental model: treat the first translation of a language pair like a one-time asset download, because that is what it is. Plan the UX around the download existing, and the offline-and-free benefit lands; ignore it, and the benefit hides behind a hang.

Two more facts that cost an afternoon if you learn them late. The translation UI surfaces are SwiftUI modifiers, so a UIKit screen hosts a SwiftUI view (through UIHostingController) to reach them, though a TranslationSession can also be constructed directly for no-UI work4. And the framework does not run in the iOS or iPadOS Simulator; you test translation on a real device4. Neither is documented loudly, and both are easy to hit head-first.

Pairing translation with Foundation Models

The synthesis worth taking away ties this framework to the rest of the on-device stack. Most on-device language work assumes the input is in a language your logic and the system model handle well. Real users do not cooperate. The Translation framework closes that gap: translate the user’s input into the language your feature reasons in, run the Foundation Models work on the translated text, then translate the result back.

The shape is a bracket: translate in, reason, translate out.

// 1. translate the user's text into English (session configured for their language -> en)
let english = try await inboundSession.translate(userText).targetText
// 2. reason on-device, monolingually, in English
let summary = try await LanguageModelSession()
    .respond(to: "Summarize in one line: \(english)").content
// 3. translate the result back (a session configured for en -> their language)
let localized = try await outboundSession.translate(summary).targetText

A support-triage feature, a note summarizer, an intent extractor: each can be written once, monolingually, and made to work in any language the device can install, by bracketing the Foundation Models call with Translation. The two directions use two session configurations (their language to English, then English back), since a session is configured for one language pair. Both layers are on-device, both are free, and nothing leaves the phone, so the multilingual version costs no privacy and no cloud bill the monolingual version did not. That composition (translate in, reason, translate out) is the pattern that makes a small on-device feature genuinely global, and it is only possible because both halves run locally for free.

When not to use it

On-device translation is free and private, which makes it the right default for in-app translation. It is the wrong tool in a few honest cases.

  • You need the best possible translation quality or the widest language coverage. The on-device models are good, not the best available, and the installable language set is finite. For high-stakes translation (legal, medical, published content), a dedicated cloud translation service still wins on quality and breadth.
  • You cannot tolerate the first-use download. For a feature that must work instantly on first launch with no network, the download requirement may disqualify on-device translation by itself unless you pre-download during onboarding.
  • Your app is UIKit with no room for a SwiftUI host, or your flow must run in the Simulator (an automated UI test, for instance). The SwiftUI-only and no-Simulator constraints are hard, not advisory.

The framework is one of the quieter wins in the on-device toolkit: a genuinely free, private translation engine that most apps could adopt in an afternoon. The skill is the same one the rest of this stack rewards. Know which surface fits (the system popover or your own session), batch when you have a list, and design for the download instead of pretending it is not there. Do that, and translation stops being a cloud dependency and becomes a local capability you compose with everything else the device does for free.



  1. Apple Developer, “Translation” framework. A first-party framework for on-device, system-provided translation built on Core ML models, performing translation locally without a network call once language assets are installed. 

  2. Apple Developer, “translationPresentation(isPresented:text:attachmentAnchor:arrowEdge:replacementAction:)” (iOS 17.4+) presents the system’s built-in translation UI over a view; “translationTask(_:action:)” (iOS 18+) runs an async closure that provides a TranslationSession for programmatic translation in your own interface. 

  3. Apple Developer, TranslationSession and TranslationSession.Request. translate(_:) handles a single string; the batch API takes an array of requests, each carrying a clientIdentifier that returns on the matching response so results can be reassociated with their inputs independent of order. 

  4. The Translation framework’s translation APIs are surfaced through SwiftUI view modifiers (translationTask, translationPresentation) and have no UIKit entry point; a UIKit screen hosts a SwiftUI view (for example via UIHostingController) to use them. The framework also requires a physical device and does not function in the iOS Simulator. See the Translation framework documentation and “Translating text within your app”

  5. Apple Developer, “Translating text within your app” and LanguageAvailability. The first translation of a source-to-target language pair downloads the required language assets; the framework exposes language-availability checks and a way to prepare (download) a pair in advance so apps can manage the download moment rather than stall on first use. 

  6. Author’s related analysis on composing on-device capabilities: Apple Foundation Models: The On-Device LLM Framework, On-Device LLMs with Apple’s Foundation Models, and Writing Tools API adoption. The translate-in, reason, translate-out pattern brackets a monolingual Foundation Models call with Translation to make an on-device feature multilingual without leaving the device. 

Related Posts

Image Playground API: SwiftUI Sheet, Programmatic Image Creator, And Style Control

Image Playground gives apps two paths: the SwiftUI imagePlaygroundSheet modifier and the programmatic ImageCreator API f…

11 min read

Genmoji and NSAdaptiveImageGlyph: Inline Emoji on iOS 18+

Genmoji ships as NSAdaptiveImageGlyph in attributed text. Apps using UITextView with TextKit 2 enable supportsAdaptiveIm…

10 min read

When the Maintainer Is the Attacker: jqwik 1.10.0

jqwik 1.10.0 emits a destructive prompt-injection string in Maven output. ANSI escapes hide it from humans. The maintain…

18 min read