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

# Checkout Language

> Which field sets the language of the Waffo checkout, the exact BCP 47 format and validation errors, the full priority order the checkout uses to pick a display language, and the complete list of languages the checkout renders.

This page covers only the **language** of the Waffo-hosted checkout: the field
you pass, what values it accepts, and how the page decides which language to
render when you pass nothing.

Which payment methods appear and in what order is a different decision — see
[Checkout Payment Method Display Rules](/docs/en/support/cashier-display-rules-faq).
Country switching, redirect URLs and `orderAction.actionType` are covered in
[Checkout Redirects & Action Types](/docs/en/support/cashier-redirect-and-action-type-faq).

## Which field controls the language the checkout is displayed in?

`paymentInfo.cashierLanguage`, passed when you create the order.

| | |
| - | - |
| Field | `paymentInfo.cashierLanguage` |
| Required | No |
| Max length | 24 characters |
| Format | IETF BCP 47 language tag |
| Validated | At order creation — an invalid value fails the create call |
| Scope | That one order. It is not an account-level setting, so you pass it per order. |

There is no Merchant Portal setting that pins the checkout language. If you
want a fixed language, pass `cashierLanguage` on every order.

## What format does `cashierLanguage` have to be in?

`language[-script][-region]`, and nothing else:

| Subtag | Standard | Rule |
| - | - | - |
| language | ISO 639-1, two letters | Required. Must be a language the checkout supports — the set is closed. |
| script | ISO 15924, four letters | Optional. If present, must be `Hant` or `Hans` — the set is closed. |
| region | ISO 3166-1 alpha-2, two letters | Optional. Must be a valid country/region code. Any valid code is accepted, whether or not the checkout has copy specific to it. |

Examples that pass:

| Value | Meaning |
| - | - |
| `en` | English |
| `en-HK` | English, Hong Kong |
| `zh-Hant-HK` | Traditional Chinese, Hong Kong |
| `zh-Hans-HK` | Simplified Chinese, Hong Kong |
| `ja-JP` | Japanese, Japan |

Values that are rejected:

* Underscore separators — `en_US` is not a BCP 47 tag.
* Variant subtags — `de-DE-1996`.
* Extension subtags — `en-US-x-foo`.
* Anything longer than 24 characters.

Casing is normalised for you, so `en-us` is accepted and stored as `en-US`.

## What error do I get if the value is wrong?

Two distinct outcomes, and they mean different things:

| Situation | Error | Message |
| - | - | - |
| The tag is structurally invalid, carries a variant/extension subtag, or the region is not a real ISO 3166-1 alpha-2 code | `A0047` | `The cashier language format is invalid, it must follow IETF BCP 47: a two-letter ISO 639-1 language, an optional four-letter ISO 15924 script, and an optional two-letter ISO 3166-1 alpha-2 region` |
| The tag is well formed, but the language or the script is outside the supported set | `A0026` | `The cashier language specified by merchant currently is not supported` |

Both are returned as HTTP 400 at order creation, so a bad value never reaches
the checkout page. If you get `A0047`, fix the shape of the string. If you get
`A0026`, the shape is fine but the language itself is not one Waffo renders —
check the list below.

## Does the same field work for subscriptions?

Yes. `paymentInfo.cashierLanguage` exists on subscription creation with the
same name, the same 24-character limit, the same BCP 47 rules and the same two
errors. It is validated in the same place in the subscription create flow.

## Does `cashierLanguage` affect anything besides the checkout page?

For subscriptions, yes: **end-user subscription notifications reuse the same
decision**. When Waffo sends a subscription notification to your customer, the
language is `cashierLanguage` if you supplied one, and otherwise is inferred
from the customer's payment currency.

Note the difference from the checkout page: a notification is not a browser
session, so there is no `Accept-Language` header and no country tab to read.
The fallback for notifications is **currency-based**; the fallback for the
checkout page is **country-based** (see the next question).

## How does the checkout decide the display language? Is there a priority order?

