Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions docs/how-to-guides/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ icons
configure-plate-code-block-languages
bind-metadata-fields-to-plate-text-blocks
custom-content-types
translate-content
```
65 changes: 65 additions & 0 deletions docs/how-to-guides/translate-content.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
---
myst:
html_meta:
"description": "How to create a translation of a content item in Plone Aurora while reading the original side by side"
"property=og:description": "How to create a translation of a content item in Plone Aurora while reading the original side by side"
"property=og:title": "Translate content"
"keywords": "Plone Aurora, translate, translation, multilingual, babel"
---

# Translate content

This guide shows you how to create a translation of a content item while reading the original side by side.

## Requirements

Your site must be multilingual.
Install the `plone.app.multilingual` add-on, and configure at least two site languages in the {guilabel}`Language` control panel.
The backend then provides one root folder per language, and Aurora places every translation in the folder of its language.

## Open the translation view

Open the translation view directly by URL.
Append the path of the item to translate to `@@translate`, and pass the target language as a query parameter.

```text
https://example.com/@@translate/<path-to-item>?language=<language-code>
```

For example, to translate the English page at `/en/welcome` to German, open the following URL.

```text
https://example.com/@@translate/en/welcome?language=de
```

If a translation in the target language already exists, the view redirects to the edit view of that translation.

```{todo}
Describe the user interface entry point for the translation view, once the toolbar provides a {guilabel}`Translate` action.
See [issue 21](https://github.com/plone/aurora/issues/21) and [issue 31](https://github.com/plone/aurora/issues/31).
```

## Translate the content

The view shows the original on the left, and the new translation on the right.
The {guilabel}`Blocks` and {guilabel}`Content` tabs switch both columns at the same time.

In the {guilabel}`Blocks` tab, the left column shows the rendered original, and the right column shows the block editor for the translation.
The translation starts with the same block structure as the original.

- Text blocks contain the original text.
Overwrite it with your translation.
- All other blocks, such as a teaser, appear empty at their original position.
Fill them as you would fill a manually added block.

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.
The fields start empty and show the values of the original as placeholders.
Enter the translated values, starting with the {guilabel}`Title`.

## Save the translation

Select the {guilabel}`Save` button in the toolbar.
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.

Fields that you leave empty stay empty on the translation.
The link between the original and the translation lets visitors switch between the two language versions of the item.
129 changes: 80 additions & 49 deletions packages/cmsui/components/ContentForm/ContentForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,19 @@ interface ContentFormProps {
schema: Schema;
heading: ReactNode;
submitMethod: 'post' | 'patch';
/** Per-tab sticky panels rendered left of the form (eg. translation source). */
asidePanels?: { header?: ReactNode; blocks?: ReactNode; content?: ReactNode };
/** Placeholder text per field name (eg. the source values of a translation). */
fieldPlaceholders?: Record<string, string>;
}

export default function ContentForm({
content,
schema,
heading,
submitMethod,
asidePanels,
fieldPlaceholders,
}: ContentFormProps) {
const { t } = useTranslation();
const fetcher = useFetcher();
Expand All @@ -64,6 +70,68 @@ export default function ContentForm({
},
});

const fieldsetsForm = (
<form>
{schema.fieldsets.map((fieldset) => (
<Accordion defaultExpandedKeys={['default']} key={fieldset.id}>
<AccordionItem id={fieldset.id} key={fieldset.id}>
<AccordionItemTrigger>{fieldset.title}</AccordionItemTrigger>
<AccordionPanel>
{(fieldset.fields as DeepKeys<Content>[]).map(
(schemaField, index) => (
<form.AppField
name={schemaField}
key={index}
// eslint-disable-next-line react/no-children-prop
children={(field) => (
<field.Quanta
{...schema.properties[schemaField]}
className="mb-4"
label={schema.properties[field.name].title}
name={field.name}
defaultValue={field.state.value}
required={schema.required.indexOf(schemaField) !== -1}
error={field.state.meta.errors}
formAtom={formAtom}
value={field.state.value}
{...(fieldPlaceholders?.[field.name]
? { placeholder: fieldPlaceholders[field.name] }
: {})}
/>
)}
/>
),
)}
</AccordionPanel>
</AccordionItem>
</Accordion>
))}
</form>
);

// The shared header row keeps both columns starting level; px-6 mirrors the
// blocks editor's own gutters, padBody pads bodies that lack them.
const withAside = (panel: ReactNode, body: ReactNode, padBody = false) =>
asidePanels ? (
<div
className={`
grid grid-cols-1 gap-x-8 pt-4
lg:grid-cols-2 lg:grid-rows-[auto_1fr]
`}
>
<div className="mb-4 self-end px-6">{asidePanels.header}</div>
<h1 className="mb-4 self-end px-6 text-2xl font-bold">{heading}</h1>
<aside className="sticky top-0 max-h-screen self-start overflow-y-auto px-6">
{panel}
</aside>
<div className={padBody ? 'flex flex-col px-6' : 'flex flex-col'}>
{body}
</div>
</div>
) : (
body
);

return (
<Provider store={store}>
<InitAtoms atomValues={[[formAtom, content]]}>
Expand All @@ -86,59 +154,22 @@ export default function ContentForm({
{
id: 'blocks',
title: t('cmsui.blocksEditor.blocksTab'),
content: <BlocksEditor />,
content: withAside(asidePanels?.blocks, <BlocksEditor />),
},
{
id: 'content',
title: t('cmsui.blocksEditor.contentTab'),
content: (
<div className="flex flex-col">
<h1 className="mb-4 text-2xl font-bold">{heading}</h1>
<form>
{schema.fieldsets.map((fieldset) => (
<Accordion
defaultExpandedKeys={['default']}
key={fieldset.id}
>
<AccordionItem id={fieldset.id} key={fieldset.id}>
<AccordionItemTrigger>
{fieldset.title}
</AccordionItemTrigger>
<AccordionPanel>
{(fieldset.fields as DeepKeys<Content>[]).map(
(schemaField, index) => (
<form.AppField
name={schemaField}
key={index}
// eslint-disable-next-line react/no-children-prop
children={(field) => (
<field.Quanta
{...schema.properties[schemaField]}
className="mb-4"
label={
schema.properties[field.name].title
}
name={field.name}
defaultValue={field.state.value}
required={
schema.required.indexOf(
schemaField,
) !== -1
}
error={field.state.meta.errors}
formAtom={formAtom}
value={field.state.value}
/>
)}
/>
),
)}
</AccordionPanel>
</AccordionItem>
</Accordion>
))}
</form>
</div>
content: withAside(
asidePanels?.content,
asidePanels ? (
fieldsetsForm
) : (
<div className="flex flex-col">
<h1 className="mb-4 text-2xl font-bold">{heading}</h1>
{fieldsetsForm}
</div>
),
true,
),
},
]}
Expand Down
11 changes: 11 additions & 0 deletions packages/cmsui/config/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,17 @@ export default function install(config: ConfigType) {
},
],
},
{
type: 'prefix',
path: '@@translate',
children: [
{
type: 'route',
path: '*',
file: '@plone/cmsui/routes/translate.tsx',
},
],
},
{
type: 'prefix',
path: 'controlpanel',
Expand Down
5 changes: 5 additions & 0 deletions packages/cmsui/locales/de/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@
"groupMembership": "Gruppenmitgliedschaft",
"groups": "Gruppen"
},
"translate": {
"source": "Quelle",
"view_source": "Quellinhalt ansehen",
"heading_new": "\"{{title}}\" nach {{language}} übersetzen"
},
"blocksEditor": {
"blocksTab": "Blöcke",
"contentTab": "Inhalt"
Expand Down
5 changes: 5 additions & 0 deletions packages/cmsui/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@
"groupMembership": "Group Membership",
"groups": "Groups"
},
"translate": {
"source": "Source",
"view_source": "View source content",
"heading_new": "Translate \"{{title}}\" to {{language}}"
},
"blocksEditor": {
"blocksTab": "Blocks",
"contentTab": "Content"
Expand Down
5 changes: 5 additions & 0 deletions packages/cmsui/locales/it/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@
"groupMembership": "Appartenenza ai gruppi",
"groups": "Gruppi"
},
"translate": {
"source": "Origine",
"view_source": "Visualizza il contenuto di origine",
"heading_new": "Traduci \"{{title}}\" in {{language}}"
},
"blocksEditor": {
"blocksTab": "Blocchi",
"contentTab": "Contenuto"
Expand Down
1 change: 1 addition & 0 deletions packages/cmsui/news/+translate-route.feature
Original file line number Diff line number Diff line change
@@ -0,0 +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
Loading
Loading