Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
236 changes: 236 additions & 0 deletions packages/react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <h1>{tx.t('Hello world')}</h1>;
}
```

> 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 <TXProvider instance={instance}>{children}</TXProvider>;
}
```

```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 (
<TxClientProvider locale={txLocale} translations={translations}>
{children}
</TxClientProvider>
);
}
```

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 (
<div>
<T _str="Hello world" />
</div>
);
}

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 <Component {...pageProps} />;
}
```

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.
3 changes: 2 additions & 1 deletion packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
28 changes: 28 additions & 0 deletions packages/react/scripts/add-use-client.js
Original file line number Diff line number Diff line change
@@ -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}`);
});
38 changes: 38 additions & 0 deletions packages/react/scripts/check-use-client.js
Original file line number Diff line number Diff line change
@@ -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');
4 changes: 4 additions & 0 deletions packages/react/scripts/use-client-directive.js
Original file line number Diff line number Diff line change
@@ -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\';';
Loading