> ## Documentation Index
> Fetch the complete documentation index at: https://docs.encorekit.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Localization

> How Encore picks each user's language, which copy it translates, which copy stays yours, and how to check what was served.

Encore serves translated copy to each user in their language. The SDKs send the device or browser language for you, Encore resolves it against the languages enabled for your app, and the sheet and its offers come back in that language. Copy you pass per call is the one exception: Encore shows it exactly as you wrote it.

Read this page when your app ships in more than one language, when a user reports seeing English in a translated app, or when you need to know what a `locale` field in a response means.

## How Encore picks the language

Every request resolves to one served language. Encore takes the first candidate that is enabled for your app:

| Order | Candidate | Where it comes from |
| - | - | - |
| 1 | Your app's language | `setLanguage()` in the SDKs. On the API, `?language=` on `/config` and `/ui-config`, `attributes.language` in the body of `/offers/feed`, `/offers/message`, and `/offers/search`, or `language` in the body of `/offers/catalog`. `/creators/links` has no explicit language; see [Check what was served](#check-what-was-served) |
| 2 | The device or browser languages | The `Accept-Language` header, tried in the order the user ranked them |
| 3 | Your app's default language | Set by Encore when your languages are enabled. `en` unless you asked for another |

A candidate is used only when your app has that language enabled. One that is not enabled is skipped, not rejected. Encore walks the device's languages in order and serves the first one your app has enabled. A device set to French, then Spanish, on an app with only English and Spanish enabled gets Spanish. The app's default language is served only when none of the user's languages is enabled. English is always enabled, so a device that lists English always has a match.

**Encore serves by language, not by region.** It accepts any BCP-47 tag and keeps only the language part, so `es-MX`, `es-ES`, and `es` all serve `es`. Send whichever form you already have. Two consequences worth knowing:

* `pt` is translated as Brazilian Portuguese, so users in Portugal see Brazilian spelling.
* `zh` is translated as Simplified Chinese, so users in Taiwan and Hong Kong see Simplified characters.

## What the SDKs send for you

You do not have to do anything for a user to get their own language.

| Surface | What it sends |
| - | - |
| iOS | The device's preferred languages as `Accept-Language` on every request |
| Android | The device's languages as `Accept-Language` on every request, from Android SDK 2.3.0. Earlier releases send nothing, so set the `language` user attribute if you need translated copy on them |
| Web SDK | The browser's own `Accept-Language` |
| React Native, Flutter | Whatever the native SDK underneath sends |

## Override the language

Call `setLanguage()` when your app has its own language picker, or when the user's app language differs from their device language. It outranks the device language on every request the SDK makes. The SDK keeps the language on the device, across launches, until you clear it. Encore never stores it on its servers.

<CodeGroup>
  ```swift iOS theme={null}
  Encore.shared.setLanguage("es")
  ```

  ```kotlin Android theme={null}
  Encore.shared.setLanguage("es")
  ```

  ```typescript React Native theme={null}
  await Encore.setLanguage('es');
  ```

  ```dart Flutter theme={null}
  await Encore.shared.setLanguage('es');
  ```

  ```javascript Web theme={null}
  Encore.setLanguage('es');
  ```
</CodeGroup>

To go back to the device language, for example when the user picks "System default" in your picker, call `clearLanguage()`. It keeps the user and every other attribute.

The language is a preference of the device, like appearance, not user state. `reset()` and logout leave it in place, and only `clearLanguage()` removes it.

<CodeGroup>
  ```swift iOS theme={null}
  Encore.shared.clearLanguage()
  ```

  ```kotlin Android theme={null}
  Encore.shared.clearLanguage()
  ```

  ```typescript React Native theme={null}
  await Encore.clearLanguage();
  ```

  ```dart Flutter theme={null}
  await Encore.shared.clearLanguage();
  ```

  ```javascript Web theme={null}
  Encore.clearLanguage();
  ```
</CodeGroup>

