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 steps prop.
  • Chrome — everything else: "Continue", "Done", "Processing...", the confirm button, success titles, offer card labels. This is what i18n controls.

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:

  1. The step's savedTitle / savedDescription props (most specific)
  2. The accepted offer type's entry (success.saved.pause, success.saved.discount, …)
  3. The generic success.saved.title / description — also the fallback for contact, 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:

  1. Built-in English defaults
  2. i18n.messages (through the locale fallback chain)
  3. 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.)