Iwf Guide
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:
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:
# config/i18n.catalog
en.nav.home = Home
fr.nav.home = Accueil
en.cart.items.one = %{count} item
en.cart.items.other = %{count} items
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:
locale : String
locale =
selectLocale catalog request
Render copy:
<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:
translateWith catalog locale "greeting" [("name", "Alice")]
Plural messages use .one for count 1 and .other for all other counts. translatePlural automatically injects %{count}:
translatePlural catalog locale "cart.items" 3 []
Missing-key diagnostics are explicit:
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:
session1 : Session
session1 =
persistRequestedLocale catalog request session0
locale : String
locale =
selectLocaleWithPersistence catalog session1 request
Useful helpers:
translationparseI18nCatalogloadI18nCatalogpreferredLocalesavailableLocalestranslateIntranslatetranslateWithtranslatePluraltranslateDiagnosticmissingTranslationsselectLocaleselectLocaleWithPersistencepersistRequestedLocalelookupPersistedLocaleputPersistedLocaledeletePersistedLocale
2. Stripe Checkout
Iwf builds Stripe Checkout Session requests. It pins the API version to:
2026-02-25.clover
Build a request:
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:
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:
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:
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:
OAuthProviderConfigOAuthPkceChallengeOAuthCallbackOAuthTokenRequestoauthAuthorizationUrlissueOAuthStatecompleteOAuthCallbackverifyOAuthStatecompleteOAuthLoginissuePasskeyChallengecompletePasskeyRegistrationverifyPasskeyAssertion
The app owns provider credentials, callback routes, persistence, and cryptographic verifier implementations.