From a513565aa568d00e9503394506732f126270a4ba Mon Sep 17 00:00:00 2001 From: Konstantinos Bairaktaris Date: Fri, 17 Jun 2022 10:06:10 +0300 Subject: [PATCH 1/5] Translatable body inside T-component --- packages/cli/src/api/parsers/babel.js | 52 ++++++++++ packages/react/README.md | 67 +++++++++++-- packages/react/src/components/T.jsx | 42 +++++++- packages/react/src/utils/toStr.js | 139 ++++++++++++++++++++++++++ 4 files changed, 288 insertions(+), 12 deletions(-) create mode 100644 packages/react/src/utils/toStr.js diff --git a/packages/cli/src/api/parsers/babel.js b/packages/cli/src/api/parsers/babel.js index 99d088ba..de8aac3f 100644 --- a/packages/cli/src/api/parsers/babel.js +++ b/packages/cli/src/api/parsers/babel.js @@ -35,6 +35,53 @@ function babelParse(source) { } } +/* Converts a list of JSX AST nodes to a string. Each "tag" must be converted + * to a numbered tag in the order they were encountered in and all props must + * be stripped. + * + * const root = babelParse('<>four'); + * const children = root.program.body[0].expression.children; + * const [result] = toStr(children) + * console.log(result.join('')); + * // <<< '<1>four<2/>' + * + * The second argument and return value are there because of how recursion + * works. For high-level invocation you won't have to worry about them. + * */ +function toStr(children, counter = 0) { + if (!children) { return [[], 0]; } + + let result = []; + + let actualCounter = counter; + for (let i = 0; i < children.length; i += 1) { + const child = children[i]; + if (child.type === 'JSXElement') { + actualCounter += 1; + if (child.children && child.children.length > 0) { + // child has children, recursively run 'toStr' on them + const [newResult, newCounter] = toStr(child.children, actualCounter); + if (newResult.length === 0) { return [[], 0]; } + result.push(`<${actualCounter}>`); // <4> + result = result.concat(newResult); // <4>... + result.push(``); // <4>... + // Take numbered tags that were found during the recursion into account + actualCounter = newCounter; + } else { + // child has no children of its own, replace with something like '<4/>' + result.push(`<${actualCounter}/>`); + } + } else if (child.type === 'JSXText') { + // Child is not a React element, append as-is + const chunk = child.value.trim(); + if (chunk) { result.push(chunk); } + } else { + return [[], 0]; + } + } + return [result, actualCounter]; +} + function babelExtractPhrases(HASHES, source, relativeFile, options) { const ast = babelParse(source); babelTraverse(ast, { @@ -140,6 +187,11 @@ function babelExtractPhrases(HASHES, source, relativeFile, options) { params[property] = attrValue; }); + if (!string && elem.name.name === 'T' && node.children && node.children.length) { + const [result] = toStr(node.children); + string = result.join(' ').trim(); + } + if (!string) return; const partial = createPayload(string, params, relativeFile, options); diff --git a/packages/react/README.md b/packages/react/README.md index ffb5725a..0ddc69ce 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -60,6 +60,8 @@ npm install @transifex/native @transifex/react --save ## `T` Component +### Regular usage + ```javascript import React from 'react'; @@ -86,6 +88,8 @@ Available optional props: | _charlimit | Number | Character limit instruction for translators | | _tags | String | Comma separated list of tags | +### Interpolation of React elements + The T-component can accept React elements as properties and they will be rendered properly, ie this would be possible: @@ -96,6 +100,14 @@ rendered properly, ie this would be possible: bold={} /> ``` +Assuming the translations look like this: + +| source | translation | +|-----------------------------------------|--------------------------------------------------| +| A {button} and a {bold} walk into a bar | Ένα {button} και ένα {bold} μπαίνουν σε ένα μπαρ | +| button | κουμπί | +| bold | βαρύ | + This will render like this in English: ```html @@ -108,17 +120,56 @@ And like this in Greek: Ένα και ένα βαρύ μπαίνουν σε ένα μπαρ ``` -Assuming the translations look like this: - -| source | translation | -|-----------------------------------------|--------------------------------------------------| -| A {button} and a {bold} walk into a bar | Ένα {button} και ένα {bold} μπαίνουν σε ένα μπαρ | -| button | κουμπί | -| bold | βαρύ | - The main thing to keep in mind is that the `_str` property to the T-component must **always** be a valid ICU messageformat template. +### Translatable body + +Another way to use the T-component is to include a translatable body that is a +mix of text and React elements: + +```javascript + + A and a bold walk into a bar + +``` + +You must not inject any javascript code in the content of a T-component because: + +1. It will be rendered differently every time and the SDK won't be able to + predictably find a translation +2. The CLI will not be able to extract a source string from it + +If you do this, the string that will be sent to Transifex for translation will +look like this: + +``` +A <1> button and a <2> bold walk into a bar +``` + +As long as the translation respects the numbered tags, the T-component will +render the translation properly. Any props that the React elements have in the +source version of the text will be applied to the translation as well. + +You can interpolate parameters as before, but you have to be careful with how +you define them in the source body: + +```javascript +// ✗ Wrong, this is a javascript expression +hello {username} + +// ✓ Correct, this is a string +hello {'{username}'} +``` + +This time however, the interpolated values **cannot** be React elements. + +```javascript +// ✗ Wrong, this will fail to render +BOLD}>This is {'{bold}'} +``` + + ## `UT` Component ```javascript diff --git a/packages/react/src/components/T.jsx b/packages/react/src/components/T.jsx index a2bef8c3..140f8244 100644 --- a/packages/react/src/components/T.jsx +++ b/packages/react/src/components/T.jsx @@ -1,6 +1,8 @@ +import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; import useT from '../hooks/useT'; +import { toStr, toElement } from '../utils/toStr'; /* Main transifex-native component for react. It delegates the translation to * the `useT` hook, which will force the component to rerender in the event of @@ -19,10 +21,42 @@ import useT from '../hooks/useT'; *

* * ); - * } */ + * } + * + * You can also include translatable content as the body of the T-tag. The body + * must be a combination of text and React elements; you should **not** include + * any javascript logic or it won't manage to be picked up by the CLI and + * translated properly. + * + * function App() { + * const [name, setName] = useState('Bill'); + * return ( + * <> + *

hello world

+ *

hello world

+ *

+ * setName(e.target.value)} /> + * hello {'{name}'} + *

