Skip to content

Commit

Permalink
feat: Add Trans component for interpolating JSX in translations (exca…
Browse files Browse the repository at this point in the history
…lidraw#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>
  • Loading branch information
3 people authored and alswl committed Nov 15, 2023
1 parent 3455283 commit 2537b58
Show file tree
Hide file tree
Showing 5 changed files with 280 additions and 64 deletions.
57 changes: 29 additions & 28 deletions src/components/BraveMeasureTextError.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,40 @@
import { t } from "../i18n";
import Trans from "./Trans";

const BraveMeasureTextError = () => {
return (
<div data-testid="brave-measure-text-error">
<p>
{t("errors.brave_measure_text_error.start")} &nbsp;
<span style={{ fontWeight: 600 }}>
{t("errors.brave_measure_text_error.aggressive_block_fingerprint")}
</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")}.
<Trans
i18nKey="errors.brave_measure_text_error.line1"
bold={(el) => <span style={{ fontWeight: 600 }}>{el}</span>}
/>
</p>
<p>
<Trans
i18nKey="errors.brave_measure_text_error.line2"
bold={(el) => <span style={{ fontWeight: 600 }}>{el}</span>}
/>
</p>
<p>
{t("errors.brave_measure_text_error.strongly_recommend")}{" "}
<a href="http://docs.excalidraw.com/docs/@excalidraw/excalidraw/faq#turning-off-aggresive-block-fingerprinting-in-brave-browser">
{" "}
{t("errors.brave_measure_text_error.steps")}
</a>{" "}
{t("errors.brave_measure_text_error.how")}.
<Trans
i18nKey="errors.brave_measure_text_error.line3"
link={(el) => (
<a href="http://docs.excalidraw.com/docs/@excalidraw/excalidraw/faq#turning-off-aggresive-block-fingerprinting-in-brave-browser">
{el}
</a>
)}
/>
</p>
<p>
{t("errors.brave_measure_text_error.disable_setting")}{" "}
<a href="https://github.com/excalidraw/excalidraw/issues/new">
{t("errors.brave_measure_text_error.issue")}
</a>{" "}
{t("errors.brave_measure_text_error.write")}{" "}
<a href="https://discord.gg/UexuTaE">
{t("errors.brave_measure_text_error.discord")}
</a>
.
<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>
</div>
);
Expand Down
67 changes: 67 additions & 0 deletions src/components/Trans.test.tsx
Original file line number Diff line number Diff line change
@@ -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 changes: 169 additions & 0 deletions src/components/Trans.tsx
Original file line number Diff line number Diff line change
@@ -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;
35 changes: 11 additions & 24 deletions src/components/__snapshots__/App.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -5,59 +5,46 @@ exports[`Test <App/> should show error modal when using brave and measureText AP
data-testid="brave-measure-text-error"
>
<p>
Looks like you are using Brave browser with the
 
Looks like you are using Brave browser with the
<span
style="font-weight: 600;"
>
Aggressively Block Fingerprinting
</span>
setting enabled
.
<br />
<br />
This could result in breaking the
setting enabled.
</p>
<p>
This could result in breaking the
<span
style="font-weight: 600;"
>
Text Elements
</span>
in your drawings
.
in your drawings.
</p>
<p>
We strongly recommend disabling this setting. You can follow
We strongly recommend disabling this setting. You can follow
<a
href="http://docs.excalidraw.com/docs/@excalidraw/excalidraw/faq#turning-off-aggresive-block-fingerprinting-in-brave-browser"
>
these steps
</a>
on how to do so
.
on how to do so.
</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
href="https://github.com/excalidraw/excalidraw/issues/new"
>
issue
</a>
on our GitHub, or write us on
on our GitHub, or write us on
<a
href="https://discord.gg/UexuTaE"
>
Discord
.
</a>
.
</p>
</div>
`;
Loading

0 comments on commit 2537b58

Please sign in to comment.