Skip to content

feat: runtime core macro#2618

Draft
timofei-iatsenko wants to merge 6 commits into
mainfrom
feat/runtime-core-macro
Draft

feat: runtime core macro#2618
timofei-iatsenko wants to merge 6 commits into
mainfrom
feat/runtime-core-macro

Conversation

@timofei-iatsenko

@timofei-iatsenko timofei-iatsenko commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

PoC: Runtime JS Macro

Motivation

Currently, Lingui requires a compilation step (babel/swc plugin) to transform macros like t, plural, select, and selectOrdinal into runtime code. This creates a barrier for use cases where a build pipeline is either unavailable or undesirable — most notably on the backend (Node.js scripts, serverless functions, CLI tools) where bundle size is not a concern but DX simplicity is.

This PR introduces a runtime macro — a set of functions with the same signature/contract as the compile-time macro, but executed entirely at runtime without any compiler plugin. This allows using Lingui's familiar API without any compilation step.

Trade-offs

  • Bundle size penalty: The compile-time macro strips the original message field in production, leaving only the id — this significantly reduces the i18n overhead in the bundle. The runtime macro cannot do this.
  • Labeled placeholders only: Since variable names cannot be inferred at runtime, passing raw values like ${variable} is not supported and will throw with a helpful error message. Users must always use the labeled placeholder syntax: ${{name: value}}. This is intentionally strict — it ensures the runtime macro produces the same ICU messages regardless of variable naming, and keeps code portable between runtime and compile-time macros. We should add a configuration flag to the compile-time macro to enforce the same restriction, so that code written for one works identically with the other.
  • Option values cannot be template literals: Template literals inside plural/select options (e.g., `She is ${gender}`) are already evaluated by the time they reach the function. However, the full functionality is preserved — instead of a plain template literal, users wrap it with t to produce a macro marker: t`She is ${{gender}}` instead of `She is ${gender}`. Nested macro markers (plural, select, selectOrdinal) work as option values as well.

Compatibility with existing infrastructure

The runtime macro is a subset of the compile-time macro API. It produces the same MessageDescriptor shape ({id, message, values}) with the same ID generation algorithm. This means:

  • The existing message extractor should work out of the box with runtime macro usage
  • Catalogs produced from runtime macro code are identical in format
  • The unsupported syntax (like labeled placeholders inferred from variable names) was already being phased out in prior iterations

What's left

This is PoC, not fully working feature. I wanted to check what features could be implemented without a compile time macro and what's not. The current implementation is doing just macro expansion but not actually connected to i18n.

This is still should be in done. Also JSX macro is not supported (i have another branch with PoC for that as well)

Key questions:

  • How this should be published? Included into @lingui/core or distributed as a standalone package?
  • How it should be connected to i18n? Do everethying as is for compile time macro? (t bound to global i18n, or learn from experience and re-design this, to something like i18n.t`Message`

How to try this in your app

Copy the file to your repository. Say you have setup an alias for this file @mycomp/native-core-macro, then add configuration to lingui config:

import { defineConfig } from '@lingui/cli'

export default defineConfig({
  /* ... */
  macro: {
    corePackage: ['@mycomp/native-core-macro'],
  },
})

Then use in your code:

// assume you have an i18n instance

import { msg } from '@mycomp/native-core-macro'
i18n.t(msg`Hello world!`)

Usage example

import { msg, plural, select } from "@lingui/core/runtime-macro"

// Simple
msg`Hello World`
// → { id: "...", message: "Hello World" }

// Positional placeholders
msg`Hello ${name}`
// → { id: "...", message: "Hello {0}", values: { "0": name } }

// Named placeholders
msg`Hello ${{name}}`
// → { id: "...", message: "Hello {name}", values: { name: "Alice" } }

// Nested plural
msg`You have ${plural({count}, { one: "# message", other: "# messages" })}`
// → { id: "...", message: "You have {count, plural, one {# message} other {# messages}}", values: { count: 5 } }

// Call expression form
msg({ id: "welcome", message: "Welcome back", context: "homepage" })

For myself

  • Maybe invent a new, more ergonomical way to set context and other properties?

@vercel

vercel Bot commented Jul 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
js-lingui Ready Ready Preview Jul 14, 2026 3:03pm

Request Review

@github-actions

Copy link
Copy Markdown

size-limit report 📦

Path Size
packages/core/dist/index.mjs 1.69 KB (0%)
packages/detect-locale/dist/index.mjs 633 B (0%)
packages/react/dist/index.mjs 1.89 KB (0%)

@timofei-iatsenko timofei-iatsenko changed the title Feat/runtime core macro feat: runtime core macro Jul 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant