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.
Files changed (116) hide show
  1. package/dist/cjs/development/plugin.cjs +129 -8
  2. package/dist/esm/development/extractor/ExtractionCompiler.js +41 -0
  3. package/dist/esm/development/extractor/catalog/CatalogLocales.js +117 -0
  4. package/dist/esm/development/extractor/catalog/CatalogManager.js +286 -0
  5. package/dist/esm/development/extractor/catalog/CatalogPersister.js +45 -0
  6. package/dist/esm/development/extractor/catalog/SaveScheduler.js +66 -0
  7. package/dist/esm/development/extractor/catalogLoader.js +35 -0
  8. package/dist/esm/development/extractor/extractMessages.js +8 -0
  9. package/dist/esm/development/extractor/extractionLoader.js +22 -0
  10. package/dist/esm/development/extractor/extractor/ASTScope.js +18 -0
  11. package/dist/esm/development/extractor/extractor/KeyGenerator.js +11 -0
  12. package/dist/esm/development/extractor/extractor/LRUCache.js +30 -0
  13. package/dist/esm/development/extractor/extractor/MessageExtractor.js +402 -0
  14. package/dist/esm/development/extractor/formatters/Formatter.js +3 -0
  15. package/dist/esm/development/extractor/formatters/JSONFormatter.js +42 -0
  16. package/dist/esm/development/extractor/formatters/POFormatter.js +51 -0
  17. package/dist/esm/development/extractor/formatters/index.js +6 -0
  18. package/dist/esm/development/extractor/formatters/utils.js +13 -0
  19. package/dist/esm/development/extractor/source/SourceFileFilter.js +11 -0
  20. package/dist/esm/development/extractor/source/SourceFileScanner.js +27 -0
  21. package/dist/esm/development/extractor/utils/ObjectUtils.js +14 -0
  22. package/dist/esm/development/extractor/utils/POParser.js +222 -0
  23. package/dist/esm/development/extractor.js +1 -0
  24. package/dist/esm/development/index.react-client.js +2 -1
  25. package/dist/esm/development/index.react-server.js +1 -0
  26. package/dist/esm/development/plugin/createNextIntlPlugin.js +1 -1
  27. package/dist/esm/development/plugin/{createMessagesDeclaration.js → declaration/createMessagesDeclaration.js} +2 -2
  28. package/dist/esm/development/plugin/getNextConfig.js +117 -8
  29. package/dist/esm/development/plugin/{hasStableTurboConfig.js → nextFlags.js} +7 -2
  30. package/dist/esm/development/react-client/index.js +0 -1
  31. package/dist/esm/development/react-server/useExtracted.js +9 -0
  32. package/dist/esm/development/server/react-client/index.js +2 -1
  33. package/dist/esm/development/server/react-server/getExtracted.js +24 -0
  34. package/dist/esm/development/server/react-server/getServerExtractor.js +38 -0
  35. package/dist/esm/development/server.react-client.js +1 -1
  36. package/dist/esm/development/server.react-server.js +1 -0
  37. package/dist/esm/production/extractor/ExtractionCompiler.js +1 -0
  38. package/dist/esm/production/extractor/catalog/CatalogLocales.js +1 -0
  39. package/dist/esm/production/extractor/catalog/CatalogManager.js +1 -0
  40. package/dist/esm/production/extractor/catalog/CatalogPersister.js +1 -0
  41. package/dist/esm/production/extractor/catalog/SaveScheduler.js +1 -0
  42. package/dist/esm/production/extractor/catalogLoader.js +1 -0
  43. package/dist/esm/production/extractor/extractMessages.js +1 -0
  44. package/dist/esm/production/extractor/extractionLoader.js +1 -0
  45. package/dist/esm/production/extractor/extractor/ASTScope.js +1 -0
  46. package/dist/esm/production/extractor/extractor/KeyGenerator.js +1 -0
  47. package/dist/esm/production/extractor/extractor/LRUCache.js +1 -0
  48. package/dist/esm/production/extractor/extractor/MessageExtractor.js +1 -0
  49. package/dist/esm/production/extractor/formatters/Formatter.js +1 -0
  50. package/dist/esm/production/extractor/formatters/JSONFormatter.js +1 -0
  51. package/dist/esm/production/extractor/formatters/POFormatter.js +1 -0
  52. package/dist/esm/production/extractor/formatters/index.js +1 -0
  53. package/dist/esm/production/extractor/formatters/utils.js +1 -0
  54. package/dist/esm/production/extractor/source/SourceFileFilter.js +1 -0
  55. package/dist/esm/production/extractor/source/SourceFileScanner.js +1 -0
  56. package/dist/esm/production/extractor/utils/ObjectUtils.js +1 -0
  57. package/dist/esm/production/extractor/utils/POParser.js +1 -0
  58. package/dist/esm/production/extractor.js +1 -0
  59. package/dist/esm/production/index.react-client.js +1 -1
  60. package/dist/esm/production/index.react-server.js +1 -1
  61. package/dist/esm/production/plugin/createNextIntlPlugin.js +1 -1
  62. package/dist/esm/production/plugin/declaration/createMessagesDeclaration.js +1 -0
  63. package/dist/esm/production/plugin/getNextConfig.js +1 -1
  64. package/dist/esm/production/plugin/nextFlags.js +1 -0
  65. package/dist/esm/production/react-client/index.js +1 -1
  66. package/dist/esm/production/react-server/useExtracted.js +1 -0
  67. package/dist/esm/production/server/react-client/index.js +1 -1
  68. package/dist/esm/production/server/react-server/getExtracted.js +1 -0
  69. package/dist/esm/production/server/react-server/getServerExtractor.js +1 -0
  70. package/dist/esm/production/server.react-client.js +1 -1
  71. package/dist/esm/production/server.react-server.js +1 -1
  72. package/dist/types/extractor/ExtractionCompiler.d.ts +14 -0
  73. package/dist/types/extractor/catalog/CatalogLocales.d.ts +31 -0
  74. package/dist/types/extractor/catalog/CatalogManager.d.ts +46 -0
  75. package/dist/types/extractor/catalog/CatalogPersister.d.ts +11 -0
  76. package/dist/types/extractor/catalog/SaveScheduler.d.ts +17 -0
  77. package/dist/types/extractor/extractMessages.d.ts +2 -0
  78. package/dist/types/extractor/extractor/ASTScope.d.ts +12 -0
  79. package/dist/types/extractor/extractor/KeyGenerator.d.ts +3 -0
  80. package/dist/types/extractor/extractor/LRUCache.d.ts +7 -0
  81. package/dist/types/extractor/extractor/MessageExtractor.d.ts +20 -0
  82. package/dist/types/extractor/formatters/Formatter.d.ts +10 -0
  83. package/dist/types/extractor/formatters/JSONFormatter.d.ts +10 -0
  84. package/dist/types/extractor/formatters/POFormatter.d.ts +10 -0
  85. package/dist/types/extractor/formatters/index.d.ts +5 -0
  86. package/dist/types/extractor/formatters/utils.d.ts +2 -0
  87. package/dist/types/extractor/index.d.ts +1 -0
  88. package/dist/types/extractor/source/SourceFileFilter.d.ts +4 -0
  89. package/dist/types/extractor/source/SourceFileScanner.d.ts +4 -0
  90. package/dist/types/extractor/types.d.ts +23 -0
  91. package/dist/types/extractor/utils/ObjectUtils.d.ts +1 -0
  92. package/dist/types/extractor/utils/POParser.d.ts +24 -0
  93. package/dist/types/extractor.d.ts +1 -0
  94. package/dist/types/navigation/react-client/createNavigation.d.ts +9 -9
  95. package/dist/types/navigation/react-server/createNavigation.d.ts +9 -9
  96. package/dist/types/navigation/shared/createSharedNavigationFns.d.ts +10 -10
  97. package/dist/types/plugin/catalog/catalogLoader.d.ts +10 -0
  98. package/dist/types/plugin/declaration/index.d.ts +1 -0
  99. package/dist/types/plugin/extractor/extractionLoader.d.ts +3 -0
  100. package/dist/types/plugin/nextFlags.d.ts +2 -0
  101. package/dist/types/plugin/types.d.ts +18 -0
  102. package/dist/types/react-client/index.d.ts +3 -1
  103. package/dist/types/react-server/index.d.ts +1 -0
  104. package/dist/types/react-server/useExtracted.d.ts +2 -0
  105. package/dist/types/server/react-client/index.d.ts +2 -1
  106. package/dist/types/server/react-server/getExtracted.d.ts +10 -0
  107. package/dist/types/server/react-server/getServerExtractor.d.ts +5 -0
  108. package/dist/types/server/react-server/index.d.ts +1 -0
  109. package/extractor/catalogLoader.d.ts +4 -0
  110. package/extractor/extractionLoader.d.ts +4 -0
  111. package/extractor.d.ts +2 -0
  112. package/package.json +20 -5
  113. package/dist/esm/production/plugin/createMessagesDeclaration.js +0 -1
  114. package/dist/esm/production/plugin/hasStableTurboConfig.js +0 -1
  115. package/dist/types/plugin/hasStableTurboConfig.d.ts +0 -2
  116. /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,8 @@
1
+ import ExtractionCompiler from './ExtractionCompiler.js';
2
+
3
+ async function extractMessages(params) {
4
+ const compiler = new ExtractionCompiler(params);
5
+ await compiler.extract();
6
+ }
7
+
8
+ export { extractMessages 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,3 @@
1
+ class Formatter {}
2
+
3
+ export { Formatter 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 };