Add i18n: typed message catalog and timing-aware copy#34
Conversation
Every string the SDK renders itself now flows through a typed
CancelFlowMessages catalog instead of hardcoded literals. The new i18n
prop takes a locale plus deep-partial per-locale overrides; lookup runs
an exact -> base-language -> en fallback chain and empty-string
overrides are dropped so a blank field can't clobber a real string.
Timing-sensitive messages (confirm CTA, period-end notice, cancelled
success copy) accept { immediate, atPeriodEnd } pairs resolved against
the server's cancelAtPeriodEnd, which is now threaded from the token
config into FlowState. The period-end notice renders only when timing
is known to be period-end: an earlier hardcoded notice was removed
because it could contradict the merchant's setting, and that still
holds in local mode where the SDK can't verify billing behavior.
All new props are optional and default strings are unchanged, so
existing integrations render identically. Headless consumers get
messages and cancelAtPeriodEnd from useCancelFlow, plus an exported
selectTiming helper.
Locale picker (en / de / de-AT) exercising the fallback chain — de is a deliberately partial catalog, de-AT has no entry and resolves via the base language. The en catalog uses timing-aware pairs on confirm.cta and success.cancelled.*; local mode has unknown timing so the atPeriodEnd variants render and the access notice stays hidden, which is the designed behavior. Steps omit per-step label props since those outrank i18n messages and would mask the overrides.
Review follow-up: timing-aware messages silently resolved to the atPeriodEnd variant in local mode, where the SDK has no way to know the real timing — an immediate-cancel integration could show "Turn off auto-renew" copy. New optional cancelAtPeriodEnd on FlowConfig / CancelFlowProps lets the developer declare their billing behavior; token mode's server-resolved value stays authoritative. An explicit declaration also satisfies the known-timing gate on the confirm step's access-until notice, so local integrations that declare period-end get it too. The playground i18n scenario grows a timing picker exercising all three states.
|
Review follow-up pushed: local mode can now declare its billing behavior via a new optional |
There was a problem hiding this comment.
Hey Rob, approved and found two follow-ups, please check it out:
- Token mode isn't actually authoritative when the server omits the value. The state fallback uses ?? this.localCancelAtPeriodEnd (), but the real cancel call falls back to ?? true (
sdk/packages/react/src/core/machine.ts
Lines 495 to 499 in 10a8fbb
). When settings.cancelAtPeriodEnd is absent (it's optional in the schema), the displayed timing and the server action can diverge, and the local declaration leaks into token mode — contradicting the JSDoc "In token mode ... this is ignored" (sdk/packages/react/src/core/machine.ts
Lines 423 to 426 in 10a8fbb
). I suggest aligning both fallbacks and gating the local value on non-token mode.sdk/packages/react/src/core/types.ts
Lines 711 to 714 in 10a8fbb
- periodEndNotice can't be suppressed via override despite the JSDoc. The doc says "An empty variant suppresses the notice" (), but mergeValue drops empty strings (patch === '' ? base : patch and the truthy checks on the timing pair) (
sdk/packages/react/src/core/messages.ts
Lines 47 to 50 in 10a8fbb
), so periodEndNotice: '' / { atPeriodEnd: '' } renders the default instead of suppressing. Either honor empty overrides for this key or drop the "empty suppresses" claim from the doc.sdk/packages/react/src/core/messages.ts
Lines 193 to 203 in 10a8fbb
Review follow-ups. The local cancelAtPeriodEnd declaration leaked into token mode when the server omitted settings.cancelAtPeriodEnd, so the displayed timing could diverge from the cancel action's own end-of-period fallback — and contradicted the prop's documented token-mode behavior. The declaration is now stored only in local mode; an absent server value leaves timing unknown, whose copy default matches the action's fallback. Also corrects the periodEndNotice doc: empty variants only suppress via the DEFAULTS (that's how immediate timing hides the notice) — empty override values are dropped like any blank override, so suppression goes through the Confirm component or classNames instead.
|
Both follow-ups addressed (
|
Adds text-override and localization support to
@churnkey/react. Driven by a customer who needs different cancel-button and confirmation copy depending on whether the flow cancels immediately ("Cancel") or at period end ("Turn off auto-renew").Design docs:
What
core/messages.ts: typedCancelFlowMessagescatalog covering every chrome string (~40 keys). Flow content (step titles, offer copy) stays blueprint-authored and is deliberately not in it.i18nprop onCancelFlow/useCancelFlow:{ locale?, messages? }with deep-partial per-locale overrides,{token}interpolation, and an exact → base-language →enfallback chain.confirm.cta,confirm.periodEndNotice, andsuccess.cancelled.*acceptstring | { immediate, atPeriodEnd }, resolved against the server'scancelAtPeriodEnd, which is now threaded intoFlowState.useCancelFlowreturnsmessagesandcancelAtPeriodEnd;selectTimingis exported.Why the period-end notice is token-mode only
The docs described an "access continues until X" notice that the code didn't render — it was removed deliberately because the SDK couldn't know the flow's real timing and could promise access the customer wouldn't get. Token mode now knows the resolved timing, so the notice renders when it's definitively period-end and never in local mode. The pinned test documents this.
Compatibility
No breaking changes. New props are optional, default components fall back to
defaultMessageswhen rendered standalone, and every default string is byte-identical. Precedence: defaults <i18n.messages< per-step props (confirmLabeletc. still win).Pairs with a churnkey-api change that clamps
settings.cancelAtPeriodEndfor legacy providers (Braintree cancels immediately regardless of the flag) so timing-conditional copy can't lie, and an sdk-docs PR adding the Text & localization page.Testing
198 tests (26 new): merge/precedence/timing/interpolation units, machine state threading, and token-mode component tests that stub the config fetch and assert the rendered variant flips on the server boolean. tsc, biome, and the package build are clean.