Logix

Internationalization (i18n)

Recommended patterns for wiring an external i18n instance into Logix (injection, tokens, language switching, async readiness).

Logix recommends treating the “translation engine” as an external capability injected into the Runtime, instead of creating a separate instance inside each module. This ensures:

  • A single i18n instance is shared within one Runtime tree.
  • Logic can read i18n via an explicit root-singleton entry (not affected by local RuntimeProvider.layer overrides).
  • UI can keep using your existing i18n solution (e.g. i18next + react-i18next).

This guide uses the minimal adapter provided by @logixjs/i18n.

1) Inject an external i18n instance

@logixjs/i18n defines a minimal I18nDriver shape and provides I18n.layer(driver) for injection:

import * as Logix from "@logixjs/core"
import { Layer } from "effect"
import { I18n } from "@logixjs/i18n"

export const AppRuntime = Logix.Runtime.make(RootImpl, {
  layer: Layer.mergeAll(
    I18n.layer(i18nDriver),
    AppInfraLayer,
  ),
})

If you use RuntimeProvider.layer in React, you can also put I18n.layer(i18nDriver) at the outermost Provider (the one that provides runtime={...}):

  • Then $.root.resolve(I18nTag) in Logic always resolves the same root-provider singleton.
  • If you only put I18n.layer(...) inside an inner Provider, it becomes a local override; $.root.resolve(...) will ignore it (by design).

2) Get i18n in Logic (root singleton)

Inside module Logic, use $.root.resolve(I18nTag) to explicitly read the i18n service provided by the root provider:

import { Effect } from "effect"
import { I18nTag } from "@logixjs/i18n"

export const MyLogic = MyModule.logic(($) =>
  Effect.gen(function* () {
    const i18n = yield* $.root.resolve(I18nTag)
    // ...
  }),
)

If your goal is replayable/auditable state, a good default is to generate a message token in Logic and write it into module state:

const i18n = yield* $.root.resolve(I18nTag)
const msg = i18n.token("form.required", { field: "name", defaultValue: "Required" })

In UI, translate the token into a final string (e.g. t(msg.key, msg.options)). After language switches, the token itself stays the same, while the rendered text updates with the i18n instance.

Token options should be serializable (null/boolean/number/string, etc.). Don’t put big objects in it, and don’t pass fields like lng/lngs that “freeze language”.

4) Switching language

Switch language via the entry provided by the i18n service:

const i18n = yield* $.root.resolve(I18nTag)
yield* i18n.changeLanguage("zh")

If you want to model language switching as module Actions, consider I18nModule in the next section.

5) Async readiness (ready / no-wait vs wait)

External i18n instances may require async initialization. @logixjs/i18n provides two modes:

  • t(key, options): no waiting; if i18n is not ready, it immediately returns a fallback (defaultValue or key).
  • tReady(key, options, timeoutMs?): wait for readiness (default max 5s); on timeout/failure it returns a fallback.

This lets you choose between “fast availability” and “eventual consistency”. In most cases, prefer storing tokens in state and letting UI naturally render final text once ready. Only use tReady when Logic must produce a final string.

6) I18nModule (optional: expose snapshot as module state)

If you want an explicit i18n module in your Runtime (e.g. let Devtools/pages subscribe to snapshot, or drive language switching via Actions), add I18nModule.impl into root imports:

import { I18nModule } from "@logixjs/i18n"

const RootModule = RootDef.implement({
  initial: { /* ... */ },
  imports: [I18nModule.impl],
})

const RootImpl = RootModule.impl

Each Runtime tree can inject its own I18n.layer(driver) without sharing instances across trees; I18nModule forwards to the same i18n service within that tree.

Runnable examples

  • Index: Runnable examples
  • Code:
    • examples/logix/src/i18n-message-token.ts
    • examples/logix/src/i18n-async-ready.ts
    • examples/logix-react/src/modules/i18n-demo.ts

On this page