Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for content translation in RTE #1587

Merged
merged 20 commits into from
Feb 20, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
80a18ff
Copy xml code from https://github.com/vivid-planet/comet/pull/1040/files
VP-DS Jan 17, 2024
0986c46
Add subscript and superscript support to xml code
VP-DS Jan 17, 2024
ef6eda5
Add translation button to rte
VP-DS Jan 17, 2024
f5db0a1
Add changelog md file
VP-DS Jan 17, 2024
d20b457
Adjust changelog md file
VP-DS Jan 26, 2024
75a6d3f
copy test .spec.ts files of copied xml code
VP-DS Jan 26, 2024
5a9ac19
Only display translate button on rte block if enabled
VP-DS Jan 26, 2024
dbe8ae3
Add support for custom inline styles + decode escaped special charact…
VP-DS Jan 29, 2024
f5bacdd
Join and split by tag <split /> to send xml to translation as a singl…
VP-DS Jan 30, 2024
fb6ac0c
Remove xml code and use html packages instead
VP-DS Jan 31, 2024
a2a5e2e
Replace uuid with number for link data array matching + shorten/renam…
VP-DS Feb 1, 2024
19652ab
Add jest tests
VP-DS Feb 7, 2024
3f85503
Use class instead of style for html span
VP-DS Feb 13, 2024
0bbd007
Rename linkDataList to entities for future support of a broader use case
VP-DS Feb 13, 2024
d1be921
Throw an error if an unsupported entityType is used in the stateToHtm…
VP-DS Feb 13, 2024
7e5c676
Reduce the options used in the spec.ts to only the minimum IRteOption…
VP-DS Feb 14, 2024
364ad7b
Add trimHtml function which removes newlines and spaces to compare ht…
VP-DS Feb 14, 2024
798c5a7
Use comet ToolTip instead of mui
VP-DS Feb 20, 2024
453d449
Merge remote-tracking branch 'origin/feature/translation-module' into…
VP-DS Feb 20, 2024
ecd75ec
Replace any with type used in draft-js-export-html file
VP-DS Feb 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/seven-sailor-sing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@comet/admin-rte": minor
---

