
# Translation Studio — reference

> Load this file when: evaluating, demoing or starting to use **Translation Studio** — translating a HubSpot campaign's website pages, landing pages, blog posts and marketing emails with a glossary (term list, brand terms, "do not translate" words) that HubSpot's built-in DeepL translation does not offer; the campaign × language matrix and its statuses (Up to date, Outdated, Missing, Blocked); the DeepL, Claude and Test engines and their keys; the HubDB glossary; what it translates and what it does not. Platform background: [translation-and-terminology.md](translation-and-terminology.md).

> 🔒 **worksby.design system — not a HubSpot platform feature.** Translation Studio is our HubSpot app; nothing here ships with a HubSpot subscription, and it is not on your portal unless you installed it. Portable regardless: recording which source version a translation was made from instead of comparing timestamps, translating a page's module field values rather than its JSON, and keeping the glossary where the marketing team already works. What we share vs. what we can only demo: [projects-catalog.md](projects-catalog.md)

Translation Studio translates a HubSpot campaign — its website pages, landing pages, blog posts and marketing emails — into the languages you choose, as drafts in HubSpot's own editors, with your term list applied every time. It shows, per asset and per language, what is translated, what is missing and what has gone out of date since it was translated.

The problem it answers: brand names and industry terms come out differently from one page to the next, and HubSpot's built-in AI translation gives you no way to fix that. It is powered by DeepL and has no glossary, terminology or do-not-translate setting.

