@linguanahq/compiler 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -14
- package/dist/index.js +51 -17
- package/dist/transform.d.ts +17 -0
- package/dist/transform.js +468 -0
- package/package.json +12 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import generateImport from "@babel/generator";
|
|
2
|
-
import { type Diagnostic, type ManifestMessage } from "@linguanahq/catalog";
|
|
3
1
|
import { type Plugin } from "vite";
|
|
4
|
-
|
|
2
|
+
export { type TransformOptions, type TransformResult, transformSource, } from "./transform";
|
|
5
3
|
export type StrictPolicy = boolean | {
|
|
6
4
|
ambiguous?: boolean;
|
|
7
5
|
unsafe?: boolean;
|
|
@@ -25,16 +23,5 @@ export type LinguanaPluginOptions = {
|
|
|
25
23
|
publish: "development";
|
|
26
24
|
};
|
|
27
25
|
};
|
|
28
|
-
export type TransformResult = {
|
|
29
|
-
code: string;
|
|
30
|
-
map: ReturnType<typeof generate>["map"];
|
|
31
|
-
messages: ManifestMessage[];
|
|
32
|
-
diagnostics: Diagnostic[];
|
|
33
|
-
};
|
|
34
|
-
export declare function transformSource(code: string, filename: string, options: Pick<LinguanaPluginOptions, "sourceLocale"> & {
|
|
35
|
-
root?: string;
|
|
36
|
-
runtimePackage?: string;
|
|
37
|
-
compilerRuntime?: string;
|
|
38
|
-
}): TransformResult;
|
|
39
26
|
export declare function linguana(options: LinguanaPluginOptions): Plugin;
|
|
40
27
|
export default linguana;
|
package/dist/index.js
CHANGED
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
-
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
3
|
+
import { dirname, isAbsolute, relative as relative2, resolve } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
contentHash as contentHash2,
|
|
6
|
+
manifestHash,
|
|
7
|
+
manifestSchema,
|
|
8
|
+
SCHEMA_VERSION
|
|
9
|
+
} from "@linguanahq/catalog";
|
|
10
|
+
import { createFilter } from "vite";
|
|
11
|
+
|
|
12
|
+
// src/transform.ts
|
|
13
|
+
import { relative } from "node:path";
|
|
4
14
|
import generateImport from "@babel/generator";
|
|
5
15
|
import { parse } from "@babel/parser";
|
|
6
16
|
import traverseImport from "@babel/traverse";
|
|
7
17
|
import * as t from "@babel/types";
|
|
8
18
|
import {
|
|
9
19
|
contentHash,
|
|
10
|
-
manifestHash,
|
|
11
|
-
manifestSchema,
|
|
12
20
|
normalizeSource,
|
|
13
21
|
parseMessagePlaceholders,
|
|
14
|
-
SCHEMA_VERSION,
|
|
15
22
|
stableMessageId
|
|
16
23
|
} from "@linguanahq/catalog";
|
|
17
|
-
import { createFilter } from "vite";
|
|
18
|
-
var COMPILER_VERSION = "0.1.1";
|
|
19
24
|
var generate = generateImport.default ?? generateImport;
|
|
20
25
|
var traverse = traverseImport.default ?? traverseImport;
|
|
21
26
|
var EXCLUDED_ELEMENTS = /* @__PURE__ */ new Set(["script", "style", "code", "pre"]);
|
|
@@ -149,8 +154,15 @@ function isTranslationCall(path, runtimePackage = "@linguanahq/react") {
|
|
|
149
154
|
}
|
|
150
155
|
const declarator = binding.path.isVariableDeclarator() ? binding.path : binding.path.findParent((candidate) => candidate.isVariableDeclarator());
|
|
151
156
|
if (!declarator?.isVariableDeclarator()) return false;
|
|
152
|
-
|
|
157
|
+
const init = t.isAwaitExpression(declarator.node.init) ? declarator.node.init.argument : declarator.node.init;
|
|
158
|
+
if (!t.isCallExpression(init) || !t.isIdentifier(init.callee) || !t.isObjectPattern(declarator.node.id))
|
|
153
159
|
return false;
|
|
160
|
+
const factory = path.scope.getBinding(init.callee.name);
|
|
161
|
+
const imported = factory?.path;
|
|
162
|
+
const nextFactory = imported?.isImportSpecifier() && imported.parentPath.isImportDeclaration() && ["@linguanahq/next/client", "@linguanahq/next/server"].includes(
|
|
163
|
+
imported.parentPath.node.source.value
|
|
164
|
+
) && t.isIdentifier(imported.node.imported) && ["useTranslation", "getTranslations"].includes(imported.node.imported.name);
|
|
165
|
+
if (!nextFactory && init.callee.name !== "useTranslation") return false;
|
|
154
166
|
return declarator.node.id.properties.some(
|
|
155
167
|
(property) => t.isObjectProperty(property) && t.isIdentifier(property.key, { name: "t" }) && t.isIdentifier(property.value, { name })
|
|
156
168
|
);
|
|
@@ -162,6 +174,25 @@ function transformSource(code, filename, options) {
|
|
|
162
174
|
sourceFilename: filename,
|
|
163
175
|
plugins: ["jsx", "typescript"]
|
|
164
176
|
});
|
|
177
|
+
const names = /* @__PURE__ */ new Set();
|
|
178
|
+
traverse(ast, {
|
|
179
|
+
Identifier(path) {
|
|
180
|
+
names.add(path.node.name);
|
|
181
|
+
},
|
|
182
|
+
JSXIdentifier(path) {
|
|
183
|
+
names.add(path.node.name);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
const unique = (base) => {
|
|
187
|
+
let name = base;
|
|
188
|
+
let suffix = 0;
|
|
189
|
+
while (names.has(name)) name = `${base}${++suffix}`;
|
|
190
|
+
names.add(name);
|
|
191
|
+
return name;
|
|
192
|
+
};
|
|
193
|
+
const messageName = unique("__LinguanaMessage");
|
|
194
|
+
const attributesName = unique("__LinguanaAttributes");
|
|
195
|
+
const localizedName = unique("__localized");
|
|
165
196
|
const messages = /* @__PURE__ */ new Map();
|
|
166
197
|
const diagnostics = [];
|
|
167
198
|
let needsRuntime = false;
|
|
@@ -236,7 +267,7 @@ function transformSource(code, filename, options) {
|
|
|
236
267
|
t.jsxExpressionContainer(
|
|
237
268
|
t.jsxElement(
|
|
238
269
|
t.jsxOpeningElement(
|
|
239
|
-
t.jsxIdentifier(
|
|
270
|
+
t.jsxIdentifier(messageName),
|
|
240
271
|
[
|
|
241
272
|
t.jsxAttribute(t.jsxIdentifier("id"), t.stringLiteral(id)),
|
|
242
273
|
t.jsxAttribute(
|
|
@@ -373,7 +404,7 @@ function transformSource(code, filename, options) {
|
|
|
373
404
|
if (t.isJSXAttribute(attribute) && t.isJSXIdentifier(attribute.name) && localizedNames.includes(attribute.name.name)) {
|
|
374
405
|
attribute.value = t.jsxExpressionContainer(
|
|
375
406
|
t.memberExpression(
|
|
376
|
-
t.identifier(
|
|
407
|
+
t.identifier(localizedName),
|
|
377
408
|
t.stringLiteral(attribute.name.name),
|
|
378
409
|
true
|
|
379
410
|
)
|
|
@@ -382,7 +413,7 @@ function transformSource(code, filename, options) {
|
|
|
382
413
|
}
|
|
383
414
|
const wrapper = t.jsxElement(
|
|
384
415
|
t.jsxOpeningElement(
|
|
385
|
-
t.jsxIdentifier(
|
|
416
|
+
t.jsxIdentifier(attributesName),
|
|
386
417
|
[
|
|
387
418
|
t.jsxAttribute(
|
|
388
419
|
t.jsxIdentifier("descriptors"),
|
|
@@ -391,11 +422,11 @@ function transformSource(code, filename, options) {
|
|
|
391
422
|
],
|
|
392
423
|
false
|
|
393
424
|
),
|
|
394
|
-
t.jsxClosingElement(t.jsxIdentifier(
|
|
425
|
+
t.jsxClosingElement(t.jsxIdentifier(attributesName)),
|
|
395
426
|
[
|
|
396
427
|
t.jsxExpressionContainer(
|
|
397
428
|
t.arrowFunctionExpression(
|
|
398
|
-
[t.identifier(
|
|
429
|
+
[t.identifier(localizedName)],
|
|
399
430
|
original
|
|
400
431
|
)
|
|
401
432
|
)
|
|
@@ -413,11 +444,11 @@ function transformSource(code, filename, options) {
|
|
|
413
444
|
t.importDeclaration(
|
|
414
445
|
[
|
|
415
446
|
t.importSpecifier(
|
|
416
|
-
t.identifier(
|
|
447
|
+
t.identifier(messageName),
|
|
417
448
|
t.identifier("LinguanaMessage")
|
|
418
449
|
),
|
|
419
450
|
t.importSpecifier(
|
|
420
|
-
t.identifier(
|
|
451
|
+
t.identifier(attributesName),
|
|
421
452
|
t.identifier("LinguanaAttributes")
|
|
422
453
|
)
|
|
423
454
|
],
|
|
@@ -443,6 +474,9 @@ function transformSource(code, filename, options) {
|
|
|
443
474
|
diagnostics
|
|
444
475
|
};
|
|
445
476
|
}
|
|
477
|
+
|
|
478
|
+
// src/index.ts
|
|
479
|
+
var COMPILER_VERSION = "0.1.2";
|
|
446
480
|
function shouldFail(strict, diagnostics) {
|
|
447
481
|
if (!strict) return false;
|
|
448
482
|
if (strict === true)
|
|
@@ -519,7 +553,7 @@ async function translateManifest(options, manifestId, hash, onProgress) {
|
|
|
519
553
|
const translation = options.translation;
|
|
520
554
|
if (!translation || !options.apiUrl || !options.token) return;
|
|
521
555
|
const endpoint = options.apiUrl.replace(/\/$/, "");
|
|
522
|
-
const buildKey =
|
|
556
|
+
const buildKey = contentHash2({
|
|
523
557
|
manifestHash: hash,
|
|
524
558
|
targetLocales: [...translation.targetLocales].sort(),
|
|
525
559
|
publish: translation.publish
|
|
@@ -593,7 +627,7 @@ async function translateManifest(options, manifestId, hash, onProgress) {
|
|
|
593
627
|
headers: {
|
|
594
628
|
authorization: `Bearer ${options.token}`,
|
|
595
629
|
"content-type": "application/json",
|
|
596
|
-
"idempotency-key": `${buildKey}:release:${
|
|
630
|
+
"idempotency-key": `${buildKey}:release:${contentHash2(
|
|
597
631
|
created.jobs.map(({ id }) => id).sort()
|
|
598
632
|
).slice(0, 16)}`
|
|
599
633
|
},
|
|
@@ -661,7 +695,7 @@ function linguana(options) {
|
|
|
661
695
|
},
|
|
662
696
|
transform(code, id) {
|
|
663
697
|
const cleanId = id.split("?", 1)[0] ?? id;
|
|
664
|
-
const projectRelativeId =
|
|
698
|
+
const projectRelativeId = relative2(root, cleanId);
|
|
665
699
|
if (restrictToRoot && (projectRelativeId === ".." || projectRelativeId.startsWith("../") || projectRelativeId.startsWith("..\\") || isAbsolute(projectRelativeId)))
|
|
666
700
|
return null;
|
|
667
701
|
if (!filter(cleanId)) return null;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import generateImport from "@babel/generator";
|
|
2
|
+
import { type Diagnostic, type ManifestMessage } from "@linguanahq/catalog";
|
|
3
|
+
export type TransformOptions = {
|
|
4
|
+
sourceLocale: string;
|
|
5
|
+
root?: string;
|
|
6
|
+
runtimePackage?: string;
|
|
7
|
+
compilerRuntime?: string;
|
|
8
|
+
};
|
|
9
|
+
declare const generate: typeof generateImport;
|
|
10
|
+
export type TransformResult = {
|
|
11
|
+
code: string;
|
|
12
|
+
map: ReturnType<typeof generate>["map"];
|
|
13
|
+
messages: ManifestMessage[];
|
|
14
|
+
diagnostics: Diagnostic[];
|
|
15
|
+
};
|
|
16
|
+
export declare function transformSource(code: string, filename: string, options: TransformOptions): TransformResult;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
// src/transform.ts
|
|
2
|
+
import { relative } from "node:path";
|
|
3
|
+
import generateImport from "@babel/generator";
|
|
4
|
+
import { parse } from "@babel/parser";
|
|
5
|
+
import traverseImport from "@babel/traverse";
|
|
6
|
+
import * as t from "@babel/types";
|
|
7
|
+
import {
|
|
8
|
+
contentHash,
|
|
9
|
+
normalizeSource,
|
|
10
|
+
parseMessagePlaceholders,
|
|
11
|
+
stableMessageId
|
|
12
|
+
} from "@linguanahq/catalog";
|
|
13
|
+
var generate = generateImport.default ?? generateImport;
|
|
14
|
+
var traverse = traverseImport.default ?? traverseImport;
|
|
15
|
+
var EXCLUDED_ELEMENTS = /* @__PURE__ */ new Set(["script", "style", "code", "pre"]);
|
|
16
|
+
var ALLOWED_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
17
|
+
"title",
|
|
18
|
+
"placeholder",
|
|
19
|
+
"aria-label",
|
|
20
|
+
"alt",
|
|
21
|
+
"label",
|
|
22
|
+
"value"
|
|
23
|
+
]);
|
|
24
|
+
var MACHINE_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
25
|
+
"id",
|
|
26
|
+
"class",
|
|
27
|
+
"className",
|
|
28
|
+
"name",
|
|
29
|
+
"role",
|
|
30
|
+
"href",
|
|
31
|
+
"src",
|
|
32
|
+
"action",
|
|
33
|
+
"method",
|
|
34
|
+
"data-testid",
|
|
35
|
+
"data-test-id",
|
|
36
|
+
"data-analytics",
|
|
37
|
+
"data-event"
|
|
38
|
+
]);
|
|
39
|
+
function jsxName(node) {
|
|
40
|
+
if (t.isJSXIdentifier(node)) return node.name;
|
|
41
|
+
if (t.isJSXMemberExpression(node)) return jsxName(node.property);
|
|
42
|
+
return `${node.namespace.name}:${node.name.name}`;
|
|
43
|
+
}
|
|
44
|
+
function renderedJsxText(value) {
|
|
45
|
+
const lines = value.replace(/\r\n?/g, "\n").split("\n");
|
|
46
|
+
let result = "";
|
|
47
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
48
|
+
let line = lines[index]?.replace(/\t/g, " ") ?? "";
|
|
49
|
+
if (index !== 0) line = line.replace(/^\s+/, "");
|
|
50
|
+
if (index !== lines.length - 1) line = line.replace(/\s+$/, "");
|
|
51
|
+
if (!line) continue;
|
|
52
|
+
if (result && !result.endsWith(" ")) result += " ";
|
|
53
|
+
result += line;
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
function location(path, root, filename) {
|
|
58
|
+
const start = path.node.loc?.start;
|
|
59
|
+
return {
|
|
60
|
+
file: relative(root, filename).replaceAll("\\", "/"),
|
|
61
|
+
line: start?.line ?? 1,
|
|
62
|
+
column: start?.column ?? 0
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function getLiteralAttribute(opening, name) {
|
|
66
|
+
for (const attribute of opening.attributes) {
|
|
67
|
+
if (t.isJSXAttribute(attribute) && t.isJSXIdentifier(attribute.name) && attribute.name.name === name && t.isStringLiteral(attribute.value)) {
|
|
68
|
+
return attribute.value.value;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return void 0;
|
|
72
|
+
}
|
|
73
|
+
function hasAttribute(opening, name) {
|
|
74
|
+
return opening.attributes.some(
|
|
75
|
+
(attribute) => t.isJSXAttribute(attribute) && t.isJSXIdentifier(attribute.name) && attribute.name.name === name
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
function attributeValue(opening, name) {
|
|
79
|
+
for (const attribute of opening.attributes) {
|
|
80
|
+
if (!t.isJSXAttribute(attribute) || !t.isJSXIdentifier(attribute.name, { name }))
|
|
81
|
+
continue;
|
|
82
|
+
if (attribute.value === null) return true;
|
|
83
|
+
if (t.isStringLiteral(attribute.value)) return attribute.value.value;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function isUnsafeStaticCopy(source) {
|
|
87
|
+
const normalized = normalizeSource(source);
|
|
88
|
+
return /^(?:https?:\/\/|mailto:|tel:|www\.)/i.test(normalized) || /^\/?[\w.-]+(?:\/[\w./-]+)+(?:\?\S*)?$/.test(normalized) || /^[a-z0-9]+(?:[_:.][a-z0-9-]+)+$/i.test(normalized);
|
|
89
|
+
}
|
|
90
|
+
function isExcluded(path) {
|
|
91
|
+
return Boolean(
|
|
92
|
+
path.findParent((parent) => {
|
|
93
|
+
if (!parent.isJSXElement()) return false;
|
|
94
|
+
const opening = parent.node.openingElement;
|
|
95
|
+
const tag = jsxName(opening.name);
|
|
96
|
+
return tag === "T" || EXCLUDED_ELEMENTS.has(tag.toLowerCase()) || hasAttribute(opening, "data-no-translate") || hasAttribute(opening, "hidden") || attributeValue(opening, "aria-hidden") === "true" || tag.toLowerCase() === "input" && attributeValue(opening, "type") === "hidden";
|
|
97
|
+
})
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
function isExcludedElement(opening) {
|
|
101
|
+
const tag = jsxName(opening.name);
|
|
102
|
+
return EXCLUDED_ELEMENTS.has(tag.toLowerCase()) || hasAttribute(opening, "data-no-translate") || hasAttribute(opening, "hidden") || attributeValue(opening, "aria-hidden") === "true" || tag.toLowerCase() === "input" && attributeValue(opening, "type") === "hidden";
|
|
103
|
+
}
|
|
104
|
+
function hasMixedRichChildren(path) {
|
|
105
|
+
return Boolean(
|
|
106
|
+
path.findParent((parent) => {
|
|
107
|
+
if (!parent.isJSXElement()) return false;
|
|
108
|
+
const children = parent.node.children;
|
|
109
|
+
const hasElement = children.some(
|
|
110
|
+
(child) => t.isJSXElement(child) || t.isJSXExpressionContainer(child) && !t.isJSXEmptyExpression(child.expression)
|
|
111
|
+
);
|
|
112
|
+
const hasText = children.some(
|
|
113
|
+
(child) => t.isJSXText(child) && Boolean(renderedJsxText(child.value))
|
|
114
|
+
);
|
|
115
|
+
return hasElement && hasText;
|
|
116
|
+
})
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
function placeholders(source) {
|
|
120
|
+
try {
|
|
121
|
+
return parseMessagePlaceholders(source);
|
|
122
|
+
} catch {
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function stringProperty(object, name) {
|
|
127
|
+
for (const property of object.properties) {
|
|
128
|
+
if (!t.isObjectProperty(property)) continue;
|
|
129
|
+
const key = t.isIdentifier(property.key) ? property.key.name : t.isStringLiteral(property.key) ? property.key.value : void 0;
|
|
130
|
+
if (key === name && t.isStringLiteral(property.value))
|
|
131
|
+
return property.value.value;
|
|
132
|
+
}
|
|
133
|
+
return void 0;
|
|
134
|
+
}
|
|
135
|
+
function isTranslationCall(path, runtimePackage = "@linguanahq/react") {
|
|
136
|
+
if (!t.isIdentifier(path.node.callee)) return false;
|
|
137
|
+
const name = path.node.callee.name;
|
|
138
|
+
const binding = path.scope.getBinding(name);
|
|
139
|
+
if (!binding) return false;
|
|
140
|
+
if (binding.path.isImportSpecifier()) {
|
|
141
|
+
const declaration = binding.path.parentPath;
|
|
142
|
+
return declaration.isImportDeclaration() && declaration.node.source.value === runtimePackage && t.isIdentifier(binding.path.node.imported, { name: "t" });
|
|
143
|
+
}
|
|
144
|
+
const declarator = binding.path.isVariableDeclarator() ? binding.path : binding.path.findParent((candidate) => candidate.isVariableDeclarator());
|
|
145
|
+
if (!declarator?.isVariableDeclarator()) return false;
|
|
146
|
+
const init = t.isAwaitExpression(declarator.node.init) ? declarator.node.init.argument : declarator.node.init;
|
|
147
|
+
if (!t.isCallExpression(init) || !t.isIdentifier(init.callee) || !t.isObjectPattern(declarator.node.id))
|
|
148
|
+
return false;
|
|
149
|
+
const factory = path.scope.getBinding(init.callee.name);
|
|
150
|
+
const imported = factory?.path;
|
|
151
|
+
const nextFactory = imported?.isImportSpecifier() && imported.parentPath.isImportDeclaration() && ["@linguanahq/next/client", "@linguanahq/next/server"].includes(
|
|
152
|
+
imported.parentPath.node.source.value
|
|
153
|
+
) && t.isIdentifier(imported.node.imported) && ["useTranslation", "getTranslations"].includes(imported.node.imported.name);
|
|
154
|
+
if (!nextFactory && init.callee.name !== "useTranslation") return false;
|
|
155
|
+
return declarator.node.id.properties.some(
|
|
156
|
+
(property) => t.isObjectProperty(property) && t.isIdentifier(property.key, { name: "t" }) && t.isIdentifier(property.value, { name })
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
function transformSource(code, filename, options) {
|
|
160
|
+
const root = options.root ?? process.cwd();
|
|
161
|
+
const ast = parse(code, {
|
|
162
|
+
sourceType: "module",
|
|
163
|
+
sourceFilename: filename,
|
|
164
|
+
plugins: ["jsx", "typescript"]
|
|
165
|
+
});
|
|
166
|
+
const names = /* @__PURE__ */ new Set();
|
|
167
|
+
traverse(ast, {
|
|
168
|
+
Identifier(path) {
|
|
169
|
+
names.add(path.node.name);
|
|
170
|
+
},
|
|
171
|
+
JSXIdentifier(path) {
|
|
172
|
+
names.add(path.node.name);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
const unique = (base) => {
|
|
176
|
+
let name = base;
|
|
177
|
+
let suffix = 0;
|
|
178
|
+
while (names.has(name)) name = `${base}${++suffix}`;
|
|
179
|
+
names.add(name);
|
|
180
|
+
return name;
|
|
181
|
+
};
|
|
182
|
+
const messageName = unique("__LinguanaMessage");
|
|
183
|
+
const attributesName = unique("__LinguanaAttributes");
|
|
184
|
+
const localizedName = unique("__localized");
|
|
185
|
+
const messages = /* @__PURE__ */ new Map();
|
|
186
|
+
const diagnostics = [];
|
|
187
|
+
let needsRuntime = false;
|
|
188
|
+
const richDiagnosticLocations = /* @__PURE__ */ new Set();
|
|
189
|
+
const addMessage = (message) => {
|
|
190
|
+
const existing = messages.get(message.id);
|
|
191
|
+
if (existing && existing.contentHash !== message.contentHash) {
|
|
192
|
+
diagnostics.push({
|
|
193
|
+
code: "duplicate-id",
|
|
194
|
+
severity: "error",
|
|
195
|
+
message: `Message ID ${message.id} refers to incompatible source content`,
|
|
196
|
+
location: message.locations[0],
|
|
197
|
+
remediation: "Choose a unique explicit ID or make the source messages identical."
|
|
198
|
+
});
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (existing) {
|
|
202
|
+
existing.locations.push(...message.locations);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
messages.set(message.id, message);
|
|
206
|
+
};
|
|
207
|
+
traverse(ast, {
|
|
208
|
+
JSXText(path) {
|
|
209
|
+
const source = renderedJsxText(path.node.value);
|
|
210
|
+
if (!source || isExcluded(path)) return;
|
|
211
|
+
if (isUnsafeStaticCopy(source)) {
|
|
212
|
+
diagnostics.push({
|
|
213
|
+
code: "unsafe-static-copy",
|
|
214
|
+
severity: "info",
|
|
215
|
+
message: "URL-like or machine-readable text was excluded from extraction.",
|
|
216
|
+
location: location(path, root, filename)
|
|
217
|
+
});
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (hasMixedRichChildren(path)) {
|
|
221
|
+
const itemLocation = location(path, root, filename);
|
|
222
|
+
const key = `${itemLocation.file}:${itemLocation.line}:${itemLocation.column}`;
|
|
223
|
+
if (!richDiagnosticLocations.has(key)) {
|
|
224
|
+
richDiagnosticLocations.add(key);
|
|
225
|
+
diagnostics.push({
|
|
226
|
+
code: "ambiguous-rich-text",
|
|
227
|
+
severity: "warning",
|
|
228
|
+
message: "Mixed text and JSX elements require an explicit <T> message.",
|
|
229
|
+
location: itemLocation,
|
|
230
|
+
remediation: "Use <T defaultMessage components> or data-no-translate."
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const parent = path.findParent((candidate) => candidate.isJSXElement());
|
|
236
|
+
if (!parent?.isJSXElement()) return;
|
|
237
|
+
const element = jsxName(parent.node.openingElement.name);
|
|
238
|
+
const context = { kind: "text", element };
|
|
239
|
+
const id = stableMessageId({
|
|
240
|
+
sourceLocale: options.sourceLocale,
|
|
241
|
+
source,
|
|
242
|
+
context
|
|
243
|
+
});
|
|
244
|
+
addMessage({
|
|
245
|
+
id,
|
|
246
|
+
source,
|
|
247
|
+
normalized: normalizeSource(source),
|
|
248
|
+
contentHash: contentHash(source),
|
|
249
|
+
context,
|
|
250
|
+
placeholders: placeholders(source),
|
|
251
|
+
confidence: "high",
|
|
252
|
+
locations: [location(path, root, filename)]
|
|
253
|
+
});
|
|
254
|
+
needsRuntime = true;
|
|
255
|
+
path.replaceWith(
|
|
256
|
+
t.jsxExpressionContainer(
|
|
257
|
+
t.jsxElement(
|
|
258
|
+
t.jsxOpeningElement(
|
|
259
|
+
t.jsxIdentifier(messageName),
|
|
260
|
+
[
|
|
261
|
+
t.jsxAttribute(t.jsxIdentifier("id"), t.stringLiteral(id)),
|
|
262
|
+
t.jsxAttribute(
|
|
263
|
+
t.jsxIdentifier("defaultMessage"),
|
|
264
|
+
t.stringLiteral(source)
|
|
265
|
+
)
|
|
266
|
+
],
|
|
267
|
+
true
|
|
268
|
+
),
|
|
269
|
+
null,
|
|
270
|
+
[],
|
|
271
|
+
true
|
|
272
|
+
)
|
|
273
|
+
)
|
|
274
|
+
);
|
|
275
|
+
},
|
|
276
|
+
JSXExpressionContainer(path) {
|
|
277
|
+
if (isExcluded(path) || t.isJSXEmptyExpression(path.node.expression) || !path.parentPath.isJSXElement())
|
|
278
|
+
return;
|
|
279
|
+
if (t.isConditionalExpression(path.node.expression) || t.isBinaryExpression(path.node.expression) || t.isTemplateLiteral(path.node.expression)) {
|
|
280
|
+
diagnostics.push({
|
|
281
|
+
code: "dynamic-candidate",
|
|
282
|
+
severity: "warning",
|
|
283
|
+
message: "Dynamic JSX copy was not automatically extracted.",
|
|
284
|
+
location: location(path, root, filename),
|
|
285
|
+
remediation: "Use t(), <T>, data-no-translate, or an explicit annotation."
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
CallExpression(path) {
|
|
290
|
+
if (!isTranslationCall(path, options.runtimePackage)) return;
|
|
291
|
+
const [sourceNode, , optionsNode] = path.node.arguments;
|
|
292
|
+
if (!t.isStringLiteral(sourceNode)) return;
|
|
293
|
+
const explicitId = t.isObjectExpression(optionsNode) ? stringProperty(optionsNode, "id") : void 0;
|
|
294
|
+
const developer = t.isObjectExpression(optionsNode) ? stringProperty(optionsNode, "context") : void 0;
|
|
295
|
+
const context = { kind: "explicit", developer };
|
|
296
|
+
const id = stableMessageId({
|
|
297
|
+
sourceLocale: options.sourceLocale,
|
|
298
|
+
source: sourceNode.value,
|
|
299
|
+
context,
|
|
300
|
+
explicitId
|
|
301
|
+
});
|
|
302
|
+
addMessage({
|
|
303
|
+
id,
|
|
304
|
+
source: sourceNode.value,
|
|
305
|
+
normalized: normalizeSource(sourceNode.value),
|
|
306
|
+
contentHash: contentHash(sourceNode.value),
|
|
307
|
+
context,
|
|
308
|
+
placeholders: placeholders(sourceNode.value),
|
|
309
|
+
confidence: "explicit",
|
|
310
|
+
locations: [location(path, root, filename)]
|
|
311
|
+
});
|
|
312
|
+
},
|
|
313
|
+
JSXElement: {
|
|
314
|
+
exit(path) {
|
|
315
|
+
if (isExcluded(path) || isExcludedElement(path.node.openingElement))
|
|
316
|
+
return;
|
|
317
|
+
const opening = path.node.openingElement;
|
|
318
|
+
const tag = jsxName(opening.name);
|
|
319
|
+
if (tag === "T") {
|
|
320
|
+
const source = getLiteralAttribute(opening, "defaultMessage");
|
|
321
|
+
if (!source) return;
|
|
322
|
+
const explicitId = getLiteralAttribute(opening, "id");
|
|
323
|
+
const developer = getLiteralAttribute(opening, "context");
|
|
324
|
+
const context = { kind: "explicit", developer };
|
|
325
|
+
const id = stableMessageId({
|
|
326
|
+
sourceLocale: options.sourceLocale,
|
|
327
|
+
source,
|
|
328
|
+
context,
|
|
329
|
+
explicitId
|
|
330
|
+
});
|
|
331
|
+
addMessage({
|
|
332
|
+
id,
|
|
333
|
+
source,
|
|
334
|
+
normalized: normalizeSource(source),
|
|
335
|
+
contentHash: contentHash(source),
|
|
336
|
+
context,
|
|
337
|
+
placeholders: placeholders(source),
|
|
338
|
+
confidence: "explicit",
|
|
339
|
+
locations: [location(path, root, filename)]
|
|
340
|
+
});
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const descriptors = [];
|
|
344
|
+
const localizedNames = [];
|
|
345
|
+
for (const attribute of opening.attributes) {
|
|
346
|
+
if (!t.isJSXAttribute(attribute) || !t.isJSXIdentifier(attribute.name) || !t.isStringLiteral(attribute.value))
|
|
347
|
+
continue;
|
|
348
|
+
const name = attribute.name.name;
|
|
349
|
+
if (MACHINE_ATTRIBUTES.has(name) || name.startsWith("on") || name === "data-translate")
|
|
350
|
+
continue;
|
|
351
|
+
const optInValue = attributeValue(opening, "data-translate");
|
|
352
|
+
const optedIn = typeof optInValue === "string" && optInValue.split(/[\s,]+/).includes(name);
|
|
353
|
+
if (!ALLOWED_ATTRIBUTES.has(name) && !optedIn) continue;
|
|
354
|
+
if (name === "value" && tag.toLowerCase() !== "button") continue;
|
|
355
|
+
const source = attribute.value.value;
|
|
356
|
+
if (!source || isUnsafeStaticCopy(source)) continue;
|
|
357
|
+
const context = {
|
|
358
|
+
kind: "attribute",
|
|
359
|
+
element: tag,
|
|
360
|
+
attribute: name
|
|
361
|
+
};
|
|
362
|
+
const id = stableMessageId({
|
|
363
|
+
sourceLocale: options.sourceLocale,
|
|
364
|
+
source,
|
|
365
|
+
context
|
|
366
|
+
});
|
|
367
|
+
addMessage({
|
|
368
|
+
id,
|
|
369
|
+
source,
|
|
370
|
+
normalized: normalizeSource(source),
|
|
371
|
+
contentHash: contentHash(source),
|
|
372
|
+
context,
|
|
373
|
+
placeholders: placeholders(source),
|
|
374
|
+
confidence: "high",
|
|
375
|
+
locations: [location(path, root, filename)]
|
|
376
|
+
});
|
|
377
|
+
localizedNames.push(name);
|
|
378
|
+
descriptors.push(
|
|
379
|
+
t.objectExpression([
|
|
380
|
+
t.objectProperty(t.identifier("name"), t.stringLiteral(name)),
|
|
381
|
+
t.objectProperty(t.identifier("id"), t.stringLiteral(id)),
|
|
382
|
+
t.objectProperty(
|
|
383
|
+
t.identifier("defaultMessage"),
|
|
384
|
+
t.stringLiteral(source)
|
|
385
|
+
)
|
|
386
|
+
])
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
if (!descriptors.length) return;
|
|
390
|
+
needsRuntime = true;
|
|
391
|
+
const original = t.cloneNode(path.node, true);
|
|
392
|
+
for (const attribute of original.openingElement.attributes) {
|
|
393
|
+
if (t.isJSXAttribute(attribute) && t.isJSXIdentifier(attribute.name) && localizedNames.includes(attribute.name.name)) {
|
|
394
|
+
attribute.value = t.jsxExpressionContainer(
|
|
395
|
+
t.memberExpression(
|
|
396
|
+
t.identifier(localizedName),
|
|
397
|
+
t.stringLiteral(attribute.name.name),
|
|
398
|
+
true
|
|
399
|
+
)
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const wrapper = t.jsxElement(
|
|
404
|
+
t.jsxOpeningElement(
|
|
405
|
+
t.jsxIdentifier(attributesName),
|
|
406
|
+
[
|
|
407
|
+
t.jsxAttribute(
|
|
408
|
+
t.jsxIdentifier("descriptors"),
|
|
409
|
+
t.jsxExpressionContainer(t.arrayExpression(descriptors))
|
|
410
|
+
)
|
|
411
|
+
],
|
|
412
|
+
false
|
|
413
|
+
),
|
|
414
|
+
t.jsxClosingElement(t.jsxIdentifier(attributesName)),
|
|
415
|
+
[
|
|
416
|
+
t.jsxExpressionContainer(
|
|
417
|
+
t.arrowFunctionExpression(
|
|
418
|
+
[t.identifier(localizedName)],
|
|
419
|
+
original
|
|
420
|
+
)
|
|
421
|
+
)
|
|
422
|
+
]
|
|
423
|
+
);
|
|
424
|
+
path.replaceWith(wrapper);
|
|
425
|
+
path.skip();
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
Program: {
|
|
429
|
+
exit(path) {
|
|
430
|
+
if (!needsRuntime) return;
|
|
431
|
+
path.unshiftContainer(
|
|
432
|
+
"body",
|
|
433
|
+
t.importDeclaration(
|
|
434
|
+
[
|
|
435
|
+
t.importSpecifier(
|
|
436
|
+
t.identifier(messageName),
|
|
437
|
+
t.identifier("LinguanaMessage")
|
|
438
|
+
),
|
|
439
|
+
t.importSpecifier(
|
|
440
|
+
t.identifier(attributesName),
|
|
441
|
+
t.identifier("LinguanaAttributes")
|
|
442
|
+
)
|
|
443
|
+
],
|
|
444
|
+
t.stringLiteral(
|
|
445
|
+
options.compilerRuntime ?? "@linguanahq/react/compiler-runtime"
|
|
446
|
+
)
|
|
447
|
+
)
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
const output = generate(
|
|
453
|
+
ast,
|
|
454
|
+
{ sourceMaps: true, sourceFileName: filename },
|
|
455
|
+
code
|
|
456
|
+
);
|
|
457
|
+
return {
|
|
458
|
+
code: output.code,
|
|
459
|
+
map: output.map,
|
|
460
|
+
messages: [...messages.values()].sort(
|
|
461
|
+
(left, right) => left.id.localeCompare(right.id)
|
|
462
|
+
),
|
|
463
|
+
diagnostics
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
export {
|
|
467
|
+
transformSource
|
|
468
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linguanahq/compiler",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Linguana compiler integration",
|
|
6
6
|
"exports": {
|
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
9
9
|
"import": "./dist/index.js",
|
|
10
10
|
"default": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./transform": {
|
|
13
|
+
"types": "./dist/transform.d.ts",
|
|
14
|
+
"import": "./dist/transform.js",
|
|
15
|
+
"default": "./dist/transform.js"
|
|
11
16
|
}
|
|
12
17
|
},
|
|
13
18
|
"files": [
|
|
@@ -21,11 +26,16 @@
|
|
|
21
26
|
"@babel/parser": "^7.28.5",
|
|
22
27
|
"@babel/traverse": "^7.28.5",
|
|
23
28
|
"@babel/types": "^7.29.8",
|
|
24
|
-
"@linguanahq/catalog": "0.1.
|
|
29
|
+
"@linguanahq/catalog": "0.1.2"
|
|
25
30
|
},
|
|
26
31
|
"peerDependencies": {
|
|
27
32
|
"vite": ">=7 <9"
|
|
28
33
|
},
|
|
34
|
+
"peerDependenciesMeta": {
|
|
35
|
+
"vite": {
|
|
36
|
+
"optional": true
|
|
37
|
+
}
|
|
38
|
+
},
|
|
29
39
|
"publishConfig": {
|
|
30
40
|
"access": "public"
|
|
31
41
|
}
|