@babelize/engine 0.1.2 → 0.2.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/config.d.ts +3 -3
- package/dist/detection/stack.d.ts +3 -1
- package/dist/detection/stack.js +13 -6
- package/dist/injection/StrategyFactory.d.ts +5 -1
- package/dist/injection/StrategyFactory.js +7 -2
- package/dist/injection/strategies/VanillaStrategy.d.ts +8 -0
- package/dist/injection/strategies/VanillaStrategy.js +10 -3
- package/dist/local/translate-core.d.ts +1 -1
- package/dist/local/translate-core.js +1 -1
- package/dist/modules/extractor/extract.js +67 -10
- package/dist/modules/finalization/dependencies.d.ts +10 -0
- package/dist/modules/finalization/dependencies.js +23 -0
- package/dist/modules/finalization/finalize.js +41 -39
- package/dist/modules/finalization/nextjs-finalize.js +2 -1
- package/dist/modules/finalization/react-native-finalize.d.ts +45 -0
- package/dist/modules/finalization/react-native-finalize.js +324 -0
- package/dist/modules/format-injection/index.js +38 -9
- package/dist/modules/format-injection/inject-formats.d.ts +3 -2
- package/dist/modules/format-injection/inject-formats.js +4 -3
- package/dist/modules/injection/inject.d.ts +14 -1
- package/dist/modules/injection/inject.js +307 -154
- package/dist/modules/sync-detector/sync-detector.js +11 -4
- package/dist/modules/translator/line-breaks.d.ts +8 -0
- package/dist/modules/translator/line-breaks.js +63 -0
- package/dist/modules/translator/translate.js +19 -7
- package/dist/modules/workspace/validate.d.ts +13 -1
- package/dist/modules/workspace/validate.js +95 -58
- package/dist/runtime/types.d.ts +22 -0
- package/dist/runtime/types.js +33 -0
- package/dist/utils/ast-helpers.d.ts +30 -2
- package/dist/utils/ast-helpers.js +355 -42
- package/dist/utils/extraction-options.js +1 -1
- package/dist/utils/file-utils.js +4 -1
- package/dist/utils/i18n-config-path.d.ts +18 -0
- package/dist/utils/i18n-config-path.js +64 -0
- package/dist/utils/i18next-icu.js +1 -1
- package/dist/utils/icu-message.d.ts +2 -2
- package/dist/utils/icu-message.js +43 -7
- package/dist/utils/jsx-message.d.ts +16 -1
- package/dist/utils/jsx-message.js +89 -9
- package/dist/utils/key-usage.js +1 -1
- package/dist/utils/metadata.d.ts +5 -1
- package/dist/utils/metadata.js +19 -1
- package/dist/utils/module-constants.d.ts +21 -3
- package/dist/utils/module-constants.js +123 -25
- package/dist/utils/react-native.d.ts +17 -0
- package/dist/utils/react-native.js +108 -0
- package/dist/utils/string-utils.d.ts +8 -0
- package/dist/utils/string-utils.js +29 -1
- package/dist/utils/ui-copy.d.ts +5 -4
- package/dist/utils/ui-copy.js +10 -5
- package/package.json +3 -6
|
@@ -37,12 +37,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.traverse = exports.t = exports.generate = void 0;
|
|
40
|
+
exports.sourceOffset = sourceOffset;
|
|
40
41
|
exports.parseCode = parseCode;
|
|
41
42
|
exports.generateCode = generateCode;
|
|
42
43
|
exports.placeStatementsOnOwnLines = placeStatementsOnOwnLines;
|
|
43
44
|
exports.isJSXText = isJSXText;
|
|
44
45
|
exports.isAlreadyTranslated = isAlreadyTranslated;
|
|
45
46
|
exports.isNonTranslatableContext = isNonTranslatableContext;
|
|
47
|
+
exports.isValuePassthrough = isValuePassthrough;
|
|
46
48
|
exports.getComponentName = getComponentName;
|
|
47
49
|
exports.isInsideComponent = isInsideComponent;
|
|
48
50
|
exports.findComponentBody = findComponentBody;
|
|
@@ -57,6 +59,7 @@ exports.getParentContext = getParentContext;
|
|
|
57
59
|
exports.extractExpressionContent = extractExpressionContent;
|
|
58
60
|
exports.extractConditionalContent = extractConditionalContent;
|
|
59
61
|
exports.hasUseClientDirective = hasUseClientDirective;
|
|
62
|
+
exports.bindsTopLevel = bindsTopLevel;
|
|
60
63
|
const generator_1 = __importDefault(require("@babel/generator"));
|
|
61
64
|
exports.generate = generator_1.default;
|
|
62
65
|
const parser = __importStar(require("@babel/parser"));
|
|
@@ -64,7 +67,80 @@ const traverse_1 = __importDefault(require("@babel/traverse"));
|
|
|
64
67
|
exports.traverse = traverse_1.default;
|
|
65
68
|
const t = __importStar(require("@babel/types"));
|
|
66
69
|
exports.t = t;
|
|
70
|
+
const recast = __importStar(require("recast"));
|
|
71
|
+
const jsx_message_1 = require("./jsx-message");
|
|
72
|
+
/** ASTs parsed with `preserveFormat`, which `generateCode` prints with recast. */
|
|
73
|
+
const formatPreserving = new WeakMap();
|
|
74
|
+
const LINE_BREAK = /\r\n|\r(?!\n)|\n|\u2028|\u2029/g;
|
|
75
|
+
/** The file's own layout, which recast keeps when it prints new code. */
|
|
76
|
+
function layoutOf(code) {
|
|
77
|
+
const tabIndented = (code.match(/^\t/gm)?.length ?? 0) > (code.match(/^ {2}/gm)?.length ?? 0);
|
|
78
|
+
const unit = code.match(/^( {2,})\S/m)?.[1].length ?? 2;
|
|
79
|
+
return {
|
|
80
|
+
lineTerminator: code.includes('\r\n') ? '\r\n' : '\n',
|
|
81
|
+
tabWidth: tabIndented ? 2 : Math.min(unit, 4),
|
|
82
|
+
useTabs: tabIndented,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** Columns a line's leading whitespace takes in recast's copy of the source (its `countSpaces`). */
|
|
86
|
+
function indentColumns(spaces, tabWidth) {
|
|
87
|
+
let count = 0;
|
|
88
|
+
for (const ch of spaces) {
|
|
89
|
+
if (ch === '\t') {
|
|
90
|
+
const next = Math.ceil(count / tabWidth) * tabWidth;
|
|
91
|
+
count = next === count ? count + tabWidth : next;
|
|
92
|
+
}
|
|
93
|
+
else if (!'\v\f\r'.includes(ch)) {
|
|
94
|
+
count++;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return count;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Where a node starts in the file as written. Recast hands the parser a
|
|
101
|
+
* normalized copy (`\n` line endings, leading tabs expanded), so the node's
|
|
102
|
+
* own offset can differ from the source offsets extraction recorded.
|
|
103
|
+
*/
|
|
104
|
+
function sourceOffset(ast, node) {
|
|
105
|
+
const preserved = formatPreserving.get(ast);
|
|
106
|
+
if (!preserved || !node.loc)
|
|
107
|
+
return node.start ?? undefined;
|
|
108
|
+
const { line, column } = node.loc.start;
|
|
109
|
+
const text = preserved.lines[line - 1];
|
|
110
|
+
if (text === undefined)
|
|
111
|
+
return undefined;
|
|
112
|
+
const lead = /^\s*/.exec(text)[0];
|
|
113
|
+
const indent = indentColumns(lead, preserved.layout.tabWidth ?? 4);
|
|
114
|
+
return preserved.lineStarts[line - 1] + (column >= indent ? lead.length + (column - indent) : column);
|
|
115
|
+
}
|
|
67
116
|
function parseCode(code, options = {}) {
|
|
117
|
+
if (options.preserveFormat) {
|
|
118
|
+
try {
|
|
119
|
+
// One line ending throughout (recast prints one), no exotic terminators.
|
|
120
|
+
const crlf = code.split('\r\n').length - 1;
|
|
121
|
+
if (crlf > 0 && crlf !== code.split('\n').length - 1)
|
|
122
|
+
throw new Error('mixed line endings');
|
|
123
|
+
if (/\r(?!\n)|[\u2028\u2029]/.test(code))
|
|
124
|
+
throw new Error('unusual line terminators');
|
|
125
|
+
const layout = layoutOf(code);
|
|
126
|
+
const ast = recast.parse(code, {
|
|
127
|
+
...layout,
|
|
128
|
+
parser: { parse: (source) => babelParse(source, { ...options, tokens: true }) },
|
|
129
|
+
});
|
|
130
|
+
const lines = code.split(LINE_BREAK);
|
|
131
|
+
const lineStarts = [0];
|
|
132
|
+
for (const m of code.matchAll(LINE_BREAK))
|
|
133
|
+
lineStarts.push(m.index + m[0].length);
|
|
134
|
+
formatPreserving.set(ast, { layout, source: code, lines, lineStarts });
|
|
135
|
+
return ast;
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// Syntax recast can't track: fall back to reprinting the whole file.
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return babelParse(code, options);
|
|
142
|
+
}
|
|
143
|
+
function babelParse(code, options) {
|
|
68
144
|
const { sourceType = 'module', isTypeScript = options.filename ? !/\.jsx?$/i.test(options.filename) : true, isJSX = true } = options;
|
|
69
145
|
const plugins = [];
|
|
70
146
|
if (isJSX)
|
|
@@ -76,9 +152,19 @@ function parseCode(code, options = {}) {
|
|
|
76
152
|
sourceType,
|
|
77
153
|
plugins,
|
|
78
154
|
errorRecovery: true,
|
|
155
|
+
tokens: options.tokens,
|
|
79
156
|
});
|
|
80
157
|
}
|
|
81
158
|
function generateCode(ast, originalCode) {
|
|
159
|
+
const preserved = formatPreserving.get(ast);
|
|
160
|
+
if (preserved) {
|
|
161
|
+
try {
|
|
162
|
+
return printPreservingFormat(ast, preserved.layout, preserved.source);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// Fall through to a full reprint.
|
|
166
|
+
}
|
|
167
|
+
}
|
|
82
168
|
const result = (0, generator_1.default)(ast, {
|
|
83
169
|
retainLines: true,
|
|
84
170
|
retainFunctionParens: true,
|
|
@@ -98,6 +184,169 @@ function generateCode(ast, originalCode) {
|
|
|
98
184
|
}
|
|
99
185
|
return code;
|
|
100
186
|
}
|
|
187
|
+
const SNIPPET_PARSER = {
|
|
188
|
+
parser: { parse: (source) => babelParse(source, { tokens: true }) },
|
|
189
|
+
};
|
|
190
|
+
/**
|
|
191
|
+
* One line of Babel output for a node the engine added, or null when it holds
|
|
192
|
+
* original code that spans lines (a component body wrapped in a block): that
|
|
193
|
+
* is left to recast, which keeps the original part as written.
|
|
194
|
+
*/
|
|
195
|
+
function oneLineCode(node) {
|
|
196
|
+
let multiLine = false;
|
|
197
|
+
t.traverseFast(node, (n) => {
|
|
198
|
+
if (n.loc && n.loc.start.line !== n.loc.end.line)
|
|
199
|
+
multiLine = true;
|
|
200
|
+
});
|
|
201
|
+
if (multiLine)
|
|
202
|
+
return null;
|
|
203
|
+
// Without locations, retainLines prints everything on one line.
|
|
204
|
+
let code;
|
|
205
|
+
try {
|
|
206
|
+
code = (0, generator_1.default)(t.cloneNode(node, true, true), { comments: false, retainLines: true, jsescOption: { minimal: true } }).code.trim();
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return null; // a node Babel can't print on its own: recast prints it
|
|
210
|
+
}
|
|
211
|
+
return code.includes('\n') ? null : code;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Recast prints an added expression with its own printer, which puts every
|
|
215
|
+
* object on several lines (`t("k", {\n total\n})`). Instead, print it with
|
|
216
|
+
* Babel and swap in that node parsed from its own text: recast reuses a
|
|
217
|
+
* parsed node's source as it is. Statements and blocks stay with recast,
|
|
218
|
+
* which spaces them within the surrounding code.
|
|
219
|
+
*/
|
|
220
|
+
function snippetNode(node, parent) {
|
|
221
|
+
if (t.isStatement(node) || t.isProgram(node) || t.isFunction(node))
|
|
222
|
+
return null;
|
|
223
|
+
// A name (it may carry a type annotation) or a pattern prints fine as it is.
|
|
224
|
+
if (t.isIdentifier(node) || t.isPattern(node))
|
|
225
|
+
return null;
|
|
226
|
+
// A declared object (a map of labels) reads better one entry per line.
|
|
227
|
+
if (t.isObjectExpression(node) && t.isVariableDeclarator(parent))
|
|
228
|
+
return null;
|
|
229
|
+
const swappable = t.isExpression(node) || t.isJSXExpressionContainer(node) || t.isJSXText(node) || t.isJSXAttribute(node) || t.isObjectProperty(node);
|
|
230
|
+
if (!swappable)
|
|
231
|
+
return null;
|
|
232
|
+
const code = oneLineCode(node);
|
|
233
|
+
if (code === null)
|
|
234
|
+
return null;
|
|
235
|
+
const parse = (source) => recast.parse(source, SNIPPET_PARSER).program.body;
|
|
236
|
+
const init = (source) => {
|
|
237
|
+
const decl = parse(source)[0];
|
|
238
|
+
return t.isVariableDeclaration(decl) ? decl.declarations[0].init : null;
|
|
239
|
+
};
|
|
240
|
+
if (t.isExpression(node))
|
|
241
|
+
return init(`const __babelize = ${code};`) ?? null;
|
|
242
|
+
if (t.isJSXExpressionContainer(node) || t.isJSXText(node)) {
|
|
243
|
+
const fragment = init(`const __babelize = <>${code}</>;`);
|
|
244
|
+
return t.isJSXFragment(fragment) && fragment.children.length === 1 ? fragment.children[0] : null;
|
|
245
|
+
}
|
|
246
|
+
if (t.isJSXAttribute(node)) {
|
|
247
|
+
const el = init(`const __babelize = <x ${code} />;`);
|
|
248
|
+
return t.isJSXElement(el) ? (el.openingElement.attributes[0] ?? null) : null;
|
|
249
|
+
}
|
|
250
|
+
if (t.isObjectProperty(node)) {
|
|
251
|
+
const obj = init(`const __babelize = { ${code} };`);
|
|
252
|
+
return t.isObjectExpression(obj) ? (obj.properties[0] ?? null) : null;
|
|
253
|
+
}
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
/** A node recast didn't parse from this file's source: the engine added (or cloned) it. */
|
|
257
|
+
function isAdded(node) {
|
|
258
|
+
return !node.original;
|
|
259
|
+
}
|
|
260
|
+
const escapeRegex = (x) => x.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
261
|
+
function printPreservingFormat(ast, layout, source) {
|
|
262
|
+
// Added statements recast may print across lines (`const {\n t\n} = …`), as one line each.
|
|
263
|
+
const statements = new Set();
|
|
264
|
+
(0, traverse_1.default)(ast, {
|
|
265
|
+
enter(path) {
|
|
266
|
+
if (!isAdded(path.node) || path.isProgram() || path.isFile())
|
|
267
|
+
return;
|
|
268
|
+
if (path.isStatement() && !path.isBlockStatement()) {
|
|
269
|
+
// Short ones (a hook, an import); a long one, like a map of labels, reads better across lines.
|
|
270
|
+
const declaresObject = path.isVariableDeclaration() && path.node.declarations.some((d) => t.isObjectExpression(d.init));
|
|
271
|
+
const code = declaresObject ? null : oneLineCode(path.node);
|
|
272
|
+
if (code && code.length <= 100)
|
|
273
|
+
statements.add(code);
|
|
274
|
+
}
|
|
275
|
+
// Comments the engine attached stay with recast's printer.
|
|
276
|
+
if (path.node.leadingComments?.length || path.node.trailingComments?.length)
|
|
277
|
+
return;
|
|
278
|
+
const replacement = snippetNode(path.node, path.parent);
|
|
279
|
+
if (!replacement)
|
|
280
|
+
return; // recast prints it; the added nodes inside get their turn
|
|
281
|
+
// Not replaceWith: it copies comment arrays onto the node, and recast then
|
|
282
|
+
// sees it as changed and reprints it (wrapping at 74 columns).
|
|
283
|
+
// Recast reuses the snippet's text through `.original`; without locations
|
|
284
|
+
// of their own, the swapped-in nodes are also safe for the Babel fallback.
|
|
285
|
+
t.traverseFast(replacement, (n) => {
|
|
286
|
+
n.loc = null;
|
|
287
|
+
});
|
|
288
|
+
if (Array.isArray(path.container))
|
|
289
|
+
path.container[path.key] = replacement;
|
|
290
|
+
else
|
|
291
|
+
path.parent[path.key] = replacement;
|
|
292
|
+
path.skip();
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
let code = recast.print(ast, { ...layout, quote: 'double' }).code;
|
|
296
|
+
// Recast reprints a directive whose program body changed with a second `;`.
|
|
297
|
+
const directive = /^([ \t]*(['"])use [\w ]+\2);;/gm;
|
|
298
|
+
if (!directive.test(source))
|
|
299
|
+
code = code.replace(directive, '$1;');
|
|
300
|
+
for (const statement of statements) {
|
|
301
|
+
const tokens = statement.match(/[A-Za-z_$][\w$]*|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\S/g) ?? [];
|
|
302
|
+
if (tokens.length === 0)
|
|
303
|
+
continue;
|
|
304
|
+
// Recast follows a statement it printed across lines with a blank line; that goes too.
|
|
305
|
+
const re = new RegExp(`${tokens.map(escapeRegex).join('\\s*')}(\\r?\\n[ \\t]*(?=\\r?\\n))?`, 'g');
|
|
306
|
+
code = code.replace(re, (match, blank) => {
|
|
307
|
+
const printed = blank === undefined ? match : match.slice(0, -blank.length);
|
|
308
|
+
// Only what recast printed for the added statement: the same code the user wrote stays theirs.
|
|
309
|
+
if (!printed.includes('\n') || source.includes(printed))
|
|
310
|
+
return match;
|
|
311
|
+
return statement;
|
|
312
|
+
});
|
|
313
|
+
// An added import is followed by a blank line when code, not another import, comes next.
|
|
314
|
+
if (statement.startsWith('import ')) {
|
|
315
|
+
const importLine = new RegExp(`^(${escapeRegex(statement)})(\\r?\\n)(?!\\r?\\n|import\\b)`, 'm');
|
|
316
|
+
code = code.replace(importLine, '$1$2$2');
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
// Recast indents the lines it adds with spaces even with useTabs; give them the file's tabs.
|
|
320
|
+
if (layout.useTabs)
|
|
321
|
+
code = tabsOnAddedLines(code, layout, source);
|
|
322
|
+
return code;
|
|
323
|
+
}
|
|
324
|
+
/** Space indentation → tabs on lines recast added, never inside a template literal (that's the string's content). */
|
|
325
|
+
function tabsOnAddedLines(code, layout, source) {
|
|
326
|
+
const eol = layout.lineTerminator ?? '\n';
|
|
327
|
+
const originalLines = new Set(source.split(eol));
|
|
328
|
+
const lines = code.split(eol);
|
|
329
|
+
if (lines.every((line) => originalLines.has(line) || !/^ /.test(line)))
|
|
330
|
+
return code;
|
|
331
|
+
const inTemplate = new Set();
|
|
332
|
+
try {
|
|
333
|
+
(0, traverse_1.default)(babelParse(code, {}), {
|
|
334
|
+
TemplateElement(p) {
|
|
335
|
+
const loc = p.node.loc;
|
|
336
|
+
if (loc)
|
|
337
|
+
for (let l = loc.start.line + 1; l <= loc.end.line; l++)
|
|
338
|
+
inTemplate.add(l);
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
return code; // can't tell where literals are: leave the indentation as printed
|
|
344
|
+
}
|
|
345
|
+
const width = layout.tabWidth ?? 2;
|
|
346
|
+
return lines
|
|
347
|
+
.map((line, i) => originalLines.has(line) || inTemplate.has(i + 1) ? line : line.replace(/^ +/, (spaces) => '\t'.repeat(Math.ceil(spaces.length / width))))
|
|
348
|
+
.join(eol);
|
|
349
|
+
}
|
|
101
350
|
/**
|
|
102
351
|
* With `retainLines`, statements the injector adds (which have no source
|
|
103
352
|
* location) are printed on the line of the preceding token, e.g.
|
|
@@ -178,6 +427,9 @@ function isNonTranslatableContext(path) {
|
|
|
178
427
|
let current = path;
|
|
179
428
|
let isInFunctionOrClass = false;
|
|
180
429
|
let hasFunctionOrClassBetween = false;
|
|
430
|
+
// The expression that would be a computed key: the literal itself, or the
|
|
431
|
+
// ternary / logical expression it is a branch of.
|
|
432
|
+
let keyCandidate = path.node;
|
|
181
433
|
while (current) {
|
|
182
434
|
if (current.isFunction() || current.isClass()) {
|
|
183
435
|
isInFunctionOrClass = true;
|
|
@@ -227,7 +479,9 @@ function isNonTranslatableContext(path) {
|
|
|
227
479
|
return true;
|
|
228
480
|
}
|
|
229
481
|
}
|
|
230
|
-
|
|
482
|
+
// An object key, also when picked by a ternary or fallback:
|
|
483
|
+
// `Colors[scheme === 'unspecified' ? 'light' : scheme]`, `map[key ?? 'default']`.
|
|
484
|
+
if (current.isMemberExpression() && current.node.computed && current.node.property === keyCandidate) {
|
|
231
485
|
return true;
|
|
232
486
|
}
|
|
233
487
|
if (current.isSwitchCase() && current.node.test === path.node) {
|
|
@@ -272,10 +526,25 @@ function isNonTranslatableContext(path) {
|
|
|
272
526
|
return true;
|
|
273
527
|
}
|
|
274
528
|
}
|
|
275
|
-
|
|
529
|
+
const parent = current.parentPath;
|
|
530
|
+
if (parent && keyCandidate === current.node && isValuePassthrough(parent.node, current.node)) {
|
|
531
|
+
keyCandidate = parent.node;
|
|
532
|
+
}
|
|
533
|
+
current = parent;
|
|
276
534
|
}
|
|
277
535
|
return false;
|
|
278
536
|
}
|
|
537
|
+
/**
|
|
538
|
+
* Expressions a value passes through unchanged on its way to its slot:
|
|
539
|
+
* `a ? 'x' : b`, `k ?? 'x'`, `('x' as K)`, `x!`.
|
|
540
|
+
*/
|
|
541
|
+
function isValuePassthrough(node, child) {
|
|
542
|
+
if (t.isConditionalExpression(node))
|
|
543
|
+
return node.consequent === child || node.alternate === child;
|
|
544
|
+
if (t.isLogicalExpression(node))
|
|
545
|
+
return node.left === child || node.right === child;
|
|
546
|
+
return t.isTSAsExpression(node) || t.isTSSatisfiesExpression(node) || t.isTSNonNullExpression(node) || t.isParenthesizedExpression(node);
|
|
547
|
+
}
|
|
279
548
|
function getComponentName(path) {
|
|
280
549
|
if (path.isVariableDeclarator()) {
|
|
281
550
|
if (t.isIdentifier(path.node.id))
|
|
@@ -390,11 +659,16 @@ function returnsJsx(fn) {
|
|
|
390
659
|
return found;
|
|
391
660
|
}
|
|
392
661
|
/**
|
|
393
|
-
* Whether `fn` is a component the hook can go in: not a
|
|
394
|
-
* another call (`items.map((x) => …)`, except
|
|
395
|
-
* PascalCase-named, the default export,
|
|
662
|
+
* Whether `fn` is a component (or custom hook) the hook can go in: not a
|
|
663
|
+
* callback passed to another call (`items.map((x) => …)`, except
|
|
664
|
+
* memo/forwardRef), and PascalCase-named, `use*`-named, the default export,
|
|
665
|
+
* or an unnamed function returning JSX. A lowercase function that returns JSX
|
|
666
|
+
* (`function renderHint() { return <Text>…</Text> }`) is a helper: a hook in
|
|
667
|
+
* it breaks the rules of hooks (and `react-hooks/rules-of-hooks` fails the
|
|
668
|
+
* user's lint), so its strings belong to the enclosing component, or to the
|
|
669
|
+
* module-level `t` when there is none.
|
|
396
670
|
*/
|
|
397
|
-
function isComponentFunction(fn) {
|
|
671
|
+
function isComponentFunction(fn, renderHelpers = false) {
|
|
398
672
|
const parent = fn.parentPath;
|
|
399
673
|
if (parent?.isCallExpression() && parent.node.arguments.includes(fn.node) && !isComponentWrapperCall(parent))
|
|
400
674
|
return false;
|
|
@@ -412,8 +686,8 @@ function isComponentFunction(fn) {
|
|
|
412
686
|
}
|
|
413
687
|
if (parent?.isExportDefaultDeclaration())
|
|
414
688
|
return true;
|
|
415
|
-
if (name
|
|
416
|
-
return
|
|
689
|
+
if (name)
|
|
690
|
+
return /^[A-Z]/.test(name) || /^use[A-Z0-9]/.test(name) || (renderHelpers && returnsJsx(fn));
|
|
417
691
|
return returnsJsx(fn);
|
|
418
692
|
}
|
|
419
693
|
/**
|
|
@@ -421,14 +695,16 @@ function isComponentFunction(fn) {
|
|
|
421
695
|
* goes. Skips callbacks (a hook inside `.map(…)` breaks the rules of hooks);
|
|
422
696
|
* null when there's no component with a block body.
|
|
423
697
|
*/
|
|
424
|
-
function findComponentBody(path
|
|
698
|
+
function findComponentBody(path,
|
|
699
|
+
/** Also lowercase functions returning JSX (render helpers), for when there is no module-level t. */
|
|
700
|
+
options = {}) {
|
|
425
701
|
let current = path;
|
|
426
702
|
while (current) {
|
|
427
703
|
if (current.isArrowFunctionExpression() ||
|
|
428
704
|
current.isFunctionExpression() ||
|
|
429
705
|
current.isFunctionDeclaration()) {
|
|
430
706
|
const body = current.get('body');
|
|
431
|
-
if (!Array.isArray(body) && body.isBlockStatement() && isComponentFunction(current))
|
|
707
|
+
if (!Array.isArray(body) && body.isBlockStatement() && isComponentFunction(current, options.renderHelpers))
|
|
432
708
|
return body;
|
|
433
709
|
}
|
|
434
710
|
current = current.parentPath;
|
|
@@ -527,25 +803,50 @@ function getParentContext(path) {
|
|
|
527
803
|
siblingGroupId: Math.abs(hash).toString(36),
|
|
528
804
|
};
|
|
529
805
|
}
|
|
530
|
-
|
|
806
|
+
/**
|
|
807
|
+
* A string-building expression as one message: `\`Hi ${user.name}\`` and
|
|
808
|
+
* `'Hi ' + name` → "Hi {name}". `variables` receives the placeholder names
|
|
809
|
+
* and, when given, `expressions` the expression behind each one (same order),
|
|
810
|
+
* so injection can rebuild the message as `t(key, { name: user.name })`.
|
|
811
|
+
*/
|
|
812
|
+
function extractExpressionContent(node, variables = [], expressions = []) {
|
|
531
813
|
if (t.isStringLiteral(node))
|
|
532
814
|
return node.value;
|
|
533
815
|
if (t.isTemplateLiteral(node)) {
|
|
534
816
|
let result = '';
|
|
535
817
|
const quasis = node.quasis;
|
|
536
|
-
const
|
|
818
|
+
const exprs = node.expressions;
|
|
819
|
+
const nameOf = (expr) => t.isIdentifier(expr) ? expr.name
|
|
820
|
+
: t.isMemberExpression(expr) && t.isIdentifier(expr.property) ? expr.property.name
|
|
821
|
+
: `var${variables.length}`;
|
|
537
822
|
for (let i = 0; i < quasis.length; i++) {
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
823
|
+
// The text as it runs (`\n` is a line break), not as it's escaped in the source.
|
|
824
|
+
result += quasis[i].value.cooked ?? quasis[i].value.raw;
|
|
825
|
+
if (i < exprs.length) {
|
|
826
|
+
const expr = exprs[i];
|
|
827
|
+
// `${n} item${n === 1 ? '' : 's'}` → `{n, plural, one {# item} other {# items}}`.
|
|
828
|
+
const suffix = (0, jsx_message_1.pluralSuffix)(expr);
|
|
829
|
+
const word = suffix ? /([A-Za-z]+)$/.exec(result) : null;
|
|
830
|
+
if (suffix && word) {
|
|
831
|
+
const at = variables.findIndex((_, j) => t.isNodesEquivalent(expressions[j], suffix.count));
|
|
832
|
+
// A new count gets a name of its own: `${user.count} item${count === 1 …}` has two counts.
|
|
833
|
+
let name = at >= 0 ? variables[at] : nameOf(suffix.count);
|
|
834
|
+
for (let n = 2; at < 0 && variables.includes(name); n++)
|
|
835
|
+
name = `${nameOf(suffix.count)}${n}`;
|
|
836
|
+
if (at < 0) {
|
|
837
|
+
variables.push(name);
|
|
838
|
+
expressions.push(suffix.count);
|
|
839
|
+
}
|
|
840
|
+
const before = result.slice(0, -word[1].length);
|
|
841
|
+
const counted = before.endsWith(`{${name}} `);
|
|
842
|
+
const hash = counted ? '# ' : '';
|
|
843
|
+
result = (counted ? before.slice(0, -`{${name}} `.length) : before)
|
|
844
|
+
+ `{${name}, plural, one {${hash}${word[1]}${suffix.one}} other {${hash}${word[1]}${suffix.other}}}`;
|
|
845
|
+
continue;
|
|
547
846
|
}
|
|
847
|
+
const varName = nameOf(expr);
|
|
548
848
|
variables.push(varName);
|
|
849
|
+
expressions.push(expr);
|
|
549
850
|
result += `{${varName}}`;
|
|
550
851
|
}
|
|
551
852
|
}
|
|
@@ -554,36 +855,35 @@ function extractExpressionContent(node, variables = []) {
|
|
|
554
855
|
if (t.isBinaryExpression(node) && node.operator === '+') {
|
|
555
856
|
const leftVars = [];
|
|
556
857
|
const rightVars = [];
|
|
557
|
-
const
|
|
558
|
-
const
|
|
858
|
+
const leftExprs = [];
|
|
859
|
+
const rightExprs = [];
|
|
860
|
+
const left = extractExpressionContent(node.left, leftVars, leftExprs);
|
|
861
|
+
const right = extractExpressionContent(node.right, rightVars, rightExprs);
|
|
862
|
+
const operandName = (operand) => t.isIdentifier(operand) ? operand.name
|
|
863
|
+
: t.isMemberExpression(operand) && t.isIdentifier(operand.property) ? operand.property.name
|
|
864
|
+
: null;
|
|
559
865
|
if (left !== null && right !== null) {
|
|
560
866
|
variables.push(...leftVars, ...rightVars);
|
|
867
|
+
expressions.push(...leftExprs, ...rightExprs);
|
|
561
868
|
return left + right;
|
|
562
869
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
870
|
+
// Identifiers before member expressions, as before: `'Hi ' + name`, `'Hi ' + user.name`.
|
|
871
|
+
const rightName = left !== null ? operandName(node.right) : null;
|
|
872
|
+
if (left !== null && rightName !== null) {
|
|
873
|
+
variables.push(...leftVars, rightName);
|
|
874
|
+
expressions.push(...leftExprs, node.right);
|
|
875
|
+
return left + `{${rightName}}`;
|
|
567
876
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
variables.push(
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
if (left !== null && t.isMemberExpression(node.right) && t.isIdentifier(node.right.property)) {
|
|
574
|
-
const varName = node.right.property.name;
|
|
575
|
-
variables.push(...leftVars, varName);
|
|
576
|
-
return left + `{${varName}}`;
|
|
577
|
-
}
|
|
578
|
-
if (right !== null && t.isMemberExpression(node.left) && t.isIdentifier(node.left.property)) {
|
|
579
|
-
const varName = node.left.property.name;
|
|
580
|
-
variables.push(varName, ...rightVars);
|
|
581
|
-
return `{${varName}}` + right;
|
|
877
|
+
const leftName = right !== null ? operandName(node.left) : null;
|
|
878
|
+
if (right !== null && leftName !== null) {
|
|
879
|
+
variables.push(leftName, ...rightVars);
|
|
880
|
+
expressions.push(node.left, ...rightExprs);
|
|
881
|
+
return `{${leftName}}` + right;
|
|
582
882
|
}
|
|
583
883
|
return null;
|
|
584
884
|
}
|
|
585
885
|
if (t.isJSXExpressionContainer(node)) {
|
|
586
|
-
return extractExpressionContent(node.expression, variables);
|
|
886
|
+
return extractExpressionContent(node.expression, variables, expressions);
|
|
587
887
|
}
|
|
588
888
|
return null;
|
|
589
889
|
}
|
|
@@ -606,3 +906,16 @@ function hasUseClientDirective(ast) {
|
|
|
606
906
|
}
|
|
607
907
|
return false;
|
|
608
908
|
}
|
|
909
|
+
/** A top-level import or declaration already binds `name` (e.g. `t` from an earlier run's `@/i18n` import). */
|
|
910
|
+
function bindsTopLevel(ast, name) {
|
|
911
|
+
for (const node of ast.program.body) {
|
|
912
|
+
if (t.isImportDeclaration(node) && node.specifiers.some((sp) => sp.local.name === name))
|
|
913
|
+
return true;
|
|
914
|
+
const decl = t.isExportNamedDeclaration(node) ? node.declaration : node;
|
|
915
|
+
if (t.isVariableDeclaration(decl) && decl.declarations.some((d) => t.isIdentifier(d.id, { name })))
|
|
916
|
+
return true;
|
|
917
|
+
if ((t.isFunctionDeclaration(decl) || t.isClassDeclaration(decl)) && decl.id?.name === name)
|
|
918
|
+
return true;
|
|
919
|
+
}
|
|
920
|
+
return false;
|
|
921
|
+
}
|
|
@@ -19,7 +19,7 @@ async function resolveIcuOptions(workspace, llm) {
|
|
|
19
19
|
return { sentences: false, plurals: true, icuRuntime: false, targetPlurals: false };
|
|
20
20
|
}
|
|
21
21
|
const deps = { ...workspace.packageJson?.dependencies, ...workspace.packageJson?.devDependencies };
|
|
22
|
-
const strategy = ['react', 'nextjs'].includes(workspace.framework) ? StrategyFactory_1.StrategyFactory.getStrategy(workspace.framework, deps).id : 'other';
|
|
22
|
+
const strategy = ['react', 'nextjs', 'react-native'].includes(workspace.framework) ? StrategyFactory_1.StrategyFactory.getStrategy(workspace.framework, deps).id : 'other';
|
|
23
23
|
let icuRuntime = false;
|
|
24
24
|
let sentences = true;
|
|
25
25
|
if (strategy === 'next-intl') {
|
package/dist/utils/file-utils.js
CHANGED
|
@@ -130,9 +130,12 @@ async function findFilesUncached(dir, extensions, excludePatterns = []) {
|
|
|
130
130
|
}
|
|
131
131
|
// Whole path segments only: a substring match excluded files like
|
|
132
132
|
// `components/Calibrate.tsx` ("lib") or `app/distance/page.tsx` ("dist").
|
|
133
|
+
// A leading `/` anchors to the walk root (`/ios` is the native
|
|
134
|
+
// project, not `src/screens/ios`).
|
|
133
135
|
if (pattern.includes('/')) {
|
|
134
136
|
const rel = relativePath.split(path.sep).join('/');
|
|
135
|
-
|
|
137
|
+
const anchored = pattern.startsWith('/') ? pattern.slice(1) : pattern;
|
|
138
|
+
return rel === anchored || rel.startsWith(`${anchored}/`);
|
|
136
139
|
}
|
|
137
140
|
if (entry.name !== pattern)
|
|
138
141
|
return false;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { WorkspaceInfo } from '../runtime/types';
|
|
2
|
+
type ConfigLocation = Pick<WorkspaceInfo, 'framework' | 'sourceDirs' | 'publicDir'>;
|
|
3
|
+
/**
|
|
4
|
+
* The directory (workspace-relative) holding Babelize's generated i18next
|
|
5
|
+
* config, `i18n.ts`/`i18n.js`. Web: the source directory (`src`). React
|
|
6
|
+
* Native: next to the bundled locales, `src/` or the project root — never
|
|
7
|
+
* Expo Router's `app/`, where every file is a route.
|
|
8
|
+
*/
|
|
9
|
+
export declare function i18nConfigDir(workspace: ConfigLocation): string;
|
|
10
|
+
/** The config's module path without extension, workspace-relative and '/'-separated (`src/i18n`, `i18n`). */
|
|
11
|
+
export declare function i18nConfigModule(workspace: ConfigLocation): string;
|
|
12
|
+
/**
|
|
13
|
+
* Import specifier for the generated config from `fromFile` (workspace-relative):
|
|
14
|
+
* `./i18n`, `../i18n`, `../../src/i18n`. Relative, because Metro and plain
|
|
15
|
+
* Vite/CRA projects have no `@/` alias.
|
|
16
|
+
*/
|
|
17
|
+
export declare function i18nImportPath(workspace: ConfigLocation, fromFile: string): string;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.i18nConfigDir = i18nConfigDir;
|
|
37
|
+
exports.i18nConfigModule = i18nConfigModule;
|
|
38
|
+
exports.i18nImportPath = i18nImportPath;
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
/**
|
|
41
|
+
* The directory (workspace-relative) holding Babelize's generated i18next
|
|
42
|
+
* config, `i18n.ts`/`i18n.js`. Web: the source directory (`src`). React
|
|
43
|
+
* Native: next to the bundled locales, `src/` or the project root — never
|
|
44
|
+
* Expo Router's `app/`, where every file is a route.
|
|
45
|
+
*/
|
|
46
|
+
function i18nConfigDir(workspace) {
|
|
47
|
+
if (workspace.framework === 'react-native')
|
|
48
|
+
return workspace.publicDir || '.';
|
|
49
|
+
return workspace.sourceDirs.find((d) => d.includes('src')) || workspace.sourceDirs[0] || '.';
|
|
50
|
+
}
|
|
51
|
+
/** The config's module path without extension, workspace-relative and '/'-separated (`src/i18n`, `i18n`). */
|
|
52
|
+
function i18nConfigModule(workspace) {
|
|
53
|
+
return path.posix.join(i18nConfigDir(workspace).split(path.sep).join('/'), 'i18n');
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Import specifier for the generated config from `fromFile` (workspace-relative):
|
|
57
|
+
* `./i18n`, `../i18n`, `../../src/i18n`. Relative, because Metro and plain
|
|
58
|
+
* Vite/CRA projects have no `@/` alias.
|
|
59
|
+
*/
|
|
60
|
+
function i18nImportPath(workspace, fromFile) {
|
|
61
|
+
const from = path.posix.dirname(fromFile.split(path.sep).join('/'));
|
|
62
|
+
const rel = path.posix.relative(from, i18nConfigModule(workspace));
|
|
63
|
+
return rel.startsWith('.') ? rel : `./${rel}`;
|
|
64
|
+
}
|
|
@@ -96,7 +96,7 @@ async function detectI18nextIcu(workspace) {
|
|
|
96
96
|
for (const dir of new Set(['.', ...workspace.sourceDirs])) {
|
|
97
97
|
let files = [];
|
|
98
98
|
try {
|
|
99
|
-
files = (await (0, file_utils_1.findFiles)(path.join(workspace.path, dir), types_1.SUPPORTED_EXTENSIONS, CONFIG_SCAN_EXCLUDES)) ?? [];
|
|
99
|
+
files = (await (0, file_utils_1.findFiles)(path.join(workspace.path, dir), types_1.SUPPORTED_EXTENSIONS, [...CONFIG_SCAN_EXCLUDES, ...(workspace.excludedPatterns ?? [])])) ?? [];
|
|
100
100
|
}
|
|
101
101
|
catch {
|
|
102
102
|
files = [];
|
|
@@ -38,8 +38,8 @@ export interface IcuIssue {
|
|
|
38
38
|
/**
|
|
39
39
|
* Compare a translated ICU message to its source. Errors mean the message is
|
|
40
40
|
* broken at runtime (unparseable, arguments renamed, select branches changed,
|
|
41
|
-
* no `other
|
|
42
|
-
* falls back to `other` for
|
|
41
|
+
* no `other`, a target plural category whole numbers use). A missing category
|
|
42
|
+
* only fractions use is a warning: ICU falls back to `other` for it.
|
|
43
43
|
*/
|
|
44
44
|
export declare function compareIcuMessages(source: string, translated: string, targetLocale: string, options?: {
|
|
45
45
|
/**
|