@saykit/transform-js 0.8.0 → 0.9.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.
- package/dist/index.mjs +2 -3
- package/dist/parser.d.mts +18 -1
- package/dist/parser.mjs +36 -38
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { generateSayCallExpression } from "./generator.mjs";
|
|
2
|
-
import { isEquivalentPlaceholder, parseExpression } from "./parser.mjs";
|
|
2
|
+
import { collectLeadingComments, 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";
|
|
@@ -41,7 +41,7 @@ function createJsTransformer() {
|
|
|
41
41
|
const program = createProgram(code, id);
|
|
42
42
|
const messages = [];
|
|
43
43
|
traverse(program, { Expression(path) {
|
|
44
|
-
path.node.leadingComments = path
|
|
44
|
+
path.node.leadingComments = collectLeadingComments(path);
|
|
45
45
|
const message = parseExpression(path.node);
|
|
46
46
|
if (message) {
|
|
47
47
|
assignSequenceIdentifiers(message, { current: 0 }, isEquivalentPlaceholder);
|
|
@@ -61,7 +61,6 @@ function createJsTransformer() {
|
|
|
61
61
|
transform(code, id) {
|
|
62
62
|
const program = createProgram(code, id);
|
|
63
63
|
traverse(program, { Expression(path) {
|
|
64
|
-
path.node.leadingComments = path.node.leadingComments ?? [];
|
|
65
64
|
const message = parseExpression(path.node);
|
|
66
65
|
if (message) {
|
|
67
66
|
assignSequenceIdentifiers(message, { current: 0 }, isEquivalentPlaceholder);
|
package/dist/parser.d.mts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { NodePath } from "@babel/traverse";
|
|
1
2
|
import { AUTO_INCREMENT_IDENTIFIER, ArgumentMessage, CompositeMessage } from "@saykit/config/features/messages";
|
|
2
3
|
import * as t from "@babel/types";
|
|
3
4
|
//#region src/parser.d.ts
|
|
@@ -28,5 +29,21 @@ declare function unwrapPlaceholder(expression: t.Expression): [string | typeof A
|
|
|
28
29
|
declare function isEquivalentPlaceholder(a: any, b: any): boolean;
|
|
29
30
|
declare function parseExpression(expression: t.Expression, fallback?: false): CompositeMessage | null;
|
|
30
31
|
declare function parseExpression(expression: t.Expression, fallback: true): CompositeMessage | ArgumentMessage;
|
|
32
|
+
/**
|
|
33
|
+
* The notes a translator is meant to read, taken from the comments written
|
|
34
|
+
* above a message — `// TRANSLATORS: keep this under 20 characters`. A comment
|
|
35
|
+
* that does not open with the marker is a note to whoever is reading the code,
|
|
36
|
+
* and stays there.
|
|
37
|
+
*/
|
|
38
|
+
declare function getTranslatorComments(comments: readonly t.Comment[] | null | undefined): string[];
|
|
39
|
+
/**
|
|
40
|
+
* The comments written in front of a message, in the order they were written.
|
|
41
|
+
*
|
|
42
|
+
* A comment on its own line is attached by Babel to whatever node happens to
|
|
43
|
+
* begin at it — `const a = say\`Hi\`` puts it on the declaration, not on the
|
|
44
|
+
* template — so a message is rarely the node holding its own notes. Walking out
|
|
45
|
+
* to the statement gathers them from wherever they landed.
|
|
46
|
+
*/
|
|
47
|
+
declare function collectLeadingComments(path: NodePath<t.Node>): t.Comment[];
|
|
31
48
|
//#endregion
|
|
32
|
-
export { isEquivalentPlaceholder, parseCallExpression, parseExpression, parseTaggedTemplateExpression, processExpression, unwrapPlaceholder };
|
|
49
|
+
export { collectLeadingComments, getTranslatorComments, isEquivalentPlaceholder, parseCallExpression, parseExpression, parseTaggedTemplateExpression, processExpression, unwrapPlaceholder };
|
package/dist/parser.mjs
CHANGED
|
@@ -10,12 +10,8 @@ function parseTaggedTemplateExpression(tagged) {
|
|
|
10
10
|
return new CompositeMessage({
|
|
11
11
|
id: descriptor ? findPropertyValueIfStringLiteralAsString(descriptor, "id") : void 0,
|
|
12
12
|
context: descriptor ? findPropertyValueIfStringLiteralAsString(descriptor, "context") : void 0
|
|
13
|
-
}, getTranslatorComments(tagged.leadingComments
|
|
13
|
+
}, getTranslatorComments(tagged.leadingComments), tagged.loc ? [`${tagged.loc.filename}:${tagged.loc.start.line}`] : [], children, accessor);
|
|
14
14
|
}
|
|
15
|
-
/**
|
|
16
|
-
* The text and the values of a template literal, in the order they were
|
|
17
|
-
* written — the message the template spells out.
|
|
18
|
-
*/
|
|
19
15
|
function buildTemplateChildren(template) {
|
|
20
16
|
return template.quasis.reduce((c, q, i) => {
|
|
21
17
|
c.push(new LiteralMessage(q.value.cooked ?? q.value.raw));
|
|
@@ -32,7 +28,7 @@ function parseCallExpression(call) {
|
|
|
32
28
|
const wrap = (children) => new CompositeMessage({
|
|
33
29
|
id: descriptorId,
|
|
34
30
|
context: descriptorContext
|
|
35
|
-
},
|
|
31
|
+
}, getTranslatorComments(call.leadingComments), call.loc ? [`${call.loc.filename}:${call.loc.start.line}`] : [], children, accessor);
|
|
36
32
|
if (typeof kind === "string" && [
|
|
37
33
|
"select",
|
|
38
34
|
"ordinal",
|
|
@@ -77,15 +73,6 @@ function parseCallExpression(call) {
|
|
|
77
73
|
}
|
|
78
74
|
return null;
|
|
79
75
|
}
|
|
80
|
-
/**
|
|
81
|
-
* The `offset` a plural subtracts from its selector before `#` is formatted, so
|
|
82
|
-
* "You and 2 others" can select on a total of three.
|
|
83
|
-
*
|
|
84
|
-
* Read only from an integer literal: the offset is baked into the extracted
|
|
85
|
-
* message, so it has to be known at build time, and a fractional or negative
|
|
86
|
-
* one is not something ICU accepts. Anything else stays an ordinary branch and
|
|
87
|
-
* is validated as the key it looks like.
|
|
88
|
-
*/
|
|
89
76
|
function findPluralOffset(object) {
|
|
90
77
|
for (const property of object.properties) {
|
|
91
78
|
if (!t.isObjectProperty(property) || property.computed) continue;
|
|
@@ -136,16 +123,6 @@ function getExpressionAsKey(node) {
|
|
|
136
123
|
if (t.isJSXIdentifier(node)) return node.name;
|
|
137
124
|
return AUTO_INCREMENT_IDENTIFIER;
|
|
138
125
|
}
|
|
139
|
-
/**
|
|
140
|
-
* Read the name off a value written as a single-key object and hand back the
|
|
141
|
-
* value alone, so the wrapper never reaches the output: `${{ total: sum() }}`
|
|
142
|
-
* is `{total}` rather than `{0}`.
|
|
143
|
-
*
|
|
144
|
-
* An inline one-key object is the only shape this claims, and it is a shape
|
|
145
|
-
* that could not mean anything else — interpolating an object stringifies it
|
|
146
|
-
* to `[object Object]`, and rendering one in JSX throws. Anything else is
|
|
147
|
-
* returned untouched and named the way it always was.
|
|
148
|
-
*/
|
|
149
126
|
function unwrapPlaceholder(expression) {
|
|
150
127
|
if (!t.isObjectExpression(expression) || expression.properties.length !== 1) return [getExpressionAsKey(expression), expression];
|
|
151
128
|
const [property] = expression.properties;
|
|
@@ -156,16 +133,6 @@ function unwrapPlaceholder(expression) {
|
|
|
156
133
|
if (!PLACEHOLDER_PATTERN.test(name)) throw new Error(`Invalid placeholder name '${name}', expected a letter or underscore followed by letters, digits, or underscores`);
|
|
157
134
|
return [name, property.value];
|
|
158
135
|
}
|
|
159
|
-
/**
|
|
160
|
-
* Whether two placeholders sharing a name are the same placeholder, and so
|
|
161
|
-
* compile to one prop a translator can move around the sentence freely.
|
|
162
|
-
*
|
|
163
|
-
* A value is compared whole: the same variable interpolated twice is one value,
|
|
164
|
-
* while `${name}` beside `${{ name: author.name }}` is two things claiming one
|
|
165
|
-
* name. An element is compared on its opening element alone — `say-tag` has
|
|
166
|
-
* been stripped by the time this runs, and its children come from the
|
|
167
|
-
* translation rather than the source.
|
|
168
|
-
*/
|
|
169
136
|
function isEquivalentPlaceholder(a, b) {
|
|
170
137
|
if (t.isJSXElement(a) || t.isJSXElement(b)) {
|
|
171
138
|
if (!t.isJSXElement(a) || !t.isJSXElement(b)) return false;
|
|
@@ -207,12 +174,43 @@ function findPropertyValueIfStringLiteralAsString(object, key) {
|
|
|
207
174
|
if (t.isIdentifier(property.key) && property.key.name === key && t.isStringLiteral(property.value)) return property.value.value;
|
|
208
175
|
}
|
|
209
176
|
}
|
|
177
|
+
const TRANSLATOR_COMMENT_PREFIX = "translators:";
|
|
210
178
|
function getTranslatorComments(comments) {
|
|
211
|
-
return comments.reduce((a, c) => {
|
|
179
|
+
return (comments ?? []).reduce((a, c) => {
|
|
212
180
|
const text = c.value.trim();
|
|
213
|
-
if (text.toLowerCase().startsWith(
|
|
181
|
+
if (text.toLowerCase().startsWith(TRANSLATOR_COMMENT_PREFIX)) a.push(text.slice(12).trim());
|
|
214
182
|
return a;
|
|
215
183
|
}, []);
|
|
216
184
|
}
|
|
185
|
+
function collectLeadingComments(path) {
|
|
186
|
+
const levels = [];
|
|
187
|
+
let current = path;
|
|
188
|
+
while (current) {
|
|
189
|
+
levels.push(current.node.leadingComments ?? []);
|
|
190
|
+
const parent = current.parentPath;
|
|
191
|
+
/* v8 ignore next */
|
|
192
|
+
if (!parent) break;
|
|
193
|
+
if (t.isJSXElement(parent.node) || t.isJSXFragment(parent.node)) {
|
|
194
|
+
levels.push(getPrecedingJSXComments(parent.node, current.node));
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
if (t.isStatement(current.node) && !t.isExportDeclaration(parent.node)) break;
|
|
198
|
+
current = parent;
|
|
199
|
+
}
|
|
200
|
+
return [...new Set(levels.reverse().flat())];
|
|
201
|
+
}
|
|
202
|
+
function getPrecedingJSXComments(parent, node) {
|
|
203
|
+
const index = parent.children.indexOf(node);
|
|
204
|
+
/* v8 ignore next */
|
|
205
|
+
if (index === -1) return [];
|
|
206
|
+
const comments = [];
|
|
207
|
+
for (let i = index - 1; i >= 0; i--) {
|
|
208
|
+
const sibling = parent.children[i];
|
|
209
|
+
if (t.isJSXText(sibling) && !sibling.value.trim()) continue;
|
|
210
|
+
if (!t.isJSXExpressionContainer(sibling) || !t.isJSXEmptyExpression(sibling.expression)) break;
|
|
211
|
+
comments.unshift(...sibling.expression.innerComments ?? []);
|
|
212
|
+
}
|
|
213
|
+
return comments;
|
|
214
|
+
}
|
|
217
215
|
//#endregion
|
|
218
|
-
export { isEquivalentPlaceholder, parseCallExpression, parseExpression, parseTaggedTemplateExpression, processExpression, unwrapPlaceholder };
|
|
216
|
+
export { collectLeadingComments, getTranslatorComments, 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.9.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.
|
|
50
|
+
"@saykit/config": "^0.9.1"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@babel/core": "^7.29.7",
|