diff options
Diffstat (limited to 'packages/excalidraw/components/Trans.tsx')
| -rw-r--r-- | packages/excalidraw/components/Trans.tsx | 170 |
1 files changed, 170 insertions, 0 deletions
diff --git a/packages/excalidraw/components/Trans.tsx b/packages/excalidraw/components/Trans.tsx new file mode 100644 index 0000000..0cb0f78 --- /dev/null +++ b/packages/excalidraw/components/Trans.tsx @@ -0,0 +1,170 @@ +import React from "react"; + +import type { TranslationKeys } from "../i18n"; +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: TranslationKeys; + [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; |
