v154 · payments · secure payment confirmation
When your locale and the dialog disagree
Secure Payment Confirmation shows a browser dialog carrying your merchant name, instrument and amount. You supply that text; the browser supplies the frame around it. If you say the text is French and the dialog is in English, the result is a sentence in two languages — so Chrome 154 refuses instead, with a NotSupportedError.
concepts
-
Building the request
Every field of a real
secure-payment-confirmationrequest, constructed live. Including the one that surprises everybody: the data is JSON-serialised, so a getter in there is aTypeErrorbefore anything else is checked. -
Which of your tags would match
Canonicalise a list of language tags, find out which ones this browser has data for, and see which one a best-fit match resolves to — all through
Intl, which is the same machinery the question is really about. -
A checkout that survives the refusal
The practical shape: check what you can before showing anything, and have somewhere to go when the answer is no. A refusal at the dialog is a refusal in front of the customer.
why it shipped
The dialog is a mixture: the browser's own words around the merchant's. The locale field is how a caller says which language its half is in, so the browser can pick a matching frame. When it cannot — because the browser has no dialog in that language — the previous behaviour was to show the dialog anyway, in whatever language it had, wrapped around text in another.
That is worse than an error. A payment confirmation is exactly the moment a user should not be confused about what they are agreeing to, and a half-translated one invites the reaction the flow is designed to prevent. Refusing gives the caller a chance to supply text the browser can actually frame.
the API
const request = new PaymentRequest([{
supportedMethods: "secure-payment-confirmation",
data: {
rpId: "example.com",
credentialIds: [credentialId],
challenge,
instrument: { displayName: "Visa ····4242", icon: "/card.png" },
payeeName: "Example Shop",
locale: ["fr-CA", "fr", "en"], // ordered by preference
timeout: 60_000,
},
}], details);
// The locale is not validated here — it is validated when the dialog opens.
await request.show(); // NotSupportedError if none of them match
enabling it now
Measured on the Chrome 150 used to build these pages: an SPC request constructs successfully with any locale value, including ["zz"] and [] — because the field is not checked at construction. canMakePayment() resolves false here, as it does on any machine with no matching credential, so the dialog is never reached and the refusal cannot be provoked.
new PaymentRequest([{ supportedMethods: "secure-payment-confirmation",
data: { …, locale: ["zz"] } }], details) // constructs fine
await request.canMakePayment() // false — no credential here
So these pages exercise everything up to the dialog and say plainly where they stop. The locale matching itself is demonstrated through Intl, which answers the same question with the same data the browser has.