react-i18next 17.0.10 → 17.0.11

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/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## 17.0.11
2
+
3
+ - chore: `html-parse-stringify` updated to `^4.0.1`. The parser powering `<Trans>` is now actively maintained under the i18next org ([i18next/html-parse-stringify](https://github.com/i18next/html-parse-stringify)) after years without upstream releases. 4.x brings modern dual ESM/CJS packaging with an `exports` map, zero runtime dependencies, reworked TypeScript types and a long list of parser fixes (literal `<` in text, multiline/CRLF attribute values, comments containing `>`, doctype handling, quote-aware bracket handling).
4
+ - refactor(Trans): the internal `escapeLiteralLessThan` scanner (~80 lines) is replaced by the parser's new `allowedTags` option with identical semantics: only numbered tags, kept basic HTML tags and known component names are parsed as markup, any other tag-shaped sequence in the translation stays literal text. Rendered output is unchanged (all 493 tests pass, including the [#1880](https://github.com/i18next/react-i18next/issues/1880) and [#1893](https://github.com/i18next/react-i18next/issues/1893) escaping cases).
5
+
1
6
  ## 17.0.10
2
7
 
3
8
  - fix(warnings): the `useTranslation` and `Trans` "You will need to pass in an i18next instance" warnings now match the `useSSR` wording, mentioning the props/context alternatives and the most common unexplained cause at scale: duplicate react-i18next copies in monorepo setups. The `Trans` variant also referenced the internal `i18nextReactModule` name; it now points to the public `initReactI18next` API.
@@ -2241,127 +2241,318 @@ define(['exports', 'react'], (function (exports, React) { 'use strict';
2241
2241
  instance.loadNamespaces;
2242
2242
  instance.loadLanguages;
2243
2243
 
2244
- function getDefaultExportFromCjs (x) {
2245
- return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
2244
+ const voidElements = {
2245
+ area: true,
2246
+ base: true,
2247
+ br: true,
2248
+ col: true,
2249
+ embed: true,
2250
+ hr: true,
2251
+ img: true,
2252
+ input: true,
2253
+ link: true,
2254
+ meta: true,
2255
+ param: true,
2256
+ source: true,
2257
+ track: true,
2258
+ wbr: true,
2259
+ '!doctype': true,
2260
+ '!DOCTYPE': true
2261
+ };
2262
+ const attrRE = /\s([^'"/\s><]+?)[\s/>]|([^\s=]+)=\s?("[^"]*"|'[^']*')/g;
2263
+ function parseTag(tag) {
2264
+ const res = {
2265
+ type: 'tag',
2266
+ name: '',
2267
+ voidElement: false,
2268
+ attrs: {},
2269
+ children: []
2270
+ };
2271
+ const tagMatch = tag.match(/<\/?([^\s]+?)[/\s>]/);
2272
+ if (tagMatch) {
2273
+ res.name = tagMatch[1];
2274
+ if (voidElements[tagMatch[1]] || tag.charAt(tag.length - 2) === '/') {
2275
+ res.voidElement = true;
2276
+ }
2277
+ if (res.name.startsWith('!--')) {
2278
+ const endIndex = tag.indexOf('-->');
2279
+ return {
2280
+ type: 'comment',
2281
+ comment: endIndex !== -1 ? tag.slice(4, endIndex) : ''
2282
+ };
2283
+ }
2284
+ }
2285
+ const reg = new RegExp(attrRE);
2286
+ let result = null;
2287
+ for (;;) {
2288
+ result = reg.exec(tag);
2289
+ if (result === null) {
2290
+ break;
2291
+ }
2292
+ if (!result[0].trim()) {
2293
+ continue;
2294
+ }
2295
+ if (result[1]) {
2296
+ const attr = result[1].trim();
2297
+ let arr = [attr, null];
2298
+ const eq = attr.indexOf('=');
2299
+ if (eq > -1) {
2300
+ arr = [attr.slice(0, eq), attr.slice(eq + 1)];
2301
+ }
2302
+ res.attrs[arr[0]] = arr[1];
2303
+ reg.lastIndex--;
2304
+ } else if (result[2]) {
2305
+ res.attrs[result[2]] = result[3].trim().substring(1, result[3].length - 1);
2306
+ }
2307
+ }
2308
+ return res;
2246
2309
  }
2247
-
2248
- var voidElements;
2249
- var hasRequiredVoidElements;
2250
-
2251
- function requireVoidElements () {
2252
- if (hasRequiredVoidElements) return voidElements;
2253
- hasRequiredVoidElements = 1;
2254
- voidElements = {
2255
- "area": true,
2256
- "base": true,
2257
- "br": true,
2258
- "col": true,
2259
- "embed": true,
2260
- "hr": true,
2261
- "img": true,
2262
- "input": true,
2263
- "link": true,
2264
- "meta": true,
2265
- "param": true,
2266
- "source": true,
2267
- "track": true,
2268
- "wbr": true
2269
- };
2270
- return voidElements;
2310
+ const tagRE = /<!--[\s\S]*?-->|<[a-zA-Z0-9\-!/](?:"[^"]*"|'[^']*'|[^'">])*>/g;
2311
+ const tagNameRE = /<\/?([^\s]+?)[/\s>]/;
2312
+ const whitespaceRE = /^\s*$/;
2313
+ const rawTextRE = /^(script|style)$/i;
2314
+ const sentinel = '\u0000';
2315
+ const empty = Object.create(null);
2316
+ function restoreSentinels(nodes) {
2317
+ nodes.forEach(function (node) {
2318
+ if (node.type === 'text') {
2319
+ node.content = node.content.split(sentinel).join('<');
2320
+ return;
2321
+ }
2322
+ if (node.type === 'comment') {
2323
+ node.comment = node.comment.split(sentinel).join('<');
2324
+ return;
2325
+ }
2326
+ for (const key in node.attrs) {
2327
+ const value = node.attrs[key];
2328
+ if (typeof value === 'string' && value.indexOf(sentinel) > -1) {
2329
+ node.attrs[key] = value.split(sentinel).join('<');
2330
+ }
2331
+ }
2332
+ if (node.children.length) {
2333
+ restoreSentinels(node.children);
2334
+ }
2335
+ });
2271
2336
  }
2272
-
2273
- var voidElementsExports = requireVoidElements();
2274
- var e = /*@__PURE__*/getDefaultExportFromCjs(voidElementsExports);
2275
-
2276
- var t = /\s([^'"/\s><]+?)[\s/>]|([^\s=]+)=\s?(".*?"|'.*?')/g;
2277
- function n(n) {
2278
- var r = {
2279
- type: "tag",
2280
- name: "",
2281
- voidElement: false,
2282
- attrs: {},
2283
- children: []
2284
- },
2285
- i = n.match(/<\/?([^\s]+?)[/\s>]/);
2286
- if (i && (r.name = i[1], (e[i[1]] || "/" === n.charAt(n.length - 2)) && (r.voidElement = true), r.name.startsWith("!--"))) {
2287
- var s = n.indexOf("--\x3e");
2288
- return {
2289
- type: "comment",
2290
- comment: -1 !== s ? n.slice(4, s) : ""
2337
+ function parse(html, options) {
2338
+ const components = options && options.components || empty;
2339
+ const allowedTags = options && options.allowedTags;
2340
+ let restoreNeeded = false;
2341
+ if (allowedTags) {
2342
+ const isAllowed = typeof allowedTags === 'function' ? allowedTags : function (name) {
2343
+ return allowedTags.indexOf(name) > -1;
2291
2344
  };
2345
+ let out = '';
2346
+ let pos = 0;
2347
+ tagRE.lastIndex = 0;
2348
+ let am;
2349
+ while (am = tagRE.exec(html)) {
2350
+ const tag = am[0];
2351
+ out += html.slice(pos, am.index);
2352
+ const nameMatch = tag.match(tagNameRE);
2353
+ if (tag.startsWith('<!--') || nameMatch && isAllowed(nameMatch[1])) {
2354
+ out += tag;
2355
+ pos = am.index + tag.length;
2356
+ } else {
2357
+ restoreNeeded = true;
2358
+ out += sentinel;
2359
+ pos = am.index + 1;
2360
+ tagRE.lastIndex = pos;
2361
+ }
2362
+ }
2363
+ html = out + html.slice(pos);
2292
2364
  }
2293
- for (var a = new RegExp(t), c = null; null !== (c = a.exec(n));) if (c[0].trim()) if (c[1]) {
2294
- var o = c[1].trim(),
2295
- l = [o, ""];
2296
- o.indexOf("=") > -1 && (l = o.split("=")), r.attrs[l[0]] = l[1], a.lastIndex--;
2297
- } else c[2] && (r.attrs[c[2]] = c[3].trim().substring(1, c[3].length - 1));
2298
- return r;
2299
- }
2300
- var r = /<[a-zA-Z0-9\-\!\/](?:"[^"]*"|'[^']*'|[^'">])*>/g,
2301
- i = /^\s*$/,
2302
- s = Object.create(null);
2303
- function a(e, t) {
2304
- switch (t.type) {
2305
- case "text":
2306
- return e + t.content;
2307
- case "tag":
2308
- return e += "<" + t.name + (t.attrs ? function (e) {
2309
- var t = [];
2310
- for (var n in e) t.push(n + '="' + e[n] + '"');
2311
- return t.length ? " " + t.join(" ") : "";
2312
- }(t.attrs) : "") + (t.voidElement ? "/>" : ">"), t.voidElement ? e : e + t.children.reduce(a, "") + "</" + t.name + ">";
2313
- case "comment":
2314
- return e + "\x3c!--" + t.comment + "--\x3e";
2365
+ const result = [];
2366
+ const arr = [];
2367
+ let current;
2368
+ let level = -1;
2369
+ let inComponent = false;
2370
+ let rawUntil = 0;
2371
+ let htmlLower;
2372
+ if (html.indexOf('<') !== 0) {
2373
+ const end = html.indexOf('<');
2374
+ result.push({
2375
+ type: 'text',
2376
+ content: end === -1 ? html : html.substring(0, end)
2377
+ });
2315
2378
  }
2316
- }
2317
- var c = {
2318
- parse: function (e, t) {
2319
- t || (t = {}), t.components || (t.components = s);
2320
- var a,
2321
- c = [],
2322
- o = [],
2323
- l = -1,
2324
- m = false;
2325
- if (0 !== e.indexOf("<")) {
2326
- var u = e.indexOf("<");
2327
- c.push({
2328
- type: "text",
2329
- content: -1 === u ? e : e.substring(0, u)
2330
- });
2379
+ const matches = [];
2380
+ let m;
2381
+ while (m = tagRE.exec(html)) {
2382
+ matches.push(m);
2383
+ }
2384
+ matches.forEach(function (match, i) {
2385
+ const tag = match[0];
2386
+ if (!tag) return;
2387
+ if (tag.startsWith('<!--')) return;
2388
+ let lts = 0;
2389
+ let gts = 0;
2390
+ let secondLt = -1;
2391
+ let quote = null;
2392
+ for (let j = 0; j < tag.length; j++) {
2393
+ const c = tag.charAt(j);
2394
+ if (quote) {
2395
+ if (c === quote) quote = null;
2396
+ } else if (c === '"' || c === "'") {
2397
+ quote = c;
2398
+ } else if (c === '<') {
2399
+ lts++;
2400
+ if (lts === 2) secondLt = j;
2401
+ } else if (c === '>') {
2402
+ gts++;
2403
+ }
2404
+ }
2405
+ const validSplit = secondLt > -1 && /[a-zA-Z0-9\-!/]/.test(tag.charAt(secondLt + 1));
2406
+ if (lts > gts && validSplit) {
2407
+ const firstPart = tag.substring(0, secondLt);
2408
+ const secondPart = tag.substring(firstPart.length);
2409
+ matches[i][0] = secondPart;
2410
+ matches[i].index += firstPart.length;
2331
2411
  }
2332
- return e.replace(r, function (r, s) {
2333
- if (m) {
2334
- if (r !== "</" + a.name + ">") return;
2335
- m = false;
2336
- }
2337
- var u,
2338
- f = "/" !== r.charAt(1),
2339
- h = r.startsWith("\x3c!--"),
2340
- p = s + r.length,
2341
- d = e.charAt(p);
2342
- if (h) {
2343
- var v = n(r);
2344
- return l < 0 ? (c.push(v), c) : ((u = o[l]).children.push(v), c);
2345
- }
2346
- if (f && (l++, "tag" === (a = n(r)).type && t.components[a.name] && (a.type = "component", m = true), a.voidElement || m || !d || "<" === d || a.children.push({
2347
- type: "text",
2348
- content: e.slice(p, e.indexOf("<", p))
2349
- }), 0 === l && c.push(a), (u = o[l - 1]) && u.children.push(a), o[l] = a), (!f || a.voidElement) && (l > -1 && (a.voidElement || a.name === r.slice(2, -1)) && (l--, a = -1 === l ? c : o[l]), !m && "<" !== d && d)) {
2350
- u = -1 === l ? c : o[l].children;
2351
- var x = e.indexOf("<", p),
2352
- g = e.slice(p, -1 === x ? void 0 : x);
2353
- i.test(g) && (g = " "), (x > -1 && l + u.length >= 0 || " " !== g) && u.push({
2354
- type: "text",
2355
- content: g
2412
+ });
2413
+ matches.forEach(function (match, i) {
2414
+ const tag = match[0];
2415
+ if (!tag) return;
2416
+ const index = match.index;
2417
+ if (index < rawUntil) return;
2418
+ if (inComponent) {
2419
+ if (tag !== '</' + current.name + '>') {
2420
+ return;
2421
+ } else {
2422
+ inComponent = false;
2423
+ }
2424
+ }
2425
+ const isOpen = tag.charAt(1) !== '/';
2426
+ const isComment = tag.startsWith('<!--');
2427
+ const start = index + tag.length;
2428
+ const nextChar = html.charAt(start);
2429
+ const nextMatch = matches[i + 1];
2430
+ let isText;
2431
+ if (nextChar === '<' && nextMatch) {
2432
+ const nextTag = html.substring(start, nextMatch.index);
2433
+ isText = nextTag.split('<').length > nextTag.split('>').length;
2434
+ }
2435
+ let parent;
2436
+ if (isComment) {
2437
+ const comment = parseTag(tag);
2438
+ if (level < 0) {
2439
+ result.push(comment);
2440
+ return result;
2441
+ }
2442
+ parent = arr[level];
2443
+ parent.children.push(comment);
2444
+ const text = html.slice(start, nextMatch ? nextMatch.index : undefined);
2445
+ if (text.length > 0) {
2446
+ parent.children.push({
2447
+ type: 'text',
2448
+ content: text
2356
2449
  });
2357
2450
  }
2358
- }), c;
2359
- },
2360
- stringify: function (e) {
2361
- return e.reduce(function (e, t) {
2362
- return e + a("", t);
2363
- }, "");
2451
+ return result;
2452
+ }
2453
+ if (isOpen) {
2454
+ level++;
2455
+ current = parseTag(tag);
2456
+ if (current.type === 'tag' && components[current.name]) {
2457
+ current.type = 'component';
2458
+ inComponent = true;
2459
+ }
2460
+ let isRawText = false;
2461
+ if (!inComponent && !current.voidElement && rawTextRE.test(current.name)) {
2462
+ isRawText = true;
2463
+ htmlLower || (htmlLower = html.toLowerCase());
2464
+ const closeIndex = htmlLower.indexOf('</' + current.name.toLowerCase() + '>', start);
2465
+ const contentEnd = closeIndex === -1 ? html.length : closeIndex;
2466
+ const content = html.slice(start, contentEnd);
2467
+ if (content) {
2468
+ current.children.push({
2469
+ type: 'text',
2470
+ content
2471
+ });
2472
+ }
2473
+ rawUntil = contentEnd;
2474
+ }
2475
+ if (!current.voidElement && !inComponent && !isRawText && nextChar && nextChar !== '<') {
2476
+ current.children.push({
2477
+ type: 'text',
2478
+ content: html.slice(start, nextMatch ? nextMatch.index : undefined)
2479
+ });
2480
+ }
2481
+ if (level === 0) {
2482
+ result.push(current);
2483
+ }
2484
+ parent = arr[level - 1];
2485
+ if (parent) {
2486
+ parent.children.push(current);
2487
+ }
2488
+ arr[level] = current;
2489
+ }
2490
+ if (!isOpen || current.voidElement) {
2491
+ if (level > -1 && (current.voidElement || current.name === tag.slice(2, -1))) {
2492
+ level--;
2493
+ current = level === -1 ? result : arr[level];
2494
+ }
2495
+ if (!inComponent && (nextChar !== '<' || isText) && nextChar) {
2496
+ parent = level === -1 ? result : arr[level].children;
2497
+ const end = nextMatch ? nextMatch.index : -1;
2498
+ let content = html.slice(start, end === -1 ? undefined : end);
2499
+ if (whitespaceRE.test(content)) {
2500
+ content = ' ';
2501
+ }
2502
+ if (end > -1 && level + parent.length >= 0 || content !== ' ') {
2503
+ parent.push({
2504
+ type: 'text',
2505
+ content
2506
+ });
2507
+ }
2508
+ }
2509
+ }
2510
+ });
2511
+ if (restoreNeeded) {
2512
+ restoreSentinels(result);
2513
+ }
2514
+ return result;
2515
+ }
2516
+ function attrString(attrs) {
2517
+ const buff = [];
2518
+ for (const key in attrs) {
2519
+ if (attrs[key] === null) {
2520
+ buff.push(key);
2521
+ } else {
2522
+ buff.push(key + '="' + String(attrs[key]).replace(/"/g, '&quot;') + '"');
2523
+ }
2524
+ }
2525
+ if (!buff.length) {
2526
+ return '';
2364
2527
  }
2528
+ return ' ' + buff.join(' ');
2529
+ }
2530
+ function stringifyNode(buff, doc) {
2531
+ switch (doc.type) {
2532
+ case 'text':
2533
+ return buff + doc.content;
2534
+ case 'tag':
2535
+ {
2536
+ const tagEnd = doc.voidElement && doc.name.toLowerCase() !== '!doctype' ? '/>' : '>';
2537
+ buff += '<' + doc.name + (doc.attrs ? attrString(doc.attrs) : '') + tagEnd;
2538
+ if (doc.voidElement) {
2539
+ return buff;
2540
+ }
2541
+ return buff + doc.children.reduce(stringifyNode, '') + '</' + doc.name + '>';
2542
+ }
2543
+ case 'comment':
2544
+ buff += '<!--' + doc.comment + '-->';
2545
+ return buff;
2546
+ }
2547
+ }
2548
+ function stringify(doc) {
2549
+ return doc.reduce(function (token, rootEl) {
2550
+ return token + stringifyNode('', rootEl);
2551
+ }, '');
2552
+ }
2553
+ var index = {
2554
+ parse,
2555
+ stringify
2365
2556
  };
2366
2557
 
2367
2558
  const warn = (i18n, code, msg, rest) => {
@@ -2579,46 +2770,6 @@ define(['exports', 'react'], (function (exports, React) { 'use strict';
2579
2770
  });
2580
2771
  return stringNode;
2581
2772
  };
2582
- const escapeLiteralLessThan = (str, keepArray = [], knownComponentsMap = {}) => {
2583
- if (!str) return str;
2584
- const knownNames = Object.keys(knownComponentsMap);
2585
- const allValidNames = [...keepArray, ...knownNames];
2586
- let result = '';
2587
- let i = 0;
2588
- while (i < str.length) {
2589
- if (str[i] === '<') {
2590
- let isValidTag = false;
2591
- const closingMatch = str.slice(i).match(/^<\/(\d+|[a-zA-Z][a-zA-Z0-9_-]*)>/);
2592
- if (closingMatch) {
2593
- const tagName = closingMatch[1];
2594
- if (/^\d+$/.test(tagName) || allValidNames.includes(tagName)) {
2595
- isValidTag = true;
2596
- result += closingMatch[0];
2597
- i += closingMatch[0].length;
2598
- }
2599
- }
2600
- if (!isValidTag) {
2601
- const openingMatch = str.slice(i).match(/^<(\d+|[a-zA-Z][a-zA-Z0-9_-]*)(\s+[\w-]+(?:=(?:"[^"]*"|'[^']*'|[^\s>]+))?)*\s*(\/)?>/);
2602
- if (openingMatch) {
2603
- const tagName = openingMatch[1];
2604
- if (/^\d+$/.test(tagName) || allValidNames.includes(tagName)) {
2605
- isValidTag = true;
2606
- result += openingMatch[0];
2607
- i += openingMatch[0].length;
2608
- }
2609
- }
2610
- }
2611
- if (!isValidTag) {
2612
- result += '&lt;';
2613
- i += 1;
2614
- }
2615
- } else {
2616
- result += str[i];
2617
- i += 1;
2618
- }
2619
- }
2620
- return result;
2621
- };
2622
2773
  const renderNodes = (children, knownComponentsMap, targetString, i18n, i18nOptions, combinedTOpts, shouldUnescape) => {
2623
2774
  if (targetString === '') return [];
2624
2775
  const keepArray = i18nOptions.transKeepBasicHtmlNodesFor || [];
@@ -2633,8 +2784,11 @@ define(['exports', 'react'], (function (exports, React) { 'use strict';
2633
2784
  });
2634
2785
  };
2635
2786
  getData(children);
2636
- const escapedString = escapeLiteralLessThan(targetString, keepArray, data);
2637
- const ast = c.parse(`<0>${escapedString}</0>`);
2787
+ const knownNames = Object.keys(data);
2788
+ const allowedTags = name => /^\d+$/.test(name) || keepArray.indexOf(name) > -1 || knownNames.indexOf(name) > -1;
2789
+ const ast = index.parse(`<0>${targetString}</0>`, {
2790
+ allowedTags
2791
+ });
2638
2792
  const opts = {
2639
2793
  ...data,
2640
2794
  ...combinedTOpts