Syncing
Read new and changed messages from a source, from the backoffice or a deployment pipeline, and tell your application when anything changes.
Syncing a source
Umbraco reads a source when someone presses Sync now in Settings > App translations. History next to it lists every sync, with what it added, changed and removed, or why it failed. Umbraco can also sync on a schedule, and a deployment can sync through the endpoint below.
Syncing on a schedule
To pick up new messages without anyone pressing a button, choose how often under Sync automatically in the source’s settings: every hour, every 6 or 12 hours, every day or every week. Umbraco then syncs the source once its last sync is that old, whether that sync was started from the backoffice, a deployment or the schedule. The source shows when it syncs next.
- A failed sync counts too, so a source that is down is tried again at the next interval rather than every few minutes. History says why it failed.
- A sync that finds nothing new changes nothing and is not announced, so a short interval costs your application nothing.
- In a load-balanced setup, only the Single or SchedulingPublisher server runs the schedule.
A deployment that syncs as it ships new translation files is still the quickest way to get them in. The schedule catches the files that change some other way.
What a sync changes
A sync compares the files with what Umbraco has and applies the difference. Custom text is never lost.
- A new key arrives with the application’s text.
- Changed default text replaces the old default. Custom text written for that message stays, marked Default changed so an editor can check it still fits.
- A key the files no longer have is deleted if nobody wrote custom text for it. If somebody did, the message stays as No longer used, with its custom text, but the delivery endpoints stop serving it. If the key comes back, so does the text.
- Text written for a language the source does not supply is left alone. See Languages your application does not ship.
A sync that reads exactly what Umbraco already has changes nothing, and is not announced.
Syncing from a deployment
A deployment can sync a source by its identifier, without a backoffice login. The endpoint is off until you give your pipeline a way in: a secret, or the OIDC token its platform already issues. You can set up both and move pipelines over one at a time.
Have the pipeline call the endpoint once the new translation files are live. Replace website with the source identifier from the source’s Advanced settings. The request waits for the sync to finish and returns how many messages it added, changed and removed.
The API reference lists what it returns, and every status code.
With a secret
Configure a long random value. Anyone who has it can start a sync.
AppTranslations__DeploymentSync__ApiKey=replace-with-a-long-random-value
Give your deployment pipeline the same secret, and send it as a bearer token.
curl --fail-with-body \
--request POST \
--header "Authorization: Bearer $UMBRACO_TRANSLATIONS_SYNC_KEY" \
"$UMBRACO_URL/umbraco/app-translations/api/v1/sources/website/sync"
With an OIDC token
GitHub Actions, GitLab CI and the big cloud platforms can give a job a short-lived token that proves which repository or workload it came from. Send that token instead of a secret, and there is nothing to store or rotate.
App Translations doesn’t check the token itself. Instead, you register an ASP.NET Core authorization policy under the name TranslationPolicies.DeploymentSync, and any request that passes the policy can start a sync. API OIDC, a separate package, checks the tokens. To set it up for GitHub Actions:
-
Install API OIDC, version 0.2.3 or later, and trust your repository’s tokens, as its Issuers page describes:
{ "ApiOidc": { "Issuers": [ { "Preset": "GitHubActions", "Owner": "acme", "Repositories": ["acme/site"] } ] } } -
Register the policy in
Program.cs. API OIDC’sRequireOidcTokenlets in a token it accepted from the issuers you name:using TheBuilder.ApiOidc.Authentication; using TheBuilder.AppTranslations.Authorization; builder.Services.AddAuthorization(options => options.AddPolicy( TranslationPolicies.DeploymentSync, policy => policy.RequireOidcToken("GitHubActions")));GitHubActionsis the issuer’sNamein API OIDC, which is the preset’s name unless you set another. Checking it matters when API OIDC also trusts your frontends: their tokens are valid too, and get a403here. -
In the workflow, get a token and send it in the
X-Oidc-Tokenheader:jobs: deploy: runs-on: ubuntu-latest permissions: id-token: write env: UMBRACO_URL: ${{ vars.UMBRACO_URL }} steps: # Deploy the new translation files first. - name: Sync translations run: | token=$(curl --silent --fail \ --header "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \ "$ACTIONS_ID_TOKEN_REQUEST_URL" | jq --raw-output .value) curl --fail-with-body \ --request POST \ --header "X-Oidc-Token: $token" \ "$UMBRACO_URL/umbraco/app-translations/api/v1/sources/website/sync"The token is made for GitHub’s default audience, which is what the
GitHubActionspreset expects. If you gave the issuer your ownAudience, add it to the token request:"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https://cms.example.com".
Writing your own policy
The policy can check anything ASP.NET Core can. Two rules keep it safe:
- It must read a token the caller sends. Name the authentication scheme that reads it:
RequireOidcTokendoes that for you, and withAddAuthenticationSchemesyou name it yourself. A policy with no scheme, or with a cookie scheme, would let in whoever is signed in to the browser, so a web page an editor opens could start a sync. The site won’t start with a policy like that. - It alone decides who gets in. If it lets anonymous requests through, anyone can start a sync. For example, don’t build it from API OIDC’s
TheBuilder.Oidc.DeliveryApiAccesspolicy: while the Delivery API is public, that lets everyone in.
Telling your application something changed
To drop a cached dictionary the moment it changes, add a webhook in Settings > Webhooks for the App translations updated event (under Other). Umbraco handles the URL, headers, retries and the request log as it does for any webhook.
The event fires once for every change that reaches the delivery endpoints.
change |
When |
|---|---|
Synchronized |
A sync added, changed or removed at least one message. |
OverrideSaved |
An editor saved custom text. |
OverrideRemoved |
An editor reverted custom text to the default. |
SourceDeleted |
Someone deleted a source, and everything it provided. |
{
"sourceId": "6f2f5d3e-1f4a-4a1e-9a5b-2c7d8e9f0a1b",
"sourceAlias": "website",
"change": "OverrideSaved",
"occurredAt": "2026-08-26T09:30:00+00:00",
"message": { "namespace": "website", "key": "cart.title", "locale": "da-DK" },
"synchronization": null
}
For a saved or reverted message, message names it. Its locale and namespace tell you exactly which dictionary to purge. For a sync, synchronization holds the revision and the added, changed and removed counts. Umbraco does not announce a sync that found nothing new.
Umbraco sends webhooks only when Umbraco:CMS:Webhook:Enabled is on, which it is by default. In a load-balanced setup, Umbraco sends webhooks only from the Single or SchedulingPublisher server.