diff --git a/packages/react/README.md b/packages/react/README.md
index ffb5725a..ffad9681 100644
--- a/packages/react/README.md
+++ b/packages/react/README.md
@@ -443,6 +443,242 @@ export default function App() {
}
```
+# Next.js
+
+## App Router
+
+All components and hooks in this package are Client Components (they rely on
+React hooks and context), and the built bundle is published with the
+`"use client"` directive. This means you can import `T`, `UT`, `LanguagePicker`,
+`TXProvider` and the hooks directly inside the Next.js App Router without getting
+the "you're importing a component that needs `useState` ..." error, as long as
+they render inside a Client Component subtree.
+
+Because the App Router renders on the server across concurrent requests, do
+**not** rely on the global `tx` singleton for server rendering. Instead create a
+per-request instance with `createNativeInstance` so locales from different
+requests cannot leak into each other.
+
+### Rendering translations in a Server Component
+
+Create a small per-request helper:
+
+```js
+// app/i18n/getServerTx.js
+import { createNativeInstance, normalizeLocale } from '@transifex/native';
+
+export async function getServerTx(locale) {
+ const txLocale = normalizeLocale(locale); // e.g. pt-br -> pt_BR
+ const instance = createNativeInstance({
+ token: process.env.NEXT_PUBLIC_TRANSIFEX_TOKEN,
+ currentLocale: txLocale,
+ });
+ await instance.fetchTranslations(txLocale);
+ return instance;
+}
+```
+
+Use it inside an async Server Component (for example a `[locale]` segment):
+
+```jsx
+// app/[locale]/page.jsx
+import { getServerTx } from '../i18n/getServerTx';
+
+export default async function Page({ params }) {
+ const { locale } = await params;
+ const tx = await getServerTx(locale);
+ return
{tx.t('Hello world')}
;
+}
+```
+
+> In Next.js 15+, `params` is a Promise and must be awaited (as shown above).
+> In Next.js 14 and earlier, `params` is a plain object, so you can read
+> `params.locale` directly without `await`.
+
+You can enumerate the available locales for `generateStaticParams` with
+`tx.getLocales()`.
+
+### Hydrating Client Components
+
+For interactive parts that use `T`, hooks or the `LanguagePicker`, fetch the
+translations on the server and pass them into a Client Component that seeds a
+`TXProvider`:
+
+```jsx
+// app/i18n/TxClientProvider.jsx
+'use client';
+
+import { useMemo } from 'react';
+import { createNativeInstance } from '@transifex/native';
+import { TXProvider } from '@transifex/react';
+
+export default function TxClientProvider({ locale, translations, children }) {
+ const instance = useMemo(() => {
+ const tx = createNativeInstance({ currentLocale: locale });
+ tx.cache.update(locale, translations);
+ return tx;
+ }, [locale, translations]);
+
+ return {children};
+}
+```
+
+```jsx
+// app/[locale]/layout.jsx (Server Component)
+import { normalizeLocale } from '@transifex/native';
+import { getServerTx } from '../i18n/getServerTx';
+import TxClientProvider from '../i18n/TxClientProvider';
+
+export default async function LocaleLayout({ children, params }) {
+ const { locale } = await params;
+ const txLocale = normalizeLocale(locale);
+ const tx = await getServerTx(locale);
+ const translations = tx.cache.getTranslations(txLocale);
+
+ return (
+
+ {children}
+
+ );
+}
+```
+
+Client Components rendered inside the provider can then use `T` and the hooks as
+usual.
+
+### App Router notes and limitations
+
+- The App Router does not use the `i18n` option in `next.config.js`. Set up
+ locale routing with a `[locale]` dynamic segment (or middleware) instead.
+- `publicRuntimeConfig` is not available in the App Router. Use environment
+ variables (`NEXT_PUBLIC_*`) for the public token.
+- Live language switching, over-the-air auto-refresh and `LanguagePicker`
+ require Client Components.
+- Server-side rendering of translations (`tx.t(...)` in a Server Component) uses
+ the per-request instance and does not ship JavaScript for that content.
+
+## Pages Router
+
+The Pages Router uses Next.js built-in [Internationalized Routing](https://nextjs.org/docs/pages/building-your-application/routing/internationalization)
+(`i18n` in `next.config.js`) together with `getServerSideProps` to fetch
+translations on the server and hydrate the client. In this model the global `tx`
+singleton is the recommended approach — each `getServerSideProps` call runs in
+isolation per request.
+
+### Configure locales and token
+
+```js
+// next.config.js
+module.exports = {
+ i18n: {
+ locales: ['en', 'fr', 'de', 'pt-BR'],
+ defaultLocale: 'en',
+ localeDetection: false,
+ },
+ publicRuntimeConfig: {
+ TxNativePublicToken: process.env.NEXT_PUBLIC_TRANSIFEX_TOKEN,
+ },
+};
+```
+
+### Create a Transifex utility
+
+```js
+// lib/i18n.js
+import { tx, normalizeLocale } from '@transifex/native';
+import getConfig from 'next/config';
+
+const { publicRuntimeConfig } = getConfig();
+
+/**
+ * Used by SSR to pass translations to the browser.
+ *
+ * @param {{ locale: string, locales: string[] }} context
+ * @returns {{ locale: string, locales: string[], translations: object }}
+ */
+export async function getServerSideTranslations({ locale, locales }) {
+ tx.init({
+ token: publicRuntimeConfig.TxNativePublicToken,
+ });
+
+ const txLocale = normalizeLocale(locale);
+ await tx.fetchTranslations(txLocale);
+
+ return {
+ locale,
+ locales,
+ translations: tx.cache.getTranslations(txLocale),
+ };
+}
+
+/**
+ * Initialize the client-side Transifex Native cache from server props.
+ *
+ * @param {{ locale: string, translations: object }} props
+ */
+export function setClientSideTranslations({ locale, translations }) {
+ if (!locale || !translations) return;
+ tx.init({ currentLocale: locale });
+ tx.cache.update(locale, translations);
+}
+```
+
+### Load translations per page
+
+```jsx
+// pages/index.js
+import { T } from '@transifex/react';
+import { getServerSideTranslations, setClientSideTranslations } from '../lib/i18n';
+
+export default function Home(props) {
+ setClientSideTranslations(props);
+
+ return (
+
+
+
+ );
+}
+
+export async function getServerSideProps(context) {
+ const data = await getServerSideTranslations(context);
+ return { props: { ...data } };
+}
+```
+
+### Set translations globally in `_app.js`
+
+To avoid calling `setClientSideTranslations` on every page, initialize the
+client cache once in your custom App:
+
+```jsx
+// pages/_app.js
+import { setClientSideTranslations } from '../lib/i18n';
+
+export default function MyApp({ Component, pageProps }) {
+ setClientSideTranslations(pageProps);
+ return ;
+}
+```
+
+You still need `getServerSideProps` on each page that requires translations,
+because `_app.js` does not support data fetching methods.
+
+### Pages Router notes and limitations
+
+- Locale routing is handled by Next.js `i18n` config — no `[locale]` segment or
+ middleware is required.
+- `publicRuntimeConfig` is available and is the documented way to pass the public
+ token to server-side code.
+- `T`, `UT`, hooks and `LanguagePicker` work directly in page components without
+ a `"use client"` boundary (the Pages Router does not use React Server
+ Components).
+- For over-the-air translation refresh without a server restart, add a TTL-based
+ refresh in `getServerSideTranslations` (see the
+ [Next.js guide](https://developers.transifex.com/docs/nextjs#auto-refresh-translations)).
+- For large sites, consider content splitting with tagged fetches to reduce data
+ transfer.
+
# License
Licensed under Apache License 2.0, see [LICENSE](https://github.com/transifex/transifex-javascript/blob/HEAD/LICENSE) file.
diff --git a/packages/react/package.json b/packages/react/package.json
index 92bc9fa6..fe812b1e 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -23,7 +23,8 @@
},
"repository": "git://github.com/transifex/transifex-javascript.git",
"scripts": {
- "build": "NODE_ENV=production microbundle-crl --no-compress --format modern,cjs && cp src/index.d.ts dist/index.d.ts && cp src/index.d.ts dist/index.modern.d.ts",
+ "build": "NODE_ENV=production microbundle-crl --no-compress --format modern,cjs && node scripts/add-use-client.js && cp src/index.d.ts dist/index.d.ts && cp src/index.d.ts dist/index.modern.d.ts",
+ "postbuild": "node scripts/check-use-client.js",
"prepare": "run-s build",
"lint": "eslint src/ tests/",
"test": "run-s test:unit test:build",
diff --git a/packages/react/scripts/add-use-client.js b/packages/react/scripts/add-use-client.js
new file mode 100644
index 00000000..61687e30
--- /dev/null
+++ b/packages/react/scripts/add-use-client.js
@@ -0,0 +1,28 @@
+/* Prepend the React Server Components `"use client"` directive to the built
+ * bundles.
+ *
+ * Every export in this package (components and hooks) relies on client-only
+ * React features (useState, useEffect, useContext, createContext), so the whole
+ * package is a Client Component boundary. The bundler (microbundle/rollup)
+ * strips module-level directives from the source, so we re-add the banner to the
+ * emitted bundles here. Without it, importing this package from a Next.js App
+ * Router Server Component throws a "needs to be a Client Component" error. */
+
+const fs = require('fs');
+const path = require('path');
+const DIRECTIVE = require('./use-client-directive');
+
+const DIST_DIR = path.resolve(__dirname, '..', 'dist');
+const FILES = ['index.js', 'index.modern.js'];
+
+FILES.forEach((file) => {
+ const filePath = path.join(DIST_DIR, file);
+ if (!fs.existsSync(filePath)) return;
+
+ const contents = fs.readFileSync(filePath, 'utf8');
+ if (contents.startsWith(DIRECTIVE)) return;
+
+ fs.writeFileSync(filePath, `${DIRECTIVE}\n${contents}`);
+ // eslint-disable-next-line no-console
+ console.log(`Prepended "use client" to dist/${file}`);
+});
diff --git a/packages/react/scripts/check-use-client.js b/packages/react/scripts/check-use-client.js
new file mode 100644
index 00000000..a493408c
--- /dev/null
+++ b/packages/react/scripts/check-use-client.js
@@ -0,0 +1,38 @@
+/* Assert that the built bundles start with the `"use client"` directive.
+ *
+ * The build relies on scripts/add-use-client.js to prepend the directive to the
+ * emitted bundles. This guard fails the build/test if the directive is missing
+ * (e.g. the post-build step was removed, or a bundler upgrade changed the dist
+ * output), which would otherwise silently break Next.js App Router consumers
+ * with a "needs to be a Client Component" error. */
+
+const fs = require('fs');
+const path = require('path');
+const DIRECTIVE = require('./use-client-directive');
+
+const DIST_DIR = path.resolve(__dirname, '..', 'dist');
+const FILES = ['index.js', 'index.modern.js'];
+
+const errors = [];
+
+FILES.forEach((file) => {
+ const filePath = path.join(DIST_DIR, file);
+ if (!fs.existsSync(filePath)) {
+ errors.push(`dist/${file} does not exist (was the build run?)`);
+ return;
+ }
+
+ const firstLine = fs.readFileSync(filePath, 'utf8').split('\n')[0].trim();
+ if (firstLine !== DIRECTIVE) {
+ errors.push(`dist/${file} is missing the "use client" directive (found: ${JSON.stringify(firstLine)})`);
+ }
+});
+
+if (errors.length) {
+ // eslint-disable-next-line no-console
+ console.error(`"use client" check failed:\n- ${errors.join('\n- ')}`);
+ process.exit(1);
+}
+
+// eslint-disable-next-line no-console
+console.log('"use client" directive present in all dist bundles');
diff --git a/packages/react/scripts/use-client-directive.js b/packages/react/scripts/use-client-directive.js
new file mode 100644
index 00000000..c2407aa2
--- /dev/null
+++ b/packages/react/scripts/use-client-directive.js
@@ -0,0 +1,4 @@
+/* Single source of truth for the React Server Components client boundary
+ * directive, shared by the add/check post-build scripts. */
+
+module.exports = '\'use client\';';