@saykit/transform-js 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,7 @@ import * as t from "@babel/types";
4
4
  function generateSayCallExpression(message) {
5
5
  const id = message.descriptor.id ?? message.toHashString();
6
6
  const children = generateChildExpressions(message.children);
7
- const properties = t.objectExpression([t.objectProperty(t.identifier("id"), t.stringLiteral(id)), ...children.map(([ident, expr]) => t.objectProperty(t.identifier(ident), expr))]);
7
+ const properties = t.objectExpression([t.objectProperty(t.identifier("id"), t.stringLiteral(id)), ...[...new Map(children).entries()].map(([ident, expr]) => t.objectProperty(t.identifier(`_${ident}`), expr))]);
8
8
  return t.callExpression(t.memberExpression(message.accessor, t.identifier("call")), [properties]);
9
9
  }
10
10
  function generateChildExpressions(messages) {
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { generateSayCallExpression } from "./generator.mjs";
2
- import { parseExpression } from "./parser.mjs";
2
+ import { isEquivalentPlaceholder, parseExpression } from "./parser.mjs";
3
3
  import { generate } from "@babel/generator";
4
4
  import * as parser from "@babel/parser";
5
5
  import traverse_ from "@babel/traverse";
@@ -44,7 +44,7 @@ function createJsTransformer() {
44
44
  path.node.leadingComments = path.node.leadingComments ?? [];
45
45
  const message = parseExpression(path.node);
46
46
  if (message) {
47
- assignSequenceIdentifiers(message, { current: 0 });
47
+ assignSequenceIdentifiers(message, { current: 0 }, isEquivalentPlaceholder);
48
48
  messages.push(message);
49
49
  path.skip();
50
50
  }
@@ -64,7 +64,7 @@ function createJsTransformer() {
64
64
  path.node.leadingComments = path.node.leadingComments ?? [];
65
65
  const message = parseExpression(path.node);
66
66
  if (message) {
67
- assignSequenceIdentifiers(message, { current: 0 });
67
+ assignSequenceIdentifiers(message, { current: 0 }, isEquivalentPlaceholder);
68
68
  const replacement = generateSayCallExpression(message);
69
69
  path.replaceWith(replacement);
70
70
  path.skip();
package/dist/parser.d.mts CHANGED
@@ -1,10 +1,32 @@
1
- import { ArgumentMessage, CompositeMessage } from "@saykit/config/features/messages";
1
+ import { AUTO_INCREMENT_IDENTIFIER, ArgumentMessage, CompositeMessage } from "@saykit/config/features/messages";
2
2
  import * as t from "@babel/types";
3
3
  //#region src/parser.d.ts
4
4
  declare function parseTaggedTemplateExpression(tagged: t.TaggedTemplateExpression): CompositeMessage | null;
5
5
  declare function parseCallExpression(call: t.CallExpression): CompositeMessage | null;
6
6
  declare function processExpression(expression: t.Node): [t.Expression, t.ObjectExpression | null, string | null] | null;
7
+ /**
8
+ * Read the name off a value written as a single-key object and hand back the
9
+ * value alone, so the wrapper never reaches the output: `${{ total: sum() }}`
10
+ * is `{total}` rather than `{0}`.
11
+ *
12
+ * An inline one-key object is the only shape this claims, and it is a shape
13
+ * that could not mean anything else — interpolating an object stringifies it
14
+ * to `[object Object]`, and rendering one in JSX throws. Anything else is
15
+ * returned untouched and named the way it always was.
16
+ */
17
+ declare function unwrapPlaceholder(expression: t.Expression): [string | typeof AUTO_INCREMENT_IDENTIFIER, t.Expression];
18
+ /**
19
+ * Whether two placeholders sharing a name are the same placeholder, and so
20
+ * compile to one prop a translator can move around the sentence freely.
21
+ *
22
+ * A value is compared whole: the same variable interpolated twice is one value,
23
+ * while `${name}` beside `${{ name: author.name }}` is two things claiming one
24
+ * name. An element is compared on its opening element alone — `say-tag` has
25
+ * been stripped by the time this runs, and its children come from the
26
+ * translation rather than the source.
27
+ */
28
+ declare function isEquivalentPlaceholder(a: any, b: any): boolean;
7
29
  declare function parseExpression(expression: t.Expression, fallback?: false): CompositeMessage | null;
8
30
  declare function parseExpression(expression: t.Expression, fallback: true): CompositeMessage | ArgumentMessage;
9
31
  //#endregion
10
- export { parseCallExpression, parseExpression, parseTaggedTemplateExpression, processExpression };
32
+ export { isEquivalentPlaceholder, parseCallExpression, parseExpression, parseTaggedTemplateExpression, processExpression, unwrapPlaceholder };
package/dist/parser.mjs CHANGED
@@ -1,6 +1,7 @@
1
- import { AUTO_INCREMENT_IDENTIFIER, ArgumentMessage, ChoiceMessage, CompositeMessage, LiteralMessage } from "@saykit/config/features/messages";
1
+ import { AUTO_INCREMENT_IDENTIFIER, ArgumentMessage, ChoiceMessage, CompositeMessage, LiteralMessage, validateBranchIdentifier } from "@saykit/config/features/messages";
2
2
  import * as t from "@babel/types";
3
3
  //#region src/parser.ts
4
+ const PLACEHOLDER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
4
5
  function parseTaggedTemplateExpression(tagged) {
5
6
  const processed = processExpression(tagged.tag);
6
7
  if (!processed) return null;
@@ -38,8 +39,9 @@ function parseCallExpression(call) {
38
39
  });
39
40
  return c;
40
41
  }, []);
41
- const value = call.arguments[0];
42
- const choice = new ChoiceMessage(kind, getExpressionAsKey(value), branches, value);
42
+ for (const branch of branches) validateBranchIdentifier(kind, branch.identifier);
43
+ const [identifier, value] = unwrapPlaceholder(call.arguments[0]);
44
+ const choice = new ChoiceMessage(kind, identifier, branches, value);
43
45
  return new CompositeMessage({
44
46
  id: descriptor ? findPropertyValueIfStringLiteralAsString(descriptor, "id") : void 0,
45
47
  context: descriptor ? findPropertyValueIfStringLiteralAsString(descriptor, "context") : void 0
@@ -88,6 +90,44 @@ function getExpressionAsKey(node) {
88
90
  if (t.isJSXIdentifier(node)) return node.name;
89
91
  return AUTO_INCREMENT_IDENTIFIER;
90
92
  }
93
+ /**
94
+ * Read the name off a value written as a single-key object and hand back the
95
+ * value alone, so the wrapper never reaches the output: `${{ total: sum() }}`
96
+ * is `{total}` rather than `{0}`.
97
+ *
98
+ * An inline one-key object is the only shape this claims, and it is a shape
99
+ * that could not mean anything else — interpolating an object stringifies it
100
+ * to `[object Object]`, and rendering one in JSX throws. Anything else is
101
+ * returned untouched and named the way it always was.
102
+ */
103
+ function unwrapPlaceholder(expression) {
104
+ if (!t.isObjectExpression(expression) || expression.properties.length !== 1) return [getExpressionAsKey(expression), expression];
105
+ const [property] = expression.properties;
106
+ if (!t.isObjectProperty(property) || property.computed || !t.isExpression(property.value)) return [getExpressionAsKey(expression), expression];
107
+ const name = getPropertyNameAsString(property.key);
108
+ /* v8 ignore next */
109
+ if (typeof name !== "string") return [getExpressionAsKey(expression), expression];
110
+ if (!PLACEHOLDER_PATTERN.test(name)) throw new Error(`Invalid placeholder name '${name}', expected a letter or underscore followed by letters, digits, or underscores`);
111
+ return [name, property.value];
112
+ }
113
+ /**
114
+ * Whether two placeholders sharing a name are the same placeholder, and so
115
+ * compile to one prop a translator can move around the sentence freely.
116
+ *
117
+ * A value is compared whole: the same variable interpolated twice is one value,
118
+ * while `${name}` beside `${{ name: author.name }}` is two things claiming one
119
+ * name. An element is compared on its opening element alone — `say-tag` has
120
+ * been stripped by the time this runs, and its children come from the
121
+ * translation rather than the source.
122
+ */
123
+ function isEquivalentPlaceholder(a, b) {
124
+ if (t.isJSXElement(a) || t.isJSXElement(b)) {
125
+ if (!t.isJSXElement(a) || !t.isJSXElement(b)) return false;
126
+ return t.isNodesEquivalent(a.openingElement, b.openingElement);
127
+ }
128
+ if (!t.isNode(a) || !t.isNode(b)) return false;
129
+ return t.isNodesEquivalent(a, b);
130
+ }
91
131
  function parseExpression(expression, fallback) {
92
132
  let message = null;
93
133
  switch (true) {
@@ -99,8 +139,14 @@ function parseExpression(expression, fallback) {
99
139
  break;
100
140
  }
101
141
  if (message) return message;
102
- else if (fallback) return new ArgumentMessage(getExpressionAsKey(expression), expression);
103
- else return null;
142
+ else if (fallback) {
143
+ const [key, value] = unwrapPlaceholder(expression);
144
+ if (value !== expression) {
145
+ const nested = parseExpression(value);
146
+ if (nested) return nested;
147
+ }
148
+ return new ArgumentMessage(key, value);
149
+ } else return null;
104
150
  }
105
151
  function getPropertyNameAsString(key) {
106
152
  if (t.isIdentifier(key)) return key.name;
@@ -123,4 +169,4 @@ function getTranslatorComments(comments) {
123
169
  }, []);
124
170
  }
125
171
  //#endregion
126
- export { parseCallExpression, parseExpression, parseTaggedTemplateExpression, processExpression };
172
+ export { isEquivalentPlaceholder, parseCallExpression, parseExpression, parseTaggedTemplateExpression, processExpression, unwrapPlaceholder };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saykit/transform-js",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "JavaScript and TypeScript transformer for SayKit",
5
5
  "keywords": [
6
6
  "i18n",
@@ -47,7 +47,7 @@
47
47
  "@babel/parser": "^7.29.7",
48
48
  "@babel/traverse": "^7.29.7",
49
49
  "@babel/types": "^7.29.7",
50
- "@saykit/config": "^0.5.0"
50
+ "@saykit/config": "^0.6.1"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@babel/core": "^7.29.7",