Skip to main content

Handle In-App Review Prompts in a Multilingual App

2026-10-29

Handle In-App Review Prompts in a Multilingual App

In app review localization breaks at a boundary your string catalog does not control. A user selects Turkish or Arabic inside the app, finishes a task, and sees a rating card in the device language. The card may not appear at all because Apple or Google suppresses the request. Your app also cannot safely infer that the user submitted a rating when the flow ends.

Build the review flow around those limits. Keep eligibility, timing, localized app copy, and diagnostics under app control. Treat the native rating card as an optional system surface. Never block progress on it, never claim that a review happened, and provide a separate user-initiated route to the store listing.

Why the review prompt can disagree with the app

An in-app language selector changes resources that your process owns. The native review card belongs to StoreKit or Google Play. It is not another screen in your view hierarchy, and you cannot solve a mismatch by adding its labels to your translation files.

Two Stack Overflow authors describe the same iOS failure from different app setups. One reports that SKStoreReviewController followed the device language after the app switched languages. Another reports that changing the AppleLanguages preference did not force the pop-up language. These are practitioner reports, not an Apple guarantee about every OS release. They still establish the implementation problem: app locale and system-owned review locale can diverge.

Calling the request API does not mean the card appeared. Apple's guide to requesting App Store reviews says people can disable review requests and that the system limits how often it displays them. Google likewise warns in its in-app review integration guide that quota can prevent the flow from appearing.

Completion remains opaque too. Your code requests a flow; it does not receive a trustworthy review record. A completed API task means the platform finished the request flow, not that the user selected stars, wrote text, or submitted anything. Analytics that name the event review_submitted manufacture a conversion the platform never confirmed.

Define ownership before writing platform code

Treat the feature as two connected components.

The app-owned component decides eligibility, records completed product milestones, resolves the effective app locale, schedules a request, and renders any ordinary settings copy. It can log that a request was attempted. It cannot log a rating.

The store-owned component decides whether a card is eligible to appear, renders the card, chooses its presentation language, accepts or ignores user input, and applies platform quotas. The app must not patch, overlay, translate, or inspect this component.

Represent that contract directly:

ReviewRequestContext
  appVersion: "8.4.0"
  effectiveAppLocale: "tr"
  deviceLocale: "en-US"
  milestone: "first-export-completed"
  completedMilestoneCount: 4
  lastRequestedVersion: "8.2.0"
  requestSurface: "export-success"

None of these fields claims that the card appeared or that a review was submitted. They explain why the app decided to make a request. Keeping effectiveAppLocale and deviceLocale separate also makes a language mismatch visible during testing without pretending the app can override the card.

Build a suppression-safe eligibility policy

A useful eligibility policy starts from a successful product outcome, not time since install alone. The user should finish a meaningful task, reach a stable screen, and have enough experience to judge the app. Apple's current guidance recommends requesting after a completed sequence and avoiding launch-time or action-triggered interruptions. It also documents a maximum of three displayed prompts within a 365-day period, controlled by the system.

Keep your own policy conservative because platform quotas are not a scheduling service. A local record can prevent repeated attempts after every success, but it cannot tell you whether the platform showed a card.

shouldRequestReview(context, history):
    if context.isAutomatedTest:
        return false
    if context.milestone not in approvedMilestones:
        return false
    if context.completedMilestoneCount < minimumExperience:
        return false
    if context.appVersion == history.lastRequestedVersion:
        return false
    if daysSince(history.lastAttemptAt) < localCooldownDays:
        return false
    if context.hasBlockingError or context.isInCriticalFlow:
        return false
    return true

Persist lastAttemptAt and lastRequestedVersion before invoking the native API. If the app crashes or the request is suppressed, the next launch should not immediately retry. Name the record reviewRequestAttempt, not reviewPromptShown.

Do not insert a sentiment gate such as “Do you love the app?” before the native card. Google's integration guidance explicitly says not to ask opinion or predictive rating questions before or while presenting the card. Sentiment gates also create a coercive split where satisfied users are sent to the store and dissatisfied users are diverted elsewhere.

A general feedback action can exist in settings or support, but it should not determine who receives the platform request. Keep feedback and rating as separate user choices.

Request the native card without coupling navigation to it

On iOS, call the supported StoreKit review request API for the active scene after the milestone screen is settled. On Android, request the review-flow token and launch it from a valid activity. Exact API names change across SDK generations, so isolate the platform call behind a small interface rather than spreading it through feature screens.

interface ReviewRequester:
    requestIfAvailable(surfaceContext) -> RequestAttemptResult

RequestAttemptResult
  requested: true
  platformCallCompleted: true
  promptShown: unknown
  reviewSubmitted: unknown

