Skip to content

Commit 7f6a18d

Browse files
committed
Add @@translate route for creating and editing content translations
1 parent 492471e commit 7f6a18d

10 files changed

Lines changed: 800 additions & 49 deletions

File tree

docs/how-to-guides/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,5 @@ icons
3030
configure-plate-code-block-languages
3131
bind-metadata-fields-to-plate-text-blocks
3232
custom-content-types
33+
translate-content
3334
```
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
---
2+
myst:
3+
html_meta:
4+
"description": "How to create a translation of a content item in Plone Aurora while reading the original side by side"
5+
"property=og:description": "How to create a translation of a content item in Plone Aurora while reading the original side by side"
6+
"property=og:title": "Translate content"
7+
"keywords": "Plone Aurora, translate, translation, multilingual, babel"
8+
---
9+
10+
# Translate content
11+
12+
This guide shows you how to create a translation of a content item while reading the original side by side.
13+
14+
## Requirements
15+
16+
Your site must be multilingual.
17+
Install the `plone.app.multilingual` add-on, and configure at least two site languages in the {guilabel}`Language` control panel.
18+
The backend then provides one root folder per language, and Aurora places every translation in the folder of its language.
19+
20+
## Open the translation view
21+
22+
Open the translation view directly by URL.
23+
Append the path of the item to translate to `@@translate`, and pass the target language as a query parameter.
24+
25+
```text
26+
https://example.com/@@translate/<path-to-item>?language=<language-code>
27+
```
28+
29+
For example, to translate the English page at `/en/welcome` to German, open the following URL.
30+
31+
```text
32+
https://example.com/@@translate/en/welcome?language=de
33+
```
34+
35+
If a translation in the target language already exists, the view redirects to the edit view of that translation.
36+
37+
```{todo}
38+
Describe the user interface entry point for the translation view, once the toolbar provides a {guilabel}`Translate` action.
39+
See [issue 21](https://github.com/plone/aurora/issues/21) and [issue 31](https://github.com/plone/aurora/issues/31).
40+
```
41+
42+
## Translate the content
43+
44+
The view shows the original on the left, and the new translation on the right.
45+
The {guilabel}`Blocks` and {guilabel}`Content` tabs switch both columns at the same time.
46+
47+
In the {guilabel}`Blocks` tab, the left column shows the rendered original, and the right column shows the block editor for the translation.
48+
The translation starts with the same block structure as the original.
49+
50+
- Text blocks contain the original text.
51+
Overwrite it with your translation.
52+
- All other blocks, such as a teaser, appear empty at their original position.
53+
Fill them as you would fill a manually added block.
54+
55+
In the {guilabel}`Content` tab, the left column shows the field values of the original, and the right column shows the fields of the translation.
56+
The fields start empty and show the values of the original as placeholders.
57+
Enter the translated values, starting with the {guilabel}`Title`.
58+
59+
## Save the translation
60+
61+
Select the {guilabel}`Save` button in the toolbar.
62+
Aurora creates the translation in the root folder of the target language, links it to the original, and opens the edit view of the new translation.
63+
64+
Fields that you leave empty stay empty on the translation.
65+
The link between the original and the translation lets visitors switch between the two language versions of the item.

packages/cmsui/components/ContentForm/ContentForm.tsx

Lines changed: 80 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,19 @@ interface ContentFormProps {
4040
schema: Schema;
4141
heading: ReactNode;
4242
submitMethod: 'post' | 'patch';
43+
/** Per-tab sticky panels rendered left of the form (eg. translation source). */
44+
asidePanels?: { header?: ReactNode; blocks?: ReactNode; content?: ReactNode };
45+
/** Placeholder text per field name (eg. the source values of a translation). */
46+
fieldPlaceholders?: Record<string, string>;
4347
}
4448

4549
export default function ContentForm({
4650
content,
4751
schema,
4852
heading,
4953
submitMethod,
54+
asidePanels,
55+
fieldPlaceholders,
5056
}: ContentFormProps) {
5157
const { t } = useTranslation();
5258
const fetcher = useFetcher();
@@ -64,6 +70,68 @@ export default function ContentForm({
6470
},
6571
});
6672

73+
const fieldsetsForm = (
74+
<form>
75+
{schema.fieldsets.map((fieldset) => (
76+
<Accordion defaultExpandedKeys={['default']} key={fieldset.id}>
77+
<AccordionItem id={fieldset.id} key={fieldset.id}>
78+
<AccordionItemTrigger>{fieldset.title}</AccordionItemTrigger>
79+
<AccordionPanel>
80+
{(fieldset.fields as DeepKeys<Content>[]).map(
81+
(schemaField, index) => (
82+
<form.AppField
83+
name={schemaField}
84+
key={index}
85+
// eslint-disable-next-line react/no-children-prop
86+
children={(field) => (
87+
<field.Quanta
88+
{...schema.properties[schemaField]}
89+
className="mb-4"
90+
label={schema.properties[field.name].title}
91+
name={field.name}
92+
defaultValue={field.state.value}
93+
required={schema.required.indexOf(schemaField) !== -1}
94+
error={field.state.meta.errors}
95+
formAtom={formAtom}
96+
value={field.state.value}
97+
{...(fieldPlaceholders?.[field.name]
98+
? { placeholder: fieldPlaceholders[field.name] }
99+
: {})}
100+
/>
101+
)}
102+
/>
103+
),
104+
)}
105+
</AccordionPanel>
106+
</AccordionItem>
107+
</Accordion>
108+
))}
109+
</form>
110+
);
111+
112+
// The shared header row keeps both columns starting level; px-6 mirrors the
113+
// blocks editor's own gutters, padBody pads bodies that lack them.
114+
const withAside = (panel: ReactNode, body: ReactNode, padBody = false) =>
115+
asidePanels ? (
116+
<div
117+
className={`
118+
grid grid-cols-1 gap-x-8 pt-4
119+
lg:grid-cols-2 lg:grid-rows-[auto_1fr]
120+
`}
121+
>
122+
<div className="mb-4 self-end px-6">{asidePanels.header}</div>
123+
<h1 className="mb-4 self-end px-6 text-2xl font-bold">{heading}</h1>
124+
<aside className="sticky top-0 max-h-screen self-start overflow-y-auto px-6">
125+
{panel}
126+
</aside>
127+
<div className={padBody ? 'flex flex-col px-6' : 'flex flex-col'}>
128+
{body}
129+
</div>
130+
</div>
131+
) : (
132+
body
133+
);
134+
67135
return (
68136
<Provider store={store}>
69137
<InitAtoms atomValues={[[formAtom, content]]}>
@@ -86,59 +154,22 @@ export default function ContentForm({
86154
{
87155
id: 'blocks',
88156
title: t('cmsui.blocksEditor.blocksTab'),
89-
content: <BlocksEditor />,
157+
content: withAside(asidePanels?.blocks, <BlocksEditor />),
90158
},
91159
{
92160
id: 'content',
93161
title: t('cmsui.blocksEditor.contentTab'),
94-
content: (
95-
<div className="flex flex-col">
96-
<h1 className="mb-4 text-2xl font-bold">{heading}</h1>
97-
<form>
98-
{schema.fieldsets.map((fieldset) => (
99-
<Accordion
100-
defaultExpandedKeys={['default']}
101-
key={fieldset.id}
102-
>
103-
<AccordionItem id={fieldset.id} key={fieldset.id}>
104-
<AccordionItemTrigger>
105-
{fieldset.title}
106-
</AccordionItemTrigger>
107-
<AccordionPanel>
108-
{(fieldset.fields as DeepKeys<Content>[]).map(
109-
(schemaField, index) => (
110-
<form.AppField
111-
name={schemaField}
112-
key={index}
113-
// eslint-disable-next-line react/no-children-prop
114-
children={(field) => (
115-
<field.Quanta
116-
{...schema.properties[schemaField]}
117-
className="mb-4"
118-
label={
119-
schema.properties[field.name].title
120-
}
121-
name={field.name}
122-
defaultValue={field.state.value}
123-
required={
124-
schema.required.indexOf(
125-
schemaField,
126-
) !== -1
127-
}
128-
error={field.state.meta.errors}
129-
formAtom={formAtom}
130-
value={field.state.value}
131-
/>
132-
)}
133-
/>
134-
),
135-
)}
136-
</AccordionPanel>
137-
</AccordionItem>
138-
</Accordion>
139-
))}
140-
</form>
141-
</div>
162+
content: withAside(
163+
asidePanels?.content,
164+
asidePanels ? (
165+
fieldsetsForm
166+
) : (
167+
<div className="flex flex-col">
168+
<h1 className="mb-4 text-2xl font-bold">{heading}</h1>
169+
{fieldsetsForm}
170+
</div>
171+
),
172+
true,
142173
),
143174
},
144175
]}

packages/cmsui/config/routes.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,17 @@ export default function install(config: ConfigType) {
4949
},
5050
],
5151
},
52+
{
53+
type: 'prefix',
54+
path: '@@translate',
55+
children: [
56+
{
57+
type: 'route',
58+
path: '*',
59+
file: '@plone/cmsui/routes/translate.tsx',
60+
},
61+
],
62+
},
5263
{
5364
type: 'prefix',
5465
path: 'controlpanel',

packages/cmsui/locales/de/common.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@
5656
"groupMembership": "Gruppenmitgliedschaft",
5757
"groups": "Gruppen"
5858
},
59+
"translate": {
60+
"source": "Quelle",
61+
"view_source": "Quellinhalt ansehen",
62+
"heading_new": "\"{{title}}\" nach {{language}} übersetzen"
63+
},
5964
"blocksEditor": {
6065
"blocksTab": "Blöcke",
6166
"contentTab": "Inhalt"

packages/cmsui/locales/en/common.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,11 @@
104104
"groupMembership": "Group Membership",
105105
"groups": "Groups"
106106
},
107+
"translate": {
108+
"source": "Source",
109+
"view_source": "View source content",
110+
"heading_new": "Translate \"{{title}}\" to {{language}}"
111+
},
107112
"blocksEditor": {
108113
"blocksTab": "Blocks",
109114
"contentTab": "Content"

packages/cmsui/locales/it/common.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@
9494
"groupMembership": "Appartenenza ai gruppi",
9595
"groups": "Gruppi"
9696
},
97+
"translate": {
98+
"source": "Origine",
99+
"view_source": "Visualizza il contenuto di origine",
100+
"heading_new": "Traduci \"{{title}}\" in {{language}}"
101+
},
97102
"blocksEditor": {
98103
"blocksTab": "Blocchi",
99104
"contentTab": "Contenuto"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add `@@translate` route to create and edit translations of a content item. The form is pre-filled with the source content (or the existing translation), and saving creates the translation in the target language root folder and links it to the source. @nils-pzr

0 commit comments

Comments
 (0)