next-intl 4.4.0 → 4.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/development/plugin.cjs +129 -8
- package/dist/esm/development/extractor/ExtractionCompiler.js +41 -0
- package/dist/esm/development/extractor/catalog/CatalogLocales.js +117 -0
- package/dist/esm/development/extractor/catalog/CatalogManager.js +286 -0
- package/dist/esm/development/extractor/catalog/CatalogPersister.js +45 -0
- package/dist/esm/development/extractor/catalog/SaveScheduler.js +66 -0
- package/dist/esm/development/extractor/catalogLoader.js +35 -0
- package/dist/esm/development/extractor/extractMessages.js +8 -0
- package/dist/esm/development/extractor/extractionLoader.js +22 -0
- package/dist/esm/development/extractor/extractor/ASTScope.js +18 -0
- package/dist/esm/development/extractor/extractor/KeyGenerator.js +11 -0
- package/dist/esm/development/extractor/extractor/LRUCache.js +30 -0
- package/dist/esm/development/extractor/extractor/MessageExtractor.js +402 -0
- package/dist/esm/development/extractor/formatters/Formatter.js +3 -0
- package/dist/esm/development/extractor/formatters/JSONFormatter.js +42 -0
- package/dist/esm/development/extractor/formatters/POFormatter.js +51 -0
- package/dist/esm/development/extractor/formatters/index.js +6 -0
- package/dist/esm/development/extractor/formatters/utils.js +13 -0
- package/dist/esm/development/extractor/source/SourceFileFilter.js +11 -0
- package/dist/esm/development/extractor/source/SourceFileScanner.js +27 -0
- package/dist/esm/development/extractor/utils/ObjectUtils.js +14 -0
- package/dist/esm/development/extractor/utils/POParser.js +222 -0
- package/dist/esm/development/extractor.js +1 -0
- package/dist/esm/development/index.react-client.js +2 -1
- package/dist/esm/development/index.react-server.js +1 -0
- package/dist/esm/development/middleware/utils.js +6 -2
- package/dist/esm/development/plugin/createNextIntlPlugin.js +1 -1
- package/dist/esm/development/plugin/{createMessagesDeclaration.js → declaration/createMessagesDeclaration.js} +2 -2
- package/dist/esm/development/plugin/getNextConfig.js +117 -8
- package/dist/esm/development/plugin/{hasStableTurboConfig.js → nextFlags.js} +7 -2
- package/dist/esm/development/react-client/index.js +0 -1
- package/dist/esm/development/react-server/useExtracted.js +9 -0
- package/dist/esm/development/server/react-client/index.js +2 -1
- package/dist/esm/development/server/react-server/getExtracted.js +24 -0
- package/dist/esm/development/server/react-server/getServerExtractor.js +38 -0
- package/dist/esm/development/server.react-client.js +1 -1
- package/dist/esm/development/server.react-server.js +1 -0
- package/dist/esm/development/shared/utils.js +2 -1
- package/dist/esm/production/extractor/ExtractionCompiler.js +1 -0
- package/dist/esm/production/extractor/catalog/CatalogLocales.js +1 -0
- package/dist/esm/production/extractor/catalog/CatalogManager.js +1 -0
- package/dist/esm/production/extractor/catalog/CatalogPersister.js +1 -0
- package/dist/esm/production/extractor/catalog/SaveScheduler.js +1 -0
- package/dist/esm/production/extractor/catalogLoader.js +1 -0
- package/dist/esm/production/extractor/extractMessages.js +1 -0
- package/dist/esm/production/extractor/extractionLoader.js +1 -0
- package/dist/esm/production/extractor/extractor/ASTScope.js +1 -0
- package/dist/esm/production/extractor/extractor/KeyGenerator.js +1 -0
- package/dist/esm/production/extractor/extractor/LRUCache.js +1 -0
- package/dist/esm/production/extractor/extractor/MessageExtractor.js +1 -0
- package/dist/esm/production/extractor/formatters/Formatter.js +1 -0
- package/dist/esm/production/extractor/formatters/JSONFormatter.js +1 -0
- package/dist/esm/production/extractor/formatters/POFormatter.js +1 -0
- package/dist/esm/production/extractor/formatters/index.js +1 -0
- package/dist/esm/production/extractor/formatters/utils.js +1 -0
- package/dist/esm/production/extractor/source/SourceFileFilter.js +1 -0
- package/dist/esm/production/extractor/source/SourceFileScanner.js +1 -0
- package/dist/esm/production/extractor/utils/ObjectUtils.js +1 -0
- package/dist/esm/production/extractor/utils/POParser.js +1 -0
- package/dist/esm/production/extractor.js +1 -0
- package/dist/esm/production/index.react-client.js +1 -1
- package/dist/esm/production/index.react-server.js +1 -1
- package/dist/esm/production/middleware/utils.js +1 -1
- package/dist/esm/production/plugin/createNextIntlPlugin.js +1 -1
- package/dist/esm/production/plugin/declaration/createMessagesDeclaration.js +1 -0
- package/dist/esm/production/plugin/getNextConfig.js +1 -1
- package/dist/esm/production/plugin/nextFlags.js +1 -0
- package/dist/esm/production/react-client/index.js +1 -1
- package/dist/esm/production/react-server/useExtracted.js +1 -0
- package/dist/esm/production/server/react-client/index.js +1 -1
- package/dist/esm/production/server/react-server/getExtracted.js +1 -0
- package/dist/esm/production/server/react-server/getServerExtractor.js +1 -0
- package/dist/esm/production/server.react-client.js +1 -1
- package/dist/esm/production/server.react-server.js +1 -1
- package/dist/esm/production/shared/utils.js +1 -1
- package/dist/types/extractor/ExtractionCompiler.d.ts +14 -0
- package/dist/types/extractor/catalog/CatalogLocales.d.ts +31 -0
- package/dist/types/extractor/catalog/CatalogManager.d.ts +46 -0
- package/dist/types/extractor/catalog/CatalogPersister.d.ts +11 -0
- package/dist/types/extractor/catalog/SaveScheduler.d.ts +17 -0
- package/dist/types/extractor/extractMessages.d.ts +2 -0
- package/dist/types/extractor/extractor/ASTScope.d.ts +12 -0
- package/dist/types/extractor/extractor/KeyGenerator.d.ts +3 -0
- package/dist/types/extractor/extractor/LRUCache.d.ts +7 -0
- package/dist/types/extractor/extractor/MessageExtractor.d.ts +20 -0
- package/dist/types/extractor/formatters/Formatter.d.ts +10 -0
- package/dist/types/extractor/formatters/JSONFormatter.d.ts +10 -0
- package/dist/types/extractor/formatters/POFormatter.d.ts +10 -0
- package/dist/types/extractor/formatters/index.d.ts +5 -0
- package/dist/types/extractor/formatters/utils.d.ts +2 -0
- package/dist/types/extractor/index.d.ts +1 -0
- package/dist/types/extractor/source/SourceFileFilter.d.ts +4 -0
- package/dist/types/extractor/source/SourceFileScanner.d.ts +4 -0
- package/dist/types/extractor/types.d.ts +23 -0
- package/dist/types/extractor/utils/ObjectUtils.d.ts +1 -0
- package/dist/types/extractor/utils/POParser.d.ts +24 -0
- package/dist/types/extractor.d.ts +1 -0
- package/dist/types/navigation/react-client/createNavigation.d.ts +9 -9
- package/dist/types/navigation/react-server/createNavigation.d.ts +9 -9
- package/dist/types/navigation/shared/createSharedNavigationFns.d.ts +10 -10
- package/dist/types/plugin/catalog/catalogLoader.d.ts +10 -0
- package/dist/types/plugin/declaration/index.d.ts +1 -0
- package/dist/types/plugin/extractor/extractionLoader.d.ts +3 -0
- package/dist/types/plugin/nextFlags.d.ts +2 -0
- package/dist/types/plugin/types.d.ts +18 -0
- package/dist/types/react-client/index.d.ts +3 -1
- package/dist/types/react-server/index.d.ts +1 -0
- package/dist/types/react-server/useExtracted.d.ts +2 -0
- package/dist/types/server/react-client/index.d.ts +2 -1
- package/dist/types/server/react-server/getExtracted.d.ts +10 -0
- package/dist/types/server/react-server/getServerExtractor.d.ts +5 -0
- package/dist/types/server/react-server/index.d.ts +1 -0
- package/extractor/catalogLoader.d.ts +4 -0
- package/extractor/extractionLoader.d.ts +4 -0
- package/extractor.d.ts +2 -0
- package/package.json +20 -5
- package/dist/esm/production/plugin/createMessagesDeclaration.js +0 -1
- package/dist/esm/production/plugin/hasStableTurboConfig.js +0 -1
- package/dist/types/plugin/hasStableTurboConfig.d.ts +0 -2
- /package/dist/types/plugin/{createMessagesDeclaration.d.ts → declaration/createMessagesDeclaration.d.ts} +0 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { setNestedProperty } from '../utils/ObjectUtils.js';
|
|
2
|
+
import POParser from '../utils/POParser.js';
|
|
3
|
+
import Formatter from './Formatter.js';
|
|
4
|
+
import { getSortedMessages } from './utils.js';
|
|
5
|
+
|
|
6
|
+
class POFormatter extends Formatter {
|
|
7
|
+
// See also https://www.gnu.org/software/gettext/manual/html_node/Header-Entry.html
|
|
8
|
+
static DEFAULT_METADATA = {
|
|
9
|
+
// Recommended by spec
|
|
10
|
+
'Content-Type': 'text/plain; charset=utf-8',
|
|
11
|
+
'Content-Transfer-Encoding': '8bit',
|
|
12
|
+
// Otherwise other tools might set this
|
|
13
|
+
'X-Generator': 'next-intl',
|
|
14
|
+
// Crowdin defaults to using msgid as source key
|
|
15
|
+
'X-Crowdin-SourceKey': 'msgstr'
|
|
16
|
+
};
|
|
17
|
+
EXTENSION = '.po';
|
|
18
|
+
|
|
19
|
+
// Metadata is stored so it can be retained when writing
|
|
20
|
+
metadataByLocale = (() => new Map())();
|
|
21
|
+
parse(content, context) {
|
|
22
|
+
const catalog = POParser.parse(content);
|
|
23
|
+
|
|
24
|
+
// Store metadata for this locale
|
|
25
|
+
if (catalog.meta) {
|
|
26
|
+
this.metadataByLocale.set(context.locale, catalog.meta);
|
|
27
|
+
}
|
|
28
|
+
return catalog.messages || [];
|
|
29
|
+
}
|
|
30
|
+
serialize(messages, context) {
|
|
31
|
+
const meta = {
|
|
32
|
+
Language: context.locale,
|
|
33
|
+
...POFormatter.DEFAULT_METADATA,
|
|
34
|
+
...this.metadataByLocale.get(context.locale)
|
|
35
|
+
};
|
|
36
|
+
return POParser.serialize({
|
|
37
|
+
meta,
|
|
38
|
+
messages: getSortedMessages(messages)
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
toJSONString(source, context) {
|
|
42
|
+
const parsed = this.parse(source, context);
|
|
43
|
+
const messagesObject = {};
|
|
44
|
+
for (const message of parsed) {
|
|
45
|
+
setNestedProperty(messagesObject, message.id, message.message);
|
|
46
|
+
}
|
|
47
|
+
return JSON.stringify(messagesObject, null, 2);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export { POFormatter as default };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
function getSortedMessages(messages) {
|
|
2
|
+
return messages.toSorted((a, b) => {
|
|
3
|
+
const aPath = a.references?.[0]?.path ?? a.message;
|
|
4
|
+
const bPath = b.references?.[0]?.path ?? b.message;
|
|
5
|
+
if (aPath === bPath) {
|
|
6
|
+
return a.message.localeCompare(b.message);
|
|
7
|
+
} else {
|
|
8
|
+
return aPath.localeCompare(bPath);
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { getSortedMessages };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
|
|
3
|
+
class SourceFileFilter {
|
|
4
|
+
static EXTENSIONS = ['ts', 'tsx', 'js', 'jsx'];
|
|
5
|
+
static isSourceFile(filePath) {
|
|
6
|
+
const ext = path.extname(filePath);
|
|
7
|
+
return SourceFileFilter.EXTENSIONS.map(cur => '.' + cur).includes(ext);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export { SourceFileFilter as default };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import SourceFileFilter from './SourceFileFilter.js';
|
|
4
|
+
|
|
5
|
+
class SourceFileScanner {
|
|
6
|
+
static async walkSourceFiles(dir, srcPaths, acc = []) {
|
|
7
|
+
const entries = await fs.readdir(dir, {
|
|
8
|
+
withFileTypes: true
|
|
9
|
+
});
|
|
10
|
+
for (const entry of entries) {
|
|
11
|
+
const entryPath = path.join(dir, entry.name);
|
|
12
|
+
if (entry.isDirectory()) {
|
|
13
|
+
await SourceFileScanner.walkSourceFiles(entryPath, srcPaths, acc);
|
|
14
|
+
} else {
|
|
15
|
+
if (SourceFileFilter.isSourceFile(entry.name)) {
|
|
16
|
+
acc.push(entryPath);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return acc;
|
|
21
|
+
}
|
|
22
|
+
static async getSourceFiles(srcPaths) {
|
|
23
|
+
return (await Promise.all(srcPaths.map(srcPath => SourceFileScanner.walkSourceFiles(srcPath, srcPaths)))).flat();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export { SourceFileScanner as default };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
function setNestedProperty(obj, keyPath, value) {
|
|
2
|
+
const keys = keyPath.split('.');
|
|
3
|
+
let current = obj;
|
|
4
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
5
|
+
const key = keys[i];
|
|
6
|
+
if (!(key in current) || typeof current[key] !== 'object' || current[key] === null) {
|
|
7
|
+
current[key] = {};
|
|
8
|
+
}
|
|
9
|
+
current = current[key];
|
|
10
|
+
}
|
|
11
|
+
current[keys[keys.length - 1]] = value;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export { setNestedProperty };
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
class POParser {
|
|
2
|
+
static KEYWORDS = (() => ({
|
|
3
|
+
MSGID: 'msgid',
|
|
4
|
+
MSGSTR: 'msgstr',
|
|
5
|
+
MSGCTXT: 'msgctxt',
|
|
6
|
+
MSGID_PLURAL: 'msgid_plural'
|
|
7
|
+
}))();
|
|
8
|
+
static COMMENTS = (() => ({
|
|
9
|
+
REFERENCE: '#:',
|
|
10
|
+
EXTRACTED: '#.',
|
|
11
|
+
TRANSLATOR: '#',
|
|
12
|
+
FLAG: '#,',
|
|
13
|
+
PREVIOUS: '#|'
|
|
14
|
+
}))();
|
|
15
|
+
static NAMESPACE_SEPARATOR = '.';
|
|
16
|
+
static QUOTE = '"';
|
|
17
|
+
static NEWLINE = '\\n';
|
|
18
|
+
static FILE_COLUMN_SEPARATOR = ':';
|
|
19
|
+
static META_SEPARATOR = ':';
|
|
20
|
+
static parse(content) {
|
|
21
|
+
const lines = POParser.splitLines(content);
|
|
22
|
+
const messages = [];
|
|
23
|
+
const meta = {};
|
|
24
|
+
let state = 'entry';
|
|
25
|
+
let entry;
|
|
26
|
+
for (let i = 0; i < lines.length; i++) {
|
|
27
|
+
const line = lines[i].trim();
|
|
28
|
+
|
|
29
|
+
// An empty line indicates the end of an entry
|
|
30
|
+
if (!line) {
|
|
31
|
+
if (state === 'entry' && entry) {
|
|
32
|
+
messages.push(POParser.finishEntry(entry));
|
|
33
|
+
entry = undefined;
|
|
34
|
+
}
|
|
35
|
+
state = 'entry';
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (state === 'meta') {
|
|
39
|
+
if (line.startsWith(POParser.QUOTE)) {
|
|
40
|
+
const metaLine = POParser.extractQuotedString(line, state);
|
|
41
|
+
const cleaned = metaLine.endsWith(POParser.NEWLINE) ? metaLine.slice(0, -2) : metaLine;
|
|
42
|
+
const separatorIndex = cleaned.indexOf(POParser.META_SEPARATOR);
|
|
43
|
+
if (separatorIndex > 0) {
|
|
44
|
+
const key = cleaned.substring(0, separatorIndex).trim();
|
|
45
|
+
const value = cleaned.substring(separatorIndex + 1).trim();
|
|
46
|
+
meta[key] = value;
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
POParser.throwWithLine('Encountered unexpected non-quoted metadata line', line);
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
// Unsupported comment types
|
|
53
|
+
if (POParser.lineStartsWithPrefix(line, POParser.COMMENTS.TRANSLATOR)) {
|
|
54
|
+
POParser.throwWithLine('Translator comments (#) are not supported, use inline descriptions instead', line);
|
|
55
|
+
}
|
|
56
|
+
if (POParser.lineStartsWithPrefix(line, POParser.COMMENTS.FLAG)) {
|
|
57
|
+
POParser.throwWithLine('Flag comments (#,) are not supported', line);
|
|
58
|
+
}
|
|
59
|
+
if (POParser.lineStartsWithPrefix(line, POParser.COMMENTS.PREVIOUS)) {
|
|
60
|
+
POParser.throwWithLine('Previous string key comments (#|) are not supported', line);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Reference comments
|
|
64
|
+
if (POParser.lineStartsWithPrefix(line, POParser.COMMENTS.REFERENCE)) {
|
|
65
|
+
entry = POParser.ensureEntry(entry);
|
|
66
|
+
// Only use the path part, ignore line and column numbers
|
|
67
|
+
const path = line.substring(POParser.COMMENTS.REFERENCE.length).trim().split(POParser.FILE_COLUMN_SEPARATOR).at(0);
|
|
68
|
+
entry.references ??= [];
|
|
69
|
+
entry.references.push({
|
|
70
|
+
path
|
|
71
|
+
});
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Extracted comments
|
|
76
|
+
if (POParser.lineStartsWithPrefix(line, POParser.COMMENTS.EXTRACTED)) {
|
|
77
|
+
entry = POParser.ensureEntry(entry);
|
|
78
|
+
entry.description = line.substring(POParser.COMMENTS.EXTRACTED.length).trim();
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Check for unsupported features
|
|
83
|
+
if (POParser.lineStartsWithPrefix(line, POParser.KEYWORDS.MSGID_PLURAL)) {
|
|
84
|
+
POParser.throwWithLine('Plural forms (msgid_plural) are not supported, use ICU pluralization instead', line);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// msgctxt
|
|
88
|
+
if (POParser.lineStartsWithPrefix(line, POParser.KEYWORDS.MSGCTXT)) {
|
|
89
|
+
entry = POParser.ensureEntry(entry);
|
|
90
|
+
entry.msgctxt = POParser.extractQuotedString(line.substring(POParser.KEYWORDS.MSGCTXT.length + 1), state);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// msgid
|
|
95
|
+
if (POParser.lineStartsWithPrefix(line, POParser.KEYWORDS.MSGID)) {
|
|
96
|
+
entry = POParser.ensureEntry(entry);
|
|
97
|
+
entry.msgid = POParser.extractQuotedString(line.substring(POParser.KEYWORDS.MSGID.length + 1), state);
|
|
98
|
+
if (POParser.isMetaEntry(entry, messages)) {
|
|
99
|
+
state = 'meta';
|
|
100
|
+
entry = undefined;
|
|
101
|
+
}
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// msgstr
|
|
106
|
+
if (POParser.lineStartsWithPrefix(line, POParser.KEYWORDS.MSGSTR)) {
|
|
107
|
+
entry = POParser.ensureEntry(entry);
|
|
108
|
+
entry.msgstr = POParser.extractQuotedString(line.substring(POParser.KEYWORDS.MSGSTR.length + 1), state);
|
|
109
|
+
if (POParser.isMetaEntry(entry, messages)) {
|
|
110
|
+
state = 'meta';
|
|
111
|
+
entry = undefined;
|
|
112
|
+
}
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Multi-line strings are not supported in entry mode
|
|
117
|
+
if (line.startsWith(POParser.QUOTE)) {
|
|
118
|
+
POParser.throwWithLine('Multi-line strings are not supported, use single-line strings instead', line);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Finish any remaining entry
|
|
124
|
+
if (state === 'entry' && entry) {
|
|
125
|
+
messages.push(POParser.finishEntry(entry));
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
meta: Object.keys(meta).length > 0 ? meta : undefined,
|
|
129
|
+
messages: messages.length > 0 ? messages : undefined
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
static isMetaEntry(entry, messages) {
|
|
133
|
+
return messages.length === 0 && entry.msgid === '' && entry.msgstr === '';
|
|
134
|
+
}
|
|
135
|
+
static serialize(catalog) {
|
|
136
|
+
const lines = [];
|
|
137
|
+
|
|
138
|
+
// Metadata
|
|
139
|
+
if (catalog.meta) {
|
|
140
|
+
lines.push(`${POParser.KEYWORDS.MSGID} ${POParser.QUOTE}${POParser.QUOTE}`);
|
|
141
|
+
lines.push(`${POParser.KEYWORDS.MSGSTR} ${POParser.QUOTE}${POParser.QUOTE}`);
|
|
142
|
+
for (const [key, value] of Object.entries(catalog.meta)) {
|
|
143
|
+
lines.push(`${POParser.QUOTE}${key}${POParser.META_SEPARATOR} ${value}${POParser.NEWLINE}${POParser.QUOTE}`);
|
|
144
|
+
}
|
|
145
|
+
lines.push('');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Messages
|
|
149
|
+
if (catalog.messages) {
|
|
150
|
+
for (const message of catalog.messages) {
|
|
151
|
+
if (message.description) {
|
|
152
|
+
lines.push(`${POParser.COMMENTS.EXTRACTED} ${message.description}`);
|
|
153
|
+
}
|
|
154
|
+
if (message.references && message.references.length > 0) {
|
|
155
|
+
for (const ref of message.references) {
|
|
156
|
+
lines.push(`${POParser.COMMENTS.REFERENCE} ${ref.path}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
let msgctxt;
|
|
160
|
+
let msgid;
|
|
161
|
+
const lastDotIndex = message.id.lastIndexOf(POParser.NAMESPACE_SEPARATOR);
|
|
162
|
+
if (lastDotIndex > 0) {
|
|
163
|
+
msgctxt = message.id.substring(0, lastDotIndex);
|
|
164
|
+
msgid = message.id.substring(lastDotIndex + 1);
|
|
165
|
+
} else {
|
|
166
|
+
msgid = message.id;
|
|
167
|
+
}
|
|
168
|
+
if (msgctxt) {
|
|
169
|
+
lines.push(`${POParser.KEYWORDS.MSGCTXT} ${POParser.QUOTE}${msgctxt}${POParser.QUOTE}`);
|
|
170
|
+
}
|
|
171
|
+
lines.push(`${POParser.KEYWORDS.MSGID} ${POParser.QUOTE}${msgid}${POParser.QUOTE}`);
|
|
172
|
+
lines.push(`${POParser.KEYWORDS.MSGSTR} ${POParser.QUOTE}${message.message}${POParser.QUOTE}`);
|
|
173
|
+
lines.push('');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return lines.join('\n');
|
|
177
|
+
}
|
|
178
|
+
static lineStartsWithPrefix(line, prefix) {
|
|
179
|
+
return line.startsWith(prefix + ' ');
|
|
180
|
+
}
|
|
181
|
+
static throwWithLine(message, line) {
|
|
182
|
+
throw new Error(`${message}:\n> ${line}`);
|
|
183
|
+
}
|
|
184
|
+
static splitLines(content) {
|
|
185
|
+
// Avoid overhead for Unix newlines only
|
|
186
|
+
if (content.includes('\r')) {
|
|
187
|
+
content = content.replace(/\r\n/g, '\n');
|
|
188
|
+
}
|
|
189
|
+
return content.split('\n');
|
|
190
|
+
}
|
|
191
|
+
static ensureEntry(entry) {
|
|
192
|
+
return entry || {};
|
|
193
|
+
}
|
|
194
|
+
static finishEntry(entry) {
|
|
195
|
+
if (entry.msgid == null || entry.msgstr == null) {
|
|
196
|
+
throw new Error('Incomplete message entry: both msgid and msgstr are required');
|
|
197
|
+
}
|
|
198
|
+
let fullId = entry.msgid;
|
|
199
|
+
if (entry.msgctxt) {
|
|
200
|
+
fullId = entry.msgctxt + POParser.NAMESPACE_SEPARATOR + entry.msgid;
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
id: fullId,
|
|
204
|
+
message: entry.msgstr,
|
|
205
|
+
description: entry.description,
|
|
206
|
+
references: entry.references
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
static extractQuotedString(line, state) {
|
|
210
|
+
const trimmed = line.trim();
|
|
211
|
+
const endIndex = trimmed.indexOf(POParser.QUOTE, POParser.QUOTE.length);
|
|
212
|
+
if (endIndex === -1) {
|
|
213
|
+
if (state === 'meta') {
|
|
214
|
+
return trimmed.substring(POParser.QUOTE.length);
|
|
215
|
+
}
|
|
216
|
+
POParser.throwWithLine('Incomplete quoted string', line);
|
|
217
|
+
}
|
|
218
|
+
return trimmed.substring(POParser.QUOTE.length, endIndex);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export { POParser as default };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as unstable_extractMessages } from './extractor/extractMessages.js';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { useFormatter, useTranslations } from './react-client/index.js';
|
|
2
2
|
export { default as NextIntlClientProvider } from './shared/NextIntlClientProvider.js';
|
|
3
|
-
export
|
|
3
|
+
export { IntlProvider, _useExtracted as useExtracted, useLocale, useMessages, useNow, useTimeZone } from 'use-intl/react';
|
|
4
|
+
export * from 'use-intl/core';
|
|
@@ -5,4 +5,5 @@ export { default as useNow } from './react-server/useNow.js';
|
|
|
5
5
|
export { default as useTimeZone } from './react-server/useTimeZone.js';
|
|
6
6
|
export { default as useMessages } from './react-server/useMessages.js';
|
|
7
7
|
export { default as NextIntlClientProvider } from './react-server/NextIntlClientProviderServer.js';
|
|
8
|
+
export { default as useExtracted } from './react-server/useExtracted.js';
|
|
8
9
|
export * from 'use-intl/core';
|
|
@@ -121,9 +121,13 @@ function getRouteParams(template, pathname) {
|
|
|
121
121
|
const match = regex.exec(normalizedPathname);
|
|
122
122
|
if (!match) return undefined;
|
|
123
123
|
const params = {};
|
|
124
|
+
const keys = normalizedTemplate.match(/\[([^\]]+)\]/g) ?? [];
|
|
124
125
|
for (let i = 1; i < match.length; i++) {
|
|
125
|
-
const
|
|
126
|
-
if (
|
|
126
|
+
const rawKey = keys[i - 1];
|
|
127
|
+
if (!rawKey) continue;
|
|
128
|
+
const key = rawKey.replace(/[[\]]/g, '');
|
|
129
|
+
const value = match[i] ?? '';
|
|
130
|
+
params[key] = value;
|
|
127
131
|
}
|
|
128
132
|
return params;
|
|
129
133
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import createMessagesDeclaration from './createMessagesDeclaration.js';
|
|
2
1
|
import getNextConfig from './getNextConfig.js';
|
|
3
2
|
import { warn } from './utils.js';
|
|
3
|
+
import createMessagesDeclaration from './declaration/createMessagesDeclaration.js';
|
|
4
4
|
|
|
5
5
|
function initPlugin(pluginConfig, nextConfig) {
|
|
6
6
|
if (nextConfig?.i18n != null) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { throwError } from '
|
|
4
|
-
import watchFile from '
|
|
3
|
+
import { throwError } from '../utils.js';
|
|
4
|
+
import watchFile from '../watchFile.js';
|
|
5
5
|
|
|
6
6
|
function runOnce(fn) {
|
|
7
7
|
if (process.env._NEXT_INTL_COMPILE_MESSAGES === '1') {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import
|
|
3
|
+
import SourceFileFilter from '../extractor/source/SourceFileFilter.js';
|
|
4
|
+
import { isNextJs16OrHigher, hasStableTurboConfig } from './nextFlags.js';
|
|
4
5
|
import { throwError } from './utils.js';
|
|
5
6
|
|
|
6
7
|
function withExtensions(localPath) {
|
|
@@ -39,22 +40,99 @@ const withNextIntl = createNextIntlPlugin(
|
|
|
39
40
|
function getNextConfig(pluginConfig, nextConfig) {
|
|
40
41
|
const useTurbo = process.env.TURBOPACK != null;
|
|
41
42
|
const nextIntlConfig = {};
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
function getExtractMessagesLoaderConfig() {
|
|
44
|
+
const experimental = pluginConfig.experimental;
|
|
45
|
+
if (!experimental.srcPath || !experimental.messages) {
|
|
46
|
+
throwError('`srcPath` and `messages` are required when using `extractor`.');
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
loader: 'next-intl/extractor/extractionLoader',
|
|
50
|
+
options: {
|
|
51
|
+
srcPath: experimental.srcPath,
|
|
52
|
+
sourceLocale: experimental.extract.sourceLocale,
|
|
53
|
+
messages: experimental.messages
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function getCatalogLoaderConfig() {
|
|
58
|
+
return {
|
|
59
|
+
loader: 'next-intl/extractor/catalogLoader',
|
|
60
|
+
options: {
|
|
61
|
+
messages: pluginConfig.experimental.messages
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function getTurboRules() {
|
|
66
|
+
return nextConfig?.turbopack?.rules ||
|
|
67
|
+
// @ts-expect-error -- For Next.js <16
|
|
68
|
+
nextConfig?.experimental?.turbo?.rules || {};
|
|
69
|
+
}
|
|
70
|
+
function addTurboRule(rules, glob, rule) {
|
|
71
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
72
|
+
if (rules[glob]) {
|
|
73
|
+
if (Array.isArray(rules[glob])) {
|
|
74
|
+
rules[glob].push(rule);
|
|
75
|
+
} else {
|
|
76
|
+
rules[glob] = [rules[glob], rule];
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
rules[glob] = rule;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
44
82
|
if (useTurbo) {
|
|
45
|
-
if (pluginConfig.requestConfig
|
|
83
|
+
if (pluginConfig.requestConfig && path.isAbsolute(pluginConfig.requestConfig)) {
|
|
46
84
|
throwError("Turbopack support for next-intl currently does not support absolute paths, please provide a relative one (e.g. './src/i18n/config.ts').\n\nFound: " + pluginConfig.requestConfig);
|
|
47
85
|
}
|
|
86
|
+
|
|
87
|
+
// Assign alias for `next-intl/config`
|
|
48
88
|
const resolveAlias = {
|
|
49
89
|
// Turbo aliases don't work with absolute
|
|
50
90
|
// paths (see error handling above)
|
|
51
91
|
'next-intl/config': resolveI18nPath(pluginConfig.requestConfig)
|
|
52
92
|
};
|
|
53
|
-
|
|
93
|
+
|
|
94
|
+
// Add loaders
|
|
95
|
+
let rules;
|
|
96
|
+
|
|
97
|
+
// Add loader for extractor
|
|
98
|
+
if (pluginConfig.experimental?.extract) {
|
|
99
|
+
if (!isNextJs16OrHigher()) {
|
|
100
|
+
throwError('Message extraction requires Next.js 16 or higher.');
|
|
101
|
+
}
|
|
102
|
+
rules ??= getTurboRules();
|
|
103
|
+
addTurboRule(rules, `*.{${SourceFileFilter.EXTENSIONS.join(',')}}`, {
|
|
104
|
+
loaders: [getExtractMessagesLoaderConfig()],
|
|
105
|
+
condition: {
|
|
106
|
+
// Note: We don't need `not: 'foreign'`, because this is
|
|
107
|
+
// implied by the filter based on `srcPath`.
|
|
108
|
+
path: (Array.isArray(pluginConfig.experimental.srcPath) ? `{${pluginConfig.experimental.srcPath.join(',')}}` : pluginConfig.experimental.srcPath) + '/**/*',
|
|
109
|
+
content: /(useExtracted|getExtracted)/
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Add loader for catalog
|
|
115
|
+
if (pluginConfig.experimental?.messages) {
|
|
116
|
+
if (!isNextJs16OrHigher()) {
|
|
117
|
+
throwError('Message catalog loading requires Next.js 16 or higher.');
|
|
118
|
+
}
|
|
119
|
+
rules ??= getTurboRules();
|
|
120
|
+
addTurboRule(rules, `*.${pluginConfig.experimental.messages.format}`, {
|
|
121
|
+
loaders: [getCatalogLoaderConfig()],
|
|
122
|
+
condition: {
|
|
123
|
+
path: `${pluginConfig.experimental.messages.path}/**/*`
|
|
124
|
+
},
|
|
125
|
+
as: '*.js'
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
if (hasStableTurboConfig() &&
|
|
54
129
|
// @ts-expect-error -- For Next.js <16
|
|
55
130
|
!nextConfig?.experimental?.turbo) {
|
|
56
131
|
nextIntlConfig.turbopack = {
|
|
57
132
|
...nextConfig?.turbopack,
|
|
133
|
+
...(rules && {
|
|
134
|
+
rules
|
|
135
|
+
}),
|
|
58
136
|
resolveAlias: {
|
|
59
137
|
...nextConfig?.turbopack?.resolveAlias,
|
|
60
138
|
...resolveAlias
|
|
@@ -67,6 +145,9 @@ function getNextConfig(pluginConfig, nextConfig) {
|
|
|
67
145
|
turbo: {
|
|
68
146
|
// @ts-expect-error -- For Next.js <16
|
|
69
147
|
...nextConfig?.experimental?.turbo,
|
|
148
|
+
...(rules && {
|
|
149
|
+
rules
|
|
150
|
+
}),
|
|
70
151
|
resolveAlias: {
|
|
71
152
|
// @ts-expect-error -- For Next.js <16
|
|
72
153
|
...nextConfig?.experimental?.turbo?.resolveAlias,
|
|
@@ -76,11 +157,39 @@ function getNextConfig(pluginConfig, nextConfig) {
|
|
|
76
157
|
};
|
|
77
158
|
}
|
|
78
159
|
} else {
|
|
79
|
-
nextIntlConfig.webpack = function webpack(
|
|
80
|
-
|
|
160
|
+
nextIntlConfig.webpack = function webpack(config, context) {
|
|
161
|
+
if (!config.resolve) config.resolve = {};
|
|
162
|
+
if (!config.resolve.alias) config.resolve.alias = {};
|
|
163
|
+
|
|
164
|
+
// Assign alias for `next-intl/config`
|
|
165
|
+
// (Webpack requires absolute paths)
|
|
81
166
|
config.resolve.alias['next-intl/config'] = path.resolve(config.context, resolveI18nPath(pluginConfig.requestConfig, config.context));
|
|
167
|
+
|
|
168
|
+
// Add loader for extractor
|
|
169
|
+
if (pluginConfig.experimental?.extract) {
|
|
170
|
+
if (!config.module) config.module = {};
|
|
171
|
+
if (!config.module.rules) config.module.rules = [];
|
|
172
|
+
const srcPath = pluginConfig.experimental.srcPath;
|
|
173
|
+
config.module.rules.push({
|
|
174
|
+
test: new RegExp(`\\.(${SourceFileFilter.EXTENSIONS.join('|')})$`),
|
|
175
|
+
include: Array.isArray(srcPath) ? srcPath.map(cur => path.resolve(config.context, cur)) : path.resolve(config.context, srcPath || ''),
|
|
176
|
+
use: [getExtractMessagesLoaderConfig()]
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Add loader for catalog
|
|
181
|
+
if (pluginConfig.experimental?.messages) {
|
|
182
|
+
if (!config.module) config.module = {};
|
|
183
|
+
if (!config.module.rules) config.module.rules = [];
|
|
184
|
+
config.module.rules.push({
|
|
185
|
+
test: new RegExp(`\\.${pluginConfig.experimental.messages.format}$`),
|
|
186
|
+
include: path.resolve(config.context, pluginConfig.experimental.messages.path),
|
|
187
|
+
use: [getCatalogLoaderConfig()],
|
|
188
|
+
type: 'javascript/auto'
|
|
189
|
+
});
|
|
190
|
+
}
|
|
82
191
|
if (typeof nextConfig?.webpack === 'function') {
|
|
83
|
-
return nextConfig.webpack(config,
|
|
192
|
+
return nextConfig.webpack(config, context);
|
|
84
193
|
}
|
|
85
194
|
return config;
|
|
86
195
|
};
|
|
@@ -22,6 +22,11 @@ function compareVersions(version1, version2) {
|
|
|
22
22
|
}
|
|
23
23
|
return 0;
|
|
24
24
|
}
|
|
25
|
-
|
|
25
|
+
function hasStableTurboConfig() {
|
|
26
|
+
return compareVersions(getCurrentVersion(), '15.3.0') >= 0;
|
|
27
|
+
}
|
|
28
|
+
function isNextJs16OrHigher() {
|
|
29
|
+
return compareVersions(getCurrentVersion(), '16.0.0') >= 0;
|
|
30
|
+
}
|
|
26
31
|
|
|
27
|
-
export { hasStableTurboConfig
|
|
32
|
+
export { hasStableTurboConfig, isNextJs16OrHigher };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import getServerExtractor from '../server/react-server/getServerExtractor.js';
|
|
2
|
+
import useConfig from './useConfig.js';
|
|
3
|
+
|
|
4
|
+
function useExtracted(namespace) {
|
|
5
|
+
const config = useConfig('useExtracted');
|
|
6
|
+
return getServerExtractor(config, namespace);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export { useExtracted as default };
|
|
@@ -21,6 +21,7 @@ const getNow = notSupported('getNow');
|
|
|
21
21
|
const getTimeZone = notSupported('getTimeZone');
|
|
22
22
|
const getMessages = notSupported('getMessages');
|
|
23
23
|
const getLocale = notSupported('getLocale');
|
|
24
|
+
const getExtracted = notSupported('getExtracted');
|
|
24
25
|
|
|
25
26
|
// The type of `getTranslations` is not assigned here because it
|
|
26
27
|
// causes a type error. The types use the `react-server` entry
|
|
@@ -28,4 +29,4 @@ const getLocale = notSupported('getLocale');
|
|
|
28
29
|
const getTranslations = notSupported('getTranslations');
|
|
29
30
|
const setRequestLocale = notSupported('setRequestLocale');
|
|
30
31
|
|
|
31
|
-
export { getFormatter, getLocale, getMessages, getNow, getRequestConfig, getTimeZone, getTranslations, setRequestLocale };
|
|
32
|
+
export { getExtracted, getFormatter, getLocale, getMessages, getNow, getRequestConfig, getTimeZone, getTranslations, setRequestLocale };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { cache } from 'react';
|
|
2
|
+
import getConfig from './getConfig.js';
|
|
3
|
+
import getServerExtractor from './getServerExtractor.js';
|
|
4
|
+
|
|
5
|
+
// Call signature 1: `getExtracted(namespace)`
|
|
6
|
+
|
|
7
|
+
// Call signature 2: `getExtracted({locale, namespace})`
|
|
8
|
+
|
|
9
|
+
// Implementation
|
|
10
|
+
async function getExtractedImpl(namespaceOrOpts) {
|
|
11
|
+
let namespace;
|
|
12
|
+
let locale;
|
|
13
|
+
if (typeof namespaceOrOpts === 'string') {
|
|
14
|
+
namespace = namespaceOrOpts;
|
|
15
|
+
} else if (namespaceOrOpts) {
|
|
16
|
+
locale = namespaceOrOpts.locale;
|
|
17
|
+
namespace = namespaceOrOpts.namespace;
|
|
18
|
+
}
|
|
19
|
+
const config = await getConfig(locale);
|
|
20
|
+
return getServerExtractor(config, namespace);
|
|
21
|
+
}
|
|
22
|
+
const getExtracted = cache(getExtractedImpl);
|
|
23
|
+
|
|
24
|
+
export { getExtracted as default };
|