# Integrations, I18n, and Payments

Iwf includes small integrations that cover common product needs while keeping
provider-specific work inside the app.

## 1. Internationalization

Create a catalog in Idris:

```idris
catalog : I18nCatalog
catalog =
  MkI18nCatalog "en"
    [ translation "en" "nav.home" "Home"
    , translation "en" "article.publish" "Publish article"
    , translation "fr" "nav.home" "Accueil"
    , translation "fr" "article.publish" "Publier l'article"
    ]
```

Or load the canonical catalog format:

```text
# config/i18n.catalog
en.nav.home = Home
fr.nav.home = Accueil
en.cart.items.one = %{count} item
en.cart.items.other = %{count} items
```

```idris
loaded : IO (Either String I18nCatalog)
loaded =
  loadI18nCatalog "en" "config/i18n.catalog"
```

Each non-comment line is `locale.key = message`. The locale is the segment
before the first dot, and the rest is the translation key.

Choose a locale from the request:

```idris
locale : String
locale =
  selectLocale catalog request
```

Render copy:

```idris
<a href="/">{translate catalog locale "nav.home"}</a>
```

Lookup falls back from regional locales such as `fr-CA` to `fr`, then to the
catalog default locale, then to the key.

Interpolated messages use `%{name}` placeholders:

```idris
translateWith catalog locale "greeting" [("name", "Alice")]
```

Plural messages use `.one` for count `1` and `.other` for all other counts.
`translatePlural` automatically injects `%{count}`:

```idris
translatePlural catalog locale "cart.items" 3 []
```

Missing-key diagnostics are explicit:

```idris
case translateDiagnostic catalog locale "nav.admin" of
  Right message => message
  Left missing => missing.key
```

Locale persistence is session-based. Query parameter `locale` wins, then the
persisted session locale, then `Accept-Language`, then the catalog default:

```idris
session1 : Session
session1 =
  persistRequestedLocale catalog request session0

locale : String
locale =
  selectLocaleWithPersistence catalog session1 request
```

Useful helpers:

- `translation`
- `parseI18nCatalog`
- `loadI18nCatalog`
- `preferredLocales`
- `availableLocales`
- `translateIn`
- `translate`
- `translateWith`
- `translatePlural`
- `translateDiagnostic`
- `missingTranslations`
- `selectLocale`
- `selectLocaleWithPersistence`
- `persistRequestedLocale`
- `lookupPersistedLocale`
- `putPersistedLocale`
- `deletePersistedLocale`

## 2. Stripe Checkout

Iwf builds Stripe Checkout Session requests. It pins the API version to:

```text
2026-02-25.clover
```

Build a request:

```idris
stripeConfig : StripeConfig
stripeConfig =
  MkStripeConfig stripeSecret webhookSecret

checkout : StripeCheckoutSession
checkout =
  MkStripeCheckoutSession
    CheckoutPayment
    "https://example.test/billing/success"
    "https://example.test/billing/cancel"
    [MkStripeLineItem "price_123" 1]
    (Just "user-42")
    [("plan", "pro")]

request : StripeApiRequest
request =
  checkoutSessionRequest stripeConfig checkout
    |> withStripeIdempotencyKey "checkout-user-42"
```

The app sends `StripeApiRequest` through a `StripeTransport`:

```idris
result : IO (Either String StripeApiResponse)
result =
  sendStripeRequest transport request
```

`StripeTransport` is the HTTP boundary. It lets the app use its HTTP client,
timeouts, retries, observability, and secret-management policy while Iwf owns
request construction. Use `checkoutSessionResult` to normalize the response into
either a hosted Checkout URL or a Stripe error message.

## 3. Stripe Webhooks

Verify webhooks with an app-provided HMAC-SHA256 function:

```idris
handleWebhook : Request -> Response
handleWebhook request =
  case verifyStripeWebhookAt now 300 stripeConfig hmacSha256 request of
    Left response => response
    Right payload => okText "received"
```

Iwf parses Stripe's `Stripe-Signature` header, builds the signed
`timestamp.body` payload, matches any `v1` digest, and can enforce a timestamp
tolerance through `verifyStripeWebhookAt`. The app supplies the HMAC-SHA256
primitive as `StripeWebhookHmacSha256` so it can choose its crypto package and
secret-management policy.

After verification, decode the event envelope:

```idris
case decodeStripeEvent payload of
  Right event =>
    case event.eventType of
      StripeCheckoutSessionCompleted => okText "checkout complete"
      _ => okText "ignored"
  Left error => badRequest error
```

Known event constructors cover Checkout completion/expiry, subscription updates,
and invoice payment success. Unknown event names are preserved as
`StripeUnknownEvent`.

## 4. OAuth and Passkeys

OAuth and passkey helpers live with auth because they affect identity:

- `OAuthProviderConfig`
- `OAuthPkceChallenge`
- `OAuthCallback`
- `OAuthTokenRequest`
- `oauthAuthorizationUrl`
- `issueOAuthState`
- `completeOAuthCallback`
- `verifyOAuthState`
- `completeOAuthLogin`
- `issuePasskeyChallenge`
- `completePasskeyRegistration`
- `verifyPasskeyAssertion`

The app owns provider credentials, callback routes, persistence, and
cryptographic verifier implementations.

## Next

Read [HTMX, Auto Refresh, and Modals](htmx-auto-refresh-and-modals.md).
