import React from "react"; import { useI18n } from "../i18n"; // Used for splitting i18nKey into tokens in Trans component // Example: // "Please <link>click {{location}}</link> to continue.".split(SPLIT_REGEX).filter(Boolean) // produces // ["Please ", "<link>", "click ", "{{location}}", "</link>", " to continue."] const SPLIT_REGEX = /({{[\w-]+}})|(<[\w-]+>)|(<\/[\w-]+>)/g; // Used for extracting "location" from "{{location}}" const KEY_REGEXP = /{{([\w-]+)}}/; // Used for extracting "link" from "<link>" const TAG_START_REGEXP = /<([\w-]+)>/; // Used for extracting "link" from "</link>" 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 <tag>. Set the tag name as the name if it's one of the // props, e.g. for "Please <link>click the button</link> 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 <link>click the // button</link> to continue", tagEndMatch is for "</link>", stack last item name = // "link" and props.link = (el) => <a // href="https://example.com">{el}</a> then its prop value will be // pushed to "link"'s children so on DOM when rendering it's rendered as // <a href="https://example.com">click the button</a> 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 <link>click the button</link> to continue.", "example3": "Please <link>click {{location}}</link> to continue.", "example4": "Please <link>click <bold>{{location}}</bold></link> to continue.", } ``` ```jsx <Trans i18nKey="example1" audience="world" /> <Trans i18nKey="example2" connectLink={(el) => <a href="https://example.com">{el}</a>} /> <Trans i18nKey="example3" connectLink={(el) => <a href="https://example.com">{el}</a>} location="the button" /> <Trans i18nKey="example4" connectLink={(el) => <a href="https://example.com">{el}</a>} location="the button" bold={(el) => <strong>{el}</strong>} /> ``` Output: ```html Hello world Please <a href="https://example.com">click the button</a> to continue. Please <a href="https://example.com">click the button</a> to continue. Please <a href="https://example.com">click <strong>the button</strong></a> 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;