Skip to content

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 formEager (non-reactive)Core macro
$tt.eagert
$pluralplural.eagerplural
$selectselect.eagerselect
$selectOrdinalselectOrdinal.eagerselectOrdinal

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.

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.

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>
<!-- 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.

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.

<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.