@better-intl/compiler 0.1.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/index.cjs ADDED
@@ -0,0 +1,374 @@
1
+ 'use strict';
2
+
3
+ var parser = require('@babel/parser');
4
+ var _traverse = require('@babel/traverse');
5
+ var core = require('@better-intl/core');
6
+ var _generate = require('@babel/generator');
7
+ var t = require('@babel/types');
8
+ var path = require('path');
9
+
10
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
11
+
12
+ function _interopNamespace(e) {
13
+ if (e && e.__esModule) return e;
14
+ var n = Object.create(null);
15
+ if (e) {
16
+ Object.keys(e).forEach(function (k) {
17
+ if (k !== 'default') {
18
+ var d = Object.getOwnPropertyDescriptor(e, k);
19
+ Object.defineProperty(n, k, d.get ? d : {
20
+ enumerable: true,
21
+ get: function () { return e[k]; }
22
+ });
23
+ }
24
+ });
25
+ }
26
+ n.default = e;
27
+ return Object.freeze(n);
28
+ }
29
+
30
+ var _traverse__default = /*#__PURE__*/_interopDefault(_traverse);
31
+ var _generate__default = /*#__PURE__*/_interopDefault(_generate);
32
+ var t__namespace = /*#__PURE__*/_interopNamespace(t);
33
+
34
+ // src/catalog-generator.ts
35
+ function generateDefaultCatalog(messages) {
36
+ const catalog = {};
37
+ for (const msg of messages) {
38
+ catalog[msg.id] = msg.defaultMessage;
39
+ }
40
+ return { locale: "en", messages: catalog };
41
+ }
42
+ function mergeCatalog(existing, extracted) {
43
+ const newIds = new Set(extracted.map((m) => m.id));
44
+ const existingIds = new Set(Object.keys(existing));
45
+ const added = [];
46
+ const removed = [];
47
+ const unchanged = [];
48
+ const messages = {};
49
+ for (const msg of extracted) {
50
+ if (existingIds.has(msg.id)) {
51
+ messages[msg.id] = existing[msg.id];
52
+ unchanged.push(msg.id);
53
+ } else {
54
+ messages[msg.id] = msg.defaultMessage;
55
+ added.push(msg.id);
56
+ }
57
+ }
58
+ for (const id of existingIds) {
59
+ if (!newIds.has(id)) {
60
+ removed.push(id);
61
+ }
62
+ }
63
+ return { messages, added, removed, unchanged };
64
+ }
65
+ function findMissingKeys(defaultCatalog, targetCatalog) {
66
+ return Object.keys(defaultCatalog).filter((id) => !(id in targetCatalog));
67
+ }
68
+ function findDeadKeys(catalog, extracted) {
69
+ const extractedIds = new Set(extracted.map((m) => m.id));
70
+ return Object.keys(catalog).filter((id) => !extractedIds.has(id));
71
+ }
72
+ var traverse = typeof _traverse__default.default === "function" ? _traverse__default.default : _traverse__default.default.default;
73
+ var IGNORED_ELEMENTS = /* @__PURE__ */ new Set(["script", "style", "code", "pre", "Helmet"]);
74
+ var IGNORED_FILE_PATTERNS = [
75
+ /\.test\.[tj]sx?$/,
76
+ /\.spec\.[tj]sx?$/,
77
+ /\.stories\.[tj]sx?$/,
78
+ /\/__tests__\//
79
+ ];
80
+ function extract(source, options) {
81
+ const { filePath, mode = "auto" } = options;
82
+ if (IGNORED_FILE_PATTERNS.some((p) => p.test(filePath))) {
83
+ return [];
84
+ }
85
+ const ast = parser.parse(source, {
86
+ sourceType: "module",
87
+ plugins: ["jsx", "typescript"]
88
+ });
89
+ const ctx = {
90
+ componentName: void 0,
91
+ messages: [],
92
+ ignoredLines: collectIgnoreComments(source),
93
+ filePath,
94
+ mode
95
+ };
96
+ traverse(ast, {
97
+ // Track component name for context
98
+ FunctionDeclaration(path) {
99
+ if (path.node.id) {
100
+ ctx.componentName = path.node.id.name;
101
+ }
102
+ },
103
+ VariableDeclarator(path) {
104
+ if (path.node.id.type === "Identifier" && (path.node.init?.type === "ArrowFunctionExpression" || path.node.init?.type === "FunctionExpression")) {
105
+ ctx.componentName = path.node.id.name;
106
+ }
107
+ },
108
+ // Extract text from JSX
109
+ JSXText(path) {
110
+ const text = path.node.value.trim();
111
+ if (!text) return;
112
+ const line = path.node.loc?.start.line ?? 0;
113
+ if (ctx.ignoredLines.has(line) || ctx.ignoredLines.has(line - 1)) return;
114
+ const parentElement = findParentElement(path);
115
+ if (!parentElement) return;
116
+ const elementName = getElementName(parentElement);
117
+ if (IGNORED_ELEMENTS.has(elementName)) return;
118
+ if (ctx.mode === "explicit") {
119
+ const hasI18nProp = hasAttribute(parentElement, "i18n");
120
+ if (elementName !== "T" && !hasI18nProp) return;
121
+ }
122
+ addMessage(ctx, text, elementName, path.node.loc);
123
+ },
124
+ // Also handle string literals in JSX expressions like {"Hello"}
125
+ JSXExpressionContainer(path) {
126
+ const expr = path.node.expression;
127
+ if (expr.type !== "StringLiteral") return;
128
+ const text = expr.value.trim();
129
+ if (!text) return;
130
+ const line = expr.loc?.start.line ?? 0;
131
+ if (ctx.ignoredLines.has(line) || ctx.ignoredLines.has(line - 1)) return;
132
+ const parentElement = findParentElement(path);
133
+ if (!parentElement) return;
134
+ const elementName = getElementName(parentElement);
135
+ if (IGNORED_ELEMENTS.has(elementName)) return;
136
+ if (ctx.mode === "explicit") {
137
+ const hasI18nProp = hasAttribute(parentElement, "i18n");
138
+ if (elementName !== "T" && !hasI18nProp) return;
139
+ }
140
+ addMessage(ctx, text, elementName, expr.loc);
141
+ }
142
+ });
143
+ return ctx.messages;
144
+ }
145
+ function addMessage(ctx, text, elementType, loc) {
146
+ const id = core.generateId({
147
+ text,
148
+ filePath: ctx.filePath,
149
+ context: ctx.componentName
150
+ });
151
+ if (ctx.messages.some((m) => m.id === id)) return;
152
+ ctx.messages.push({
153
+ id,
154
+ defaultMessage: text,
155
+ description: `${ctx.filePath}:${elementType}`,
156
+ filePath: ctx.filePath,
157
+ componentName: ctx.componentName,
158
+ elementType,
159
+ line: loc?.start.line ?? 0,
160
+ column: loc?.start.column ?? 0
161
+ });
162
+ }
163
+ function findParentElement(path) {
164
+ let current = path.parentPath;
165
+ while (current) {
166
+ if (current.isJSXElement()) {
167
+ return current.node.openingElement;
168
+ }
169
+ current = current.parentPath;
170
+ }
171
+ return null;
172
+ }
173
+ function getElementName(opening) {
174
+ if (opening.name.type === "JSXIdentifier") {
175
+ return opening.name.name;
176
+ }
177
+ if (opening.name.type === "JSXMemberExpression") {
178
+ return `${getMemberName(opening.name)}`;
179
+ }
180
+ return "unknown";
181
+ }
182
+ function getMemberName(node) {
183
+ const object = node.object.type === "JSXIdentifier" ? node.object.name : getMemberName(node.object);
184
+ return `${object}.${node.property.name}`;
185
+ }
186
+ function hasAttribute(opening, name) {
187
+ return opening.attributes.some(
188
+ (attr) => attr.type === "JSXAttribute" && attr.name.type === "JSXIdentifier" && attr.name.name === name
189
+ );
190
+ }
191
+ function collectIgnoreComments(source) {
192
+ const ignored = /* @__PURE__ */ new Set();
193
+ const lines = source.split("\n");
194
+ for (let i = 0; i < lines.length; i++) {
195
+ if (lines[i].includes("i18n-ignore")) {
196
+ ignored.add(i + 1);
197
+ }
198
+ }
199
+ return ignored;
200
+ }
201
+ var traverse2 = typeof _traverse__default.default === "function" ? _traverse__default.default : _traverse__default.default.default;
202
+ var generate = typeof _generate__default.default === "function" ? _generate__default.default : _generate__default.default.default;
203
+ var IGNORED_ELEMENTS2 = /* @__PURE__ */ new Set(["script", "style", "code", "pre", "Helmet"]);
204
+ function transform(source, options) {
205
+ const {
206
+ filePath,
207
+ locale,
208
+ messages,
209
+ tFunctionName = "__t",
210
+ runtimeImport = "@better-intl/react",
211
+ mode = "auto"
212
+ } = options;
213
+ const ast = parser.parse(source, {
214
+ sourceType: "module",
215
+ plugins: ["jsx", "typescript"]
216
+ });
217
+ let hasTranslations = false;
218
+ let componentName;
219
+ let needsImport = false;
220
+ const functionsNeedingHook = /* @__PURE__ */ new Set();
221
+ traverse2(ast, {
222
+ FunctionDeclaration(path) {
223
+ if (path.node.id) componentName = path.node.id.name;
224
+ },
225
+ VariableDeclarator(path) {
226
+ if (path.node.id.type === "Identifier" && (path.node.init?.type === "ArrowFunctionExpression" || path.node.init?.type === "FunctionExpression")) {
227
+ componentName = path.node.id.name;
228
+ }
229
+ },
230
+ JSXText(path) {
231
+ const text = path.node.value.trim();
232
+ if (!text) return;
233
+ const parent = path.parentPath;
234
+ if (!parent?.isJSXElement()) return;
235
+ const elementName = getJSXName(parent.node.openingElement);
236
+ if (IGNORED_ELEMENTS2.has(elementName)) return;
237
+ const line = path.node.loc?.start.line ?? 0;
238
+ if (hasIgnoreComment(source, line)) return;
239
+ if (mode === "explicit") {
240
+ const hasI18n = parent.node.openingElement.attributes.some(
241
+ (a) => a.type === "JSXAttribute" && a.name.type === "JSXIdentifier" && a.name.name === "i18n"
242
+ );
243
+ if (elementName !== "T" && !hasI18n) return;
244
+ }
245
+ const id = core.generateId({ text, filePath, context: componentName });
246
+ hasTranslations = true;
247
+ if (locale && messages?.[id]) {
248
+ const newNode = t__namespace.jsxText(messages[id]);
249
+ path.replaceWith(newNode);
250
+ path.skip();
251
+ } else {
252
+ needsImport = true;
253
+ path.replaceWith(
254
+ t__namespace.jsxExpressionContainer(
255
+ t__namespace.callExpression(t__namespace.identifier(tFunctionName), [
256
+ t__namespace.stringLiteral(id),
257
+ t__namespace.identifier("undefined"),
258
+ t__namespace.stringLiteral(text)
259
+ ])
260
+ )
261
+ );
262
+ const fnPath = path.findParent(
263
+ (p) => p.isFunctionDeclaration() || p.isFunctionExpression() || p.isArrowFunctionExpression()
264
+ );
265
+ if (fnPath) {
266
+ functionsNeedingHook.add(fnPath.node);
267
+ }
268
+ }
269
+ }
270
+ });
271
+ if (needsImport) {
272
+ let injectHook2 = function(path) {
273
+ if (!functionsNeedingHook.has(path.node)) return;
274
+ const hookCall = t__namespace.variableDeclaration("const", [
275
+ t__namespace.variableDeclarator(
276
+ t__namespace.objectPattern([
277
+ t__namespace.objectProperty(
278
+ t__namespace.identifier("t"),
279
+ t__namespace.identifier(tFunctionName),
280
+ false,
281
+ false
282
+ )
283
+ ]),
284
+ t__namespace.callExpression(t__namespace.identifier("useTranslation"), [])
285
+ )
286
+ ]);
287
+ const body = path.node.body;
288
+ if (t__namespace.isBlockStatement(body)) {
289
+ body.body.unshift(hookCall);
290
+ } else {
291
+ path.node.body = t__namespace.blockStatement([hookCall, t__namespace.returnStatement(body)]);
292
+ }
293
+ };
294
+ traverse2(ast, {
295
+ FunctionDeclaration(path) {
296
+ injectHook2(path);
297
+ },
298
+ FunctionExpression(path) {
299
+ injectHook2(path);
300
+ },
301
+ ArrowFunctionExpression(path) {
302
+ injectHook2(path);
303
+ }
304
+ });
305
+ const hasImport = ast.program.body.some(
306
+ (node) => t__namespace.isImportDeclaration(node) && node.source.value === runtimeImport && node.specifiers.some(
307
+ (s) => t__namespace.isImportSpecifier(s) && t__namespace.isIdentifier(s.imported) && s.imported.name === "useTranslation"
308
+ )
309
+ );
310
+ if (!hasImport) {
311
+ const importDecl = t__namespace.importDeclaration(
312
+ [
313
+ t__namespace.importSpecifier(
314
+ t__namespace.identifier("useTranslation"),
315
+ t__namespace.identifier("useTranslation")
316
+ )
317
+ ],
318
+ t__namespace.stringLiteral(runtimeImport)
319
+ );
320
+ ast.program.body.unshift(importDecl);
321
+ }
322
+ }
323
+ const output = generate(ast, { sourceMaps: true, sourceFileName: filePath });
324
+ return {
325
+ code: output.code,
326
+ map: output.map,
327
+ hasTranslations
328
+ };
329
+ }
330
+ function getJSXName(opening) {
331
+ if (opening.name.type === "JSXIdentifier") return opening.name.name;
332
+ return "unknown";
333
+ }
334
+ function hasIgnoreComment(source, line) {
335
+ const lines = source.split("\n");
336
+ const prevLine = lines[line - 2];
337
+ return prevLine ? prevLine.includes("i18n-ignore") : false;
338
+ }
339
+ function betterIntlLoader(source) {
340
+ const options = this.getOptions?.() ?? {};
341
+ const absolutePath = this.resourcePath;
342
+ const filePath = path.relative(this.rootContext || process.cwd(), absolutePath);
343
+ if (absolutePath.includes("node_modules")) return source;
344
+ if (!/\.[jt]sx$/.test(filePath)) return source;
345
+ if (/\.(test|spec|stories)\.[jt]sx?$/.test(filePath)) return source;
346
+ try {
347
+ const result = transform(source, {
348
+ filePath,
349
+ locale: options.locale,
350
+ messages: options.messages,
351
+ mode: options.mode ?? "auto"
352
+ });
353
+ if (result.hasTranslations) {
354
+ console.log(
355
+ `[better-intl] Transformed ${filePath} (${result.hasTranslations})`
356
+ );
357
+ return result.code;
358
+ }
359
+ return source;
360
+ } catch (err) {
361
+ console.error(`[better-intl] Error transforming ${filePath}:`, err);
362
+ return source;
363
+ }
364
+ }
365
+
366
+ exports.extract = extract;
367
+ exports.findDeadKeys = findDeadKeys;
368
+ exports.findMissingKeys = findMissingKeys;
369
+ exports.generateDefaultCatalog = generateDefaultCatalog;
370
+ exports.mergeCatalog = mergeCatalog;
371
+ exports.transform = transform;
372
+ exports.webpackLoader = betterIntlLoader;
373
+ //# sourceMappingURL=index.cjs.map
374
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/catalog-generator.ts","../src/extractor.ts","../src/transformer.ts","../src/webpack-loader.ts"],"names":["_traverse","parse","generateId","traverse","_generate","IGNORED_ELEMENTS","t","injectHook","relative"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Catalog generator — writes extracted messages to locale JSON files.\n */\n\nimport type { ExtractedMessage, Messages } from \"@better-intl/core\";\n\nexport interface CatalogFile {\n locale: string;\n messages: Messages;\n}\n\n/**\n * Generate a catalog for the default locale from extracted messages.\n */\nexport function generateDefaultCatalog(\n messages: ExtractedMessage[],\n): CatalogFile {\n const catalog: Messages = {};\n for (const msg of messages) {\n catalog[msg.id] = msg.defaultMessage;\n }\n return { locale: \"en\", messages: catalog };\n}\n\n/**\n * Merge new messages into an existing catalog, preserving existing translations.\n * Returns the merged catalog and lists of added/removed keys.\n */\nexport function mergeCatalog(\n existing: Messages,\n extracted: ExtractedMessage[],\n): {\n messages: Messages;\n added: string[];\n removed: string[];\n unchanged: string[];\n} {\n const newIds = new Set(extracted.map((m) => m.id));\n const existingIds = new Set(Object.keys(existing));\n\n const added: string[] = [];\n const removed: string[] = [];\n const unchanged: string[] = [];\n const messages: Messages = {};\n\n // Keep existing translations, mark new ones\n for (const msg of extracted) {\n if (existingIds.has(msg.id)) {\n messages[msg.id] = existing[msg.id];\n unchanged.push(msg.id);\n } else {\n messages[msg.id] = msg.defaultMessage;\n added.push(msg.id);\n }\n }\n\n // Track removed keys\n for (const id of existingIds) {\n if (!newIds.has(id)) {\n removed.push(id);\n }\n }\n\n return { messages, added, removed, unchanged };\n}\n\n/**\n * Find missing translations (keys present in default but not in target locale).\n */\nexport function findMissingKeys(\n defaultCatalog: Messages,\n targetCatalog: Messages,\n): string[] {\n return Object.keys(defaultCatalog).filter((id) => !(id in targetCatalog));\n}\n\n/**\n * Find dead keys (keys in catalog but not in extracted messages).\n */\nexport function findDeadKeys(\n catalog: Messages,\n extracted: ExtractedMessage[],\n): string[] {\n const extractedIds = new Set(extracted.map((m) => m.id));\n return Object.keys(catalog).filter((id) => !extractedIds.has(id));\n}\n","/**\n * AST-based extractor for JSX text nodes.\n *\n * Walks the Babel AST to find translatable text, respecting ignore rules,\n * and emits `ExtractedMessage` descriptors with stable IDs.\n */\n\nimport { parse } from \"@babel/parser\";\nimport type { NodePath } from \"@babel/traverse\";\nimport _traverse from \"@babel/traverse\";\nimport type * as t from \"@babel/types\";\nimport type { ExtractedMessage } from \"@better-intl/core\";\nimport { generateId } from \"@better-intl/core\";\n\n// Handle CJS/ESM interop for @babel/traverse\nconst traverse =\n typeof _traverse === \"function\"\n ? _traverse\n : (_traverse as { default: typeof _traverse }).default;\n\n/** Elements whose text content should never be extracted. */\nconst IGNORED_ELEMENTS = new Set([\"script\", \"style\", \"code\", \"pre\", \"Helmet\"]);\n\n/** File patterns that should be skipped entirely. */\nconst IGNORED_FILE_PATTERNS = [\n /\\.test\\.[tj]sx?$/,\n /\\.spec\\.[tj]sx?$/,\n /\\.stories\\.[tj]sx?$/,\n /\\/__tests__\\//,\n];\n\nexport interface ExtractorOptions {\n filePath: string;\n /** Extraction mode: \"auto\" extracts all text, \"explicit\" only <T> and i18n prop */\n mode?: \"auto\" | \"explicit\";\n}\n\ninterface ExtractionContext {\n componentName: string | undefined;\n messages: ExtractedMessage[];\n ignoredLines: Set<number>;\n filePath: string;\n mode: \"auto\" | \"explicit\";\n}\n\n/**\n * Extract translatable messages from a source file.\n */\nexport function extract(\n source: string,\n options: ExtractorOptions,\n): ExtractedMessage[] {\n const { filePath, mode = \"auto\" } = options;\n\n // Skip ignored file patterns\n if (IGNORED_FILE_PATTERNS.some((p) => p.test(filePath))) {\n return [];\n }\n\n const ast = parse(source, {\n sourceType: \"module\",\n plugins: [\"jsx\", \"typescript\"],\n });\n\n const ctx: ExtractionContext = {\n componentName: undefined,\n messages: [],\n ignoredLines: collectIgnoreComments(source),\n filePath,\n mode,\n };\n\n traverse(ast, {\n // Track component name for context\n FunctionDeclaration(path: NodePath<t.FunctionDeclaration>) {\n if (path.node.id) {\n ctx.componentName = path.node.id.name;\n }\n },\n\n VariableDeclarator(path: NodePath<t.VariableDeclarator>) {\n if (\n path.node.id.type === \"Identifier\" &&\n (path.node.init?.type === \"ArrowFunctionExpression\" ||\n path.node.init?.type === \"FunctionExpression\")\n ) {\n ctx.componentName = path.node.id.name;\n }\n },\n\n // Extract text from JSX\n JSXText(path: NodePath<t.JSXText>) {\n const text = path.node.value.trim();\n if (!text) return;\n\n const line = path.node.loc?.start.line ?? 0;\n if (ctx.ignoredLines.has(line) || ctx.ignoredLines.has(line - 1)) return;\n\n const parentElement = findParentElement(path);\n if (!parentElement) return;\n\n const elementName = getElementName(parentElement);\n if (IGNORED_ELEMENTS.has(elementName)) return;\n\n // In explicit mode, only extract from <T> or elements with i18n prop\n if (ctx.mode === \"explicit\") {\n const hasI18nProp = hasAttribute(parentElement, \"i18n\");\n if (elementName !== \"T\" && !hasI18nProp) return;\n }\n\n addMessage(ctx, text, elementName, path.node.loc);\n },\n\n // Also handle string literals in JSX expressions like {\"Hello\"}\n JSXExpressionContainer(path: NodePath<t.JSXExpressionContainer>) {\n const expr = path.node.expression;\n if (expr.type !== \"StringLiteral\") return;\n\n const text = expr.value.trim();\n if (!text) return;\n\n const line = expr.loc?.start.line ?? 0;\n if (ctx.ignoredLines.has(line) || ctx.ignoredLines.has(line - 1)) return;\n\n const parentElement = findParentElement(path);\n if (!parentElement) return;\n\n const elementName = getElementName(parentElement);\n if (IGNORED_ELEMENTS.has(elementName)) return;\n\n if (ctx.mode === \"explicit\") {\n const hasI18nProp = hasAttribute(parentElement, \"i18n\");\n if (elementName !== \"T\" && !hasI18nProp) return;\n }\n\n addMessage(ctx, text, elementName, expr.loc);\n },\n });\n\n return ctx.messages;\n}\n\nfunction addMessage(\n ctx: ExtractionContext,\n text: string,\n elementType: string,\n loc: t.SourceLocation | null | undefined,\n): void {\n const id = generateId({\n text,\n filePath: ctx.filePath,\n context: ctx.componentName,\n });\n\n // Avoid duplicate IDs within the same file\n if (ctx.messages.some((m) => m.id === id)) return;\n\n ctx.messages.push({\n id,\n defaultMessage: text,\n description: `${ctx.filePath}:${elementType}`,\n filePath: ctx.filePath,\n componentName: ctx.componentName,\n elementType,\n line: loc?.start.line ?? 0,\n column: loc?.start.column ?? 0,\n });\n}\n\nfunction findParentElement(path: NodePath): t.JSXOpeningElement | null {\n let current = path.parentPath;\n while (current) {\n if (current.isJSXElement()) {\n return current.node.openingElement;\n }\n current = current.parentPath;\n }\n return null;\n}\n\nfunction getElementName(opening: t.JSXOpeningElement): string {\n if (opening.name.type === \"JSXIdentifier\") {\n return opening.name.name;\n }\n if (opening.name.type === \"JSXMemberExpression\") {\n return `${getMemberName(opening.name)}`;\n }\n return \"unknown\";\n}\n\nfunction getMemberName(node: t.JSXMemberExpression): string {\n const object =\n node.object.type === \"JSXIdentifier\"\n ? node.object.name\n : getMemberName(node.object);\n return `${object}.${node.property.name}`;\n}\n\nfunction hasAttribute(opening: t.JSXOpeningElement, name: string): boolean {\n return opening.attributes.some(\n (attr) =>\n attr.type === \"JSXAttribute\" &&\n attr.name.type === \"JSXIdentifier\" &&\n attr.name.name === name,\n );\n}\n\n/**\n * Collect line numbers that have `i18n-ignore` comments.\n */\nfunction collectIgnoreComments(source: string): Set<number> {\n const ignored = new Set<number>();\n const lines = source.split(\"\\n\");\n for (let i = 0; i < lines.length; i++) {\n if (lines[i].includes(\"i18n-ignore\")) {\n ignored.add(i + 1); // 1-indexed\n }\n }\n return ignored;\n}\n","/**\n * Babel AST transformer.\n *\n * Replaces static JSX text with `__t(\"id\")` calls,\n * or with static text when the locale is known at build time.\n *\n * For runtime mode, injects `const { t: __t } = useTranslation();`\n * at the top of each component function that has translatable text.\n */\n\nimport _generate from \"@babel/generator\";\nimport { parse } from \"@babel/parser\";\nimport type { NodePath } from \"@babel/traverse\";\nimport _traverse from \"@babel/traverse\";\nimport * as t from \"@babel/types\";\nimport type { Messages } from \"@better-intl/core\";\nimport { generateId } from \"@better-intl/core\";\n\nconst traverse =\n typeof _traverse === \"function\"\n ? _traverse\n : (_traverse as { default: typeof _traverse }).default;\n\nconst generate =\n typeof _generate === \"function\"\n ? _generate\n : (_generate as { default: typeof _generate }).default;\n\nconst IGNORED_ELEMENTS = new Set([\"script\", \"style\", \"code\", \"pre\", \"Helmet\"]);\n\nexport interface TransformOptions {\n filePath: string;\n /** When set, inline the translated text directly (zero-runtime). */\n locale?: string;\n messages?: Messages;\n /** The identifier for the `t` function (default: `__t`) */\n tFunctionName?: string;\n /** Import path for the runtime (default: `@better-intl/react`) */\n runtimeImport?: string;\n mode?: \"auto\" | \"explicit\";\n}\n\nexport interface TransformResult {\n code: string;\n map: ReturnType<typeof generate>[\"map\"];\n hasTranslations: boolean;\n}\n\nexport function transform(\n source: string,\n options: TransformOptions,\n): TransformResult {\n const {\n filePath,\n locale,\n messages,\n tFunctionName = \"__t\",\n runtimeImport = \"@better-intl/react\",\n mode = \"auto\",\n } = options;\n\n const ast = parse(source, {\n sourceType: \"module\",\n plugins: [\"jsx\", \"typescript\"],\n });\n\n let hasTranslations = false;\n let componentName: string | undefined;\n let needsImport = false;\n\n // Track which function bodies need the hook injection\n const functionsNeedingHook = new Set<t.Node>();\n\n traverse(ast, {\n FunctionDeclaration(path: NodePath<t.FunctionDeclaration>) {\n if (path.node.id) componentName = path.node.id.name;\n },\n\n VariableDeclarator(path: NodePath<t.VariableDeclarator>) {\n if (\n path.node.id.type === \"Identifier\" &&\n (path.node.init?.type === \"ArrowFunctionExpression\" ||\n path.node.init?.type === \"FunctionExpression\")\n ) {\n componentName = path.node.id.name;\n }\n },\n\n JSXText(path: NodePath<t.JSXText>) {\n const text = path.node.value.trim();\n if (!text) return;\n\n const parent = path.parentPath;\n if (!parent?.isJSXElement()) return;\n\n const elementName = getJSXName(parent.node.openingElement);\n if (IGNORED_ELEMENTS.has(elementName)) return;\n\n // Check ignore comment\n const line = path.node.loc?.start.line ?? 0;\n if (hasIgnoreComment(source, line)) return;\n\n // In explicit mode, only transform <T> and i18n prop elements\n if (mode === \"explicit\") {\n const hasI18n = parent.node.openingElement.attributes.some(\n (a) =>\n a.type === \"JSXAttribute\" &&\n a.name.type === \"JSXIdentifier\" &&\n a.name.name === \"i18n\",\n );\n if (elementName !== \"T\" && !hasI18n) return;\n }\n\n const id = generateId({ text, filePath, context: componentName });\n hasTranslations = true;\n\n if (locale && messages?.[id]) {\n // Zero-runtime: inline the translated text\n const newNode = t.jsxText(messages[id]);\n path.replaceWith(newNode);\n path.skip();\n } else {\n // Runtime: replace with __t() call\n needsImport = true;\n path.replaceWith(\n t.jsxExpressionContainer(\n t.callExpression(t.identifier(tFunctionName), [\n t.stringLiteral(id),\n t.identifier(\"undefined\"),\n t.stringLiteral(text),\n ]),\n ),\n );\n\n // Mark the enclosing function as needing the hook\n const fnPath = path.findParent(\n (p) =>\n p.isFunctionDeclaration() ||\n p.isFunctionExpression() ||\n p.isArrowFunctionExpression(),\n );\n if (fnPath) {\n functionsNeedingHook.add(fnPath.node);\n }\n }\n },\n });\n\n // Inject `const { t: __t } = useTranslation();` at the top of each component\n if (needsImport) {\n traverse(ast, {\n FunctionDeclaration(path: NodePath<t.FunctionDeclaration>) {\n injectHook(path);\n },\n FunctionExpression(path: NodePath<t.FunctionExpression>) {\n injectHook(path);\n },\n ArrowFunctionExpression(path: NodePath<t.ArrowFunctionExpression>) {\n injectHook(path);\n },\n });\n\n function injectHook(\n path: NodePath<\n t.FunctionDeclaration | t.FunctionExpression | t.ArrowFunctionExpression\n >,\n ) {\n if (!functionsNeedingHook.has(path.node)) return;\n\n const hookCall = t.variableDeclaration(\"const\", [\n t.variableDeclarator(\n t.objectPattern([\n t.objectProperty(\n t.identifier(\"t\"),\n t.identifier(tFunctionName),\n false,\n false,\n ),\n ]),\n t.callExpression(t.identifier(\"useTranslation\"), []),\n ),\n ]);\n\n const body = path.node.body;\n if (t.isBlockStatement(body)) {\n body.body.unshift(hookCall);\n } else {\n // Arrow function with expression body: convert to block\n path.node.body = t.blockStatement([hookCall, t.returnStatement(body)]);\n }\n }\n\n // Add import for useTranslation (if not already present)\n const hasImport = ast.program.body.some(\n (node) =>\n t.isImportDeclaration(node) &&\n node.source.value === runtimeImport &&\n node.specifiers.some(\n (s) =>\n t.isImportSpecifier(s) &&\n t.isIdentifier(s.imported) &&\n s.imported.name === \"useTranslation\",\n ),\n );\n\n if (!hasImport) {\n const importDecl = t.importDeclaration(\n [\n t.importSpecifier(\n t.identifier(\"useTranslation\"),\n t.identifier(\"useTranslation\"),\n ),\n ],\n t.stringLiteral(runtimeImport),\n );\n (ast.program.body as t.Statement[]).unshift(importDecl);\n }\n }\n\n const output = generate(ast, { sourceMaps: true, sourceFileName: filePath });\n\n return {\n code: output.code,\n map: output.map,\n hasTranslations,\n };\n}\n\nfunction getJSXName(opening: t.JSXOpeningElement): string {\n if (opening.name.type === \"JSXIdentifier\") return opening.name.name;\n return \"unknown\";\n}\n\nfunction hasIgnoreComment(source: string, line: number): boolean {\n const lines = source.split(\"\\n\");\n const prevLine = lines[line - 2]; // line is 1-indexed, check line above\n return prevLine ? prevLine.includes(\"i18n-ignore\") : false;\n}\n","/**\n * Webpack loader for better-intl.\n *\n * Transforms JSX text nodes into t() calls automatically during build.\n * Used by Next.js integration to enable zero-config i18n.\n */\n\nimport { relative } from \"node:path\";\nimport { transform } from \"./transformer.js\";\n\nexport interface BetterIntlLoaderOptions {\n locale?: string;\n messages?: Record<string, string>;\n mode?: \"auto\" | \"explicit\";\n}\n\n// biome-ignore lint/suspicious/noExplicitAny: webpack loader context type\nexport default function betterIntlLoader(this: any, source: string): string {\n const options: BetterIntlLoaderOptions = this.getOptions?.() ?? {};\n const absolutePath = this.resourcePath;\n const filePath = relative(this.rootContext || process.cwd(), absolutePath);\n\n // Skip node_modules and non-JSX files\n if (absolutePath.includes(\"node_modules\")) return source;\n if (!/\\.[jt]sx$/.test(filePath)) return source;\n\n // Skip test/spec/stories files\n if (/\\.(test|spec|stories)\\.[jt]sx?$/.test(filePath)) return source;\n\n try {\n const result = transform(source, {\n filePath,\n locale: options.locale,\n messages: options.messages,\n mode: options.mode ?? \"auto\",\n });\n\n if (result.hasTranslations) {\n console.log(\n `[better-intl] Transformed ${filePath} (${result.hasTranslations})`,\n );\n return result.code;\n }\n\n return source;\n } catch (err) {\n console.error(`[better-intl] Error transforming ${filePath}:`, err);\n return source;\n }\n}\n"]}
@@ -0,0 +1,82 @@
1
+ import { Messages, ExtractedMessage } from '@better-intl/core';
2
+ import _generate from '@babel/generator';
3
+ export { BetterIntlLoaderOptions, default as webpackLoader } from './webpack-loader.cjs';
4
+
5
+ /**
6
+ * Catalog generator — writes extracted messages to locale JSON files.
7
+ */
8
+
9
+ interface CatalogFile {
10
+ locale: string;
11
+ messages: Messages;
12
+ }
13
+ /**
14
+ * Generate a catalog for the default locale from extracted messages.
15
+ */
16
+ declare function generateDefaultCatalog(messages: ExtractedMessage[]): CatalogFile;
17
+ /**
18
+ * Merge new messages into an existing catalog, preserving existing translations.
19
+ * Returns the merged catalog and lists of added/removed keys.
20
+ */
21
+ declare function mergeCatalog(existing: Messages, extracted: ExtractedMessage[]): {
22
+ messages: Messages;
23
+ added: string[];
24
+ removed: string[];
25
+ unchanged: string[];
26
+ };
27
+ /**
28
+ * Find missing translations (keys present in default but not in target locale).
29
+ */
30
+ declare function findMissingKeys(defaultCatalog: Messages, targetCatalog: Messages): string[];
31
+ /**
32
+ * Find dead keys (keys in catalog but not in extracted messages).
33
+ */
34
+ declare function findDeadKeys(catalog: Messages, extracted: ExtractedMessage[]): string[];
35
+
36
+ /**
37
+ * AST-based extractor for JSX text nodes.
38
+ *
39
+ * Walks the Babel AST to find translatable text, respecting ignore rules,
40
+ * and emits `ExtractedMessage` descriptors with stable IDs.
41
+ */
42
+
43
+ interface ExtractorOptions {
44
+ filePath: string;
45
+ /** Extraction mode: "auto" extracts all text, "explicit" only <T> and i18n prop */
46
+ mode?: "auto" | "explicit";
47
+ }
48
+ /**
49
+ * Extract translatable messages from a source file.
50
+ */
51
+ declare function extract(source: string, options: ExtractorOptions): ExtractedMessage[];
52
+
53
+ /**
54
+ * Babel AST transformer.
55
+ *
56
+ * Replaces static JSX text with `__t("id")` calls,
57
+ * or with static text when the locale is known at build time.
58
+ *
59
+ * For runtime mode, injects `const { t: __t } = useTranslation();`
60
+ * at the top of each component function that has translatable text.
61
+ */
62
+
63
+ declare const generate: typeof _generate;
64
+ interface TransformOptions {
65
+ filePath: string;
66
+ /** When set, inline the translated text directly (zero-runtime). */
67
+ locale?: string;
68
+ messages?: Messages;
69
+ /** The identifier for the `t` function (default: `__t`) */
70
+ tFunctionName?: string;
71
+ /** Import path for the runtime (default: `@better-intl/react`) */
72
+ runtimeImport?: string;
73
+ mode?: "auto" | "explicit";
74
+ }
75
+ interface TransformResult {
76
+ code: string;
77
+ map: ReturnType<typeof generate>["map"];
78
+ hasTranslations: boolean;
79
+ }
80
+ declare function transform(source: string, options: TransformOptions): TransformResult;
81
+
82
+ export { type CatalogFile, type ExtractorOptions, type TransformOptions, type TransformResult, extract, findDeadKeys, findMissingKeys, generateDefaultCatalog, mergeCatalog, transform };
@@ -0,0 +1,82 @@
1
+ import { Messages, ExtractedMessage } from '@better-intl/core';
2
+ import _generate from '@babel/generator';
3
+ export { BetterIntlLoaderOptions, default as webpackLoader } from './webpack-loader.js';
4
+
5
+ /**
6
+ * Catalog generator — writes extracted messages to locale JSON files.
7
+ */
8
+
9
+ interface CatalogFile {
10
+ locale: string;
11
+ messages: Messages;
12
+ }
13
+ /**
14
+ * Generate a catalog for the default locale from extracted messages.
15
+ */
16
+ declare function generateDefaultCatalog(messages: ExtractedMessage[]): CatalogFile;
17
+ /**
18
+ * Merge new messages into an existing catalog, preserving existing translations.
19
+ * Returns the merged catalog and lists of added/removed keys.
20
+ */
21
+ declare function mergeCatalog(existing: Messages, extracted: ExtractedMessage[]): {
22
+ messages: Messages;
23
+ added: string[];
24
+ removed: string[];
25
+ unchanged: string[];
26
+ };
27
+ /**
28
+ * Find missing translations (keys present in default but not in target locale).
29
+ */
30
+ declare function findMissingKeys(defaultCatalog: Messages, targetCatalog: Messages): string[];
31
+ /**
32
+ * Find dead keys (keys in catalog but not in extracted messages).
33
+ */
34
+ declare function findDeadKeys(catalog: Messages, extracted: ExtractedMessage[]): string[];
35
+
36
+ /**
37
+ * AST-based extractor for JSX text nodes.
38
+ *
39
+ * Walks the Babel AST to find translatable text, respecting ignore rules,
40
+ * and emits `ExtractedMessage` descriptors with stable IDs.
41
+ */
42
+
43
+ interface ExtractorOptions {
44
+ filePath: string;
45
+ /** Extraction mode: "auto" extracts all text, "explicit" only <T> and i18n prop */
46
+ mode?: "auto" | "explicit";
47
+ }
48
+ /**
49
+ * Extract translatable messages from a source file.
50
+ */
51
+ declare function extract(source: string, options: ExtractorOptions): ExtractedMessage[];
52
+
53
+ /**
54
+ * Babel AST transformer.
55
+ *
56
+ * Replaces static JSX text with `__t("id")` calls,
57
+ * or with static text when the locale is known at build time.
58
+ *
59
+ * For runtime mode, injects `const { t: __t } = useTranslation();`
60
+ * at the top of each component function that has translatable text.
61
+ */
62
+
63
+ declare const generate: typeof _generate;
64
+ interface TransformOptions {
65
+ filePath: string;
66
+ /** When set, inline the translated text directly (zero-runtime). */
67
+ locale?: string;
68
+ messages?: Messages;
69
+ /** The identifier for the `t` function (default: `__t`) */
70
+ tFunctionName?: string;
71
+ /** Import path for the runtime (default: `@better-intl/react`) */
72
+ runtimeImport?: string;
73
+ mode?: "auto" | "explicit";
74
+ }
75
+ interface TransformResult {
76
+ code: string;
77
+ map: ReturnType<typeof generate>["map"];
78
+ hasTranslations: boolean;
79
+ }
80
+ declare function transform(source: string, options: TransformOptions): TransformResult;
81
+
82
+ export { type CatalogFile, type ExtractorOptions, type TransformOptions, type TransformResult, extract, findDeadKeys, findMissingKeys, generateDefaultCatalog, mergeCatalog, transform };