Svelte: Reactive Macros
lingui-for-svelte provides reactive forms of the Lingui core macros.
TL;DR: Use $t and friends for reactive use in markup and $derived. Use *.eager for an explicit non-reactive snapshot. Use bare forms only when nesting inside $t or msg.
| Reactive form | Eager (non-reactive) | Core macro |
|---|---|---|
$t | t.eager | t |
$plural | plural.eager | plural |
$select | select.eager | select |
$selectOrdinal | selectOrdinal.eager | selectOrdinal |
Standalone vs. embedded
Section titled “Standalone vs. embedded”The $ prefix is for the standalone reactive form.
Standalone: the macro is the full expression. Use the $ prefix.
<p>{$t`Hello ${name}`}</p><p>{$plural(count, { one: "# item", other: "# items" })}</p><p>{$select(tone, { formal: "Welcome", casual: "Hi", other: "Hello" })}</p>Embedded inside $t`...`: the outer call already provides the reactive subscription. Use
the bare form for the inner macro.
<p>{$t`Cart: ${plural(count, { one: "# item", other: "# items" })}`}</p><p> {$t`Greeting: ${select(tone, { formal: "Welcome", casual: "Hi", other: "Hello" })}`}</p>Using $plural inside $t`...` is not needed. The outer $t already re-evaluates the whole
expression.
Reactive values in script
Section titled “Reactive values in script”There is no implicit $derived wrapping for script variables. If you want a reactive translated
value in script, write the rune yourself.
<script lang="ts"> import { t } from "lingui-for-svelte/macro";
let name = $state("Lingui");
const greeting = $derived($t`Hello ${name}`);</script>That keeps the rule simple: Lingui provides the reactive macro form, and you choose where to use a Svelte rune around it.
Eager (non-reactive) form
Section titled “Eager (non-reactive) form”t.eager, plural.eager, select.eager, and selectOrdinal.eager are escape hatches. They
produce a translated string immediately using the current locale but do not subscribe to locale
changes.
If you need a message in a non-reactive context, the preferred approach is to define a descriptor
with msg and translate it with $t(descriptor) at render time:
<script lang="ts"> import { msg, t } from "lingui-for-svelte/macro";
// Define a descriptor - no translation happens here. const ariaLabel = msg`Submit`;</script>
<!-- Translate reactively at render time. --><button aria-label={$t(ariaLabel)}>{$t`Submit`}</button>Use *.eager only when you genuinely need a translated string at initialization time and the
descriptor pattern does not fit. For example, passing a translated label to a third-party API
that runs once during component setup and cannot re-render later.
<script lang="ts"> import { t } from "lingui-for-svelte/macro";
// One-time call to an external API that cannot re-render. externalLib.setTitle(t.eager`My App`);</script>Bare t (without .eager or $) is a compile-time error in .svelte files. The compiler
rejects it because it silently drops locale reactivity.
Bare plural, select, and selectOrdinal are also rejected when used as standalone string
producers. They remain valid as embedded descriptors inside $t or msg.
<!-- Allowed: bare plural as a descriptor embedded inside $t --><p>{$t`Cart: ${plural(count, { one: "# item", other: "# items" })}`}</p>
<!-- Not allowed: bare plural as a standalone string producer --><!-- ✗ <p>{plural(count, { one: "# item", other: "# items" })}</p> --><!-- Use $plural (reactive) or plural.eager (non-reactive) instead. -->Embedding inside msg works the same way. Use msg when you want to define the descriptor once
and translate it reactively at multiple call sites:
<script lang="ts"> import { msg, plural, t } from "lingui-for-svelte/macro";
let count = $state(0);
// Descriptor defined once with plural embedded. No translation happens here. const cartLabel = msg`Cart: ${plural(count, { one: "# item", other: "# items" })}`;</script>
<!-- Translated reactively wherever needed. --><p>{$t(cartLabel)}</p><title>{$t(cartLabel)}</title>How they work
Section titled “How they work”<!-- What you write --><p>{$t`Hello ${name}`}</p>
<!-- What the compiler produces --><p> {$__l4s_translate({ id: "...", message: "Hello {name}", values: { name } })}</p>__l4s_translate is a Svelte store (Readable<Translate>). It subscribes to Lingui’s internal
"change" event. When i18n.activate(locale) fires that event, every $t expression in the tree
re-evaluates and Svelte re-renders the affected parts.
The $ in $__l4s_translate is standard Svelte store auto-subscribe syntax. It uses the same
mechanism as $myStore in any Svelte component.
The $ prefix is not a Svelte 5 rune
Section titled “The $ prefix is not a Svelte 5 rune”Despite the $ prefix, these are not Svelte 5 runes ($state, $derived, etc.). The
implementation uses classic Svelte stores (readable, derived) and works in both rune-mode and
non-rune-mode components without any configuration.
The $ in user-written $t is a naming convention that tells the lingui-for-svelte macro
transform to emit a reactive store subscription. After compilation it becomes Svelte’s own
$storeName auto-subscribe syntax.
Locale switch example
Section titled “Locale switch example”<script lang="ts"> import { setupI18n } from "@lingui/core"; import { setLinguiContext } from "lingui-for-svelte"; import { t } from "lingui-for-svelte/macro";
import { messages as enMessages } from "$lib/i18n/locales/en.js"; import { messages as jaMessages } from "$lib/i18n/locales/ja.js";
let locale = $state<"en" | "ja">("en");
const i18n = setupI18n({ locale: "en", messages: { en: enMessages, ja: jaMessages, }, }); setLinguiContext(i18n);
function toggle() { const next = locale === "en" ? "ja" : "en"; i18n.activate(next); locale = next; }</script>
<button onclick={toggle}>{$t`Switch language`}</button>
<p>{$t`Hello`}</p>After i18n.activate returns, all $t, $plural, $select, and $selectOrdinal
expressions in the tree re-render synchronously.
Next, read i18n Context for how the active Lingui instance is installed in the component tree. Then use the macro reference when you want the exact authoring rules for each macro.