From 7f6a18d9d4b87d1e069ef76de3023a1c3d71ba83 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Nils=20Pl=C3=BCtzer?=
Date: Thu, 11 Jun 2026 15:26:47 +0200
Subject: [PATCH 1/2] Add `@@translate` route for creating and editing content
translations
---
docs/how-to-guides/index.md | 1 +
docs/how-to-guides/translate-content.md | 65 ++++
.../components/ContentForm/ContentForm.tsx | 129 ++++---
packages/cmsui/config/routes.ts | 11 +
packages/cmsui/locales/de/common.json | 5 +
packages/cmsui/locales/en/common.json | 5 +
packages/cmsui/locales/it/common.json | 5 +
packages/cmsui/news/+translate-route.feature | 1 +
packages/cmsui/routes/translate.test.tsx | 290 +++++++++++++++
packages/cmsui/routes/translate.tsx | 337 ++++++++++++++++++
10 files changed, 800 insertions(+), 49 deletions(-)
create mode 100644 docs/how-to-guides/translate-content.md
create mode 100644 packages/cmsui/news/+translate-route.feature
create mode 100644 packages/cmsui/routes/translate.test.tsx
create mode 100644 packages/cmsui/routes/translate.tsx
diff --git a/docs/how-to-guides/index.md b/docs/how-to-guides/index.md
index b42b76e4c..0cce268c6 100644
--- a/docs/how-to-guides/index.md
+++ b/docs/how-to-guides/index.md
@@ -30,4 +30,5 @@ icons
configure-plate-code-block-languages
bind-metadata-fields-to-plate-text-blocks
custom-content-types
+translate-content
```
diff --git a/docs/how-to-guides/translate-content.md b/docs/how-to-guides/translate-content.md
new file mode 100644
index 000000000..f621195ad
--- /dev/null
+++ b/docs/how-to-guides/translate-content.md
@@ -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/?language=
+```
+
+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.
diff --git a/packages/cmsui/components/ContentForm/ContentForm.tsx b/packages/cmsui/components/ContentForm/ContentForm.tsx
index 7de5f8a3c..cbf307bd7 100644
--- a/packages/cmsui/components/ContentForm/ContentForm.tsx
+++ b/packages/cmsui/components/ContentForm/ContentForm.tsx
@@ -40,6 +40,10 @@ 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;
}
export default function ContentForm({
@@ -47,6 +51,8 @@ export default function ContentForm({
schema,
heading,
submitMethod,
+ asidePanels,
+ fieldPlaceholders,
}: ContentFormProps) {
const { t } = useTranslation();
const fetcher = useFetcher();
@@ -64,6 +70,68 @@ export default function ContentForm({
},
});
+ const fieldsetsForm = (
+
+ );
+
+ // 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 ? (
+
+ ),
+ }}
+ heading={t('cmsui.translate.heading_new', {
+ title: source.title,
+ language: targetLanguage,
+ })}
+ submitMethod="post"
+ />
+ );
+}
+
+function formatValue(value: unknown): string {
+ if (value == null || value === '') return '';
+ if (typeof value === 'boolean') return value ? '✓' : '';
+ if (Array.isArray(value)) {
+ return value
+ .map((item) => formatValue(item))
+ .filter(Boolean)
+ .join(', ');
+ }
+ if (typeof value === 'object') {
+ const record = value as Record;
+ return String(record.title ?? record.token ?? '');
+ }
+ return String(value);
+}
From 4eea35c30ae3b665746601c19646e20a2952493b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Nils=20Pl=C3=BCtzer?=
Date: Fri, 12 Jun 2026 13:30:56 +0200
Subject: [PATCH 2/2] Add `@@manage-translations` route for managing content
translations
---
docs/how-to-guides/index.md | 1 +
docs/how-to-guides/manage-translations.md | 59 +++
.../ObjectBrowserModal.test.tsx | 44 ++-
.../ObjectBrowserModal.tsx | 122 +++---
packages/cmsui/config/routes.ts | 11 +
packages/cmsui/locales/de/common.json | 14 +
packages/cmsui/locales/en/common.json | 14 +
packages/cmsui/locales/it/common.json | 14 +
packages/cmsui/routes/layout.tsx | 5 +
.../cmsui/routes/manageTranslations.test.tsx | 363 ++++++++++++++++++
packages/cmsui/routes/manageTranslations.tsx | 362 +++++++++++++++++
packages/cmsui/styles/cmsui.css | 1 +
12 files changed, 941 insertions(+), 69 deletions(-)
create mode 100644 docs/how-to-guides/manage-translations.md
create mode 100644 packages/cmsui/routes/manageTranslations.test.tsx
create mode 100644 packages/cmsui/routes/manageTranslations.tsx
diff --git a/docs/how-to-guides/index.md b/docs/how-to-guides/index.md
index 0cce268c6..8087489e7 100644
--- a/docs/how-to-guides/index.md
+++ b/docs/how-to-guides/index.md
@@ -31,4 +31,5 @@ configure-plate-code-block-languages
bind-metadata-fields-to-plate-text-blocks
custom-content-types
translate-content
+manage-translations
```
diff --git a/docs/how-to-guides/manage-translations.md b/docs/how-to-guides/manage-translations.md
new file mode 100644
index 000000000..2edc84b70
--- /dev/null
+++ b/docs/how-to-guides/manage-translations.md
@@ -0,0 +1,59 @@
+---
+myst:
+ html_meta:
+ "description": "How to link, unlink, and create translations of a content item in the Plone Aurora manage translations view"
+ "property=og:description": "How to link, unlink, and create translations of a content item in the Plone Aurora manage translations view"
+ "property=og:title": "Manage translations"
+ "keywords": "Plone Aurora, translations, multilingual, language, link, unlink"
+---
+
+# Manage translations
+
+This guide shows you how to link content items in different languages together as translations of each other, how to unlink them, and how to create a missing translation.
+Translations work on a multilingual site, where each language has its own root folder, such as `/en` and `/de`.
+Refer to {doc}`/how-to-guides/translate-content` for how to configure a multilingual site.
+
+## Open the manage translations view
+
+Log in to your site, then navigate to `https:///@@manage-translations/`, where `` is the path of the item whose translations you want to manage.
+For example, to manage the translations of the page at `/en/my-page`, navigate to `https:///@@manage-translations/en/my-page`.
+
+The page heading {guilabel}`Manage translations for ""` confirms which item you're working on.
+Below it, a table with the columns {guilabel}`Language`, {guilabel}`Path`, and {guilabel}`Tools` lists one row for each language of the site.
+The row of the item's own language appears in bold and offers no tools.
+Every other row shows either the path of the linked translation or {guilabel}`No translation`.
+Select a path in the {guilabel}`Path` column to open that item.
+To leave the view, select the {guilabel}`Back` button in the toolbar.
+
+```{note}
+The view works only for content inside a language folder.
+For content outside a language folder, such as the site root, the view responds with an {guilabel}`Error 400` page instead.
+```
+
+## Link an existing translation
+
+If the translation already exists as an item on the site, link it.
+
+1. In the row of the language, select the {guilabel}`+` button, labeled {guilabel}`Link an existing translation`.
+2. In the object browser, choose the item to link as the translation.
+
+The object browser starts at the root folder of that language.
+To find an item in another folder, select the {guilabel}`Search content` button, and search the whole site.
+A toast {guilabel}`Translation linked` confirms the link, and the row now shows the path of the linked item.
+
+If the server rejects the link, for example when the chosen item has the same language as the current item, an error toast {guilabel}`Translation update failed` shows the reason.
+
+## Create a translation
+
+If the translation doesn't exist yet, create it.
+
+In the row of the language, select the language icon, labeled {guilabel}`Create the translation`.
+This opens the translation view for that language, with the original next to your translation.
+Refer to {doc}`/how-to-guides/translate-content` for how to work in the translation view.
+
+## Unlink a translation
+
+To unlink a translation, select the chain icon in its row, labeled {guilabel}`Unlink the translation`.
+Aurora removes the link between the two items immediately and confirms it with a toast {guilabel}`Translation unlinked`.
+Unlinking doesn't delete any content.
+Both items stay on the site, but they're no longer connected as translations of each other.
diff --git a/packages/cmsui/components/ObjectBrowserWidget/ObjectBrowserModal.test.tsx b/packages/cmsui/components/ObjectBrowserWidget/ObjectBrowserModal.test.tsx
index 9ab9449b9..11fe83945 100644
--- a/packages/cmsui/components/ObjectBrowserWidget/ObjectBrowserModal.test.tsx
+++ b/packages/cmsui/components/ObjectBrowserWidget/ObjectBrowserModal.test.tsx
@@ -43,14 +43,9 @@ vi.mock('react-i18next', () => ({
}));
// Mock dei componenti
-vi.mock('@plone/components', () => ({
- Modal: ({ children, isOpen, onOpenChange, className, ...props }: any) => (
-