---
title: "Translation synchronisation"
manual: "Academic Profiles"
version: "main"
permalink: "https://docs.typo3.org/permalink/fgtclb/academic-persons:developers-translation-synchronisation@main"
source: "Developers/TranslationSynchronisation.rst"
rendered: "2026-10-15T10:24:29+00:00"
---

# Translation synchronisation {#developers-translation-synchronisation}

> [!WARNING]
> **The translation synchronisation is experimental.**
> `RecordSynchronizerInterface` and `RecordSynchronizer` are marked
> `@internal`, and the rest of this page, `SynchronizerContext`
> among it, is to be treated the same way. It works and is covered by
> functional tests, but signatures may still change in a minor release.
> Depend on it deliberately. The events are public API.

[`fgtclb/academic-persons-edit`](https://packagist.org/packages/fgtclb/academic-persons-edit) keeps the translations of a profile
in step with its default language record. Its listener of
[AfterProfileUpdateEvent](https://docs.typo3.org/permalink/fgtclb/academic-persons:developers-event-after-profile-update-event@main) calls the synchronizer of
this extension for the languages its `profile.allowedLanguages` setting
allows.

## The synchronisation surface {#developers-synchronisation}

`\FGTCLB\AcademicPersons\Service\RecordSynchronizerInterface` declares one
method, `synchronize(SynchronizerContext $context)`. The shipped
implementation (`RecordSynchronizer`) routes **every write through the
TYPO3 DataHandler**. Nothing in TYPO3 outside the DataHandler honours
`l10n_mode=exclude` or keeps translations consistent, so going through it
is what makes the created translations indistinguishable from ones created in
the backend: inline children, file references, MM relations,
`l10n_diffsource`, reference index, history and hooks are all carried along.

`\FGTCLB\AcademicPersons\Domain\Model\Dto\Synchronizer\SynchronizerContext`
describes one synchronisation run. Build it through
`SynchronizerContext::create()`, which takes the synchronizer instance,
the `Site`, the allowed language ids, the table name and the record uid,
and silently drops language ids that are not positive or that the site does not
define, so a run never targets a language the site cannot render.

For each remaining language, `synchronize()`:

-   creates a missing translation with a DataHandler `localize` command,
    the full record, including its inline child tree,
-   for an existing translation, re-submits the default record's
    `l10n_mode=exclude` column values as a datamap (core's
    `DataMapProcessor` propagates them into every translation) and issues an
    `inlineLocalizeSynchronize` command per inline column, which carries
    child records added to the default record after the translation was
    created.

A missing record, a record that is not in the default language, or a record
that is invisible in the acting workspace makes the run a silent no-op.

## Workspace behaviour {#developers-workspaces}

The synchronisation acts **in the workspace of the acting backend user**: run
from a backend context inside a workspace, it creates versioned rows only
(`t3ver_wsid` set, `t3ver_state=1`) and never touches the live records.
Publishing the workspace publishes the translations. When no backend user is
available (frontend and CLI contexts), a synthetic in-memory admin user acting
in the workspace of the current `Context` is used.

Two refusals protect the live state:

-   A **frontend request acting in a non-live workspace** (a workspace preview)
    is refused entirely. A notice is logged and nothing is written. This
    policy is currently hardcoded.
-   A uid addressing a **workspace version row** (`t3ver_oid > 0`) is
    refused: the DataHandler addresses versioned records through their live
    uid, and accepting the version uid would publish draft values as live
    translations.