+ * + * ); + * } + * + * */ + +export default function T({ _str, children, ...props }) { + const t = useT(); + if (!children) { return t(_str, props); } + + const [templateArray, propsContainer] = toStr(children); + const templateString = templateArray.join(' ').trim(); + const translation = t(templateString, props); -export default function T({ _str, ...props }) { - return useT()(_str, props); + const result = toElement(translation, propsContainer); + if (result.length === 0) { return ''; } + if (result.length === 1) { return result[0]; } + return {result}; } -T.propTypes = { _str: PropTypes.string.isRequired }; +T.defaultProps = { _str: null, children: null }; +T.propTypes = { _str: PropTypes.string, children: PropTypes.node }; diff --git a/packages/react/src/utils/toStr.js b/packages/react/src/utils/toStr.js new file mode 100644 index 00000000..7ad0169f --- /dev/null +++ b/packages/react/src/utils/toStr.js @@ -0,0 +1,139 @@ +import React from 'react'; + +/* Convert a React component's children to a string. Each "tag" must be + * converted to a numbered tag in the order they were encountered in and all + * props must be stripped. The props must be preserved in the second return + * value so that they can be reinserted again later. + * + * element = <>four; + * const [result, propsContainer] = toStr(element.props.children); + * console.log(result.join('')); + * // <<< '<1>four<2/>' + * console.log(propsContainer); + * // <<< [['one', {two: 'three'}], ['five', {six: 'seven'}]] + * + * The second argument and third return value are there because of how + * recursion works. For high-level invocation you won't have to worry about + * them. + * */ +export function toStr(children, counter = 0) { + if (!children) { return [[], [], 0]; } + let actualChildren = children; + if (!Array.isArray(children)) { + actualChildren = [children]; + } + + // Return values + let result = []; + let propsContainer = []; + + let actualCounter = counter; + for (let i = 0; i < actualChildren.length; i += 1) { + const child = actualChildren[i]; + if (React.isValidElement(child)) { + actualCounter += 1; + + // Each entry in propsContainer matches one matched react element. So for + // the element replaced with '<4>', the relevant props will be + // `propsContainer[3]` (4 - 1) + const props = [ + child.type, + { ...child.props }, // Do this so that delete can work later + ]; + delete props[1].children; + propsContainer.push(props); + + if (child.props.children) { + // child has children, recursively run 'toStr' on them + const [newResult, newProps, newCounter] = toStr( + child.props.children, + actualCounter, + ); + result.push(`<${actualCounter}>`); // <4> + result = result.concat(newResult); // <4>... + result.push(``); // <4>... + // Extend propsContainer with what was found during the recursion + propsContainer = propsContainer.concat(newProps); + // Take numbered tags that were found during the recursion into account + actualCounter = newCounter; + } else { + // child has no children of its own, replace with something like '<4/>' + result.push(`<${actualCounter}/>`); + } + } else { + // Child is not a React element, append as-is + /* eslint-disable no-lonely-if */ + if (typeof child === 'string' || child instanceof String) { + const chunk = child.trim(); + if (chunk) { result.push(chunk); } + } else { + result.push(child); + } + /* eslint-enable */ + } + } + + return [result, propsContainer, actualCounter]; +} + +/* Convert a string that was generated from 'toStr', or its translation, back + * to a React element, combining it with the props that were extracted during + * 'toStr'. + * + * toElement( + * 'one<1>five<2/>', + * [['two', {three: 'four'}], ['six', {seven: 'eight'}]], + * ); + * // The browser will render the equivalent of + * // onefive + * */ +export function toElement(translation, propsContainer) { + const regexp = /<(\d+)(\/?)>/; // Find opening or single tags + const result = []; + + let lastEnd = 0; // Last position in 'translation' we have "consumed" so far + let lastKey = 0; + + for (;;) { + const match = regexp.exec(translation.substring(lastEnd)); + if (match === null) { break; } // We've reached the end + + // Copy until match + const matchIndex = lastEnd + match.index; + const chunk = translation.substring(lastEnd, matchIndex); + if (chunk) { result.push(chunk); } + + const [openingTag, numberString, rightSlash] = match; + const number = parseInt(numberString, 10); + const [type, props] = propsContainer[number - 1]; // Find relevant props + if (rightSlash) { + // Single tag, copy props and don't include children in the React element + result.push(React.createElement(type, { ...props, key: lastKey })); + lastEnd += openingTag.length; + } else { + // Opening tag, find the closing tag which is guaranteed to be there and + // to be unique + const endingTag = ``; + const endingTagPos = translation.indexOf(endingTag); + // Recursively convert contents to React elements + const newResult = toElement( + translation.substring(matchIndex + openingTag.length, endingTagPos), + propsContainer, + ); + // Copy props and include recursion result as children + result.push(React.createElement( + type, + { ...props, key: lastKey }, + ...newResult, + )); + lastEnd = endingTagPos + endingTag.length; + } + lastKey += 1; + } + + // Copy rest of 'translation' + const chunk = translation.substring(lastEnd, translation.length); + if (chunk) { result.push(chunk); } + + return result; +} From d562c368b6a1a4e209c3afb578ac85f6545199e9 Mon Sep 17 00:00:00 2001 From: Konstantinos Bairaktaris Date: Fri, 17 Jun 2022 11:01:23 +0300 Subject: [PATCH 2/5] Support javascript expressions with strings in CLI eg `hello {'{username}'}` --- packages/cli/src/api/parsers/babel.js | 6 ++++++ packages/react/README.md | 14 +++++++------- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/api/parsers/babel.js b/packages/cli/src/api/parsers/babel.js index de8aac3f..99a6d4f6 100644 --- a/packages/cli/src/api/parsers/babel.js +++ b/packages/cli/src/api/parsers/babel.js @@ -75,6 +75,12 @@ function toStr(children, counter = 0) { // Child is not a React element, append as-is const chunk = child.value.trim(); if (chunk) { result.push(chunk); } + } else if ( + child.type === 'JSXExpressionContainer' + && child.expression.type === 'StringLiteral' + ) { + const chunk = child.expression.value.trim(); + if (chunk) { result.push(chunk); } } else { return [[], 0]; } diff --git a/packages/react/README.md b/packages/react/README.md index 0ddc69ce..b6f70020 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -130,16 +130,10 @@ mix of text and React elements: ```javascript - A and a bold walk into a bar + A and a bold walk into a bar ``` -You must not inject any javascript code in the content of a T-component because: - -1. It will be rendered differently every time and the SDK won't be able to - predictably find a translation -2. The CLI will not be able to extract a source string from it - If you do this, the string that will be sent to Transifex for translation will look like this: @@ -151,6 +145,12 @@ As long as the translation respects the numbered tags, the T-component will render the translation properly. Any props that the React elements have in the source version of the text will be applied to the translation as well. +You must not inject any javascript code in the content of a T-component because: + +1. It will be rendered differently every time and the SDK won't be able to + predictably find a translation +2. The CLI will not be able to extract a source string from it + You can interpolate parameters as before, but you have to be careful with how you define them in the source body: From da7e0c328c1711fb35b1441be2e2abe13b7a70c4 Mon Sep 17 00:00:00 2001 From: Konstantinos Bairaktaris Date: Fri, 17 Jun 2022 12:21:26 +0300 Subject: [PATCH 3/5] Tests and a minor fix --- .../cli/test/api/extract.hashedkeys.test.js | 28 +++++++++++++++++++ .../cli/test/api/extract.sourcekeys.test.js | 28 +++++++++++++++++++ packages/cli/test/fixtures/react.jsx | 7 +++++ packages/react/src/utils/toStr.js | 2 +- packages/react/tests/T.test.js | 24 ++++++++++++++++ 5 files changed, 88 insertions(+), 1 deletion(-) diff --git a/packages/cli/test/api/extract.hashedkeys.test.js b/packages/cli/test/api/extract.hashedkeys.test.js index 2881387a..13bed81a 100644 --- a/packages/cli/test/api/extract.hashedkeys.test.js +++ b/packages/cli/test/api/extract.hashedkeys.test.js @@ -154,6 +154,34 @@ describe('extractPhrases with hashed keys', () => { string: 'HTML inline text', meta: { context: [], tags: [], occurrences: ['react.jsx'] }, }, + '57b0d93fc0e1c3af68a41214147efd97': { + string: 'Text 5', + meta: { context: [], tags: [], occurrences: ['react.jsx'] }, + }, + '404d0c0fef510bc89da7bc58ef160ccc': { + string: 'Text <1> 6 ', + meta: { context: [], tags: [], occurrences: ['react.jsx'] }, + }, + '4f5fe2d7356c474bd2f4c03176c6bc45': { + string: 'Text <1> <2> 7 ', + meta: { context: [], tags: [], occurrences: ['react.jsx'] }, + }, + '1ecaf4c087b894bf86987fc2972ddba7': { + string: 'Text 8', + meta: { context: ['foo'], tags: [], occurrences: ['react.jsx'] }, + }, + f9818c4a4b3772c365b8522ff29cb785: { + string: 'Text <1/> 9', + meta: { context: [], tags: [], occurrences: ['react.jsx'] }, + }, + '37678ce8d9c3a694ce19b947c64b9787': { + string: 'Text {msg}', + meta: { context: [], tags: [], occurrences: ['react.jsx'] }, + }, + '5c6622f57e93ed83011b45833a12b0aa': { + string: 'Text 10', + meta: { context: [], tags: ['tag1', 'tag2'], occurrences: ['react.jsx'] }, + }, }); }); diff --git a/packages/cli/test/api/extract.sourcekeys.test.js b/packages/cli/test/api/extract.sourcekeys.test.js index 7de31557..05125385 100644 --- a/packages/cli/test/api/extract.sourcekeys.test.js +++ b/packages/cli/test/api/extract.sourcekeys.test.js @@ -147,6 +147,34 @@ describe('extractPhrases with source keys', () => { string: 'HTML inline text', meta: { context: [], tags: [], occurrences: ['react.jsx'] }, }, + 'Text 5': { + string: 'Text 5', + meta: { context: [], tags: [], occurrences: ['react.jsx'] }, + }, + 'Text <1> 6 ': { + string: 'Text <1> 6 ', + meta: { context: [], tags: [], occurrences: ['react.jsx'] }, + }, + 'Text <1> <2> 7 ': { + string: 'Text <1> <2> 7 ', + meta: { context: [], tags: [], occurrences: ['react.jsx'] }, + }, + 'Text 8::foo': { + string: 'Text 8', + meta: { context: ['foo'], tags: [], occurrences: ['react.jsx'] }, + }, + 'Text <1/> 9': { + string: 'Text <1/> 9', + meta: { context: [], tags: [], occurrences: ['react.jsx'] }, + }, + 'Text {msg}': { + string: 'Text {msg}', + meta: { context: [], tags: [], occurrences: ['react.jsx'] }, + }, + 'Text 10': { + string: 'Text 10', + meta: { context: [], tags: ['tag1', 'tag2'], occurrences: ['react.jsx'] }, + }, }); }); diff --git a/packages/cli/test/fixtures/react.jsx b/packages/cli/test/fixtures/react.jsx index 48666399..44d28238 100644 --- a/packages/cli/test/fixtures/react.jsx +++ b/packages/cli/test/fixtures/react.jsx @@ -30,6 +30,13 @@ function foo() { {msg} {msg2} + Text 5 + Text 6 + Text 7 + Text 8 + Text
9
+ Text {'{msg}'} + Text 10 ); } diff --git a/packages/react/src/utils/toStr.js b/packages/react/src/utils/toStr.js index 7ad0169f..1738e912 100644 --- a/packages/react/src/utils/toStr.js +++ b/packages/react/src/utils/toStr.js @@ -109,7 +109,7 @@ export function toElement(translation, propsContainer) { if (rightSlash) { // Single tag, copy props and don't include children in the React element result.push(React.createElement(type, { ...props, key: lastKey })); - lastEnd += openingTag.length; + lastEnd += matchIndex + openingTag.length; } else { // Opening tag, find the closing tag which is guaranteed to be there and // to be unique diff --git a/packages/react/tests/T.test.js b/packages/react/tests/T.test.js index af864ead..a653cebc 100644 --- a/packages/react/tests/T.test.js +++ b/packages/react/tests/T.test.js @@ -44,4 +44,28 @@ describe('T', () => { render(world} />); expect(screen.getByText('world')).toBeTruthy(); }); + + it('renders body', () => { + render(hello safe text); + expect(screen.queryByText('hello')).toBeInTheDocument(); + expect(screen.queryByText('safe text')).toBeInTheDocument(); + }); + + it('renders body with single tags', () => { + render(hello
safe text
); + expect(screen.queryByText('hello')).toBeInTheDocument(); + expect(screen.queryByText('safe text')).toBeInTheDocument(); + }); + + it('renders nestedbody', () => { + render(hello safe text); + expect(screen.queryByText('hello')).toBeInTheDocument(); + expect(screen.queryByText('safe text')).toBeInTheDocument(); + }); + + it('renders body with params', () => { + render(hello mister {'{username}'}); + expect(screen.queryByText('hello')).toBeInTheDocument(); + expect(screen.queryByText('mister Bill')).toBeInTheDocument(); + }); }); From 0dbcce559de1047db608df8ac802b5d973e6a84d Mon Sep 17 00:00:00 2001 From: Konstantinos Bairaktaris Date: Sun, 26 Jun 2022 15:48:26 +0300 Subject: [PATCH 4/5] Fix small inconsistencies in source strings --- packages/cli/src/api/parsers/babel.js | 2 +- packages/react/src/utils/toStr.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/api/parsers/babel.js b/packages/cli/src/api/parsers/babel.js index 99a6d4f6..3667c202 100644 --- a/packages/cli/src/api/parsers/babel.js +++ b/packages/cli/src/api/parsers/babel.js @@ -73,7 +73,7 @@ function toStr(children, counter = 0) { } } else if (child.type === 'JSXText') { // Child is not a React element, append as-is - const chunk = child.value.trim(); + const chunk = child.value.trim().replace(/\s+/g, ' '); if (chunk) { result.push(chunk); } } else if ( child.type === 'JSXExpressionContainer' diff --git a/packages/react/src/utils/toStr.js b/packages/react/src/utils/toStr.js index 1738e912..7da07fd9 100644 --- a/packages/react/src/utils/toStr.js +++ b/packages/react/src/utils/toStr.js @@ -64,7 +64,7 @@ export function toStr(children, counter = 0) { // Child is not a React element, append as-is /* eslint-disable no-lonely-if */ if (typeof child === 'string' || child instanceof String) { - const chunk = child.trim(); + const chunk = child.trim().replace(/\s+/g, ' '); if (chunk) { result.push(chunk); } } else { result.push(child); From 16e99af798375af27cd4785d70a94ae4be9ec606 Mon Sep 17 00:00:00 2001 From: Konstantinos Bairaktaris Date: Thu, 25 Jul 2024 20:34:39 +0300 Subject: [PATCH 5/5] (Finally) fix the issue with how JSX handles newlines --- packages/cli/src/api/parsers/babel.js | 18 +++++++++++++++--- .../cli/test/api/extract.hashedkeys.test.js | 8 ++++---- .../cli/test/api/extract.sourcekeys.test.js | 8 ++++---- packages/react/README.md | 2 +- packages/react/src/components/T.jsx | 2 +- packages/react/src/utils/toStr.js | 3 +-- 6 files changed, 26 insertions(+), 15 deletions(-) diff --git a/packages/cli/src/api/parsers/babel.js b/packages/cli/src/api/parsers/babel.js index 3667c202..2cc12981 100644 --- a/packages/cli/src/api/parsers/babel.js +++ b/packages/cli/src/api/parsers/babel.js @@ -73,13 +73,25 @@ function toStr(children, counter = 0) { } } else if (child.type === 'JSXText') { // Child is not a React element, append as-is - const chunk = child.value.trim().replace(/\s+/g, ' '); + let chunk = child.value; + + // Try to mimic how JSX is parsed in runtime React + const [startMatch] = /^[\s\n]*/.exec(child.value); + if (startMatch.includes('\n')) { + chunk = chunk.substring(startMatch.length); + } + + const [endMatch] = /[\s\n]*$/.exec(child.value); + if (endMatch.includes('\n')) { + chunk = chunk.substring(0, chunk.length - endMatch.length); + } + if (chunk) { result.push(chunk); } } else if ( child.type === 'JSXExpressionContainer' && child.expression.type === 'StringLiteral' ) { - const chunk = child.expression.value.trim(); + const chunk = child.expression.value; if (chunk) { result.push(chunk); } } else { return [[], 0]; @@ -195,7 +207,7 @@ function babelExtractPhrases(HASHES, source, relativeFile, options) { if (!string && elem.name.name === 'T' && node.children && node.children.length) { const [result] = toStr(node.children); - string = result.join(' ').trim(); + string = result.join(''); } if (!string) return; diff --git a/packages/cli/test/api/extract.hashedkeys.test.js b/packages/cli/test/api/extract.hashedkeys.test.js index 13bed81a..1be9dfa1 100644 --- a/packages/cli/test/api/extract.hashedkeys.test.js +++ b/packages/cli/test/api/extract.hashedkeys.test.js @@ -158,12 +158,12 @@ describe('extractPhrases with hashed keys', () => { string: 'Text 5', meta: { context: [], tags: [], occurrences: ['react.jsx'] }, }, - '404d0c0fef510bc89da7bc58ef160ccc': { - string: 'Text <1> 6 ', + '121b687b8625b4e58ba7f36dca77ad7f': { + string: 'Text <1>6', meta: { context: [], tags: [], occurrences: ['react.jsx'] }, }, - '4f5fe2d7356c474bd2f4c03176c6bc45': { - string: 'Text <1> <2> 7 ', + '3ed8a3c47f6a32ece9c9ae0c2a060d45': { + string: 'Text <1><2>7', meta: { context: [], tags: [], occurrences: ['react.jsx'] }, }, '1ecaf4c087b894bf86987fc2972ddba7': { diff --git a/packages/cli/test/api/extract.sourcekeys.test.js b/packages/cli/test/api/extract.sourcekeys.test.js index 05125385..ebf8f017 100644 --- a/packages/cli/test/api/extract.sourcekeys.test.js +++ b/packages/cli/test/api/extract.sourcekeys.test.js @@ -151,12 +151,12 @@ describe('extractPhrases with source keys', () => { string: 'Text 5', meta: { context: [], tags: [], occurrences: ['react.jsx'] }, }, - 'Text <1> 6 ': { - string: 'Text <1> 6 ', + 'Text <1>6': { + string: 'Text <1>6', meta: { context: [], tags: [], occurrences: ['react.jsx'] }, }, - 'Text <1> <2> 7 ': { - string: 'Text <1> <2> 7 ', + 'Text <1><2>7': { + string: 'Text <1><2>7', meta: { context: [], tags: [], occurrences: ['react.jsx'] }, }, 'Text 8::foo': { diff --git a/packages/react/README.md b/packages/react/README.md index b6f70020..c5a34d52 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -138,7 +138,7 @@ If you do this, the string that will be sent to Transifex for translation will look like this: ``` -A <1> button and a <2> bold walk into a bar +A <1>button and a <2>bold walk into a bar ``` As long as the translation respects the numbered tags, the T-component will diff --git a/packages/react/src/components/T.jsx b/packages/react/src/components/T.jsx index 140f8244..60b0ba01 100644 --- a/packages/react/src/components/T.jsx +++ b/packages/react/src/components/T.jsx @@ -49,7 +49,7 @@ export default function T({ _str, children, ...props }) { if (!children) { return t(_str, props); } const [templateArray, propsContainer] = toStr(children); - const templateString = templateArray.join(' ').trim(); + const templateString = templateArray.join(''); const translation = t(templateString, props); const result = toElement(translation, propsContainer); diff --git a/packages/react/src/utils/toStr.js b/packages/react/src/utils/toStr.js index 7da07fd9..861f827d 100644 --- a/packages/react/src/utils/toStr.js +++ b/packages/react/src/utils/toStr.js @@ -64,8 +64,7 @@ export function toStr(children, counter = 0) { // Child is not a React element, append as-is /* eslint-disable no-lonely-if */ if (typeof child === 'string' || child instanceof String) { - const chunk = child.trim().replace(/\s+/g, ' '); - if (chunk) { result.push(chunk); } + if (child) { result.push(child); } } else { result.push(child); }