On-device machine translation with no network connection and no data leaving the device. Powered by an int8-quantized neural engine built on CTranslate2.
- Fully offline — translation runs on device; nothing is sent anywhere.
- Fast — int8 inference with hand-tuned NEON kernels on Apple Silicon and modern iPhones.
- Private — no tracking, no analytics, no data collection (see
PrivacyInfo.xcprivacy). - Any-to-any translation — language pairs pivot through English automatically.
- iOS 13+ / macOS 10.15+
- Xcode 14+
- A language pack (see Language packs)
In Xcode: File → Add Package Dependencies… and enter:
https://github.com/lingvanex-mt/offline-translation-apple-sdk
Select Up to Next Major Version starting from 3.1.0.
Or in Package.swift:
dependencies: [
.package(url: "https://github.com/lingvanex-mt/offline-translation-apple-sdk", from: "3.1.0")
]Translation models ship separately as .zip language packs (one pack per language, each translates to and from English; English itself is built in).
High-quality offline models are available for 110 languages. To request a demo pack for the languages you need, write to info@lingvanex.com.
A sample Portuguese pack (English ↔ Portuguese, 87 MB) is ready to try right away: download pt.zip. Drag it into your project (Copy If Needed + your target checked) — the Example/ apps expect exactly that.
Both directions come from our current mobile model generation (46.9 MB each, int8). Measured through this SDK on FLORES-200 devtest, 1012 sentences, beam 2, Apple M1 Pro:
| English → Portuguese | Portuguese → English | |
|---|---|---|
| BLEU | 52.31 | 47.27 |
| chrF2 | 71.56 | 69.45 |
| COMET-DA | 0.899 | 0.886 |
| Speed | 78 ms/sentence | 77 ms/sentence |
Works out of the box with the sample Portuguese pack added to your project as pt.zip:
import OfflineTranslator
// 1. Install a language pack bundled with your app. Installation happens once per
// `version`: every launch after the first reports `versionAlreadyInstalled`,
// which means "already on disk" — log it and carry on, do not bail out.
PackageManager.install(id: "pt", zip: "pt", version: "1.0.0") { error in // bump `version` to reinstall an updated pack
if let error = error {
print("Install: \(error.localizedDescription)")
}
// 2. Pick the language pair. English is built in — use .english for it.
// A missing package is the failure that matters here: with no pair loaded
// the translator hands your input straight back (see Translator below).
guard let pt = PackageManager.packageWithId("pt") else {
print("Portuguese pack is not installed")
return
}
try? Translator.shared.setup(fromPackage: .english, toPackage: pt)
// 3. Translate.
Translator.shared.translateWithString("Hello world! How are you today?") { result in
switch result {
case .success(let translation):
print(translation) // "Olá mundo! Como estás hoje?"
case .failure(let error):
print("Translate error: \(error)")
}
}
}Packs can also be downloaded by your app at runtime — fetch the zip with URLSession and pass its file path to PackageManager.install(id:path:version:). That call deletes the zip once it has been installed, so pass a downloaded temporary file, not a file you still need.
Non-English pairs work the same way — the SDK pivots through English internally:
guard
let de = PackageManager.packageWithId("de"),
let pt = PackageManager.packageWithId("pt")
else { return }
try? Translator.shared.setup(fromPackage: de, toPackage: pt)Installed packs are unpacked into the caches directory, under Packages. Two consequences are worth planning for:
- iOS may evict the caches directory when the device runs low on storage, taking installed packs with it. Ask
packageWithId(_:)on every launch rather than remembering "installed" in your own settings, and reinstall if the pack is gone. - A non-sandboxed macOS app shares
~/Library/Caches/Packageswith every other non-sandboxed app on the machine, soPackageManager.installedcan return packs your app never installed. Filter it by the ids you ship — theExample/apps do exactly that. Sandboxed apps (including the macOS demo) get their own container and see only their own packs.
| Method | Description |
|---|---|
install(id:zip:version:queue:installBlock:) |
Install a pack bundled in the app (zip is the resource name without extension). |
install(id:path:version:queue:installBlock:) |
Install a pack from an arbitrary file path (e.g. downloaded at runtime). |
uninstall(id:queue:uninstallBlock:) |
Remove an installed pack. |
installed |
All installed packs. |
packageWithId(_:) |
Look up an installed pack by id. |
install is idempotent per version: replacing the zip without bumping version does not reinstall. It says so by calling back with PackageManagerError.versionAlreadyInstalled — the expected result on every launch after the first, not an error to abort on.
| Method | Description |
|---|---|
setup(fromPackage:toPackage:) |
Load a language pair. Call again to switch pairs; loading is asynchronous and deduplicated. Translations requested right after it wait for the load — both run on the same serial queue — so there is nothing to await. Throws if the pack carries no model for the direction you asked for. |
translateWithString(_:queue:translateBlock:) |
Translate a string. Result<String, Error> callback; an (String?, Error?) variant exists for Objective-C. |
cancelCurrentTranslation() |
Cancel the in-flight translation at the next chunk boundary. |
setBeamSize(_:) |
Decoder beam width. Default 2. 1 (greedy) is ~25% faster with near-identical quality — recommended on iPhone. |
setChunkSize(_:) |
Sentences per inference batch. Default 4 — keeps cancellation responsive on mobile. On desktop set 0 (whole text in one batch) for up to 2× throughput on long texts. |
translateWithString never reports a missing language pair. With no engine loaded — setup was not called, or it threw and you ignored the error — the text passes through untouched and arrives as .success. Handle the error from setup, and read "output identical to input" as "no model loaded", not as a translation.
Everything above is @objc and callable from Objective-C, except cancelCurrentTranslation(), which is Swift-only.
Each loaded direction holds its model in memory. Mobile-sized packs like the Portuguese sample cost about 75 MB per direction (measured on an iPhone 12 simulator: 73 MB for the model, 210 MB for the whole app); our full-size packs cost around 330 MB. A non-English pair loads two models, so it doubles that. On devices with 4 GB RAM or less, prefer pairs involving English and unload by calling setup with .english → .english when the translation UI is dismissed.
Example/ contains iOS and macOS demo apps. They ship without a language pack, so add one first:
- Download
pt.zip(87 MB). - Open
Example/Example.xcodeprojand dragpt.zipinto the project navigator. - In the sheet Xcode shows, tick Copy items if needed and check the target you are going to run —
Example-iOS,Example-macOS, or both.
Build and run — English ↔ Portuguese works immediately.
The apps install every .zip in their bundle and list what they find, so adding a second pack needs no code change: drop de.zip next to pt.zip and German appears in the pickers — including the non-English pairs, which the SDK pivots through English.
Offline models are available for 110 languages. For a demo pack, pricing, or help picking the right model size for your app, write to info@lingvanex.com or visit lingvanex.com.
The SDK is released under the MIT license. Language packs are licensed separately — write to info@lingvanex.com.