The caller should continue its normal navigation regardless of the result. Do not wait for a rating before unlocking content, granting a reward, dismissing a success screen, or saving completed work. If the platform declines to show the card, the user should see no broken state and no empty overlay.

Log only facts your process knows:

  • review_request_eligible
  • review_request_attempted
  • review_request_api_failed
  • review_store_link_opened

Attach effective_app_locale, device_locale, app_version, platform, and the milestone identifier. Do not record review_prompt_seen, review_rating_selected, or review_submitted unless a future public platform API explicitly confirms that event.

Handle app-language and device-language disagreement

Assume the rating card may follow platform language rules that differ from your app selector. Do not mutate undocumented preferences, swizzle bundle lookup, walk system view hierarchies, or place translated labels above the card. Those approaches are unsupported and can fail after an OS update.

Instead, make every app-owned part of the transaction coherent:

  1. Resolve the effective app locale from the same state used by the current success screen.
  2. Record both app and device locales in diagnostic events.
  3. Keep eligibility messages, settings labels, and voluntary review links in the app-selected language.
  4. Invoke the native card without promising its language or appearance.
  5. Return immediately to the localized app flow when the platform call completes.

If product requirements demand that every rating instruction use the selected app language, the native card cannot satisfy that guarantee. State the limitation in the engineering decision record. Do not replace the native flow with a custom star form that implies it submits an App Store or Play review.

Add a localized user-initiated fallback

Automatic requests and explicit user intent are different paths. Apple documents a store deep link with the action=write-review query parameter for a user-initiated review action. Put a localized “Write a review” item in Settings or About, then open the official listing. On Android, open the app's Google Play listing through the supported store URI or HTTPS fallback.

Use this route only for someone who independently chooses to review the app, not as a retry button for a suppressed native prompt. Keep it available, but do not show a message such as “The review prompt failed.” Suppression is normal platform behavior, not an error the user needs to fix.

The route needs the same locale discipline as any external handoff. Verify that the listing exists in every launched market, the app ID or package name is correct, store-app and browser fallbacks work, and the label is translated. The store may still choose its own locale from account, storefront, device, or URL context.

Test behavior, not a fabricated submission

A unit test can prove eligibility and event naming. It cannot prove how a production store decides to display the card. Keep tests at three levels.

Policy tests should cover milestone count, cooldown, app version, blocking errors, and repeat attempts. Include app-language versus device-language disagreement as ordinary input, not as an exception.

Integration tests should verify that a platform request occurs at most once for an eligible event, normal navigation continues when the requester returns success or failure, and no analytics event claims display or submission. Inject a fake requester rather than invoking a store in every test.

Store-track tests should exercise the real installation channel. Android's in-app review testing guide explains that internal test tracks bypass quota limits, internal app sharing cannot submit reviews, and FakeReviewManager verifies API behavior without showing the real UI. Each surface verifies a different part of the integration, so passing a fake-manager test is not proof that the Play card will appear in production.

Use this cross-locale matrix on both platforms:

App language Device language Expected app behavior Card assertion
English English Request after eligible milestone May appear
Turkish English Turkish app copy and normal continuation Language observed, not forced
Arabic English RTL app screen remains correct after return Language observed, not forced
English French English app flow remains stable Language observed, not forced
Unsupported app locale German Documented app fallback is used May appear

Capture the OS version, store account region, install channel, app version, and observed card language when the real card appears. Do not fail the whole release because a quota-controlled card is absent. Fail when the app repeats requests, blocks navigation, emits false conversion events, opens the wrong listing, or leaves its own UI in the wrong locale.

Failure modes to catch before release

A review integration is broken when it creates product problems around a platform limit.

  • Repeated attempts: every completed task calls the API because local cooldown state is written after the call.
  • False conversion data: API completion is logged as a submitted five-star review.
  • Language promises: app copy says the next screen will be Turkish when the system owns the card locale.
  • Blocked progress: a success screen waits for a callback that never proves user action.
  • Manipulative routing: a sentiment question sends only positive users to the store.
  • Unsupported UI changes: code searches the native card's view hierarchy to replace labels.
  • Weak tests: a fake requester passes, but no store-track build tests the real integration.
  • Broken fallback: the voluntary review link opens the wrong storefront, package, or app ID.

Run these cases after StoreKit, Play Core, target SDK, locale-handling, or navigation changes. The checklist catches bugs in app-owned behavior without treating a missing platform card as a release failure.

Put the boundary into the next release

Start by renaming any review_submitted analytics event that currently fires after the API returns. Then put the native request behind one interface, add the cross-locale matrix to release QA, and verify the voluntary store-listing route for each platform.

Store-facing copy has its own localization path; see how to localize release notes for the App Store and Google Play.