next-intl 4.4.0 → 4.5.0
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/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/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/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/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,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* De-duplicates excessive save invocations,
|
|
3
|
+
* while keeping a single one instant.
|
|
4
|
+
*/
|
|
5
|
+
class SaveScheduler {
|
|
6
|
+
isSaving = false;
|
|
7
|
+
pendingResolvers = [];
|
|
8
|
+
constructor(delayMs = 50) {
|
|
9
|
+
this.delayMs = delayMs;
|
|
10
|
+
}
|
|
11
|
+
async schedule(saveTask) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
this.pendingResolvers.push({
|
|
14
|
+
resolve,
|
|
15
|
+
reject
|
|
16
|
+
});
|
|
17
|
+
if (this.pendingResolvers.length === 1 && !this.isSaving) {
|
|
18
|
+
// No pending saves and not currently saving, save immediately
|
|
19
|
+
this.executeSave(saveTask);
|
|
20
|
+
} else if (this.pendingResolvers.length > 1) {
|
|
21
|
+
// Multiple pending saves, schedule/reschedule save
|
|
22
|
+
this.scheduleSave(saveTask);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
scheduleSave(saveTask) {
|
|
27
|
+
if (this.saveTimeout) {
|
|
28
|
+
clearTimeout(this.saveTimeout);
|
|
29
|
+
}
|
|
30
|
+
this.saveTimeout = setTimeout(() => {
|
|
31
|
+
this.executeSave(saveTask);
|
|
32
|
+
}, this.delayMs);
|
|
33
|
+
}
|
|
34
|
+
async executeSave(saveTask) {
|
|
35
|
+
if (this.isSaving) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
this.isSaving = true;
|
|
39
|
+
try {
|
|
40
|
+
const result = await saveTask();
|
|
41
|
+
|
|
42
|
+
// Resolve all pending promises with the same result
|
|
43
|
+
this.pendingResolvers.forEach(({
|
|
44
|
+
resolve
|
|
45
|
+
}) => resolve(result));
|
|
46
|
+
} catch (error) {
|
|
47
|
+
// Reject all pending promises with the same error
|
|
48
|
+
this.pendingResolvers.forEach(({
|
|
49
|
+
reject
|
|
50
|
+
}) => reject(error));
|
|
51
|
+
} finally {
|
|
52
|
+
this.pendingResolvers = [];
|
|
53
|
+
this.isSaving = false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
destroy() {
|
|
57
|
+
if (this.saveTimeout) {
|
|
58
|
+
clearTimeout(this.saveTimeout);
|
|
59
|
+
this.saveTimeout = undefined;
|
|
60
|
+
}
|
|
61
|
+
this.pendingResolvers = [];
|
|
62
|
+
this.isSaving = false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export { SaveScheduler as default };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import formatters from './formatters/index.js';
|
|
3
|
+
|
|
4
|
+
let cachedFormatter = null;
|
|
5
|
+
async function getFormatter(options) {
|
|
6
|
+
if (!cachedFormatter) {
|
|
7
|
+
const FormatterClass = (await formatters[options.messages.format]()).default;
|
|
8
|
+
cachedFormatter = new FormatterClass();
|
|
9
|
+
}
|
|
10
|
+
return cachedFormatter;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Parses and optimizes catalog files.
|
|
15
|
+
*
|
|
16
|
+
* Note that if we use a dynamic import like `import(`${locale}.json`)`, then
|
|
17
|
+
* the loader will optimistically run for all candidates in this folder (both
|
|
18
|
+
* during dev as well as at build time).
|
|
19
|
+
*/
|
|
20
|
+
function catalogLoader(source) {
|
|
21
|
+
const options = this.getOptions();
|
|
22
|
+
const callback = this.async();
|
|
23
|
+
getFormatter(options).then(formatter => {
|
|
24
|
+
const locale = path.basename(this.resourcePath, formatter.EXTENSION);
|
|
25
|
+
const jsonString = formatter.toJSONString(source, {
|
|
26
|
+
locale
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// https://v8.dev/blog/cost-of-javascript-2019#json
|
|
30
|
+
const result = `export default JSON.parse(${JSON.stringify(jsonString)});`;
|
|
31
|
+
callback(null, result);
|
|
32
|
+
}).catch(callback);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export { catalogLoader as default };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import ExtractionCompiler from './ExtractionCompiler.js';
|
|
2
|
+
|
|
3
|
+
// This instance:
|
|
4
|
+
// - Remains available through HMR
|
|
5
|
+
// - Is the same across react-client and react-server
|
|
6
|
+
// - Is only lost when the dev server restarts (e.g. due to change to Next.js config)
|
|
7
|
+
let compiler;
|
|
8
|
+
function extractionLoader(source) {
|
|
9
|
+
const options = this.getOptions();
|
|
10
|
+
const callback = this.async();
|
|
11
|
+
if (!compiler) {
|
|
12
|
+
compiler = new ExtractionCompiler(options, {
|
|
13
|
+
// Avoid rollup's `replace` plugin to compile this away
|
|
14
|
+
isDevelopment: process.env['NODE_ENV'.trim()] === 'development'
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
compiler.compile(this.resourcePath, source).then(result => {
|
|
18
|
+
callback(null, result);
|
|
19
|
+
}).catch(callback);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export { extractionLoader as default };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class ASTScope {
|
|
2
|
+
vars = (() => new Map())();
|
|
3
|
+
constructor(parent) {
|
|
4
|
+
this.parent = parent;
|
|
5
|
+
}
|
|
6
|
+
define(name, kind, namespace) {
|
|
7
|
+
this.vars.set(name, {
|
|
8
|
+
kind,
|
|
9
|
+
namespace
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
lookup(name) {
|
|
13
|
+
if (this.vars.has(name)) return this.vars.get(name);
|
|
14
|
+
return this.parent?.lookup(name);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { ASTScope as default };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
|
|
3
|
+
class KeyGenerator {
|
|
4
|
+
static generate(message) {
|
|
5
|
+
const hash = crypto.createHash('sha512').update(message).digest();
|
|
6
|
+
const base64 = hash.toString('base64');
|
|
7
|
+
return base64.slice(0, 6);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export { KeyGenerator as default };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
class LRUCache {
|
|
2
|
+
constructor(maxSize) {
|
|
3
|
+
this.maxSize = maxSize;
|
|
4
|
+
this.cache = new Map();
|
|
5
|
+
}
|
|
6
|
+
set(key, value) {
|
|
7
|
+
const isNewKey = !this.cache.has(key);
|
|
8
|
+
if (isNewKey && this.cache.size >= this.maxSize) {
|
|
9
|
+
const lruKey = this.cache.keys().next().value;
|
|
10
|
+
if (lruKey !== undefined) {
|
|
11
|
+
this.cache.delete(lruKey);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
this.cache.set(key, {
|
|
15
|
+
key,
|
|
16
|
+
value
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
get(key) {
|
|
20
|
+
const item = this.cache.get(key);
|
|
21
|
+
if (item) {
|
|
22
|
+
this.cache.delete(key);
|
|
23
|
+
this.cache.set(key, item);
|
|
24
|
+
return item.value;
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export { LRUCache as default };
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { parse, print } from '@swc/core';
|
|
3
|
+
import { warn } from '../../plugin/utils.js';
|
|
4
|
+
import ASTScope from './ASTScope.js';
|
|
5
|
+
import KeyGenerator from './KeyGenerator.js';
|
|
6
|
+
import LRUCache from './LRUCache.js';
|
|
7
|
+
|
|
8
|
+
class MessageExtractor {
|
|
9
|
+
static NAMESPACE_SEPARATOR = '.';
|
|
10
|
+
compileCache = (() => new LRUCache(750))();
|
|
11
|
+
constructor(opts) {
|
|
12
|
+
this.isDevelopment = opts.isDevelopment;
|
|
13
|
+
this.projectRoot = opts.projectRoot;
|
|
14
|
+
}
|
|
15
|
+
async processFileContent(absoluteFilePath, source) {
|
|
16
|
+
const cacheKey = source;
|
|
17
|
+
const cached = this.compileCache.get(cacheKey);
|
|
18
|
+
if (cached) return cached;
|
|
19
|
+
|
|
20
|
+
// Shortcut parsing if hook is not used. The Turbopack integration already
|
|
21
|
+
// pre-filters this, but for webpack this feature doesn't exist, so we need
|
|
22
|
+
// to do it here.
|
|
23
|
+
if (!source.includes('useExtracted') && !source.includes('getExtracted')) {
|
|
24
|
+
return {
|
|
25
|
+
messages: [],
|
|
26
|
+
source
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const ast = await parse(source, {
|
|
30
|
+
syntax: 'typescript',
|
|
31
|
+
tsx: true,
|
|
32
|
+
target: 'es2022',
|
|
33
|
+
decorators: true
|
|
34
|
+
});
|
|
35
|
+
const relativeFilePath = path.relative(this.projectRoot, absoluteFilePath);
|
|
36
|
+
const processResult = await this.processAST(ast, relativeFilePath);
|
|
37
|
+
const finalResult = processResult.source ? processResult : {
|
|
38
|
+
...processResult,
|
|
39
|
+
source
|
|
40
|
+
};
|
|
41
|
+
this.compileCache.set(cacheKey, finalResult);
|
|
42
|
+
return finalResult;
|
|
43
|
+
}
|
|
44
|
+
async processAST(ast, filePath) {
|
|
45
|
+
const results = [];
|
|
46
|
+
let hookLocalName = null;
|
|
47
|
+
let hookType = null;
|
|
48
|
+
const isDevelopment = this.isDevelopment;
|
|
49
|
+
const scopeStack = [new ASTScope()];
|
|
50
|
+
function currentScope() {
|
|
51
|
+
return scopeStack[scopeStack.length - 1];
|
|
52
|
+
}
|
|
53
|
+
function createUndefinedArgument() {
|
|
54
|
+
return {
|
|
55
|
+
expression: {
|
|
56
|
+
type: 'Identifier',
|
|
57
|
+
value: 'undefined',
|
|
58
|
+
optional: false,
|
|
59
|
+
ctxt: 1,
|
|
60
|
+
span: {
|
|
61
|
+
start: 0,
|
|
62
|
+
end: 0,
|
|
63
|
+
ctxt: 0
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function extractStaticString(value) {
|
|
69
|
+
if (value.type === 'StringLiteral') {
|
|
70
|
+
return value.value;
|
|
71
|
+
} else if (value.type === 'TemplateLiteral') {
|
|
72
|
+
const templateLiteral = value;
|
|
73
|
+
// Only handle simple template literals without expressions
|
|
74
|
+
if (templateLiteral.expressions.length === 0 && templateLiteral.quasis.length === 1) {
|
|
75
|
+
return templateLiteral.quasis[0].cooked || templateLiteral.quasis[0].raw;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
function visit(node) {
|
|
81
|
+
if (typeof node !== 'object') return;
|
|
82
|
+
switch (node.type) {
|
|
83
|
+
case 'ImportDeclaration':
|
|
84
|
+
{
|
|
85
|
+
const decl = node;
|
|
86
|
+
if (decl.source.value === 'next-intl') {
|
|
87
|
+
for (const spec of decl.specifiers) {
|
|
88
|
+
if (spec.type === 'ImportSpecifier') {
|
|
89
|
+
const importedName = spec.imported?.value;
|
|
90
|
+
const localName = spec.local.value;
|
|
91
|
+
if (importedName === 'useExtracted' || localName === 'useExtracted') {
|
|
92
|
+
hookLocalName = localName;
|
|
93
|
+
hookType = 'useTranslations';
|
|
94
|
+
|
|
95
|
+
// Transform import to useTranslations
|
|
96
|
+
spec.imported = undefined;
|
|
97
|
+
spec.local.value = 'useTranslations';
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} else if (decl.source.value === 'next-intl/server') {
|
|
102
|
+
for (const spec of decl.specifiers) {
|
|
103
|
+
if (spec.type === 'ImportSpecifier') {
|
|
104
|
+
const importedName = spec.imported?.value;
|
|
105
|
+
const localName = spec.local.value;
|
|
106
|
+
if (importedName === 'getExtracted' || localName === 'getExtracted') {
|
|
107
|
+
hookLocalName = localName;
|
|
108
|
+
hookType = 'getTranslations';
|
|
109
|
+
|
|
110
|
+
// Transform import to getTranslations
|
|
111
|
+
spec.imported = undefined;
|
|
112
|
+
spec.local.value = 'getTranslations';
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
case 'VariableDeclarator':
|
|
120
|
+
{
|
|
121
|
+
const decl = node;
|
|
122
|
+
let callExpr = null;
|
|
123
|
+
|
|
124
|
+
// Handle direct CallExpression: const t = useExtracted();
|
|
125
|
+
if (decl.init?.type === 'CallExpression' && decl.init.callee.type === 'Identifier' && decl.init.callee.value === hookLocalName) {
|
|
126
|
+
callExpr = decl.init;
|
|
127
|
+
}
|
|
128
|
+
// Handle AwaitExpression: const t = await getExtracted();
|
|
129
|
+
else if (decl.init?.type === 'AwaitExpression' && decl.init.argument.type === 'CallExpression' && decl.init.argument.callee.type === 'Identifier' && decl.init.argument.callee.value === hookLocalName) {
|
|
130
|
+
callExpr = decl.init.argument;
|
|
131
|
+
}
|
|
132
|
+
if (callExpr && decl.id.type === 'Identifier') {
|
|
133
|
+
// Extract namespace from first argument if present
|
|
134
|
+
let namespace;
|
|
135
|
+
if (callExpr.arguments.length > 0) {
|
|
136
|
+
const firstArg = callExpr.arguments[0].expression;
|
|
137
|
+
if (firstArg.type === 'StringLiteral') {
|
|
138
|
+
namespace = firstArg.value;
|
|
139
|
+
} else if (firstArg.type === 'ObjectExpression') {
|
|
140
|
+
const objectExpression = firstArg;
|
|
141
|
+
for (const prop of objectExpression.properties) {
|
|
142
|
+
if (prop.type === 'KeyValueProperty') {
|
|
143
|
+
const key = prop.key;
|
|
144
|
+
if (key.type === 'Identifier' && key.value === 'namespace') {
|
|
145
|
+
const staticNamespace = extractStaticString(prop.value);
|
|
146
|
+
if (staticNamespace != null) {
|
|
147
|
+
namespace = staticNamespace;
|
|
148
|
+
}
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
currentScope().define(decl.id.value, 'translator', namespace);
|
|
156
|
+
|
|
157
|
+
// Transform the call based on the hook type
|
|
158
|
+
if (hookType) {
|
|
159
|
+
callExpr.callee.value = hookType;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
case 'CallExpression':
|
|
165
|
+
{
|
|
166
|
+
const call = node;
|
|
167
|
+
let isTranslatorCall = false;
|
|
168
|
+
let namespace;
|
|
169
|
+
|
|
170
|
+
// Handle Identifier case: t("message")
|
|
171
|
+
if (call.callee.type === 'Identifier') {
|
|
172
|
+
const name = call.callee.value;
|
|
173
|
+
const resolved = currentScope().lookup(name);
|
|
174
|
+
isTranslatorCall = resolved?.kind === 'translator';
|
|
175
|
+
namespace = resolved?.namespace;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Handle MemberExpression case: t.rich, t.markup, or t.has
|
|
179
|
+
else if (call.callee.type === 'MemberExpression') {
|
|
180
|
+
const memberExpr = call.callee;
|
|
181
|
+
if (memberExpr.object.type === 'Identifier' && memberExpr.property.type === 'Identifier') {
|
|
182
|
+
const objectName = memberExpr.object.value;
|
|
183
|
+
const propertyName = memberExpr.property.value;
|
|
184
|
+
const resolved = currentScope().lookup(objectName);
|
|
185
|
+
isTranslatorCall = resolved?.kind === 'translator' && (propertyName === 'rich' || propertyName === 'markup' || propertyName === 'has');
|
|
186
|
+
namespace = resolved?.namespace;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (isTranslatorCall) {
|
|
190
|
+
const arg0 = call.arguments[0]?.expression;
|
|
191
|
+
let messageText = null;
|
|
192
|
+
let explicitId = null;
|
|
193
|
+
let description = null;
|
|
194
|
+
let valuesNode = null;
|
|
195
|
+
let formatsNode = null;
|
|
196
|
+
function warnDynamicExpression(expressionNode) {
|
|
197
|
+
const hasSpan = 'span' in expressionNode && expressionNode.span && typeof expressionNode.span === 'object' && 'start' in expressionNode.span;
|
|
198
|
+
const location = hasSpan ? path.basename(filePath) : undefined;
|
|
199
|
+
warn((location ? `${location}: ` : '') + 'Cannot extract message from dynamic expression, messages need to be statically analyzable. If you need to provide runtime values, pass them as a separate argument.');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
203
|
+
if (arg0) {
|
|
204
|
+
// Handle object syntax: t({id: 'key', message: 'text'})
|
|
205
|
+
if (arg0.type === 'ObjectExpression') {
|
|
206
|
+
const objectExpression = arg0;
|
|
207
|
+
|
|
208
|
+
// Look for id, message, values, and formats properties
|
|
209
|
+
for (const prop of objectExpression.properties) {
|
|
210
|
+
if (prop.type === 'KeyValueProperty') {
|
|
211
|
+
const key = prop.key;
|
|
212
|
+
if (key.type === 'Identifier' && key.value === 'id') {
|
|
213
|
+
const staticId = extractStaticString(prop.value);
|
|
214
|
+
if (staticId !== null) {
|
|
215
|
+
explicitId = staticId;
|
|
216
|
+
}
|
|
217
|
+
} else if (key.type === 'Identifier' && key.value === 'message') {
|
|
218
|
+
const staticMessage = extractStaticString(prop.value);
|
|
219
|
+
if (staticMessage != null) {
|
|
220
|
+
messageText = staticMessage;
|
|
221
|
+
} else {
|
|
222
|
+
warnDynamicExpression(prop.value);
|
|
223
|
+
}
|
|
224
|
+
} else if (key.type === 'Identifier' && key.value === 'description') {
|
|
225
|
+
const staticDescription = extractStaticString(prop.value);
|
|
226
|
+
if (staticDescription != null) {
|
|
227
|
+
description = staticDescription;
|
|
228
|
+
} else {
|
|
229
|
+
warnDynamicExpression(prop.value);
|
|
230
|
+
}
|
|
231
|
+
} else if (key.type === 'Identifier' && key.value === 'values') {
|
|
232
|
+
valuesNode = prop.value;
|
|
233
|
+
} else if (key.type === 'Identifier' && key.value === 'formats') {
|
|
234
|
+
formatsNode = prop.value;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Handle string syntax: t('text') or t(`text`)
|
|
241
|
+
else {
|
|
242
|
+
const staticString = extractStaticString(arg0);
|
|
243
|
+
if (staticString != null) {
|
|
244
|
+
messageText = staticString;
|
|
245
|
+
} else {
|
|
246
|
+
// Dynamic expression (Identifier, CallExpression, BinaryExpression, etc.)
|
|
247
|
+
warnDynamicExpression(arg0);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (messageText) {
|
|
252
|
+
const callKey = explicitId || KeyGenerator.generate(messageText);
|
|
253
|
+
const fullKey = namespace ? [namespace, callKey].join(MessageExtractor.NAMESPACE_SEPARATOR) : callKey;
|
|
254
|
+
const message = {
|
|
255
|
+
id: fullKey,
|
|
256
|
+
message: messageText,
|
|
257
|
+
references: [{
|
|
258
|
+
path: filePath
|
|
259
|
+
}]
|
|
260
|
+
};
|
|
261
|
+
if (description) {
|
|
262
|
+
message.description = description;
|
|
263
|
+
}
|
|
264
|
+
results.push(message);
|
|
265
|
+
|
|
266
|
+
// Transform the argument based on type
|
|
267
|
+
if (arg0.type === 'StringLiteral') {
|
|
268
|
+
arg0.value = callKey;
|
|
269
|
+
arg0.raw = undefined;
|
|
270
|
+
} else if (arg0.type === 'TemplateLiteral') {
|
|
271
|
+
// Replace template literal with string literal
|
|
272
|
+
Object.assign(arg0, {
|
|
273
|
+
type: 'StringLiteral',
|
|
274
|
+
value: callKey,
|
|
275
|
+
raw: undefined
|
|
276
|
+
});
|
|
277
|
+
} else if (arg0.type === 'ObjectExpression') {
|
|
278
|
+
// Transform object expression to individual parameters
|
|
279
|
+
// Replace the object with the key as first argument
|
|
280
|
+
Object.assign(arg0, {
|
|
281
|
+
type: 'StringLiteral',
|
|
282
|
+
value: callKey,
|
|
283
|
+
raw: undefined
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// Add values as second argument if present
|
|
287
|
+
if (valuesNode) {
|
|
288
|
+
if (call.arguments.length < 2) {
|
|
289
|
+
call.arguments.push({
|
|
290
|
+
// @ts-expect-error -- Node type compatible with Expression
|
|
291
|
+
expression: valuesNode
|
|
292
|
+
});
|
|
293
|
+
} else {
|
|
294
|
+
// @ts-expect-error -- Node type compatible with Expression
|
|
295
|
+
call.arguments[1].expression = valuesNode;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Add formats as third argument if present
|
|
300
|
+
if (formatsNode) {
|
|
301
|
+
// Ensure we have a second argument (values or undefined)
|
|
302
|
+
while (call.arguments.length < 2) {
|
|
303
|
+
call.arguments.push(createUndefinedArgument());
|
|
304
|
+
}
|
|
305
|
+
if (call.arguments.length < 3) {
|
|
306
|
+
// Append argument
|
|
307
|
+
call.arguments.push({
|
|
308
|
+
// @ts-expect-error -- Node type compatible with Expression
|
|
309
|
+
expression: formatsNode
|
|
310
|
+
});
|
|
311
|
+
} else {
|
|
312
|
+
// Replace argument
|
|
313
|
+
// @ts-expect-error -- Node type compatible with Expression
|
|
314
|
+
call.arguments[2].expression = formatsNode;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Check if this is a t.has call (which doesn't need fallback)
|
|
320
|
+
const isHasCall = call.callee.type === 'MemberExpression' && call.callee.property.type === 'Identifier' && call.callee.property.value === 'has';
|
|
321
|
+
|
|
322
|
+
// Add fallback message as fourth parameter in development mode (except for t.has)
|
|
323
|
+
if (isDevelopment && !isHasCall) {
|
|
324
|
+
// Ensure we have at least 3 arguments
|
|
325
|
+
while (call.arguments.length < 3) {
|
|
326
|
+
call.arguments.push(createUndefinedArgument());
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Add fallback message
|
|
330
|
+
call.arguments.push({
|
|
331
|
+
expression: {
|
|
332
|
+
type: 'StringLiteral',
|
|
333
|
+
value: messageText,
|
|
334
|
+
raw: JSON.stringify(messageText),
|
|
335
|
+
// @ts-expect-error -- Seems required
|
|
336
|
+
ctxt: 1,
|
|
337
|
+
span: {
|
|
338
|
+
start: 0,
|
|
339
|
+
end: 0,
|
|
340
|
+
ctxt: 0
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
case 'FunctionDeclaration':
|
|
350
|
+
case 'FunctionExpression':
|
|
351
|
+
case 'ArrowFunctionExpression':
|
|
352
|
+
case 'BlockStatement':
|
|
353
|
+
{
|
|
354
|
+
scopeStack.push(new ASTScope(currentScope()));
|
|
355
|
+
for (const key of Object.keys(node)) {
|
|
356
|
+
const child = node[key];
|
|
357
|
+
if (Array.isArray(child)) {
|
|
358
|
+
child.forEach(item => {
|
|
359
|
+
if (item && typeof item === 'object') {
|
|
360
|
+
if ('expression' in item && typeof item.expression === 'object' && 'type' in item.expression) {
|
|
361
|
+
visit(item.expression);
|
|
362
|
+
} else if ('type' in item) {
|
|
363
|
+
visit(item);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
} else if (child && typeof child === 'object' && 'type' in child) {
|
|
368
|
+
visit(child);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
scopeStack.pop();
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Generic recursion
|
|
377
|
+
for (const key of Object.keys(node)) {
|
|
378
|
+
const child = node[key];
|
|
379
|
+
if (Array.isArray(child)) {
|
|
380
|
+
child.forEach(item => {
|
|
381
|
+
if (item && typeof item === 'object') {
|
|
382
|
+
if ('expression' in item && item.expression && typeof item.expression === 'object' && 'type' in item.expression) {
|
|
383
|
+
visit(item.expression);
|
|
384
|
+
} else if ('type' in item) {
|
|
385
|
+
visit(item);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
} else if (child && typeof child === 'object' && 'type' in child) {
|
|
390
|
+
visit(child);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
visit(ast);
|
|
395
|
+
return {
|
|
396
|
+
messages: results,
|
|
397
|
+
source: (await print(ast)).code
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export { MessageExtractor as default };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { setNestedProperty } from '../utils/ObjectUtils.js';
|
|
2
|
+
import Formatter from './Formatter.js';
|
|
3
|
+
import { getSortedMessages } from './utils.js';
|
|
4
|
+
|
|
5
|
+
class JSONFormatter extends Formatter {
|
|
6
|
+
static NAMESPACE_SEPARATOR = '.';
|
|
7
|
+
EXTENSION = '.json';
|
|
8
|
+
parse(source) {
|
|
9
|
+
const json = JSON.parse(source);
|
|
10
|
+
const messages = [];
|
|
11
|
+
this.traverseMessages(json, (message, id) => {
|
|
12
|
+
messages.push({
|
|
13
|
+
id,
|
|
14
|
+
message
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
return messages;
|
|
18
|
+
}
|
|
19
|
+
serialize(messages) {
|
|
20
|
+
const root = {};
|
|
21
|
+
for (const message of getSortedMessages(messages)) {
|
|
22
|
+
setNestedProperty(root, message.id, message.message);
|
|
23
|
+
}
|
|
24
|
+
return JSON.stringify(root, null, 2);
|
|
25
|
+
}
|
|
26
|
+
toJSONString(source) {
|
|
27
|
+
return source;
|
|
28
|
+
}
|
|
29
|
+
traverseMessages(obj, callback, path = '') {
|
|
30
|
+
for (const key of Object.keys(obj)) {
|
|
31
|
+
const newPath = path ? path + JSONFormatter.NAMESPACE_SEPARATOR + key : key;
|
|
32
|
+
const value = obj[key];
|
|
33
|
+
if (typeof value === 'string') {
|
|
34
|
+
callback(value, newPath);
|
|
35
|
+
} else if (typeof value === 'object') {
|
|
36
|
+
this.traverseMessages(value, callback, newPath);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export { JSONFormatter as default };
|