@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 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
- declare const generate: typeof generateImport;
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
- if (!t.isCallExpression(declarator.node.init) || !t.isIdentifier(declarator.node.init.callee, { name: "useTranslation" }) || !t.isObjectPattern(declarator.node.id))
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("__LinguanaMessage"),
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("__localized"),
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("__LinguanaAttributes"),
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("__LinguanaAttributes")),
425
+ t.jsxClosingElement(t.jsxIdentifier(attributesName)),
395
426
  [
396
427
  t.jsxExpressionContainer(
397
428
  t.arrowFunctionExpression(
398
- [t.identifier("__localized")],
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("__LinguanaMessage"),
447
+ t.identifier(messageName),
417
448
  t.identifier("LinguanaMessage")
418
449
  ),
419
450
  t.importSpecifier(
420
- t.identifier("__LinguanaAttributes"),
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 = contentHash({
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:${contentHash(
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 = relative(root, cleanId);
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.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.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
  }