Skip to main content
@craft-ts
LearnLearn with EffectAI agentsGuideReference
@craft-ts/core@craft-ts/component@craft-ts/dev-tools@craft-ts/effect@craft-ts/style@craft-ts/style-testing@craft-ts/i18n@craft-ts/i18n-effect@craft-ts/mcp
ExamplesMigrationEffect compatibility & maturityAdopting CraftTS progressivelyPress kitRoadmapBacklog
Beta
  • Learn
  • Learn with Effect
  • AI agents
  • Guide
  • Reference
  • Packages
  • Resources
  • Guide overview
  • Create a project
i.Core concepts
  • The mental model
  • Which primitive should I use?
  • Anatomy of a primitive
  • Generators and yield*
  • Insertions
  • Typed insertion pipes
  • Exceptions as values
ii.Managing state
  • Local state
  • State machines
  • query
  • Mutations
  • queryParams
  • asyncProcess
  • Selecting
  • Reacting to mutations
  • Collections
  • Collection utilities
  • Persistence
  • GlobalPersisterHandler
  • Pagination placeholders
  • Schema validation
iii.Recommended approaches
  • Inject at the point of use
iv.Structuring the app
  • craftService
  • Service scopes
  • Shaping the public API
  • Abstract services
  • App start
  • Lazy services
  • Server functions
  • craftRegisterFor
  • Target wrapper
v.Routing & type-safe DI
  • Setup
  • CLI automation
  • ESLint rules
  • Route providers
  • Route guards
  • Route exception handling
  • Non-blocking navigation
  • Global error component
  • Route load errors
  • Scaling routes
vi.Components & templates
  • Components
  • Activating the style system
  • Typed styles
  • Defining a design system
  • Tokens and variables
  • Global foundation and fonts
  • Pseudo-elements and animations
  • Axes and the matrix
  • Context obligations
  • Testing visual states
  • Text contrast
  • Visual attestation
  • Template obligations
  • Typed CSS variables
  • Fine-grained reactivity
  • Progressive rendering
  • Directives and .pipe(...)
  • settledValue & pendingNode
  • Accessibility
  • Customization
  • Content projection
  • Styling a component
  • Template migrator
vii.Forms
  • Overview
  • Validators
  • Submitting
  • Nested forms
  • Exception handling
  • Complete examples
viii.Testing
  • Testing services
  • Testing components
  • Type-level tests
  • Browser boundaries
  • Page stability
  • Architecture rules
    • Overview
    • Declarative baseline
    • Unique identities
    • HTTP endpoint ownership
    • Computed purity
    • Dependency cycles
    • Mutation reactions
    • Route DI proofs
    • Route component files
    • Path boundaries
    • Exclusive links
    • Persisted identities
    • insertSelect keys
    • craftEffect and network
    • craftEffect and imperative sync
    • Interactive element names
    • Metric thresholds
    • Server-state loaders
    • Primitive loader requirements
  • Extensible architecture graph
  • Graph insights
  • Craft graph vs Nx
ix.Reactivity utilities
  • craftComputed
  • craftEffect
  • craftMethod
  • source$
  • on$
  • fromEventToSource$
  • sourceFromEvent
  • afterRecomputation
x.Internationalisation
  • Type-safe i18n
  • The catalogue
  • Tokens
  • The runtime
  • With Effect
xi.Deployment (experimental)
  • Overview
  • Manifest reference
  • Diagnostics
  • Providers
  • Alchemy provider
xii.Going further
  • SSR and hydration
  • Effect integration
  • Program operators
  • Pattern matching
  • Observability
  • AI agents
  • Temporal runtime
  1. Internationalisation
  2. With Effect

i18n with Effect#

@craft-ts/i18n-effect is an adapter, and only an adapter. It exposes three things — a service tag, a Layer, and one function — over a runtime you built the ordinary way. @craft-ts/i18n itself never imports Effect, and plain component code should keep calling t directly.

The Layer#

ts
import { Effect } from 'effect';
import { provideI18nRuntime, translateEffect } from '@craft-ts/i18n-effect';

const runtime = createI18nRuntime({ locales, defaultLocale: 'en-US' });

// One Layer, built from the runtime the rest of the app already uses.
export const i18nLayer = provideI18nRuntime(runtime);

provideI18nRuntime(runtime) returns Layer.Layer<I18nEffectService>. It wraps the runtime you already have, so there is exactly one active locale in the process — the Effect side does not get its own.

Bind the locales once#

translateEffect has no value parameter carrying the locales, so TypeScript has nothing to infer them from. Called bare, its key parameter resolves to never and even a valid key is rejected. Bind them once, in the same file as the Layer:

ts
import type { TranslationKey, TranslationParams } from '@craft-ts/i18n';

type AppLocales = typeof locales;

/**
 * `translateEffect` has no value parameter carrying the locales, so TypeScript
 * cannot infer them: called bare, its key parameter resolves to `never` and
 * even a valid key is rejected. Bind them once, here, and every call site gets
 * the closed key union back.
 */
export const t = <Key extends TranslationKey<AppLocales[number]>>(
  key: Key,
  ...params: keyof TranslationParams<
    AppLocales[number],
    Key & string
  > extends never
    ? [params?: TranslationParams<AppLocales[number], Key & string>]
    : [params: TranslationParams<AppLocales[number], Key & string>]
) => translateEffect<AppLocales, Key>(key, ...params);

From there, t has the closed key union and the typed params back. Passing the type arguments at every call site — translateEffect<typeof locales, 'order.total'>(…) — works too, and is what this wrapper spares you.

translateEffect#

ts
// Same keys, same params, same string as runtime.t — but as an Effect that
// declares I18nEffectService in its requirements.
const summary = Effect.gen(function* () {
  const total = yield* t('order.total', { amount: 1234.5 });
  const items = yield* t('order.items', { count: 2 });
  return `${total} ${items}`;
});

The signature is translateEffect(key, params) => Effect.Effect<string, never, I18nEffectService>. Same closed key union, same typed params, same string as runtime.t — the snippet above is checked against runtime.t in the docs test suite rather than trusted.

The error channel is never on purpose: a translation that reaches this point cannot fail on a bad key or a bad parameter, because neither compiles. What can fail is the locale not being loaded, and that is a defect in the app's startup, which is why it throws I18nRuntimeError rather than becoming a typed failure every call site would have to handle.

When to reach for it#

Use translateEffect inside an Effect program — a domain service building a message, a server handler rendering an email. In a component, the bound translator is the shorter path, and reaching for Effect just to format a string adds a requirement to the program for nothing.

An Effect program is not a Craft injection context, so translateEffect accepts StaticTranslationKey — the keys whose formatting resolves no service. A message whose token yields a Craft service is rendered by the component-side translator; that is what keeps the never error channel above honest.

See also the Effect adapters page for the rest of the @craft-ts/*-effect family.

←The runtimeOverview→

On this page

  • The Layer
  • Bind the locales once
  • translateEffect
  • When to reach for it

@craft-ts · MIT licensed

GitHubnpm

Search