feat: Add Trans component for interpolating JSX in translations (#6534)
* feat: add Trans component * Add comments * tweak * Move brave to trans component * fix test and tweaks * remove any * fix * fix * comment * replace render function type * Use tags for Trans * Fix a typo Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com> * Cleanup, add comments, add support for kebab case * tweaks --------- Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com> Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
parent
e9cae918a7
commit
1184a8c0e9
@ -1,39 +1,40 @@
|
|||||||
import { t } from "../i18n";
|
import Trans from "./Trans";
|
||||||
|
|
||||||
const BraveMeasureTextError = () => {
|
const BraveMeasureTextError = () => {
|
||||||
return (
|
return (
|
||||||
<div data-testid="brave-measure-text-error">
|
<div data-testid="brave-measure-text-error">
|
||||||
<p>
|
<p>
|
||||||
{t("errors.brave_measure_text_error.start")}
|
<Trans
|
||||||
<span style={{ fontWeight: 600 }}>
|
i18nKey="errors.brave_measure_text_error.line1"
|
||||||
{t("errors.brave_measure_text_error.aggressive_block_fingerprint")}
|
bold={(el) => <span style={{ fontWeight: 600 }}>{el}</span>}
|
||||||
</span>{" "}
|
/>
|
||||||
{t("errors.brave_measure_text_error.setting_enabled")}.
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
{t("errors.brave_measure_text_error.break")}{" "}
|
|
||||||
<span style={{ fontWeight: 600 }}>
|
|
||||||
{t("errors.brave_measure_text_error.text_elements")}
|
|
||||||
</span>{" "}
|
|
||||||
{t("errors.brave_measure_text_error.in_your_drawings")}.
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{t("errors.brave_measure_text_error.strongly_recommend")}{" "}
|
<Trans
|
||||||
<a href="http://docs.excalidraw.com/docs/@excalidraw/excalidraw/faq#turning-off-aggresive-block-fingerprinting-in-brave-browser">
|
i18nKey="errors.brave_measure_text_error.line2"
|
||||||
{" "}
|
bold={(el) => <span style={{ fontWeight: 600 }}>{el}</span>}
|
||||||
{t("errors.brave_measure_text_error.steps")}
|
/>
|
||||||
</a>{" "}
|
|
||||||
{t("errors.brave_measure_text_error.how")}.
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{t("errors.brave_measure_text_error.disable_setting")}{" "}
|
<Trans
|
||||||
<a href="https://github.com/excalidraw/excalidraw/issues/new">
|
i18nKey="errors.brave_measure_text_error.line3"
|
||||||
{t("errors.brave_measure_text_error.issue")}
|
link={(el) => (
|
||||||
</a>{" "}
|
<a href="http://docs.excalidraw.com/docs/@excalidraw/excalidraw/faq#turning-off-aggresive-block-fingerprinting-in-brave-browser">
|
||||||
{t("errors.brave_measure_text_error.write")}{" "}
|
{el}
|
||||||
<a href="https://discord.gg/UexuTaE">
|
</a>
|
||||||
{t("errors.brave_measure_text_error.discord")}
|
)}
|
||||||
</a>
|
/>
|
||||||
.
|
</p>
|
||||||
|
<p>
|
||||||
|
<Trans
|
||||||
|
i18nKey="errors.brave_measure_text_error.line4"
|
||||||
|
issueLink={(el) => (
|
||||||
|
<a href="https://github.com/excalidraw/excalidraw/issues/new">
|
||||||
|
{el}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
discordLink={(el) => <a href="https://discord.gg/UexuTaE">{el}.</a>}
|
||||||
|
/>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
67
src/components/Trans.test.tsx
Normal file
67
src/components/Trans.test.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { render } from "@testing-library/react";
|
||||||
|
|
||||||
|
import fallbackLangData from "../locales/en.json";
|
||||||
|
|
||||||
|
import Trans from "./Trans";
|
||||||
|
|
||||||
|
describe("Test <Trans/>", () => {
|
||||||
|
it("should translate the the strings correctly", () => {
|
||||||
|
//@ts-ignore
|
||||||
|
fallbackLangData.transTest = {
|
||||||
|
key1: "Hello {{audience}}",
|
||||||
|
key2: "Please <link>click the button</link> to continue.",
|
||||||
|
key3: "Please <link>click {{location}}</link> to continue.",
|
||||||
|
key4: "Please <link>click <bold>{{location}}</bold></link> to continue.",
|
||||||
|
key5: "Please <connect-link>click the button</connect-link> to continue.",
|
||||||
|
};
|
||||||
|
|
||||||
|
const { getByTestId } = render(
|
||||||
|
<>
|
||||||
|
<div data-testid="test1">
|
||||||
|
<Trans i18nKey="transTest.key1" audience="world" />
|
||||||
|
</div>
|
||||||
|
<div data-testid="test2">
|
||||||
|
<Trans
|
||||||
|
i18nKey="transTest.key2"
|
||||||
|
link={(el) => <a href="https://example.com">{el}</a>}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div data-testid="test3">
|
||||||
|
<Trans
|
||||||
|
i18nKey="transTest.key3"
|
||||||
|
link={(el) => <a href="https://example.com">{el}</a>}
|
||||||
|
location="the button"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div data-testid="test4">
|
||||||
|
<Trans
|
||||||
|
i18nKey="transTest.key4"
|
||||||
|
link={(el) => <a href="https://example.com">{el}</a>}
|
||||||
|
location="the button"
|
||||||
|
bold={(el) => <strong>{el}</strong>}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div data-testid="test5">
|
||||||
|
<Trans
|
||||||
|
i18nKey="transTest.key5"
|
||||||
|
connect-link={(el) => <a href="https://example.com">{el}</a>}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getByTestId("test1").innerHTML).toEqual("Hello world");
|
||||||
|
expect(getByTestId("test2").innerHTML).toEqual(
|
||||||
|
`Please <a href="https://example.com">click the button</a> to continue.`,
|
||||||
|
);
|
||||||
|
expect(getByTestId("test3").innerHTML).toEqual(
|
||||||
|
`Please <a href="https://example.com">click the button</a> to continue.`,
|
||||||
|
);
|
||||||
|
expect(getByTestId("test4").innerHTML).toEqual(
|
||||||
|
`Please <a href="https://example.com">click <strong>the button</strong></a> to continue.`,
|
||||||
|
);
|
||||||
|
expect(getByTestId("test5").innerHTML).toEqual(
|
||||||
|
`Please <a href="https://example.com">click the button</a> to continue.`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
169
src/components/Trans.tsx
Normal file
169
src/components/Trans.tsx
Normal file
@ -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;
|
@ -6,58 +6,45 @@ exports[`Test <App/> should show error modal when using brave and measureText AP
|
|||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
Looks like you are using Brave browser with the
|
Looks like you are using Brave browser with the
|
||||||
|
|
||||||
<span
|
<span
|
||||||
style="font-weight: 600;"
|
style="font-weight: 600;"
|
||||||
>
|
>
|
||||||
Aggressively Block Fingerprinting
|
Aggressively Block Fingerprinting
|
||||||
</span>
|
</span>
|
||||||
|
setting enabled.
|
||||||
setting enabled
|
</p>
|
||||||
.
|
<p>
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
This could result in breaking the
|
This could result in breaking the
|
||||||
|
|
||||||
<span
|
<span
|
||||||
style="font-weight: 600;"
|
style="font-weight: 600;"
|
||||||
>
|
>
|
||||||
Text Elements
|
Text Elements
|
||||||
</span>
|
</span>
|
||||||
|
in your drawings.
|
||||||
in your drawings
|
|
||||||
.
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
We strongly recommend disabling this setting. You can follow
|
We strongly recommend disabling this setting. You can follow
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="http://docs.excalidraw.com/docs/@excalidraw/excalidraw/faq#turning-off-aggresive-block-fingerprinting-in-brave-browser"
|
href="http://docs.excalidraw.com/docs/@excalidraw/excalidraw/faq#turning-off-aggresive-block-fingerprinting-in-brave-browser"
|
||||||
>
|
>
|
||||||
|
|
||||||
these steps
|
these steps
|
||||||
</a>
|
</a>
|
||||||
|
on how to do so.
|
||||||
on how to do so
|
|
||||||
.
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
If disabling this setting doesn't fix the display of text elements, please open an
|
If disabling this setting doesn't fix the display of text elements, please open an
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="https://github.com/excalidraw/excalidraw/issues/new"
|
href="https://github.com/excalidraw/excalidraw/issues/new"
|
||||||
>
|
>
|
||||||
issue
|
issue
|
||||||
</a>
|
</a>
|
||||||
|
on our GitHub, or write us on
|
||||||
on our GitHub, or write us on
|
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="https://discord.gg/UexuTaE"
|
href="https://discord.gg/UexuTaE"
|
||||||
>
|
>
|
||||||
Discord
|
Discord
|
||||||
|
.
|
||||||
</a>
|
</a>
|
||||||
.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
@ -208,18 +208,10 @@
|
|||||||
"collabSaveFailed": "Couldn't save to the backend database. If problems persist, you should save your file locally to ensure you don't lose your work.",
|
"collabSaveFailed": "Couldn't save to the backend database. If problems persist, you should save your file locally to ensure you don't lose your work.",
|
||||||
"collabSaveFailed_sizeExceeded": "Couldn't save to the backend database, the canvas seems to be too big. You should save the file locally to ensure you don't lose your work.",
|
"collabSaveFailed_sizeExceeded": "Couldn't save to the backend database, the canvas seems to be too big. You should save the file locally to ensure you don't lose your work.",
|
||||||
"brave_measure_text_error": {
|
"brave_measure_text_error": {
|
||||||
"start": "Looks like you are using Brave browser with the",
|
"line1": "Looks like you are using Brave browser with the <bold>Aggressively Block Fingerprinting</bold> setting enabled.",
|
||||||
"aggressive_block_fingerprint": "Aggressively Block Fingerprinting",
|
"line2": "This could result in breaking the <bold>Text Elements</bold> in your drawings.",
|
||||||
"setting_enabled": "setting enabled",
|
"line3": "We strongly recommend disabling this setting. You can follow <link>these steps</link> on how to do so.",
|
||||||
"break": "This could result in breaking the",
|
"line4": " If disabling this setting doesn't fix the display of text elements, please open an <issueLink>issue</issueLink> on our GitHub, or write us on <discordLink>Discord</discordLink>",
|
||||||
"text_elements": "Text Elements",
|
|
||||||
"in_your_drawings": "in your drawings",
|
|
||||||
"strongly_recommend": "We strongly recommend disabling this setting. You can follow",
|
|
||||||
"steps": "these steps",
|
|
||||||
"how": "on how to do so",
|
|
||||||
"disable_setting": " If disabling this setting doesn't fix the display of text elements, please open an",
|
|
||||||
"issue": "issue",
|
|
||||||
"write": "on our GitHub, or write us on",
|
|
||||||
"discord": "Discord"
|
"discord": "Discord"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user