@saykit/transform-js 0.4.1 → 0.6.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/generator.d.mts +0 -1
- package/dist/generator.mjs +1 -1
- package/dist/index.d.mts +0 -1
- package/dist/index.mjs +3 -3
- package/dist/parser.d.mts +24 -3
- package/dist/parser.mjs +50 -5
- package/package.json +2 -2
package/dist/generator.d.mts
CHANGED
package/dist/generator.mjs
CHANGED
|
@@ -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.d.mts
CHANGED
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,11 +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
|
-
|
|
4
3
|
//#region src/parser.d.ts
|
|
5
4
|
declare function parseTaggedTemplateExpression(tagged: t.TaggedTemplateExpression): CompositeMessage | null;
|
|
6
5
|
declare function parseCallExpression(call: t.CallExpression): CompositeMessage | null;
|
|
7
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;
|
|
8
29
|
declare function parseExpression(expression: t.Expression, fallback?: false): CompositeMessage | null;
|
|
9
30
|
declare function parseExpression(expression: t.Expression, fallback: true): CompositeMessage | ArgumentMessage;
|
|
10
31
|
//#endregion
|
|
11
|
-
export { parseCallExpression, parseExpression, parseTaggedTemplateExpression, processExpression };
|
|
32
|
+
export { isEquivalentPlaceholder, parseCallExpression, parseExpression, parseTaggedTemplateExpression, processExpression, unwrapPlaceholder };
|
package/dist/parser.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AUTO_INCREMENT_IDENTIFIER, ArgumentMessage, ChoiceMessage, CompositeMessage, LiteralMessage } 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,8 @@ function parseCallExpression(call) {
|
|
|
38
39
|
});
|
|
39
40
|
return c;
|
|
40
41
|
}, []);
|
|
41
|
-
const value = call.arguments[0];
|
|
42
|
-
const choice = new ChoiceMessage(kind,
|
|
42
|
+
const [identifier, value] = unwrapPlaceholder(call.arguments[0]);
|
|
43
|
+
const choice = new ChoiceMessage(kind, identifier, branches, value);
|
|
43
44
|
return new CompositeMessage({
|
|
44
45
|
id: descriptor ? findPropertyValueIfStringLiteralAsString(descriptor, "id") : void 0,
|
|
45
46
|
context: descriptor ? findPropertyValueIfStringLiteralAsString(descriptor, "context") : void 0
|
|
@@ -88,6 +89,44 @@ function getExpressionAsKey(node) {
|
|
|
88
89
|
if (t.isJSXIdentifier(node)) return node.name;
|
|
89
90
|
return AUTO_INCREMENT_IDENTIFIER;
|
|
90
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Read the name off a value written as a single-key object and hand back the
|
|
94
|
+
* value alone, so the wrapper never reaches the output: `${{ total: sum() }}`
|
|
95
|
+
* is `{total}` rather than `{0}`.
|
|
96
|
+
*
|
|
97
|
+
* An inline one-key object is the only shape this claims, and it is a shape
|
|
98
|
+
* that could not mean anything else — interpolating an object stringifies it
|
|
99
|
+
* to `[object Object]`, and rendering one in JSX throws. Anything else is
|
|
100
|
+
* returned untouched and named the way it always was.
|
|
101
|
+
*/
|
|
102
|
+
function unwrapPlaceholder(expression) {
|
|
103
|
+
if (!t.isObjectExpression(expression) || expression.properties.length !== 1) return [getExpressionAsKey(expression), expression];
|
|
104
|
+
const [property] = expression.properties;
|
|
105
|
+
if (!t.isObjectProperty(property) || property.computed || !t.isExpression(property.value)) return [getExpressionAsKey(expression), expression];
|
|
106
|
+
const name = getPropertyNameAsString(property.key);
|
|
107
|
+
/* v8 ignore next */
|
|
108
|
+
if (typeof name !== "string") return [getExpressionAsKey(expression), expression];
|
|
109
|
+
if (!PLACEHOLDER_PATTERN.test(name)) throw new Error(`Invalid placeholder name '${name}', expected a letter or underscore followed by letters, digits, or underscores`);
|
|
110
|
+
return [name, property.value];
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Whether two placeholders sharing a name are the same placeholder, and so
|
|
114
|
+
* compile to one prop a translator can move around the sentence freely.
|
|
115
|
+
*
|
|
116
|
+
* A value is compared whole: the same variable interpolated twice is one value,
|
|
117
|
+
* while `${name}` beside `${{ name: author.name }}` is two things claiming one
|
|
118
|
+
* name. An element is compared on its opening element alone — `say-tag` has
|
|
119
|
+
* been stripped by the time this runs, and its children come from the
|
|
120
|
+
* translation rather than the source.
|
|
121
|
+
*/
|
|
122
|
+
function isEquivalentPlaceholder(a, b) {
|
|
123
|
+
if (t.isJSXElement(a) || t.isJSXElement(b)) {
|
|
124
|
+
if (!t.isJSXElement(a) || !t.isJSXElement(b)) return false;
|
|
125
|
+
return t.isNodesEquivalent(a.openingElement, b.openingElement);
|
|
126
|
+
}
|
|
127
|
+
if (!t.isNode(a) || !t.isNode(b)) return false;
|
|
128
|
+
return t.isNodesEquivalent(a, b);
|
|
129
|
+
}
|
|
91
130
|
function parseExpression(expression, fallback) {
|
|
92
131
|
let message = null;
|
|
93
132
|
switch (true) {
|
|
@@ -99,8 +138,14 @@ function parseExpression(expression, fallback) {
|
|
|
99
138
|
break;
|
|
100
139
|
}
|
|
101
140
|
if (message) return message;
|
|
102
|
-
else if (fallback)
|
|
103
|
-
|
|
141
|
+
else if (fallback) {
|
|
142
|
+
const [key, value] = unwrapPlaceholder(expression);
|
|
143
|
+
if (value !== expression) {
|
|
144
|
+
const nested = parseExpression(value);
|
|
145
|
+
if (nested) return nested;
|
|
146
|
+
}
|
|
147
|
+
return new ArgumentMessage(key, value);
|
|
148
|
+
} else return null;
|
|
104
149
|
}
|
|
105
150
|
function getPropertyNameAsString(key) {
|
|
106
151
|
if (t.isIdentifier(key)) return key.name;
|
|
@@ -123,4 +168,4 @@ function getTranslatorComments(comments) {
|
|
|
123
168
|
}, []);
|
|
124
169
|
}
|
|
125
170
|
//#endregion
|
|
126
|
-
export { parseCallExpression, parseExpression, parseTaggedTemplateExpression, processExpression };
|
|
171
|
+
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.
|
|
3
|
+
"version": "0.6.0",
|
|
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.
|
|
50
|
+
"@saykit/config": "^0.6.0"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@babel/core": "^7.29.7",
|