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.
Files changed (2) hide show
  1. package/package.json +2 -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.0",
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
- "test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js"
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 content across subsequent children.
341
- let message = '';
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
- message += normalizeJsxTextForKey(text);
399
+ templateText += text;
400
+ rawKeyText += text;
354
401
  return { done: false };
355
402
  }
356
- message += normalizeJsxTextForKey(text.slice(0, endIdx));
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
- // Consume seed (which may contain ]]]).
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 keyRaw = message;
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: keyRaw,
419
+ key: rawKey,
420
+ rawKey,
374
421
  });
375
422
 
376
423
  if (translated == null) {
377
- if (state.localePoPath != null && state.warnOnMissingTranslations) emitMissing(keyRaw);
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(t.jsxText(keyRaw + res.remainder));
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
- out.push(...buildJsxChildrenFromTranslation(translated, values));
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 keyRaw = message;
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: keyRaw,
467
+ key: rawKey,
468
+ rawKey,
414
469
  });
415
470
 
416
471
  if (translated == null) {
417
- if (state.localePoPath != null && state.warnOnMissingTranslations) emitMissing(keyRaw);
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(keyRaw, values));
481
+ out.push(...buildJsxChildrenFromTranslation(templateText, values));
427
482
  } else {
428
- out.push(...buildJsxChildrenFromTranslation(translated, values));
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
- message += `%${index}`;
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
- message += `%${index}`;
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
- message += `%${index}`;
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) {