# Content, choices, and persisted answers

Content labels may change; analytics-safe option IDs and answer keys must remain stable.

<!-- funnelsgrove:generated:start contract-v3/contract/content-answers -->
Step contract version: `3`

Contract hash: `d761e91d5ac6ff9e72c6d49c5bcd014270912473a66f99c49d726c65998085cf`

| Type | Cardinality | Presentation | Completion | allowEmpty config |
| --- | --- | --- | --- | --- |
| `multi_select_choice` | `many` | `standard` | `explicit` | `true` |
| `single_step_choice` | `one` | `standard` | `selection` | `false` |
| `single_step_choice_emoji` | `one` | `emoji` | `selection` | `false` |

| Capability | Allowed types | Allowed identities | Required owner |
| --- | --- | --- | --- |
| `choice-write` | `single_step_choice`, `single_step_choice_emoji`, `multi_select_choice` | `any` | `useStepChoices` |
| `email-capture` | `form_input` | `email-capture` | `submitEmailCapture` |
<!-- funnelsgrove:generated:end contract-v3/contract/content-answers -->

## Choice rules

- Declare `choice.answerKey` in manifest and component metadata.
- Use stable option IDs, never labels, array positions, or entire objects.
- Persist a scalar ID for single-choice types and an ID array for multi-select.
- Represent an allowed empty multi-select as `[]`.
- Use `useStepChoices` for write, completion, and routing order.

## Form and email rules

Ordinary form inputs validate and persist through their owning runtime path. The reserved email form calls `submitEmailCapture`; it never sends analytics manually and never decides the next route.

## Content/editor parity

Keep builder-editable values in content definitions, expose matching editor sections, and register them once. Do not move stable IDs or answer keys into editable copy.

## Product strategy and plan ownership

- `PRODUCT_SENSE.md` owns audience, promise, positioning, objections, value stack, and brand direction. Preserve `## Strategic Pre-Work` and its exact five columns.
- `PLAN.md` owns the ordered screen contract, exact screen structure and copy, shared design system/components, and `## Funnel Fuel`. Keep one row per planned screen.
- Update the owning artifact in the same change. Never implement a step absent from `PLAN.md`.

See the exact choice and form pages in the [step-type index](../steps/INDEX.md). Run `fgrove validate` and test reload persistence before completion.

## Localization

All funnel-authored visitor copy belongs in `src/**/*.content.ts` localized
content definitions: headings, options, CTA labels, placeholders, validation and
error messages, loading/empty states, accessible labels, shared shell copy, and
paywall/legal copy. JSX renders resolved content; metadata IDs, answer keys,
provider IDs, prices and billing rules retain their existing owners. Provider-hosted
payment fields and messages are configured through that provider's locale support.

1. Keep original `*.content.ts` files at their existing locations alongside the
   funnel's steps/shared code. They own the default English content and exported
   content types. Run `fgrove locales add ru --dir .` to create translations under
   `src/localization/ru/`, mirroring each original path relative to `src/`:
   `src/steps/content/intro.content.ts` →
   `src/localization/ru/steps/content/intro.content.ts`.
   Use BCP 47 tags such as `pt-BR`. CLI copies source only: no translation, login,
   sync or publication. Repeating the command preserves translated values, adds missing exports and
   repairs missing registrations in the originals.
   An existing inline locale is extracted with its authored text preserved.
2. Translate **every** generated content file, including shared shell and
   returning-subscriber content. Each translated export imports its named type
   with `import type` from the original file. Original definitions import these
   translated values into their `locales` map; the reverse import is type-only.
   Preserve keys, option IDs, placeholders such as `{amount}`, URLs and tokens.
   Same-file inheritance from the default or requested locale refers to the
   translated export. References to other locales require explicit copy before
   running the command. Relative imports
   are rebased; imported expressions remain authored references. Inspect those
   references for remaining visitor copy before completing a translation.
3. Define the complete structure in an exported type in the original content file
   and use `satisfies LocalizedStepContent<YourContentType>`. When adding a field,
   update this type and the English content, then supply the field in **every**
   translation. Keep required fields required; do not silence errors with casts,
   optional fields or English spreads. Run `npx tsc --noEmit` at the funnel root:
   an incomplete translation must fail with its file and missing field.
   Keep `defaultLocale` and `locales` as explicit literal properties. CLI preflights
   unsupported definitions before writing. Use named non-generic content types
   and one default language per file. Local helpers referenced by copied
   content (including enums, classes and type casts) must be inlined or moved to
   independent imported modules. If a registered translation file is lost, restore
   it before retrying; CLI cannot recover translated text from an import. A new step
   must include every supported locale: rerun `locales add` for each language.
4. Resolve step copy with `usePreviewStepLocalizedContent(stepId, definition,
   getStepContentLocale(attributes))`. Runtime versions supporting localization
   give `?locale=ru` priority; otherwise the requested/browser language applies.
   Lookup uses exact match, base language (`pt-BR` → `pt`), then `defaultLocale`.
   The override persists in step URLs and never changes country, currency or offers.
5. Review each language with `?locale=ru`, including direct step links, reload,
   next/back, validation errors, long text and checkout. Builder **Preview language**
   selects languages announced by its live frame and carries the choice into the
   external preview link. Static screenshots cannot switch language. Older
   published runtimes need an SDK update and republish before exposing this control.
6. Run `fgrove validate`, sync through the funnel's normal CLI/GitHub workflow,
   then publish preview. Existing funnels keep their own source and package pins;
   adding a locale does not upgrade their SDK or migrate hard-coded JSX. Update
   those explicitly and review the diff. Refresh this managed guide with `fgrove docs`.

New funnel code must route shell buttons and custom action-bar text through
content too. `src/steps/content/runtime.content.ts` owns the starter's shared
Continue/loading/unavailable messages. Server-returned display labels need
locale-owned presentation overrides; changing copy must not change plan IDs,
amounts, billing intervals or provider configuration.

The starter paywall's `catalogLabels` maps exact catalog display text to localized
text. Keep its keys unchanged and translate the values, preserving monetary
amounts. Add mappings for new admin-authored labels as needed. Unrecognized
catalog labels remain unchanged; translation never substitutes one offer for
another or alters payment identifiers and amounts.
