diff --git a/package.json b/package.json index d32e408..30f0c5d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "i18next-scanner", - "version": "3.3.0", + "version": "4.0.0", "description": "Scan your code, extract translation keys/values, and merge them into i18n resource files.", "homepage": "https://github.com/i18next/i18next-scanner", "author": "Cheton Wu ", @@ -57,13 +57,14 @@ "clone-deep": "^4.0.0", "commander": "^9.0.0", "deepmerge": "^4.0.0", - "ensure-array": "^1.0.0", + "ensure-type": "^1.5.0", "eol": "^0.9.1", "esprima-next": "^5.7.0", "gulp-sort": "^2.0.0", "i18next": "*", "lodash": "^4.0.0", "parse5": "^6.0.0", + "react-is": ">=16", "sortobject": "^4.0.0", "through2": "^4.0.0", "vinyl": "^2.2.0", diff --git a/src/nodes-to-string.js b/src/nodes-to-string.js index 49778ab..ecb6719 100644 --- a/src/nodes-to-string.js +++ b/src/nodes-to-string.js @@ -1,3 +1,4 @@ +import { ensureArray, ensureBoolean } from 'ensure-type'; import _get from 'lodash/get'; const isJSXText = (node) => { @@ -32,7 +33,10 @@ const isObjectExpression = (node) => { return node.type === 'ObjectExpression'; }; -const nodesToString = (nodes, code) => { +const nodesToString = (nodes, options) => { + const supportBasicHtmlNodes = ensureBoolean(options?.supportBasicHtmlNodes); + const keepBasicHtmlNodesFor = ensureArray(options?.keepBasicHtmlNodesFor); + let memo = ''; let nodeIndex = 0; nodes.forEach((node, i) => { @@ -55,14 +59,23 @@ const nodesToString = (nodes, code) => { } if (isStringLiteral(expression)) { memo += expression.value; } else if (isObjectExpression(expression) && (_get(expression, 'properties[0].type') === 'Property')) { - memo += `<${nodeIndex}>{{${expression.properties[0].key.name}}}`; + memo += `{{${expression.properties[0].key.name}}}`; } else { console.error(`Unsupported JSX expression. Only static values or {{interpolation}} blocks are supported. Got ${expression.type}:`); - console.error(code.slice(node.start, node.end)); + console.error(ensureString(options?.code).slice(node.start, node.end)); console.error(node.expression); } } else if (node.children) { - memo += `<${nodeIndex}>${nodesToString(node.children, code)}`; + const shouldKeepChild = supportBasicHtmlNodes && keepBasicHtmlNodesFor.indexOf(node.openingElement?.name?.name) > -1; + const selfClosing = node.openingElement?.selfClosing; + const openingElement = shouldKeepChild ? node.openingElement?.name?.name : nodeIndex; + const closingElement = shouldKeepChild ? node.closingElement?.name?.name : nodeIndex; + + if (selfClosing) { + memo += `<${openingElement}/>${nodesToString(node.children, options)}`; + } else { + memo += `<${openingElement}>${nodesToString(node.children, options)}`; + } } ++nodeIndex; diff --git a/src/parser.js b/src/parser.js index 53ce926..adff27c 100644 --- a/src/parser.js +++ b/src/parser.js @@ -7,7 +7,7 @@ import acornStage3 from 'acorn-stage3'; import chalk from 'chalk'; import cloneDeep from 'clone-deep'; import deepMerge from 'deepmerge'; -import ensureArray from 'ensure-array'; +import { ensureArray } from 'ensure-type'; import { parse } from 'esprima-next'; import _ from 'lodash'; import parse5 from 'parse5'; @@ -47,7 +47,9 @@ const defaults = { ecmaVersion: 2020, // defaults to 2020 sourceType: 'module', // defaults to 'module' // Check out https://github.com/acornjs/acorn/tree/master/acorn#interface for additional options - } + }, + supportBasicHtmlNodes: true, // Enables keeping the name of simple nodes (e.g.
) in translations instead of indexed keys + keepBasicHtmlNodesFor: ['br', 'strong', 'i', 'p'], // Which nodes are allowed to be kept in translations during defaultValue generation of . }, lngs: ['en'], // array of supported languages @@ -171,6 +173,12 @@ const normalizeOptions = (options) => { if (_.isUndefined(_.get(options, 'trans.acorn'))) { _.set(options, 'trans.acorn', defaults.trans.acorn); } + if (_.isUndefined(_.get(options, 'trans.supportBasicHtmlNodes'))) { + _.set(options, 'trans.supportBasicHtmlNodes', defaults.trans.supportBasicHtmlNodes); + } + if (_.isUndefined(_.get(options, 'trans.keepBasicHtmlNodesFor'))) { + _.set(options, 'trans.keepBasicHtmlNodesFor', defaults.trans.keepBasicHtmlNodesFor); + } } // Resource @@ -538,6 +546,8 @@ class Parser { defaultsKey = this.options.trans.defaultsKey, // string fallbackKey, // boolean|function acorn: acornOptions = this.options.trans.acorn, // object + supportBasicHtmlNodes = this.options.trans.supportBasicHtmlNodes, // boolean + keepBasicHtmlNodesFor = this.options.trans.keepBasicHtmlNodesFor, // array } = { ...opts }; const parseJSXElement = (node, code) => { @@ -634,7 +644,11 @@ class Parser { const tOptions = attr.tOptions; const options = { ...tOptions, - defaultValue: defaultsString || nodesToString(node.children, code), + defaultValue: defaultsString || nodesToString(node.children, { + code, + supportBasicHtmlNodes, + keepBasicHtmlNodesFor, + }), fallbackKey: fallbackKey || this.options.trans.fallbackKey }; diff --git a/test/jsx-parser.test.js b/test/jsx-parser.test.js index f7d98a4..37e0c08 100644 --- a/test/jsx-parser.test.js +++ b/test/jsx-parser.test.js @@ -1,6 +1,6 @@ import { Parser } from 'acorn'; import jsx from 'acorn-jsx'; -import ensureArray from 'ensure-array'; +import { ensureArray } from 'ensure-type'; import _get from 'lodash/get'; import nodesToString from '../src/nodes-to-string'; @@ -13,7 +13,13 @@ const jsxToString = (code) => { return ''; } - return nodesToString(nodes, code); + const options = { + code, + supportBasicHtmlNodes: true, + keepBasicHtmlNodesFor: ['br', 'strong', 'i', 'p'], + }; + + return nodesToString(nodes, options); } catch (e) { console.error(e); return ''; @@ -22,11 +28,29 @@ const jsxToString = (code) => { test('JSX to i18next', () => { expect(jsxToString('Basic text')).toBe('Basic text'); - expect(jsxToString('Hello, {{name}}')).toBe('Hello, <1>{{name}}'); + expect(jsxToString('Hello, {{name}}')).toBe('Hello, {{name}}'); expect(jsxToString('I agree to the terms.')).toBe('I agree to the <1>terms.'); expect(jsxToString('One & two')).toBe('One & two'); }); test('HTML entities', () => { - expect(jsxToString('Don't do this Dave')).toBe('Don\'t do this <1>Dave'); + expect(jsxToString('Don't do this Dave')).toBe('Don\'t do this Dave'); +}); + +test('HTML nodes', () => { + expect(jsxToString('Hello, {{name}}')).toBe('Hello, <1>{{name}}'); + expect(jsxToString(` + <> +
title
+
content
+ + `)).toBe('<0><0>title<1>content'); + expect(jsxToString(` + <> + line 1 +
+ line 2 +
+ + `)).toBe('<0>line 1
line 2
'); }); diff --git a/test/parser.test.js b/test/parser.test.js index b559b9f..95b0719 100644 --- a/test/parser.test.js +++ b/test/parser.test.js @@ -160,8 +160,8 @@ test('Parse Trans components', () => { expect(parser.get()).toEqual({ en: { dev: { - 'Hello <1>World, you have <3>{{count}} unread message.': 'Hello <1>World, you have <3>{{count}} unread message.', - 'Hello <1>World, you have <3>{{count}} unread message._plural': 'Hello <1>World, you have <3>{{count}} unread message.' + 'Hello World, you have {{count}} unread message.': 'Hello World, you have {{count}} unread message.', + 'Hello World, you have {{count}} unread message._plural': 'Hello World, you have {{count}} unread message.' }, translation: { // quote style @@ -169,8 +169,8 @@ test('Parse Trans components', () => { 'jsx-quotes-single': 'Use single quote for the i18nKey attribute', // plural - 'plural': 'You have <1>{{count}} apples', - 'plural_plural': 'You have <1>{{count}} apples', + 'plural': 'You have {{count}} apples', + 'plural_plural': 'You have {{count}} apples', // context 'context': 'A boyfriend', @@ -178,22 +178,23 @@ test('Parse Trans components', () => { // i18nKey 'multiline-text-string': 'multiline text string', - 'string-literal': 'This is a <1>test', - 'object-expression': 'This is a <1><0>{{test}}', - 'arithmetic-expression': '2 + 2 = <1>{{result}}', + 'string-literal': 'This is a test', + 'object-expression': 'This is a {{test}}', + 'arithmetic-expression': '2 + 2 = {{result}}', 'components': 'Go to <1>Administration > Tools to download administrative tools.', - 'lorem-ipsum': '<0>Lorem Ipsum is simply dummy text of the printing and typesetting industry.Lorem Ipsum is simply dummy text of the printing and typesetting industry.<2>Lorem Ipsum has been the industry\'s standard dummy text ever since the 1500s', - 'lorem-ipsum-nested': 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.<1>Lorem Ipsum is simply dummy text of the printing and typesetting industry.<1>Lorem Ipsum is simply dummy text of the printing and typesetting industry.<2>Lorem Ipsum has been the industry\'s standard dummy text ever since the 1500s', + + "lorem-ipsum": "

Lorem Ipsum is simply dummy text of the printing and typesetting industry.

Lorem Ipsum is simply dummy text of the printing and typesetting industry.

Lorem Ipsum has been the industry's standard dummy text ever since the 1500s

", + "lorem-ipsum-nested": "Lorem Ipsum is simply dummy text of the printing and typesetting industry.

Lorem Ipsum is simply dummy text of the printing and typesetting industry.

Lorem Ipsum is simply dummy text of the printing and typesetting industry.

Lorem Ipsum has been the industry's standard dummy text ever since the 1500s

", // fallback key 'Hello, World!': 'Hello, World!', 'multiline text string': 'multiline text string', - 'This is a <1>test': 'This is a <1>test', - 'This is a <1><0>{{test}}': 'This is a <1><0>{{test}}', - '2 + 2 = <1>{{result}}': '2 + 2 = <1>{{result}}', + 'This is a test': 'This is a test', + 'This is a {{test}}': 'This is a {{test}}', + '2 + 2 = {{result}}': '2 + 2 = {{result}}', 'Go to <1>Administration > Tools to download administrative tools.': 'Go to <1>Administration > Tools to download administrative tools.', - '<0>Lorem Ipsum is simply dummy text of the printing and typesetting industry.Lorem Ipsum is simply dummy text of the printing and typesetting industry.<2>Lorem Ipsum has been the industry\'s standard dummy text ever since the 1500s': '<0>Lorem Ipsum is simply dummy text of the printing and typesetting industry.Lorem Ipsum is simply dummy text of the printing and typesetting industry.<2>Lorem Ipsum has been the industry\'s standard dummy text ever since the 1500s', - 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.<1>Lorem Ipsum is simply dummy text of the printing and typesetting industry.<1>Lorem Ipsum is simply dummy text of the printing and typesetting industry.<2>Lorem Ipsum has been the industry\'s standard dummy text ever since the 1500s': 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.<1>Lorem Ipsum is simply dummy text of the printing and typesetting industry.<1>Lorem Ipsum is simply dummy text of the printing and typesetting industry.<2>Lorem Ipsum has been the industry\'s standard dummy text ever since the 1500s', + "

Lorem Ipsum is simply dummy text of the printing and typesetting industry.

Lorem Ipsum is simply dummy text of the printing and typesetting industry.

Lorem Ipsum has been the industry's standard dummy text ever since the 1500s

": "

Lorem Ipsum is simply dummy text of the printing and typesetting industry.

Lorem Ipsum is simply dummy text of the printing and typesetting industry.

Lorem Ipsum has been the industry's standard dummy text ever since the 1500s

", + "Lorem Ipsum is simply dummy text of the printing and typesetting industry.

Lorem Ipsum is simply dummy text of the printing and typesetting industry.

Lorem Ipsum is simply dummy text of the printing and typesetting industry.

Lorem Ipsum has been the industry's standard dummy text ever since the 1500s

": "Lorem Ipsum is simply dummy text of the printing and typesetting industry.

Lorem Ipsum is simply dummy text of the printing and typesetting industry.

Lorem Ipsum is simply dummy text of the printing and typesetting industry.

Lorem Ipsum has been the industry's standard dummy text ever since the 1500s

", // defaults 'The component might be self-closing': 'The component might be self-closing', @@ -233,8 +234,8 @@ test('Parse Trans components with fallback key', () => { expect(parser.get()).toEqual({ en: { dev: { - '2290678f8f33c49494499fe5e32b4ebd124d9292': 'Hello <1>World, you have <3>{{count}} unread message.', - '2290678f8f33c49494499fe5e32b4ebd124d9292_plural': 'Hello <1>World, you have <3>{{count}} unread message.' + "2f9bdac18af14a38ed4c147f8076c70b740c2ca6": "Hello World, you have {{count}} unread message.", + "2f9bdac18af14a38ed4c147f8076c70b740c2ca6_plural": "Hello World, you have {{count}} unread message.", }, translation: { // quote style @@ -242,8 +243,8 @@ test('Parse Trans components with fallback key', () => { 'jsx-quotes-single': 'Use single quote for the i18nKey attribute', // plural - 'plural': 'You have <1>{{count}} apples', - 'plural_plural': 'You have <1>{{count}} apples', + 'plural': 'You have {{count}} apples', + 'plural_plural': 'You have {{count}} apples', // context 'context': 'A boyfriend', @@ -251,22 +252,22 @@ test('Parse Trans components with fallback key', () => { // i18nKey 'multiline-text-string': 'multiline text string', - 'string-literal': 'This is a <1>test', - 'object-expression': 'This is a <1><0>{{test}}', - 'arithmetic-expression': '2 + 2 = <1>{{result}}', + "string-literal": "This is a test", + "object-expression": "This is a {{test}}", + 'arithmetic-expression': '2 + 2 = {{result}}', 'components': 'Go to <1>Administration > Tools to download administrative tools.', - 'lorem-ipsum': '<0>Lorem Ipsum is simply dummy text of the printing and typesetting industry.Lorem Ipsum is simply dummy text of the printing and typesetting industry.<2>Lorem Ipsum has been the industry\'s standard dummy text ever since the 1500s', - 'lorem-ipsum-nested': 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.<1>Lorem Ipsum is simply dummy text of the printing and typesetting industry.<1>Lorem Ipsum is simply dummy text of the printing and typesetting industry.<2>Lorem Ipsum has been the industry\'s standard dummy text ever since the 1500s', + "lorem-ipsum": "

Lorem Ipsum is simply dummy text of the printing and typesetting industry.

Lorem Ipsum is simply dummy text of the printing and typesetting industry.

Lorem Ipsum has been the industry's standard dummy text ever since the 1500s

", + "lorem-ipsum-nested": "Lorem Ipsum is simply dummy text of the printing and typesetting industry.

Lorem Ipsum is simply dummy text of the printing and typesetting industry.

Lorem Ipsum is simply dummy text of the printing and typesetting industry.

Lorem Ipsum has been the industry's standard dummy text ever since the 1500s

", // fallback key '0a0a9f2a6772942557ab5355d76af442f8f65e01': 'Hello, World!', '32876cbad378f3153c900c297ed2efa06243e0e2': 'multiline text string', - 'e4ca61dff6bc759d214e32c4e37c8ae594ca163d': 'This is a <1>test', - '0ce90193dd25c93cdc12f25a36d31004a74c63de': 'This is a <1><0>{{test}}', - '493781e20cd3cfd5b3137963519571c3d97ab383': '2 + 2 = <1>{{result}}', + "e2eff612e7754fb3954034c9be7e600a0456b84b": "This is a test", + "453d2d7753427d2693a0c49758dbcfa30cd71834": "This is a {{test}}", + 'd9ad3431d982619e3b7bd34ed248205312e95bff': '2 + 2 = {{result}}', '083eac6b4f73ec317824caaaeea57fba3b83c1d9': 'Go to <1>Administration > Tools to download administrative tools.', - '938c04be9e14562b7532a19458fe92b65c6ef941': '<0>Lorem Ipsum is simply dummy text of the printing and typesetting industry.Lorem Ipsum is simply dummy text of the printing and typesetting industry.<2>Lorem Ipsum has been the industry\'s standard dummy text ever since the 1500s', - '9c3ca5d5d8089e96135c8c7c9f42ba34a635fb47': 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.<1>Lorem Ipsum is simply dummy text of the printing and typesetting industry.<1>Lorem Ipsum is simply dummy text of the printing and typesetting industry.<2>Lorem Ipsum has been the industry\'s standard dummy text ever since the 1500s', + "11a5b6b7b39fc588da3a1a82d77051bc0e80ad71": "

Lorem Ipsum is simply dummy text of the printing and typesetting industry.

Lorem Ipsum is simply dummy text of the printing and typesetting industry.

Lorem Ipsum has been the industry's standard dummy text ever since the 1500s

", + "99dbaae55f815e76bdd1b375068c5cc734a7c5b4": "Lorem Ipsum is simply dummy text of the printing and typesetting industry.

Lorem Ipsum is simply dummy text of the printing and typesetting industry.

Lorem Ipsum is simply dummy text of the printing and typesetting industry.

Lorem Ipsum has been the industry's standard dummy text ever since the 1500s

", // defaults '7551746c2d33a1d0a24658c22821c8700fa58a0d': 'Hello <1>{{planet}}!', diff --git a/test/transform-stream.test.js b/test/transform-stream.test.js index cdaf9bb..1c68727 100644 --- a/test/transform-stream.test.js +++ b/test/transform-stream.test.js @@ -185,8 +185,9 @@ test('[Trans Component] fallbackKey', done => { 'jsx-quotes-single': 'Use single quote for the i18nKey attribute', // plural - 'plural': 'You have <1>{{count}} apples', - 'plural_plural': 'You have <1>{{count}} apples', + "plural": "You have {{count}} apples", + "plural_plural": "You have {{count}} apples", + // context 'context': 'A boyfriend', @@ -194,22 +195,23 @@ test('[Trans Component] fallbackKey', done => { // i18nKey 'multiline-text-string': 'multiline text string', - 'string-literal': 'This is a <1>test', - 'object-expression': 'This is a <1><0>{{test}}', - 'arithmetic-expression': '2 + 2 = <1>{{result}}', + 'string-literal': 'This is a test', + "object-expression": "This is a {{test}}", + "arithmetic-expression": "2 + 2 = {{result}}", 'components': 'Go to <1>Administration > Tools to download administrative tools.', - 'lorem-ipsum': '<0>Lorem Ipsum is simply dummy text of the printing and typesetting industry.Lorem Ipsum is simply dummy text of the printing and typesetting industry.<2>Lorem Ipsum has been the industry\'s standard dummy text ever since the 1500s', - 'lorem-ipsum-nested': 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.<1>Lorem Ipsum is simply dummy text of the printing and typesetting industry.<1>Lorem Ipsum is simply dummy text of the printing and typesetting industry.<2>Lorem Ipsum has been the industry\'s standard dummy text ever since the 1500s', + "lorem-ipsum": "

Lorem Ipsum is simply dummy text of the printing and typesetting industry.

Lorem Ipsum is simply dummy text of the printing and typesetting industry.

Lorem Ipsum has been the industry's standard dummy text ever since the 1500s

", + "lorem-ipsum-nested": "Lorem Ipsum is simply dummy text of the printing and typesetting industry.

Lorem Ipsum is simply dummy text of the printing and typesetting industry.

Lorem Ipsum is simply dummy text of the printing and typesetting industry.

Lorem Ipsum has been the industry's standard dummy text ever since the 1500s

", // fallback key 'Hello, World!': 'Hello, World!', 'multiline text string': 'multiline text string', - 'This is a <1>test': 'This is a <1>test', - 'This is a <1><0>{{test}}': 'This is a <1><0>{{test}}', - '2 + 2 = <1>{{result}}': '2 + 2 = <1>{{result}}', + "This is a test": "This is a test", + "This is a {{test}}": "This is a {{test}}", + "2 + 2 = {{result}}": "2 + 2 = {{result}}", 'Go to <1>Administration > Tools to download administrative tools.': 'Go to <1>Administration > Tools to download administrative tools.', - '<0>Lorem Ipsum is simply dummy text of the printing and typesetting industry.Lorem Ipsum is simply dummy text of the printing and typesetting industry.<2>Lorem Ipsum has been the industry\'s standard dummy text ever since the 1500s': '<0>Lorem Ipsum is simply dummy text of the printing and typesetting industry.Lorem Ipsum is simply dummy text of the printing and typesetting industry.<2>Lorem Ipsum has been the industry\'s standard dummy text ever since the 1500s', - 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.<1>Lorem Ipsum is simply dummy text of the printing and typesetting industry.<1>Lorem Ipsum is simply dummy text of the printing and typesetting industry.<2>Lorem Ipsum has been the industry\'s standard dummy text ever since the 1500s': 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.<1>Lorem Ipsum is simply dummy text of the printing and typesetting industry.<1>Lorem Ipsum is simply dummy text of the printing and typesetting industry.<2>Lorem Ipsum has been the industry\'s standard dummy text ever since the 1500s', + + '

Lorem Ipsum is simply dummy text of the printing and typesetting industry.

Lorem Ipsum is simply dummy text of the printing and typesetting industry.

Lorem Ipsum has been the industry\'s standard dummy text ever since the 1500s

': '

Lorem Ipsum is simply dummy text of the printing and typesetting industry.

Lorem Ipsum is simply dummy text of the printing and typesetting industry.

Lorem Ipsum has been the industry\'s standard dummy text ever since the 1500s

', + "Lorem Ipsum is simply dummy text of the printing and typesetting industry.

Lorem Ipsum is simply dummy text of the printing and typesetting industry.

Lorem Ipsum is simply dummy text of the printing and typesetting industry.

Lorem Ipsum has been the industry's standard dummy text ever since the 1500s

": "Lorem Ipsum is simply dummy text of the printing and typesetting industry.

Lorem Ipsum is simply dummy text of the printing and typesetting industry.

Lorem Ipsum is simply dummy text of the printing and typesetting industry.

Lorem Ipsum has been the industry's standard dummy text ever since the 1500s

", // defaults 'The component might be self-closing': 'The component might be self-closing',