@better-intl/compiler 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/babel.cjs +138 -0
- package/dist/babel.cjs.map +1 -0
- package/dist/babel.d.cts +33 -0
- package/dist/babel.d.ts +33 -0
- package/dist/babel.js +136 -0
- package/dist/babel.js.map +1 -0
- package/dist/chunk-RKXQPVVG.js +178 -0
- package/dist/chunk-RKXQPVVG.js.map +1 -0
- package/dist/index.cjs +374 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +82 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.js +176 -0
- package/dist/index.js.map +1 -0
- package/dist/webpack-loader.cjs +204 -0
- package/dist/webpack-loader.cjs.map +1 -0
- package/dist/webpack-loader.d.cts +14 -0
- package/dist/webpack-loader.d.ts +14 -0
- package/dist/webpack-loader.js +3 -0
- package/dist/webpack-loader.js.map +1 -0
- package/package.json +56 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
export { transform, betterIntlLoader as webpackLoader } from './chunk-RKXQPVVG.js';
|
|
2
|
+
import { parse } from '@babel/parser';
|
|
3
|
+
import _traverse from '@babel/traverse';
|
|
4
|
+
import { generateId } from '@better-intl/core';
|
|
5
|
+
|
|
6
|
+
// src/catalog-generator.ts
|
|
7
|
+
function generateDefaultCatalog(messages) {
|
|
8
|
+
const catalog = {};
|
|
9
|
+
for (const msg of messages) {
|
|
10
|
+
catalog[msg.id] = msg.defaultMessage;
|
|
11
|
+
}
|
|
12
|
+
return { locale: "en", messages: catalog };
|
|
13
|
+
}
|
|
14
|
+
function mergeCatalog(existing, extracted) {
|
|
15
|
+
const newIds = new Set(extracted.map((m) => m.id));
|
|
16
|
+
const existingIds = new Set(Object.keys(existing));
|
|
17
|
+
const added = [];
|
|
18
|
+
const removed = [];
|
|
19
|
+
const unchanged = [];
|
|
20
|
+
const messages = {};
|
|
21
|
+
for (const msg of extracted) {
|
|
22
|
+
if (existingIds.has(msg.id)) {
|
|
23
|
+
messages[msg.id] = existing[msg.id];
|
|
24
|
+
unchanged.push(msg.id);
|
|
25
|
+
} else {
|
|
26
|
+
messages[msg.id] = msg.defaultMessage;
|
|
27
|
+
added.push(msg.id);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
for (const id of existingIds) {
|
|
31
|
+
if (!newIds.has(id)) {
|
|
32
|
+
removed.push(id);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return { messages, added, removed, unchanged };
|
|
36
|
+
}
|
|
37
|
+
function findMissingKeys(defaultCatalog, targetCatalog) {
|
|
38
|
+
return Object.keys(defaultCatalog).filter((id) => !(id in targetCatalog));
|
|
39
|
+
}
|
|
40
|
+
function findDeadKeys(catalog, extracted) {
|
|
41
|
+
const extractedIds = new Set(extracted.map((m) => m.id));
|
|
42
|
+
return Object.keys(catalog).filter((id) => !extractedIds.has(id));
|
|
43
|
+
}
|
|
44
|
+
var traverse = typeof _traverse === "function" ? _traverse : _traverse.default;
|
|
45
|
+
var IGNORED_ELEMENTS = /* @__PURE__ */ new Set(["script", "style", "code", "pre", "Helmet"]);
|
|
46
|
+
var IGNORED_FILE_PATTERNS = [
|
|
47
|
+
/\.test\.[tj]sx?$/,
|
|
48
|
+
/\.spec\.[tj]sx?$/,
|
|
49
|
+
/\.stories\.[tj]sx?$/,
|
|
50
|
+
/\/__tests__\//
|
|
51
|
+
];
|
|
52
|
+
function extract(source, options) {
|
|
53
|
+
const { filePath, mode = "auto" } = options;
|
|
54
|
+
if (IGNORED_FILE_PATTERNS.some((p) => p.test(filePath))) {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
const ast = parse(source, {
|
|
58
|
+
sourceType: "module",
|
|
59
|
+
plugins: ["jsx", "typescript"]
|
|
60
|
+
});
|
|
61
|
+
const ctx = {
|
|
62
|
+
componentName: void 0,
|
|
63
|
+
messages: [],
|
|
64
|
+
ignoredLines: collectIgnoreComments(source),
|
|
65
|
+
filePath,
|
|
66
|
+
mode
|
|
67
|
+
};
|
|
68
|
+
traverse(ast, {
|
|
69
|
+
// Track component name for context
|
|
70
|
+
FunctionDeclaration(path) {
|
|
71
|
+
if (path.node.id) {
|
|
72
|
+
ctx.componentName = path.node.id.name;
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
VariableDeclarator(path) {
|
|
76
|
+
if (path.node.id.type === "Identifier" && (path.node.init?.type === "ArrowFunctionExpression" || path.node.init?.type === "FunctionExpression")) {
|
|
77
|
+
ctx.componentName = path.node.id.name;
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
// Extract text from JSX
|
|
81
|
+
JSXText(path) {
|
|
82
|
+
const text = path.node.value.trim();
|
|
83
|
+
if (!text) return;
|
|
84
|
+
const line = path.node.loc?.start.line ?? 0;
|
|
85
|
+
if (ctx.ignoredLines.has(line) || ctx.ignoredLines.has(line - 1)) return;
|
|
86
|
+
const parentElement = findParentElement(path);
|
|
87
|
+
if (!parentElement) return;
|
|
88
|
+
const elementName = getElementName(parentElement);
|
|
89
|
+
if (IGNORED_ELEMENTS.has(elementName)) return;
|
|
90
|
+
if (ctx.mode === "explicit") {
|
|
91
|
+
const hasI18nProp = hasAttribute(parentElement, "i18n");
|
|
92
|
+
if (elementName !== "T" && !hasI18nProp) return;
|
|
93
|
+
}
|
|
94
|
+
addMessage(ctx, text, elementName, path.node.loc);
|
|
95
|
+
},
|
|
96
|
+
// Also handle string literals in JSX expressions like {"Hello"}
|
|
97
|
+
JSXExpressionContainer(path) {
|
|
98
|
+
const expr = path.node.expression;
|
|
99
|
+
if (expr.type !== "StringLiteral") return;
|
|
100
|
+
const text = expr.value.trim();
|
|
101
|
+
if (!text) return;
|
|
102
|
+
const line = expr.loc?.start.line ?? 0;
|
|
103
|
+
if (ctx.ignoredLines.has(line) || ctx.ignoredLines.has(line - 1)) return;
|
|
104
|
+
const parentElement = findParentElement(path);
|
|
105
|
+
if (!parentElement) return;
|
|
106
|
+
const elementName = getElementName(parentElement);
|
|
107
|
+
if (IGNORED_ELEMENTS.has(elementName)) return;
|
|
108
|
+
if (ctx.mode === "explicit") {
|
|
109
|
+
const hasI18nProp = hasAttribute(parentElement, "i18n");
|
|
110
|
+
if (elementName !== "T" && !hasI18nProp) return;
|
|
111
|
+
}
|
|
112
|
+
addMessage(ctx, text, elementName, expr.loc);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
return ctx.messages;
|
|
116
|
+
}
|
|
117
|
+
function addMessage(ctx, text, elementType, loc) {
|
|
118
|
+
const id = generateId({
|
|
119
|
+
text,
|
|
120
|
+
filePath: ctx.filePath,
|
|
121
|
+
context: ctx.componentName
|
|
122
|
+
});
|
|
123
|
+
if (ctx.messages.some((m) => m.id === id)) return;
|
|
124
|
+
ctx.messages.push({
|
|
125
|
+
id,
|
|
126
|
+
defaultMessage: text,
|
|
127
|
+
description: `${ctx.filePath}:${elementType}`,
|
|
128
|
+
filePath: ctx.filePath,
|
|
129
|
+
componentName: ctx.componentName,
|
|
130
|
+
elementType,
|
|
131
|
+
line: loc?.start.line ?? 0,
|
|
132
|
+
column: loc?.start.column ?? 0
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
function findParentElement(path) {
|
|
136
|
+
let current = path.parentPath;
|
|
137
|
+
while (current) {
|
|
138
|
+
if (current.isJSXElement()) {
|
|
139
|
+
return current.node.openingElement;
|
|
140
|
+
}
|
|
141
|
+
current = current.parentPath;
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
function getElementName(opening) {
|
|
146
|
+
if (opening.name.type === "JSXIdentifier") {
|
|
147
|
+
return opening.name.name;
|
|
148
|
+
}
|
|
149
|
+
if (opening.name.type === "JSXMemberExpression") {
|
|
150
|
+
return `${getMemberName(opening.name)}`;
|
|
151
|
+
}
|
|
152
|
+
return "unknown";
|
|
153
|
+
}
|
|
154
|
+
function getMemberName(node) {
|
|
155
|
+
const object = node.object.type === "JSXIdentifier" ? node.object.name : getMemberName(node.object);
|
|
156
|
+
return `${object}.${node.property.name}`;
|
|
157
|
+
}
|
|
158
|
+
function hasAttribute(opening, name) {
|
|
159
|
+
return opening.attributes.some(
|
|
160
|
+
(attr) => attr.type === "JSXAttribute" && attr.name.type === "JSXIdentifier" && attr.name.name === name
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
function collectIgnoreComments(source) {
|
|
164
|
+
const ignored = /* @__PURE__ */ new Set();
|
|
165
|
+
const lines = source.split("\n");
|
|
166
|
+
for (let i = 0; i < lines.length; i++) {
|
|
167
|
+
if (lines[i].includes("i18n-ignore")) {
|
|
168
|
+
ignored.add(i + 1);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return ignored;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export { extract, findDeadKeys, findMissingKeys, generateDefaultCatalog, mergeCatalog };
|
|
175
|
+
//# sourceMappingURL=index.js.map
|
|
176
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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"]}
|
|
@@ -0,0 +1,204 @@
|
|
|
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/webpack-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
|
+
mode = "auto"
|
|
46
|
+
} = options;
|
|
47
|
+
const ast = parser.parse(source, {
|
|
48
|
+
sourceType: "module",
|
|
49
|
+
plugins: ["jsx", "typescript"]
|
|
50
|
+
});
|
|
51
|
+
let hasTranslations = false;
|
|
52
|
+
let componentName;
|
|
53
|
+
let needsImport = false;
|
|
54
|
+
const functionsNeedingHook = /* @__PURE__ */ new Set();
|
|
55
|
+
traverse(ast, {
|
|
56
|
+
FunctionDeclaration(path) {
|
|
57
|
+
if (path.node.id) componentName = path.node.id.name;
|
|
58
|
+
},
|
|
59
|
+
VariableDeclarator(path) {
|
|
60
|
+
if (path.node.id.type === "Identifier" && (path.node.init?.type === "ArrowFunctionExpression" || path.node.init?.type === "FunctionExpression")) {
|
|
61
|
+
componentName = path.node.id.name;
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
JSXText(path) {
|
|
65
|
+
const text = path.node.value.trim();
|
|
66
|
+
if (!text) return;
|
|
67
|
+
const parent = path.parentPath;
|
|
68
|
+
if (!parent?.isJSXElement()) return;
|
|
69
|
+
const elementName = getJSXName(parent.node.openingElement);
|
|
70
|
+
if (IGNORED_ELEMENTS.has(elementName)) return;
|
|
71
|
+
const line = path.node.loc?.start.line ?? 0;
|
|
72
|
+
if (hasIgnoreComment(source, line)) return;
|
|
73
|
+
if (mode === "explicit") {
|
|
74
|
+
const hasI18n = parent.node.openingElement.attributes.some(
|
|
75
|
+
(a) => a.type === "JSXAttribute" && a.name.type === "JSXIdentifier" && a.name.name === "i18n"
|
|
76
|
+
);
|
|
77
|
+
if (elementName !== "T" && !hasI18n) return;
|
|
78
|
+
}
|
|
79
|
+
const id = core.generateId({ text, filePath, context: componentName });
|
|
80
|
+
hasTranslations = true;
|
|
81
|
+
if (locale && messages?.[id]) {
|
|
82
|
+
const newNode = t__namespace.jsxText(messages[id]);
|
|
83
|
+
path.replaceWith(newNode);
|
|
84
|
+
path.skip();
|
|
85
|
+
} else {
|
|
86
|
+
needsImport = true;
|
|
87
|
+
path.replaceWith(
|
|
88
|
+
t__namespace.jsxExpressionContainer(
|
|
89
|
+
t__namespace.callExpression(t__namespace.identifier(tFunctionName), [
|
|
90
|
+
t__namespace.stringLiteral(id),
|
|
91
|
+
t__namespace.identifier("undefined"),
|
|
92
|
+
t__namespace.stringLiteral(text)
|
|
93
|
+
])
|
|
94
|
+
)
|
|
95
|
+
);
|
|
96
|
+
const fnPath = path.findParent(
|
|
97
|
+
(p) => p.isFunctionDeclaration() || p.isFunctionExpression() || p.isArrowFunctionExpression()
|
|
98
|
+
);
|
|
99
|
+
if (fnPath) {
|
|
100
|
+
functionsNeedingHook.add(fnPath.node);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
if (needsImport) {
|
|
106
|
+
let injectHook2 = function(path) {
|
|
107
|
+
if (!functionsNeedingHook.has(path.node)) return;
|
|
108
|
+
const hookCall = t__namespace.variableDeclaration("const", [
|
|
109
|
+
t__namespace.variableDeclarator(
|
|
110
|
+
t__namespace.objectPattern([
|
|
111
|
+
t__namespace.objectProperty(
|
|
112
|
+
t__namespace.identifier("t"),
|
|
113
|
+
t__namespace.identifier(tFunctionName),
|
|
114
|
+
false,
|
|
115
|
+
false
|
|
116
|
+
)
|
|
117
|
+
]),
|
|
118
|
+
t__namespace.callExpression(t__namespace.identifier("useTranslation"), [])
|
|
119
|
+
)
|
|
120
|
+
]);
|
|
121
|
+
const body = path.node.body;
|
|
122
|
+
if (t__namespace.isBlockStatement(body)) {
|
|
123
|
+
body.body.unshift(hookCall);
|
|
124
|
+
} else {
|
|
125
|
+
path.node.body = t__namespace.blockStatement([hookCall, t__namespace.returnStatement(body)]);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
traverse(ast, {
|
|
129
|
+
FunctionDeclaration(path) {
|
|
130
|
+
injectHook2(path);
|
|
131
|
+
},
|
|
132
|
+
FunctionExpression(path) {
|
|
133
|
+
injectHook2(path);
|
|
134
|
+
},
|
|
135
|
+
ArrowFunctionExpression(path) {
|
|
136
|
+
injectHook2(path);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
const hasImport = ast.program.body.some(
|
|
140
|
+
(node) => t__namespace.isImportDeclaration(node) && node.source.value === runtimeImport && node.specifiers.some(
|
|
141
|
+
(s) => t__namespace.isImportSpecifier(s) && t__namespace.isIdentifier(s.imported) && s.imported.name === "useTranslation"
|
|
142
|
+
)
|
|
143
|
+
);
|
|
144
|
+
if (!hasImport) {
|
|
145
|
+
const importDecl = t__namespace.importDeclaration(
|
|
146
|
+
[
|
|
147
|
+
t__namespace.importSpecifier(
|
|
148
|
+
t__namespace.identifier("useTranslation"),
|
|
149
|
+
t__namespace.identifier("useTranslation")
|
|
150
|
+
)
|
|
151
|
+
],
|
|
152
|
+
t__namespace.stringLiteral(runtimeImport)
|
|
153
|
+
);
|
|
154
|
+
ast.program.body.unshift(importDecl);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const output = generate(ast, { sourceMaps: true, sourceFileName: filePath });
|
|
158
|
+
return {
|
|
159
|
+
code: output.code,
|
|
160
|
+
map: output.map,
|
|
161
|
+
hasTranslations
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function getJSXName(opening) {
|
|
165
|
+
if (opening.name.type === "JSXIdentifier") return opening.name.name;
|
|
166
|
+
return "unknown";
|
|
167
|
+
}
|
|
168
|
+
function hasIgnoreComment(source, line) {
|
|
169
|
+
const lines = source.split("\n");
|
|
170
|
+
const prevLine = lines[line - 2];
|
|
171
|
+
return prevLine ? prevLine.includes("i18n-ignore") : false;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// src/webpack-loader.ts
|
|
175
|
+
function betterIntlLoader(source) {
|
|
176
|
+
const options = this.getOptions?.() ?? {};
|
|
177
|
+
const absolutePath = this.resourcePath;
|
|
178
|
+
const filePath = path.relative(this.rootContext || process.cwd(), absolutePath);
|
|
179
|
+
if (absolutePath.includes("node_modules")) return source;
|
|
180
|
+
if (!/\.[jt]sx$/.test(filePath)) return source;
|
|
181
|
+
if (/\.(test|spec|stories)\.[jt]sx?$/.test(filePath)) return source;
|
|
182
|
+
try {
|
|
183
|
+
const result = transform(source, {
|
|
184
|
+
filePath,
|
|
185
|
+
locale: options.locale,
|
|
186
|
+
messages: options.messages,
|
|
187
|
+
mode: options.mode ?? "auto"
|
|
188
|
+
});
|
|
189
|
+
if (result.hasTranslations) {
|
|
190
|
+
console.log(
|
|
191
|
+
`[better-intl] Transformed ${filePath} (${result.hasTranslations})`
|
|
192
|
+
);
|
|
193
|
+
return result.code;
|
|
194
|
+
}
|
|
195
|
+
return source;
|
|
196
|
+
} catch (err) {
|
|
197
|
+
console.error(`[better-intl] Error transforming ${filePath}:`, err);
|
|
198
|
+
return source;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
module.exports = betterIntlLoader;
|
|
203
|
+
//# sourceMappingURL=webpack-loader.cjs.map
|
|
204
|
+
//# sourceMappingURL=webpack-loader.cjs.map
|
|
@@ -0,0 +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"]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webpack loader for better-intl.
|
|
3
|
+
*
|
|
4
|
+
* Transforms JSX text nodes into t() calls automatically during build.
|
|
5
|
+
* Used by Next.js integration to enable zero-config i18n.
|
|
6
|
+
*/
|
|
7
|
+
interface BetterIntlLoaderOptions {
|
|
8
|
+
locale?: string;
|
|
9
|
+
messages?: Record<string, string>;
|
|
10
|
+
mode?: "auto" | "explicit";
|
|
11
|
+
}
|
|
12
|
+
declare function betterIntlLoader(this: any, source: string): string;
|
|
13
|
+
|
|
14
|
+
export { type BetterIntlLoaderOptions, betterIntlLoader as default };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webpack loader for better-intl.
|
|
3
|
+
*
|
|
4
|
+
* Transforms JSX text nodes into t() calls automatically during build.
|
|
5
|
+
* Used by Next.js integration to enable zero-config i18n.
|
|
6
|
+
*/
|
|
7
|
+
interface BetterIntlLoaderOptions {
|
|
8
|
+
locale?: string;
|
|
9
|
+
messages?: Record<string, string>;
|
|
10
|
+
mode?: "auto" | "explicit";
|
|
11
|
+
}
|
|
12
|
+
declare function betterIntlLoader(this: any, source: string): string;
|
|
13
|
+
|
|
14
|
+
export { type BetterIntlLoaderOptions, betterIntlLoader as default };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"webpack-loader.js"}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@better-intl/compiler",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AST extractor and Babel/SWC transform for better-intl",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"require": "./dist/index.cjs",
|
|
13
|
+
"types": "./dist/index.d.ts"
|
|
14
|
+
},
|
|
15
|
+
"./babel": {
|
|
16
|
+
"import": "./dist/babel.js",
|
|
17
|
+
"require": "./dist/babel.cjs",
|
|
18
|
+
"types": "./dist/babel.d.ts"
|
|
19
|
+
},
|
|
20
|
+
"./webpack-loader": {
|
|
21
|
+
"import": "./dist/webpack-loader.js",
|
|
22
|
+
"require": "./dist/webpack-loader.cjs",
|
|
23
|
+
"types": "./dist/webpack-loader.d.ts"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist"
|
|
28
|
+
],
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@better-intl/core": "0.1.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@babel/core": "^7.26.0",
|
|
34
|
+
"@babel/generator": "^7.26.0",
|
|
35
|
+
"@babel/parser": "^7.26.0",
|
|
36
|
+
"@babel/traverse": "^7.26.0",
|
|
37
|
+
"@babel/types": "^7.26.0",
|
|
38
|
+
"@types/babel__core": "^7.20.0",
|
|
39
|
+
"@types/babel__generator": "^7.27.0",
|
|
40
|
+
"@types/babel__traverse": "^7.20.0"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"@babel/core": "^7.20.0"
|
|
44
|
+
},
|
|
45
|
+
"peerDependenciesMeta": {
|
|
46
|
+
"@babel/core": {
|
|
47
|
+
"optional": true
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"sideEffects": false,
|
|
51
|
+
"license": "MIT",
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "tsup",
|
|
54
|
+
"dev": "tsup --watch"
|
|
55
|
+
}
|
|
56
|
+
}
|