webpack-easyi18n 0.6.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.
- package/package.json +2 -2
- package/src/transform.js +104 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webpack-easyi18n",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Go from gettext catalog (.po files) to embeded localization in your Webpack bundles",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=4.3.0 <5.0.0 || >=5.10"
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
],
|
|
11
11
|
"main": "src/index.js",
|
|
12
12
|
"scripts": {
|
|
13
|
-
|
|
13
|
+
"test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"i18next-conv": "^16.0.0"
|
package/src/transform.js
CHANGED
|
@@ -314,6 +314,47 @@ function transformJsxNuggets(children, state, fileNameForWarnings) {
|
|
|
314
314
|
const out = [];
|
|
315
315
|
let i = 0;
|
|
316
316
|
|
|
317
|
+
// React/JSX nugget support notes:
|
|
318
|
+
// - In JSX, text is NOT a single string at runtime/source level. It's a list of children:
|
|
319
|
+
// JSXText nodes, JSXExpressionContainer nodes (e.g. {foo}, {" "}), and JSXElement nodes.
|
|
320
|
+
// - Nuggets can span multiple children, e.g.:
|
|
321
|
+
// [[[Hello {name} <b>world</b>]]]
|
|
322
|
+
// - For "raw-key" matching we must reproduce the exact key that shows up in translationLookup
|
|
323
|
+
// (e.g. the keys produced by webpack-easyi18n-temp). That includes the original source text
|
|
324
|
+
// for embedded nodes like {changeStatusBtn} / {" "} and <button ...>...</button>, including
|
|
325
|
+
// formatting and newlines.
|
|
326
|
+
// - For emitting output we support two styles of translation values:
|
|
327
|
+
// (1) Placeholder-based: translated string contains %0, %1 ...; we reinsert captured nodes.
|
|
328
|
+
// (2) Raw JSX-based: translated string contains literal JSX/expressions; we parse it as JSX
|
|
329
|
+
// and inject those AST children directly.
|
|
330
|
+
|
|
331
|
+
// Returns the original source text for a node (used to build raw-key strings).
|
|
332
|
+
const getSourceSliceForNode = (node) => {
|
|
333
|
+
if (!node || state.source == null) return null;
|
|
334
|
+
if (typeof node.start !== 'number' || typeof node.end !== 'number') return null;
|
|
335
|
+
return state.source.slice(node.start, node.end);
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const parseTranslatedJsxChildren = (translated) => {
|
|
339
|
+
// Used for raw JSX translation values.
|
|
340
|
+
// Example translation value:
|
|
341
|
+
// "Просто{\" \"}<button onClick={onClick}>нажмите</button>"
|
|
342
|
+
// We parse it as JSX and splice the resulting children into the output.
|
|
343
|
+
// Parse translation as JSX so translations can include markup like <button ...>...</button>
|
|
344
|
+
// and expression containers like {" "}.
|
|
345
|
+
try {
|
|
346
|
+
// Wrap in a fragment so we can accept multiple top-level children.
|
|
347
|
+
const expr = parser.parseExpression(`<>${translated}</>`, {
|
|
348
|
+
plugins: ['jsx', 'typescript', 'classProperties'],
|
|
349
|
+
});
|
|
350
|
+
if (t.isJSXFragment(expr)) return expr.children;
|
|
351
|
+
if (t.isJSXElement(expr)) return [expr];
|
|
352
|
+
} catch {
|
|
353
|
+
// Caller decides how to fall back.
|
|
354
|
+
}
|
|
355
|
+
return null;
|
|
356
|
+
};
|
|
357
|
+
|
|
317
358
|
const emitMissing = (key) => {
|
|
318
359
|
if (!state.warnOnMissingTranslations) return;
|
|
319
360
|
if (typeof state.emitWarning === 'function') {
|
|
@@ -337,8 +378,13 @@ function transformJsxNuggets(children, state, fileNameForWarnings) {
|
|
|
337
378
|
|
|
338
379
|
if (before) out.push(t.jsxText(before));
|
|
339
380
|
|
|
340
|
-
// Begin capturing nugget
|
|
341
|
-
|
|
381
|
+
// Begin capturing one nugget.
|
|
382
|
+
// - rawKeyText: exact (source-derived) key contents inside [[[...]]]
|
|
383
|
+
// - templateText: placeholder skeleton used to re-emit original content when we need to
|
|
384
|
+
// remove brackets but have no translation. Each embedded node becomes %N and we capture the
|
|
385
|
+
// corresponding node/expression in `values[N]`.
|
|
386
|
+
let rawKeyText = '';
|
|
387
|
+
let templateText = '';
|
|
342
388
|
const values = [];
|
|
343
389
|
let done = false;
|
|
344
390
|
|
|
@@ -350,49 +396,59 @@ function transformJsxNuggets(children, state, fileNameForWarnings) {
|
|
|
350
396
|
if (!text) return;
|
|
351
397
|
const endIdx = text.indexOf(NUGGET_END);
|
|
352
398
|
if (endIdx === -1) {
|
|
353
|
-
|
|
399
|
+
templateText += text;
|
|
400
|
+
rawKeyText += text;
|
|
354
401
|
return { done: false };
|
|
355
402
|
}
|
|
356
|
-
|
|
403
|
+
templateText += text.slice(0, endIdx);
|
|
404
|
+
rawKeyText += text.slice(0, endIdx);
|
|
357
405
|
const remainder = text.slice(endIdx + NUGGET_END.length);
|
|
358
406
|
return { done: true, remainder };
|
|
359
407
|
};
|
|
360
408
|
|
|
361
|
-
//
|
|
409
|
+
// Fast path: the closing marker (]]]) is in the same JSXText that opened the nugget.
|
|
362
410
|
{
|
|
363
411
|
const res = consumeText(seed);
|
|
364
412
|
if (res.done) {
|
|
365
413
|
// Entire nugget is within the first JSXText.
|
|
366
|
-
const
|
|
367
|
-
const key = keyRaw;
|
|
414
|
+
const rawKey = normalizeKey(rawKeyText);
|
|
368
415
|
const translated = getTranslation({
|
|
369
416
|
localePoPath: state.localePoPath,
|
|
370
417
|
alwaysRemoveBrackets: state.alwaysRemoveBrackets,
|
|
371
418
|
translationLookup: state.translationLookup,
|
|
372
|
-
key,
|
|
373
|
-
rawKey
|
|
419
|
+
key: rawKey,
|
|
420
|
+
rawKey,
|
|
374
421
|
});
|
|
375
422
|
|
|
376
423
|
if (translated == null) {
|
|
377
|
-
if (state.localePoPath != null && state.warnOnMissingTranslations) emitMissing(
|
|
424
|
+
if (state.localePoPath != null && state.warnOnMissingTranslations) emitMissing(rawKey);
|
|
378
425
|
if (state.localePoPath == null && !state.alwaysRemoveBrackets) {
|
|
379
426
|
// Leave original unmodified
|
|
380
427
|
out.push(child);
|
|
381
428
|
i++;
|
|
382
429
|
continue;
|
|
383
430
|
}
|
|
384
|
-
out.push(
|
|
431
|
+
out.push(...buildJsxChildrenFromTranslation(templateText, values));
|
|
432
|
+
if (res.remainder) out.push(t.jsxText(res.remainder));
|
|
385
433
|
i++;
|
|
386
434
|
continue;
|
|
387
435
|
}
|
|
388
436
|
|
|
389
|
-
|
|
437
|
+
if (/%\d+/.test(translated)) {
|
|
438
|
+
out.push(...buildJsxChildrenFromTranslation(translated, values));
|
|
439
|
+
} else {
|
|
440
|
+
const parsedChildren = parseTranslatedJsxChildren(translated);
|
|
441
|
+
if (parsedChildren != null) out.push(...parsedChildren);
|
|
442
|
+
else out.push(t.jsxText(translated));
|
|
443
|
+
}
|
|
390
444
|
if (res.remainder) out.push(t.jsxText(res.remainder));
|
|
391
445
|
i++;
|
|
392
446
|
continue;
|
|
393
447
|
}
|
|
394
448
|
}
|
|
395
449
|
|
|
450
|
+
// Slow path: the nugget spans multiple JSX children; keep consuming until we find ]]]
|
|
451
|
+
// in a later JSXText node.
|
|
396
452
|
localIndex = i + 1;
|
|
397
453
|
|
|
398
454
|
while (localIndex < children.length) {
|
|
@@ -403,18 +459,17 @@ function transformJsxNuggets(children, state, fileNameForWarnings) {
|
|
|
403
459
|
if (res.done) {
|
|
404
460
|
done = true;
|
|
405
461
|
// Finish: translate and emit remainder
|
|
406
|
-
const
|
|
407
|
-
const key = keyRaw;
|
|
462
|
+
const rawKey = normalizeKey(rawKeyText);
|
|
408
463
|
const translated = getTranslation({
|
|
409
464
|
localePoPath: state.localePoPath,
|
|
410
465
|
alwaysRemoveBrackets: state.alwaysRemoveBrackets,
|
|
411
466
|
translationLookup: state.translationLookup,
|
|
412
|
-
key,
|
|
413
|
-
rawKey
|
|
467
|
+
key: rawKey,
|
|
468
|
+
rawKey,
|
|
414
469
|
});
|
|
415
470
|
|
|
416
471
|
if (translated == null) {
|
|
417
|
-
if (state.localePoPath != null && state.warnOnMissingTranslations) emitMissing(
|
|
472
|
+
if (state.localePoPath != null && state.warnOnMissingTranslations) emitMissing(rawKey);
|
|
418
473
|
if (state.localePoPath == null && !state.alwaysRemoveBrackets) {
|
|
419
474
|
// Leave original sequence unmodified
|
|
420
475
|
out.push(child);
|
|
@@ -423,9 +478,15 @@ function transformJsxNuggets(children, state, fileNameForWarnings) {
|
|
|
423
478
|
continue;
|
|
424
479
|
}
|
|
425
480
|
|
|
426
|
-
out.push(...buildJsxChildrenFromTranslation(
|
|
481
|
+
out.push(...buildJsxChildrenFromTranslation(templateText, values));
|
|
427
482
|
} else {
|
|
428
|
-
|
|
483
|
+
if (/%\d+/.test(translated)) {
|
|
484
|
+
out.push(...buildJsxChildrenFromTranslation(translated, values));
|
|
485
|
+
} else {
|
|
486
|
+
const parsedChildren = parseTranslatedJsxChildren(translated);
|
|
487
|
+
if (parsedChildren != null) out.push(...parsedChildren);
|
|
488
|
+
else out.push(t.jsxText(translated));
|
|
489
|
+
}
|
|
429
490
|
}
|
|
430
491
|
|
|
431
492
|
if (res.remainder) out.push(t.jsxText(res.remainder));
|
|
@@ -443,25 +504,42 @@ function transformJsxNuggets(children, state, fileNameForWarnings) {
|
|
|
443
504
|
localIndex++;
|
|
444
505
|
continue;
|
|
445
506
|
}
|
|
507
|
+
// The raw key wants the exact source representation (e.g. {" "}, {foo}).
|
|
508
|
+
// The template wants a placeholder to preserve the original child ordering.
|
|
509
|
+
{
|
|
510
|
+
const slice = getSourceSliceForNode(current);
|
|
511
|
+
if (slice != null) rawKeyText += slice;
|
|
512
|
+
}
|
|
446
513
|
const index = values.length;
|
|
447
514
|
values.push(current.expression);
|
|
448
|
-
|
|
515
|
+
templateText += `%${index}`;
|
|
449
516
|
localIndex++;
|
|
450
517
|
continue;
|
|
451
518
|
}
|
|
452
519
|
|
|
453
520
|
if (t.isJSXElement(current) || t.isJSXFragment(current)) {
|
|
521
|
+
// Same strategy for nested elements/fragments (e.g. <button>...</button>):
|
|
522
|
+
// - rawKeyText captures the exact source slice
|
|
523
|
+
// - templateText captures a %N placeholder
|
|
524
|
+
{
|
|
525
|
+
const slice = getSourceSliceForNode(current);
|
|
526
|
+
if (slice != null) rawKeyText += slice;
|
|
527
|
+
}
|
|
454
528
|
const index = values.length;
|
|
455
529
|
values.push(current);
|
|
456
|
-
|
|
530
|
+
templateText += `%${index}`;
|
|
457
531
|
localIndex++;
|
|
458
532
|
continue;
|
|
459
533
|
}
|
|
460
534
|
|
|
461
535
|
// Other child types: keep them as placeholders to avoid losing content.
|
|
536
|
+
{
|
|
537
|
+
const slice = getSourceSliceForNode(current);
|
|
538
|
+
if (slice != null) rawKeyText += slice;
|
|
539
|
+
}
|
|
462
540
|
const index = values.length;
|
|
463
541
|
values.push(current);
|
|
464
|
-
|
|
542
|
+
templateText += `%${index}`;
|
|
465
543
|
localIndex++;
|
|
466
544
|
}
|
|
467
545
|
|
|
@@ -504,6 +582,7 @@ function buildJsxChildrenFromTranslation(translated, values) {
|
|
|
504
582
|
|
|
505
583
|
function transformSource(source, options = {}) {
|
|
506
584
|
const state = {
|
|
585
|
+
source,
|
|
507
586
|
localeKey: options.localeKey || '',
|
|
508
587
|
localePoPath: options.localePoPath ?? null,
|
|
509
588
|
alwaysRemoveBrackets: Boolean(options.alwaysRemoveBrackets),
|
|
@@ -529,9 +608,12 @@ function transformSource(source, options = {}) {
|
|
|
529
608
|
|
|
530
609
|
traverse(ast, {
|
|
531
610
|
JSXElement(path) {
|
|
611
|
+
// React support: rewrite nuggets at the JSX AST level so we can preserve embedded
|
|
612
|
+
// expressions/elements as real AST nodes (not string concatenations).
|
|
532
613
|
path.node.children = transformJsxNuggets(path.node.children, state, options.fileNameForWarnings);
|
|
533
614
|
},
|
|
534
615
|
JSXFragment(path) {
|
|
616
|
+
// Same as JSXElement, but for fragments: <>...</>
|
|
535
617
|
path.node.children = transformJsxNuggets(path.node.children, state, options.fileNameForWarnings);
|
|
536
618
|
},
|
|
537
619
|
StringLiteral(path) {
|