Docs navigation
Text & localization
The i18n prop overrides the SDK's own strings — buttons, loading and error text, confirmation copy — and selects the display language.
Two kinds of text render in a cancel flow, and they're customized in different places:
- Flow content — step titles, offer headlines and descriptions. In connected mode this is authored in the Churnkey dashboard and localized server-side by Automatic AI Translations. Override it there, or via the
stepsprop. - Chrome — everything else: "Continue", "Done", "Processing...", the confirm button, success titles, offer card labels. This is what
i18ncontrols.
Overriding messages
Pass partial messages per locale. Anything you don't set keeps its default.
<CancelFlow
session={token}
i18n={{
messages: {
en: {
common: { continue: 'Next' },
success: {
saved: { title: 'Great to have you back!' },
},
},
},
}}
/>Keys are typed — your editor autocompletes the catalog and typos fail the build. The full catalog is in the message reference below.
Timing-aware messages
Some messages should read differently depending on whether the flow cancels immediately or at the end of the billing period. That timing is configured per org (and per flow) in the dashboard; in connected mode the server resolves it and the SDK picks the right variant automatically.
Timing-aware keys accept either a plain string (used for both) or an { immediate, atPeriodEnd } pair:
<CancelFlow
session={token}
i18n={{
messages: {
en: {
confirm: {
cta: {
immediate: 'Cancel subscription',
atPeriodEnd: 'Turn off auto-renew',
},
},
success: {
cancelled: {
title: {
immediate: 'Subscription cancelled',
atPeriodEnd: 'Auto-renew is off',
},
},
},
},
},
}}
/>The timing-aware keys: confirm.cta, confirm.periodEndNotice, success.cancelled.title, success.cancelled.description.
In local mode the SDK can't infer your billing behavior, so declare it with the cancelAtPeriodEnd prop:
<CancelFlow steps={steps} cancelAtPeriodEnd={false} handleCancel={handleCancel} i18n={i18n} />The declaration drives timing-aware variant selection, and cancelAtPeriodEnd={true} also enables the confirm step's access-until notice. Undeclared, pairs resolve to atPeriodEnd and the notice stays hidden. In connected mode the server-resolved value is authoritative and the prop is ignored.
Per-offer success copy
The saved success screen confirms the specific outcome: accepting a pause shows "Subscription paused.", a discount shows "Discount applied.", and so on — the same copy the Churnkey widget uses. Resolution order for the saved screen:
- The step's
savedTitle/savedDescriptionprops (most specific) - The accepted offer type's entry (
success.saved.pause,success.saved.discount, …) - The generic
success.saved.title/description— also the fallback forcontact,redirect, and custom offer types
Pause uses resumeDescription instead of description when the accepted result carries the selected duration (the built-in pause UI always passes it), filling {resumeDate} with the computed resume date.
i18n={{
messages: {
en: {
success: {
saved: {
pause: { title: 'See you soon!', resumeDescription: "We'll restart billing on {resumeDate}." },
},
},
},
},
}}Offer button text
Offer accept buttons and the decline link default to per-type standard copy ("Accept refund", "No thanks", ...). Override them with offer.acceptCta.<type> and offer.declineCta — in code or in the dashboard's SDK Text Overrides. An unset key keeps the offer's own button text.
i18n={{ messages: { en: { offer: { acceptCta: { rebate: 'Claim my refund' }, declineCta: 'Keep my plan' } } } }}Locale selection
i18n={{ locale: 'de', messages: { de: { ... } } }}locale defaults to the browser language. Lookup runs through a fallback chain — exact tag (pt-BR), base language (pt), then en — so a partial regional catalog falls back gracefully. The SDK ships English defaults; other languages render whatever you provide, falling back to English for missing keys.
Messages may contain {token} placeholders where documented (e.g. {periodEnd}, {planName}, {minLength}); the SDK fills them at render time.
Precedence
Lowest to highest:
- Built-in English defaults
i18n.messages(through the locale fallback chain)- Per-step props —
confirmLabel,savedTitle, and friends always win, since they're the most specific
Message reference
interface CancelFlowMessages {
common: {
continue: string // "Continue"
back: string // "Back"
close: string // "Close" (close button aria-label)
done: string // "Done"
processing: string // "Processing..."
tryAgain: string // "Try again"
loading: string // "Loading your options..."
loadError: string // "We couldn't load your cancellation options. Please try again."
error: string // "Something went wrong. Please try again."
day: string; days: string; month: string; months: string
}
survey: {
title: string // "Why are you cancelling?"
followupPlaceholder: string // "Tell us more (optional)"
followupAriaLabel: string // "Additional detail"
}
feedback: {
title: string // "Any other feedback?"
placeholder: string // "Type your thoughts…"
placeholderWithMin: string // "At least {minLength} characters…"
}
confirm: {
title: string // "Confirm cancellation"
lossesLabel: string // "You'll lose access to:"
cta: TimingAware // "Cancel subscription"
goBack: string // "Go back"
periodEndNotice: TimingAware // atPeriodEnd: "Your access continues until {periodEnd}."
}
success: {
saved: {
title: string // "Welcome back!" — fallback for offer types without an entry
description: string // "Your offer has been applied."
discount: { title: string; description: string } // "Discount applied." / "We're so happy you're still here."
pause: {
title: string // "Subscription paused."
description: string // "You won't be billed until your subscription resumes."
resumeDescription: string // "You won't be billed again until {resumeDate}." — used when the resume date is known
}
trial_extension: { title: string; description: string } // "Trial extended." / …
plan_change: { title: string; description: string } // "Plan changed." / …
rebate: { title: string; description: string } // "Refund issued." / …
}
cancelled: {
title: TimingAware // "Subscription cancelled"
description: TimingAware // "We're sorry to see you go."
}
}
offer: {
limitedTimeEyebrow: string // "Limited-time offer"
pauseEyebrow: string // "We'll see you back on"
newEndDateLabel: string // "New end date"
currentPlanBadge: string // "Current"
switchToCta: string // "Switch to {planName}"
rebate: {
paidLabel: string // "You paid this period"
moneyBackLabel: string // "Money back"
inclTax: string // "(incl. {amount} tax)"
netLabel: string // "Your net for this period"
}
}
}
type TimingAware = string | { immediate: string; atPeriodEnd: string }Headless
useCancelFlow returns the resolved catalog as messages, and cancelAtPeriodEnd on state carries the resolved timing (null when unknown). Use the exported selectTiming helper to pick a variant:
import { useCancelFlow } from '@churnkey/react/headless'
import { selectTiming } from '@churnkey/react/core'
const flow = useCancelFlow({ session: token, i18n })
const confirmLabel = selectTiming(flow.messages.confirm.cta, flow.cancelAtPeriodEnd)(Both are also re-exported from the package root.)