@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.
- package/dist/{chunk-RKXQPVVG.js → chunk-MDYQVJ3W.js} +51 -54
- package/dist/chunk-MDYQVJ3W.js.map +1 -0
- package/dist/chunk-SF5H7SHR.js +33 -0
- package/dist/chunk-SF5H7SHR.js.map +1 -0
- package/dist/index.cjs +47 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/loader.cjs +223 -0
- package/dist/loader.cjs.map +1 -0
- package/dist/loader.d.cts +16 -0
- package/dist/loader.d.ts +16 -0
- package/dist/loader.js +26 -0
- package/dist/loader.js.map +1 -0
- package/dist/webpack-loader.cjs +47 -21
- package/dist/webpack-loader.cjs.map +1 -1
- package/dist/webpack-loader.js +2 -1
- package/package.json +7 -2
- package/dist/chunk-RKXQPVVG.js.map +0 -1
|
@@ -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/
|
|
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
|
|
83
|
+
let injectTranslation2 = function(path) {
|
|
81
84
|
if (!functionsNeedingHook.has(path.node)) return;
|
|
82
|
-
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(
|
|
105
|
+
body.body.unshift(statement);
|
|
98
106
|
} else {
|
|
99
|
-
path.node.body = t.blockStatement([
|
|
107
|
+
path.node.body = t.blockStatement([statement, t.returnStatement(body)]);
|
|
100
108
|
}
|
|
101
109
|
};
|
|
102
110
|
traverse(ast, {
|
|
103
111
|
FunctionDeclaration(path) {
|
|
104
|
-
|
|
112
|
+
injectTranslation2(path);
|
|
105
113
|
},
|
|
106
114
|
FunctionExpression(path) {
|
|
107
|
-
|
|
115
|
+
injectTranslation2(path);
|
|
108
116
|
},
|
|
109
117
|
ArrowFunctionExpression(path) {
|
|
110
|
-
|
|
118
|
+
injectTranslation2(path);
|
|
111
119
|
}
|
|
112
120
|
});
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
t.
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
276
|
+
let injectTranslation2 = function(path) {
|
|
273
277
|
if (!functionsNeedingHook.has(path.node)) return;
|
|
274
|
-
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(
|
|
298
|
+
body.body.unshift(statement);
|
|
290
299
|
} else {
|
|
291
|
-
path.node.body = t__namespace.blockStatement([
|
|
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
|
-
|
|
305
|
+
injectTranslation2(path);
|
|
297
306
|
},
|
|
298
307
|
FunctionExpression(path) {
|
|
299
|
-
|
|
308
|
+
injectTranslation2(path);
|
|
300
309
|
},
|
|
301
310
|
ArrowFunctionExpression(path) {
|
|
302
|
-
|
|
311
|
+
injectTranslation2(path);
|
|
303
312
|
}
|
|
304
313
|
});
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
(
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
t__namespace.
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 });
|
package/dist/index.cjs.map
CHANGED
|
@@ -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":"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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 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 {
|
|
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"]}
|
package/dist/loader.cjs
ADDED
|
@@ -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":"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","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 };
|
package/dist/loader.d.ts
ADDED
|
@@ -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"]}
|
package/dist/webpack-loader.cjs
CHANGED
|
@@ -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
|
|
110
|
+
let injectTranslation2 = function(path) {
|
|
107
111
|
if (!functionsNeedingHook.has(path.node)) return;
|
|
108
|
-
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(
|
|
132
|
+
body.body.unshift(statement);
|
|
124
133
|
} else {
|
|
125
|
-
path.node.body = t__namespace.blockStatement([
|
|
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
|
-
|
|
139
|
+
injectTranslation2(path);
|
|
131
140
|
},
|
|
132
141
|
FunctionExpression(path) {
|
|
133
|
-
|
|
142
|
+
injectTranslation2(path);
|
|
134
143
|
},
|
|
135
144
|
ArrowFunctionExpression(path) {
|
|
136
|
-
|
|
145
|
+
injectTranslation2(path);
|
|
137
146
|
}
|
|
138
147
|
});
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
t__namespace.
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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"]}
|
package/dist/webpack-loader.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-intl/compiler",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
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"]}
|