From 6ec259a0e71174651bae95d4628138bf6fd68742 Mon Sep 17 00:00:00 2001 From: kj_sh604 Date: Sun, 15 Mar 2026 16:19:35 -0400 Subject: refactor: packages/ --- packages/excalidraw/components/Trans.tsx | 170 +++++++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 packages/excalidraw/components/Trans.tsx (limited to 'packages/excalidraw/components/Trans.tsx') 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 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: 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; -- cgit v1.2.3