`setLanguage()` and `clearLanguage()` are new in iOS SDK 2.3.0, Android SDK 2.3.0, and the web SDK release that includes language support. React Native and Flutter add them in the release that pins native 2.3.0. On the web SDK, call `setLanguage()` after [`configure()`](/publishers/web/sdk-reference/configure); `configure()` takes no language. Reference: `setLanguage()` on [iOS](/publishers/ios/sdk-reference/set-language), [Android](/publishers/android/sdk-reference/set-language), [React Native](/publishers/react-native/sdk-reference/set-language), [Flutter](/publishers/flutter/sdk-reference/set-language), and [web](/publishers/web/sdk-reference/set-language).

### Language tags

`setLanguage()` takes a language tag in the BCP-47 syntax (RFC 5646) and checks it before anything is sent. The rule is narrower than the RFC on purpose, and the same on every SDK:

* The tag starts with a language code of two or three letters, for example `en`, `pt`, or `fil`. Longer language codes and old tags such as `i-klingon` are rejected.
* Its parts are separated by hyphens only. `pt-BR` is accepted, `pt_BR` is not.
* Case does not matter, so `PT-br` is the same as `pt-BR`.
* The tag has no surrounding spaces or trailing newline. `' en'` and `'en\n'` are rejected.
* A language name is not a tag. `english` is rejected; use `en`.
* Extended language forms are not accepted, because Encore resolves a tag to its language and region and would pick the wrong language. `zh-yue-HK` is rejected; pass the preferred form `yue-HK`.

Every SDK checks the same rule, so a tag accepted on one platform is accepted on all of them.

After the check, a withdrawn ISO 639 code is replaced by its current one: `iw` becomes `he`, `in` becomes `id`, `ji` becomes `yi`, and `jw` becomes `jv`. Every SDK and Encore's own resolver do this, so `setLanguage("iw")` serves Hebrew everywhere. Encore's resolver also serves `nb` (Norwegian Bokmål) as `no`.

Every SDK also normalizes the case of an accepted tag, following RFC 5646: the language is lowercase, a region is uppercase, a script is title case, and everything else is lowercase. `PT-br` becomes `pt-BR`, and `en-US-X-Twain` becomes `en-US-x-twain`.

The language you set stays on the device. It is never sent as a user attribute or in an `identify()` call, and Encore's analytics record only the language that was served.

What happens to a tag that fails the check depends on the SDK:

| SDK | Invalid tag |
| - | - |
| iOS, Android, web | Logged and ignored. The language already in effect stays |
| React Native | The returned `Promise` rejects: a `RangeError` for a malformed tag, a `TypeError` for a value that is not a string |
| Flutter | The returned `Future` completes with an `ArgumentError` |

### The `language` user attribute is deprecated

Earlier releases set the language through the `language` field of `setUserAttributes()`. That field still works: from the releases in the preceding paragraph, setting it calls `setLanguage()` for you and logs a deprecation warning once. It is removed at the next major release, so move to `setLanguage()`. A language your app saved through the attribute on an earlier release carries over the first time the new SDK starts, so users keep their language when they update. A language set through the attribute is stored as the same override, so it also survives `reset()`.

