Skip to content

feat(react): add runWithI18n for i18n in Server Functions - #2619

Open
yslpn wants to merge 5 commits into
lingui:mainfrom
yslpn:feat/run-with-i18n
Open

feat(react): add runWithI18n for i18n in Server Functions#2619
yslpn wants to merge 5 commits into
lingui:mainfrom
yslpn:feat/run-with-i18n

Conversation

@yslpn

@yslpn yslpn commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Description

This adds ambient, request-scoped i18n access for downstream helpers called by
React Server Functions. setI18n keeps its role — storing the instance in
React.cache during a Server Component render — and now warns in development if
called outside one, where the write would otherwise be silently lost. Server
Functions use the new runWithI18n API instead.

Code that already has a local i18n instance should continue to call
i18n.t(...) directly. This feature is for call chains where the helper doing
the translation does not receive that instance.

Before this change, every function in the chain has to pass i18n along, even
when it does not use it:

export async function getGreeting(locale: string) {
  const i18n = setupI18n({
    locale,
    messages: { [locale]: await loadMessages(locale) },
  })

  return buildGreeting(i18n)
}

function buildGreeting(i18n: I18n) {
  return translateGreeting(i18n)
}

function translateGreeting(i18n: I18n) {
  return i18n.t(msg`Hello, world!`)
}

After this change, a Server Function can scope the instance to a callback and
helpers can read it from the callback's async context. setupI18n creates the
instance; runWithI18n makes it available to the execution chain created inside
the callback:

export async function getGreeting(locale: string) {
  const i18n = setupI18n({
    locale,
    messages: { [locale]: await loadMessages(locale) },
  })

  return runWithI18n(i18n, () => buildGreeting())
}

function buildGreeting() {
  return translateGreeting()
}

function translateGreeting() {
  return getI18nOrThrow().i18n.t(msg`Hello, world!`)
}

The existing React.cache path remains authoritative inside an RSC render.
runWithI18n uses AsyncLocalStorage.run outside RSC, which keeps concurrent
Server Functions isolated across await calls and restores the caller's context
after the callback. getI18nOrThrow provides a non-nullable accessor for helpers,
and I18nContext is now exported from @lingui/react/server.

The async-context path uses AsyncLocalStorage from node:async_hooks, so it
works on Node.js, the Vercel Edge Runtime, and Cloudflare Workers (with the
nodejs_compat / nodejs_als compatibility flag). It relies only on run and
getStore, which all three runtimes support.

The docs now include a Server Action using this API. The tests cover both
storage paths, concurrent locales, nested callbacks, and cleanup. .swc/ is
also ignored.

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Examples update

Checklist

  • I have read the CONTRIBUTING and CODE_OF_CONDUCT docs
  • I have added tests that prove my fix is effective or that my feature works
  • I have added the necessary documentation (if appropriate)

@vercel

vercel Bot commented Jul 16, 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 20, 2026 10:30am

Request Review

@timofei-iatsenko

Copy link
Copy Markdown
Collaborator

I'm not using Nextjs myself anymore, but i saw other library took an approach with a middlewares + AsyncLocalStorage, have you considered this as well? It might give a better DX overall, instead of choosing what approach to use between Server Functions and RSC you just set i18n to AsyncLocalStorage once in the middleware and it makes it availalble for the whole request run.

@yslpn

yslpn commented Jul 18, 2026

Copy link
Copy Markdown
Contributor Author

I'm not using Nextjs myself anymore, but i saw other library took an approach with a middlewares + AsyncLocalStorage, have you considered this as well? It might give a better DX overall, instead of choosing what approach to use between Server Functions and RSC you just set i18n to AsyncLocalStorage once in the middleware and it makes it availalble for the whole request run.

I'm just starting to work with Next's server functions.

Next.js middleware runs as a separate invocation before route matching -it can only touch headers/cookies/redirects. It doesn't wrap the RSC render or a Server Function call in the same call stack. So, AsyncLocalStorage.run() wouldn't be visible during rendering or in a Server Action; that's why this PR has two primitives (setI18n via React.cache for RSC, runWithI18n via AsyncLocalStorage for Server Functions) instead of one.

It's interesting that your solution could work in Tanstack Start, because there the middleware and the request handler itself are one continuous call chain, not two different execution boundaries. But I think this can be added to the documentation later, outside of this PR.

const i18nMiddleware = createMiddleware().server(({ next }) => {
    return runWithI18n(setupI18n(...), () => next())
})

docs:
https://tanstack.com/start/v0/docs/framework/react/guide/middleware

@codecov

codecov Bot commented Jul 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.34%. Comparing base (6bb8983) to head (c5853d1).
⚠️ Report is 372 commits behind head on main.

Additional details and impacted files
@@             Coverage Diff             @@
##             main    #2619       +/-   ##
===========================================
+ Coverage   77.05%   90.34%   +13.29%     
===========================================
  Files          84      126       +42     
  Lines        2157     3843     +1686     
  Branches      555     1138      +583     
===========================================
+ Hits         1662     3472     +1810     
+ Misses        382      337       -45     
+ Partials      113       34       -79     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@timofei-iatsenko

Copy link
Copy Markdown
Collaborator

Next.js middleware runs as a separate invocation before route matching -it can only touch headers/cookies/redirects. It doesn't wrap the RSC render or a Server Function call in the same call stack.

Ah, poor NextJs developers 😔

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a request-scoped, ambient Lingui i18n context for React 19+ Server Functions (e.g. Next.js Server Actions) while keeping the existing React Server Components (RSC) React.cache mechanism authoritative during RSC renders.

Changes:

  • Introduces runWithI18n (AsyncLocalStorage-backed) and getI18nOrThrow, and updates server/RSC entrypoints to use the non-nullable accessor.
  • Adds coverage for Server Function scoping, nested/concurrent isolation, and RSC-cache behavior; adds a dedicated TransRsc test.
  • Updates docs to explain when to use setI18n vs runWithI18n, plus bumps the React dev/test toolchain to React 19 and ignores .swc/.

Reviewed changes

Copilot reviewed 9 out of 11 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
yarn.lock Updates React/ReactDOM and related type dependencies for React 19 testing.
website/docs/tutorials/react-rsc.md Documents Server Functions usage (runWithI18n/getI18nOrThrow) and updates Next.js config/layout examples.
website/docs/ref/react.md Adds/extends server API reference for I18nContext, setI18n, runWithI18n, getI18n, getI18nOrThrow.
packages/react/src/TransRsc.tsx Switches RSC Trans implementation to use getI18nOrThrow.
packages/react/src/TransRsc.test.tsx Adds tests verifying TransRsc works with runWithI18n and throws actionable errors without context.
packages/react/src/server.ts Adds AsyncLocalStorage-backed scoping (runWithI18n) and a non-nullable accessor (getI18nOrThrow); makes setI18n warn when misused.
packages/react/src/server.test.ts Adds tests for both storage paths, nested scopes, concurrency isolation, and setI18n warning behavior.
packages/react/src/index-rsc.ts Switches RSC useLingui to getI18nOrThrow.
packages/react/src/format.tsx Tightens typing of formatElements element map entries (children-aware).
packages/react/package.json Bumps React/ReactDOM + types to React 19 for package tests/dev.
.gitignore Ignores .swc/ build artifacts and normalizes .lingui/ entry.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/react/src/server.ts
Comment thread packages/react/src/server.ts Outdated
Comment thread website/docs/tutorials/react-rsc.md Outdated
…mjs)

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
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.

3 participants