Skip to main content

Add On-Device Translation for User-Generated App Content

2026-09-28

Add On-Device Translation for User-Generated App Content

Translation is not a one-call feature. An on-device translation app has to preserve the author's text, check the language pair, manage model downloads, invalidate stale results, and behave predictably when the device is offline or the pair is unsupported. A weak implementation can keep a translation after its source changes, replace the author's words, or leave the reader waiting for a model that is not installed.

The design below treats a translation as a reader-controlled view of immutable source content. It applies to chat messages, marketplace listings, reviews, support conversations, and community posts. Runtime translations stay out of the app's ordinary localization files.

Separate runtime translation from app localization

App localization and runtime content translation solve different problems. Localization supplies reviewed interface copy such as buttons, settings, validation messages, and accessibility labels. Runtime translation helps one reader understand text authored by another person after the app has shipped.

Keep those pipelines separate. A message should not become a key in a string catalog. A translated review should not enter the same approval state as a product label. Conversely, an on-device model should never translate navigation labels that belong in the app's reviewed resources.

This separation gives each kind of text a clear owner and recovery path. Product copy has a source version, translator context, release gates, and a supported locale matrix. User content has an author, revision, source language, visibility policy, and reader-specific target language. If both use one pipeline, nobody can tell which text is authoritative or who should correct an error.

Model the translation as a derived view:

TranslationKey = (
  content_id,
  content_revision,
  source_language,
  target_language,
  engine_version
)

TranslationRecord = {
  key,
  translated_text,
  created_at,
  state
}

The source remains authoritative. The translation can be discarded and regenerated without altering what the author wrote.

Define the reader action before the API

Start with the interaction contract. Meshtastic's public design for cross-platform per-message translation uses an explicit action, retains the translated result, lets the reader switch between original and translated text, and provides a separate clearing action. That pattern is more predictable than silently translating every visible message.

A practical first pass works like this:

  1. Show Translate only when the content is nonempty and the feature can attempt the requested target language.
  2. Keep the original visible until a translation succeeds.
  3. Show a progress state while checking availability or downloading a model.
  4. Replace or expand the text only after a complete result arrives.
  5. Label translated text so it cannot be mistaken for the author's exact words.
  6. Offer Show original after success.
  7. Remove or invalidate the result when the source revision changes.

Automatic translation can still be an account preference, but build it on the same state machine. Do not create a second path with different cache and error rules.

Decide whether translated text replaces the source in place or appears below it. In chat, replacement saves space and a Show original action preserves trust. In reviews, showing both may be preferable because readers often need to inspect names, measurements, or quoted wording. The choice is a product rule, not an API constraint.

Resolve languages without guessing past uncertainty

The feature needs three language values: the content's claimed or detected source language, the reader's target language, and the effective pair accepted by the platform engine.

Prefer an author-provided language when the content system already records one. Detection is useful for untagged text, but short strings such as "OK," names, emoji, URLs, and code fragments may not provide enough evidence. Treat an uncertain result as unknown rather than forcing a plausible language code.

The target should normally come from an explicit reading or app language preference. Device language is a fallback, not an automatic override of an app-specific choice. Normalize both ends through the app's canonical locale mapping before asking the engine about availability. If that mapping does not exist yet, start with a locale mapping layer across app code, stores, and translation systems. A content tag such as zh-Hant-TW may need to map to a broader model language while the UI still preserves the original locale metadata.

Apple's Translation framework supports a language availability check before translation. Use that check before showing a model-download prompt or starting work. On Android and iOS, Google's ML Kit supports on-device translation for more than 50 languages, but its Android translation guide and iOS translation guide still require the app to specify a supported source and target.

Use a result type that preserves uncertainty:

enum TranslationEligibility {
    case available(source: Language, target: Language)
    case sourceUnknown
    case sameLanguage
    case unsupportedPair
    case restrictedContent
}

Do not send sourceUnknown through a guessed default. Ask the reader to choose a source language when translation matters, or leave the original unchanged.

Put model downloads in the product state machine

On-device does not mean instantly available. Apple notes that custom translation flows may ask the person for permission to download language models when necessary. ML Kit uses on-demand language models and exposes model management on both supported mobile platforms.

That download boundary needs visible states:

idle
  -> checkingPair
  -> needsModel
  -> downloading
  -> translating
  -> translated

checkingPair -> unavailable
needsModel -> declined
any active state -> cancelled
any network state -> offline
any state -> failed

Do not hide a large model download behind a spinner labeled Translating. Tell the reader what is being downloaded and whether a network connection is required. Respect platform controls and the user's decision. If the platform API owns the permission sheet, prepare the surrounding UI for cancellation rather than trying to imitate the sheet.

For an automatic translation preference, prefetching deserves stricter rules. Download only the pairs the reader has chosen, apply an appropriate network policy, and provide storage controls. Google's Android guidance supports explicit model download and deletion, which makes a Manage translation languages screen possible instead of leaving storage use invisible.

Translation requests should be cancellable. Recycled list cells, rapid scrolling, target-language changes, and deleted messages can all make an in-flight result irrelevant. Completion must confirm that the content ID, revision, and requested target still match the visible item before applying text.

Cache results without serving the wrong revision

Caching reduces repeat work and lets a reader switch between original and translated text quickly. The key must include more than a message ID.

At minimum, include the source revision, normalized source language, target language, and engine version. A hash of the exact source text can replace a numeric revision when the content system lacks revisions, but keep the stable content ID for deletion and retention operations.

Never overwrite the original field with the translated result. Keep translated text in a reader-local cache unless the product has a documented reason to synchronize it. A local cache improves privacy and avoids turning machine output into shared author content. If translations are synchronized across devices, label that as a separate server feature with its own retention and access policy.

Invalidate on these events:

  • The author edits the source.
  • The source or target language changes.
  • The content is deleted or access is revoked.
  • The translation engine changes in a way that makes cached output incompatible.
  • A policy decision makes the item ineligible for translation.

A stale result should never flash before the new source appears. Read source content and its translation key as one display decision, then show the cached result only on an exact match.

Protect source text and private content

On-device processing limits the need to send user text to a translation server, but it does not remove product responsibilities. The app still displays user-generated content, may persist translated output, and may expose it through logs, analytics, backups, screenshots, or crash reports.

Keep raw and translated message text out of analytics events. Record operational states such as pair available, model requested, download failed, or translation completed without recording the content. Redact both forms from error reports unless the user deliberately submits them for support.

Apply the same visibility and deletion checks to translations as to source content. If a message is removed, its cached translation must disappear. If a conversation becomes inaccessible, local derived data should not keep it readable through a translation cache.

Moderation order also needs an explicit rule. Translation can alter wording and model output can be wrong, so do not treat translated text as authoritative evidence for an enforcement decision. Moderate source content through the system's established policy, and present a report flow that makes clear whether the reader is reporting the original or the displayed translation.

Give each failure one clear response

A generic "Translation failed" message does not help the reader recover, but the user-facing response should stay concise. Map each technical failure to an action:

Failure Reader response App action
Same language Keep original Hide or disable Translate
Unknown source Ask for language when useful Do not guess
Unsupported pair Explain that the pair is unavailable Keep original visible
Model missing Offer the platform download flow Preserve request context
Offline during download Ask the reader to reconnect Allow retry
Cancelled Return to original Clear transient state
Source edited Discard old result Start only on a new action
Engine error Keep original and offer retry Log an error category only

Separate availability from failure. Unsupported is a stable result for the current pair, not a retry loop. Offline is temporary. Cancellation is not an error. An edited source invalidates work rather than making the engine responsible.

Accessibility state belongs in this mapping. Announce when translation finishes, expose the translated label and its language to assistive technology where the platform permits it, and keep Show original reachable without relying on color or an unlabeled icon.

Use one adapter per platform

Keep platform APIs behind a small application interface. The shared layer should own eligibility, cache keys, cancellation, privacy rules, and presentation state. Native adapters should own model availability and translation calls.

interface RuntimeTranslator {
    suspend fun availability(
        source: LanguageTag,
        target: LanguageTag
    ): Availability

    suspend fun prepare(
        source: LanguageTag,
        target: LanguageTag,
        networkPolicy: NetworkPolicy
    ): PrepareResult

    suspend fun translate(request: TranslationRequest): TranslationResult

    suspend fun removeModels(languages: Set<LanguageTag>)
}

Apple offers both system presentation and custom TranslationSession flows in its in-app translation guidance. Choose the system presentation when the standard interaction fits. Use a custom session only when the product needs inline results, batch behavior, or its own translation state.

ML Kit's Android model management and iOS model management make preparation an explicit adapter responsibility. Close or release native translator objects according to the platform lifecycle. The shared layer should not assume that a model exists because the same pair worked during an earlier session.

Do not force both adapters to expose identical platform prompts. Require identical product outcomes instead: source retained, pair checked, download understandable, request cancellable, result keyed correctly, and failure recoverable.

Test the complete lifecycle

Unit tests should cover language normalization, eligibility, cache keys, source revisions, and state transitions without invoking a model. Adapter tests should cover each platform's availability and preparation responses. Installed-build tests must exercise real model behavior because simulators and preloaded development devices can hide first-use downloads.

Use this release matrix:

  • Supported pair with model already present
  • Supported pair requiring a model download
  • Download accepted, declined, cancelled, interrupted, and retried
  • Device offline before and during preparation
  • Unknown, same-language, and unsupported source-target combinations
  • Source edited while translation is running
  • Message deleted or access revoked after caching
  • App language changed after a translation exists
  • Rapid reuse of a list cell while requests complete out of order
  • Screen reader focus before, during, and after replacement
  • Cold launch with a valid cache and with an obsolete cache
  • Model storage removal followed by another request

Capture no user text in test telemetry. Use synthetic fixtures that include emoji, URLs, names, mixed scripts, line breaks, and bidirectional text. Translation quality varies by language and content, so verify state and safety deterministically while using language reviewers for representative output checks.

A successful demo string proves very little. Ship only after source preservation, availability checks, downloads, cache invalidation, privacy, accessibility, and recovery pass on installed builds for both platform adapters.

Next action

Write the TranslationKey, TranslationEligibility, and state machine before connecting either native SDK. Then implement one message-level vertical slice with Show original, model-download handling, revision-aware caching, and the release matrix above. Once that slice passes on a clean device, reuse the same contract for reviews, listings, or support messages.

References