Add support for content translation
7 changes: 7 additions & 0 deletions packages/admin/admin-rte/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
reporters: ["default", "jest-junit"],
rootDir: "./src",
};
15 changes: 14 additions & 1 deletion packages/admin/admin-rte/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,16 @@
"lint:tsc": "tsc --noEmit",
"start": "run-p start:babel start:types",
"start:babel": "npx babel ./src -x \".ts,.tsx\" -d lib -w",
"start:types": "tsc --project ./tsconfig.json --emitDeclarationOnly --watch --preserveWatchOutput"
"start:types": "tsc --project ./tsconfig.json --emitDeclarationOnly --watch --preserveWatchOutput",
"test": "jest",
"test:watch": "jest --watch"
},
"dependencies": {
"@comet/admin": "workspace:^5.3.0",
"@comet/admin-icons": "workspace:^5.3.0",
"detect-browser": "^5.2.1",
"draft-js-export-html": "^1.4.1",
"draft-js-import-html": "^1.4.1",
"draftjs-conductor": "^3.0.0",
"immutable": "~3.7.4"
},
Expand All @@ -38,20 +43,28 @@
"@mui/icons-material": "^5.0.0",
"@mui/material": "^5.0.0",
"@mui/styles": "^5.0.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^12.0.0",
"@types/draft-js": "^0.11.10",
"@types/immutable": "^3.8.7",
"@types/jest": "^29.5.0",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@types/uuid": "^9.0.2",
VP-DS marked this conversation as resolved.
Show resolved Hide resolved
"draft-js": "^0.11.4",
"eslint": "^8.0.0",
"final-form": "^4.16.1",
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0",
"jest-junit": "^15.0.0",
VP-DS marked this conversation as resolved.
Show resolved Hide resolved
"npm-run-all": "^4.1.5",
"prettier": "^2.0.0",
"react": "^17.0",
"react-dom": "^17.0",
"react-final-form": "^6.3.1",
"react-intl": "^5.10.0",
"rimraf": "^3.0.2",
"ts-jest": "^29.0.0",
"typescript": "^4.0.0"
},
"peerDependencies": {
Expand Down
2 changes: 2 additions & 0 deletions packages/admin/admin-rte/src/core/Controls/Controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import ListsControls from "./ListsControls";
import ListsIndentControls from "./ListsIndentControls";
import SpecialCharactersControls from "./SpecialCharactersControls";
import Toolbar from "./Toolbar";
import TranslationControls from "./TranslationControls";

export default function Controls(p: IControlProps) {
const {
Expand All @@ -22,6 +23,7 @@ export default function Controls(p: IControlProps) {
{[
HistoryControls,
BlockTypesControls,
TranslationControls,
InlineStyleTypeControls,
ListsControls,
ListsIndentControls,
Expand Down
21 changes: 21 additions & 0 deletions packages/admin/admin-rte/src/core/Controls/TranslationControls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useContentTranslationService } from "@comet/admin";
import { ButtonGroup } from "@mui/material";
import * as React from "react";

import TranslationToolbarButton from "../translation/ToolbarButton";
import { IControlProps } from "../types";

function TranslationControls(props: IControlProps) {
const translationContext = useContentTranslationService();

if (translationContext.enabled) {
return (
<ButtonGroup>
<TranslationToolbarButton {...props} />
</ButtonGroup>
);
}
return null;
}

export default TranslationControls;
38 changes: 38 additions & 0 deletions packages/admin/admin-rte/src/core/translation/ToolbarButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { useContentTranslationService } from "@comet/admin";
import { Translate } from "@comet/admin-icons";
import Tooltip from "@mui/material/Tooltip";
johnnyomair marked this conversation as resolved.
Show resolved Hide resolved
import * as React from "react";
import { FormattedMessage } from "react-intl";

import ControlButton from "../Controls/ControlButton";
import { IControlProps } from "../types";
import { htmlToState } from "./htmlToState";
import { stateToHtml } from "./stateToHtml";

function ToolbarButton({ editorState, setEditorState, options }: IControlProps): React.ReactElement {
const translationContext = useContentTranslationService();

async function handleClick(event: React.MouseEvent) {
if (!translationContext) return;

event.preventDefault();

const { html, entities } = stateToHtml({ editorState, options });

const translation = await translationContext.translate(html);

const translatedEditorState = htmlToState({ html: translation, entities });

setEditorState(translatedEditorState);
}

return (
<Tooltip title={<FormattedMessage id="comet.rte.translation.buttonTooltip" defaultMessage="Translate" />} placement="top">
<span>
<ControlButton icon={Translate} onButtonClick={handleClick} />
</span>
</Tooltip>
);
}

export default ToolbarButton;
176 changes: 176 additions & 0 deletions packages/admin/admin-rte/src/core/translation/htmlToState.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/**
* @jest-environment jsdom
*/

import { convertFromRaw, EditorState, RawDraftContentState } from "draft-js";

import { IRteOptions } from "../Rte";
import { htmlToState } from "./htmlToState";
import { stateToHtml } from "./stateToHtml";

describe("htmlToState", () => {
const options = { customInlineStyles: { HIGHLIGHT: { label: "Highlight!", style: { backgroundColor: "yellow" } } } } as unknown as IRteOptions;

it("should convert html to state to html with the html staying the same", () => {
const blocks = [
// Basic stylings
{ key: "52cmg", text: "Normal Text", type: "unstyled", depth: 0, inlineStyleRanges: [], entityRanges: [], data: {} },
{
key: "8psic",
text: "Bold Text",
type: "unstyled",
depth: 0,
inlineStyleRanges: [{ offset: 0, length: 9, style: "BOLD" }],
entityRanges: [],
data: {},
},
{
key: "4m6ou",
text: "Italic Text",
type: "unstyled",
depth: 0,
inlineStyleRanges: [{ offset: 0, length: 11, style: "ITALIC" }],
entityRanges: [],
data: {},
},
{
key: "fask6",
text: "Bold Italic Text",
type: "unstyled",
depth: 0,
inlineStyleRanges: [
{ offset: 0, length: 16, style: "ITALIC" },
{ offset: 0, length: 16, style: "BOLD" },
],
entityRanges: [],
data: {},
},
{
key: "fm23u",
text: "Strikethrough Text",
type: "unstyled",
depth: 0,
inlineStyleRanges: [{ offset: 0, length: 18, style: "STRIKETHROUGH" }],
entityRanges: [],
data: {},
},
{
key: "9q8m5",
text: "A Subscript Text",
type: "unstyled",
depth: 0,
inlineStyleRanges: [{ offset: 2, length: 14, style: "SUB" }],
entityRanges: [],
data: {},
},
{
key: "t3nk",
text: "B Superscript Text",
type: "unstyled",
depth: 0,
inlineStyleRanges: [{ offset: 2, length: 16, style: "SUP" }],
entityRanges: [],
data: {},
},
{ key: "e6k04", text: "Headline 1", type: "header-one", depth: 0, inlineStyleRanges: [], entityRanges: [], data: {} },
{ key: "ect4f", text: "Headline 2", type: "header-two", depth: 0, inlineStyleRanges: [], entityRanges: [], data: {} },
{ key: "e038j", text: "Headline 3", type: "header-three", depth: 0, inlineStyleRanges: [], entityRanges: [], data: {} },
{ key: "4bha8", text: "Headline 4", type: "header-four", depth: 0, inlineStyleRanges: [], entityRanges: [], data: {} },
{ key: "aje6k", text: "Headline 5", type: "header-five", depth: 0, inlineStyleRanges: [], entityRanges: [], data: {} },
{ key: "7u6on", text: "Headline 6", type: "header-six", depth: 0, inlineStyleRanges: [], entityRanges: [], data: {} },
// Unordered List
{ key: "a9t3", text: "Unordered List", type: "unordered-list-item", depth: 0, inlineStyleRanges: [], entityRanges: [], data: {} },
{
key: "f4o2c",
text: "123456",
type: "unordered-list-item",
depth: 1,
inlineStyleRanges: [{ offset: 3, length: 3, style: "SUB" }],
entityRanges: [],
data: {},
},
{
key: "7v61p",
text: "234",
type: "unordered-list-item",
depth: 2,
inlineStyleRanges: [{ offset: 0, length: 3, style: "ITALIC" }],
entityRanges: [],
data: {},
},
{
key: "1duir",
text: "345",
type: "unordered-list-item",
depth: 2,
inlineStyleRanges: [{ offset: 0, length: 3, style: "BOLD" }],
entityRanges: [],
data: {},
},
// Ordered List
{ key: "1iahs", text: "List", type: "ordered-list-item", depth: 0, inlineStyleRanges: [], entityRanges: [], data: {} },
{
key: "aqjhb",
text: "123456",
type: "ordered-list-item",
depth: 1,
inlineStyleRanges: [{ offset: 3, length: 3, style: "SUP" }],
entityRanges: [],
data: {},
},
{
key: "c4js6",
text: "234",
type: "ordered-list-item",
depth: 2,
inlineStyleRanges: [{ offset: 0, length: 3, style: "ITALIC" }],
entityRanges: [],
data: {},
},
{
key: "3qjfc",
text: "345",
type: "ordered-list-item",
depth: 2,
inlineStyleRanges: [{ offset: 0, length: 3, style: "BOLD" }],
entityRanges: [],
data: {},
},
// Custom Style
{
key: "7l333",
text: "A rte text with custom styling",
type: "unstyled",
depth: 0,
inlineStyleRanges: [{ offset: 0, length: 30, style: "HIGHLIGHT" }],
entityRanges: [],
data: {},
},
];
const rawContent = {
entityMap: {},
blocks,
} as RawDraftContentState;

const content = convertFromRaw(rawContent);
const editorState = EditorState.createWithContent(content);

const { html, entities } = stateToHtml({
editorState,
options,
});

const state = htmlToState({
html: html,
entities,
});

const { html: html2, entities: linkDataList2 } = stateToHtml({
editorState: state,
options,
});

expect(html).toEqual(html2);
expect(entities).toEqual(linkDataList2);
});
});
42 changes: 42 additions & 0 deletions packages/admin/admin-rte/src/core/translation/htmlToState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { convertFromRaw, convertToRaw, EditorState } from "draft-js";
import { stateFromHTML } from "draft-js-import-html";

export function htmlToState({
johnnyomair marked this conversation as resolved.
Show resolved Hide resolved
html,
entities,
}: {
html: string;
entities: {
id: string;
data: any;
}[];
}) {
const translatedContentState = stateFromHTML(html, {
customInlineFn: (element, { Style, Entity }) => {
johnnyomair marked this conversation as resolved.
Show resolved Hide resolved
if (element.tagName === "SUB") {
return Style("SUB");
}
if (element.tagName === "SUP") {
return Style("SUP");
}
if (element.tagName == "SPAN") {
return Style((element.attributes as any).class.value);
}
if (element.tagName === "A") {
return Entity("LINK", { id: (element.attributes as any).id.value });
}
},
});

const { entityMap, blocks } = convertToRaw(translatedContentState);

for (const key of Object.keys(entityMap)) {
if ("id" in entityMap[key].data) {
entityMap[key].data = entities.find((item) => item.id == entityMap[key].data.id)?.data;
}
}

const translatedContentStateWithLinkData = convertFromRaw({ entityMap, blocks });

return EditorState.createWithContent(translatedContentStateWithLinkData);
}
Loading