●PRODUCT — Rork Max generates native Swift apps for iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and iMessage●NATIVE — Rork Max unlocks AR/LiDAR, Metal 3D games, Dynamic Island, Live Activities, HealthKit, and Core ML●CLASSIC — The original Rork uses React Native (Expo), turning plain-English prompts into shippable iOS/Android apps●FUNDING — Rork raised $2.8M from a16z (plus $15M more), reaching 743,000 monthly visits at 85% growth●PRICING — Rork is free to start, with paid plans from $25/month; Rork Max is $200/month●CHOICE — Pick cross-platform Rork or Rork Max for deep Apple-native capabilities, depending on your goal●PRODUCT — Rork Max generates native Swift apps for iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and iMessage●NATIVE — Rork Max unlocks AR/LiDAR, Metal 3D games, Dynamic Island, Live Activities, HealthKit, and Core ML●CLASSIC — The original Rork uses React Native (Expo), turning plain-English prompts into shippable iOS/Android apps●FUNDING — Rork raised $2.8M from a16z (plus $15M more), reaching 743,000 monthly visits at 85% growth●PRICING — Rork is free to start, with paid plans from $25/month; Rork Max is $200/month●CHOICE — Pick cross-platform Rork or Rork Max for deep Apple-native capabilities, depending on your goal
On-Device Translation in a Rork Max App with iOS 18 — Free, Offline, Multilingual
Add free, offline, real-time translation to a Rork Max Swift app using the iOS 18 Translation framework. Covers checking language availability, batch translation, and avoiding empty results — all with working Swift code.
Being able to read the reviews and in-app posts that arrive from overseas users, right where they appear — that has been a quiet frustration of mine for years. As an indie developer running several wallpaper and wellness apps, there are days when I reply to App Store reviews in close to thirty languages. Sending each one through an external translation API kept snagging on three things at once: network, cost, and privacy.
With iOS 18, Apple opened up the Translation framework. It runs entirely on device, costs nothing extra, and once a model is downloaded it keeps working offline. Because no text leaves the device, whatever a user wrote never travels to a third-party server. The catch is that React Native cannot touch this cleanly. From a Rork-generated Expo app, you would have to bridge into a Swift framework yourself, which means writing native modules. It only became a realistic option once Rork Max started generating Swift directly — and that is where this guide begins.
Why in-app real-time translation is a native-only feature
The Translation framework borrows the OS-integrated translation engine and language models directly. APIs like SwiftUI's .translationTask and TranslationSession are only callable from Swift. React Native's JavaScript layer has no built-in bridge for them, so you would have to write your own Expo config plugin and native module.
It is not strictly impossible in plain Rork (React Native / Expo). But wiring async results back to JS, monitoring model download state, and re-homing an API designed as a SwiftUI view modifier onto a bridge — by the time you finish, the "build it no-code" advantage is mostly gone. When Rork Max generates Swift, these pieces sit naturally inside a SwiftUI view instead. The code below assumes you are growing the screen code that Rork Max emitted, by hand.
What the Translation framework can and cannot do
Set expectations first. This is an area where over-promising leads to "that's not what I thought" later.
Aspect
Behavior
Cost
Free. No usage billing, no API key
Offline
Supported, but the language model must be downloaded first
Privacy
Text is never sent off device (on-device processing)
Languages
Follows the OS translation languages; some pairs are unsupported
Trigger model
View-scoped, user-initiated. Not meant for server-side batch jobs
OS required
iOS 18.0+ (some system-presented UI is 17.4+)
The biggest trap here is "not meant for server-side batch jobs." Translation is an API for foreground, in-app experiences — it is not designed for processing large volumes of text overnight in a backend. In my case I narrowed it to exactly the foreground job it is good at: drafting review replies, and showing an instant translation of a user post that is on screen.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦If you have been leaving overseas reviews and user posts unread, you can add free, offline in-app translation today
✦You will learn the real TranslationSession patterns — availability checks, batch translation, and handling un-downloaded models — as working Swift
✦You will understand why this is out of reach in React Native (plain Rork), making it a concrete reason to choose the $200/month Rork Max tier
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Minimal implementation — translate one string with .translationTask
Start with the smallest useful form: translate the displayed text with a single button. .translationTask spins up a session whenever the configuration is set or changed, and runs the translation inside it.
import SwiftUIimport Translationstruct ReviewTranslateView: View { let originalText: String // the original, written by an overseas user @State private var translated = "" @State private var configuration: TranslationSession.Configuration? var body: some View { VStack(alignment: .leading, spacing: 12) { Text(originalText) if !translated.isEmpty { Text(translated) .foregroundStyle(.secondary) } Button("Translate to English") { if configuration == nil { // source: nil lets the framework auto-detect the input language configuration = .init( source: nil, target: Locale.Language(identifier: "en") ) } else { // re-run when tapped again with the same configuration configuration?.invalidate() } } } .translationTask(configuration) { session in do { let response = try await session.translate(originalText) translated = response.targetText // the translation lands here } catch { translated = "Translation failed (\(error.localizedDescription))" } } }}
Tapping the button sets configuration, which fires the .translationTask closure. The expected output is the translation appearing in muted text below the original. The source: nil auto-detection was especially welcome in a review feed where the incoming language is all over the place.
Check the language inventory and prompt a download
Running offline has a flip side: if the model is not on the device, nothing happens. On first use the model is often un-downloaded, and silently skipping past that state produces the worst experience — "I tapped it and nothing happened."
First check the inventory with LanguageAvailability.
import Translationenum TranslateReadiness { case ready // can translate right now case needsDownload // supported, but the model is not downloaded case unsupported // this language pair is not supported}func checkReadiness(from source: Locale.Language) async -> TranslateReadiness { let availability = LanguageAvailability() let target = Locale.Language(identifier: "en") let status = await availability.status(from: source, to: target) switch status { case .installed: return .ready case .supported: return .needsDownload case .unsupported: return .unsupported @unknown default: return .unsupported }}
When .supported (i.e., not downloaded) comes back, call prepareTranslation() before attempting a translation. That is the trigger that presents the system's download confirmation sheet.
.translationTask(configuration) { session in do { // If not downloaded, the system download prompt appears here. If ready, it returns immediately try await session.prepareTranslation() let response = try await session.translate(originalText) translated = response.targetText } catch { translated = "Could not prepare translation (\(error.localizedDescription))" }}
I got caught here once. Skipping prepareTranslation() and calling only translate fails silently on a device without the model, and to the user it just looks broken. The two-step of availability check plus preparation is tedious, but it is the safer path.
Translate several texts at once
For a review list or comment thread, translating many strings in one pass is faster than calling translate one at a time, and it pays the model startup cost only once. Give each TranslationSession.Request an identifier and reconcile the responses.
struct UserComment: Identifiable { let id: Int let body: String var translated: String = ""}func translateAll(_ comments: inout [UserComment], session: TranslationSession) async throws { // attach a clientIdentifier so responses can be matched back let requests = comments.map { comment in TranslationSession.Request( sourceText: comment.body, clientIdentifier: "\(comment.id)" ) } let responses = try await session.translations(from: requests) // responses may arrive out of order, so map by identifier instead of array index let indexByID = Dictionary( uniqueKeysWithValues: comments.enumerated().map { ($0.element.id, $0.offset) } ) for response in responses { if let id = response.clientIdentifier.flatMap(Int.init), let i = indexByID[id] { comments[i].translated = response.targetText } }}
The key here is clientIdentifier. Responses do not necessarily return in the order you sent them, so resolve them back to the source data by identifier rather than relying on array position. I neglected this at first and ended up pasting one comment's translation onto another.
Why this is so much harder in React Native (plain Rork)
Doing the same in plain Rork roughly adds the following work.
First, .translationTask is designed as a SwiftUI view modifier. It is tied to the view lifecycle, so it does not map cleanly onto React Native's imperative bridge calls. Second, the system download sheet from prepareTranslation() is presented within the native view hierarchy; controlling that presentation from Expo's JS side means wrapping it in a native module and shuttling state back and forth. Third, you have to wire up async monitoring of the language status and reflect it in the JS UI.
None of it is impossible, but together it adds up to enough effort to cancel out the "fast and no-code" appeal that makes Rork worth using. That is exactly where I draw the line between plain Rork and Rork Max. The stage of shaping screens quickly? Plain Rork is plenty. The stage of reaching into deeply OS-integrated features — translation, HealthKit, Live Activities — is where I switch to Max, which generates Swift directly. For the limited time of indie development, that two-stage split wastes the least.
Where I tripped — empty results and dead taps
A few time sinks worth recording, so you do not repeat them.
The first is translated coming back empty. Most often this was an un-downloaded model caused by skipping prepareTranslation(). Inserting the availability check up front clears it.
The second is nothing happening when you try to re-translate the same string. A Configuration will not re-fire .translationTask unless its contents change. To force a re-run, call configuration?.invalidate() explicitly. That is why the minimal button was split into two stages.
The third is failures on unexpected-language reviews because source was pinned to a fixed language. In a review feed where you cannot read the incoming language, leaving it to source: nil auto-detection holds up better. Conversely, in a UI where the input language is known for certain (a single-country screen), specifying it explicitly is faster.
As a yardstick for choosing the $200/month Rork Max
Honestly, you do not need to move to Rork Max ($200/month) for translation alone. Plain Rork starts free and is $25/month even paid. The axis I use is "how many OS-integrated features you are bundling together."
If your app stacks on-device translation with two or three more features that React Native struggles to reach — Live Activities, HealthKit, widgets — then the value of Max generating Swift directly starts to pay off. If translation is needed on just one screen, it is cheaper to write a native extension once for that screen, or settle for an external API.
For my part, I use "total count of OS-integrated features" as the measure for migrating each of my Dolice-branded apps. Across the current wallpaper apps, I shifted to the Max side once widgets and on-device processing started overlapping. Once you can see two or more truly native-only features in your own app, that is when it is worth considering the switch.
To start, run the minimal implementation above on an iOS 18 device, on a single screen. As long as you keep the two-step of availability check and prepareTranslation(), free offline translation comes up surprisingly cleanly.
Share
Thank You for Reading
Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.