+ >,
+ );
+
+ expect(getByTestId("test1").innerHTML).toEqual("Hello world");
+ expect(getByTestId("test2").innerHTML).toEqual(
+ `Please click the button to continue.`,
+ );
+ expect(getByTestId("test3").innerHTML).toEqual(
+ `Please click the button to continue.`,
+ );
+ expect(getByTestId("test4").innerHTML).toEqual(
+ `Please click the button to continue.`,
+ );
+ expect(getByTestId("test5").innerHTML).toEqual(
+ `Please click the button to continue.`,
+ );
+ });
+});
diff --git a/src/components/Trans.tsx b/src/components/Trans.tsx
new file mode 100644
index 00000000..189cda23
--- /dev/null
+++ b/src/components/Trans.tsx
@@ -0,0 +1,169 @@
+import React from "react";
+
+import { useI18n } from "../i18n";
+
+// Used for splitting i18nKey into tokens in Trans component
+// Example:
+// "Please click {{location}} to continue.".split(SPLIT_REGEX).filter(Boolean)
+// produces
+// ["Please ", "", "click ", "{{location}}", "", " to continue."]
+const SPLIT_REGEX = /({{[\w-]+}})|(<[\w-]+>)|(<\/[\w-]+>)/g;
+// Used for extracting "location" from "{{location}}"
+const KEY_REGEXP = /{{([\w-]+)}}/;
+// Used for extracting "link" from ""
+const TAG_START_REGEXP = /<([\w-]+)>/;
+// Used for extracting "link" from ""
+const TAG_END_REGEXP = /<\/([\w-]+)>/;
+
+const getTransChildren = (
+ format: string,
+ props: {
+ [key: string]: React.ReactNode | ((el: React.ReactNode) => React.ReactNode);
+ },
+): React.ReactNode[] => {
+ const stack: { name: string; children: React.ReactNode[] }[] = [
+ {
+ name: "",
+ children: [],
+ },
+ ];
+
+ format
+ .split(SPLIT_REGEX)
+ .filter(Boolean)
+ .forEach((match) => {
+ const tagStartMatch = match.match(TAG_START_REGEXP);
+ const tagEndMatch = match.match(TAG_END_REGEXP);
+ const keyMatch = match.match(KEY_REGEXP);
+
+ if (tagStartMatch !== null) {
+ // The match is . Set the tag name as the name if it's one of the
+ // props, e.g. for "Please click the button to continue"
+ // tagStartMatch[1] = "link" and props contain "link" then it will be
+ // pushed to stack.
+ const name = tagStartMatch[1];
+ if (props.hasOwnProperty(name)) {
+ stack.push({
+ name,
+ children: [],
+ });
+ } else {
+ console.warn(
+ `Trans: missed to pass in prop ${name} for interpolating ${format}`,
+ );
+ }
+ } else if (tagEndMatch !== null) {
+ // If tag end match is found, this means we need to replace the content with
+ // its actual value in prop e.g. format = "Please click the
+ // button to continue", tagEndMatch is for "", stack last item name =
+ // "link" and props.link = (el) => {el} then its prop value will be
+ // pushed to "link"'s children so on DOM when rendering it's rendered as
+ // click the button
+ const name = tagEndMatch[1];
+ if (name === stack[stack.length - 1].name) {
+ const item = stack.pop()!;
+ const itemChildren = React.createElement(
+ React.Fragment,
+ {},
+ ...item.children,
+ );
+ const fn = props[item.name];
+ if (typeof fn === "function") {
+ stack[stack.length - 1].children.push(fn(itemChildren));
+ }
+ } else {
+ console.warn(
+ `Trans: unexpected end tag ${match} for interpolating ${format}`,
+ );
+ }
+ } else if (keyMatch !== null) {
+ // The match is for {{key}}. Check if the key is present in props and set
+ // the prop value as children of last stack item e.g. format = "Hello
+ // {{name}}", key = "name" and props.name = "Excalidraw" then its prop
+ // value will be pushed to "name"'s children so it's rendered on DOM as
+ // "Hello Excalidraw"
+ const name = keyMatch[1];
+ if (props.hasOwnProperty(name)) {
+ stack[stack.length - 1].children.push(props[name] as React.ReactNode);
+ } else {
+ console.warn(
+ `Trans: key ${name} not in props for interpolating ${format}`,
+ );
+ }
+ } else {
+ // If none of cases match means we just need to push the string
+ // to stack eg - "Hello {{name}} Whats up?" "Hello", "Whats up" will be pushed
+ stack[stack.length - 1].children.push(match);
+ }
+ });
+
+ if (stack.length !== 1) {
+ console.warn(`Trans: stack not empty for interpolating ${format}`);
+ }
+
+ return stack[0].children;
+};
+
+/*
+Trans component is used for translating JSX.
+
+```json
+{
+ "example1": "Hello {{audience}}",
+ "example2": "Please click the button to continue.",
+ "example3": "Please click {{location}} to continue.",
+ "example4": "Please click {{location}} to continue.",
+}
+```
+
+```jsx
+
+
+{el}}
+/>
+
+{el}}
+ location="the button"
+/>
+
+{el}}
+ location="the button"
+ bold={(el) => {el}}
+/>
+```
+
+Output:
+
+```html
+Hello world
+Please click the button to continue.
+Please click the button to continue.
+Please click the button to continue.
+```
+*/
+const Trans = ({
+ i18nKey,
+ children,
+ ...props
+}: {
+ i18nKey: string;
+ [key: string]: React.ReactNode | ((el: React.ReactNode) => React.ReactNode);
+}) => {
+ const { t } = useI18n();
+
+ // This is needed to avoid unique key error in list which gets rendered from getTransChildren
+ return React.createElement(
+ React.Fragment,
+ {},
+ ...getTransChildren(t(i18nKey), props),
+ );
+};
+
+export default Trans;
diff --git a/src/components/__snapshots__/App.test.tsx.snap b/src/components/__snapshots__/App.test.tsx.snap
index b36d678c..25da39e3 100644
--- a/src/components/__snapshots__/App.test.tsx.snap
+++ b/src/components/__snapshots__/App.test.tsx.snap
@@ -5,59 +5,46 @@ exports[`Test should show error modal when using brave and measureText AP
data-testid="brave-measure-text-error"
>
- Looks like you are using Brave browser with the
-
+ Looks like you are using Brave browser with the
Aggressively Block Fingerprinting
-
- setting enabled
- .
-
-
- This could result in breaking the
-
+ setting enabled.
+
+
+ This could result in breaking the
Text Elements
-
- in your drawings
- .
+ in your drawings.
- We strongly recommend disabling this setting. You can follow
-
+ We strongly recommend disabling this setting. You can follow
-
these steps
-
- on how to do so
- .
+ on how to do so.
- If disabling this setting doesn't fix the display of text elements, please open an
-
+ If disabling this setting doesn't fix the display of text elements, please open an
issue
-
- on our GitHub, or write us on
-
+ on our GitHub, or write us on
Discord
+ .
- .
`;
diff --git a/src/locales/en.json b/src/locales/en.json
index 7e250a80..041fb564 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -208,18 +208,10 @@
"collabSaveFailed": "Couldn't save to the backend database. If problems persist, you should save your file locally to ensure you don't lose your work.",
"collabSaveFailed_sizeExceeded": "Couldn't save to the backend database, the canvas seems to be too big. You should save the file locally to ensure you don't lose your work.",
"brave_measure_text_error": {
- "start": "Looks like you are using Brave browser with the",
- "aggressive_block_fingerprint": "Aggressively Block Fingerprinting",
- "setting_enabled": "setting enabled",
- "break": "This could result in breaking the",
- "text_elements": "Text Elements",
- "in_your_drawings": "in your drawings",
- "strongly_recommend": "We strongly recommend disabling this setting. You can follow",
- "steps": "these steps",
- "how": "on how to do so",
- "disable_setting": " If disabling this setting doesn't fix the display of text elements, please open an",
- "issue": "issue",
- "write": "on our GitHub, or write us on",
+ "line1": "Looks like you are using Brave browser with the Aggressively Block Fingerprinting setting enabled.",
+ "line2": "This could result in breaking the Text Elements in your drawings.",
+ "line3": "We strongly recommend disabling this setting. You can follow these steps on how to do so.",
+ "line4": " If disabling this setting doesn't fix the display of text elements, please open an issue on our GitHub, or write us on Discord",
"discord": "Discord"
}
},