Merge org-level translations into message resolution#35
Conversation
The token-mode config now carries an optional translations field — dashboard-configured overrides keyed by language. initializeFromConfig re-resolves the catalog with that layer between the built-in defaults and the developer's i18n.messages, so org copy applies automatically while code-level overrides still win. Both per-language layers run through the same locale fallback chain.
# Conflicts: # packages/react/src/core/machine.ts # packages/react/tests/core/machine.test.ts
Review finding (critical): mergeValue dispatched on the patch's shape,
so malformed org overrides could corrupt the catalog's structure — a
bare string landing on a category node replaced the whole node, and an
{ immediate, atPeriodEnd } object landing on a plain leaf reached the
renderer as an object child and threw. The catalog is the schema now:
patches shaped wrong for their slot are dropped, pair patches are only
accepted on the four timing-aware paths, variant values must be
non-empty strings, and keys the catalog doesn't declare are ignored.
|
Review follow-up: the merge now dispatches on the catalog's shape instead of the patch's. Previously a bare string landing on a category node replaced the node, and an |
Accepting a pause and accepting a discount previously rendered the
identical success screen; the embed has always confirmed each outcome
specifically (discountApplied, pauseApplied, …). The saved catalog now
carries per-offer entries with the embed's copy as defaults — discount,
pause, trial_extension, plan_change, rebate — resolving as
savedTitle prop, then the per-offer entry, then the generic saved copy.
Contact, redirect, and custom types keep the generic fallback.
Pause gets a resumeDescription with a {resumeDate} token, used when the
accept result carries the selected duration (the built-in pause UI
always passes it). The accepted offer is snapshotted into FlowState at
the success transition because currentOffer derives from currentStepId,
which a declared success step moves off the offer; Success components
also receive it as a new optional acceptedOffer prop.
|
Added per-offer success copy per Rob's request: |
Phase 2 of SDK i18n. #34 has merged; this now targets main directly.
Design docs:
What
The token-mode config payload gains an optional
translationsfield — dashboard-configured message overrides keyed by language (stored on the neworg.sdkTranslations, delivered by churnkey-api#948).initializeFromConfigre-resolves the catalog with that layer in the middle:built-in defaults < org translations < developer
i18n.messages< per-step propsBoth per-language layers run through the same locale fallback chain (exact tag → base language → en). Local mode is unaffected — no org fetch, no layer, per the design decision.
Testing
New units for the org layer (below developer messages, locale chain, timing-pair values) and machine tests for
config.translationsapplication. 203 tests passing, tsc + biome clean.