Skip to content

Commit c02818e

Browse files
amannncursoragent
andauthored
feat: Custom formats for useExtracted, consistency fixes for file references, pruning of messages and sorting of keys (#2155)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
1 parent e8ae56e commit c02818e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+3420
-1829
lines changed

.github/workflows/prerelease-canary.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,9 @@ jobs:
1616
- uses: pnpm/action-setup@v4
1717
- uses: actions/setup-node@v4
1818
with:
19-
registry-url: "https://registry.npmjs.org"
20-
node-version: 20.x
2119
cache: "pnpm"
22-
- run: npm install -g npm@latest # Trusted publishers
20+
node-version: 20.x
21+
registry-url: "https://registry.npmjs.org"
2322
- run: pnpm install
2423
- run: pnpm turbo run build --filter './packages/**'
2524
- run: |

.github/workflows/release.yml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ jobs:
1616
- uses: pnpm/action-setup@v4
1717
- uses: actions/setup-node@v4
1818
with:
19-
registry-url: 'https://registry.npmjs.org'
19+
cache: "pnpm"
2020
node-version: 20.x
21-
cache: 'pnpm'
21+
registry-url: "https://registry.npmjs.org"
2222
- run: npm install -g npm@latest # Trusted publishers
2323
- run: pnpm install
2424
- run: |
@@ -28,5 +28,4 @@ jobs:
2828
if: "${{startsWith(github.event.head_commit.message, 'fix: ') || startsWith(github.event.head_commit.message, 'feat: ') || startsWith(github.event.head_commit.message, 'feat!: ')}}"
2929
env:
3030
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
31-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
32-
NPM_CONFIG_PROVENANCE: true
31+
# No NODE_AUTH_TOKEN since we use Trusted Publishers

docs/src/pages/docs/usage/configuration.mdx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import PartnerContentLink from '@/components/PartnerContentLink';
21
import Callout from '@/components/Callout';
32
import {Tabs} from 'nextra/components';
43
import Details from '@/components/Details';

docs/src/pages/docs/usage/extraction.mdx

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Image from 'next/image';
22
import Callout from '@/components/Callout';
33
import Details from '@/components/Details';
44

5-
# `useExtracted`
5+
# `useExtracted` (experimental)
66

77
As an alternative to managing namespaces and keys manually, `next-intl` provides an additional API that works similar to [`useTranslations`](/docs/usage/translations) but automatically extracts messages from your source files.
88

@@ -48,7 +48,7 @@ function InlineMessages() {
4848

4949
**Links:**
5050

51-
- [Blog post](/blog/use-extracted)
51+
- [Introduction blog post](/blog/use-extracted)
5252
- [Example app](/examples#app-router-extracted)
5353

5454
## Getting started
@@ -73,7 +73,7 @@ const withNextIntl = createNextIntlPlugin({
7373
// Relative path to the directory
7474
path: './messages',
7575

76-
// Either 'json' or 'po'
76+
// Either 'json', 'po', or a custom format (see below)
7777
format: 'json',
7878

7979
// Either 'infer' to automatically detect locales based on
@@ -268,11 +268,11 @@ it('renders', () => {
268268
});
269269
```
270270

271-
## Formatters
271+
## Formats [#formats]
272272

273-
Currently, messages can be extracted as either JSON or PO files. Support for custom formatters is planned for a future release.
273+
Messages can be extracted as JSON, PO, or with custom file formats.
274274

275-
When [`messages`](/docs/usage/plugin#messages) is configured, this will also set up a Turbo- or Webpack loader that will ensure loaded messages can be imported as plain JavaScript objects.
275+
When [`messages`](/docs/usage/plugin#messages) is configured, this will also set up a Turbo- or Webpack loader that will ensure imported messages can be used as plain JavaScript objects.
276276

277277
For example, when using `format: 'po'`, messages can be imported as:
278278

@@ -283,14 +283,14 @@ import {getRequestConfig} from 'next-intl/server';
283283
export default getRequestConfig(async () => {
284284
const locale = 'en';
285285

286-
// E.g. `{"NhX4DJ": "Hello"}`
286+
// E.g. `[{"NhX4DJ": "Hello"}]`
287287
const messages = (await import(`../../messages/${locale}.po`)).default;
288288

289289
// ...
290290
});
291291
```
292292

293-
### JSON formatter [#formatters-json]
293+
### JSON format [#formats-json]
294294

295295
When using this option, your messages will look like this:
296296

@@ -300,7 +300,7 @@ When using this option, your messages will look like this:
300300
}
301301
```
302302

303-
Note that JSON files can only hold pairs of keys and values. To provide more context about a message like file references and descriptions, it's therefore recommended to use [PO files](#po) instead. Another alternative will be to use a custom JSON formatter in the future.
303+
Note that JSON files can only hold pairs of keys and values. To provide more context about a message like file references and descriptions, it's therefore recommended to use [PO files](#formats-po) instead. Alternatively, you can create a [custom format](#formats-custom) to store additional metadata.
304304

305305
For local editing of JSON messages, you can use e.g. a [VSCode integration](/docs/workflows/vscode-integration) like i18n Ally:
306306

@@ -318,7 +318,7 @@ For local editing of JSON messages, you can use e.g. a [VSCode integration](/doc
318318

319319
</Callout>
320320

321-
### PO formatter [#formatters-po]
321+
### PO format [#formats-po]
322322

323323
When using this option, your messages will look like this:
324324

@@ -331,10 +331,58 @@ msgstr "Right"
331331

332332
Besides the message key and the label itself, this format also supports optional descriptions and file references to all modules that consume this message.
333333

334-
For local editing of PO messages, you can use e.g. a tool like [Poedit](https://poedit.net/) (replacing keys with source text requires a pro license).
334+
For local editing of .po files, you can use e.g. a tool like [Poedit](https://poedit.net/) (note however that replacing keys with source text requires a pro license).
335335

336336
<Callout>
337337

338338
**Tip:** AI-based translation can be automated with a translation management system like [Crowdin](/docs/workflows/localization-management).
339339

340340
</Callout>
341+
342+
### Custom format [#formats-custom]
343+
344+
To configure a custom format, you need to specify a codec along with an extension.
345+
346+
The codec can be created via `defineCodec` from `next-intl/extractor`:
347+
348+
```tsx filename="./CustomCodec.ts"
349+
import {defineCodec} from 'next-intl/extractor';
350+
351+
export default defineCodec(() => ({
352+
decode(content, context) {
353+
// ...
354+
},
355+
356+
encode(messages, context) {
357+
// ...
358+
},
359+
360+
toJSONString(content, context) {
361+
// ...
362+
}
363+
}));
364+
```
365+
366+
Then, reference it in your configuration along with an `extension`:
367+
368+
```tsx filename="next.config.ts"
369+
const withNextIntl = createNextIntlPlugin({
370+
experimental: {
371+
messages: {
372+
format: {
373+
codec: './CustomCodec.ts',
374+
extension: '.json'
375+
}
376+
// ...
377+
}
378+
}
379+
});
380+
```
381+
382+
See also the built-in [`codecs`](https://github.com/amannn/next-intl/tree/main/packages/next-intl/src/extractor/format/codecs) for inspiration.
383+
384+
<Callout>
385+
386+
Node.js supports native TypeScript execution like it's needed for the example above starting with v22.18. If you're on an older version, you should define your codec as a JavaScript file.
387+
388+
</Callout>

docs/src/pages/docs/usage/plugin.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ const withNextIntl = createNextIntlPlugin({
9393
// Automatically detects locales based on `path`
9494
locales: 'infer',
9595

96-
// Either 'json' or 'po'
96+
// Either 'json', 'po', or a custom format
9797
format: 'json'
9898
}
9999
// ...
@@ -107,7 +107,7 @@ If you want to specify the locales explicitly, you can provide an array for `loc
107107
locales: ['en', 'de'];
108108
```
109109

110-
Configuring `experimental.messages` will also set up a Turbo- or Webpack loader that will ensure loaded messages can be imported as plain JavaScript objects (see [formatters](/docs/usage/extraction#formatters)).
110+
Configuring `experimental.messages` will also set up a Turbo- or Webpack loader that will ensure loaded messages can be imported as plain JavaScript objects (see [formats](/docs/usage/extraction#formats)).
111111

112112
**Note:** The `messages` option should be used together with [`extract`](#extract) and [`srcPath`](#src-path).
113113

examples/example-app-router-extracted/src/app/Counter.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import {useExtracted} from 'next-intl';
44
import {useState} from 'react';
55

6-
export default function Client() {
6+
export default function Counter() {
77
const [count, setCount] = useState(1000);
88
const t = useExtracted();
99

examples/example-app-router-mixed-routing/playwright.config.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
/* eslint-disable import/no-extraneous-dependencies */
21
import type {PlaywrightTestConfig} from '@playwright/test';
32
import {devices} from '@playwright/test';
43

54
// Use a distinct port on CI to avoid conflicts during concurrent tests
65
const PORT = process.env.CI ? 3002 : 3000;
76

87
const config: PlaywrightTestConfig = {
9-
retries: process.env.CI ? 1 : 0,
8+
retries: process.env.CI ? 2 : 0,
109
testDir: './tests',
1110
projects: [
1211
{

examples/example-app-router-next-auth/playwright.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {devices} from '@playwright/test';
66
const PORT = process.env.CI ? 3003 : 3000;
77

88
const config: PlaywrightTestConfig = {
9-
retries: process.env.CI ? 1 : 0,
9+
retries: process.env.CI ? 2 : 0,
1010
testDir: './tests',
1111
projects: [
1212
{

examples/example-app-router-playground/playwright.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const PORT = process.env.CI ? 3004 : 3000;
99
process.env.PORT = PORT.toString();
1010

1111
const config: PlaywrightTestConfig = {
12-
retries: process.env.CI ? 1 : 0,
12+
retries: process.env.CI ? 2 : 0,
1313
testMatch: process.env.TEST_MATCH || 'main.spec.ts',
1414
testDir: './tests',
1515
projects: [

examples/example-app-router-single-locale/playwright.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {devices} from '@playwright/test';
66
const PORT = process.env.CI ? 3005 : 3000;
77

88
const config: PlaywrightTestConfig = {
9-
retries: process.env.CI ? 1 : 0,
9+
retries: process.env.CI ? 2 : 0,
1010
testDir: './tests',
1111
projects: [
1212
{

0 commit comments

Comments
 (0)