DEV Community

Cover image for Ship Translation Updates to iOS, Android & Flutter Without Resubmitting to the App Store
Nissim Pardo
Nissim Pardo

Posted on

Ship Translation Updates to iOS, Android & Flutter Without Resubmitting to the App Store

Every time you fix a typo in your app's translations, you have to submit a new build, wait for App Review, and ask users to update. For a single string change.

There's a better way.


The problem with traditional localization

Most mobile apps bundle their translations directly into the binary — .strings files on iOS, strings.xml on Android, .arb files on Flutter. That means every translation change — a new language, a fixed copy, a reworded button — requires:

  1. Update the resource file
  2. Build and sign a new binary
  3. Submit for App Store / Play Store review (1–3 days)
  4. Hope users actually update

If you're serving multiple markets and moving fast, this creates a painful bottleneck.


Over-The-Air (OTA) localization

OTA localization works the same way as OTA JavaScript in React Native or feature flags in Firebase Remote Config: the strings live on a server, not in the binary. The app fetches them at launch, caches them, and uses them immediately.

You ship translations the way you ship config — instantly, without going through the stores.


How TranslationsHub works

TranslationsHub is an OTA localization platform for iOS, Android, and Flutter. You manage all your translation keys from a dashboard, and the SDK delivers updates to your users automatically every day.

The architecture is simple:

  1. Your app calls a Cloud Function with its API key at launch (once per day)
  2. The function validates the key, records the MAU check-in, and returns a short-lived signed URL
  3. The SDK fetches the JSON from that URL and caches it locally
  4. If the network is unavailable, it falls back to the cached version, then to a bundled asset

No credentials or download tokens ever touch your binary. The API key only identifies your project — it can't be used to access the raw storage bucket.


Integration in under 5 minutes

iOS (Swift Package Manager)

Add https://github.com/translate-hub/ios_sdk in Xcode → File › Add Package Dependencies.

// MyApp.swift
import SwiftUI
import TranslateHubSDK

@main
struct MyApp: App {
    init() {
        TranslateHub.shared.initialize(
            config: TranslateHubConfig(apiKey: "YOUR_API_KEY")
        ) {
            // translations ready — refresh UI here
        }
    }
    var body: some Scene { WindowGroup { ContentView() } }
}

// ContentView.swift
Text("greeting".translate ?? "Hello!")
Text("subtitle".translate ?? "Your app, in every language")
Enter fullscreen mode Exit fullscreen mode

Android (Kotlin + JitPack)

// settings.gradle
repositories { maven { url 'https://jitpack.io' } }

// app/build.gradle
implementation 'com.github.translate-hub:android_sdk:1.0.3'
Enter fullscreen mode Exit fullscreen mode
// MyApplication.kt
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        TranslateHub.shared.initialize(
            context = applicationContext,
            config  = TranslateHubConfig(apiKey = "YOUR_API_KEY")
        ) { /* translations ready */ }
    }
}

// In any Fragment or Composable:
val title = "greeting".translate ?: "Hello!"
Enter fullscreen mode Exit fullscreen mode

Don't forget <uses-permission android:name="android.permission.INTERNET"/> — Flutter and Android don't add it by default.

Flutter (pub.dev)

# pubspec.yaml
dependencies:
  translate_hub_handler: ^1.0.0
Enter fullscreen mode Exit fullscreen mode
// main.dart
Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await TranslateHub.shared.initialize(
    const TranslateHubConfig(apiKey: 'YOUR_API_KEY'),
  );
  runApp(const MyApp());
}

// Anywhere in your widget tree:
Text('greeting'.translate ?? 'Hello!')
Enter fullscreen mode Exit fullscreen mode

The translations JSON

All three SDKs read the same file format, so you manage one source of truth:

{
  "languages": [
    { "code": "en", "name": "English", "direction": "ltr" },
    { "code": "he", "name": "Hebrew",  "direction": "rtl" }
  ],
  "en": { "greeting": "Hello!", "subtitle": "Your app, in every language" },
  "he": { "greeting": "שלום!",  "subtitle": "האפליקציה שלך, בכל שפה" }
}
Enter fullscreen mode Exit fullscreen mode

RTL is handled automatically via the direction field — no extra configuration needed.


Offline support

If the network is unavailable or the API call fails, the SDK falls back gracefully:

  1. Local cache — the last successful fetch (up to 24 hours old)
  2. Bundled assettranslations.json shipped inside the binary

This means your app always has strings to show, even on the first launch with no internet.


Switching languages at runtime

// iOS
TranslateHub.shared.pickLanguage(code: "he")
// refresh your SwiftUI views or call setNeedsDisplay

// Android
TranslateHub.shared.pickLanguage("he")
refreshUI()

// Flutter
TranslateHub.shared.pickLanguage('he');
setState(() {});
Enter fullscreen mode Exit fullscreen mode

The selection is persisted across app launches via UserDefaults / SharedPreferences.


Who is this for?

  • Apps targeting multiple markets that want to iterate on copy without going through review
  • Teams that manage translations externally (translators, marketing) and need to push updates independently of the dev cycle
  • Apps that need to add a new language after launch without shipping a new binary

Try it

Dashboard and API keys at translate-io.com


Have questions or ran into an issue? Drop a comment below or open an issue on GitHub.

Top comments (0)