Because some apps pass a locale identifier, the deprecated attribute still accepts one and turns it into a tag before forwarding it: surrounding spaces are trimmed, anything from `@` on is dropped, and `_` becomes `-`, so `de_DE@currency=EUR` becomes `de-DE` and `pt_BR` becomes `pt-BR`. The first-launch carry-over does the same, so no user loses their language on upgrade. `setLanguage()` itself is strict and accepts none of these forms; see [Language tags](#language-tags).

## What Encore translates

Encore translates the copy it serves from its own side. Once a language is enabled for your app, these arrive translated:

* The headline and subheadline you set for your app in the Encore dashboard
* Use-case copy: the reward headline and subheadline on [Reward Users](/publishers/use-cases/post-action-reward)
* The copy inside each sheet variant, including its button labels
* Campaign copy: each offer's `perk` on `/offers/feed`, `/offers/message`, `/offers/catalog`, `/offers/search`, and [`/creators/links`](/publishers/offers-api/reference/creator-links), and its display properties on the responses that carry them (`/offers/feed`, `/offers/catalog`, and `/offers/search`)
* Category names on `/offers/search`, as `categoryDisplayName` and `categoryGroupDisplayName` (see [Offer search](#offer-search))
* The SDK's own button and status words (see [SDK strings](#sdk-strings))

Offer creative text (`title`, `subtitle`, `description`, `ctaText`, `tooltipText`, and the redemption instructions) is no longer translated. Creative text that already has translations keeps serving them. New creative text is shown in its source copy, and the campaign copy around it is translated.

Encore machine-translates this copy and checks each result automatically. What happens next depends on where the copy is shown:

* Your app's own copy (the dashboard headline and subheadline, and use-case copy) goes live once it passes the checks.
* Offer copy is shared by every app that runs the offer, so a person at Encore approves each machine translation before it goes live. This includes a perk Encore set for your app alone.
* Sheet-variant copy, SDK strings, and category names are also shared by every app, so a person at Encore approves each translation before it goes live.

When the English of an offer's copy changes, its old translations stop serving, and the new English shows until the new translations are approved.

The Encore team can also correct any string by hand. Once approved, a hand correction always wins over the machine translation.

A field without a translation yet falls back to its source copy on its own. One untranslated field never blanks the sheet or pulls the rest of it back into English.

## What stays yours

Copy you pass per call is never translated. That means `.headline()` and `.subheadline()` on the placement builder in the iOS, Android, React Native, and Flutter SDKs, and the `headline` and `subheadline` placement options in the Web SDK. Encore shows the string exactly as you passed it, so localize it the way your app already localizes its own strings.

```swift theme={null}
let result = await Encore.shared.placement("milestone_reached")
  .useCase(.rewardUsers)
  .headline(String(localized: "reward.headline"))  // your string, your translation
  .show()
```

Reference: [`placement()` on iOS](/publishers/ios/sdk-reference/placement).

The per-call value still outranks the Encore-resolved copy. If you pass an English headline to a Spanish user, the headline stays English while the rest of the sheet is Spanish. Pass a per-call headline only when you can supply it in the user's language, and otherwise let the translated copy through. The full order is in [Where the copy comes from](/publishers/use-cases/post-action-reward#where-the-copy-comes-from).

## Enabling languages

Encore enables languages for each app. To add one, contact [admin@encorekit.com](mailto:admin@encorekit.com) with the app and the languages you want. Encore can translate a language for your app before enabling it, so the copy can be read first, but no user is served a language until it is enabled. Until then, every user of that app gets the default language.

* English is always enabled and cannot be turned off.
* A new language can be enabled only once the SDK's own strings are approved in it, so a sheet never mixes translated copy with English buttons. Languages already enabled for your app keep serving.

## Check what was served

`/config`, `/offers/search`, and `/offers/catalog` report the language they served in a `locale` field. It is always language-only, for example `es`. Compare it with the language you asked for to see whether a request fell back. `/offers/feed`, `/offers/message`, and `/creators/links` translate their copy the same way but return no `locale`.

In every row, a language is used only when your app has it enabled; otherwise the next source decides.

| Endpoint | Where the language comes from | `locale` in the response |
| - | - | - |
| `GET /config` (every SDK) | `?language=`, then `Accept-Language`, then the app default | Yes |
| `POST /offers/search` (Web SDK) | `attributes.language` in the body, then `Accept-Language`, then the app default | Yes |
| [`POST /offers/catalog`](/publishers/offers-api/reference/catalog) | `language` in the body, then `Accept-Language`, then the app default | Yes |
| [`POST /offers/feed`](/publishers/offers-api/reference/feed) | `attributes.language` in the body (required), then `Accept-Language`, then the app default | No |
| [`POST /offers/message`](/publishers/offers-api/reference/message) | `attributes.language` in the body (required), then `Accept-Language`, then the app default | No |
| [`POST /creators/links`](/publishers/offers-api/reference/creator-links) | `Accept-Language`, then the app default. There is no `language` body field | No |

`?language=` on `/config` works the same way as `setLanguage()`: region codes are stripped and it outranks `Accept-Language`. The SDKs add it for you after `setLanguage()`. The legacy `/ui-config` endpoint accepts the same parameter.

To see what a device receives, request the config with a language hint and read `locale`:

```bash theme={null}
curl -s "https://api.encorekit.com/encore/publisher/sdk/v1/config?userId=test-user&sdkVersion=2.0.0&language=es" \
  -H "X-API-Key: pk_test_your_key" | jq '.locale'
```

`"es"` means Spanish is enabled and was served. Your default language, usually `"en"`, means Spanish is not enabled for the app yet.

## Right-to-left languages

When the served language is Arabic, Hebrew, Persian, or Urdu, the sheet lays itself out right to left. Text aligns to the start edge, arrows and chevrons point the other way, and swipes on carousels and the slide-to-claim control follow reading order. On iOS, Android, and web the direction follows the served language, not the host app, so English copy shown to a user with an Arabic device stays left to right. The Web SDK also sets `lang` and `dir` on the dialog, so screen readers use the right voice.

## Offline and first launch

The iOS and Android SDKs cache the last config they received, stamped with the language it was fetched for. A cached copy is only reused for the same language. After a language change the SDK fetches the copy again in the new language. If that copy cannot arrive in time, for example offline, and there is no cached copy for the language, the sheet renders the copy shipped inside the SDK. That built-in copy is English.

## Offer search

`POST /offers/search` is the call the Web SDK makes to fill its sheet. From the September 2026 backend release, each offer in its response carries two display names in the served language:

| Field | Type | What it holds |
| - | - | - |
| `categoryDisplayName` | string \| `null` | `category` translated for display. `null` when the offer has no category |
| `categoryGroupDisplayName` | string \| `null` | `categoryGroup` translated for display, for example the label on a category tab |

`category` and `categoryGroup` stay English on every response, so group and filter on them and show the display names. Both display names fall back to English when a translation is missing.

## SDK strings

Encore also translates the SDK's own words: button labels such as "Claim", status lines on the confirmation screens, and accessibility labels. You do nothing to get them.

From the September 2026 backend release, the `/config` response carries them under `ui.values` in two maps:

* `strings` maps a string id to its translated text.
* `plurals` maps a string id to its plural forms from the Unicode Common Locale Data Repository (CLDR) (`zero`, `one`, `two`, `few`, `many`, `other`), where `other` is always present and `${count}` stands for the number. Every SDK writes the number with ASCII digits (`0` to `9`) and no thousands separator, in every language, so `1200` stays `1200`.

Each map holds only the ids that have an approved translation in the served language, and neither is sent for English. The SDK uses its built-in English for any id that is missing, one id at a time, so a partly translated language never blanks a button. iOS SDK 2.3.0, Android SDK 2.3.0, and the web SDK release that includes language support read these maps. React Native and Flutter read them from the release that pins native 2.3.0. Older SDKs ignore them and show English.

## Related

* [Where the copy comes from](/publishers/use-cases/post-action-reward#where-the-copy-comes-from): the priority order for headline and subheadline
* `setLanguage()`: [iOS](/publishers/ios/sdk-reference/set-language), [Android](/publishers/android/sdk-reference/set-language), [React Native](/publishers/react-native/sdk-reference/set-language), [Flutter](/publishers/flutter/sdk-reference/set-language), [Web](/publishers/web/sdk-reference/set-language)
* `clearLanguage()`: [iOS](/publishers/ios/sdk-reference/clear-language), [Android](/publishers/android/sdk-reference/clear-language), [React Native](/publishers/react-native/sdk-reference/clear-language), [Flutter](/publishers/flutter/sdk-reference/clear-language), [Web](/publishers/web/sdk-reference/clear-language)
* [POST /offers/catalog](/publishers/offers-api/reference/catalog): the `language` body field


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.