@better-intl/compiler 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,10 @@
1
- import { relative } from 'path';
2
1
  import _generate from '@babel/generator';
3
2
  import { parse } from '@babel/parser';
4
3
  import _traverse from '@babel/traverse';
5
4
  import * as t from '@babel/types';
6
5
  import { generateId } from '@better-intl/core';
7
6
 
8
- // src/webpack-loader.ts
7
+ // src/transformer.ts
9
8
  var traverse = typeof _traverse === "function" ? _traverse : _traverse.default;
10
9
  var generate = typeof _generate === "function" ? _generate : _generate.default;
11
10
  var IGNORED_ELEMENTS = /* @__PURE__ */ new Set(["script", "style", "code", "pre", "Helmet"]);
@@ -16,12 +15,16 @@ function transform(source, options) {
16
15
  messages,
17
16
  tFunctionName = "__t",
18
17
  runtimeImport = "@better-intl/react",
18
+ rscImport = "@better-intl/next/rsc",
19
19
  mode = "auto"
20
20
  } = options;
21
21
  const ast = parse(source, {
22
22
  sourceType: "module",
23
23
  plugins: ["jsx", "typescript"]
24
24
  });
25
+ const isClientComponent = ast.program.directives?.some((d) => d.value.value === "use client") ?? ast.program.body.some(
26
+ (node) => t.isExpressionStatement(node) && t.isStringLiteral(node.expression) && node.expression.value === "use client"
27
+ );
25
28
  let hasTranslations = false;
26
29
  let componentName;
27
30
  let needsImport = false;
@@ -77,9 +80,9 @@ function transform(source, options) {
77
80
  }
78
81
  });
