-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1725 from vivid-planet/feature/translation-module
[Feature] Content translation
- Loading branch information
Showing
17 changed files
with
1,274 additions
and
493 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
--- | ||
"@comet/admin": minor | ||
--- | ||
|
||
Add basis for content translation | ||
|
||
Wrap a component with a `ContentTranslationServiceProvider` to add support for content translation to all underlying `FinalFormInput` inputs. | ||
|
||
```tsx | ||
<ContentTranslationServiceProvider | ||
enabled={true} | ||
translate={async function (text: string): Promise<string> { | ||
return yourTranslationFnc(text); | ||
}} | ||
> | ||
... | ||
</ContentTranslationServiceProvider> | ||
``` | ||
|
||
You can disable translation for a specific `FinalFormInput` by using the `disableContentTranslation` prop. | ||
|
||
```diff | ||
<Field | ||
required | ||
fullWidth | ||
name="myField" | ||
component={FinalFormInput} | ||
label={<FormattedMessage id="myField" defaultMessage="My Field" />} | ||
+ disableContentTranslation | ||
/> | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@comet/admin-rte": minor | ||
--- | ||
|
||
Add support for content translation |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
21 changes: 21 additions & 0 deletions
21
packages/admin/admin-rte/src/core/Controls/TranslationControls.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
37 changes: 37 additions & 0 deletions
37
packages/admin/admin-rte/src/core/translation/ToolbarButton.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import { Tooltip, useContentTranslationService } from "@comet/admin"; | ||
import { Translate } from "@comet/admin-icons"; | ||
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
176
packages/admin/admin-rte/src/core/translation/htmlToState.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
42
packages/admin/admin-rte/src/core/translation/htmlToState.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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({ | ||
html, | ||
entities, | ||
}: { | ||
html: string; | ||
entities: { | ||
id: string; | ||
data: any; | ||
}[]; | ||
}) { | ||
const translatedContentState = stateFromHTML(html, { | ||
customInlineFn: (element, { Style, Entity }) => { | ||
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); | ||
} |
Oops, something went wrong.