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;