79
82
  if (needsImport) {
80
- let injectHook2 = function(path) {
83
+ let injectTranslation2 = function(path) {
81
84
  if (!functionsNeedingHook.has(path.node)) return;
82
- const hookCall = t.variableDeclaration("const", [
85
+ const statement = isClientComponent ? t.variableDeclaration("const", [
83
86
  t.variableDeclarator(
84
87
  t.objectPattern([
85
88
  t.objectProperty(
@@ -91,41 +94,63 @@ function transform(source, options) {
91
94
  ]),
92
95
  t.callExpression(t.identifier("useTranslation"), [])
93
96
  )
97
+ ]) : t.variableDeclaration("const", [
98
+ t.variableDeclarator(
99
+ t.identifier(tFunctionName),
100
+ t.callExpression(t.identifier("rsc"), [])
101
+ )
94
102
  ]);
95
103
  const body = path.node.body;
96
104
  if (t.isBlockStatement(body)) {
97
- body.body.unshift(hookCall);
105
+ body.body.unshift(statement);
98
106
  } else {
99
- path.node.body = t.blockStatement([hookCall, t.returnStatement(body)]);
107
+ path.node.body = t.blockStatement([statement, t.returnStatement(body)]);
100
108
  }
101
109
  };
102
110
  traverse(ast, {
103
111
  FunctionDeclaration(path) {
104
- injectHook2(path);
112
+ injectTranslation2(path);
105
113
  },
106
114
  FunctionExpression(path) {
107
- injectHook2(path);
115
+ injectTranslation2(path);
108
116
  },
109
117
  ArrowFunctionExpression(path) {
110
- injectHook2(path);
118
+ injectTranslation2(path);
111
119
  }
112
120
  });
113
- const hasImport = ast.program.body.some(
114
- (node) => t.isImportDeclaration(node) && node.source.value === runtimeImport && node.specifiers.some(
115
- (s) => t.isImportSpecifier(s) && t.isIdentifier(s.imported) && s.imported.name === "useTranslation"
116
- )
117
- );
118
- if (!hasImport) {
119
- const importDecl = t.importDeclaration(
120
- [
121
- t.importSpecifier(
122
- t.identifier("useTranslation"),
123
- t.identifier("useTranslation")
121
+ if (isClientComponent) {
122
+ const hasImport = ast.program.body.some(
123
+ (node) => t.isImportDeclaration(node) && node.source.value === runtimeImport && node.specifiers.some(
124
+ (s) => t.isImportSpecifier(s) && t.isIdentifier(s.imported) && s.imported.name === "useTranslation"
125
+ )
126
+ );
127
+ if (!hasImport) {
128
+ ast.program.body.unshift(
129
+ t.importDeclaration(
130
+ [
131
+ t.importSpecifier(
132
+ t.identifier("useTranslation"),
133
+ t.identifier("useTranslation")
134
+ )
135
+ ],
136
+ t.stringLiteral(runtimeImport)
124
137
  )
125
- ],
126
- t.stringLiteral(runtimeImport)
138
+ );
139
+ }
140
+ } else {
141
+ const hasImport = ast.program.body.some(
142
+ (node) => t.isImportDeclaration(node) && node.source.value === rscImport && node.specifiers.some(
143
+ (s) => t.isImportSpecifier(s) && t.isIdentifier(s.imported) && s.imported.name === "rsc"
144
+ )
127
145
  );
128
- ast.program.body.unshift(importDecl);
146
+ if (!hasImport) {
147
+ ast.program.body.unshift(
148
+ t.importDeclaration(
149
+ [t.importSpecifier(t.identifier("rsc"), t.identifier("rsc"))],
150
+ t.stringLiteral(rscImport)
151
+ )
152
+ );
153
+ }
129
154
  }
130
155
  }
131
156
  const output = generate(ast, { sourceMaps: true, sourceFileName: filePath });
@@ -145,34 +170,6 @@ function hasIgnoreComment(source, line) {
145
170
  return prevLine ? prevLine.includes("i18n-ignore") : false;
146
171
  }
147
172
 
148
- // src/webpack-loader.ts
149
- function betterIntlLoader(source) {
150
- const options = this.getOptions?.() ?? {};
151
- const absolutePath = this.resourcePath;
152
- const filePath = relative(this.rootContext || process.cwd(), absolutePath);
153
- if (absolutePath.includes("node_modules")) return source;
154
- if (!/\.[jt]sx$/.test(filePath)) return source;
155
- if (/\.(test|spec|stories)\.[jt]sx?$/.test(filePath)) return source;
156
- try {
157
- const result = transform(source, {
158
- filePath,
159
- locale: options.locale,
160
- messages: options.messages,
161
- mode: options.mode ?? "auto"
162
- });
163
- if (result.hasTranslations) {
164
- console.log(
165
- `[better-intl] Transformed ${filePath} (${result.hasTranslations})`
166
- );
167
- return result.code;
168
- }
169
- return source;
170
- } catch (err) {
171
- console.error(`[better-intl] Error transforming ${filePath}:`, err);
172
- return source;
173
- }
174
- }
175
-
176
- export { betterIntlLoader, transform };
177
- //# sourceMappingURL=chunk-RKXQPVVG.js.map
178
- //# sourceMappingURL=chunk-RKXQPVVG.js.map
173
+ export { transform };
174
+ //# sourceMappingURL=chunk-MDYQVJ3W.js.map
175
+ //# sourceMappingURL=chunk-MDYQVJ3W.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/transformer.ts"],"names":["injectTranslation"],"mappings":"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","file":"chunk-MDYQVJ3W.js","sourcesContent":["/**\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 client runtime (default: `@better-intl/react`) */\n runtimeImport?: string;\n /** Import path for the RSC runtime (default: `@better-intl/next/rsc`) */\n rscImport?: 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 rscImport = \"@better-intl/next/rsc\",\n mode = \"auto\",\n } = options;\n\n const ast = parse(source, {\n sourceType: \"module\",\n plugins: [\"jsx\", \"typescript\"],\n });\n\n // Detect \"use client\" directive\n const isClientComponent =\n ast.program.directives?.some((d) => d.value.value === \"use client\") ??\n ast.program.body.some(\n (node) =>\n t.isExpressionStatement(node) &&\n t.isStringLiteral(node.expression) &&\n node.expression.value === \"use client\",\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 translation accessor at the top of each component\n if (needsImport) {\n traverse(ast, {\n FunctionDeclaration(path: NodePath<t.FunctionDeclaration>) {\n injectTranslation(path);\n },\n FunctionExpression(path: NodePath<t.FunctionExpression>) {\n injectTranslation(path);\n },\n ArrowFunctionExpression(path: NodePath<t.ArrowFunctionExpression>) {\n injectTranslation(path);\n },\n });\n\n function injectTranslation(\n path: NodePath<\n t.FunctionDeclaration | t.FunctionExpression | t.ArrowFunctionExpression\n >,\n ) {\n if (!functionsNeedingHook.has(path.node)) return;\n\n // Client: const { t: __t } = useTranslation();\n // Server: const __t = rsc();\n const statement = isClientComponent\n ? 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 : t.variableDeclaration(\"const\", [\n t.variableDeclarator(\n t.identifier(tFunctionName),\n t.callExpression(t.identifier(\"rsc\"), []),\n ),\n ]);\n\n const body = path.node.body;\n if (t.isBlockStatement(body)) {\n body.body.unshift(statement);\n } else {\n path.node.body = t.blockStatement([statement, t.returnStatement(body)]);\n }\n }\n\n if (isClientComponent) {\n // Add import { useTranslation } from \"@better-intl/react\"\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 (ast.program.body as t.Statement[]).unshift(\n t.importDeclaration(\n [\n t.importSpecifier(\n t.identifier(\"useTranslation\"),\n t.identifier(\"useTranslation\"),\n ),\n ],\n t.stringLiteral(runtimeImport),\n ),\n );\n }\n } else {\n // Add import { rsc } from \"@better-intl/next/rsc\"\n const hasImport = ast.program.body.some(\n (node) =>\n t.isImportDeclaration(node) &&\n node.source.value === rscImport &&\n node.specifiers.some(\n (s) =>\n t.isImportSpecifier(s) &&\n t.isIdentifier(s.imported) &&\n s.imported.name === \"rsc\",\n ),\n );\n\n if (!hasImport) {\n (ast.program.body as t.Statement[]).unshift(\n t.importDeclaration(\n [t.importSpecifier(t.identifier(\"rsc\"), t.identifier(\"rsc\"))],\n t.stringLiteral(rscImport),\n ),\n );\n }\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"]}
@@ -0,0 +1,33 @@
1
+ import { transform } from './chunk-MDYQVJ3W.js';
2
+ import { relative } from 'path';
3
+
4
+ function betterIntlLoader(source) {
5
+ const options = this.getOptions?.() ?? {};
6
+ const absolutePath = this.resourcePath;
7
+ const filePath = relative(this.rootContext || process.cwd(), absolutePath);
8
+ if (absolutePath.includes("node_modules")) return source;
9
+ if (!/\.[jt]sx$/.test(filePath)) return source;
10
+ if (/\.(test|spec|stories)\.[jt]sx?$/.test(filePath)) return source;
11
+ try {
12
+ const result = transform(source, {
13
+ filePath,
14
+ locale: options.locale,
15
+ messages: options.messages,
16
+ mode: options.mode ?? "auto"
17
+ });
18
+ if (result.hasTranslations) {
19
+ console.log(
20
+ `[better-intl] Transformed ${filePath} (${result.hasTranslations})`
21
+ );
22
+ return result.code;
23
+ }
24
+ return source;
25
+ } catch (err) {
26
+ console.error(`[better-intl] Error transforming ${filePath}:`, err);
27
+ return source;
28
+ }
29
+ }
30
+
31
+ export { betterIntlLoader };
32
+ //# sourceMappingURL=chunk-SF5H7SHR.js.map
33
+ //# sourceMappingURL=chunk-SF5H7SHR.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/webpack-loader.ts"],"names":[],"mappings":";;;AAiBe,SAAR,iBAA6C,MAAA,EAAwB;AAC1E,EAAA,MAAM,OAAA,GAAmC,IAAA,CAAK,UAAA,IAAa,IAAK,EAAC;AACjE,EAAA,MAAM,eAAe,IAAA,CAAK,YAAA;AAC1B,EAAA,MAAM,WAAW,QAAA,CAAS,IAAA,CAAK,eAAe,OAAA,CAAQ,GAAA,IAAO,YAAY,CAAA;AAGzE,EAAA,IAAI,YAAA,CAAa,QAAA,CAAS,cAAc,CAAA,EAAG,OAAO,MAAA;AAClD,EAAA,IAAI,CAAC,WAAA,CAAY,IAAA,CAAK,QAAQ,GAAG,OAAO,MAAA;AAGxC,EAAA,IAAI,iCAAA,CAAkC,IAAA,CAAK,QAAQ,CAAA,EAAG,OAAO,MAAA;AAE7D,EAAA,IAAI;AACF,IAAA,MAAM,MAAA,GAAS,UAAU,MAAA,EAAQ;AAAA,MAC/B,QAAA;AAAA,MACA,QAAQ,OAAA,CAAQ,MAAA;AAAA,MAChB,UAAU,OAAA,CAAQ,QAAA;AAAA,MAClB,IAAA,EAAM,QAAQ,IAAA,IAAQ;AAAA,KACvB,CAAA;AAED,IAAA,IAAI,OAAO,eAAA,EAAiB;AAC1B,MAAA,OAAA,CAAQ,GAAA;AAAA,QACN,CAAA,0BAAA,EAA6B,QAAQ,CAAA,EAAA,EAAK,MAAA,CAAO,eAAe,CAAA,CAAA;AAAA,OAClE;AACA,MAAA,OAAO,MAAA,CAAO,IAAA;AAAA,IAChB;AAEA,IAAA,OAAO,MAAA;AAAA,EACT,SAAS,GAAA,EAAK;AACZ,IAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,iCAAA,EAAoC,QAAQ,CAAA,CAAA,CAAA,EAAK,GAAG,CAAA;AAClE,IAAA,OAAO,MAAA;AAAA,EACT;AACF","file":"chunk-SF5H7SHR.js","sourcesContent":["/**\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"]}
package/dist/index.cjs CHANGED
@@ -208,12 +208,16 @@ function transform(source, options) {
208
208
  messages,
209
209
  tFunctionName = "__t",
210
210
  runtimeImport = "@better-intl/react",
211
+ rscImport = "@better-intl/next/rsc",
211
212
  mode = "auto"
212
213
  } = options;
213
214
  const ast = parser.parse(source, {
214
215
  sourceType: "module",
215
216
  plugins: ["jsx", "typescript"]
216
217
  });
218
+ const isClientComponent = ast.program.directives?.some((d) => d.value.value === "use client") ?? ast.program.body.some(
219
+ (node) => t__namespace.isExpressionStatement(node) && t__namespace.isStringLiteral(node.expression) && node.expression.value === "use client"
220
+ );
217
221
  let hasTranslations = false;
218
222
  let componentName;
219
223
  let needsImport = false;
@@ -269,9 +273,9 @@ function transform(source, options) {
269
273
  }
270
274
  });
271
275
  if (needsImport) {
272
- let injectHook2 = function(path) {
276
+ let injectTranslation2 = function(path) {
273
277
  if (!functionsNeedingHook.has(path.node)) return;
274
- const hookCall = t__namespace.variableDeclaration("const", [
278
+ const statement = isClientComponent ? t__namespace.variableDeclaration("const", [
275
279
  t__namespace.variableDeclarator(
276
280
  t__namespace.objectPattern([
277
281
  t__namespace.objectProperty(
@@ -283,41 +287,63 @@ function transform(source, options) {
283
287
  ]),
284
288
  t__namespace.callExpression(t__namespace.identifier("useTranslation"), [])
285
289
  )
290
+ ]) : t__namespace.variableDeclaration("const", [
291
+ t__namespace.variableDeclarator(
292
+ t__namespace.identifier(tFunctionName),
293
+ t__namespace.callExpression(t__namespace.identifier("rsc"), [])
294
+ )
286
295
  ]);
287
296
  const body = path.node.body;
288
297
  if (t__namespace.isBlockStatement(body)) {
289
- body.body.unshift(hookCall);
298
+ body.body.unshift(statement);
290
299
  } else {
291
- path.node.body = t__namespace.blockStatement([hookCall, t__namespace.returnStatement(body)]);
300
+ path.node.body = t__namespace.blockStatement([statement, t__namespace.returnStatement(body)]);
292
301
  }
293
302
  };
294
303
  traverse2(ast, {
295
304
  FunctionDeclaration(path) {
296
- injectHook2(path);
305
+ injectTranslation2(path);
297
306
  },
298
307
  FunctionExpression(path) {
299
- injectHook2(path);
308
+ injectTranslation2(path);
300
309
  },
301
310
  ArrowFunctionExpression(path) {
302
- injectHook2(path);
311
+ injectTranslation2(path);
303
312
  }
304
313
  });
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")
314
+ if (isClientComponent) {
315
+ const hasImport = ast.program.body.some(
316
+ (node) => t__namespace.isImportDeclaration(node) && node.source.value === runtimeImport && node.specifiers.some(
317
+ (s) => t__namespace.isImportSpecifier(s) && t__namespace.isIdentifier(s.imported) && s.imported.name === "useTranslation"
318
+ )
319
+ );
320
+ if (!hasImport) {
321
+ ast.program.body.unshift(
322
+ t__namespace.importDeclaration(
323
+ [
324
+ t__namespace.importSpecifier(
325
+ t__namespace.identifier("useTranslation"),
326
+ t__namespace.identifier("useTranslation")
327
+ )
328
+ ],
329
+ t__namespace.stringLiteral(runtimeImport)
316
330
  )
317
- ],
318
- t__namespace.stringLiteral(runtimeImport)
331
+ );
332
+ }
333
+ } else {
334
+ const hasImport = ast.program.body.some(
335
+ (node) => t__namespace.isImportDeclaration(node) && node.source.value === rscImport && node.specifiers.some(
336
+ (s) => t__namespace.isImportSpecifier(s) && t__namespace.isIdentifier(s.imported) && s.imported.name === "rsc"
337
+ )
319
338
  );
320
- ast.program.body.unshift(importDecl);
339
+ if (!hasImport) {
340
+ ast.program.body.unshift(
341
+ t__namespace.importDeclaration(
342
+ [t__namespace.importSpecifier(t__namespace.identifier("rsc"), t__namespace.identifier("rsc"))],
343
+ t__namespace.stringLiteral(rscImport)
344
+ )
345
+ );
346
+ }
321
347
  }
322
348
  }
323
349
  const output = generate(ast, { sourceMaps: true, sourceFileName: filePath });
@@ -1 +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"]}
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","injectTranslation","relative"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAcO,SAAS,uBACd,QAAA,EACa;AACb,EAAA,MAAM,UAAoB,EAAC;AAC3B,EAAA,KAAA,MAAW,OAAO,QAAA,EAAU;AAC1B,IAAA,OAAA,CAAQ,GAAA,CAAI,EAAE,CAAA,GAAI,GAAA,CAAI,cAAA;AAAA,EACxB;AACA,EAAA,OAAO,EAAE,MAAA,EAAQ,IAAA,EAAM,QAAA,EAAU,OAAA,EAAQ;AAC3C;AAMO,SAAS,YAAA,CACd,UACA,SAAA,EAMA;AACA,EAAA,MAAM,MAAA,GAAS,IAAI,GAAA,CAAI,SAAA,CAAU,IAAI,CAAC,CAAA,KAAM,CAAA,CAAE,EAAE,CAAC,CAAA;AACjD,EAAA,MAAM,cAAc,IAAI,GAAA,CAAI,MAAA,CAAO,IAAA,CAAK,QAAQ,CAAC,CAAA;AAEjD,EAAA,MAAM,QAAkB,EAAC;AACzB,EAAA,MAAM,UAAoB,EAAC;AAC3B,EAAA,MAAM,YAAsB,EAAC;AAC7B,EAAA,MAAM,WAAqB,EAAC;AAG5B,EAAA,KAAA,MAAW,OAAO,SAAA,EAAW;AAC3B,IAAA,IAAI,WAAA,CAAY,GAAA,CAAI,GAAA,CAAI,EAAE,CAAA,EAAG;AAC3B,MAAA,QAAA,CAAS,GAAA,CAAI,EAAE,CAAA,GAAI,QAAA,CAAS,IAAI,EAAE,CAAA;AAClC,MAAA,SAAA,CAAU,IAAA,CAAK,IAAI,EAAE,CAAA;AAAA,IACvB,CAAA,MAAO;AACL,MAAA,QAAA,CAAS,GAAA,CAAI,EAAE,CAAA,GAAI,GAAA,CAAI,cAAA;AACvB,MAAA,KAAA,CAAM,IAAA,CAAK,IAAI,EAAE,CAAA;AAAA,IACnB;AAAA,EACF;AAGA,EAAA,KAAA,MAAW,MAAM,WAAA,EAAa;AAC5B,IAAA,IAAI,CAAC,MAAA,CAAO,GAAA,CAAI,EAAE,CAAA,EAAG;AACnB,MAAA,OAAA,CAAQ,KAAK,EAAE,CAAA;AAAA,IACjB;AAAA,EACF;AAEA,EAAA,OAAO,EAAE,QAAA,EAAU,KAAA,EAAO,OAAA,EAAS,SAAA,EAAU;AAC/C;AAKO,SAAS,eAAA,CACd,gBACA,aAAA,EACU;AACV,EAAA,OAAO,MAAA,CAAO,KAAK,cAAc,CAAA,CAAE,OAAO,CAAC,EAAA,KAAO,EAAE,EAAA,IAAM,aAAA,CAAc,CAAA;AAC1E;AAKO,SAAS,YAAA,CACd,SACA,SAAA,EACU;AACV,EAAA,MAAM,YAAA,GAAe,IAAI,GAAA,CAAI,SAAA,CAAU,IAAI,CAAC,CAAA,KAAM,CAAA,CAAE,EAAE,CAAC,CAAA;AACvD,EAAA,OAAO,MAAA,CAAO,IAAA,CAAK,OAAO,CAAA,CAAE,MAAA,CAAO,CAAC,EAAA,KAAO,CAAC,YAAA,CAAa,GAAA,CAAI,EAAE,CAAC,CAAA;AAClE;ACtEA,IAAM,QAAA,GACJ,OAAOA,0BAAA,KAAc,UAAA,GACjBA,6BACCA,0BAAA,CAA4C,OAAA;AAGnD,IAAM,gBAAA,uBAAuB,GAAA,CAAI,CAAC,UAAU,OAAA,EAAS,MAAA,EAAQ,KAAA,EAAO,QAAQ,CAAC,CAAA;AAG7E,IAAM,qBAAA,GAAwB;AAAA,EAC5B,kBAAA;AAAA,EACA,kBAAA;AAAA,EACA,qBAAA;AAAA,EACA;AACF,CAAA;AAmBO,SAAS,OAAA,CACd,QACA,OAAA,EACoB;AACpB,EAAA,MAAM,EAAE,QAAA,EAAU,IAAA,GAAO,MAAA,EAAO,GAAI,OAAA;AAGpC,EAAA,IAAI,qBAAA,CAAsB,KAAK,CAAC,CAAA,KAAM,EAAE,IAAA,CAAK,QAAQ,CAAC,CAAA,EAAG;AACvD,IAAA,OAAO,EAAC;AAAA,EACV;AAEA,EAAA,MAAM,GAAA,GAAMC,aAAM,MAAA,EAAQ;AAAA,IACxB,UAAA,EAAY,QAAA;AAAA,IACZ,OAAA,EAAS,CAAC,KAAA,EAAO,YAAY;AAAA,GAC9B,CAAA;AAED,EAAA,MAAM,GAAA,GAAyB;AAAA,IAC7B,aAAA,EAAe,MAAA;AAAA,IACf,UAAU,EAAC;AAAA,IACX,YAAA,EAAc,sBAAsB,MAAM,CAAA;AAAA,IAC1C,QAAA;AAAA,IACA;AAAA,GACF;AAEA,EAAA,QAAA,CAAS,GAAA,EAAK;AAAA;AAAA,IAEZ,oBAAoB,IAAA,EAAuC;AACzD,MAAA,IAAI,IAAA,CAAK,KAAK,EAAA,EAAI;AAChB,QAAA,GAAA,CAAI,aAAA,GAAgB,IAAA,CAAK,IAAA,CAAK,EAAA,CAAG,IAAA;AAAA,MACnC;AAAA,IACF,CAAA;AAAA,IAEA,mBAAmB,IAAA,EAAsC;AACvD,MAAA,IACE,IAAA,CAAK,IAAA,CAAK,EAAA,CAAG,IAAA,KAAS,iBACrB,IAAA,CAAK,IAAA,CAAK,IAAA,EAAM,IAAA,KAAS,yBAAA,IACxB,IAAA,CAAK,IAAA,CAAK,IAAA,EAAM,SAAS,oBAAA,CAAA,EAC3B;AACA,QAAA,GAAA,CAAI,aAAA,GAAgB,IAAA,CAAK,IAAA,CAAK,EAAA,CAAG,IAAA;AAAA,MACnC;AAAA,IACF,CAAA;AAAA;AAAA,IAGA,QAAQ,IAAA,EAA2B;AACjC,MAAA,MAAM,IAAA,GAAO,IAAA,CAAK,IAAA,CAAK,KAAA,CAAM,IAAA,EAAK;AAClC,MAAA,IAAI,CAAC,IAAA,EAAM;AAEX,MAAA,MAAM,IAAA,GAAO,IAAA,CAAK,IAAA,CAAK,GAAA,EAAK,MAAM,IAAA,IAAQ,CAAA;AAC1C,MAAA,IAAI,GAAA,CAAI,YAAA,CAAa,GAAA,CAAI,IAAI,CAAA,IAAK,IAAI,YAAA,CAAa,GAAA,CAAI,IAAA,GAAO,CAAC,CAAA,EAAG;AAElE,MAAA,MAAM,aAAA,GAAgB,kBAAkB,IAAI,CAAA;AAC5C,MAAA,IAAI,CAAC,aAAA,EAAe;AAEpB,MAAA,MAAM,WAAA,GAAc,eAAe,aAAa,CAAA;AAChD,MAAA,IAAI,gBAAA,CAAiB,GAAA,CAAI,WAAW,CAAA,EAAG;AAGvC,MAAA,IAAI,GAAA,CAAI,SAAS,UAAA,EAAY;AAC3B,QAAA,MAAM,WAAA,GAAc,YAAA,CAAa,aAAA,EAAe,MAAM,CAAA;AACtD,QAAA,IAAI,WAAA,KAAgB,GAAA,IAAO,CAAC,WAAA,EAAa;AAAA,MAC3C;AAEA,MAAA,UAAA,CAAW,GAAA,EAAK,IAAA,EAAM,WAAA,EAAa,IAAA,CAAK,KAAK,GAAG,CAAA;AAAA,IAClD,CAAA;AAAA;AAAA,IAGA,uBAAuB,IAAA,EAA0C;AAC/D,MAAA,MAAM,IAAA,GAAO,KAAK,IAAA,CAAK,UAAA;AACvB,MAAA,IAAI,IAAA,CAAK,SAAS,eAAA,EAAiB;AAEnC,MAAA,MAAM,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,IAAA,EAAK;AAC7B,MAAA,IAAI,CAAC,IAAA,EAAM;AAEX,MAAA,MAAM,IAAA,GAAO,IAAA,CAAK,GAAA,EAAK,KAAA,CAAM,IAAA,IAAQ,CAAA;AACrC,MAAA,IAAI,GAAA,CAAI,YAAA,CAAa,GAAA,CAAI,IAAI,CAAA,IAAK,IAAI,YAAA,CAAa,GAAA,CAAI,IAAA,GAAO,CAAC,CAAA,EAAG;AAElE,MAAA,MAAM,aAAA,GAAgB,kBAAkB,IAAI,CAAA;AAC5C,MAAA,IAAI,CAAC,aAAA,EAAe;AAEpB,MAAA,MAAM,WAAA,GAAc,eAAe,aAAa,CAAA;AAChD,MAAA,IAAI,gBAAA,CAAiB,GAAA,CAAI,WAAW,CAAA,EAAG;AAEvC,MAAA,IAAI,GAAA,CAAI,SAAS,UAAA,EAAY;AAC3B,QAAA,MAAM,WAAA,GAAc,YAAA,CAAa,aAAA,EAAe,MAAM,CAAA;AACtD,QAAA,IAAI,WAAA,KAAgB,GAAA,IAAO,CAAC,WAAA,EAAa;AAAA,MAC3C;AAEA,MAAA,UAAA,CAAW,GAAA,EAAK,IAAA,EAAM,WAAA,EAAa,IAAA,CAAK,GAAG,CAAA;AAAA,IAC7C;AAAA,GACD,CAAA;AAED,EAAA,OAAO,GAAA,CAAI,QAAA;AACb;AAEA,SAAS,UAAA,CACP,GAAA,EACA,IAAA,EACA,WAAA,EACA,GAAA,EACM;AACN,EAAA,MAAM,KAAKC,eAAA,CAAW;AAAA,IACpB,IAAA;AAAA,IACA,UAAU,GAAA,CAAI,QAAA;AAAA,IACd,SAAS,GAAA,CAAI;AAAA,GACd,CAAA;AAGD,EAAA,IAAI,GAAA,CAAI,SAAS,IAAA,CAAK,CAAC,MAAM,CAAA,CAAE,EAAA,KAAO,EAAE,CAAA,EAAG;AAE3C,EAAA,GAAA,CAAI,SAAS,IAAA,CAAK;AAAA,IAChB,EAAA;AAAA,IACA,cAAA,EAAgB,IAAA;AAAA,IAChB,WAAA,EAAa,CAAA,EAAG,GAAA,CAAI,QAAQ,IAAI,WAAW,CAAA,CAAA;AAAA,IAC3C,UAAU,GAAA,CAAI,QAAA;AAAA,IACd,eAAe,GAAA,CAAI,aAAA;AAAA,IACnB,WAAA;AAAA,IACA,IAAA,EAAM,GAAA,EAAK,KAAA,CAAM,IAAA,IAAQ,CAAA;AAAA,IACzB,MAAA,EAAQ,GAAA,EAAK,KAAA,CAAM,MAAA,IAAU;AAAA,GAC9B,CAAA;AACH;AAEA,SAAS,kBAAkB,IAAA,EAA4C;AACrE,EAAA,IAAI,UAAU,IAAA,CAAK,UAAA;AACnB,EAAA,OAAO,OAAA,EAAS;AACd,IAAA,IAAI,OAAA,CAAQ,cAAa,EAAG;AAC1B,MAAA,OAAO,QAAQ,IAAA,CAAK,cAAA;AAAA,IACtB;AACA,IAAA,OAAA,GAAU,OAAA,CAAQ,UAAA;AAAA,EACpB;AACA,EAAA,OAAO,IAAA;AACT;AAEA,SAAS,eAAe,OAAA,EAAsC;AAC5D,EAAA,IAAI,OAAA,CAAQ,IAAA,CAAK,IAAA,KAAS,eAAA,EAAiB;AACzC,IAAA,OAAO,QAAQ,IAAA,CAAK,IAAA;AAAA,EACtB;AACA,EAAA,IAAI,OAAA,CAAQ,IAAA,CAAK,IAAA,KAAS,qBAAA,EAAuB;AAC/C,IAAA,OAAO,CAAA,EAAG,aAAA,CAAc,OAAA,CAAQ,IAAI,CAAC,CAAA,CAAA;AAAA,EACvC;AACA,EAAA,OAAO,SAAA;AACT;AAEA,SAAS,cAAc,IAAA,EAAqC;AAC1D,EAAA,MAAM,MAAA,GACJ,IAAA,CAAK,MAAA,CAAO,IAAA,KAAS,eAAA,GACjB,KAAK,MAAA,CAAO,IAAA,GACZ,aAAA,CAAc,IAAA,CAAK,MAAM,CAAA;AAC/B,EAAA,OAAO,CAAA,EAAG,MAAM,CAAA,CAAA,EAAI,IAAA,CAAK,SAAS,IAAI,CAAA,CAAA;AACxC;AAEA,SAAS,YAAA,CAAa,SAA8B,IAAA,EAAuB;AACzE,EAAA,OAAO,QAAQ,UAAA,CAAW,IAAA;AAAA,IACxB,CAAC,IAAA,KACC,IAAA,CAAK,IAAA,KAAS,cAAA,IACd,IAAA,CAAK,IAAA,CAAK,IAAA,KAAS,eAAA,IACnB,IAAA,CAAK,IAAA,CAAK,IAAA,KAAS;AAAA,GACvB;AACF;AAKA,SAAS,sBAAsB,MAAA,EAA6B;AAC1D,EAAA,MAAM,OAAA,uBAAc,GAAA,EAAY;AAChC,EAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,KAAA,CAAM,IAAI,CAAA;AAC/B,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,KAAA,CAAM,QAAQ,CAAA,EAAA,EAAK;AACrC,IAAA,IAAI,KAAA,CAAM,CAAC,CAAA,CAAE,QAAA,CAAS,aAAa,CAAA,EAAG;AACpC,MAAA,OAAA,CAAQ,GAAA,CAAI,IAAI,CAAC,CAAA;AAAA,IACnB;AAAA,EACF;AACA,EAAA,OAAO,OAAA;AACT;ACzMA,IAAMC,SAAAA,GACJ,OAAOH,0BAAAA,KAAc,UAAA,GACjBA,6BACCA,0BAAAA,CAA4C,OAAA;AAEnD,IAAM,QAAA,GACJ,OAAOI,0BAAA,KAAc,UAAA,GACjBA,6BACCA,0BAAA,CAA4C,OAAA;AAEnD,IAAMC,iBAAAA,uBAAuB,GAAA,CAAI,CAAC,UAAU,OAAA,EAAS,MAAA,EAAQ,KAAA,EAAO,QAAQ,CAAC,CAAA;AAsBtE,SAAS,SAAA,CACd,QACA,OAAA,EACiB;AACjB,EAAA,MAAM;AAAA,IACJ,QAAA;AAAA,IACA,MAAA;AAAA,IACA,QAAA;AAAA,IACA,aAAA,GAAgB,KAAA;AAAA,IAChB,aAAA,GAAgB,oBAAA;AAAA,IAChB,SAAA,GAAY,uBAAA;AAAA,IACZ,IAAA,GAAO;AAAA,GACT,GAAI,OAAA;AAEJ,EAAA,MAAM,GAAA,GAAMJ,aAAM,MAAA,EAAQ;AAAA,IACxB,UAAA,EAAY,QAAA;AAAA,IACZ,OAAA,EAAS,CAAC,KAAA,EAAO,YAAY;AAAA,GAC9B,CAAA;AAGD,EAAA,MAAM,iBAAA,GACJ,GAAA,CAAI,OAAA,CAAQ,UAAA,EAAY,KAAK,CAAC,CAAA,KAAM,CAAA,CAAE,KAAA,CAAM,KAAA,KAAU,YAAY,CAAA,IAClE,GAAA,CAAI,QAAQ,IAAA,CAAK,IAAA;AAAA,IACf,CAAC,IAAA,KACGK,YAAA,CAAA,qBAAA,CAAsB,IAAI,CAAA,IAC1BA,YAAA,CAAA,eAAA,CAAgB,IAAA,CAAK,UAAU,CAAA,IACjC,IAAA,CAAK,UAAA,CAAW,KAAA,KAAU;AAAA,GAC9B;AAEF,EAAA,IAAI,eAAA,GAAkB,KAAA;AACtB,EAAA,IAAI,aAAA;AACJ,EAAA,IAAI,WAAA,GAAc,KAAA;AAGlB,EAAA,MAAM,oBAAA,uBAA2B,GAAA,EAAY;AAE7C,EAAAH,UAAS,GAAA,EAAK;AAAA,IACZ,oBAAoB,IAAA,EAAuC;AACzD,MAAA,IAAI,KAAK,IAAA,CAAK,EAAA,EAAI,aAAA,GAAgB,IAAA,CAAK,KAAK,EAAA,CAAG,IAAA;AAAA,IACjD,CAAA;AAAA,IAEA,mBAAmB,IAAA,EAAsC;AACvD,MAAA,IACE,IAAA,CAAK,IAAA,CAAK,EAAA,CAAG,IAAA,KAAS,iBACrB,IAAA,CAAK,IAAA,CAAK,IAAA,EAAM,IAAA,KAAS,yBAAA,IACxB,IAAA,CAAK,IAAA,CAAK,IAAA,EAAM,SAAS,oBAAA,CAAA,EAC3B;AACA,QAAA,aAAA,GAAgB,IAAA,CAAK,KAAK,EAAA,CAAG,IAAA;AAAA,MAC/B;AAAA,IACF,CAAA;AAAA,IAEA,QAAQ,IAAA,EAA2B;AACjC,MAAA,MAAM,IAAA,GAAO,IAAA,CAAK,IAAA,CAAK,KAAA,CAAM,IAAA,EAAK;AAClC,MAAA,IAAI,CAAC,IAAA,EAAM;AAEX,MAAA,MAAM,SAAS,IAAA,CAAK,UAAA;AACpB,MAAA,IAAI,CAAC,MAAA,EAAQ,YAAA,EAAa,EAAG;AAE7B,MAAA,MAAM,WAAA,GAAc,UAAA,CAAW,MAAA,CAAO,IAAA,CAAK,cAAc,CAAA;AACzD,MAAA,IAAIE,iBAAAA,CAAiB,GAAA,CAAI,WAAW,CAAA,EAAG;AAGvC,MAAA,MAAM,IAAA,GAAO,IAAA,CAAK,IAAA,CAAK,GAAA,EAAK,MAAM,IAAA,IAAQ,CAAA;AAC1C,MAAA,IAAI,gBAAA,CAAiB,MAAA,EAAQ,IAAI,CAAA,EAAG;AAGpC,MAAA,IAAI,SAAS,UAAA,EAAY;AACvB,QAAA,MAAM,OAAA,GAAU,MAAA,CAAO,IAAA,CAAK,cAAA,CAAe,UAAA,CAAW,IAAA;AAAA,UACpD,CAAC,CAAA,KACC,CAAA,CAAE,IAAA,KAAS,cAAA,IACX,CAAA,CAAE,IAAA,CAAK,IAAA,KAAS,eAAA,IAChB,CAAA,CAAE,IAAA,CAAK,IAAA,KAAS;AAAA,SACpB;AACA,QAAA,IAAI,WAAA,KAAgB,GAAA,IAAO,CAAC,OAAA,EAAS;AAAA,MACvC;AAEA,MAAA,MAAM,KAAKH,eAAAA,CAAW,EAAE,MAAM,QAAA,EAAU,OAAA,EAAS,eAAe,CAAA;AAChE,MAAA,eAAA,GAAkB,IAAA;AAElB,MAAA,IAAI,MAAA,IAAU,QAAA,GAAW,EAAE,CAAA,EAAG;AAE5B,QAAA,MAAM,OAAA,GAAYI,YAAA,CAAA,OAAA,CAAQ,QAAA,CAAS,EAAE,CAAC,CAAA;AACtC,QAAA,IAAA,CAAK,YAAY,OAAO,CAAA;AACxB,QAAA,IAAA,CAAK,IAAA,EAAK;AAAA,MACZ,CAAA,MAAO;AAEL,QAAA,WAAA,GAAc,IAAA;AACd,QAAA,IAAA,CAAK,WAAA;AAAA,UACDA,YAAA,CAAA,sBAAA;AAAA,YACEA,YAAA,CAAA,cAAA,CAAiBA,YAAA,CAAA,UAAA,CAAW,aAAa,CAAA,EAAG;AAAA,cAC1CA,2BAAc,EAAE,CAAA;AAAA,cAChBA,wBAAW,WAAW,CAAA;AAAA,cACtBA,2BAAc,IAAI;AAAA,aACrB;AAAA;AACH,SACF;AAGA,QAAA,MAAM,SAAS,IAAA,CAAK,UAAA;AAAA,UAClB,CAAC,MACC,CAAA,CAAE,qBAAA,MACF,CAAA,CAAE,oBAAA,EAAqB,IACvB,CAAA,CAAE,yBAAA;AAA0B,SAChC;AACA,QAAA,IAAI,MAAA,EAAQ;AACV,UAAA,oBAAA,CAAqB,GAAA,CAAI,OAAO,IAAI,CAAA;AAAA,QACtC;AAAA,MACF;AAAA,IACF;AAAA,GACD,CAAA;AAGD,EAAA,IAAI,WAAA,EAAa;AAaf,IAAA,IAASC,kBAAAA,GAAT,SACE,IAAA,EAGA;AACA,MAAA,IAAI,CAAC,oBAAA,CAAqB,GAAA,CAAI,IAAA,CAAK,IAAI,CAAA,EAAG;AAI1C,MAAA,MAAM,SAAA,GAAY,iBAAA,GACZD,YAAA,CAAA,mBAAA,CAAoB,OAAA,EAAS;AAAA,QAC3BA,YAAA,CAAA,kBAAA;AAAA,UACEA,YAAA,CAAA,aAAA,CAAc;AAAA,YACZA,YAAA,CAAA,cAAA;AAAA,cACEA,wBAAW,GAAG,CAAA;AAAA,cACdA,wBAAW,aAAa,CAAA;AAAA,cAC1B,KAAA;AAAA,cACA;AAAA;AACF,WACD,CAAA;AAAA,UACCA,YAAA,CAAA,cAAA,CAAiBA,YAAA,CAAA,UAAA,CAAW,gBAAgB,CAAA,EAAG,EAAE;AAAA;AACrD,OACD,CAAA,GACCA,YAAA,CAAA,mBAAA,CAAoB,OAAA,EAAS;AAAA,QAC3BA,YAAA,CAAA,kBAAA;AAAA,UACEA,wBAAW,aAAa,CAAA;AAAA,UACxBA,YAAA,CAAA,cAAA,CAAiBA,YAAA,CAAA,UAAA,CAAW,KAAK,CAAA,EAAG,EAAE;AAAA;AAC1C,OACD,CAAA;AAEL,MAAA,MAAM,IAAA,GAAO,KAAK,IAAA,CAAK,IAAA;AACvB,MAAA,IAAMA,YAAA,CAAA,gBAAA,CAAiB,IAAI,CAAA,EAAG;AAC5B,QAAA,IAAA,CAAK,IAAA,CAAK,QAAQ,SAAS,CAAA;AAAA,MAC7B,CAAA,MAAO;AACL,QAAA,IAAA,CAAK,IAAA,CAAK,OAASA,YAAA,CAAA,cAAA,CAAe,CAAC,WAAaA,YAAA,CAAA,eAAA,CAAgB,IAAI,CAAC,CAAC,CAAA;AAAA,MACxE;AAAA,IACF,CAAA;AAhDA,IAAAH,UAAS,GAAA,EAAK;AAAA,MACZ,oBAAoB,IAAA,EAAuC;AACzD,QAAAI,mBAAkB,IAAI,CAAA;AAAA,MACxB,CAAA;AAAA,MACA,mBAAmB,IAAA,EAAsC;AACvD,QAAAA,mBAAkB,IAAI,CAAA;AAAA,MACxB,CAAA;AAAA,MACA,wBAAwB,IAAA,EAA2C;AACjE,QAAAA,mBAAkB,IAAI,CAAA;AAAA,MACxB;AAAA,KACD,CAAA;AAwCD,IAAA,IAAI,iBAAA,EAAmB;AAErB,MAAA,MAAM,SAAA,GAAY,GAAA,CAAI,OAAA,CAAQ,IAAA,CAAK,IAAA;AAAA,QACjC,CAAC,IAAA,KACGD,YAAA,CAAA,mBAAA,CAAoB,IAAI,CAAA,IAC1B,KAAK,MAAA,CAAO,KAAA,KAAU,aAAA,IACtB,IAAA,CAAK,UAAA,CAAW,IAAA;AAAA,UACd,CAAC,CAAA,KACGA,YAAA,CAAA,iBAAA,CAAkB,CAAC,CAAA,IACnBA,YAAA,CAAA,YAAA,CAAa,CAAA,CAAE,QAAQ,CAAA,IACzB,CAAA,CAAE,QAAA,CAAS,IAAA,KAAS;AAAA;AACxB,OACJ;AAEA,MAAA,IAAI,CAAC,SAAA,EAAW;AACd,QAAC,GAAA,CAAI,QAAQ,IAAA,CAAuB,OAAA;AAAA,UAChCA,YAAA,CAAA,iBAAA;AAAA,YACA;AAAA,cACIA,YAAA,CAAA,eAAA;AAAA,gBACEA,wBAAW,gBAAgB,CAAA;AAAA,gBAC3BA,wBAAW,gBAAgB;AAAA;AAC/B,aACF;AAAA,YACEA,2BAAc,aAAa;AAAA;AAC/B,SACF;AAAA,MACF;AAAA,IACF,CAAA,MAAO;AAEL,MAAA,MAAM,SAAA,GAAY,GAAA,CAAI,OAAA,CAAQ,IAAA,CAAK,IAAA;AAAA,QACjC,CAAC,IAAA,KACGA,YAAA,CAAA,mBAAA,CAAoB,IAAI,CAAA,IAC1B,KAAK,MAAA,CAAO,KAAA,KAAU,SAAA,IACtB,IAAA,CAAK,UAAA,CAAW,IAAA;AAAA,UACd,CAAC,CAAA,KACGA,YAAA,CAAA,iBAAA,CAAkB,CAAC,CAAA,IACnBA,YAAA,CAAA,YAAA,CAAa,CAAA,CAAE,QAAQ,CAAA,IACzB,CAAA,CAAE,QAAA,CAAS,IAAA,KAAS;AAAA;AACxB,OACJ;AAEA,MAAA,IAAI,CAAC,SAAA,EAAW;AACd,QAAC,GAAA,CAAI,QAAQ,IAAA,CAAuB,OAAA;AAAA,UAChCA,YAAA,CAAA,iBAAA;AAAA,YACA,CAAGA,6BAAkBA,YAAA,CAAA,UAAA,CAAW,KAAK,GAAKA,YAAA,CAAA,UAAA,CAAW,KAAK,CAAC,CAAC,CAAA;AAAA,YAC1DA,2BAAc,SAAS;AAAA;AAC3B,SACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,EAAA,MAAM,MAAA,GAAS,SAAS,GAAA,EAAK,EAAE,YAAY,IAAA,EAAM,cAAA,EAAgB,UAAU,CAAA;AAE3E,EAAA,OAAO;AAAA,IACL,MAAM,MAAA,CAAO,IAAA;AAAA,IACb,KAAK,MAAA,CAAO,GAAA;AAAA,IACZ;AAAA,GACF;AACF;AAEA,SAAS,WAAW,OAAA,EAAsC;AACxD,EAAA,IAAI,QAAQ,IAAA,CAAK,IAAA,KAAS,eAAA,EAAiB,OAAO,QAAQ,IAAA,CAAK,IAAA;AAC/D,EAAA,OAAO,SAAA;AACT;AAEA,SAAS,gBAAA,CAAiB,QAAgB,IAAA,EAAuB;AAC/D,EAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,KAAA,CAAM,IAAI,CAAA;AAC/B,EAAA,MAAM,QAAA,GAAW,KAAA,CAAM,IAAA,GAAO,CAAC,CAAA;AAC/B,EAAA,OAAO,QAAA,GAAW,QAAA,CAAS,QAAA,CAAS,aAAa,CAAA,GAAI,KAAA;AACvD;AC1Qe,SAAR,iBAA6C,MAAA,EAAwB;AAC1E,EAAA,MAAM,OAAA,GAAmC,IAAA,CAAK,UAAA,IAAa,IAAK,EAAC;AACjE,EAAA,MAAM,eAAe,IAAA,CAAK,YAAA;AAC1B,EAAA,MAAM,WAAWE,aAAA,CAAS,IAAA,CAAK,eAAe,OAAA,CAAQ,GAAA,IAAO,YAAY,CAAA;AAGzE,EAAA,IAAI,YAAA,CAAa,QAAA,CAAS,cAAc,CAAA,EAAG,OAAO,MAAA;AAClD,EAAA,IAAI,CAAC,WAAA,CAAY,IAAA,CAAK,QAAQ,GAAG,OAAO,MAAA;AAGxC,EAAA,IAAI,iCAAA,CAAkC,IAAA,CAAK,QAAQ,CAAA,EAAG,OAAO,MAAA;AAE7D,EAAA,IAAI;AACF,IAAA,MAAM,MAAA,GAAS,UAAU,MAAA,EAAQ;AAAA,MAC/B,QAAA;AAAA,MACA,QAAQ,OAAA,CAAQ,MAAA;AAAA,MAChB,UAAU,OAAA,CAAQ,QAAA;AAAA,MAClB,IAAA,EAAM,QAAQ,IAAA,IAAQ;AAAA,KACvB,CAAA;AAED,IAAA,IAAI,OAAO,eAAA,EAAiB;AAC1B,MAAA,OAAA,CAAQ,GAAA;AAAA,QACN,CAAA,0BAAA,EAA6B,QAAQ,CAAA,EAAA,EAAK,MAAA,CAAO,eAAe,CAAA,CAAA;AAAA,OAClE;AACA,MAAA,OAAO,MAAA,CAAO,IAAA;AAAA,IAChB;AAEA,IAAA,OAAO,MAAA;AAAA,EACT,SAAS,GAAA,EAAK;AACZ,IAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,iCAAA,EAAoC,QAAQ,CAAA,CAAA,CAAA,EAAK,GAAG,CAAA;AAClE,IAAA,OAAO,MAAA;AAAA,EACT;AACF","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 client runtime (default: `@better-intl/react`) */\n runtimeImport?: string;\n /** Import path for the RSC runtime (default: `@better-intl/next/rsc`) */\n rscImport?: 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 rscImport = \"@better-intl/next/rsc\",\n mode = \"auto\",\n } = options;\n\n const ast = parse(source, {\n sourceType: \"module\",\n plugins: [\"jsx\", \"typescript\"],\n });\n\n // Detect \"use client\" directive\n const isClientComponent =\n ast.program.directives?.some((d) => d.value.value === \"use client\") ??\n ast.program.body.some(\n (node) =>\n t.isExpressionStatement(node) &&\n t.isStringLiteral(node.expression) &&\n node.expression.value === \"use client\",\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 translation accessor at the top of each component\n if (needsImport) {\n traverse(ast, {\n FunctionDeclaration(path: NodePath<t.FunctionDeclaration>) {\n injectTranslation(path);\n },\n FunctionExpression(path: NodePath<t.FunctionExpression>) {\n injectTranslation(path);\n },\n ArrowFunctionExpression(path: NodePath<t.ArrowFunctionExpression>) {\n injectTranslation(path);\n },\n });\n\n function injectTranslation(\n path: NodePath<\n t.FunctionDeclaration | t.FunctionExpression | t.ArrowFunctionExpression\n >,\n ) {\n if (!functionsNeedingHook.has(path.node)) return;\n\n // Client: const { t: __t } = useTranslation();\n // Server: const __t = rsc();\n const statement = isClientComponent\n ? 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 : t.variableDeclaration(\"const\", [\n t.variableDeclarator(\n t.identifier(tFunctionName),\n t.callExpression(t.identifier(\"rsc\"), []),\n ),\n ]);\n\n const body = path.node.body;\n if (t.isBlockStatement(body)) {\n body.body.unshift(statement);\n } else {\n path.node.body = t.blockStatement([statement, t.returnStatement(body)]);\n }\n }\n\n if (isClientComponent) {\n // Add import { useTranslation } from \"@better-intl/react\"\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 (ast.program.body as t.Statement[]).unshift(\n t.importDeclaration(\n [\n t.importSpecifier(\n t.identifier(\"useTranslation\"),\n t.identifier(\"useTranslation\"),\n ),\n ],\n t.stringLiteral(runtimeImport),\n ),\n );\n }\n } else {\n // Add import { rsc } from \"@better-intl/next/rsc\"\n const hasImport = ast.program.body.some(\n (node) =>\n t.isImportDeclaration(node) &&\n node.source.value === rscImport &&\n node.specifiers.some(\n (s) =>\n t.isImportSpecifier(s) &&\n t.isIdentifier(s.imported) &&\n s.imported.name === \"rsc\",\n ),\n );\n\n if (!hasImport) {\n (ast.program.body as t.Statement[]).unshift(\n t.importDeclaration(\n [t.importSpecifier(t.identifier(\"rsc\"), t.identifier(\"rsc\"))],\n t.stringLiteral(rscImport),\n ),\n );\n }\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"]}
package/dist/index.d.cts CHANGED
@@ -68,8 +68,10 @@ interface TransformOptions {
68
68
  messages?: Messages;
69
69
  /** The identifier for the `t` function (default: `__t`) */
70
70
  tFunctionName?: string;
71
- /** Import path for the runtime (default: `@better-intl/react`) */
71
+ /** Import path for the client runtime (default: `@better-intl/react`) */
72
72
  runtimeImport?: string;
73
+ /** Import path for the RSC runtime (default: `@better-intl/next/rsc`) */
74
+ rscImport?: string;
73
75
  mode?: "auto" | "explicit";
74
76
  }
75
77
  interface TransformResult {
package/dist/index.d.ts CHANGED
@@ -68,8 +68,10 @@ interface TransformOptions {
68
68
  messages?: Messages;
69
69
  /** The identifier for the `t` function (default: `__t`) */
70
70
  tFunctionName?: string;
71
- /** Import path for the runtime (default: `@better-intl/react`) */
71
+ /** Import path for the client runtime (default: `@better-intl/react`) */
72
72
  runtimeImport?: string;
73
+ /** Import path for the RSC runtime (default: `@better-intl/next/rsc`) */
74
+ rscImport?: string;
73
75
  mode?: "auto" | "explicit";
74
76
  }
75
77
  interface TransformResult {
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
- export { transform, betterIntlLoader as webpackLoader } from './chunk-RKXQPVVG.js';
1
+ export { betterIntlLoader as webpackLoader } from './chunk-SF5H7SHR.js';
2
+ export { transform } from './chunk-MDYQVJ3W.js';
2
3
  import { parse } from '@babel/parser';
3
4
  import _traverse from '@babel/traverse';
4
5
  import { generateId } from '@better-intl/core';
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/catalog-generator.ts","../src/extractor.ts"],"names":[],"mappings":"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","file":"index.js","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"]}
1
+ {"version":3,"sources":["../src/catalog-generator.ts","../src/extractor.ts"],"names":[],"mappings":"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","file":"index.js","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"]}
@@ -0,0 +1,223 @@
1
+ 'use strict';
2
+
3
+ var path = require('path');
4
+ var _generate = require('@babel/generator');
5
+ var parser = require('@babel/parser');
6
+ var _traverse = require('@babel/traverse');
7
+ var t = require('@babel/types');
8
+ var core = require('@better-intl/core');
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 _generate__default = /*#__PURE__*/_interopDefault(_generate);
31
+ var _traverse__default = /*#__PURE__*/_interopDefault(_traverse);
32
+ var t__namespace = /*#__PURE__*/_interopNamespace(t);
33
+
34
+ // src/loader.ts
35
+ var traverse = typeof _traverse__default.default === "function" ? _traverse__default.default : _traverse__default.default.default;
36
+ var generate = typeof _generate__default.default === "function" ? _generate__default.default : _generate__default.default.default;
37
+ var IGNORED_ELEMENTS = /* @__PURE__ */ new Set(["script", "style", "code", "pre", "Helmet"]);
38
+ function transform(source, options) {
39
+ const {
40
+ filePath,
41
+ locale,
42
+ messages,
43
+ tFunctionName = "__t",
44
+ runtimeImport = "@better-intl/react",
45
+ rscImport = "@better-intl/next/rsc",
46
+ mode = "auto"
47
+ } = options;
48
+ const ast = parser.parse(source, {
49
+ sourceType: "module",
50
+ plugins: ["jsx", "typescript"]
51
+ });
52
+ const isClientComponent = ast.program.directives?.some((d) => d.value.value === "use client") ?? ast.program.body.some(
53
+ (node) => t__namespace.isExpressionStatement(node) && t__namespace.isStringLiteral(node.expression) && node.expression.value === "use client"
54
+ );
55
+ let hasTranslations = false;
56
+ let componentName;
57
+ let needsImport = false;
58
+ const functionsNeedingHook = /* @__PURE__ */ new Set();
59
+ traverse(ast, {
60
+ FunctionDeclaration(path) {
61
+ if (path.node.id) componentName = path.node.id.name;
62
+ },
63
+ VariableDeclarator(path) {
64
+ if (path.node.id.type === "Identifier" && (path.node.init?.type === "ArrowFunctionExpression" || path.node.init?.type === "FunctionExpression")) {
65
+ componentName = path.node.id.name;
66
+ }
67
+ },
68
+ JSXText(path) {
69
+ const text = path.node.value.trim();
70
+ if (!text) return;
71
+ const parent = path.parentPath;
72
+ if (!parent?.isJSXElement()) return;
73
+ const elementName = getJSXName(parent.node.openingElement);
74
+ if (IGNORED_ELEMENTS.has(elementName)) return;
75
+ const line = path.node.loc?.start.line ?? 0;
76
+ if (hasIgnoreComment(source, line)) return;
77
+ if (mode === "explicit") {
78
+ const hasI18n = parent.node.openingElement.attributes.some(
79
+ (a) => a.type === "JSXAttribute" && a.name.type === "JSXIdentifier" && a.name.name === "i18n"
80
+ );
81
+ if (elementName !== "T" && !hasI18n) return;
82
+ }
83
+ const id = core.generateId({ text, filePath, context: componentName });
84
+ hasTranslations = true;
85
+ if (locale && messages?.[id]) {
86
+ const newNode = t__namespace.jsxText(messages[id]);
87
+ path.replaceWith(newNode);
88
+ path.skip();
89
+ } else {
90
+ needsImport = true;
91
+ path.replaceWith(
92
+ t__namespace.jsxExpressionContainer(
93
+ t__namespace.callExpression(t__namespace.identifier(tFunctionName), [
94
+ t__namespace.stringLiteral(id),
95
+ t__namespace.identifier("undefined"),
96
+ t__namespace.stringLiteral(text)
97
+ ])
98
+ )
99
+ );
100
+ const fnPath = path.findParent(
101
+ (p) => p.isFunctionDeclaration() || p.isFunctionExpression() || p.isArrowFunctionExpression()
102
+ );
103
+ if (fnPath) {
104
+ functionsNeedingHook.add(fnPath.node);
105
+ }
106
+ }
107
+ }
108
+ });
109
+ if (needsImport) {
110
+ let injectTranslation2 = function(path) {
111
+ if (!functionsNeedingHook.has(path.node)) return;
112
+ const statement = isClientComponent ? t__namespace.variableDeclaration("const", [
113
+ t__namespace.variableDeclarator(
114
+ t__namespace.objectPattern([
115
+ t__namespace.objectProperty(
116
+ t__namespace.identifier("t"),
117
+ t__namespace.identifier(tFunctionName),
118
+ false,
119
+ false
120
+ )
121
+ ]),
122
+ t__namespace.callExpression(t__namespace.identifier("useTranslation"), [])
123
+ )
124
+ ]) : t__namespace.variableDeclaration("const", [
125
+ t__namespace.variableDeclarator(
126
+ t__namespace.identifier(tFunctionName),
127
+ t__namespace.callExpression(t__namespace.identifier("rsc"), [])
128
+ )
129
+ ]);
130
+ const body = path.node.body;
131
+ if (t__namespace.isBlockStatement(body)) {
132
+ body.body.unshift(statement);
133
+ } else {
134
+ path.node.body = t__namespace.blockStatement([statement, t__namespace.returnStatement(body)]);
135
+ }
136
+ };
137
+ traverse(ast, {
138
+ FunctionDeclaration(path) {
139
+ injectTranslation2(path);
140
+ },
141
+ FunctionExpression(path) {
142
+ injectTranslation2(path);
143
+ },
144
+ ArrowFunctionExpression(path) {
145
+ injectTranslation2(path);
146
+ }
147
+ });
148
+ if (isClientComponent) {
149
+ const hasImport = ast.program.body.some(
150
+ (node) => t__namespace.isImportDeclaration(node) && node.source.value === runtimeImport && node.specifiers.some(
151
+ (s) => t__namespace.isImportSpecifier(s) && t__namespace.isIdentifier(s.imported) && s.imported.name === "useTranslation"
152
+ )
153
+ );
154
+ if (!hasImport) {
155
+ ast.program.body.unshift(
156
+ t__namespace.importDeclaration(
157
+ [
158
+ t__namespace.importSpecifier(
159
+ t__namespace.identifier("useTranslation"),
160
+ t__namespace.identifier("useTranslation")
161
+ )
162
+ ],
163
+ t__namespace.stringLiteral(runtimeImport)
164
+ )
165
+ );
166
+ }
167
+ } else {
168
+ const hasImport = ast.program.body.some(
169
+ (node) => t__namespace.isImportDeclaration(node) && node.source.value === rscImport && node.specifiers.some(
170
+ (s) => t__namespace.isImportSpecifier(s) && t__namespace.isIdentifier(s.imported) && s.imported.name === "rsc"
171
+ )
172
+ );
173
+ if (!hasImport) {
174
+ ast.program.body.unshift(
175
+ t__namespace.importDeclaration(
176
+ [t__namespace.importSpecifier(t__namespace.identifier("rsc"), t__namespace.identifier("rsc"))],
177
+ t__namespace.stringLiteral(rscImport)
178
+ )
179
+ );
180
+ }
181
+ }
182
+ }
183
+ const output = generate(ast, { sourceMaps: true, sourceFileName: filePath });
184
+ return {
185
+ code: output.code,
186
+ map: output.map,
187
+ hasTranslations
188
+ };
189
+ }
190
+ function getJSXName(opening) {
191
+ if (opening.name.type === "JSXIdentifier") return opening.name.name;
192
+ return "unknown";
193
+ }
194
+ function hasIgnoreComment(source, line) {
195
+ const lines = source.split("\n");
196
+ const prevLine = lines[line - 2];
197
+ return prevLine ? prevLine.includes("i18n-ignore") : false;
198
+ }
199
+
200
+ // src/loader.ts
201
+ function betterIntlLoader(source) {
202
+ const options = this.getOptions?.() ?? {};
203
+ const absolutePath = this.resourcePath;
204
+ if (absolutePath.includes("node_modules")) return source;
205
+ if (!/\.[jt]sx$/.test(absolutePath)) return source;
206
+ if (/\.(test|spec|stories)\.[jt]sx?$/.test(absolutePath)) return source;
207
+ const filePath = path.relative(this.rootContext || process.cwd(), absolutePath);
208
+ try {
209
+ const result = transform(source, {
210
+ filePath,
211
+ locale: options.locale,
212
+ messages: options.messages,
213
+ mode: options.mode ?? "auto"
214
+ });
215
+ return result.hasTranslations ? result.code : source;
216
+ } catch {
217
+ return source;
218
+ }
219
+ }
220
+
221
+ module.exports = betterIntlLoader;
222
+ //# sourceMappingURL=loader.cjs.map
223
+ //# sourceMappingURL=loader.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/transformer.ts","../src/loader.ts"],"names":["_traverse","_generate","parse","t","generateId","injectTranslation","relative"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkBA,IAAM,QAAA,GACJ,OAAOA,0BAAA,KAAc,UAAA,GACjBA,6BACCA,0BAAA,CAA4C,OAAA;AAEnD,IAAM,QAAA,GACJ,OAAOC,0BAAA,KAAc,UAAA,GACjBA,6BACCA,0BAAA,CAA4C,OAAA;AAEnD,IAAM,gBAAA,uBAAuB,GAAA,CAAI,CAAC,UAAU,OAAA,EAAS,MAAA,EAAQ,KAAA,EAAO,QAAQ,CAAC,CAAA;AAsBtE,SAAS,SAAA,CACd,QACA,OAAA,EACiB;AACjB,EAAA,MAAM;AAAA,IACJ,QAAA;AAAA,IACA,MAAA;AAAA,IACA,QAAA;AAAA,IACA,aAAA,GAAgB,KAAA;AAAA,IAChB,aAAA,GAAgB,oBAAA;AAAA,IAChB,SAAA,GAAY,uBAAA;AAAA,IACZ,IAAA,GAAO;AAAA,GACT,GAAI,OAAA;AAEJ,EAAA,MAAM,GAAA,GAAMC,aAAM,MAAA,EAAQ;AAAA,IACxB,UAAA,EAAY,QAAA;AAAA,IACZ,OAAA,EAAS,CAAC,KAAA,EAAO,YAAY;AAAA,GAC9B,CAAA;AAGD,EAAA,MAAM,iBAAA,GACJ,GAAA,CAAI,OAAA,CAAQ,UAAA,EAAY,KAAK,CAAC,CAAA,KAAM,CAAA,CAAE,KAAA,CAAM,KAAA,KAAU,YAAY,CAAA,IAClE,GAAA,CAAI,QAAQ,IAAA,CAAK,IAAA;AAAA,IACf,CAAC,IAAA,KACGC,YAAA,CAAA,qBAAA,CAAsB,IAAI,CAAA,IAC1BA,YAAA,CAAA,eAAA,CAAgB,IAAA,CAAK,UAAU,CAAA,IACjC,IAAA,CAAK,UAAA,CAAW,KAAA,KAAU;AAAA,GAC9B;AAEF,EAAA,IAAI,eAAA,GAAkB,KAAA;AACtB,EAAA,IAAI,aAAA;AACJ,EAAA,IAAI,WAAA,GAAc,KAAA;AAGlB,EAAA,MAAM,oBAAA,uBAA2B,GAAA,EAAY;AAE7C,EAAA,QAAA,CAAS,GAAA,EAAK;AAAA,IACZ,oBAAoB,IAAA,EAAuC;AACzD,MAAA,IAAI,KAAK,IAAA,CAAK,EAAA,EAAI,aAAA,GAAgB,IAAA,CAAK,KAAK,EAAA,CAAG,IAAA;AAAA,IACjD,CAAA;AAAA,IAEA,mBAAmB,IAAA,EAAsC;AACvD,MAAA,IACE,IAAA,CAAK,IAAA,CAAK,EAAA,CAAG,IAAA,KAAS,iBACrB,IAAA,CAAK,IAAA,CAAK,IAAA,EAAM,IAAA,KAAS,yBAAA,IACxB,IAAA,CAAK,IAAA,CAAK,IAAA,EAAM,SAAS,oBAAA,CAAA,EAC3B;AACA,QAAA,aAAA,GAAgB,IAAA,CAAK,KAAK,EAAA,CAAG,IAAA;AAAA,MAC/B;AAAA,IACF,CAAA;AAAA,IAEA,QAAQ,IAAA,EAA2B;AACjC,MAAA,MAAM,IAAA,GAAO,IAAA,CAAK,IAAA,CAAK,KAAA,CAAM,IAAA,EAAK;AAClC,MAAA,IAAI,CAAC,IAAA,EAAM;AAEX,MAAA,MAAM,SAAS,IAAA,CAAK,UAAA;AACpB,MAAA,IAAI,CAAC,MAAA,EAAQ,YAAA,EAAa,EAAG;AAE7B,MAAA,MAAM,WAAA,GAAc,UAAA,CAAW,MAAA,CAAO,IAAA,CAAK,cAAc,CAAA;AACzD,MAAA,IAAI,gBAAA,CAAiB,GAAA,CAAI,WAAW,CAAA,EAAG;AAGvC,MAAA,MAAM,IAAA,GAAO,IAAA,CAAK,IAAA,CAAK,GAAA,EAAK,MAAM,IAAA,IAAQ,CAAA;AAC1C,MAAA,IAAI,gBAAA,CAAiB,MAAA,EAAQ,IAAI,CAAA,EAAG;AAGpC,MAAA,IAAI,SAAS,UAAA,EAAY;AACvB,QAAA,MAAM,OAAA,GAAU,MAAA,CAAO,IAAA,CAAK,cAAA,CAAe,UAAA,CAAW,IAAA;AAAA,UACpD,CAAC,CAAA,KACC,CAAA,CAAE,IAAA,KAAS,cAAA,IACX,CAAA,CAAE,IAAA,CAAK,IAAA,KAAS,eAAA,IAChB,CAAA,CAAE,IAAA,CAAK,IAAA,KAAS;AAAA,SACpB;AACA,QAAA,IAAI,WAAA,KAAgB,GAAA,IAAO,CAAC,OAAA,EAAS;AAAA,MACvC;AAEA,MAAA,MAAM,KAAKC,eAAA,CAAW,EAAE,MAAM,QAAA,EAAU,OAAA,EAAS,eAAe,CAAA;AAChE,MAAA,eAAA,GAAkB,IAAA;AAElB,MAAA,IAAI,MAAA,IAAU,QAAA,GAAW,EAAE,CAAA,EAAG;AAE5B,QAAA,MAAM,OAAA,GAAYD,YAAA,CAAA,OAAA,CAAQ,QAAA,CAAS,EAAE,CAAC,CAAA;AACtC,QAAA,IAAA,CAAK,YAAY,OAAO,CAAA;AACxB,QAAA,IAAA,CAAK,IAAA,EAAK;AAAA,MACZ,CAAA,MAAO;AAEL,QAAA,WAAA,GAAc,IAAA;AACd,QAAA,IAAA,CAAK,WAAA;AAAA,UACDA,YAAA,CAAA,sBAAA;AAAA,YACEA,YAAA,CAAA,cAAA,CAAiBA,YAAA,CAAA,UAAA,CAAW,aAAa,CAAA,EAAG;AAAA,cAC1CA,2BAAc,EAAE,CAAA;AAAA,cAChBA,wBAAW,WAAW,CAAA;AAAA,cACtBA,2BAAc,IAAI;AAAA,aACrB;AAAA;AACH,SACF;AAGA,QAAA,MAAM,SAAS,IAAA,CAAK,UAAA;AAAA,UAClB,CAAC,MACC,CAAA,CAAE,qBAAA,MACF,CAAA,CAAE,oBAAA,EAAqB,IACvB,CAAA,CAAE,yBAAA;AAA0B,SAChC;AACA,QAAA,IAAI,MAAA,EAAQ;AACV,UAAA,oBAAA,CAAqB,GAAA,CAAI,OAAO,IAAI,CAAA;AAAA,QACtC;AAAA,MACF;AAAA,IACF;AAAA,GACD,CAAA;AAGD,EAAA,IAAI,WAAA,EAAa;AAaf,IAAA,IAASE,kBAAAA,GAAT,SACE,IAAA,EAGA;AACA,MAAA,IAAI,CAAC,oBAAA,CAAqB,GAAA,CAAI,IAAA,CAAK,IAAI,CAAA,EAAG;AAI1C,MAAA,MAAM,SAAA,GAAY,iBAAA,GACZF,YAAA,CAAA,mBAAA,CAAoB,OAAA,EAAS;AAAA,QAC3BA,YAAA,CAAA,kBAAA;AAAA,UACEA,YAAA,CAAA,aAAA,CAAc;AAAA,YACZA,YAAA,CAAA,cAAA;AAAA,cACEA,wBAAW,GAAG,CAAA;AAAA,cACdA,wBAAW,aAAa,CAAA;AAAA,cAC1B,KAAA;AAAA,cACA;AAAA;AACF,WACD,CAAA;AAAA,UACCA,YAAA,CAAA,cAAA,CAAiBA,YAAA,CAAA,UAAA,CAAW,gBAAgB,CAAA,EAAG,EAAE;AAAA;AACrD,OACD,CAAA,GACCA,YAAA,CAAA,mBAAA,CAAoB,OAAA,EAAS;AAAA,QAC3BA,YAAA,CAAA,kBAAA;AAAA,UACEA,wBAAW,aAAa,CAAA;AAAA,UACxBA,YAAA,CAAA,cAAA,CAAiBA,YAAA,CAAA,UAAA,CAAW,KAAK,CAAA,EAAG,EAAE;AAAA;AAC1C,OACD,CAAA;AAEL,MAAA,MAAM,IAAA,GAAO,KAAK,IAAA,CAAK,IAAA;AACvB,MAAA,IAAMA,YAAA,CAAA,gBAAA,CAAiB,IAAI,CAAA,EAAG;AAC5B,QAAA,IAAA,CAAK,IAAA,CAAK,QAAQ,SAAS,CAAA;AAAA,MAC7B,CAAA,MAAO;AACL,QAAA,IAAA,CAAK,IAAA,CAAK,OAASA,YAAA,CAAA,cAAA,CAAe,CAAC,WAAaA,YAAA,CAAA,eAAA,CAAgB,IAAI,CAAC,CAAC,CAAA;AAAA,MACxE;AAAA,IACF,CAAA;AAhDA,IAAA,QAAA,CAAS,GAAA,EAAK;AAAA,MACZ,oBAAoB,IAAA,EAAuC;AACzD,QAAAE,mBAAkB,IAAI,CAAA;AAAA,MACxB,CAAA;AAAA,MACA,mBAAmB,IAAA,EAAsC;AACvD,QAAAA,mBAAkB,IAAI,CAAA;AAAA,MACxB,CAAA;AAAA,MACA,wBAAwB,IAAA,EAA2C;AACjE,QAAAA,mBAAkB,IAAI,CAAA;AAAA,MACxB;AAAA,KACD,CAAA;AAwCD,IAAA,IAAI,iBAAA,EAAmB;AAErB,MAAA,MAAM,SAAA,GAAY,GAAA,CAAI,OAAA,CAAQ,IAAA,CAAK,IAAA;AAAA,QACjC,CAAC,IAAA,KACGF,YAAA,CAAA,mBAAA,CAAoB,IAAI,CAAA,IAC1B,KAAK,MAAA,CAAO,KAAA,KAAU,aAAA,IACtB,IAAA,CAAK,UAAA,CAAW,IAAA;AAAA,UACd,CAAC,CAAA,KACGA,YAAA,CAAA,iBAAA,CAAkB,CAAC,CAAA,IACnBA,YAAA,CAAA,YAAA,CAAa,CAAA,CAAE,QAAQ,CAAA,IACzB,CAAA,CAAE,QAAA,CAAS,IAAA,KAAS;AAAA;AACxB,OACJ;AAEA,MAAA,IAAI,CAAC,SAAA,EAAW;AACd,QAAC,GAAA,CAAI,QAAQ,IAAA,CAAuB,OAAA;AAAA,UAChCA,YAAA,CAAA,iBAAA;AAAA,YACA;AAAA,cACIA,YAAA,CAAA,eAAA;AAAA,gBACEA,wBAAW,gBAAgB,CAAA;AAAA,gBAC3BA,wBAAW,gBAAgB;AAAA;AAC/B,aACF;AAAA,YACEA,2BAAc,aAAa;AAAA;AAC/B,SACF;AAAA,MACF;AAAA,IACF,CAAA,MAAO;AAEL,MAAA,MAAM,SAAA,GAAY,GAAA,CAAI,OAAA,CAAQ,IAAA,CAAK,IAAA;AAAA,QACjC,CAAC,IAAA,KACGA,YAAA,CAAA,mBAAA,CAAoB,IAAI,CAAA,IAC1B,KAAK,MAAA,CAAO,KAAA,KAAU,SAAA,IACtB,IAAA,CAAK,UAAA,CAAW,IAAA;AAAA,UACd,CAAC,CAAA,KACGA,YAAA,CAAA,iBAAA,CAAkB,CAAC,CAAA,IACnBA,YAAA,CAAA,YAAA,CAAa,CAAA,CAAE,QAAQ,CAAA,IACzB,CAAA,CAAE,QAAA,CAAS,IAAA,KAAS;AAAA;AACxB,OACJ;AAEA,MAAA,IAAI,CAAC,SAAA,EAAW;AACd,QAAC,GAAA,CAAI,QAAQ,IAAA,CAAuB,OAAA;AAAA,UAChCA,YAAA,CAAA,iBAAA;AAAA,YACA,CAAGA,6BAAkBA,YAAA,CAAA,UAAA,CAAW,KAAK,GAAKA,YAAA,CAAA,UAAA,CAAW,KAAK,CAAC,CAAC,CAAA;AAAA,YAC1DA,2BAAc,SAAS;AAAA;AAC3B,SACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,EAAA,MAAM,MAAA,GAAS,SAAS,GAAA,EAAK,EAAE,YAAY,IAAA,EAAM,cAAA,EAAgB,UAAU,CAAA;AAE3E,EAAA,OAAO;AAAA,IACL,MAAM,MAAA,CAAO,IAAA;AAAA,IACb,KAAK,MAAA,CAAO,GAAA;AAAA,IACZ;AAAA,GACF;AACF;AAEA,SAAS,WAAW,OAAA,EAAsC;AACxD,EAAA,IAAI,QAAQ,IAAA,CAAK,IAAA,KAAS,eAAA,EAAiB,OAAO,QAAQ,IAAA,CAAK,IAAA;AAC/D,EAAA,OAAO,SAAA;AACT;AAEA,SAAS,gBAAA,CAAiB,QAAgB,IAAA,EAAuB;AAC/D,EAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,KAAA,CAAM,IAAI,CAAA;AAC/B,EAAA,MAAM,QAAA,GAAW,KAAA,CAAM,IAAA,GAAO,CAAC,CAAA;AAC/B,EAAA,OAAO,QAAA,GAAW,QAAA,CAAS,QAAA,CAAS,aAAa,CAAA,GAAI,KAAA;AACvD;;;ACxQe,SAAR,iBAA6C,MAAA,EAAwB;AAC1E,EAAA,MAAM,OAAA,GAAmC,IAAA,CAAK,UAAA,IAAa,IAAK,EAAC;AACjE,EAAA,MAAM,eAAuB,IAAA,CAAK,YAAA;AAGlC,EAAA,IAAI,YAAA,CAAa,QAAA,CAAS,cAAc,CAAA,EAAG,OAAO,MAAA;AAClD,EAAA,IAAI,CAAC,WAAA,CAAY,IAAA,CAAK,YAAY,GAAG,OAAO,MAAA;AAG5C,EAAA,IAAI,iCAAA,CAAkC,IAAA,CAAK,YAAY,CAAA,EAAG,OAAO,MAAA;AAEjE,EAAA,MAAM,WAAWG,aAAA,CAAS,IAAA,CAAK,eAAe,OAAA,CAAQ,GAAA,IAAO,YAAY,CAAA;AAEzE,EAAA,IAAI;AACF,IAAA,MAAM,MAAA,GAAS,UAAU,MAAA,EAAQ;AAAA,MAC/B,QAAA;AAAA,MACA,QAAQ,OAAA,CAAQ,MAAA;AAAA,MAChB,UAAU,OAAA,CAAQ,QAAA;AAAA,MAClB,IAAA,EAAM,QAAQ,IAAA,IAAQ;AAAA,KACvB,CAAA;AAED,IAAA,OAAO,MAAA,CAAO,eAAA,GAAkB,MAAA,CAAO,IAAA,GAAO,MAAA;AAAA,EAChD,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,MAAA;AAAA,EACT;AACF","file":"loader.cjs","sourcesContent":["/**\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 client runtime (default: `@better-intl/react`) */\n runtimeImport?: string;\n /** Import path for the RSC runtime (default: `@better-intl/next/rsc`) */\n rscImport?: 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 rscImport = \"@better-intl/next/rsc\",\n mode = \"auto\",\n } = options;\n\n const ast = parse(source, {\n sourceType: \"module\",\n plugins: [\"jsx\", \"typescript\"],\n });\n\n // Detect \"use client\" directive\n const isClientComponent =\n ast.program.directives?.some((d) => d.value.value === \"use client\") ??\n ast.program.body.some(\n (node) =>\n t.isExpressionStatement(node) &&\n t.isStringLiteral(node.expression) &&\n node.expression.value === \"use client\",\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 translation accessor at the top of each component\n if (needsImport) {\n traverse(ast, {\n FunctionDeclaration(path: NodePath<t.FunctionDeclaration>) {\n injectTranslation(path);\n },\n FunctionExpression(path: NodePath<t.FunctionExpression>) {\n injectTranslation(path);\n },\n ArrowFunctionExpression(path: NodePath<t.ArrowFunctionExpression>) {\n injectTranslation(path);\n },\n });\n\n function injectTranslation(\n path: NodePath<\n t.FunctionDeclaration | t.FunctionExpression | t.ArrowFunctionExpression\n >,\n ) {\n if (!functionsNeedingHook.has(path.node)) return;\n\n // Client: const { t: __t } = useTranslation();\n // Server: const __t = rsc();\n const statement = isClientComponent\n ? 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 : t.variableDeclaration(\"const\", [\n t.variableDeclarator(\n t.identifier(tFunctionName),\n t.callExpression(t.identifier(\"rsc\"), []),\n ),\n ]);\n\n const body = path.node.body;\n if (t.isBlockStatement(body)) {\n body.body.unshift(statement);\n } else {\n path.node.body = t.blockStatement([statement, t.returnStatement(body)]);\n }\n }\n\n if (isClientComponent) {\n // Add import { useTranslation } from \"@better-intl/react\"\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 (ast.program.body as t.Statement[]).unshift(\n t.importDeclaration(\n [\n t.importSpecifier(\n t.identifier(\"useTranslation\"),\n t.identifier(\"useTranslation\"),\n ),\n ],\n t.stringLiteral(runtimeImport),\n ),\n );\n }\n } else {\n // Add import { rsc } from \"@better-intl/next/rsc\"\n const hasImport = ast.program.body.some(\n (node) =>\n t.isImportDeclaration(node) &&\n node.source.value === rscImport &&\n node.specifiers.some(\n (s) =>\n t.isImportSpecifier(s) &&\n t.isIdentifier(s.imported) &&\n s.imported.name === \"rsc\",\n ),\n );\n\n if (!hasImport) {\n (ast.program.body as t.Statement[]).unshift(\n t.importDeclaration(\n [t.importSpecifier(t.identifier(\"rsc\"), t.identifier(\"rsc\"))],\n t.stringLiteral(rscImport),\n ),\n );\n }\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 * Turbopack/Webpack loader for better-intl.\n *\n * Transforms JSX text nodes into t() calls automatically during build.\n * Detects \"use client\" to choose between useTranslation (client) and rsc() (server).\n *\n * Used by @better-intl/next plugin via turbopack.rules.\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: loader context type\nexport default function betterIntlLoader(this: any, source: string): string {\n const options: BetterIntlLoaderOptions = this.getOptions?.() ?? {};\n const absolutePath: string = this.resourcePath;\n\n // Skip node_modules and non-JSX files\n if (absolutePath.includes(\"node_modules\")) return source;\n if (!/\\.[jt]sx$/.test(absolutePath)) return source;\n\n // Skip test/spec/stories files\n if (/\\.(test|spec|stories)\\.[jt]sx?$/.test(absolutePath)) return source;\n\n const filePath = relative(this.rootContext || process.cwd(), absolutePath);\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 return result.hasTranslations ? result.code : source;\n } catch {\n return source;\n }\n}\n"]}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Turbopack/Webpack loader for better-intl.
3
+ *
4
+ * Transforms JSX text nodes into t() calls automatically during build.
5
+ * Detects "use client" to choose between useTranslation (client) and rsc() (server).
6
+ *
7
+ * Used by @better-intl/next plugin via turbopack.rules.
8
+ */
9
+ interface BetterIntlLoaderOptions {
10
+ locale?: string;
11
+ messages?: Record<string, string>;
12
+ mode?: "auto" | "explicit";
13
+ }
14
+ declare function betterIntlLoader(this: any, source: string): string;
15
+
16
+ export { type BetterIntlLoaderOptions, betterIntlLoader as default };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Turbopack/Webpack loader for better-intl.
3
+ *
4
+ * Transforms JSX text nodes into t() calls automatically during build.
5
+ * Detects "use client" to choose between useTranslation (client) and rsc() (server).
6
+ *
7
+ * Used by @better-intl/next plugin via turbopack.rules.
8
+ */
9
+ interface BetterIntlLoaderOptions {
10
+ locale?: string;
11
+ messages?: Record<string, string>;
12
+ mode?: "auto" | "explicit";
13
+ }
14
+ declare function betterIntlLoader(this: any, source: string): string;
15
+
16
+ export { type BetterIntlLoaderOptions, betterIntlLoader as default };
package/dist/loader.js ADDED
@@ -0,0 +1,26 @@
1
+ import { transform } from './chunk-MDYQVJ3W.js';
2
+ import { relative } from 'path';
3
+
4
+ function betterIntlLoader(source) {
5
+ const options = this.getOptions?.() ?? {};
6
+ const absolutePath = this.resourcePath;
7
+ if (absolutePath.includes("node_modules")) return source;
8
+ if (!/\.[jt]sx$/.test(absolutePath)) return source;
9
+ if (/\.(test|spec|stories)\.[jt]sx?$/.test(absolutePath)) return source;
10
+ const filePath = relative(this.rootContext || process.cwd(), absolutePath);
11
+ try {
12
+ const result = transform(source, {
13
+ filePath,
14
+ locale: options.locale,
15
+ messages: options.messages,
16
+ mode: options.mode ?? "auto"
17
+ });
18
+ return result.hasTranslations ? result.code : source;
19
+ } catch {
20
+ return source;
21
+ }
22
+ }
23
+
24
+ export { betterIntlLoader as default };
25
+ //# sourceMappingURL=loader.js.map
26
+ //# sourceMappingURL=loader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/loader.ts"],"names":[],"mappings":";;;AAmBe,SAAR,iBAA6C,MAAA,EAAwB;AAC1E,EAAA,MAAM,OAAA,GAAmC,IAAA,CAAK,UAAA,IAAa,IAAK,EAAC;AACjE,EAAA,MAAM,eAAuB,IAAA,CAAK,YAAA;AAGlC,EAAA,IAAI,YAAA,CAAa,QAAA,CAAS,cAAc,CAAA,EAAG,OAAO,MAAA;AAClD,EAAA,IAAI,CAAC,WAAA,CAAY,IAAA,CAAK,YAAY,GAAG,OAAO,MAAA;AAG5C,EAAA,IAAI,iCAAA,CAAkC,IAAA,CAAK,YAAY,CAAA,EAAG,OAAO,MAAA;AAEjE,EAAA,MAAM,WAAW,QAAA,CAAS,IAAA,CAAK,eAAe,OAAA,CAAQ,GAAA,IAAO,YAAY,CAAA;AAEzE,EAAA,IAAI;AACF,IAAA,MAAM,MAAA,GAAS,UAAU,MAAA,EAAQ;AAAA,MAC/B,QAAA;AAAA,MACA,QAAQ,OAAA,CAAQ,MAAA;AAAA,MAChB,UAAU,OAAA,CAAQ,QAAA;AAAA,MAClB,IAAA,EAAM,QAAQ,IAAA,IAAQ;AAAA,KACvB,CAAA;AAED,IAAA,OAAO,MAAA,CAAO,eAAA,GAAkB,MAAA,CAAO,IAAA,GAAO,MAAA;AAAA,EAChD,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,MAAA;AAAA,EACT;AACF","file":"loader.js","sourcesContent":["/**\n * Turbopack/Webpack loader for better-intl.\n *\n * Transforms JSX text nodes into t() calls automatically during build.\n * Detects \"use client\" to choose between useTranslation (client) and rsc() (server).\n *\n * Used by @better-intl/next plugin via turbopack.rules.\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: loader context type\nexport default function betterIntlLoader(this: any, source: string): string {\n const options: BetterIntlLoaderOptions = this.getOptions?.() ?? {};\n const absolutePath: string = this.resourcePath;\n\n // Skip node_modules and non-JSX files\n if (absolutePath.includes(\"node_modules\")) return source;\n if (!/\\.[jt]sx$/.test(absolutePath)) return source;\n\n // Skip test/spec/stories files\n if (/\\.(test|spec|stories)\\.[jt]sx?$/.test(absolutePath)) return source;\n\n const filePath = relative(this.rootContext || process.cwd(), absolutePath);\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 return result.hasTranslations ? result.code : source;\n } catch {\n return source;\n }\n}\n"]}
@@ -42,12 +42,16 @@ function transform(source, options) {
42
42
  messages,
43
43
  tFunctionName = "__t",
44
44
  runtimeImport = "@better-intl/react",
45
+ rscImport = "@better-intl/next/rsc",
45
46
  mode = "auto"
46
47
  } = options;
47
48
  const ast = parser.parse(source, {
48
49
  sourceType: "module",
49
50
  plugins: ["jsx", "typescript"]
50
51
  });
52
+ const isClientComponent = ast.program.directives?.some((d) => d.value.value === "use client") ?? ast.program.body.some(
53
+ (node) => t__namespace.isExpressionStatement(node) && t__namespace.isStringLiteral(node.expression) && node.expression.value === "use client"
54
+ );
51
55
  let hasTranslations = false;
52
56
  let componentName;
53
57
  let needsImport = false;
@@ -103,9 +107,9 @@ function transform(source, options) {
103
107
  }
104
108
  });
105
109
  if (needsImport) {
106
- let injectHook2 = function(path) {
110
+ let injectTranslation2 = function(path) {
107
111
  if (!functionsNeedingHook.has(path.node)) return;
108
- const hookCall = t__namespace.variableDeclaration("const", [
112
+ const statement = isClientComponent ? t__namespace.variableDeclaration("const", [
109
113
  t__namespace.variableDeclarator(
110
114
  t__namespace.objectPattern([
111
115
  t__namespace.objectProperty(
@@ -117,41 +121,63 @@ function transform(source, options) {
117
121
  ]),
118
122
  t__namespace.callExpression(t__namespace.identifier("useTranslation"), [])
119
123
  )
124
+ ]) : t__namespace.variableDeclaration("const", [
125
+ t__namespace.variableDeclarator(
126
+ t__namespace.identifier(tFunctionName),
127
+ t__namespace.callExpression(t__namespace.identifier("rsc"), [])
128
+ )
120
129
  ]);
121
130
  const body = path.node.body;
122
131
  if (t__namespace.isBlockStatement(body)) {
123
- body.body.unshift(hookCall);
132
+ body.body.unshift(statement);
124
133
  } else {
125
- path.node.body = t__namespace.blockStatement([hookCall, t__namespace.returnStatement(body)]);
134
+ path.node.body = t__namespace.blockStatement([statement, t__namespace.returnStatement(body)]);
126
135
  }
127
136
  };
128
137
  traverse(ast, {
129
138
  FunctionDeclaration(path) {
130
- injectHook2(path);
139
+ injectTranslation2(path);
131
140
  },
132
141
  FunctionExpression(path) {
133
- injectHook2(path);
142
+ injectTranslation2(path);
134
143
  },
135
144
  ArrowFunctionExpression(path) {
136
- injectHook2(path);
145
+ injectTranslation2(path);
137
146
  }
138
147
  });
139
- const hasImport = ast.program.body.some(
140
- (node) => t__namespace.isImportDeclaration(node) && node.source.value === runtimeImport && node.specifiers.some(
141
- (s) => t__namespace.isImportSpecifier(s) && t__namespace.isIdentifier(s.imported) && s.imported.name === "useTranslation"
142
- )
143
- );
144
- if (!hasImport) {
145
- const importDecl = t__namespace.importDeclaration(
146
- [
147
- t__namespace.importSpecifier(
148
- t__namespace.identifier("useTranslation"),
149
- t__namespace.identifier("useTranslation")
148
+ if (isClientComponent) {
149
+ const hasImport = ast.program.body.some(
150
+ (node) => t__namespace.isImportDeclaration(node) && node.source.value === runtimeImport && node.specifiers.some(
151
+ (s) => t__namespace.isImportSpecifier(s) && t__namespace.isIdentifier(s.imported) && s.imported.name === "useTranslation"
152
+ )
153
+ );
154
+ if (!hasImport) {
155
+ ast.program.body.unshift(
156
+ t__namespace.importDeclaration(
157
+ [
158
+ t__namespace.importSpecifier(
159
+ t__namespace.identifier("useTranslation"),
160
+ t__namespace.identifier("useTranslation")
161
+ )
162
+ ],
163
+ t__namespace.stringLiteral(runtimeImport)
150
164
  )
151
- ],
152
- t__namespace.stringLiteral(runtimeImport)
165
+ );
166
+ }
167
+ } else {
168
+ const hasImport = ast.program.body.some(
169
+ (node) => t__namespace.isImportDeclaration(node) && node.source.value === rscImport && node.specifiers.some(
170
+ (s) => t__namespace.isImportSpecifier(s) && t__namespace.isIdentifier(s.imported) && s.imported.name === "rsc"
171
+ )
153
172
  );
154
- ast.program.body.unshift(importDecl);
173
+ if (!hasImport) {
174
+ ast.program.body.unshift(
175
+ t__namespace.importDeclaration(
176
+ [t__namespace.importSpecifier(t__namespace.identifier("rsc"), t__namespace.identifier("rsc"))],
177
+ t__namespace.stringLiteral(rscImport)
178
+ )
179
+ );
180
+ }
155
181
  }
156
182
  }
157
183
  const output = generate(ast, { sourceMaps: true, sourceFileName: filePath });
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/transformer.ts","../src/webpack-loader.ts"],"names":["_traverse","_generate","parse","generateId","t","injectHook","relative"],"mappings":"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","file":"webpack-loader.cjs","sourcesContent":["/**\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"]}
1
+ {"version":3,"sources":["../src/transformer.ts","../src/webpack-loader.ts"],"names":["_traverse","_generate","parse","t","generateId","injectTranslation","relative"],"mappings":"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","file":"webpack-loader.cjs","sourcesContent":["/**\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 client runtime (default: `@better-intl/react`) */\n runtimeImport?: string;\n /** Import path for the RSC runtime (default: `@better-intl/next/rsc`) */\n rscImport?: 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 rscImport = \"@better-intl/next/rsc\",\n mode = \"auto\",\n } = options;\n\n const ast = parse(source, {\n sourceType: \"module\",\n plugins: [\"jsx\", \"typescript\"],\n });\n\n // Detect \"use client\" directive\n const isClientComponent =\n ast.program.directives?.some((d) => d.value.value === \"use client\") ??\n ast.program.body.some(\n (node) =>\n t.isExpressionStatement(node) &&\n t.isStringLiteral(node.expression) &&\n node.expression.value === \"use client\",\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 translation accessor at the top of each component\n if (needsImport) {\n traverse(ast, {\n FunctionDeclaration(path: NodePath<t.FunctionDeclaration>) {\n injectTranslation(path);\n },\n FunctionExpression(path: NodePath<t.FunctionExpression>) {\n injectTranslation(path);\n },\n ArrowFunctionExpression(path: NodePath<t.ArrowFunctionExpression>) {\n injectTranslation(path);\n },\n });\n\n function injectTranslation(\n path: NodePath<\n t.FunctionDeclaration | t.FunctionExpression | t.ArrowFunctionExpression\n >,\n ) {\n if (!functionsNeedingHook.has(path.node)) return;\n\n // Client: const { t: __t } = useTranslation();\n // Server: const __t = rsc();\n const statement = isClientComponent\n ? 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 : t.variableDeclaration(\"const\", [\n t.variableDeclarator(\n t.identifier(tFunctionName),\n t.callExpression(t.identifier(\"rsc\"), []),\n ),\n ]);\n\n const body = path.node.body;\n if (t.isBlockStatement(body)) {\n body.body.unshift(statement);\n } else {\n path.node.body = t.blockStatement([statement, t.returnStatement(body)]);\n }\n }\n\n if (isClientComponent) {\n // Add import { useTranslation } from \"@better-intl/react\"\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 (ast.program.body as t.Statement[]).unshift(\n t.importDeclaration(\n [\n t.importSpecifier(\n t.identifier(\"useTranslation\"),\n t.identifier(\"useTranslation\"),\n ),\n ],\n t.stringLiteral(runtimeImport),\n ),\n );\n }\n } else {\n // Add import { rsc } from \"@better-intl/next/rsc\"\n const hasImport = ast.program.body.some(\n (node) =>\n t.isImportDeclaration(node) &&\n node.source.value === rscImport &&\n node.specifiers.some(\n (s) =>\n t.isImportSpecifier(s) &&\n t.isIdentifier(s.imported) &&\n s.imported.name === \"rsc\",\n ),\n );\n\n if (!hasImport) {\n (ast.program.body as t.Statement[]).unshift(\n t.importDeclaration(\n [t.importSpecifier(t.identifier(\"rsc\"), t.identifier(\"rsc\"))],\n t.stringLiteral(rscImport),\n ),\n );\n }\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"]}
@@ -1,3 +1,4 @@
1
- export { betterIntlLoader as default } from './chunk-RKXQPVVG.js';
1
+ export { betterIntlLoader as default } from './chunk-SF5H7SHR.js';
2
+ import './chunk-MDYQVJ3W.js';
2
3
  //# sourceMappingURL=webpack-loader.js.map
3
4
  //# sourceMappingURL=webpack-loader.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-intl/compiler",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "AST extractor and Babel/SWC transform for better-intl",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -21,6 +21,11 @@
21
21
  "import": "./dist/webpack-loader.js",
22
22
  "require": "./dist/webpack-loader.cjs",
23
23
  "types": "./dist/webpack-loader.d.ts"
24
+ },
25
+ "./loader": {
26
+ "import": "./dist/loader.js",
27
+ "require": "./dist/loader.cjs",
28
+ "types": "./dist/loader.d.ts"
24
29
  }
25
30
  },
26
31
  "files": [
@@ -32,7 +37,7 @@
32
37
  "@babel/parser": "^7.26.0",
33
38
  "@babel/traverse": "^7.26.0",
34
39
  "@babel/types": "^7.26.0",
35
- "@better-intl/core": "0.1.2"
40
+ "@better-intl/core": "0.2.0"
36
41
  },
37
42
  "devDependencies": {
38
43
  "@babel/core": "^7.26.0",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/transformer.ts","../src/webpack-loader.ts"],"names":["injectHook"],"mappings":"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","file":"chunk-RKXQPVVG.js","sourcesContent":["/**\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"]}