|
@@ -0,0 +1,169 @@
|
|
|
+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;
|