From 1184a8c0e953312ff4a2b5beae467a1e11897477 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luka=20Zakraj=C5=A1ek?= Date: Fri, 5 May 2023 18:05:18 +0200 Subject: [PATCH] feat: Add Trans component for interpolating JSX in translations (#6534) * feat: add Trans component * Add comments * tweak * Move brave to trans component * fix test and tweaks * remove any * fix * fix * comment * replace render function type * Use tags for Trans * Fix a typo Co-authored-by: Aakansha Doshi * Cleanup, add comments, add support for kebab case * tweaks --------- Co-authored-by: Aakansha Doshi Co-authored-by: dwelle --- src/components/BraveMeasureTextError.tsx | 57 +++--- src/components/Trans.test.tsx | 67 +++++++ src/components/Trans.tsx | 169 ++++++++++++++++++ .../__snapshots__/App.test.tsx.snap | 35 ++-- src/locales/en.json | 16 +- 5 files changed, 280 insertions(+), 64 deletions(-) create mode 100644 src/components/Trans.test.tsx create mode 100644 src/components/Trans.tsx diff --git a/src/components/BraveMeasureTextError.tsx b/src/components/BraveMeasureTextError.tsx index 8a4a71e4..1932d7a2 100644 --- a/src/components/BraveMeasureTextError.tsx +++ b/src/components/BraveMeasureTextError.tsx @@ -1,39 +1,40 @@ -import { t } from "../i18n"; +import Trans from "./Trans"; + const BraveMeasureTextError = () => { return (

- {t("errors.brave_measure_text_error.start")}   - - {t("errors.brave_measure_text_error.aggressive_block_fingerprint")} - {" "} - {t("errors.brave_measure_text_error.setting_enabled")}. -
-
- {t("errors.brave_measure_text_error.break")}{" "} - - {t("errors.brave_measure_text_error.text_elements")} - {" "} - {t("errors.brave_measure_text_error.in_your_drawings")}. + {el}} + />

- {t("errors.brave_measure_text_error.strongly_recommend")}{" "} - - {" "} - {t("errors.brave_measure_text_error.steps")} - {" "} - {t("errors.brave_measure_text_error.how")}. + {el}} + />

- {t("errors.brave_measure_text_error.disable_setting")}{" "} - - {t("errors.brave_measure_text_error.issue")} - {" "} - {t("errors.brave_measure_text_error.write")}{" "} - - {t("errors.brave_measure_text_error.discord")} - - . + ( + + {el} + + )} + /> +

+

+ ( + + {el} + + )} + discordLink={(el) => {el}.} + />

); diff --git a/src/components/Trans.test.tsx b/src/components/Trans.test.tsx new file mode 100644 index 00000000..e3e9a462 --- /dev/null +++ b/src/components/Trans.test.tsx @@ -0,0 +1,67 @@ +import { render } from "@testing-library/react"; + +import fallbackLangData from "../locales/en.json"; + +import Trans from "./Trans"; + +describe("Test ", () => { + it("should translate the the strings correctly", () => { + //@ts-ignore + fallbackLangData.transTest = { + key1: "Hello {{audience}}", + key2: "Please click the button to continue.", + key3: "Please click {{location}} to continue.", + key4: "Please click {{location}} to continue.", + key5: "Please click the button to continue.", + }; + + const { getByTestId } = render( + <> +
+ +
+
+ {el}} + /> +
+
+ {el}} + location="the button" + /> +
+
+ {el}} + location="the button" + bold={(el) => {el}} + /> +
+
+ {el}} + /> +
+ , + ); + + 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" } },