| | |
|---|---|
| **Kind** | HubSpot private app (HubSpot developer platform 2026.09), installed on your own portal |
| **Built for** | Marketing teams that publish one campaign in several languages on HubSpot, and the partners who run that for them |
| **Translates** | Website pages, landing pages, blog posts, marketing emails — including text in custom modules |
| **Engines** | DeepL with a synced DeepL glossary, Claude (Anthropic) with your terms as mandatory, or a no-network Test engine |
| **Output** | Drafts only. Nothing is ever published by the app |
| **Stores data in** | Your own portal: 3 HubDB tables and an encrypted settings record. No worksby.design server is involved |
| **How to get it** | By invitation — [worksby.design/get/translation-studio](https://worksby.design/get/translation-studio): enter the invited email address and a download link that lives 24 hours is sent to it; see [Requirements and getting it](#requirements-and-getting-it) |

> [!IMPORTANT]
> **Maturity, as of 2026-11-10: proof of concept.** The whole flow — campaigns, target languages, the asset × language matrix, every status, drafts in all four content types, attaching translations to the campaign, the glossary, settings and storage — has run on a HubSpot portal with the **Test engine**, which marks text instead of translating it. The **DeepL and Claude engines are built and unit-tested against fakes, but have not yet been run against the live DeepL or Anthropic services.** It has not been used on a real customer's content. Plan a trial on a copy of one campaign before relying on it.

## Why HubSpot's own translation is not enough

HubSpot's native AI translation is good at sentences and has no way to be told your words.

| | HubSpot's native AI translation | Translation Studio |
|---|---|---|
| Engine | DeepL | DeepL or Claude, your choice and your account |
| Glossary / term list | None. Neither help article mentions one (both read 2026-11-10); the only persistent control is brand voice, which governs tone, not terms | A HubDB table your marketing team edits in HubSpot, applied on every run |
| Custom modules | "Automatic translation isn't available for custom blog or page modules" ([HubSpot knowledge base](https://knowledge.hubspot.com/website-and-landing-pages/automatically-translate-multi-language-content)) | Translated — text fields inside custom modules are read and written back |
| Overview | One asset at a time | Every asset in a campaign × every target language, with a status per cell |
| Knowing what changed | Not tracked | A translation records the source version it was made from, so an edited source reads **Outdated** |

A request for exactly this feature is open in HubSpot's community ([Custom Terminology for AI Website Translations](https://community.hubspot.com/t/custom-terminology-for-ai-website-translations/126019)). The glossary claim is the one most likely to change; it was last checked on 2026-11-10.

## When it fits

- You run **multi-language content in HubSpot** (Content Hub Professional or Enterprise) and **campaigns** (Marketing Hub Professional or higher), with an Enterprise subscription on at least one hub.
- Your translation is **machine first, human reviewed**: an engine drafts, a language owner corrects and publishes in HubSpot.
- You have **terms that must not drift** — product names, brand terms kept in English, industry vocabulary with one correct rendering per language.
- You are a **HubSpot partner** running localisation for several clients and want the term list and the status per language in the client's own portal.

It is not a translation-management system. If you work with professional translators, translation memory and per-word vendor workflows, a translation-management product with a HubSpot connector is the better fit; Translation Studio has no translation memory and no translator workspace.

## How it works

Everything starts from a campaign, because that is how a marketer thinks about it: *this launch, these languages*.

1. **Open a campaign.** The Campaigns tab lists every campaign on the portal with its target languages, progress and what needs attention.
2. **Choose the target languages.** Every asset in the campaign shows *Missing* in each new language.
3. **Translate.** Everything missing at once, one language, one asset, or one cell. Assets whose source changed since translation show *Outdated* and get an **Update** button.
4. **Review in HubSpot.** Each translated cell has an **Open draft** link to HubSpot's own page, blog or email editor, already in the target language.
5. **Publish yourself.** The app never publishes. Translations are attached to the same campaign.

![The Translation Studio app page on the Campaigns tab. A table lists three campaigns with columns Campaign, Status, Target languages, Progress and Needs attention. Two campaigns have no languages yet and read "Choose languages first". The third, Spring Launch 2026, has target languages de, fr, es and nl, progress "8 / 16 translated as of 2026-11-10 11:22 UTC", and three flags: "2 outdated", "1 blocked" and "4 in template". Further tabs for Website pages, Landing pages, Blog posts and Marketing emails sit beside Campaigns, and a "Read the user guide" link sits above the table.](assets/translation-studio/campaigns-overview.png)

*The Campaigns tab: one row per campaign, with its languages and what needs attention.*

![A campaign in Translation Studio. At the top, the target languages de, fr, es and nl, each removable, and an "Add a language" picker with the note "Removing a language only hides its column; translations already made are kept." Below, action buttons: "Translate everything missing (9)", "Missing → de (1)", "Missing → fr (2)", "Missing → es (3)", "Missing → nl (3)", "Update outdated (2)" and "Retry blocked (1)". The matrix has one row per asset and one column per language. A website page is Up to date in all four languages, each with an Open draft link. A landing page is Outdated in German and French with Update buttons, and Missing in Spanish and Dutch with Translate buttons; its row ends in "Translate missing (2)".](assets/translation-studio/campaign-languages-and-matrix.png)

*Choosing languages and acting on a whole language, a whole row, or a single cell.*

![Further rows of the same matrix. A legacy landing page, "Spring Launch — Webinar sign-up (legacy template)", reads "In template" in all four languages; under its name, once, in italics: "This page's body is coded in its template, so only its title and meta description can be translated here. Translate the template itself, or rebuild the page with drag-and-drop modules." Each cell still offers Translate, for the title and meta description. A blog post is Blocked in German, with the message "This blog has no German (de) version yet. A blog post can only be translated into a language its blog has: add a de language variation of the blog (blog settings → multi-language), then press Retry." and a Retry button; it is Missing in French and Spanish and Up to date in Dutch. A marketing email is Up to date in German, French and Spanish and Missing in Dutch. A footnote reads: "Translations are created as drafts and attached to this campaign; nothing is ever published by this app. A status comes from what Translation Studio recorded when it made a translation, so Outdated means the source changed after that moment. Re-translating replaces the draft's text, including edits made in review."](assets/translation-studio/campaign-matrix-statuses.png)

*A page whose body is in its template says so instead of pretending to be translated, a blocked blog post names its own fix, and the footnote states the two rules that matter in review.*

Screenshots on this page are from a HubSpot test portal running the **Test engine**, which prefixes each text with the language — `[DE] Welcome…` — instead of translating it. That is why the settings screenshots show a Test engine banner.

## What each status means

Every status is judged from what the app recorded when it made the translation — never inferred by comparing dates on two different objects.

| Status | What it means | What to do |
|---|---|---|
| **Up to date** | Translated, the draft still exists, and the source has not changed since | Review and publish in HubSpot (**Open draft**). If the cell says *Not yet attached to the campaign*, **Attach** |
| **Outdated** | The source's content changed after it was translated | **Update** — re-translates and replaces the draft's text |
| **Missing** | Never translated into this language, or the translation was deleted | **Translate** |
| **Exists, not tracked** | A language version exists that the app did not make — written by hand, or by HubSpot's own AI translation | **Adopt** it so the app tracks it from now on, or leave it alone |
| **Failed** | The last attempt failed; the error is shown in the cell | **Retry** — or **Retry failed (n)** for all of them |
| **Blocked** | Something in HubSpot has to change first; the cell names the fix | Make the change, then **Retry** — or **Retry blocked (n)** for all of them |
| **In template** | A page whose body is coded in its template: only its title and meta description can be translated here. The reason is stated once, under the page's name | **Translate** covers the title and meta description; the body needs the template translated, or the page rebuilt with drag-and-drop modules. Never counted as translated, and never holds the campaign's progress below 100 % |
| **Not supported yet** | An asset type this version does not translate | Listed under the matrix, for completeness |
| **Source language** | This language is the asset's own | Nothing |

A change that touches only the source's metadata — for example, the page gaining a new language version — does not make a fresh translation read Outdated: the app compares a fingerprint of the translatable text as well as the edit time.

## Your terms, every time

The glossary is a HubDB table, **Translation Studio — glossary**, that your marketing team edits in HubSpot under Content → HubDB. One row per term and target language: the source term, the language code, the term to use, and an optional note.

![The HubDB table "Translation Studio — glossary" in HubSpot, with columns Source term, Target language, Target term and Note. Example rows: Big Data → de → Massendaten, "Preferred term in German copy"; Customer Platform → de → Kundenplattform, "Product name — always this form"; Customer Platform → fr → plateforme client, "Lower case in running text"; Smart CRM → de and fr → Smart CRM, "Brand term — never translated"; lead time → de → Lieferzeit, "Industry term, not Vorlaufzeit"; lead time → nl → levertijd, "Industry term".](assets/translation-studio/glossary-in-hubdb.png)

*The glossary is an ordinary HubDB table. Draft rows count; there is no need to publish it.*

How the terms reach each engine:

| Engine | How the glossary is applied |
|---|---|
| **DeepL** | The whole table becomes one DeepL glossary in your DeepL account, with one dictionary per target language. It is rebuilt on the next run after any change to the table, and the old one is deleted. DeepL adjusts glossary entries for case, gender and tense in inflecting languages, rather than pasting them in |
| **Claude** | For each batch of text, the terms that actually occur in it are sent as mandatory terms, with their notes |
| **Test engine** | Terms are applied literally, so the flow can be checked without a provider |

Three rules to know:

- **Terms are written in the default source language** set on the settings page. An asset in a different source language is translated without the glossary, and the run says so.
- **A regional code inherits the base language.** A row for `nl` serves `nl-nl` and `nl-be`, unless another row names `nl-be` exactly.
- **Rows DeepL would refuse are skipped and counted** — a duplicate source term, an entry over 1,024 bytes, or an unsupported language. The run's notes report how many.

## Engines, keys and what is sent where

![The Translation tab of the Translation Studio settings page. A yellow banner reads "The Test engine is selected — Runs produce marked copies of the source ("[DE] …"), not translations. Choose DeepL or Claude once a key is saved." Below are three choices that save as soon as they change: Translation engine (set to "Test engine (no network — marks text, for proving the flow)"), Claude model (set to "Claude Sonnet 5.5 (default)") and Default source language (English). A "Test the engine" button has run and shows "Test engine works — en → de in 1 ms" with two sample sentences prefixed [DE], one of them HTML with its tags and link left intact.](assets/translation-studio/settings-engine.png)

*Choosing the engine, the Claude model and the default source language, then checking the engine on two samples.*

| Engine | What it does | What leaves your portal |
|---|---|---|
| **DeepL** | Translates through DeepL's API with your glossary attached. A free-plan key (ending in `:fx`) is sent to DeepL's free endpoint automatically. HubSpot's template markup (HubL) is marked as not-to-translate | The extracted text and the glossary, to DeepL |
| **Claude** | Translates through Anthropic's API, in batches of up to 2,500 characters, with the glossary terms found in each batch as mandatory terms. Choose Sonnet 5.5 (default), Opus 5.5 or Haiku 5.5 | The extracted text and the matching glossary entries, to Anthropic |
| **Test engine** | Prefixes each text with the target language and makes no network call. The default, so the flow can be proven before any key exists | Nothing |

**Only text is sent.** The app reads the text fields of each module — headings, body text, button labels, image alt text — plus the page title and meta description, a blog post's title, body and summary, and an email's subject and text. Keys, ids, URLs, colours, fonts and other style values are never sent. The app's functions run inside HubSpot, and the only outside hosts it is configured to call are DeepL's two API hosts and Anthropic's.

**Cost sits with your provider account.** DeepL bills on source characters, and each target language counts separately; HTML tags do not count when tag handling is on (DeepL's documentation, read 2026-11-10). Whether DeepL glossaries are available on DeepL's free API plan was not confirmed — check your DeepL contract.

![The API keys tab of the settings page, under the same Test engine banner. Text reads "Keys are encrypted before they are stored and are write-only: they can be replaced or removed here, never shown again. Each key saves on its own." Two fields follow, each marked "Not set" with its own Save button: the DeepL API key ("From your DeepL account → API keys. A free-plan key ends in ":fx" and is sent to api-free.deepl.com automatically.") and the Anthropic API key ("From the Claude Console → API keys. It starts with "sk-ant-".").](assets/translation-studio/settings-api-keys.png)

*Keys are write-only: once saved they can be replaced or removed, never read back.*

**Your keys stay on your portal.** A key is checked for the right shape, encrypted (AES-256-GCM) and stored in a settings record on your portal. It is never returned to the page — not even masked. It is read only where the app calls the provider.

![The Storage and glossary tab of the settings page. A table lists three HubDB tables, each with state Ready: Translation records (translation_studio_records), Campaign languages (translation_studio_campaigns) and Glossary (translation_studio_glossary). A "Set up storage" button is disabled because the tables exist, beside a "Check again" button. Below, a section headed "The glossary" explains where to edit it in HubDB, that terms are written in the default source language, that DeepL receives the whole table as one DeepL glossary rebuilt when the table changes, that Claude receives the terms occurring in the text as mandatory, that the Test engine applies them literally, and that the table is read as a draft.](assets/translation-studio/settings-storage-and-glossary.png)

*Storage is set up once from the settings page; repeating it leaves existing tables untouched.*

## What it translates today, and what it does not

| Content | Today |
|---|---|
| **Website pages and landing pages** built from drag-and-drop areas and modules, including custom modules | Translated into a native HubSpot **language variation**, as a draft |
| **Blog posts** | Translated into a native language variation, as a draft — **provided the blog already has that language**. HubSpot refuses otherwise, and the cell shows *Blocked* with the fix: add the language to the blog once, in its multi-language settings |
| **Marketing emails** | Translated into a **copy of the email with its language set**, as a draft. HubSpot's API has no language-variation route for emails, so the copy is not linked to the original as a variation; the app keeps the link in its own records |
| **Pages whose body lives in the template** (older, template-coded pages) | **Not translatable per page.** The body text is not in the page's content at all, so only the title and meta description can be translated. The matrix marks such a page *In template* and says why, under its name, instead of calling it translated |
| **Any other campaign asset** — forms, CTAs and the like | Not translated; listed under the matrix as *Not supported yet* |

What it deliberately does not do:

- **Publish.** Every translation is a draft. A person reviews and publishes in HubSpot.
- **Change your blog's settings.** Adding a language to a blog is an admin decision, so the app reports *Blocked* with the fix rather than doing it.
- **Translate anything that is not recognisably text.** A module field the app cannot identify as text is left untranslated — an untranslated field is the safe failure; a translated style value is not.

Limits of this version, stated once:

- **Update replaces the draft's text, including edits made in review.** Review last, or re-apply edits after an update.
- **Runs happen in your browser tab.** Keep the app page open while a run is going; closing it stops after the asset in flight, and a rerun picks up from what was recorded. With the Test engine, one asset into one language took about 4–5 seconds; DeepL and Claude timings have not been measured yet.
- **A field is recognised as text by its name and content.** A custom module that names a text field unusually is skipped.
- **Blocked is only known after an attempt.** A blog post whose blog lacks the language shows *Missing* until someone tries.
- **Storage is HubDB**, with HubSpot's limit of 10,000 rows per table — one row per asset and language translated.
- **Languages:** 15 — English (British by default, or American), German, French, Spanish, Italian, Dutch, Polish, Portuguese (European or Brazilian), Swedish, Danish, Finnish, Czech, Japanese and Chinese (simplified), with regional codes such as `de-at`, `fr-be` and `nl-be`. A code outside this list is refused with the list, never guessed.

## Where your data lives

Everything stays on your portal.

| Store | Holds |
|---|---|
| HubDB `translation_studio_records` | One row per asset and language: the source version the translation was made from (edit time and text fingerprint), the engine, the outcome (`ok`, `failed` or `blocked`), any error, and whether it is attached to the campaign. Statuses are read from here |
| HubDB `translation_studio_campaigns` | Each campaign's target languages, and the progress summary shown on the Campaigns tab |
| HubDB `translation_studio_glossary` | Your terms |
| Custom object `app_setting` | The engine, Claude model and default source language; the encrypted keys; the id of the DeepL glossary the app maintains |

Records are kept per asset, not per campaign: an asset in two campaigns shares its translations. The app creates no association labels; translated drafts are attached to the campaign as campaign assets. It has no public web address — its functions answer only to a signed-in user of the portal it is installed on, from its own app page and settings page.

## Requirements and getting it

**Your portal needs:**

- **Content Hub Professional or Enterprise** — multi-language pages and blog posts.
- **Marketing Hub Professional or higher** — campaigns and marketing emails.
- **An Enterprise subscription on at least one hub** — the app keeps its settings in a small custom object, and custom objects need Enterprise. It has only been run on an Enterprise portal.
- **HubDB** — the records and the glossary.
- For real translations, a **DeepL API** key, an **Anthropic API** key, or both. Neither is needed to try the flow with the Test engine.

**Permissions it asks for at install**, in plain words:

| Permission | Why |
|---|---|
| Custom objects and their schemas, read and write | The settings record |
| Content | Read pages, landing pages and blog posts; create language variations; write drafts |
| Marketing email (shown at install as *Marketing Single Send API access*) | Read, copy and update marketing emails |
| Campaigns, read and write | List campaigns, read their assets, attach translations |
| Data-integration file data sources, read and write | HubSpot requires these to read a campaign's assets; without them each asset read was refused with a 403 on the portal where this was tested |
| HubDB tables and rows, read and write | The three tables |

**Getting it.** Translation Studio is distributed as a HubSpot project you upload to your own portal with HubSpot's CLI and install there as a private app. It is shared by invitation: go to [worksby.design/get/translation-studio](https://worksby.design/get/translation-studio), enter the email address that was invited, and a download link arrives within a minute. The link works for 24 hours; when it has expired, ask for a new one on the same page. Not on the list? Say which package you are after through [worksby.design/contact](https://worksby.design/contact) — access is arranged per organisation. The zip (Apache 2.0) carries `INSTALL.md` — build, upload, install, first visit to the settings page, storage, keys and the glossary — and `DOCUMENTATION.md` for anyone extending it.

On first open the settings page prepares its settings record; **Set up storage** creates the HubDB tables; the API keys tab takes your keys; **Test the engine** translates two samples into German without storing anything.

## What comes next

In the order planned:

1. **Runs against DeepL and Claude**, with timings per page size.
2. **Glossary import** — load an existing term list from a spreadsheet file, or use a glossary you already keep in DeepL.
3. **Runs that continue on the server**, so closing the page no longer stops a run and long pages are not bound by a browser session.
4. **Text found from each module's own field definitions**, instead of by field name — and an **Update that keeps reviewer edits**, re-translating only the text whose source changed.
5. **Translation outside campaigns** — tabs per content type (website pages, landing pages, blog posts, marketing emails), and a *Translate* action in HubSpot's own content lists if HubSpot opens its bulk actions to content.
6. **Storage beyond HubDB**, and availability as a public app installed from a link rather than uploaded.

Smaller items on the list: more engines, a check that reports any glossary term an engine did not respect, a review task for the language owner when a draft is ready, an explicit "publish reviewed translations" action (never automatic), and more languages as HubSpot and the engines support them.

## Try it

The fastest evaluation is one of your own campaigns, copied, with your term list in the glossary and your own DeepL or Anthropic key — the drafts land in HubSpot's editors where your language owners already work. To set that up, or to see it running first, [contact worksby.design](https://worksby.design/contact) →

HubSpot, DeepL, Anthropic and Claude are trademarks of their respective owners. worksby.design is an independent publisher, not affiliated with HubSpot, DeepL or Anthropic.