Yes — five layers, evaluated top to bottom. The first layer that produces a
value wins.

| Layer | Source | Notes |
| - | - | - |
| 0 | **Frozen display language** | A snapshot of the language the customer was actually looking at when they submitted on the payment-method selection page. Present only after that submission. |
| 1 | **`paymentInfo.cashierLanguage`** | Your value, used as-is (already validated and normalised at order creation). |
| 2 | **Browser `Accept-Language`** | The highest-priority entry of the header only. See below for exactly what is read. |
| 3 | **Country inference** | Before submission, the customer's *real* location. After submission, the checkout's display country. See below. |
| 4 | **`en`** | Final fallback whenever layer 3 yields nothing usable. |

Two consequences worth designing around:

* **Layers 0, 1 and 2 lock the language.** Once one of them hits, the language
  no longer follows the country: the customer can switch the country tab and
  the copy stays put.
* **Only layer 3 follows the country tab.** So a customer with no
  `cashierLanguage` and no usable `Accept-Language` will see the copy change
  as they switch countries — and a customer with either of those will not.

Language resolution is display-only. If it fails for any reason, the payment
itself is unaffected.

## Which country does layer 3 actually use?

It depends on where the order is in its lifecycle, and the distinction matters
because the "display country" and the "customer's country" are not the same
thing.

| Order state | Country used for layer 3 |
| - | - |
| Still on the payment-method selection page | The customer's **real** location: the country tab they switched to, if they switched to a real country; otherwise the country inferred from their IP. |
| After submission | The checkout's **display country** (`countryCode`). |

On the selection page the display country can be back-filled to some country
from the order's candidate list when the customer's own country is not among
them — that back-filled value does not describe where the customer is, so the
language decision deliberately does not use it.

Selecting the **Global** tab is not a location, so it is not treated as one: it
falls through to the IP-inferred country.

## What is the frozen display language, and why does layer 0 exist?

When the customer submits their choice on the payment-method selection page,
the checkout stores the language they were looking at onto the order. From then
on that value wins over everything else.

It exists to stop the language from drifting mid-payment. A globally available
payment method moves the order's display country to **Global**, which is a
sentinel rather than a real country. Without the snapshot, an order that was
being displayed in a local language would flip to the English fallback the
moment the customer picked a global method — the customer would watch the page
change language between choosing and paying. The snapshot pins whatever they
already saw.

You cannot set or read this value; it is written by the checkout at submission
time. Its practical effect on your integration is only this: **after the
customer has submitted on the selection page, nothing changes the language any
more.**

## What exactly does the checkout read from `Accept-Language`?

Layer 2 is narrower than most people expect:

* Only the **highest-weighted** entry of the header is considered. If that one
  does not map to a supported language, the checkout does **not** try the next
  one — it drops to layer 3.
* Only the **primary language subtag** is used. `zh-Hant-TW` in the header
  collapses to `zh`; the script and region in the header are discarded.
* The accepted primary subtags are `en`, `es`, `id`, `ja`, `ko`, `ms`, `pl`,
  `pt`, `th`, `tr`, `vi`, `zh`. Anything else — `fr`, `ru`, and so on — yields
  nothing and falls through to layer 3.
* A malformed header (some older in-app browsers emit repeated quality values)
  degrades to reading the first language tag in the raw string, with the same
  mapping.

Because layer 2 resolves at the primary-subtag level, a `zh` from the browser
renders **Traditional** Chinese — see the script fallback below.

## Which languages does the checkout support?

These are the values `cashierLanguage` accepts as complete tags:

| Tag | Language |
| - | - |
| `en` | English |
| `pt-BR` | Portuguese (Brazil) |
| `es-MX` | Spanish (Mexico) |
| `es-PE` | Spanish (Peru) |
| `es-CO` | Spanish (Colombia) |
| `es-CL` | Spanish (Chile) |
| `id-ID` | Indonesian |
| `vi-VN` | Vietnamese |
| `ru-RU` | Russian |
| `th-TH` | Thai |
| `ja-JP` | Japanese |
| `ko-KR` | Korean |
| `ms-MY` | Malay |
| `pl-PL` | Polish |
| `tr-TR` | Turkish |
| `en-KE` | English (Kenya) |
| `en-NG` | English (Nigeria) |
| `en-HK` | English (Hong Kong) |
| `zh-Hant-TW` | Traditional Chinese (Taiwan) |
| `zh-Hant-HK` | Traditional Chinese (Hong Kong) |
| `zh-Hans-HK` | Simplified Chinese (Hong Kong) |
| `zh-Hans` | Simplified Chinese |

You are not restricted to exactly these strings. Validation is per subtag, so
any combination of a supported language, an optional supported script, and a
valid ISO region is accepted — `en-SG`, for example. What the *region* changes
is covered next.

## I passed `en-HK` and the page looks identical to `en`. Is the region subtag ignored?

For the page copy, effectively yes. Checkout copy is maintained in **13
language buckets**, and a tag is matched to a bucket by language plus script
only — the region subtag never selects a different set of copy:

| Bucket | Tags that land in it |
| - | - |
| `en` | `en`, `en-HK`, `en-KE`, `en-NG`, any `en-*` |
| `pt` | `pt-BR`, any `pt-*` |
| `es` | `es-MX`, `es-PE`, `es-CO`, `es-CL`, any `es-*` |
| `zh-Hant` | `zh-Hant-TW`, `zh-Hant-HK`, and bare `zh` |
| `zh-Hans` | `zh-Hans`, `zh-Hans-HK` |
| `vi` | `vi-VN` |
| `th` | `th-TH` |
| `id` | `id-ID` |
| `ja` | `ja-JP` |
| `ko` | `ko-KR` |
| `ms` | `ms-MY` |
| `pl` | `pl-PL` |
| `tr` | `tr-TR` |

So `es-PE` and `es-CL` render the same Spanish copy, and `en-KE` and `en-NG`
render the same English copy. The region subtag is still carried on the order
and is still used for locale-aware formatting, but it does not fork the wording.

Two specific rules that surprise people:

* **Bare `zh` renders Traditional Chinese.** With no script subtag there is
  nothing to distinguish the two, so the checkout resolves `zh` to `zh-Hant`.
  If you want Simplified, pass a `Hans` script explicitly — `zh-Hans` or
  `zh-Hans-HK`.
* **A language with no bucket renders English.** `ru-RU` passes validation,
  but Russian is not one of the 13 copy buckets, so the page renders in
  English.

## I passed nothing at all. What language does the customer see?

Layers 2 to 4 decide, in that order:

1. If the customer's browser sends a header whose top entry is one of the 12
   accepted primary subtags, that language is used, and it will not change if
   they switch country tabs.
2. Otherwise the language is inferred from the country — their real location
   before submission, the display country after — and **does** change as they
   switch tabs.
3. If neither yields anything, the page is in English.

If a deterministic language matters to you — a localized brand experience, a
regulated market, a screenshot-based support flow — pass `cashierLanguage`
rather than relying on this chain.

## Which languages can the customer pick when downloading a receipt?

The same 13 buckets, offered by their own names: English, Português, Español,
繁體中文, 简体中文, Tiếng Việt, ไทย, Bahasa Indonesia, 日本語, 한국어, Bahasa
Melayu, Polski, Türkçe.

The receipt language is the customer's own choice at download time. It is not
driven by `cashierLanguage`, and picking one does not change the language of
the checkout page.

## Can I let the customer choose the checkout language themselves?

There is no language selector on the checkout page. The language is whatever
the five-layer chain resolves to.

What the customer *can* change is the country, on the payment-method selection
page, and that shifts the language only when the chain reached layer 3 — that
is, only when you did not pass `cashierLanguage` and their browser header did
not match. See
[Checkout Redirects & Action Types](/docs/en/support/cashier-redirect-and-action-type-faq)
for when the country selector is shown at all.


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