# AI translation (https://transglot.ai/docs/concepts/ai-translation/) **Glossa** is the transglot translation engine. It drafts the translations your team then reviews. Every draft is recorded in a translation's history with the model that wrote it, for example **Glossa Flux draft**. ## Two models [#two-models] | Model | Used for | | --- | --- | | **Glossa Flux** | The default. Fast drafts for every target language. | | **Glossa Deep** | The higher-quality model. A project uses it for every target language when you turn on **Quality mode** under the project's **AI translation** settings. Available on Team and above. | ## When translation runs [#when-translation-runs] - **Automatically.** While the project's **Auto-translate** setting is on (it is by default), new and changed source strings are translated as they arrive from a push, an upload or a connector, and existing strings are translated into a language you add. - **On demand.** **Translate missing** in the editor fills a language's gaps, or only the rows you selected. See [Translate with AI](https://transglot.ai/docs/guides/translating/translate-with-ai). - **From the CLI.** `transglot init` asks before the first run, and `transglot push --wait` waits for the translation of what you pushed. See the [Quickstart](https://transglot.ai/docs/quickstart). Each run is a **batch**: you can follow its progress, and cancel it, while it runs. ## What Glossa reads [#what-glossa-reads] The context you give a project is what makes its translations specific rather than generic: - **The project description**, the line you wrote under **What is it?** when you created the project. - **Each key's description**, and its maximum length. See [Keys and strings](https://transglot.ai/docs/concepts/keys-and-strings). - **Your glossary** (Starter and above), so required and forbidden terms are respected. See [Glossary and brand voice](https://transglot.ai/docs/concepts/glossary-and-brand-voice). - **Your brand voice**, the tone, formality and rules you set on the project's **Voice** page. - **Similar approved translations**, when semantic translation memory is turned on (Team and above), so new text is phrased consistently with what you already approved. See [Translation memory](https://transglot.ai/docs/concepts/translation-memory). - **Screenshots**, when **Screenshot context** is turned on (Team and above), so Glossa can see where a string appears. Screenshots add image cost to the runs that use them. When the exact same source text has been translated before, transglot reuses that translation at no cost instead of translating it again. ## What Glossa keeps intact [#what-glossa-keeps-intact] Placeholders such as `{name}`, `{{count}}`, `%s` or `:name` and HTML tags are carried into every translation unchanged. A plural key gets one form for each plural category the target language uses, not only the forms the source has. A draft that still breaks either after a retry is not written, so the string stays untranslated rather than broken. See [Quality checks](https://transglot.ai/docs/concepts/quality-checks). ## What Glossa never does [#what-glossa-never-does] A translation you have edited or reviewed is never overwritten by AI. Glossa only writes into strings that are empty or still a Glossa draft. ## When a person takes over [#when-a-person-takes-over] Each AI translation carries a confidence signal. Translations that pass the automatic checks and come back confident can be marked as not needing review, so your reviewers only see the ones worth their time. Everything else is flagged for review. Automatic checks always win: a translation with a broken placeholder, the wrong plural forms, or a glossary conflict is always sent to review no matter how confident it was. That routing is off by default; see [Translation states](https://transglot.ai/docs/concepts/translation-states) for the setting and the states a translation moves through. ## Related [#related] - [AI translation settings](https://transglot.ai/docs/guides/translating/ai-translation-settings) - [The assistant](https://transglot.ai/docs/guides/translating/the-assistant) - [Words and usage](https://transglot.ai/docs/concepts/words-and-usage) # Glossary and brand voice (https://transglot.ai/docs/concepts/glossary-and-brand-voice/) A glossary fixes **what words** a translation uses. Brand voice sets **how it sounds**. Both are set per project, and Glossa reads both on every translation. ## The glossary [#the-glossary] Each project has a **Glossary**, available on Starter and above. A term is a word or phrase from your source text, with the wording each target language must use. A term can also be marked: | Option | Meaning | | --- | --- | | **Do not translate** | Keep the term exactly as it is in the source, such as a product name. | | **Forbidden** | A word that must never appear in the translation. | | **Case-sensitive** | Match the term only with the same capitalisation. | A term can also carry a **Notes** field for translators and the kind of word it is, such as a noun. Glossary terms are matched against source text and enforced during translation and during quality checks; a translation that departs from a glossary term is flagged. In the editor, the context panel lists the glossary terms a string contains, and the **Has glossary issue** filter finds translations that break one. How strictly a departure is treated is the **Glossary enforcement** setting on the project's **Checks** page: **Off**, **Warn** or **Enforce**. You can add terms one at a time with **Add term**, import them from a CSV or TBX file with **Import CSV or TBX**, and **Export** them. On Team and above, **Extract terms** proposes candidate terms from your source strings; candidates are not enforced until you approve them. Developers, admins and account Owners can manage the glossary. ## Brand voice [#brand-voice] The project's **Voice** settings page describes how your product sounds in every language: - **How you sound**: formality, warmth, a description of your brand voice, and regional notes. - **Lines you never cross**: rules a translation must always or never follow. - **Per-language adjustments**, so German can be formal while Spanish is not. On Team and above you can also give Glossa example pairs of a source string and the translation you want, let it propose a voice from the translations your team has already approved (a draft you edit and save; nothing applies until you save), and choose whether the glossary, the style guide and semantic translation memory are used for the project. Admins of the workspace and Owners of its account can change the Voice page. ## Related [#related] - [The glossary](https://transglot.ai/docs/guides/quality/the-glossary) - [Add a glossary term](https://transglot.ai/docs/guides/quality/add-a-glossary-term) - [Review extracted glossary terms](https://transglot.ai/docs/guides/quality/review-extracted-glossary-terms) - [Brand voice](https://transglot.ai/docs/guides/quality/brand-voice) # Core concepts (https://transglot.ai/docs/concepts/) Read these once and the rest of the docs will make sense. Each page explains one idea and links to the guides that put it to work. ## How your work is organised [#how-your-work-is-organised] ## How translation works [#how-translation-works] ## How quality is kept [#how-quality-is-kept] ## How usage is measured [#how-usage-is-measured] For the vocabulary in one place, see the [Glossary of terms](https://transglot.ai/docs/resources/glossary). # Keys and strings (https://transglot.ai/docs/concepts/keys-and-strings/) ## Keys [#keys] A **key** is the name your code asks for, such as `checkout.title`. Each key holds one **source string** in the project's source language and one translation per target language. Keys and their source strings arrive from a push, an upload, or a connected repository or content platform. Source text is read-only in the console: to change it, change your source file and push or upload it again. **New key** in the editor adds a key and its details. | Field | What it is for | | --- | --- | | **Key name** | The identifier your code uses. Nested keys from JSON or YAML files are stored with dotted names, such as `cart.items`. | | **Description (context for translators & AI)** | What the string is and where it appears. Glossa and your translators both read it, so "Headline shown on the checkout page" gets a better translation than nothing. | | **Plural key** | Marks the key as plural, so it holds one form per plural category instead of a single string. | | **Max length (optional)** | The longest a translation may be, in characters. Glossa aims to stay within it, and the [quality checks](https://transglot.ai/docs/concepts/quality-checks) report a translation that goes over. | | **Tags** | Labels for grouping keys, such as the release or feature they belong to. The editor can filter by tag. | A key can also carry screenshots, which show reviewers where the string appears, and comments, where your team discusses it. See [The screenshot library](https://transglot.ai/docs/guides/quality/the-screenshot-library) and [Comment on a string](https://transglot.ai/docs/guides/translating/comment-on-a-string). From the CLI, a key metadata file can set descriptions and maximum lengths for formats that have nowhere to keep them. See [Configuration](https://transglot.ai/docs/developers/cli/configuration). ## Plural forms [#plural-forms] Languages differ in how many plural forms they have. English has `one` and `other`; Polish has `one`, `few`, `many` and `other`; Arabic has six. A plural key stores one form per CLDR plural category of each language, so a translation into Polish has four forms even when the English source has two. Glossa writes every form the target language needs, and a translation missing a form is flagged. See [Plural forms](https://transglot.ai/docs/guides/projects/plural-forms). ## Placeholders [#placeholders] A **placeholder** is a part of a string your code fills in at runtime. transglot recognises these syntaxes and keeps them intact in every translation: | Syntax | Example | | --- | --- | | Double braces (i18next, Vue, Handlebars) | `{{count}}` | | Single braces (ICU, .NET, Python) | `{name}` | | printf | `%s`, `%d`, `%1$s` | | Laravel | `:name` | | HTML tags | ``, `` | The editor highlights placeholders in the source and the translation. A translation that loses, adds or changes a placeholder is flagged, because shipping it would break the string at runtime. The project's **Message syntax** setting, under **Checks**, decides how strictly strings are read as ICU MessageFormat: **Auto** reads a string as ICU only when it holds a plural, select, ordinal, number, date or time; **ICU** reads every string as ICU; **Legacy** never does. ## When source strings change or disappear [#when-source-strings-change-or-disappear] When a source string changes, its existing translations are marked **stale** until someone reads them again. While the project's **Auto-translate** setting is on, Glossa drafts new translations for the changed string. A translation you have edited or reviewed is never overwritten by AI: it stays as it is, marked stale, for a person to update. A push adds and updates keys but does not delete keys that are missing from the pushed file, unless you ask it to with `transglot push --sync-removals`. You can also delete keys in the editor. ## Related [#related] - [Add or edit a key](https://transglot.ai/docs/guides/projects/add-or-edit-a-key) - [Translation states](https://transglot.ai/docs/concepts/translation-states) - [File formats](https://transglot.ai/docs/developers/formats) # Languages and locales (https://transglot.ai/docs/concepts/languages-and-locales/) ## Source and target languages [#source-and-target-languages] **The source language** is the language your strings are written in. You pick it when you create the project. Everything in the project is translated from it, it is the one language you never translate into, and it cannot be removed. **Target languages** are the languages you ship. Each one is tracked on its own: its own coverage, its own review work and its own published output. You can add and remove target languages at any time from the project's **Languages** settings, under **Add a language**. Adding and removing languages needs a developer, an admin or an Owner of the account. When you add a language to a project that already has strings, transglot fills it straight away: strings already in translation memory are reused, and the rest are translated by Glossa while the project's **Auto-translate** setting is on. Removing a language deletes every translation in it; your keys and source strings are untouched. ## Language tags [#language-tags] Languages are named with BCP-47 tags, the standard your framework already uses: | Tag | Language | | --- | --- | | `de` | German | | `pt-BR` | Portuguese (Brazil) | | `fr-CA` | French (Canada) | | `zh-Hans` | Chinese (Simplified) | | `sr-Latn` | Serbian (Latin script) | transglot normalises what you type, so differences of case or separator (`pt-br`, `pt_BR`) do not create duplicates. It refuses a tag it does not recognise rather than creating a language that would never match your files: **"Unknown locale: use a BCP-47 tag like de, pt-BR, or zh-Hans."** Plural rules come from the language too. Each language has its own set of plural categories, such as `one` and `other` for English or `one`, `few`, `many` and `other` for Polish, and a plural key holds one form per category. See [Keys and strings](https://transglot.ai/docs/concepts/keys-and-strings). ## How many languages you can use [#how-many-languages-you-can-use] Free accounts can use a limited number of distinct languages across all their projects, counting source languages; paid plans have no language limit. When an account is at its limit you can still add a language it already uses in another project. See [pricing](https://transglot.ai/pricing). ## Translated is not the same as shipped [#translated-is-not-the-same-as-shipped] Whether a language reaches your app is a separate question from whether it exists in the project. A language reaches your app when you pull it into your build, merge the pull request a connected repository opens, or publish it to the CDN. See [Publish a locale](https://transglot.ai/docs/guides/delivery/publish-a-locale). ## Related [#related] - [Add a target language](https://transglot.ai/docs/guides/projects/add-a-target-language) - [Remove a language](https://transglot.ai/docs/guides/projects/remove-a-language) - [Plural forms](https://transglot.ai/docs/guides/projects/plural-forms) # Quality checks (https://transglot.ai/docs/concepts/quality-checks/) transglot checks translations in two layers: automatic checks for problems that would break your product, which run on every plan, and an AI review that scores each translation and lists specific findings, on Team and above. ## Automatic checks [#automatic-checks] These look for problems that can be detected with certainty, and they run without spending any of your usage: | Check | What it catches | | --- | --- | | **Placeholders** | A placeholder such as `{name}`, `%s` or an HTML tag that is missing, added or changed. | | **Plural forms** | A plural key missing one of the forms the target language needs. | | **Length** | A translation longer than the key's maximum length. With screenshots, a translation likely to overflow the space it renders in. | | **Glossary** | A translation that departs from a glossary term, uses a forbidden word, or translates a term marked **Do not translate**. | Glossa's drafts go through these checks as they are written. A draft that still breaks a placeholder, a plural form or a forbidden glossary term after a retry is not written: the string stays untranslated and the run reports it, rather than shipping a broken string. A draft that only breaks a softer rule, such as the maximum length, is written as a draft; with **Route AI translations by confidence** turned on, it is flagged for review. See [Translation states](https://transglot.ai/docs/concepts/translation-states). How placeholders and plurals are read depends on the project's **Message syntax** setting, and how strictly glossary departures are treated depends on **Glossary enforcement**. Both are on the project's **Checks** page. ## AI review and the quality score [#ai-review-and-the-quality-score] On Team and above, **Run QA** in the editor, or **Re-run checks** on the project's **Quality** page, has Glossa review the translations against their source. The quality score is a 0-100 rating that transglot produces for a translation, alongside a list of specific findings, each with a severity. The scorecard groups those findings using the industry-standard MQM categories so they can be compared with other tools' reports. Findings are either **blocking** or **warning**. A translation with a blocking finding, or a low score, is marked **needs checking**. A run is incremental: it scores what has changed since the last run, so running it again over an unchanged project does no new work. Reviewing translations counts towards your usage; see [Words and usage](https://transglot.ai/docs/concepts/words-and-usage). The **Quality** page shows every language's score and its findings, and the **Scorecard** shows the per-language MQM scores you can print or export. See [The per-language scorecard](https://transglot.ai/docs/guides/quality/the-per-language-scorecard). ## Self-heal [#self-heal] On Team and above, **Self-heal** retranslates machine translations that fail review. The project's **Auto-heal mode** decides whether a fix is offered as a suggestion for a reviewer to accept (**Suggest**) or written in directly (**Apply**). **Source drift repair** does the same when a source string changes, re-checking its translations and repairing the ones the edit broke. Neither ever overwrites a translation a person has edited or reviewed. See [Self-heal a translation](https://transglot.ai/docs/guides/quality/self-heal-a-translation). ## Related [#related] - [Run a quality check](https://transglot.ai/docs/guides/quality/run-a-quality-check) - [What does the quality score mean?](https://transglot.ai/docs/guides/quality/what-does-the-quality-score-mean) - [Why was my translation flagged for review?](https://transglot.ai/docs/guides/translating/why-was-my-translation-flagged-for-review) # Translation memory (https://transglot.ai/docs/concepts/translation-memory/) Translation memory works in two ways, and the difference matters. ## Exact matches [#exact-matches] When the exact same source text has been translated before, transglot reuses that translation at no cost instead of translating it again. This happens automatically during every translation run, and adding a language can fill strings from memory straight away. Exact-match reuse draws on an anonymised, shared pool as well as your own workspace's history. You can turn that off per project with the **Translation memory** setting under the project's **AI translation** settings. With it off, the project neither reads from nor adds to the pool, and every string is translated fresh. Reused words show up as **Words reused at $0** on [your dashboard](https://transglot.ai/docs/first-steps/your-dashboard), and the project's **AI translation** settings show how many words the project reused this month. ## Similar matches [#similar-matches] transglot also finds previously reviewed translations of similar text and offers them as suggestions. A similar match is only ever a suggestion; it is never written to your project automatically. In the editor, similar matches appear in the context panel under **Similar approved translations**, where a translator can insert one with a keyboard shortcut. The same matches also guide Glossa, so a new translation comes out phrased consistently with the ones you already approved. Similar matches come from **Semantic translation memory**, a project setting that is off by default and available on Team and above. Two further settings tune it: | Setting | What it does | | --- | --- | | **Minimum match** | How close a match must be before it is shown, or **Exact only**. A legal project may want nothing under 95%; a marketing one may be glad of 75%. | | **Match sources** | Where matches may come from: **All projects here** (every project in the workspace) or **This project only**, when one project's reviewed work should not surface in another client's. | Similar matches only ever come from your own workspace's reviewed translations. ## Searching past translations [#searching-past-translations] The editor's context panel also lets you search past translations by phrase, to check how a term was translated before. ## Who can change these settings [#who-can-change-these-settings] Admins of the workspace and Owners of its account, on the project's **AI translation** settings page. See [AI translation settings](https://transglot.ai/docs/guides/translating/ai-translation-settings). ## Related [#related] - [The editor](https://transglot.ai/docs/guides/translating/the-editor) - [AI translation](https://transglot.ai/docs/concepts/ai-translation) - [What counts towards my usage?](https://transglot.ai/docs/administration/billing/what-counts-towards-my-usage) # Translation states (https://transglot.ai/docs/concepts/translation-states/) Every translation in the editor shows its state as a chip. The state tells you who last stood behind the text: Glossa, a person, or a reviewer. ## The states [#the-states] | State | Meaning | | --- | --- | | **not translated** | The key has no translation in this language yet. | | **draft** | Glossa wrote it and nobody has read it yet. The editor describes it as "A GLOSSA draft. Nobody has read it." | | **edited** | A person wrote or changed the text, by hand, by importing it, or by pushing it from a file. | | **approved** | A reviewer signed it off. | | **needs checking** | Someone flagged it, or an automatic check found a problem. The editor describes it as "Flagged for someone to check." | A separate **stale** chip can appear next to any state. It means the source string changed after the translation was written, so it should be read again before anyone approves it. ## What moves a translation [#what-moves-a-translation] - **AI translation** writes a **draft**. It only ever fills strings that are empty or still a Glossa draft: a translation you have edited or reviewed is never overwritten by AI. - **Saving a change** in the editor makes the translation **edited**. - **Approve** makes it **approved** and clears any flag. You cannot approve a translation with no text, or one you wrote yourself: "You wrote this one, so someone else has to approve it. Flag it to send it back." - **Flag for a check** marks it **needs checking**. Flagging an approved translation also withdraws the approval, so it drops back to **edited**. Flagging a draft leaves it a draft. - **A quality check** that finds a blocking problem or gives a low score marks it **needs checking**. Quality checks run on Team and above. See [Quality checks](https://transglot.ai/docs/concepts/quality-checks). - **A source change** marks it **stale**. A draft is translated again while **Auto-translate** is on; an edited or approved translation keeps its text and waits for a person. Approving is a reviewer's job: the Reviewer and Admin roles and the account's Owners can approve. Translators can write and edit translations but not approve them. See [Roles and what each one can do](https://transglot.ai/docs/administration/team/roles-and-what-each-one-can-do). ## Flagging AI translations for review [#flagging-ai-translations-for-review] Each AI translation carries a confidence signal. Translations that pass the automatic checks and come back confident can be marked as not needing review, so your reviewers only see the ones worth their time. Everything else is flagged for review. Automatic checks always win: a translation with a broken placeholder, the wrong plural forms, or a glossary conflict is always sent to review no matter how confident it was. This is the **Route AI translations by confidence** setting on the project's **Checks** page. It is available on Team and above and off by default; with it off, Glossa's translations land as **draft** and are not flagged. ## Using states downstream [#using-states-downstream] - In the editor, filter by **State** (**GLOSSA drafts**, **Edited**, **Approved**), or by **Waiting for approval** and **Needs checking**. See [Filter the editor](https://transglot.ai/docs/guides/translating/filter-the-editor). - When you pull files with the CLI, `min_state` in `transglot.json` can limit a file to `human_edited` (edited or approved) or `reviewed` (approved only) translations. See [Configuration](https://transglot.ai/docs/developers/cli/configuration). ## Related [#related] - [Approve or flag a translation](https://transglot.ai/docs/guides/translating/approve-or-flag-a-translation) - [Why was my translation flagged for review?](https://transglot.ai/docs/guides/translating/why-was-my-translation-flagged-for-review) - [Version history](https://transglot.ai/docs/guides/translating/version-history) # Words and usage (https://transglot.ai/docs/concepts/words-and-usage/) Your plan includes an allowance of AI translation each month. Usage is measured against that allowance, and what happens when you exceed it depends on whether usage-based billing is turned on for your account. ## How words are counted [#how-words-are-counted] Usage is counted in source words, once for each target language a string is translated into. One hundred source words translated into five languages is five hundred words of usage. Counting is script-aware: a language written without spaces, such as Japanese or Chinese, is measured on a basis that fits its script rather than by counting spaces. ## What counts [#what-counts] Every word handed to Glossa counts, whatever it was sent for: - **Translation**, whether automatic, from the editor or from the CLI. - **AI review** of translations in a quality check. - **Repairs**, when self-heal or source drift repair translates a failing string again. - **Brand voice drafts**, when Glossa proposes a voice from your approved translations. ## What does not count [#what-does-not-count] - **Reused translations.** When the exact same source text has been translated before, transglot reuses that translation at no cost instead of translating it again. Your dashboard shows these as **Words reused at $0**. - **Anything your team writes by hand**, imports or pushes from a file. - **Reading, filtering and approving**, and the automatic checks. - **The scorecard**, which is built from findings a quality check already produced. Three settings move usage the most: **Auto-translate** translates new strings as they arrive, adding a language translates every key into it, and turning **Translation memory** off stops free reuse. ## Allowances belong to the account [#allowances-belong-to-the-account] The words are the account's, shared by every workspace in it. The account's **Usage** page shows the words used this cycle against what the plan includes, and which workspaces used them. Only people with access to a whole workspace see its usage on the dashboard. On the Free plan, the allowance is a hard limit: when it runs out, translation stops until the next cycle or until you upgrade. New Free accounts also receive a one-time grant of welcome words; see [Create your account](https://transglot.ai/docs/first-steps/create-your-account). On paid plans, the account's Owners and billing members can turn on **Usage-based overage** on the **Usage** page. With it off, translation stops in every workspace of the account when the allowance runs out. With it on, translation continues and words above the allowance are charged at the overage rate, up to the **Spend cap a cycle, in USD** you set; at the cap, translation pauses. For current allowances and rates, see [pricing](https://transglot.ai/pricing). ## Related [#related] - [What counts towards my usage?](https://transglot.ai/docs/administration/billing/what-counts-towards-my-usage) - [Usage-based billing](https://transglot.ai/docs/administration/billing/usage-based-billing) - [Your plan and seats](https://transglot.ai/docs/administration/billing/your-plan-and-seats) # Workspaces and projects (https://transglot.ai/docs/concepts/workspaces-and-projects/) transglot organises everything in three levels. | Level | What it is | What it holds | | --- | --- | --- | | **Account** | Who pays. | The plan, billing and invoices, the usage allowance, and one or more workspaces. | | **Workspace** | One company or one product estate. | Your team and their roles, projects, integrations, security settings and the audit log. | | **Project** | One product or codebase to translate. | Keys and source strings, the source language and target languages, translations, the glossary and brand voice, and the project's tokens and keys for delivery. | Your **login** is separate from all three: one login can belong to several workspaces, with a different role in each. ## Accounts [#accounts] Every workspace belongs to an account, and the account's plan applies to every workspace in it. An account has two kinds of people: - **Owners** manage the account and hold every permission in every workspace of it. Some things only an Owner can do, such as setting up single sign-on, requiring two-factor authentication, custom branding and deleting a workspace. - **Billing members** manage the plan, payment and invoices. The account's settings show its **Usage**, its **Owners & billing members**, its **Workspaces** and an **Account log** of what changed. See [Your plan and seats](https://transglot.ai/docs/administration/billing/your-plan-and-seats). ## Workspaces [#workspaces] A workspace is where your team works. Each member has one of four built-in roles: | Role | What it is for | | --- | --- | | **Admin** | Runs the workspace: members, projects, access and approvals. | | **Developer** | Ships the integration: keys, languages, glossary and AI translation. | | **Reviewer** | Writes and approves translations, and reviews assigned work. Cannot add keys or run AI translation. | | **Translator** | Writes translations. Cannot approve their own work. | A member can be given access to one project instead of the whole workspace, and reviewers and translators can be limited to some languages. Workspaces can also define custom roles. See [Roles and what each one can do](https://transglot.ai/docs/administration/team/roles-and-what-each-one-can-do) and [Grant access to one project](https://transglot.ai/docs/administration/team/grant-access-to-one-project). ## Projects [#projects] A project is one set of keys translated into a set of languages: typically one app, one website or one codebase. Each project has its own source language, its own target languages, its own settings for AI translation, checks and voice, and its own deploy tokens, publishable keys and CDN delivery. See [Your project](https://transglot.ai/docs/guides/projects/your-project). Use separate projects when the strings ship separately or need different languages, voice or reviewers. Keep strings that ship together in one project, so they share a glossary and are released together. ## Limits by plan [#limits-by-plan] How many workspaces, projects, languages and seats an account has depends on its plan. Free includes one workspace and one project; paid plans have no workspace or project limit. See [pricing](https://transglot.ai/pricing) for the current figures. ## Related [#related] - [Create a workspace](https://transglot.ai/docs/first-steps/create-a-workspace) - [Create your first project](https://transglot.ai/docs/first-steps/create-your-first-project) - [Members and roles](https://transglot.ai/docs/administration/team/members-and-roles) # Accept an invitation (https://transglot.ai/docs/first-steps/accept-an-invitation/) The link in the invitation email is the only way in: there is nowhere to look an invitation up. The invitation names one email address, and only a login with that address can accept it. ### Open the link in the invitation email [#open-the-link-in-the-invitation-email] The page shows **Join** followed by the workspace name, and the role you were invited as. ### Sign in or sign up with the invited address [#sign-in-or-sign-up-with-the-invited-address] If you are not signed in, the page offers **Sign in** or **sign up** with the invited address, and brings you straight back to the invitation afterwards. A new login confirms its email address first; open the confirmation link and you land back on the invitation, ready to accept. ### Select Accept invitation [#select-accept-invitation] You join the workspace with the role the invitation named, and it appears on your dashboard. The people who manage the workspace are notified that you joined. If the invitation was limited to one project, or to some languages, that is all you see in the workspace. See [Roles and what each one can do](https://transglot.ai/docs/administration/team/roles-and-what-each-one-can-do). ## If it doesn't work [#if-it-doesnt-work] - **"This invitation has expired. Ask for a new one."**: invitations last seven days. Ask whoever invited you to send another. - **"You are signed in as ... Only that address can accept it."**: you are signed in with a different address. Select **Sign out and use** the invited address. - **The page asks you to confirm your email address.** Open the confirmation link transglot sent when you signed up, or select **Send the confirmation email again**. - **The link does not open at all.** The invitation may have been withdrawn. Ask whoever invited you. ## Related [#related] - [Create your account](https://transglot.ai/docs/first-steps/create-your-account) - [Your dashboard](https://transglot.ai/docs/first-steps/your-dashboard) - [Invite a teammate](https://transglot.ai/docs/administration/team/invite-a-teammate) # Create a workspace (https://transglot.ai/docs/first-steps/create-a-workspace/) A workspace holds your projects, your glossary and your team. Every workspace belongs to an account, which holds the plan and pays for it; every workspace in an account shares that plan. You can create a workspace in any account you are an Owner of, or in a new account, which makes you its Owner. See [Workspaces and projects](https://transglot.ai/docs/concepts/workspaces-and-projects). Do not create your own. Open the private link in the invitation email instead; a workspace you create yourself has nobody else in it. See [Accept an invitation](https://transglot.ai/docs/first-steps/accept-an-invitation). ## Create the workspace [#create-the-workspace] ### Start the wizard [#start-the-wizard] On your dashboard, select **New workspace**. ### Workspace [#workspace] Under **Which account it belongs to**, check the account. If you can choose between several, each row names the account and its plan. Enter the **Workspace name**; the workspace address shown below it is a preview. Select **Continue**. ### Project [#project] Every workspace starts with one project. Enter the **Project name** and, under **What is it?**, a line about the product, such as "Checkout flow for an e-commerce app". Glossa reads that line as context for every translation in the project. ### Markets [#markets] Pick the **Source language** your strings are written in and the **Target languages** you want to ship. You can add more languages later. ### Team [#team] Optionally invite people under **Invitations**, each with an **Email**, an **Invite role** and, for reviewers, the **Languages** they work on. Invitations go out when you finish. You can also skip this step and invite people later. ### Strings [#strings] Choose how your strings will arrive: a file, a migration from another tool, or a repository. Then select **Create workspace**. The workspace and its first project are created together and the invitations go out. You land on the new project's **Get started** page, which asks for your strings in the way you chose. See [Setup steps](https://transglot.ai/docs/first-steps/the-activation-checklist). ## How many workspaces an account holds [#how-many-workspaces-an-account-holds] The plan decides it. Free includes one workspace; paid plans have no workspace limit. A workspace that is scheduled for deletion still counts until it is gone. See [pricing](https://transglot.ai/pricing). ## If it doesn't work [#if-it-doesnt-work] - **The name is taken.** An account cannot hold two workspaces with the same name. The wizard says so beside the name and offers **Open that one**. - **The account is full.** The wizard explains, for example "Acme is on Free, which includes 1 workspace, and already has Acme Web." If you can change the account's plan, it offers **Upgrade the account**. - **"You are not an Owner of that account, so it cannot hold a new workspace. Pick another account."** The account you picked is not one you own, for example because you were removed as an Owner in another tab. - **Languages or invitations were removed.** If the account's plan has room for fewer languages or seats than you picked, the wizard removes the extra ones and says how many. ## Related [#related] - [Create your first project](https://transglot.ai/docs/first-steps/create-your-first-project) - [Invite a teammate](https://transglot.ai/docs/administration/team/invite-a-teammate) - [Roles and what each one can do](https://transglot.ai/docs/administration/team/roles-and-what-each-one-can-do) # Create your account (https://transglot.ai/docs/first-steps/create-your-account/) Your login is yours. One login can belong to several workspaces, with a different role in each. Sign up with **the same email address the invitation was sent to**, then open the invitation link again. An invitation can only be accepted by the address it was sent to. See [Accept an invitation](https://transglot.ai/docs/first-steps/accept-an-invitation). ## Sign up [#sign-up] ### Open the sign-up page [#open-the-sign-up-page] Go to [app.transglot.ai/register](https://app.transglot.ai/register). ### Choose how to sign up [#choose-how-to-sign-up] Select **Google** or **GitHub** to sign up with that account, or fill in the form under **Or sign up with email**: your **Name**, **Email address**, a **Password**, and the same password again in **Confirm password**. Then select **Create account**. ### Confirm your email address [#confirm-your-email-address] transglot emails you a confirmation link. Open it to finish setting up your account. Until you do, the console shows **Email verification**, with **Resend verification email** if the message did not arrive and **Wrong address? Change it** if you mistyped the address. Once your address is confirmed, [create a workspace](https://transglot.ai/docs/first-steps/create-a-workspace), or [connect a repository from the CLI](https://transglot.ai/docs/quickstart), which creates one for you. ## Welcome words on the Free plan [#welcome-words-on-the-free-plan] A new account on the Free plan receives a one-time grant of welcome words on top of the plan's monthly allowance, in two parts: - The first part arrives when you confirm your email address. - The rest arrives after you confirm a mobile number once. Open the account's **Welcome words** settings page, or select **Confirm a mobile number** on the billing page. transglot sends a 6-digit code by text message; enter it and the words are added. If you sign up with an address at an email alias or forwarding service, all of the welcome words arrive after the phone check instead. Welcome words come once per person. An email address or sign-in that has already received them does not receive them again; a new account then starts with the Free plan's monthly allowance only. For current amounts, see [pricing](https://transglot.ai/pricing). ## If it doesn't work [#if-it-doesnt-work] - **The email address is already registered.** [Sign in](https://transglot.ai/docs/first-steps/sign-in) instead, or use **Forgot your password?** on the sign-in page. - **Your company uses single sign-on.** Do not create a password account. Sign in through your company's identity provider instead; see [Sign in to transglot](https://transglot.ai/docs/first-steps/sign-in). - **"We could not finish this sign-up"**: the sign-up needs a manual review. Email the review code the page shows to hello@transglot.ai and a person reviews it. - **Your country is not in the list when you confirm a mobile number.** Phone checks are not available there yet. Write to hello@transglot.ai and say where you are. - **"That is not a mobile number. The code can only go to a mobile phone."**: landline and internet phone numbers cannot receive the code. - **"This number has already unlocked welcome words. Each number works once."**: use another mobile number of your own. ## Related [#related] - [Sign in to transglot](https://transglot.ai/docs/first-steps/sign-in) - [Create a workspace](https://transglot.ai/docs/first-steps/create-a-workspace) - [Secure your account](https://transglot.ai/docs/administration/security/secure-your-account) # Create your first project (https://transglot.ai/docs/first-steps/create-your-first-project/) A project holds one set of keys, the source language they are written in, and the target languages you ship. Creating a workspace creates its first project too, so you only need this page for the projects after that, or if you skipped ahead. To create a project from your repository instead, run `npx transglot@latest init`; see the [Quickstart](https://transglot.ai/docs/quickstart). Admins of the workspace and Owners of its account. Other roles can open projects but cannot create them. ### Start the wizard [#start-the-wizard] Open the workspace and select **New project**. ### Project [#project] Enter the **Project name** and, under **What is it?**, a line about the product, such as "Checkout flow for an e-commerce app". This is not decoration: Glossa reads it as context for every translation in the project, so a specific line gets you better translations. ### Markets [#markets] Pick the **Source language**, the language your strings are written in today, and the **Target languages** you want to ship. Languages are named with BCP-47 tags such as `en`, `pt-BR` or `zh-Hans`. See [Languages and locales](https://transglot.ai/docs/concepts/languages-and-locales). ### Strings [#strings] Choose how your strings will arrive: a file, a migration from another tool, or a repository. Then select **Create project**. The project is created and you land on its **Get started** page, which walks you through the rest: bringing your source strings in and running your first AI translation. See [Setup steps](https://transglot.ai/docs/first-steps/the-activation-checklist). ## If it doesn't work [#if-it-doesnt-work] - **"Unknown locale: use a BCP-47 tag like de, pt-BR, or zh-Hans."**: the language is not a tag transglot recognises. Use the language code, optionally with a region or script. - **"The account this workspace belongs to has no room for another project on its plan."**: the account has as many projects as its plan includes. Free includes one project; paid plans have no project limit. Delete a project or ask an account Owner to upgrade. - **New project is not offered.** You are not an admin of this workspace or an Owner of its account. ## Related [#related] - [Add a target language](https://transglot.ai/docs/guides/projects/add-a-target-language) - [Import content](https://transglot.ai/docs/guides/projects/import-content) - [Import from another tool](https://transglot.ai/docs/integrations/migrate/import-from-another-tool) # Do I need a developer to set transglot up? (https://transglot.ai/docs/first-steps/do-i-need-a-developer-to-set-transglot-up/) There are three ways to get strings into transglot, and they need very different amounts of engineering. | Route | Who sets it up | Plan | | --- | --- | --- | | **Upload or migrate.** Upload language files, a zipped locale folder, or bring a project across from Lokalise, Phrase, Crowdin, Transifex, Weblate or POEditor. | Uploading: a developer, admin or account Owner. Migrating: an admin or account Owner, with a read token from the tool you are leaving. No code either way. | Every plan | | **Connect a system you already run.** A code repository, a content platform or a file-storage service, set up once from the integrations screens. | An admin, plus someone with access to the other system to authorise the connection. This is configuration, not programming. | Starter and up | | **Push from your codebase.** The CLI, the GitHub Action or the API, fitted into your existing build. | A developer. | Starter and up | Getting translations back out has the same split. Exporting files and publishing back to a connected content platform need no code. The CDN needs a few lines in your app. A connected repository opens a pull request against your own files with no change to your app at all. ## Where to start [#where-to-start] - No code: [Import content](https://transglot.ai/docs/guides/projects/import-content) or [Import from another tool](https://transglot.ai/docs/integrations/migrate/import-from-another-tool). - Connect a system: [Connect GitHub](https://transglot.ai/docs/integrations/git/connect-github), [Connect a content platform](https://transglot.ai/docs/integrations/content-platforms) or [Connect file storage](https://transglot.ai/docs/integrations/storage/connect-file-storage). - From your codebase: the [Quickstart](https://transglot.ai/docs/quickstart). See [Plan requirements](https://transglot.ai/docs/integrations/plan-requirements) for what each integration needs. # Sign in to transglot (https://transglot.ai/docs/first-steps/sign-in/) ### Open the sign-in page [#open-the-sign-in-page] Go to [app.transglot.ai/login](https://app.transglot.ai/login). ### Choose how to sign in [#choose-how-to-sign-in] - **Passkey**: use a passkey you registered on this device or with your password manager. - **Google** or **GitHub**: sign in with the account you connected. - **Email and password**: enter your **Email address** and **Password**, tick **Remember me** to stay signed in on this device, and select **Log in**. ### Enter your second factor, if asked [#enter-your-second-factor-if-asked] If you turned on two-factor authentication, transglot asks for the **Authentication code** from your authenticator app. If you do not have the app to hand, choose **log in using a recovery code** and enter one of the recovery codes you saved. Each recovery code works once. You land on [your dashboard](https://transglot.ai/docs/first-steps/your-dashboard). ## Single sign-on [#single-sign-on] If your company requires single sign-on, everyone with an email address on its verified domain is sent to the company's identity provider when they sign in with a password or a passkey, and signing in with Google or GitHub stops working for them. Sign in through the identity provider instead. The Owners of the account that holds the workspace can always sign in with their password, so a broken identity provider cannot lock everyone out. See [Set up single sign-on](https://transglot.ai/docs/administration/security/set-up-single-sign-on). ## If it doesn't work [#if-it-doesnt-work] - Use **Forgot your password?** on the sign-in form to get a reset link by email. - If you lost your authenticator, use a recovery code. - For anything else, see [I can't sign in](https://transglot.ai/docs/resources/troubleshooting/i-cant-sign-in). ## Related [#related] - [Turn on two-factor authentication](https://transglot.ai/docs/administration/security/turn-on-two-factor-authentication) - [Add a passkey](https://transglot.ai/docs/administration/security/add-a-passkey) - [Create your account](https://transglot.ai/docs/first-steps/create-your-account) # Setup steps (https://transglot.ai/docs/first-steps/the-activation-checklist/) **Setup steps** is one list that you see at two scopes. On [your dashboard](https://transglot.ai/docs/first-steps/your-dashboard), the card labelled **Across your workspaces** covers every project you can reach. On a project's **Get started** page, the card labelled **This project** covers that project only. ## The five steps [#the-five-steps] | Step | Done when | | --- | --- | | **Create your first project** | A project exists. On a project's **Get started** page this is done from the start. | | **Add a target language** | The project has a language to translate into besides its source language. | | **Add your source strings** | The project has source strings, from an upload, a push or a connector. | | **Run your first translation** | A translation run has produced translations, either drafted by Glossa or reused from translation memory. A run counts as soon as it has written something, even if it later failed or was cancelled. A run that wrote nothing leaves the step open. | | **Invite a teammate** | The workspace has a second member, or an invitation you sent has not expired yet. An invitation that expires unaccepted stops counting. | Steps the project already satisfies count from the start, such as target languages you picked while creating it, or a teammate already in the workspace. Progress is worked out from your real state each time a page loads, so finishing a step anywhere updates every place the list appears, and doing steps out of order works as well as doing them in order. ## Reading the card [#reading-the-card] - The card counts **N of M complete** beside its title, with a bar of the same figures. - The next step in order is highlighted. An unfinished step carries a link named for what it does: **Create a project**, **Add a language**, **Add strings**, **Open the editor** or **Invite someone**. - A step you are already on reads **"You are on the page for this step."** - A step your role cannot do names who does it instead of linking you to a screen that would refuse you. A translator, for example, reads **"Someone who can manage the project’s languages does this step."** under **Add a target language**. - On the dashboard, finished steps are folded behind **N already done** so what is left comes first. - When every step is done, the card says so and stays finished, even while strings are waiting to be translated. ## Bringing strings in from the Get started page [#bringing-strings-in-from-the-get-started-page] Below the setup steps, a project's **Get started** page offers two ways to add your source strings: - **Upload your language files** takes JSON, YAML, XLIFF, .strings, .arb, .po and more, straight from your machine. Nothing is written until you confirm what was detected. - **Push from your codebase** gives ready-made snippets for `transglot.json`, the command line and `curl`. Pick a framework preset and the snippets rewrite themselves for it, with your project's details filled in. If you can manage tokens, select **Generate deploy token** to fill the snippets with a real token. It is shown once: **"Deploy token minted. Copy it now, it is shown once."** The CLI, the GitHub Action and the API need a deploy token, which starts on the Starter plan. On Free, the page says so, and uploading works instead. See [Deploy tokens](https://transglot.ai/docs/developers/deploy-tokens). ## Try a sample first [#try-a-sample-first] **Seed a sample project** on the **Get started** page, or **Explore a sample** on the dashboard, creates a finished project with real strings so you can look around before wiring up your own. It is a real project, so you need to be allowed to create projects, and your plan needs room for one more. When it is not available, the dashboard shows why: **Needs a spare project** when the account has no room for another project, or **Admins and owners** when your role cannot create projects. ## If it doesn't work [#if-it-doesnt-work] - **A step you finished still reads as not done.** It is usually **Run your first translation**: a run that has not written any translation yet does not count. Open the editor, translate the missing strings again, and the step ticks once something lands. - **There is no token strip.** Either your plan does not include API access, in which case the page says so and you upload files instead, or your role cannot create deploy tokens in this project. Ask an admin. - **You see no setup card on the dashboard.** If you were given access to specific projects rather than a whole workspace and none of them has strings yet, the dashboard does not show the account-wide card, because its steps are work for whoever runs the workspace. ## Related [#related] - [Your dashboard](https://transglot.ai/docs/first-steps/your-dashboard) - [Translate with AI](https://transglot.ai/docs/guides/translating/translate-with-ai) - [The command line tool](https://transglot.ai/docs/developers/cli) # Your dashboard (https://transglot.ai/docs/first-steps/your-dashboard/) The dashboard is titled **Portfolio** and covers **Every workspace you can reach**. What it shows depends on whether anything has been translated yet. ## Before your first strings arrive [#before-your-first-strings-arrive] Every figure would be a zero, so the page guides you instead. It shows [Setup steps](https://transglot.ai/docs/first-steps/the-activation-checklist) labelled **Across your workspaces**, then the ways to bring your strings in. Only the ways your plan and your role can use are offered as cards; anything closed to you is named with what would open it. | Card | What it does | | --- | --- | | **Upload files** | Drop a JSON, YAML or XLIFF file; the format is detected for you. Included on every plan. | | **Locale folder** | Zip your locales directory, and the files map to languages. | | **Migrate** | Bring your keys and translations across from six other tools. See [Import from another tool](https://transglot.ai/docs/integrations/migrate/import-from-another-tool). | | **Connect your repo** | The CLI or the GitHub Action, with a config for your framework. See [Quickstart](https://transglot.ai/docs/quickstart). | | **Connect a tool** | The card reads "Contentful, Figma, Shopify and more from the marketplace." See [Integrations](https://transglot.ai/docs/integrations) for what you can connect. | | **Explore a sample** | A finished project with real strings, to look around first. It needs room for one more project on your plan. | ## Once your strings have arrived [#once-your-strings-have-arrived] The page reports on your work. - **Your accounts and workspaces.** Workspaces are grouped by the account they belong to, with the account's plan and its words used this cycle. Each workspace lists its projects, with one bar per target language showing its coverage. Select **Open workspace** to go to it. - **Filters.** **Everything**, **Blocked** and **Waiting on review** narrow the list to the work that needs you. - **Markets.** Every target language across your projects, with its coverage, translated and score figures, its state, and whether it is published. - **Figures.** **Words reused at $0** counts words served from translation memory instead of being translated again. **AI words this cycle** shows AI translation used against your plan's allowance. **Active batches** counts translation runs in progress. **Projects** counts the projects you can see. - **Translation memory** shows the words reused this month, **Overall progress** the share of target strings translated across all projects, and **Recent activity** what has happened lately, with **View all** for the full feed. Words used this cycle are shown only to people with access to the whole workspace. ## If it doesn't work [#if-it-doesnt-work] - **You see Start your workspace.** You do not belong to a workspace yet. If you are expecting an invitation, do not create a workspace: open the private link in the invitation email. See [Accept an invitation](https://transglot.ai/docs/first-steps/accept-an-invitation). - **A project you expected is missing.** You may not have access to it. Ask an admin of the workspace. - **Explore a sample reads Needs a spare project.** The account has as many projects as its plan includes. Free up a project or upgrade. - **You see zeros instead of setup steps.** If you were given access to specific projects rather than a whole workspace, the dashboard reports on them rather than showing setup steps, because those steps are work for whoever runs the workspace. ## Related [#related] - [Create a workspace](https://transglot.ai/docs/first-steps/create-a-workspace) - [Setup steps](https://transglot.ai/docs/first-steps/the-activation-checklist) - [How transglot works](https://transglot.ai/docs/how-transglot-works) # How transglot works (https://transglot.ai/docs/how-transglot-works/) Every screen in transglot belongs to one of four stages: content in, translation, quality and delivery. They run in that order for each string, and they keep running as your source text changes. ## Where your work lives [#where-your-work-lives] A **workspace** holds your team and your projects, and belongs to an **account** that holds the plan and billing. A **project** holds one set of keys (your source strings), the source language they are written in, and the target languages you ship. See [Workspaces and projects](https://transglot.ai/docs/concepts/workspaces-and-projects) and [Keys and strings](https://transglot.ai/docs/concepts/keys-and-strings). ## 1. Content in [#1-content-in] Strings reach a project in one of these ways: - **From your codebase**, with the [CLI](https://transglot.ai/docs/developers/cli), the [GitHub Action](https://transglot.ai/docs/developers/github-action) or the [API](https://transglot.ai/docs/developers/api). - **From a connected repository**, which extracts strings when you push. See [Connect GitHub](https://transglot.ai/docs/integrations/git/connect-github). - **From a content platform** such as a CMS, a store or a help centre. See [Connect a content platform](https://transglot.ai/docs/integrations/content-platforms). - **From a file** you upload, or a project you migrate from another tool. See [Import content](https://transglot.ai/docs/guides/projects/import-content). The project records which language the strings are written in and which languages you translate into. See [Languages and locales](https://transglot.ai/docs/concepts/languages-and-locales). ## 2. Translation [#2-translation] Glossa, the transglot translation engine, drafts the missing translations. It reads the context you give it: the project description, each key's description, your [glossary](https://transglot.ai/docs/concepts/glossary-and-brand-voice), your brand voice and, when you turn it on, similar translations your team has already approved. When the exact same source text has been translated before, transglot reuses that translation at no cost instead of translating it again. See [AI translation](https://transglot.ai/docs/concepts/ai-translation) and [Translation memory](https://transglot.ai/docs/concepts/translation-memory). New and changed strings are translated as they arrive while the project's **Auto-translate** setting is on, or you start a run yourself from the editor. Everything lands in [the editor](https://transglot.ai/docs/guides/translating/the-editor), where your team can change any translation. A translation you have edited or reviewed is never overwritten by AI. ## 3. Quality [#3-quality] Automatic checks look for broken placeholders, missing plural forms, translations longer than a key allows, and glossary conflicts, so a broken translation is never written. On Team and above, an AI review also gives each translation a 0-100 quality score with specific findings, and flags the ones a person should check. Reviewers approve or flag translations, and can see a string in context through screenshots and the in-context editor. See [Quality checks](https://transglot.ai/docs/concepts/quality-checks) and [Translation states](https://transglot.ai/docs/concepts/translation-states). ## 4. Delivery [#4-delivery] Finished languages go back out in the way that suits your product: - **Pulled into your build** with the CLI or the GitHub Action. - **Proposed as a pull request** by a connected repository. - **Published to the CDN**, which your app reads at runtime. See [The CDN](https://transglot.ai/docs/guides/delivery/the-cdn). - **Published back** to the content platform the strings came from. - **Exported** as files from the project. ## Next steps [#next-steps] # Overview (https://transglot.ai/docs/) transglot keeps the text of your product translated. You bring your source strings from a repository, a file or a content platform. Glossa, the transglot translation engine, drafts every missing translation using your glossary, your brand voice and the translations your team already approved. Automatic checks flag what needs a human, your team reviews it in the editor, and the finished languages go back out through git, the CDN or the platform the content came from. ## Start from your AI agent [#start-from-your-ai-agent] Working in Claude Code, Cursor or VS Code? Add transglot to your agent and ask it to translate, review and sync your strings for you. [More about the MCP server](https://transglot.ai/docs/mcp). ## Explore the docs [#explore-the-docs] ## What you can do [#what-you-can-do] - **Translate with AI.** Fill every missing translation in one run, or translate only the rows you pick. Translations someone has already edited or reviewed are never overwritten. See [AI translation](https://transglot.ai/docs/concepts/ai-translation) and [Translate with AI](https://transglot.ai/docs/guides/translating/translate-with-ai). - **Review.** Work through translations in [the editor](https://transglot.ai/docs/guides/translating/the-editor), [approve or flag them](https://transglot.ai/docs/guides/translating/approve-or-flag-a-translation), [assign a language to a translator](https://transglot.ai/docs/guides/translating/assign-a-language-to-a-translator) and discuss a string in [comments](https://transglot.ai/docs/guides/translating/comment-on-a-string). - **Check quality.** Every translation is checked for broken placeholders, wrong plural forms, glossary conflicts and more, and gets a quality score with specific findings. See [Quality checks](https://transglot.ai/docs/concepts/quality-checks), [the glossary](https://transglot.ai/docs/guides/quality/the-glossary) and [brand voice](https://transglot.ai/docs/guides/quality/brand-voice). - **Deliver.** Pull files into your build with [the CLI](https://transglot.ai/docs/developers/cli), get a pull request from [a connected repository](https://transglot.ai/docs/integrations/git/connect-github), serve published languages at runtime from [the CDN](https://transglot.ai/docs/guides/delivery/the-cdn), or publish back to [a content platform](https://transglot.ai/docs/integrations/content-platforms). ## Build with transglot [#build-with-transglot] New to transglot and not sure where to start? Read [How transglot works](https://transglot.ai/docs/how-transglot-works), or [Do I need a developer to set transglot up?](https://transglot.ai/docs/first-steps/do-i-need-a-developer-to-set-transglot-up) if you are choosing a route. # Use transglot from your AI agent (https://transglot.ai/docs/mcp/) Your coding agent can work on your translations directly. The transglot MCP server (`@transglot/mcp`, built on the [Model Context Protocol](https://modelcontextprotocol.io)) gives an agent tools for one project, so you can ask in plain language: "translate what is missing in French", "what does `checkout.title` say in German", "add our product name to the glossary as do-not-translate". The server runs on your machine with `npx`, so you need Node.js 20 or later. One server serves one project: the deploy token decides which, and the token's [abilities](#choose-the-tokens-abilities) decide which tools work. ## Get a deploy token [#get-a-deploy-token] ### Open the project's Developer settings [#open-the-projects-developer-settings] In the console, open the project, then **Settings** → **Developer**. Under **API tokens**, select **New token**. ### Pick the abilities [#pick-the-abilities] For an agent that reads and edits translations, choose `read`, `write` and `pull`. Add `manage` if it should also start translation runs, run quality checks and edit the glossary. [Choose the token's abilities](#choose-the-tokens-abilities) has the full list. ### Copy the token [#copy-the-token] The token is shown once. Paste it into the box above, or into your client's configuration below. More on tokens in [Deploy tokens](https://transglot.ai/docs/developers/deploy-tokens). Tokens need a plan with API access, Starter and above. See [pricing](https://transglot.ai/pricing). ## Point your agent at these docs [#point-your-agent-at-these-docs] Agents read documentation better as plain text. Give yours [llms.txt](https://transglot.ai/docs/llms.txt) (an index of every page) or [llms-full.txt](https://transglot.ai/docs/llms-full.txt) (every page in one file), or the markdown of a single page: each page has a **Copy Markdown** button. ## Set up any client by hand [#set-up-any-client-by-hand] The server speaks MCP over stdio. Every client runs the same command, `npx -y @transglot/mcp`, with the token in the `TRANSGLOT_TOKEN` environment variable. Replace `tgl_xxx` with your token. If the console showed the token with a number and a `|` in front, leave that part out: the shell reads `|` as a pipe. ```bash claude mcp add transglot --env TRANSGLOT_TOKEN=tgl_xxx -- npx -y @transglot/mcp ``` ```json title="claude_desktop_config.json" { "mcpServers": { "transglot": { "command": "npx", "args": ["-y", "@transglot/mcp"], "env": { "TRANSGLOT_TOKEN": "tgl_xxx" } } } } ``` ```json title=".cursor/mcp.json" { "mcpServers": { "transglot": { "command": "npx", "args": ["-y", "@transglot/mcp"], "env": { "TRANSGLOT_TOKEN": "tgl_xxx" } } } } ``` VS Code can prompt for the token instead of storing it in the file: ```json title=".vscode/mcp.json" { "inputs": [ { "type": "promptString", "id": "transglot-token", "description": "transglot deploy token", "password": true } ], "servers": { "transglot": { "type": "stdio", "command": "npx", "args": ["-y", "@transglot/mcp"], "env": { "TRANSGLOT_TOKEN": "${input:transglot-token}" } } } } ``` ```json title="~/.codeium/windsurf/mcp_config.json" { "mcpServers": { "transglot": { "command": "npx", "args": ["-y", "@transglot/mcp"], "env": { "TRANSGLOT_TOKEN": "tgl_xxx" } } } } ``` The **Integrations** settings page in the app also has the Claude Code, Claude Desktop and Cursor snippets, under **MCP server**. A configuration file that holds a token is a secret. Keep it out of version control, or use your client's way of reading the token from the environment. ## Environment variables [#environment-variables] | Variable | Required | Description | | --- | --- | --- | | `TRANSGLOT_TOKEN` | Yes | The deploy token. It is sent only in the `Authorization` header and is removed from any error text. | | `TRANSGLOT_BASE_URL` | No | The API address. Default: `https://api.transglot.ai`. `TRANSGLOT_URL`, the CLI's variable, is used when this one is not set. | Without a token the server exits at once with `transglot-mcp: TRANSGLOT_TOKEN is required.` on stderr, which your client shows in its MCP log. ## Tools [#tools] Read tools have no side effects. Tools that start a translation run or a quality check use the workspace's word allowance; their descriptions say so, so the agent and your client's approval prompt can tell. | Tool | What it does | Ability | | --- | --- | --- | | `get_project` | Reads the project: name, source locale, every locale and the key count. | any of `push`, `pull`, `manage` | | `list_languages` | Lists the project's locales and the source locale. Locales cannot be added through the API. | any of `push`, `pull`, `manage` | | `get_translation` | Reads one key in one locale: value, state, staleness and quality verdict. | `read` | | `upsert_translation` | Writes one value, or its plural forms, for an existing key as a human edit. Editing the source locale can queue a translation run. | `write` | | `translate_missing` | Starts a translation run that fills missing values. Never overwrites edited or reviewed translations. | `manage` | | `get_batch_status` | Reads one translation run, optionally with its failed items. | any of `push`, `pull`, `manage` | | `list_batches` | Lists recent translation runs, or only the active ones. | any of `push`, `pull`, `manage` | | `retry_failed_batch` | Queues the failed items of a finished run as a new run. | `manage` | | `cancel_batch` | Stops a running translation run. Unspent words return to the allowance. | `manage` | | `run_qa` | Runs the AI quality check over target translations. Needs a plan with Quality mode (Team and above). | `manage` | | `glossary_list_terms` | Lists glossary terms, 100 per page. | `read` | | `glossary_add_term` | Adds an active glossary term: do-not-translate, forbidden, or with required translations. | `manage` | | `glossary_update_term` | Replaces a glossary term by id. | `manage` | | `glossary_delete_term` | Deletes a glossary term. | `manage` | | `pull_file` | Exports one locale as a file, optionally only translations at a minimum state. | `pull` | | `push_file` | Imports a file into one locale. Can delete keys missing from a source file. | `push` | `pull_file` and `push_file` in version 0.1.0 of the package handle `json_flat`, `json_nested`, `android_xml`, `ios_strings`, `ios_stringsdict`, `flutter_arb`, `laravel_php` and `laravel_json`. For other formats, use the [CLI](https://transglot.ai/docs/developers/cli). The glossary tools need a plan that includes the glossary (Starter and above). See [pricing](https://transglot.ai/pricing). ### Choose the token's abilities [#choose-the-tokens-abilities] | You want the agent to | Give the token | | --- | --- | | Read translations and the glossary only | `read` and `pull` | | Also edit single translations | `read`, `write` and `pull` | | Also sync whole files | add `push` | | Also start, retry and stop translation runs, run quality checks, and edit the glossary | add `manage` | A tool the token's abilities do not cover returns an error with the hint `The token lacks the "manage" ability. Issue a token that has it.`, naming the ability that is missing. ## What the API does not offer [#what-the-api-does-not-offer] There are no tools to list or search keys, create or delete single keys, add languages, or manage webhooks, because the public API has no endpoints for them. Keys are created and removed by pushing source files. ## Behaviour [#behaviour] - **Transport:** stdio only. Diagnostics go to stderr. - **Errors:** API errors come back as tool errors in the form `slug (HTTP status): detail`, with a hint on what to do next. - **Rate limits:** the API allows 60 requests per minute per token. The server retries a rate-limited request up to twice, waiting as the API asks, before it returns an error. Check the installed version with `npx -y @transglot/mcp --version`. # Quickstart (https://transglot.ai/docs/quickstart/) This quickstart connects a repository that already has a locale file, such as `messages/en.json`, to transglot with the command line tool. When you finish, the repository has a `transglot.json` file and a translated file for each target language, and the project is ready to review in the console. - Node.js 22 or later on the machine with the repository. - A browser on any machine, to sign in and approve the connection. - The CLI, the GitHub Action and the API start on the Starter plan. If your account is on Free, the browser offers a free trial while you connect. See [pricing](https://transglot.ai/pricing). ### Run `init` in your repository [#run-init-in-your-repository] From the root of the repository, run: ```bash npx transglot@latest init ``` `init` finds your locale files and works out the source language before anything leaves your machine. It prints what it found, for example `Found messages/en.json JSON (nested), 412 strings, source language en`, then prints a link and a code and opens the link in your browser. Always run it as `npx transglot@latest`: in a repository that lists an older version of `transglot` as a dependency, plain `npx transglot` runs that older version. If `init` reports **No locale files were found in this folder.**, tell it where they are with `--path` and `--format`, for example `--path messages/{locale}.json --format json_nested`. If the repository only has the source file, name the languages to translate into with `--targets fr,de`. ### Sign up or sign in, then approve [#sign-up-or-sign-in-then-approve] The browser opens **Connect the CLI**. If you are not signed in, choose **Sign in**, or **Sign up** to create your account, and you come straight back to this page. A new account confirms its email address first; the page moves on by itself once you open the confirmation link. Under **Connect this terminal?**, check that the code matches the one in your terminal, then select **Approve**. Approve only a code you started yourself. When the browser and the terminal are on different networks (an SSH session or a container, for example), the page then shows **Type this number in your terminal**: type the two digits into the terminal. ### Create or choose a workspace [#create-or-choose-a-workspace] A workspace holds your projects and your team. - If you have no workspace yet, the page shows **Create your workspace**. Enter a **Workspace name** and select **Create workspace**. - If you have more than one, choose it under **Choose a workspace**. If the workspace's account is on Free, the page offers a trial of the Starter plan with **Start the free trial**. Stripe asks for a card and nothing is charged that day. An account that cannot start a trial is offered the paid plan instead. Only the account's owners and billing members can start it; anyone else is told who to ask, and the terminal waits. ### Create or choose the project [#create-or-choose-the-project] Under **Choose the project for this repository**, either connect an existing project, or check the name, **Source language** and **Target languages** that `init` proposed from your files and create the project. Connecting creates a project token for this machine, so you need to be an admin of the workspace or an owner of its account. If you are not, the page names the admins who can connect it. The page then says **Your terminal is connected**. ### Run the first translation [#run-the-first-translation] Back in the terminal, `init` writes `transglot.json`, uploads your source strings and any translations you already have, and shows what Glossa will translate, per language, with the number of words the run needs. Answer **Start translating?** to start it. A progress bar runs for each language. When the run finishes, `init` writes the translated files next to your source file and prints the files to commit: ```bash git add transglot.json .gitignore messages/ git commit -m "Set up transglot" ``` `transglot.json` is safe to commit: it holds no secret. The project token stays on this machine, outside the repository. ```json title="transglot.json" { "url": "https://api.transglot.ai", "sources": [{ "path": "messages/{locale}.json", "format": "json_nested" }], "pull": [{ "path": "messages/{locale}.json", "format": "json_nested", "locales": ["fr", "de"] }] } ``` ### Review the translations [#review-the-translations] `init` ends with a **Review translations** link to the project. Open it to read and edit the translations in [the editor](https://transglot.ai/docs/guides/translating/the-editor). Translations that need a human are flagged for review; see [Translation states](https://transglot.ai/docs/concepts/translation-states). ### Push and pull as you work [#push-and-pull-as-you-work] When you add or change source strings, push them and wait for the translation, then pull the results: ```bash npx transglot@latest push --wait npx transglot@latest pull ``` `push` uploads the files named under `sources`. New and changed strings are translated automatically while the project's **Auto-translate** setting is on, which it is by default. `--wait` waits for that translation to finish. `pull` writes the files named under `pull`. In the folder `init` set up, these commands use the token stored on this machine. In CI, create a [deploy token](https://transglot.ai/docs/developers/deploy-tokens) and store it as the `TRANSGLOT_TOKEN` secret instead; see [Continuous integration](https://transglot.ai/docs/developers/cli/continuous-integration) or use the [GitHub Action](https://transglot.ai/docs/developers/github-action). ## Prefer not to use the command line? [#prefer-not-to-use-the-command-line] You can bring strings in without any code: - **Upload files.** [Create a project](https://transglot.ai/docs/first-steps/create-your-first-project), then use **Upload your language files** on its **Get started** page. It accepts JSON, YAML, XLIFF, .strings, .arb, .po and more, and nothing is written until you confirm what was detected. Uploading works on every plan. See [Import content](https://transglot.ai/docs/guides/projects/import-content). - **Migrate from another tool.** Bring your keys and translations across from Lokalise, Phrase, Crowdin, Transifex, Weblate or POEditor. See [Import from another tool](https://transglot.ai/docs/integrations/migrate/import-from-another-tool). - **Connect a repository or a content platform.** See [Connect GitHub](https://transglot.ai/docs/integrations/git/connect-github) and [Connect a content platform](https://transglot.ai/docs/integrations/content-platforms). Then [translate with AI](https://transglot.ai/docs/guides/translating/translate-with-ai) from the editor, and get the results out by [publishing to the CDN](https://transglot.ai/docs/guides/delivery/publish-a-locale) or exporting files. ## Next steps [#next-steps] # Appearance and language (https://transglot.ai/docs/administration/account/appearance/) **Who can do this:** everyone, for themselves. Open **Settings**, choose the **You** tab, then **Appearance & language**. ## Theme [#theme] Under **Theme** ("Follow your system setting, or pin the console to light or dark."), pick **Light**, **Dark** or **System**. The change applies straight away. The theme is remembered in this browser only. It is not shared with your teammates, and another browser or device keeps its own choice. ## Console language [#console-language] Pick a **Console language**: "The language every screen in this console is shown in. Your projects, their source strings and their translations are not affected." The list shows the languages the console is available in, each written in its own language. The console language is saved to your login, so it follows you to any browser you sign in from. It changes the console only, not the languages of the projects you translate. ## Related [#related] - [Your profile](https://transglot.ai/docs/administration/account/your-profile) - [Notification preferences](https://transglot.ai/docs/administration/account/notification-preferences) # Notification preferences (https://transglot.ai/docs/administration/account/notification-preferences/) **Who can do this:** everyone, for themselves. These settings are yours alone and apply in every workspace you belong to. "Nothing changes for your teammates." ### Open Notifications settings [#open-notifications-settings] Open **Settings**, choose the **You** tab, then **Notifications**. You can also select **Preferences** in your notification inbox. ### Set the digest [#set-the-digest] Under **Email digest**, choose a **Frequency**: **Daily**, **Weekly** or **Off**. ### Choose the kinds of email [#choose-the-kinds-of-email] Under **Kinds of email**, turn each kind on or off. ### Choose the channels for each event [#choose-the-channels-for-each-event] Under **Per-event channels**, tick **In-app**, **Email** or both for each event. ### Save [#save] Select **Save preferences**, or **Discard** to undo your changes. ## The email digest [#the-email-digest] The digest is "a rollup of what you have not read", sent daily or weekly, or not at all. Events marked "(emailed immediately)" do not wait for the digest: they are emailed when they happen, as long as their kind of email and their own **Email** switch are on. ## Kinds of email [#kinds-of-email] | Kind | What it covers | | --- | --- | | **Security** | Sign-ins, password and credential changes, and access to your workspaces. **Always on.** | | **Billing** | Payments, invoices, renewals and plan changes. **Always on.** | | **Project activity** | Batches, QA runs, imports and integrations in your projects. | | **Reports** | Weekly and monthly summaries of your workspace. | | **Reminders** | Work waiting on you: reviews, assignments and expiring invitations. | | **Getting started & team** | Getting started, invitations, and changes to your team. | | **Product & offers** | Product tips, new features and offers. | Security and billing email cannot be turned off. ## Per-event channels [#per-event-channels] Every event appears in your in-app inbox by default. Directed events, such as a mention or a review request, are also emailed by default; routine events, such as a batch finishing, are not. | Event | Emailed immediately | | --- | :-: | | Translation batch completed | | | Translation batch failed | | | Translation batch finished with errors | | | New comment on a string | | | You were @mentioned | Yes | | QA run completed | | | Keys were deleted in bulk | Yes | | A teammate joined | | | Review requested of you | Yes | | Self-heal completed | | | Source drift repaired | | | Usage reached 50/75/100% of the plan allowance | Yes | | A fallback engine translated part of a batch | Yes | | A translation was assigned to you | Yes | | An assignment was submitted for your review | Yes | | Your assignment was approved | | | Changes were requested on your assignment | Yes | | An assignment was withdrawn from your review | Yes | | Your export is ready | Yes | Workspace notifications to a team chat channel are set up per workspace, not here. See [Connect Slack](https://transglot.ai/docs/integrations/notifications/connect-slack). ## Related [#related] - [Your notifications](https://transglot.ai/docs/administration/account/your-notifications) - [Your profile](https://transglot.ai/docs/administration/account/your-profile) - [Connect Slack](https://transglot.ai/docs/integrations/notifications/connect-slack) # Your notifications (https://transglot.ai/docs/administration/account/your-notifications/) **Who can use it:** everyone. Your inbox collects notifications from every workspace you belong to. New activity that involves you lands in your inbox: "a batch finishing, somebody answering a comment, a review request landing in your queue." Which events reach you, and which are also emailed, is set in your [notification preferences](https://transglot.ai/docs/administration/account/notification-preferences). ## The bell [#the-bell] The **Notifications** bell sits at the foot of the console's left rail and shows how many notifications you have not read. Select it to open your latest notifications in a panel: - Select a notification to mark it read and open what it is about. - Select **Mark all read** to clear the count. - Select **View all notifications** to open the full inbox. While you work, a new notification also appears briefly at the edge of the screen, with **Open** when there is something to go to. ## The inbox page [#the-inbox-page] The **Notifications** page lists **Everything that involves you**, newest first, and says how many are unread. ### Open a notification [#open-a-notification] Select a notification. It is marked read, and transglot opens the string, batch, review or page it is about. ### Clear the rest [#clear-the-rest] Select **Mark all read** to mark everything read at once. ### See older notifications [#see-older-notifications] Select **Load more** at the end of the list. **Preferences** at the top of the page opens your notification preferences. When there is nothing new, the page reads "You are all caught up". Notifications are kept for 90 days, whether you have read them or not. ## Related [#related] - [Notification preferences](https://transglot.ai/docs/administration/account/notification-preferences) - [Your assignments](https://transglot.ai/docs/guides/translating/your-assignments) - [Comment on a string](https://transglot.ai/docs/guides/translating/comment-on-a-string) # Your profile (https://transglot.ai/docs/administration/account/your-profile/) **Who can do this:** everyone, for their own login. Your profile belongs to you, not to a workspace. If you belong to several workspaces, they all show the same name and address. ## Change your name or email address [#change-your-name-or-email-address] ### Open Profile [#open-profile] Open **Settings**, choose the **You** tab, then **Profile**. ### Edit your details [#edit-your-details] Edit **Name** ("Shown on your comments, assignments and activity.") or **Email address**. ### Save [#save] Select **Save changes**. Saving does not sign you out anywhere. ### Confirm it is you (email changes only) [#confirm-it-is-you-email-changes-only] If you changed your email address, confirm your password or use a passkey when asked. Changing your name alone never asks for this. ### Verify the new address [#verify-the-new-address] transglot emails the new address a verification link. Until you open it, the page shows **address unverified**. **Re-send the verification email** sends the link again. A warning also goes to your old address, so the person who holds it always finds out. The confirmation step exists because password resets are sent to the address on your login: anyone who could move it without your password could take over the login. ## Delete your profile and sign-in [#delete-your-profile-and-sign-in] Under **Delete your profile and sign-in**, enter your **Password** and select **Delete your profile**. "Your login goes, with its sign-in methods, notifications and saved views. Invitations you sent that are still waiting are withdrawn, and any seat you held is given back." Work you did inside workspaces keeps your name. Deleting your profile is immediate, every session ends, and it cannot be undone. Deleting is refused while you are the only Owner of an account that still has workspaces, members, invitations or a billing record. Add another Owner or discard the account first. See [Account Owners and Billing members](https://transglot.ai/docs/administration/team/account-owners-and-billing-members). An account you are the only Owner of, with none of those, is closed along with your login. It is also refused while an account you own is closing or has an unpaid invoice. ## If it doesn't work [#if-it-doesnt-work] - **The form refuses the new address**: another transglot login already uses it, or it belongs to a temporary inbox service. Use a permanent address. If it is permanent and still refused, contact support. - **You signed in with Google, GitHub or your company directory and never set a password**: on the confirmation screen, choose **Sign out and reset your password**. You receive a link to set one, then sign back in and try again. ## Related [#related] - [Secure your account](https://transglot.ai/docs/administration/security/secure-your-account) - [Appearance and language](https://transglot.ai/docs/administration/account/appearance) - [Notification preferences](https://transglot.ai/docs/administration/account/notification-preferences) # Apply a promo code (https://transglot.ai/docs/administration/billing/apply-a-promo-code/) **Who can do this:** the Owners and Billing members of the account. The **Promo code** card "checks the price before you buy". The code is applied when you subscribe from **Plan & billing**. ### Open Plan & billing [#open-plan--billing] Open **Settings**, choose the **Account** tab, then **Plan & billing**. ### Pick the plan [#pick-the-plan] Under **Where you sit**, select the plan you are considering. The code is checked against the plan you select, not the one you are on. ### Enter the code [#enter-the-code] Under **Promo code**, enter the code, for example `LAUNCH50`, and select **Apply**. ### Check the price [#check-the-price] The card reports the discounted price, for example "Discount applied: the monthly price on Team becomes" followed by the new price. ### Subscribe [#subscribe] Select **Upgrade**. The code goes with you to checkout for that plan. ## If it doesn't work [#if-it-doesnt-work] - **"We do not recognise that code."**: check the spelling. - **"That code is not being accepted right now."**: the code has expired, has not started yet, or has been used up. - **"That code is not valid for the ... plan."**: the code is limited to other plans. Select one of those and apply it again. - **"That code could not be applied."**: try again, or contact support. - **"This code cannot start a new account. Start the account, then apply the code from Plan & billing."**: create the account first, then use the code on its **Plan & billing** page. ## Related [#related] - [Change your plan](https://transglot.ai/docs/administration/billing/change-your-plan) - [Invoices and payment](https://transglot.ai/docs/administration/billing/invoices-and-payment) # Change your plan (https://transglot.ai/docs/administration/billing/change-your-plan/) **Who can do this:** the Owners and Billing members of the account. Some changes take an Owner: moving to Free, and any change that would delete workspaces or stop single sign-on, SCIM or the audit log in a workspace that uses it. ### Open Plan & billing [#open-plan--billing] Open **Settings**, choose the **Account** tab, then **Plan & billing**. ### Pick a plan [#pick-a-plan] Under **Where you sit**, select the plan you want. "Picking a row here changes nothing on its own: the one action underneath says exactly what it will do." ### Choose how you pay [#choose-how-you-pay] Turn **Pay annually** on or off. Annual billing shows how much it saves. ### Apply a promo code (optional) [#apply-a-promo-code-optional] If you have one, enter it under **Promo code** before you continue. See [Apply a promo code](https://transglot.ai/docs/administration/billing/apply-a-promo-code). ### Take the action [#take-the-action] Select the action under the plans: **Upgrade**, **Switch plan** or **Downgrade to Free**. For Enterprise it is **Book a call**. Read the confirmation, then confirm. ## Subscribing for the first time [#subscribing-for-the-first-time] When the account has no subscription yet, **Upgrade** first asks **Before you go to payment** for the **Company or billing name** ("Invoices are addressed to this name."). Select **Continue to payment** to pay on the secure checkout page. Back on **Plan & billing**, the page reads "Payment received. The plan changes when Stripe confirms it." If the account is eligible for a free trial, the plan starts as a trial and the **Plan** card shows when it ends. **Start** followed by the plan's name, for example **Start Team now**, ends the trial early: the card on file is charged today, and the plan's allowance opens once the payment goes through. ## Moving between paid plans [#moving-between-paid-plans] | Change | When it applies | | --- | --- | | A higher plan, or monthly to annual on the same plan | Now. "You are charged today, prorated to the day, and the unused part of your current plan is credited." | | A lower plan, or annual to monthly | Now, if this cycle's words fit the lower plan's allowance. Otherwise at the end of the period: "This cycle has already used or started" more words than the lower plan includes. | Features the new plan does not include stop when the change applies. Check before you move down: an integration that needs the higher plan stops, and a project setting that needs it (such as quality mode) turns off. The plan changes only after the payment goes through. If your bank needs to confirm the payment, the page shows **A change waiting for payment** with **Finish payment** and **Cancel pending change**. Cancelling it voids the invoice it waits on, and nothing is charged. ## Moving to Free [#moving-to-free] **Downgrade to Free** cancels the subscription at the end of the period you have paid for. See [What happens if I cancel?](https://transglot.ai/docs/administration/billing/what-happens-if-i-cancel). ## Enterprise [#enterprise] Enterprise is not bought at checkout. Its row offers **Book a call**, and the **Contact sales** card describes it: "A word commitment instead of a cycle allowance, single sign-on and directory provisioning, an audit export, your logo and product name on the quality scorecard you hand to customers, and a contract your legal team can read." ## If it doesn't work [#if-it-doesnt-work] - **"... does not fit what this account holds: ... Remove the excess, then change plan."**: the account has more seats, projects or languages than the lower plan allows. Remove the excess first. - **"Only an account owner can cancel the subscription: ..."**: ask one of the Owners it names. - **"This change deletes workspaces after the grace, so an account owner makes it: ..."** or **"This change stops single sign-on, SCIM or the audit log in a workspace that uses it, so an account owner makes it: ..."**: ask one of the Owners it names. - **"Update the card on file first. Plan changes are paused while a payment is outstanding."**: fix the payment, then try again. See [Invoices and payment](https://transglot.ai/docs/administration/billing/invoices-and-payment). - **"This subscription is already set to end at the close of the billing period."**: you have already moved to Free. To stay on your plan, select **Cancel scheduled change**. - **"Your card was declined. Nothing changed."**: update the card in the payment portal and try again. ## Related [#related] - [Your plan and seats](https://transglot.ai/docs/administration/billing/your-plan-and-seats) - [What happens if I cancel?](https://transglot.ai/docs/administration/billing/what-happens-if-i-cancel) - [Apply a promo code](https://transglot.ai/docs/administration/billing/apply-a-promo-code) # Close an account (https://transglot.ai/docs/administration/billing/close-an-account/) **Who can do this:** the Owners of the account. Closing an account is how you stop paying and remove everything the account holds. To keep using transglot on Free instead, [move the account to Free](https://transglot.ai/docs/administration/billing/what-happens-if-i-cancel). ## Before you start [#before-you-start] - Export anything you need from the account's workspaces. - To keep a workspace, [move it to another account](https://transglot.ai/docs/administration/workspace/move-a-workspace-to-another-account) first. - Read the summary under **Close this account**. For an account with a subscription, closing "Cancels the" plan's "subscription today, with no refund of the time left, and bills usage beyond the plan so far" on a final invoice. ### Open the account's General settings [#open-the-accounts-general-settings] Open **Settings**, choose the **Account** tab, then **General**. ### Start closing [#start-closing] Under **Close this account**, select **Close account**. ### Confirm it is you [#confirm-it-is-you] Closing needs a confirmation from the last 15 minutes. Choose **Confirm your password**, or **Email me a code** and enter the 6-digit code under **Code from the email**, then select **Check code**. ### Type the account name [#type-the-account-name] Type the account's name into the **Account name** box exactly as it is written. ### Confirm [#confirm] Select **Close account**. ## What happens next [#what-happens-next] - The subscription is cancelled and the final bill is issued, usually within 15 minutes. The final invoice is emailed to the billing address. - Every workspace of the account becomes read-only straight away, and is deleted after the date shown. - The account's **General** page reads **Closing on** followed by the day closing finishes and the timezone that day is written in, for example **Closing on 9 Nov, UTC**. Any Owner can select **Cancel closing** until the workspaces are deleted. - Invoices stay available to the account's Owners and Billing members after the account has closed, until the date shown on **Plan & billing**. An account with no workspaces that has never been billed closes at once. ## Cancel closing [#cancel-closing] Select **Cancel closing** on the account's **General** page, or on the banner in any of its workspaces, then confirm. The workspaces are no longer scheduled for deletion. If the subscription was already cancelled, it cannot be restored, the time left on it is not refunded, and the account stays open on Free: subscribe again to get your plan back. Workspaces beyond what Free includes then become read-only, as they do after [a move to Free](https://transglot.ai/docs/administration/billing/what-happens-if-i-cancel). If you cancel within the first few minutes, while the final bill is still being issued, the page asks you to wait until that is done. ## If it doesn't work [#if-it-doesnt-work] - **"Type ... exactly as it is written to close it."**: the name must match exactly. - **"Too many wrong codes. Send a new one."** or **"That code has expired. Send a new one."**: select **Email me a code** again. - **"Verify your email address first, then send a code to it."**: confirm your email address, or use **Confirm your password**. - **"... is closed. Its invoices stay available; start a new account to translate again."**: a closed account cannot be reopened. ## Related [#related] - [What happens if I cancel?](https://transglot.ai/docs/administration/billing/what-happens-if-i-cancel) - [Delete a workspace](https://transglot.ai/docs/administration/workspace/delete-a-workspace) - [Invoices and payment](https://transglot.ai/docs/administration/billing/invoices-and-payment) # Invoices and payment (https://transglot.ai/docs/administration/billing/invoices-and-payment/) **Who can do this:** the Owners and Billing members of the account. Everything here is on the account's **Plan & billing** page: open **Settings**, choose the **Account** tab, then **Plan & billing**. Card details, tax details and the billing email are edited in the payment portal, run by our payment provider, Stripe. They never reach transglot. ## Download an invoice [#download-an-invoice] ### Find the invoice [#find-the-invoice] Scroll to **Invoices**. Each row shows the invoice, its **Date**, **Amount** and **Status**. ### Download it [#download-it] Select **Download** on its row. The PDFs are hosted by Stripe. Free accounts are not invoiced, so the list starts with the first paid cycle. An account on a trial shows "No invoices yet. The first one is issued on" the date the trial converts. ## Change the card or billing details [#change-the-card-or-billing-details] ### Open the payment portal [#open-the-payment-portal] Select **Manage payment** at the top of **Plan & billing**, or **Update card** on the **Payment** card. ### Make the change [#make-the-change] In the portal, change the card, the billing address, the tax details or the billing email. ### Return to transglot [#return-to-transglot] Go back to **Plan & billing**. The **Payment** card shows the card on file, for example "Visa ending 4242", and when it expires. **Company details** shows what invoices are addressed to: the **Name**, **Country** and **Billing email**. A billing email that belongs to nobody on the account is marked "not a member of this account". Select **Edit in the payment portal** to change them. Renaming the account does not change invoices already issued. ## When a payment fails [#when-a-payment-fails] If a payment cannot be collected, **Plan & billing** shows **Payment failed.** and asks you to **Update your card** to keep your plan. Every workspace of the account shows a banner too, because "translation, pushes and new members are paused" in them. "Reading, editing and export still work." ### Update the card [#update-the-card] Select **Update card** and replace the card in the payment portal. If your bank needs to confirm the payment, select **Confirm the payment** instead. ### Check the payment [#check-the-payment] Back on **Plan & billing**, select **Check payment again**. When the payment has gone through, the message reads "Payment confirmed. Translation has resumed." Translation runs that the failed payment stopped restart on their own if the payment goes through within 72 hours. Older runs are not restarted, because their source may have changed since; the workspace's developers are told instead. While a payment is outstanding, plan changes are paused. If a renewal invoice stays unpaid for 30 days after its payment first failed, the subscription ends. The invoice stays payable, and the account stays paused until it is paid. ## If it doesn't work [#if-it-doesnt-work] - **"Invoices could not be loaded from Stripe just now. Your plan and card are unaffected."**: select **Try again**. - **"Stripe has not confirmed a payment yet."**: wait a moment and check again. **Check payment again** can be used once a minute. - **There is no Manage payment button**: the account has never bought a plan. A payment method is added at checkout, when a paid plan is bought. ## Related [#related] - [Your plan and seats](https://transglot.ai/docs/administration/billing/your-plan-and-seats) - [Change your plan](https://transglot.ai/docs/administration/billing/change-your-plan) - [What happens if I cancel?](https://transglot.ai/docs/administration/billing/what-happens-if-i-cancel) # The open source plan (https://transglot.ai/docs/administration/billing/open-source-plan/) **Who can apply:** the maintainers of a project with an OSI-approved licence and a public repository. The open source plan gives an account the Team feature set at no cost. It has no price and never appears at checkout: a person reads each application, and transglot moves the account onto the plan when it is approved. ## What it includes [#what-it-includes] - The Team plan's features, including AI translation, the glossary, quality mode, in-context editing and CDN publishing. - The API, the CLI, the GitHub Action and webhooks. - Unlimited projects, languages and seats. - One workspace. - A monthly word allowance that is a hard stop rather than a meter, so the plan can never produce a bill. Usage-based billing is not available on it. The allowance and the full terms are on [transglot.ai/oss](https://transglot.ai/oss). ### Set up your account [#set-up-your-account] [Create your account](https://transglot.ai/docs/first-steps/create-your-account) and the workspace for your project, if you have not already. The plan covers one workspace. ### Open the application form [#open-the-application-form] Go to [transglot.ai/oss](https://transglot.ai/oss) and find **Tell us about the project.** ### Fill in the four fields [#fill-in-the-four-fields] Enter the **Repository URL** (public, and https), choose the **Licence**, give a **Contact email** ("Where the decision goes.") and describe **What does the project do?** in up to 2,000 characters. ### Submit [#submit] Select **Submit application**. The page confirms "Your application is in." ## What happens next [#what-happens-next] Applications are read by hand, and the decision goes to the contact email you gave. There is no published turnaround. The repository URL is stored for a person to read and is never fetched by transglot's servers. When the application is approved, the account's **Plan & billing** page reads "Open source plan, approved" with the date, followed by "One workspace, no card." ## Things to know [#things-to-know] - **One workspace.** The plan covers a single workspace. Moving that workspace to another account ends the account's open source plan, so ask support to move the grant instead. - **Subscribing ends the grant.** If you choose a paid plan on **Plan & billing**, the confirmation warns that subscribing ends the plan transglot provides. - **The README badge.** You can show a badge with your project's published language count in your repository. See [Add a progress badge to your README](https://transglot.ai/docs/guides/delivery/add-a-progress-badge-to-your-readme). ## Related [#related] - [Your plan and seats](https://transglot.ai/docs/administration/billing/your-plan-and-seats) - [What counts towards my usage?](https://transglot.ai/docs/administration/billing/what-counts-towards-my-usage) - [Create your account](https://transglot.ai/docs/first-steps/create-your-account) # Turn on usage-based billing (https://transglot.ai/docs/administration/billing/turn-on-usage-based-billing/) **Who can do this:** the Owners and Billing members of the account, on a paid plan with a working card on file. Decide the cap first. It is a hard stop, not an alert, so pick an amount you are willing to be charged in full in one cycle. ### Open Usage [#open-usage] Open **Settings**, choose the **Account** tab, then **Usage**. ### Turn on usage beyond the plan [#turn-on-usage-beyond-the-plan] Under **Usage-based overage**, turn on **Usage beyond the plan**. The switch saves as you change it. "Usage beyond the plan is on. Set a spend cap below to start paying for words above the plan. Until then translation stops at the included words." ### Set the spend cap [#set-the-spend-cap] Enter the **Spend cap a cycle, in USD**. ### Save the cap [#save-the-cap] Select **Save cap**. "Spend cap saved. It applies to every workspace of the account." ## What happens next [#what-happens-next] Once the included words are used up, translation carries on and the extra words are charged as usage until the cap is reached. At the cap, translation beyond the included words pauses in every workspace of the account until the next cycle or until you raise the cap. A banner names the cap and offers **Raise the cap**. To go back to stopping at the included words, turn off **Usage beyond the plan**. "Words above the plan are no longer charged." Clearing the cap has the same effect: "Translation stops at the included words until you set one." Changes to the cap are recorded in the account log. ## If it doesn't work [#if-it-doesnt-work] - **"Turn on usage beyond the plan before you set a spend cap."**: the cap field is locked until the switch is on. - **"... does not sell words beyond the plan. A paid plan does."**: the account is on Free or the open source plan. See [Change your plan](https://transglot.ai/docs/administration/billing/change-your-plan). - **"Words beyond the trial are not sold."**: usage beyond the plan starts with your first paid cycle. - **"Add a card in the payment portal first, so usage beyond the plan can be collected."** or **"The card on file has expired. Update it in the payment portal first."**: open **Manage payment** on **Plan & billing**. See [Invoices and payment](https://transglot.ai/docs/administration/billing/invoices-and-payment). - **"Usage beyond the plan is billed in USD, and ... pays in another currency. Contact support to change it."** - **"Usage beyond the plan is paused until the bill for ... is settled."**: an earlier cycle's usage has not been paid yet. Included words still work. ## Related [#related] - [Usage-based billing](https://transglot.ai/docs/administration/billing/usage-based-billing) - [What counts towards my usage?](https://transglot.ai/docs/administration/billing/what-counts-towards-my-usage) - [Invoices and payment](https://transglot.ai/docs/administration/billing/invoices-and-payment) # Usage-based billing (https://transglot.ai/docs/administration/billing/usage-based-billing/) **Who can change it:** the Owners and Billing members of the account. Everyone in the account's workspaces is affected by it. Your plan includes an allowance of AI translation each month. Usage is measured against that allowance, and what happens when you exceed it depends on whether usage-based billing is turned on for your workspace. The setting is called **Usage-based overage** and lives on the account's **Usage** page, so it applies to every workspace of the account at once. It is off by default. ## With usage-based billing off [#with-usage-based-billing-off] When the included words are used up, AI translation stops in every workspace of the account until the words reset at the start of the next cycle, or until you change plan. A run that needs more words than are left is refused before it starts, and nothing is charged. Nothing unexpected appears on your bill. Members of each workspace see that "The account this workspace belongs to has no AI words left", with the date the words reset and the names of the people who can change the plan. Work that does not use words carries on: reading, editing by hand, reviewing, exporting, and translations reused from memory. ## With usage-based billing on [#with-usage-based-billing-on] "Words above the plan's allowance are charged at the overage rate, up to the spend cap, instead of pausing translation." Translation carries on past the allowance, and the extra words are charged as usage on top of the plan. The **Spend cap a cycle, in USD** is a ceiling, not a warning. When the account reaches it, translation beyond the included words pauses in every workspace until the next cycle or until you raise the cap, exactly as if usage-based billing were off. With usage-based billing on and no cap set, translation still stops at the included words. While an account is new, the cap may be released in stages: part of it now, and the full amount once earlier usage charges are paid. The card on the **Usage** page shows both figures. The overage rate is listed on [transglot.ai/pricing](https://transglot.ai/pricing). Before a run that would go past the allowance, its confirmation estimates what the extra words will add to this month's bill. ## Who can use it [#who-can-use-it] Usage-based billing is available on the paid plans. It is not available: - on Free or the open source plan, which stop at their allowance; - during a trial ("Words beyond the trial are not sold."); - without a working card on file; - for an account that pays in a currency other than US dollars. The card on the **Usage** page says which of these applies. ## Related [#related] - [Turn on usage-based billing](https://transglot.ai/docs/administration/billing/turn-on-usage-based-billing) - [What counts towards my usage?](https://transglot.ai/docs/administration/billing/what-counts-towards-my-usage) - [Your plan and seats](https://transglot.ai/docs/administration/billing/your-plan-and-seats) # What counts towards my usage? (https://transglot.ai/docs/administration/billing/what-counts-towards-my-usage/) **Who this is for:** anyone watching the account's words. The account's Owners and Billing members see the full figures on the **Usage** page. Your plan includes an allowance of AI translation each month. Usage is measured against that allowance, and what happens when you exceed it depends on whether usage-based billing is turned on for your workspace. See [Usage-based billing](https://transglot.ai/docs/administration/billing/usage-based-billing). The allowance belongs to the account and is shared by every workspace in it. ## What counts [#what-counts] Every word sent to Glossa counts towards your cycle, whatever it was sent for. - **AI translation.** The source words of each string, once for every language it is translated into. Translating a 10-word string into three languages uses 30 words. - **Quality checks.** Scoring a translation sends it to Glossa to read, so the source words of every string in the check count. - **Brand voice drafts.** Asking Glossa to draft a style guide from translations you have already approved counts the words it reads. - **Repairs.** A string that fails a check and is translated again counts its words again, on every try. Auto-heal does this without anyone pressing a button. Before a run starts, its confirmation says how many words it will use and how many are left this cycle. If the run needs more words than you have left and usage-based billing is off, "The run is refused: nothing is started and nothing is charged." AI suggestions, assistant replies and glossary extraction also each have their own limit per cycle, shared by the account's workspaces. The **Plan limits** card on the **Usage** page says when they reset. ## What does not count [#what-does-not-count] - **Reused translations.** When the exact same source text has been translated before, transglot reuses that translation at no cost instead of translating it again. That is what **Words reused at $0** on your dashboard measures, and why keeping translation memory on lowers your usage. - **Anything you or your team write by hand.** - **Reading, filtering and approving.** - **The quality scorecard**, which summarises findings the quality checks have already produced. ## How words are counted [#how-words-are-counted] A word is a run of text between spaces that contains at least one letter or digit, so punctuation, bullets and arrows on their own do not count. Languages written without spaces between words are counted from their characters instead: in Chinese and Japanese each character counts as one word, following the convention translation tools use, and Thai, Lao, Khmer and Burmese are counted as their characters divided by a fixed average word length. ## Where to see your usage [#where-to-see-your-usage] - **Usage**, in the account's settings, shows **Words this cycle** against the allowance, when the words reset, the words used **By workspace**, and the **Last six cycles**. Account Owners can also select **Show projects** to see a workspace's words by project. - Members who reach a whole workspace see that workspace's AI words on its dashboard. - The account's Owners and Billing members are emailed when usage reaches 50%, 75% and 100% of the plan's allowance. ## Keeping usage down [#keeping-usage-down] - Keep translation memory on. It is the biggest single saving. - Adding a language multiplies the work: every untranslated key in it is a translation. - With **auto-translate** on, new and changed source strings are translated as they arrive, without anyone pressing a button. That is usually what you want, and it is also the setting most likely to surprise you on the meter. ## Related [#related] - [Usage-based billing](https://transglot.ai/docs/administration/billing/usage-based-billing) - [Words and usage](https://transglot.ai/docs/concepts/words-and-usage) - [Translation memory](https://transglot.ai/docs/concepts/translation-memory) - [AI translation settings](https://transglot.ai/docs/guides/translating/ai-translation-settings) # What happens if I cancel? (https://transglot.ai/docs/administration/billing/what-happens-if-i-cancel/) **Who can do this:** the Owners of the account. A Billing member cannot cancel: the message names the Owners who can. Cancelling a paid plan means moving the account to Free. The subscription ends at the close of the current billing period, not straight away, and you keep everything your plan gives you until then. ## Cancel the subscription [#cancel-the-subscription] ### Pick Free [#pick-free] Open **Settings**, choose the **Account** tab, then **Plan & billing**. Under **Where you sit**, select **Free**, then **Downgrade to Free**. ### Read what will change [#read-what-will-change] **Move to Free?** states the date the subscription ends, which workspaces become read-only and when they would be deleted, and which seats and projects stop accepting new work. ### Choose the workspace that stays active [#choose-the-workspace-that-stays-active] If the account has more than one workspace, choose one under **Which workspace stays active**. Free includes one workspace. ### Confirm [#confirm] Select **Downgrade to Free**. The message reads "The subscription ends on" the date, and that the account moves to Free then. Until the period ends, **Plan & billing** shows **A change at the period end**. To keep your plan after all, select **Cancel scheduled change**, then confirm. "Cancellation withdrawn." ## When the account moves to Free [#when-the-account-moves-to-free] - **One workspace stays active.** Its projects, keys, translations, glossary and history are all still there. - **Every other workspace becomes read-only** and is deleted after the date shown, unless you upgrade or move it to another account first. Members can still read and export everything in it. An Owner can change which workspace stays active on the account's **Workspaces** page, under **Which workspace stays active on Free**. - **The Free limits apply.** Seats and projects above the Free limits stop accepting new work, and the Free word allowance applies. Limits are listed on [transglot.ai/pricing](https://transglot.ai/pricing). - **Features above Free stop**: the API, CLI and GitHub Action, webhooks, the in-context editor, the glossary and CDN publishing, and on higher plans quality mode, AI profiles, single sign-on, SCIM, the audit log and branding. - **Usage-based billing stops**, because Free does not sell words beyond the plan. If your app loads translations from the CDN, new translations stop being published to it once the account is on Free. Plan for that before the period ends. A person can own one Free account at a time. If an Owner of this account already owns another Free account, this one has no words once it moves to Free: resubscribe, move its workspaces, or close it. ## Cancelling during a trial [#cancelling-during-a-trial] Moving to Free during a trial ends the trial on its end date without a charge. "Trial words stop now, and features stay until then. Nothing is charged." ## Other ways to stop [#other-ways-to-stop] - To stop paying and remove everything, [close the account](https://transglot.ai/docs/administration/billing/close-an-account). - Deleting a workspace does not stop billing. See [Delete a workspace](https://transglot.ai/docs/administration/workspace/delete-a-workspace). ## Related [#related] - [Change your plan](https://transglot.ai/docs/administration/billing/change-your-plan) - [Move a workspace to another account](https://transglot.ai/docs/administration/workspace/move-a-workspace-to-another-account) - [Close an account](https://transglot.ai/docs/administration/billing/close-an-account) # Your plan and seats (https://transglot.ai/docs/administration/billing/your-plan-and-seats/) **Who can use it:** the Owners and Billing members of the account. Workspace roles, Admin included, do not see billing. ## One plan for the whole account [#one-plan-for-the-whole-account] Your workspaces belong to an **account**, and the plan belongs to the account, not to a workspace. One plan covers every workspace in the account: the plan's words are shared by all of them, and its limits on seats, projects, languages and workspaces are counted across all of them. See [Account Owners and Billing members](https://transglot.ai/docs/administration/team/account-owners-and-billing-members) for who manages the account. The plans are **Free**, **Starter**, **Team**, **Business** and **Enterprise**, which is arranged with sales rather than bought at checkout. Open-source projects can apply for the [open source plan](https://transglot.ai/docs/administration/billing/open-source-plan). Current prices and allowances are on [transglot.ai/pricing](https://transglot.ai/pricing). ## The Plan & billing page [#the-plan--billing-page] Open **Settings**, choose the **Account** tab, then **Plan & billing**. The page shows: - **The header**: the plan and when it renews, and **Manage payment**, which opens the payment portal. - **Plan**: the **Current plan**, its **Plan price**, when it **Renews**, the **Words included** each cycle, and the plan's **Workspaces**, **Seats**, **Projects** and **Languages**, and its **Translation speed**. On Free, it also shows your **Welcome words**. - **Payment** and **Company details**: the card on file and what invoices are addressed to. See [Invoices and payment](https://transglot.ai/docs/administration/billing/invoices-and-payment). - **Where you sit**: every plan side by side, with a **Pay annually** switch and the one action that changes your plan. See [Change your plan](https://transglot.ai/docs/administration/billing/change-your-plan). - **Compare in detail**: what each plan includes, row by row. - **Invoices** and **Promo code**. The words you have used this cycle are on the account's **Usage** page. See [What counts towards my usage?](https://transglot.ai/docs/administration/billing/what-counts-towards-my-usage). ## How the limits are counted [#how-the-limits-are-counted] The **Plan limits** card on the **Usage** page lists what your plan allows across the account, and what counts: | Limit | What counts | | --- | --- | | **Seats** | "Anyone in any workspace of the account, account owners included, counted once." | | **Projects** | "Every project in every workspace of the account." | | **Languages** | "Every distinct language across the account's projects, the source language included." A language one project already uses is free to add to another. | | **Workspaces** | "Every workspace of the account." Free and the open source plan include one workspace. | A waiting invitation holds a seat until it is accepted or expires. Team and the plans above it have no seat limit. When a limit is reached, the action that would pass it is refused, and the message names the account's Owners, who can upgrade or free room. ## What each plan adds [#what-each-plan-adds] | Feature | Lowest plan that includes it | | --- | --- | | API, CLI and GitHub Action | Starter | | Webhooks | Starter | | In-context editor | Starter | | Glossary | Starter | | CDN publishing | Starter | | Quality mode | Team | | AI profiles | Team | | Service level agreement | Business | | Single sign-on | Enterprise | | SCIM provisioning | Enterprise | | Audit log | Enterprise | | White labelling | Enterprise | | Priority support | Enterprise | Roles, custom roles and per-project and per-language access are included on every plan, Free too. **Translation speed** also rises with the plan: **Normal** on Free and Starter, **High** on Team, and **Max speed** on Business and Enterprise. ## Welcome words on Free [#welcome-words-on-free] A new Free account receives welcome words on top of the monthly allowance. Part arrives when the account is activated, and the rest after a one-time mobile number check on the account's **Welcome words** page ("Each number works once."). Welcome words come once per person. The cycle's words are spent first, so a quiet month costs the welcome words nothing. Welcome words are used only on Free. The amounts are listed on [transglot.ai/pricing](https://transglot.ai/pricing). ## Related [#related] - [Change your plan](https://transglot.ai/docs/administration/billing/change-your-plan) - [What counts towards my usage?](https://transglot.ai/docs/administration/billing/what-counts-towards-my-usage) - [Usage-based billing](https://transglot.ai/docs/administration/billing/usage-based-billing) - [Invoices and payment](https://transglot.ai/docs/administration/billing/invoices-and-payment) # Administration (https://transglot.ai/docs/administration/) Administration in transglot happens on three levels, each with its own tab under **Settings**: - **You**: your own profile, password and sign-in methods, notifications and appearance. - **Workspace**: the people in one workspace, their roles, its security, single sign-on and branding. - **Account**: the plan, usage, invoices and the Owners who manage the workspaces the account pays for. ## Common tasks [#common-tasks] | I want to | Go to | | --- | --- | | Add a teammate | [Invite a teammate](https://transglot.ai/docs/administration/team/invite-a-teammate) | | Give an agency access to one project | [Onboard a vendor or freelancer](https://transglot.ai/docs/administration/team/onboard-a-vendor-or-freelancer) | | See what each role can do | [Roles and what each one can do](https://transglot.ai/docs/administration/team/roles-and-what-each-one-can-do) | | Hand the account to someone else | [Account Owners and Billing members](https://transglot.ai/docs/administration/team/account-owners-and-billing-members) | | Protect my login | [Turn on two-factor authentication](https://transglot.ai/docs/administration/security/turn-on-two-factor-authentication) | | Make everyone use a second factor | [Require two-factor for a workspace](https://transglot.ai/docs/administration/security/require-two-factor-for-a-workspace) | | Connect our identity provider | [Set up single sign-on](https://transglot.ai/docs/administration/security/set-up-single-sign-on) | | Upgrade or switch to annual billing | [Change your plan](https://transglot.ai/docs/administration/billing/change-your-plan) | | Keep translating past the plan's words | [Turn on usage-based billing](https://transglot.ai/docs/administration/billing/turn-on-usage-based-billing) | | Download an invoice | [Invoices and payment](https://transglot.ai/docs/administration/billing/invoices-and-payment) | ## Who manages what [#who-manages-what] | Role | What they manage | | --- | --- | | **Account Owner** | Everything: the plan and billing, every workspace of the account, single sign-on, SCIM, workspace two-factor requirements, branding, and deleting workspaces. | | **Billing member** | The plan, the card, invoices, usage and company details. No access to workspaces. | | **Admin** | One workspace: its members, roles, projects, access and integrations. Not billing. | | **Developer, Reviewer, Translator** | Their own work in the workspace. See [Roles and what each one can do](https://transglot.ai/docs/administration/team/roles-and-what-each-one-can-do). | # Add a passkey (https://transglot.ai/docs/administration/security/add-a-passkey/) **Who can do this:** everyone, for their own login. A passkey "never leaves the device and there is nothing to remember." It also counts as a second factor wherever a workspace requires two-factor authentication. ### Open Password & sign-in [#open-password--sign-in] Open **Settings**, choose the **You** tab, then **Password & sign-in**. Confirm your password if you are asked. ### Add a passkey [#add-a-passkey] Under **Passkeys**, select **Add passkey**. ### Name it [#name-it] Enter a **Passkey name**, for example `MacBook Pro` or `iPhone`. "A name helps you identify this passkey later." ### Register it [#register-it] Select **Register passkey** and follow your device's prompt: fingerprint, face, device PIN or a hardware security key. ## What happens next [#what-happens-next] The passkey is listed under **Passkeys** with when it was **Added** and **Last used**. On the sign-in page you can now use it instead of typing your password. Add one on each device you sign in from regularly. ## Remove a passkey [#remove-a-passkey] Select **Remove** on its row, then **Remove passkey**. Removal is immediate: you can no longer use it to sign in. Keep another way in, such as your password or another passkey, before you remove your last one. ## If it doesn't work [#if-it-doesnt-work] - **"Passkeys are not supported in this browser."**: use a browser or device that supports passkeys, or keep signing in with your password and two-factor authentication. - **The device prompt does not appear**: check that your device has a screen lock or biometrics set up, then try again. ## Related [#related] - [Secure your account](https://transglot.ai/docs/administration/security/secure-your-account) - [Turn on two-factor authentication](https://transglot.ai/docs/administration/security/turn-on-two-factor-authentication) - [Sign in to transglot](https://transglot.ai/docs/first-steps/sign-in) # Require two-factor for a workspace (https://transglot.ai/docs/administration/security/require-two-factor-for-a-workspace/) **Who can do this:** the Owners of the account that holds the workspace. The requirement is available on every plan. With the requirement on, "Every member needs an authenticator app or a passkey on their login before they can open this workspace." It requires that each member holds a second factor, not that every sign-in asks for one: someone returning through a linked Google or GitHub sign-in is still signed straight in. ## Before you start [#before-you-start] Add a second factor to your own login first: [turn on two-factor authentication](https://transglot.ai/docs/administration/security/turn-on-two-factor-authentication) or [add a passkey](https://transglot.ai/docs/administration/security/add-a-passkey). The page refuses to turn the requirement on while you hold none, because it would lock you out of the workspace. ### Open Security [#open-security] Open **Settings**, choose the **Workspace** tab, then **Security**. ### Check who it affects [#check-who-it-affects] **Who this affects** lists the members who do not hold a second factor yet. "Turning the requirement on now would close this workspace to them until they add an authenticator app or a passkey." ### Turn it on [#turn-it-on] Turn on **Require two-factor for everyone**. The confirmation says how many members must still add one. ### Confirm [#confirm] Select **Require two-factor**. The message reads "Two-factor is now required for everyone in this workspace." ## What happens next [#what-happens-next] - Members who already hold a second factor notice nothing. - Each member without one gets an email explaining what to do. Until they add one, opening the workspace takes them to their own security settings to set one up. Their login is not locked. - Only the members who were short a factor when you turned the requirement on are emailed. Anyone who joins later, or removes their last passkey or turns off their authenticator, appears under **Who this affects** but is not told. - The change is recorded in the audit log. The **People** page shows the requirement under **Policy**, and its two-factor column shows who holds a second factor. ## Who is exempt [#who-is-exempt] A member is exempt when an identity provider that enforces sign-in claims their email domain, even when that provider is configured in another workspace. The exception is an Owner of the account whose workspace configured the provider: an Owner keeps a password way in, so they are not exempt. See [Set up single sign-on](https://transglot.ai/docs/administration/security/set-up-single-sign-on). ## Turn the requirement off [#turn-the-requirement-off] Turn off **Require two-factor for everyone**, then select **Stop requiring two-factor**. "Members can open" the workspace "without an authenticator app or a passkey. Members who already added one keep it, and nothing else about signing in changes." ## If it doesn't work [#if-it-doesnt-work] - **"Turn on an authenticator app or add a passkey for your own login first."**: add a second factor to your own login, then try again. - **You are locked out of People**: the **People** page sits behind the requirement too. If it is keeping you out, turn the requirement off on **Security** first. - **You do not see Security in the menu**: you are not an Owner of the account that holds the workspace. ## Related [#related] - [Turn on two-factor authentication](https://transglot.ai/docs/administration/security/turn-on-two-factor-authentication) - [Add a passkey](https://transglot.ai/docs/administration/security/add-a-passkey) - [The People page](https://transglot.ai/docs/administration/team/members-and-roles) # Secure your account (https://transglot.ai/docs/administration/security/secure-your-account/) **Who can do this:** everyone, for their own login. Open **Settings**, choose the **You** tab, then **Password & sign-in**. You may be asked to confirm your password before the page opens. The page lists "Every way in to your login, and the way to take each of them back." ## Password [#password] Enter your **Current password** ("Confirms it is really you making the change."), then the **New password** and **Confirm password**, and select **Save**. Changing your password signs every other session out. Use a long, random password kept in a password manager. ## Two-factor authentication [#two-factor-authentication] The **Authenticator app** row adds a one-time code from an app on your phone to every sign-in with your password. When you turn it on you receive recovery codes, which are your way back in if you lose the phone. See [Turn on two-factor authentication](https://transglot.ai/docs/administration/security/turn-on-two-factor-authentication). ## Passkeys [#passkeys] A passkey "signs you in with the device you are holding": your fingerprint, face, device PIN or a hardware security key, instead of a password. See [Add a passkey](https://transglot.ai/docs/administration/security/add-a-passkey). ## Connected accounts [#connected-accounts] If you signed up or signed in with Google or GitHub, that sign-in is listed under **Connected accounts**, with when it was connected and last used. A connected account signs you in without your password, and it keeps working after you change your password. To revoke one, select **Disconnect** on its row and confirm. "Disconnecting takes effect immediately. If you find you cannot sign in afterwards, request a password reset from the sign-in page and follow the link sent to the email address you sign in with." ## A strong setup [#a-strong-setup] The methods are independent, and you can hold all of them at once: - A passkey on each device you sign in from, for everyday use. - A password with two-factor authentication as the fallback. - Your recovery codes stored somewhere other than the phone that holds your authenticator app. A workspace can also require every member to hold a second factor. An authenticator app or a passkey both count. See [Require two-factor for a workspace](https://transglot.ai/docs/administration/security/require-two-factor-for-a-workspace). ## Related [#related] - [Turn on two-factor authentication](https://transglot.ai/docs/administration/security/turn-on-two-factor-authentication) - [Add a passkey](https://transglot.ai/docs/administration/security/add-a-passkey) - [Sign in to transglot](https://transglot.ai/docs/first-steps/sign-in) - [I can't sign in](https://transglot.ai/docs/resources/troubleshooting/i-cant-sign-in) # Set up SCIM provisioning (https://transglot.ai/docs/administration/security/set-up-scim-provisioning/) **Who can do this:** the Owners of the account that holds the workspace, on the Enterprise plan. Set up and test [single sign-on](https://transglot.ai/docs/administration/security/set-up-single-sign-on) first, including verifying your email domain. SCIM only provisions email addresses at the connection's verified domain, and the people it provisions sign in through your identity provider. ### Open Single sign-on & SCIM [#open-single-sign-on--scim] Open **Settings**, choose the **Workspace** tab, then **Single sign-on & SCIM**, and find **4. Directory sync (SCIM 2.0)**. ### Enable SCIM [#enable-scim] Select **Enable SCIM**. ### Copy the token [#copy-the-token] **Copy your SCIM token** shows the bearer token. "Shown only once. Paste it into your IdP's SCIM connector now. We store only its hash and cannot recover it." Select **Copy**, then **Done**. ### Configure your identity provider [#configure-your-identity-provider] In your identity provider's SCIM connector, enter the **SCIM base URL**, `https://api.transglot.ai/scim/v2`, and the token as a bearer token. Then assign the users and groups who should have access. ### Map directory groups to roles (optional) [#map-directory-groups-to-roles-optional] Under **Directory groups to roles**, select **Add mapping** for each group whose members should get a role other than Translator. See below. ## What your directory can do [#what-your-directory-can-do] | Your directory does | What happens in the workspace | | --- | --- | | Assigns a user | They are added as a member. Unmapped users arrive as Translator. | | Changes a user's role or group | Their role changes, within the limits below. | | Deactivates a user (`active: false`) or deletes them | They are removed from this workspace. Their login is kept, because they may belong to other workspaces. | | Pushes a group you mapped | Every member your directory puts in that group holds the role you chose. | "Deactivating a user in your directory revokes their access here, except an Owner of" the account, "who keeps access through the account until you remove them from it." The account's Owners get an email when that happens. Every user provisioned or deprovisioned through SCIM is recorded in the [audit log](https://transglot.ai/docs/administration/security/the-audit-log). ## Map directory groups to roles [#map-directory-groups-to-roles] ### Add a mapping [#add-a-mapping] Under **Directory groups to roles**, select **Add mapping**. ### Identify the group [#identify-the-group] Fill in the **Group ID**, the **Group name**, or both. The group ID is "Your provider's stable id for the group. Survives a rename." If you give both, the group ID is what transglot matches on. Group names are matched ignoring capitalisation. ### Choose the role [#choose-the-role] Choose the **Role**: Admin, Developer, Reviewer, Translator or one of your custom roles. Select **Create mapping**. "Group mapping created. It is applied on the next directory sync." How roles are decided: - **A group you mapped** is applied exactly as you set it, on every sync, because you created the mapping inside transglot. - **A group or role you have not mapped** is raw directory data. A value named after a role the workspace has (such as `admin`, `developer`, `reviewer` or `translator`) is honoured up to Admin. Anything else, including a group called "owner", gives Translator. A mis-sync in your directory can never hand out more than an admin holds. - **No sync, mapped or not, makes anyone an Owner of the account.** Account Owners are set on the account's **Owners & billing members** page. - **Map every group you want kept in sync.** The groups your provider can read back are the built-in roles and the groups you mapped. An unmapped group is applied once, when your provider first pushes it, and your provider's next update to it fails with "Group not found". Removing a mapping does not change anyone's current role. "Members already at that role keep it until your next directory sync, which is when the mapping stops being applied." ## Rotate the token or turn SCIM off [#rotate-the-token-or-turn-scim-off] - **Rotate token** issues a new token and stops the old one working. Do this if the token may have been exposed, and update your identity provider straight away, because provisioning stops until you do. - **Disable SCIM**: "The bearer token stops working and your directory stops syncing, so joiners and leavers will no longer reach this workspace on their own. Members already here keep their access. Turning SCIM back on mints a new token." If the account moves off the Enterprise plan, provisioning stops at once. Your directory can still read and remove members for 30 days afterwards, so leavers are still removed. ## Technical reference [#technical-reference] | Item | Value | | --- | --- | | Base URL | `https://api.transglot.ai/scim/v2` | | Authentication | `Authorization: Bearer ` | | Users | `GET /Users` (paged with `startIndex` and `count`, at most 100 a page), `POST /Users`, `GET /Users/`, `PUT` or `PATCH /Users/`, `DELETE /Users/` | | Groups | `GET /Groups`, `POST /Groups`, `PUT` or `PATCH /Groups/`, `DELETE /Groups/` | | Filter | `userName eq "person@example.com"` | | User attributes read | `userName` (or the first `emails` value), `displayName` (or `name.givenName` and `name.familyName`), `active`, and an optional `role` or `roles` | | Rate limit | 120 requests a minute per token | `userName` must be an email address at the connection's verified domain. Other addresses are refused with an error naming the domain. ## If it doesn't work [#if-it-doesnt-work] - **The token was not copied**: rotate it and copy the new one. A token cannot be recovered. - **Provisioning fails**: check the base URL is exactly `https://api.transglot.ai/scim/v2`, including the version segment, and that the token is current. - **"userName must be an address at a verified domain."**: verify your email domain under single sign-on first. - **Everyone lands as Translator**: the group has no mapping, or the mapping's group ID does not match the one your provider sends. Try mapping on the group name instead. - **Your provider reports "Group not found" for a group it synced before**: that group has no mapping. Add one, keyed on the group ID your provider sends. - **"Another mapping already claims that directory group ID."** or **"Another mapping already claims that directory group name."**: edit the existing mapping instead. Names are compared ignoring capitalisation, so `Engineering-Leads` and `engineering-leads` are the same name. - **"... is the name of a built-in role, which is already its own group here. Use your directory group's own ID or name instead."**: a group named `admin`, `developer`, `reviewer` or `translator` already provisions that role without a mapping. ## Related [#related] - [Set up single sign-on](https://transglot.ai/docs/administration/security/set-up-single-sign-on) - [The People page](https://transglot.ai/docs/administration/team/members-and-roles) - [The audit log](https://transglot.ai/docs/administration/security/the-audit-log) # Set up single sign-on (https://transglot.ai/docs/administration/security/set-up-single-sign-on/) **Who can do this:** the Owners of the account that holds the workspace, on the Enterprise plan. Admins cannot: identity configuration decides who may enter the workspace at all, and an admin who could change it could give themselves a way back in after being removed. The page puts it this way: "Configure it yourself: no support ticket, no engineering call. Verify your domain, test the connection, then enforce." ## Before you start [#before-you-start] - Admin access to your identity provider, to create an application for transglot. - Control of your email domain's DNS, to publish a TXT record. - Choose the protocol. It "cannot be changed once the connection is saved." | Protocol | Use it for | What you enter in transglot | | --- | --- | --- | | **SAML 2.0 (self-managed)** | Any SAML 2.0 identity provider, such as Okta, Microsoft Entra ID, Google Workspace or JumpCloud | **IdP entity ID**, **SSO URL**, **Signing certificate** | | **OpenID Connect** | Any OpenID Connect provider, such as Microsoft Entra ID, Google Workspace, Okta, Auth0 or Keycloak | **Issuer URL**, **Client ID**, **Client secret** | ### Open Single sign-on & SCIM [#open-single-sign-on--scim] Open **Settings**, choose the **Workspace** tab, then **Single sign-on & SCIM**. ### Describe your identity provider [#describe-your-identity-provider] Under **1. Your identity provider**, choose the **Protocol** and enter your **Email domain**, for example `acme.com`. Then fill in the protocol's fields (see below) and select **Save configuration**. ### Prove you own the domain [#prove-you-own-the-domain] Under **2. Prove you own** your domain, publish the TXT record shown on your domain's DNS. Its value starts with `transglot-domain-verification=`. Then select **Verify domain**. "Until the domain is verified, SSO cannot be enforced." ### Give your provider transglot's values [#give-your-provider-transglots-values] Copy the values under **Values for your identity provider** into the application you created at your provider (see below). Copy them rather than typing them: a value that differs by one character fails at sign-in with an error from your provider. ### Test the connection [#test-the-connection] Under **3. Test the connection**, select **Test connection**. The result lists each check that passed, or the first one that failed. ### Require SSO [#require-sso] Once the domain is verified and the test passes, turn on **Require SSO for everyone in** your workspace, read the confirmation, then select **Require SSO**. ## SAML 2.0 fields [#saml-20-fields] In transglot: | Field | What to enter | | --- | --- | | **IdP entity ID** | "The issuer / entity ID your identity provider publishes, e.g. http://www.okta.com/exk1a2b3c." | | **SSO URL** | "The IdP's HTTP-POST single-sign-on endpoint. Must be a public https URL." | | **Signing certificate** | "The IdP's X.509 public certificate (PEM). Write-only: once saved, we only ever tell you whether it is present." | At your identity provider, from **Values for your identity provider**: | Value | Pattern | | --- | --- | | **Assertion consumer service (ACS) URL** | `https://app.transglot.ai/sso//acs` | | **Service provider entity ID (audience)** | shown on the page | | **SP metadata URL** | `https://app.transglot.ai/sso//metadata` | `` is specific to your workspace's connection. The values appear once the configuration is saved. transglot reads the person's email address from the SAML NameID, or from an `email`, `mail` or `emailaddress` attribute. It reads their name from `displayName` or `name`, or `givenName` and `sn`, and an optional role from a `role` or `roles` attribute. ## OpenID Connect fields [#openid-connect-fields] In transglot: | Field | What to enter | | --- | --- | | **Issuer URL** | Your provider's issuer. For Microsoft Entra ID, `https://login.microsoftonline.com//v2.0`, using the tenant ID rather than `common`. For Google Workspace, `https://accounts.google.com`. transglot reads the provider's `/.well-known/openid-configuration` from it. | | **Client ID** | "The application (client) ID your provider issued for transglot." | | **Client secret** | "Write-only: once saved, we only ever tell you whether it is present." | At your identity provider, register the **Redirect URI (callback)** shown on the page. Its pattern is `https://app.transglot.ai/sso//oidc/callback`. transglot asks for the `openid email profile` scopes, and reads the person's email address, their name, and an optional `role` or `roles` claim. ## What happens when SSO is required [#what-happens-when-sso-is-required] "Everyone with an email address ending in @" your domain "is sent to your identity provider when they sign in with a password or a passkey, and signing in with Google or GitHub stops working for them." People with an address at any other domain sign in as they do now. The Owners of the account keep their password and passkey sign-in, so a broken connection cannot lock you out. Someone has to be able to get in and turn enforcement off if the identity provider is unreachable. ## Who gets in, and with what role [#who-gets-in-and-with-what-role] - **Existing members** sign in as they are. An SSO sign-in never changes an existing member's role. - **New people at your verified domain** are added to the workspace the first time they sign in through your identity provider, as long as the workspace can take new members. They arrive as Translator. - **A role sent by your provider** (`role` or `roles`) is honoured up to Admin, and only when the person is first added. "owner", an unknown value or no value gives Translator. - **What SSO can never grant**: it never makes anyone an Owner of the account. Account Owners are set on the account's **Owners & billing members** page. The sign-in only works for an email address at the connection's verified domain, which is what stops one workspace's identity provider signing people into another. To keep roles in step with your directory after people join, set up SCIM. See [Set up SCIM provisioning](https://transglot.ai/docs/administration/security/set-up-scim-provisioning). ## Change or remove the connection [#change-or-remove-the-connection] - Changing the **Email domain** resets its verification and its connection test, so enforcement stays off until both pass again. - Pointing the connection at a different identity provider resets the test and enforcement until you test the new one. - To stop requiring SSO, turn off **Require SSO for everyone in** your workspace and select **Stop requiring SSO**. People at your domain can then sign in with a password, a passkey, Google or GitHub again. - **Remove SSO** deletes the connection and its SCIM token. "Everyone keeps their login and signs in with a password again, so make sure they have one set before you do this." Select **Remove connection** to confirm. Every configuration change, connection test and SSO sign-in is recorded in the [audit log](https://transglot.ai/docs/administration/security/the-audit-log). ## If it doesn't work [#if-it-doesnt-work] - **"Verify the domain and pass a connection test to unlock enforcement."**: the switch stays locked until both are done. Enforcing an untested connection could lock everyone out at once. - **"No matching TXT record found on ... DNS can take a few minutes to propagate."**: wait, then select **Verify domain** again. - **"... is already verified by another workspace."**: a domain can be verified by one workspace only. - **"The signing certificate could not be parsed."** or **"That does not look like a PEM-encoded X.509 certificate."**: paste the full PEM certificate, including its BEGIN and END lines. - **"The signing certificate expired on ..."**: download the current certificate from your provider. - **"The issuer URL must use https."**: use your provider's https issuer. - **"The issuer has changed, so the stored client secret no longer applies. Enter the secret from the new provider."** - **"Enforcement is configured. It activates once an SSO provider with a login flow is connected. Until then, password login is not refused."**: finish the protocol's fields and verify the domain. - **People cannot sign in**: see [My single sign-on login fails](https://transglot.ai/docs/resources/troubleshooting/my-single-sign-on-login-fails). ## Related [#related] - [Set up SCIM provisioning](https://transglot.ai/docs/administration/security/set-up-scim-provisioning) - [Require two-factor for a workspace](https://transglot.ai/docs/administration/security/require-two-factor-for-a-workspace) - [My single sign-on login fails](https://transglot.ai/docs/resources/troubleshooting/my-single-sign-on-login-fails) # The audit log (https://transglot.ai/docs/administration/security/the-audit-log/) **Who can read it:** admins and the Owners of the account that holds the workspace, on the Enterprise plan. Members whose custom role includes **Read the audit log** can read it too. The workspace audit log is "Every security-relevant act in this workspace: who let someone in, who granted power, who minted a credential. Append-only: entries are never edited or removed." Entries are kept for as long as the workspace exists, and are removed with it when a workspace is deleted. transglot records these events on every plan. Reading and exporting the log is an Enterprise feature, so if you move to Enterprise, earlier entries are already there. ## Read the log [#read-the-log] ### Open the audit log [#open-the-audit-log] Open **Settings**, choose the **Workspace** tab, then **Audit log**. ### Filter by action [#filter-by-action] Under **Events**, entries are listed newest first, each with the act, who did it (or "by the system"), its **Category** and **When**. Use **Filter by action** to show one kind of event, or **All actions** for everything. ### Export to CSV [#export-to-csv] Select **Export CSV** to download the log, or the filtered part of it, as a spreadsheet. The export holds every matching entry, with the time, action, the person's name and email address, what the act was about, the IP address and the details recorded with it. ## What is recorded [#what-is-recorded] | Area | Examples of recorded acts | | --- | --- | | People and access | Member invited, joined, role changed or removed; invitations revoked or sent again; custom roles created, updated or deleted; review policy changed; two-factor requirement changed | | Credentials | API tokens, publishable keys and CDN keys created or revoked; translations exported; export links ended | | Integrations | Webhook endpoints; the Slack channel; Jira, GitHub, GitLab, Bitbucket and Azure Repos; CMS, Dropbox, Google Drive and S3 connections; integration add-ons; migration imports | | Identity | SSO configured, tested or disabled; SSO sign-ins; SCIM token rotated or SCIM disabled; users provisioned or deprovisioned through SCIM; directory group mappings | | Assignments | Translators assigned; assignments started, submitted, approved, sent back or withdrawn | | Workspace and projects | Workspace renamed, moved to another account, deletion requested or cancelled; project web address changed; languages removed; projects deleted; branding updated | The audit log is the security record. A project's activity feed is the product history of keys, translations and runs. Use activity to find out when a string changed, and the audit log to find out who was given the ability to change it. See [Project activity](https://transglot.ai/docs/guides/projects/project-activity). ## The account log [#the-account-log] Changes to the account itself (its plan, card, invoices, seats, people and workspaces) are recorded in a separate **Account log**. Open **Settings**, choose the **Account** tab, then **Account log**. Use **Filter by category** to narrow it. - Account Owners and Billing members can read the account log, on every plan. - Billing members see the billing entries only, without the **IP address** column. - Only account Owners can select **Export CSV**. ## If it doesn't work [#if-it-doesnt-work] - **"Audit log is an Enterprise feature"**: "We record every security-relevant act regardless of plan. Reading and exporting the trail is an Enterprise feature." The panel has a **Contact sales** button, because Enterprise is arranged with sales rather than bought at checkout. - **"Nothing to audit yet"**: nothing auditable has happened in this workspace yet. - **"No entries for that action."**: the filter matches nothing. Choose **All actions**. - **Audit log is not in the Workspace settings menu**: your role does not include reading the audit log. ## Related [#related] - [Roles and what each one can do](https://transglot.ai/docs/administration/team/roles-and-what-each-one-can-do) - [Set up SCIM provisioning](https://transglot.ai/docs/administration/security/set-up-scim-provisioning) - [Project activity](https://transglot.ai/docs/guides/projects/project-activity) # Turn on two-factor authentication (https://transglot.ai/docs/administration/security/turn-on-two-factor-authentication/) **Who can do this:** everyone, for their own login. It matters most for admins and account Owners, who control access and billing. Before you start, install an authenticator app that supports time-based codes (TOTP) on your phone, and decide where you will keep your recovery codes. Do not keep them on the same phone. ### Open Password & sign-in [#open-password--sign-in] Open **Settings**, choose the **You** tab, then **Password & sign-in**. Confirm your password if you are asked. ### Start the setup [#start-the-setup] Under **Two-factor authentication**, in the **Authenticator app** row, select **Enable 2FA**. ### Pair your app [#pair-your-app] Scan the QR code with your authenticator app, or enter the setup key shown under "or, enter the code manually". Select **Continue**. ### Enter a code [#enter-a-code] Type the 6-digit code your app shows, then select **Continue**. ### Save your recovery codes [#save-your-recovery-codes] Under **2FA recovery codes**, select **View recovery codes** and store them somewhere safe, such as a password manager. ## What happens next [#what-happens-next] The **Authenticator app** row shows **Enabled**. From now on, signing in with your password also asks for a code from your app. Recovery codes let you get back in if you lose the phone. Each code works once. To replace them, select **Regenerate codes**: "The recovery codes you have now stop working immediately, including any you printed or saved." If you closed the setup before entering a code, the row offers **Continue setup** so you can finish with the code you already scanned. ## Turn two-factor off [#turn-two-factor-off] Select **Disable 2FA**. The confirmation explains that signing in with your password will no longer ask for a code and that your recovery codes stop working. It also names any workspace that requires two-factor and would stop opening for you, unless you still hold a passkey, which counts as a second factor. Select **Turn off two-factor** to confirm. ## If it doesn't work [#if-it-doesnt-work] - **The code is rejected**: check that your phone sets its clock automatically. Time-based codes fail when the clock drifts. - **You lost the phone**: on the code screen when you sign in, choose **log in using a recovery code** and enter one of your codes. Then set up two-factor again on the new phone. - **You lost both the phone and the recovery codes**: contact support. See [Get help](https://transglot.ai/docs/resources/get-help). ## Related [#related] - [Secure your account](https://transglot.ai/docs/administration/security/secure-your-account) - [Add a passkey](https://transglot.ai/docs/administration/security/add-a-passkey) - [Require two-factor for a workspace](https://transglot.ai/docs/administration/security/require-two-factor-for-a-workspace) - [I can't sign in](https://transglot.ai/docs/resources/troubleshooting/i-cant-sign-in) # Account Owners and Billing members (https://transglot.ai/docs/administration/team/account-owners-and-billing-members/) Your workspaces belong to an **account**, and the account is what holds the plan, the card and the invoices. The people who manage an account have one of two roles on it, separate from any workspace role: | Account role | What it covers | | --- | --- | | **Owner** | "Everything, including closing the account and inviting Owners. Takes a seat." An Owner reaches every workspace of the account with full control, and holds the four permissions no workspace role can have. | | **Billing** | "The plan, the card, invoices and company details. No access to workspaces." | **Who can do this:** account Owners. Billing members do not see the **Owners & billing members** page. ## Open the page [#open-the-page] Open **Settings**, choose the **Account** tab, then **Owners & billing members**. It lists: - **People who manage this account**: every Owner and Billing member, with their **Role** and when they were **Added**. - **Waiting to accept**: open invitations, with the inviter and when each expires. - **Everyone with a seat**: "One seat per person, counted once however many workspaces they reach.", with the workspaces each person reaches and when they were last active. ## Invite an Owner or a Billing member [#invite-an-owner-or-a-billing-member] ### Start the invitation [#start-the-invitation] Select **Invite someone**. ### Enter the address and role [#enter-the-address-and-role] Enter the **Email address** and choose the **Role on this account**: Owner or Billing. ### Send it [#send-it] Select **Send invitation**. "They get an email with a link that works for seven days. The invitation is listed under Waiting to accept until they accept it or an Owner revokes it." Sending a second invitation to the same address replaces its link, and the link in the earlier email stops working. To withdraw an invitation, select **Revoke** on its row. ## Change someone's account role [#change-someones-account-role] Pick the new role in the person's **Role** column. The message confirms it, for example "Dana is now Owner on this account." - A person must confirm their email address before they can become an Owner. If they have not, transglot sends them a link: promote them again after they open it. - A person can own one Free account at a time, so someone who already owns a Free account cannot become an Owner of another Free account. - While a payment for the account has failed, new Owners are paused. An invitation as Billing still works, so someone can fix the payment. ## Hand ownership to someone else [#hand-ownership-to-someone-else] An account always needs at least one Owner. To pass the account on: ### Make the new person an Owner [#make-the-new-person-an-owner] Invite them as an Owner, or change their existing role to **Owner**. ### Wait until they have accepted [#wait-until-they-have-accepted] Their row must show them as an Owner under **People who manage this account**. ### Step back yourself [#step-back-yourself] Select **Leave this account** on your own row, or ask the new Owner to change you to Billing or remove you. If you try to leave or step down as the last Owner, the page refuses: "needs at least one Owner. Make someone else an Owner first." To move a single workspace to a different account rather than handing over the whole account, see [Move a workspace to another account](https://transglot.ai/docs/administration/workspace/move-a-workspace-to-another-account). ## Remove an Owner or a Billing member [#remove-an-owner-or-a-billing-member] Select **Remove** on their row. The confirmation explains that they lose access to billing and to the implicit control of the account's workspaces, that workspaces where they are also a member keep that membership, and that pending invitations they sent are revoked. The dialog also lists access the person created that keeps working after they leave (API tokens, webhooks, CDN keys, publishable keys and CMS connections), with when each was created and last used. Tick the items you want revoked at the same time. "Unchecked items keep working after" they leave. ## Free a seat [#free-a-seat] Under **Everyone with a seat**, **Remove from every workspace** takes a person out of every workspace of the account at once. They keep their standing on the account, and anything they created keeps working. An Owner reaches every workspace through the account, so you change an Owner to Billing before you can free their seat. ## Related [#related] - [Roles and what each one can do](https://transglot.ai/docs/administration/team/roles-and-what-each-one-can-do) - [Your plan and seats](https://transglot.ai/docs/administration/billing/your-plan-and-seats) - [Close an account](https://transglot.ai/docs/administration/billing/close-an-account) # Change a teammate's role (https://transglot.ai/docs/administration/team/change-a-teammates-role/) **Who can do this:** admins, the Owners of the account that holds the workspace, and members whose role includes **Invite, re-role and remove members**. The picker only offers roles that carry no more than you hold. ### Find the person [#find-the-person] Open **Settings**, choose the **Workspace** tab, then **People**. Find their row under **Who reaches what**. ### Pick the new role [#pick-the-new-role] Choose the new role in the **Role** column: Admin, Developer, Reviewer, Translator or one of your custom roles. ### Confirm [#confirm] The confirmation asks "Change" followed by their name and the new role, describes the role, and adds "It applies straight away, and you can change it back." Select **Change role**. ## What happens next [#what-happens-next] The change applies at once, and the message reads "Role updated." followed by their name and new role. The change is recorded in the workspace audit log. The person is emailed their previous and new role, and the workspace's other admins and the account Owners receive a security email about it. A role is a set of permissions, not a step on a ladder. Moving someone from Developer to Reviewer takes away adding keys and running AI translation, and gives them approving. Moving them from Reviewer to Translator takes approving and assignment reviews away. [Roles and what each one can do](https://transglot.ai/docs/administration/team/roles-and-what-each-one-can-do) lists every permission. To change what someone can do on one project only, use a project grant instead. See [Grant access to one project](https://transglot.ai/docs/administration/team/grant-access-to-one-project). ## Account Owners are not changed here [#account-owners-are-not-changed-here] The Owners of the account are listed under **Account Owners**, with no role picker, because they reach the workspace through the account. Nobody is made an account Owner from **People**. Their access is changed on the account's **Owners & billing members** page. See [Account Owners and Billing members](https://transglot.ai/docs/administration/team/account-owners-and-billing-members). ## If it doesn't work [#if-it-doesnt-work] - **The Role column shows a role but no picker**: you cannot change that person's role. - **The picker reads "A role this workspace no longer has"**: their custom role was deleted. Pick one that exists. - **"You can only grant a role carrying capabilities you hold yourself"**: the role carries permissions you do not hold. Ask an admin or an account Owner. - **The workspace is read-only**, for example while it waits to be deleted: a change that only takes permissions away still goes through, and any other change is refused with the reason. ## Related [#related] - [Roles and what each one can do](https://transglot.ai/docs/administration/team/roles-and-what-each-one-can-do) - [Grant access to one project](https://transglot.ai/docs/administration/team/grant-access-to-one-project) - [The People page](https://transglot.ai/docs/administration/team/members-and-roles) # Create a custom role (https://transglot.ai/docs/administration/team/create-a-custom-role/) **Who can do this:** admins, the Owners of the account that holds the workspace, and members whose role includes **Invite, re-role and remove members**. Custom roles are available on every plan. The four built-in roles cover most workspaces. A custom role is for a job title that does not match one of them. It holds exactly the permissions you tick, and you can only tick permissions you hold yourself. ### Open Roles [#open-roles] Open **Settings**, choose the **Workspace** tab, then **Roles**, and select **New role**. ### Name it [#name-it] Fill in **Name** (for example `Lead Linguist`) and the **Identifier**, which takes lowercase letters, numbers and hyphens (for example `lead-linguist`) and must be unique in the workspace. The names of the built-in roles are reserved. Add an optional **Description**, for example `Keeps the glossary for the German market.` ### Choose a level [#choose-a-level] Pick **Translator level**, **Developer level** or **Admin level**. The level only orders the role against the built-in ones in the permissions table and in who may hand it out. It does not grant anything on its own: the permissions do that. ### Tick the permissions [#tick-the-permissions] The permissions are grouped under **Project**, **Organization** and **Assignment**. The table behind the panel shows the role you are assembling beside the roles that already exist, so you can compare them before you save. ### Create the role [#create-the-role] Select **Create role**. The message reads "Role “Lead Linguist” created." with your role's name. The role now appears in the role pickers on **People**, in the invitation dialog and on project grants. ## What a custom role can never do [#what-a-custom-role-can-never-do] - It can never hold the four permissions that belong to the Owners of the account: deleting the workspace, configuring single sign-on and SCIM, requiring two-factor for everyone, and managing white-label branding. These rows are marked "Account owners only. No workspace role can hold it." - It never bypasses a per-project grant, the way an account Owner or an admin does. A member with a custom role, even at **Admin level**, can still be narrowed on one project. - Languages are not narrowed on the role. "Narrowing this to specific languages is set on a per-project grant, not here." ## Edit or delete a custom role [#edit-or-delete-a-custom-role] Select **Edit** on the role's row, change it, then select **Save role**. Changing a role changes what everyone holding it can do, immediately. Select **Delete** to remove a role. If people hold it, choose a role for them under **Move them to** before you select **Delete role**, so nobody is left pointing at a role that no longer exists. If nobody holds it, nothing else changes. ## If it doesn't work [#if-it-doesnt-work] - **"You can only grant capabilities you hold yourself."**: you ticked a permission your own role does not carry. Ask an admin or an account Owner to create the role. - **"You cannot move members to a role that carries more than you hold"**: choose a replacement role with no more authority than your own. - **"Choose a different role: this is the one being deleted."**: pick another role under **Move them to**. ## Related [#related] - [Roles and what each one can do](https://transglot.ai/docs/administration/team/roles-and-what-each-one-can-do) - [Change a teammate's role](https://transglot.ai/docs/administration/team/change-a-teammates-role) - [Grant access to one project](https://transglot.ai/docs/administration/team/grant-access-to-one-project) # Grant access to one project (https://transglot.ai/docs/administration/team/grant-access-to-one-project/) **Who can do this:** admins, the Owners of the account that holds the workspace, and members whose workspace role includes **Grant per-project access**. A project grant cannot give this permission. A project grant overrides a member's workspace role on that project only. Someone who is a Translator across the workspace can be a Developer on one project, or the other way round. A grant can also narrow a member to the locales they may write and approve in. ### Open Project access [#open-project-access] Open **Settings**, choose the **Workspace** tab, pick the project, then open **Project access**. ### Choose the member [#choose-the-member] Under **Project access**, pick the person in **Member to grant**. ### Choose their role on this project [#choose-their-role-on-this-project] Pick the **Role on this project**. ### Limit the locales (optional) [#limit-the-locales-optional] In the box that reads **locales (blank = all)**, list the locales they may work in, for example `de, pt-BR`. Leave it empty for every locale in the project. ### Grant access [#grant-access] Select **Grant access**. The message reads "Access granted. An email is on its way to" followed by their name, "with what they can work on." ## What happens next [#what-happens-next] The grant is listed under **Project access** with the person, their **Role here**, and their locales (or **all locales**). Locale limits are enforced on writing and approving: a member limited to German can open the project but can only edit and approve German. Admins and account Owners always have full access to every project and cannot be limited. A grant for one of them is refused: "Account owners and admins already have full access to every project." ## Revoke a grant [#revoke-a-grant] Select **Revoke** on the grant's row, then **Revoke access**. This removes the project-level grant only. Their workspace role applies again, which may still give them access to the project, and the work they have already done is untouched. The **Assignments** section on the same page assigns a language to a translator as a tracked piece of work. Assigning opens that language to them and takes nothing away. To confine a translator to some languages, set the locales on their grant before you assign. An existing grant for all locales has to be revoked and granted again to narrow it. See [Assign a language to a translator](https://transglot.ai/docs/guides/translating/assign-a-language-to-a-translator). ## If it doesn't work [#if-it-doesnt-work] - **"Unknown locale"**: use BCP-47 tags such as `de` or `pt-BR`. - **"You can only grant a role carrying capabilities you hold yourself"**: pick a role with no more authority than your own. - **The member is not in the list**: the list holds members whose workspace role is Developer, Reviewer or Translator and who have no grant on this project yet. Admins and account Owners already have full access. To change an existing grant, revoke it and grant it again. Someone who is not in the workspace yet has to be [invited](https://transglot.ai/docs/administration/team/invite-a-teammate) first. ## Related [#related] - [Onboard a vendor or freelancer](https://transglot.ai/docs/administration/team/onboard-a-vendor-or-freelancer) - [Roles and what each one can do](https://transglot.ai/docs/administration/team/roles-and-what-each-one-can-do) - [Assign a language to a translator](https://transglot.ai/docs/guides/translating/assign-a-language-to-a-translator) # Invite a teammate (https://transglot.ai/docs/administration/team/invite-a-teammate/) **Who can do this:** admins, the Owners of the account that holds the workspace, and members whose role includes **Invite, re-role and remove members**. You can only invite someone at a role that carries no more than you hold. ### Open People [#open-people] Open **Settings**, choose the **Workspace** tab, then **People**, and select **Invite someone**. The dialog has three steps and sends nothing until the last one. ### Enter the address and role [#enter-the-address-and-role] Enter the **Email address**, then choose the **Role**: Admin, Developer, Reviewer, Translator or one of your custom roles. Each choice says what the role is for, and roles with more authority than your own are not offered. Select **Choose a scope**. ### Choose what they can open [#choose-what-they-can-open] Leave **What they can open** on **All projects** for a normal teammate. To onboard a vendor or freelancer, pick one project instead: they will see nothing else in the workspace. Once you pick a project, **Locales they will own** appears. Leave it empty for every locale in that project, or narrow it, for example `de, pt-BR`. ### Review and send [#review-and-send] Select **Review the invite**, check **What they will see**, then select **Send the invite**. ## What happens next [#what-happens-next] The invitation appears under **Waiting to accept** on **People**, with its role, its project if it has one, and the date it expires, seven days after you send it. The person receives an email with a private link that takes them through signing in or signing up. Accepting adds them to the workspace with that role and, for a scoped invitation, the project access it promised. See [Accept an invitation](https://transglot.ai/docs/first-steps/accept-an-invitation). A waiting invitation reaches nothing until it is accepted, but it holds a seat on your plan from the moment you send it until it is accepted or expires. ## Resend, change or revoke an invitation [#resend-change-or-revoke-an-invitation] Until it is accepted, the invitation's row offers: - **Resend**: sends the same link again. The link in the first email keeps working, and the invitation then expires seven days from the day you resend. - **Revoke**: ends it after you confirm. The link in their email stops working. If you type an address that already has an invitation waiting, the dialog stops on its first step and shows the waiting invitation (role, what they can open, when it was sent and when it expires). Upper and lower case do not matter. You then choose: - **Resend the same invite**: the same as **Resend** on the row. - **Change this invite**: continues through the dialog with the current values filled in. The last step lists **What changes**, and the button reads **Change and send the invite**. Changing it creates a new link, so the link in the first email stops working. When the waiting invitation is at a role you cannot hand out, or at a role the workspace no longer has, **Change this invite** is the only choice, and **Resend** is not on its row. ## If it doesn't work [#if-it-doesnt-work] - **"An invitation to ... is already waiting to be accepted."**: use **Resend the same invite** or **Change this invite**. - **"That user is already a member."**: change their role on **People** instead. - **"You cannot invite someone at a role with more authority than you hold."**: ask an admin or an Owner of the account to send it. - **"That project does not belong to this workspace."**: the project you picked is not in this workspace. - **"Unknown locale: use BCP-47 tags like de or pt-BR."**: one of the locale tags is not recognised. - **"Nothing was sent to ..."**: email to that address is being held back, usually because an earlier message to it bounced. Nothing was saved, so a waiting invitation is unchanged. - **"... is already a member of this workspace, so there was nothing to send."** after **Resend**: they joined another way. Revoke the invitation. - **"The invitation to ... has expired and its link no longer works."**: invite them again. - When the account has no free seat on its plan, the invitation is refused with the reason. Free a seat or upgrade the plan, then send it again. Changing an invitation that is already waiting is not refused this way, because it already holds its seat. ## Related [#related] - [Onboard a vendor or freelancer](https://transglot.ai/docs/administration/team/onboard-a-vendor-or-freelancer) - [Roles and what each one can do](https://transglot.ai/docs/administration/team/roles-and-what-each-one-can-do) - [The People page](https://transglot.ai/docs/administration/team/members-and-roles) # The People page (https://transglot.ai/docs/administration/team/members-and-roles/) **People** is where you manage the members of one workspace. Open **Settings**, choose the **Workspace** tab, then **People**. **Who can use it:** admins, the Owners of the account that holds the workspace, and members whose custom role includes **Invite, re-role and remove members**. Translators, reviewers and developers do not see the page. ## What the page shows [#what-the-page-shows] ### Invite someone [#invite-someone] The **Invite someone** button at the top opens the invitation dialog. See [Invite a teammate](https://transglot.ai/docs/administration/team/invite-a-teammate). ### Account Owners [#account-owners] When the Owners of the account reach this workspace, they are listed first, under **Account Owners**. The card reads "They control every workspace in" followed by the account's name, "and each takes one seat." Each row shows a name, an email address and whether that person holds a second factor. There is no role picker and no **Remove** on these rows, because their access is "changed on the account, not here". An account Owner also sees **Manage**, which opens the account's **Owners & billing members** page. ### Waiting to accept [#waiting-to-accept] Invitations nobody has accepted yet, each with the role it was sent at (**Invited as**), what it will reach, and when it **Expires**. **Resend** appears beside an invitation at a role you can hand out, and **Revoke** beside every one. An unaccepted invitation reaches nothing. ### Who reaches what [#who-reaches-what] Everyone else in the workspace, with their name and email address, their access (**all projects** or **project-scoped**), whether they hold a second factor, and their role. The role is a picker, and **Remove** takes someone out of the workspace. Both ask you to confirm first. While nobody but the account Owners reaches the workspace, this list reads "Nobody else reaches this workspace yet." ### What each role can do [#what-each-role-can-do] The permissions table for every role in the workspace, including your custom roles. The same table is explained in [Roles and what each one can do](https://transglot.ai/docs/administration/team/roles-and-what-each-one-can-do). ### Policy [#policy] Rules that apply to every project in the workspace: - **Require a second reviewer**: "Nobody signs off a translation they wrote themselves, in any project, the account's Owners and admins included. Leave this off for a workspace where one person both translates and approves." The switch saves as you change it. - The workspace's two-factor requirement, shown as **Required for everyone in this workspace** or **Not required in this workspace**, with a **Workspace security** button. Only an account Owner sees this row, and the switch itself is on the workspace **Security** page. See [Require two-factor for a workspace](https://transglot.ai/docs/administration/security/require-two-factor-for-a-workspace). Each person who reaches any workspace of the account takes one seat, however many workspaces they reach. A waiting invitation also holds a seat until it is accepted or expires. Your plan's seat limit is listed on [transglot.ai/pricing](https://transglot.ai/pricing). ## Related [#related] - [Invite a teammate](https://transglot.ai/docs/administration/team/invite-a-teammate) - [Change a teammate's role](https://transglot.ai/docs/administration/team/change-a-teammates-role) - [Remove a teammate](https://transglot.ai/docs/administration/team/remove-a-teammate) - [Account Owners and Billing members](https://transglot.ai/docs/administration/team/account-owners-and-billing-members) # Onboard a vendor or freelancer (https://transglot.ai/docs/administration/team/onboard-a-vendor-or-freelancer/) **Who can do this:** admins, the Owners of the account that holds the workspace, and members whose role includes **Invite, re-role and remove members**. Decide the project and the locales before you send the invitation. Scoping the invitation is what separates a contractor who sees one job from one who sees your whole workspace. As the **People** page puts it: "Widening a seat later is easy; narrowing one is a conversation." ### Start an invitation [#start-an-invitation] Open **Settings**, choose the **Workspace** tab, then **People**, and select **Invite someone**. ### Enter their address and role [#enter-their-address-and-role] Enter their **Email address** and choose **Translator** as the **Role**. Choose **Reviewer** instead if they should also approve translations. Select **Choose a scope**. ### Scope it to one project [#scope-it-to-one-project] Under **What they can open**, pick the project they will work on instead of **All projects**. ### Name the locales [#name-the-locales] In **Locales they will own**, list the languages, for example `de, pt-BR`. Leave it empty to give them every locale in that project. ### Review and send [#review-and-send] Select **Review the invite**, check **What they will see**, then select **Send the invite**. ## What happens next [#what-happens-next] When they accept, they join the workspace as a project-scoped member: they can open that one project and nothing else, and the **People** page shows their access as **project-scoped**. "Every other project, and every locale you leave out, is invisible to them." A project-scoped member also does not see the workspace's AI word usage. A Translator can edit translations in the locales they hold and cannot approve them, so somebody else signs off their work. To give them a tracked job with a review step rather than open-ended access, assign them the language as well. See [Assign a language to a translator](https://transglot.ai/docs/guides/translating/assign-a-language-to-a-translator). ## If it doesn't work [#if-it-doesnt-work] - **"That project does not belong to this workspace."**: the project you picked is not in this workspace. - **"Unknown locale: use BCP-47 tags like de or pt-BR."**: check the locale tags. - **They see nothing after accepting**: check their grant on the project's **Project access** page. See [Grant access to one project](https://transglot.ai/docs/administration/team/grant-access-to-one-project). ## Related [#related] - [Invite a teammate](https://transglot.ai/docs/administration/team/invite-a-teammate) - [Grant access to one project](https://transglot.ai/docs/administration/team/grant-access-to-one-project) - [Remove a teammate](https://transglot.ai/docs/administration/team/remove-a-teammate) # Remove a teammate (https://transglot.ai/docs/administration/team/remove-a-teammate/) **Who can do this:** admins, the Owners of the account that holds the workspace, and members whose role includes **Invite, re-role and remove members**. Removing a person does not revoke the API or deploy tokens they created. Tokens belong to the project, not to the person. If they created any, revoke those separately. See [Deploy tokens](https://transglot.ai/docs/developers/deploy-tokens). ### Find the person [#find-the-person] Open **Settings**, choose the **Workspace** tab, then **People**. Find their row under **Who reaches what**. ### Remove them [#remove-them] Select **Remove**. The confirmation names them and says: "They lose access to every project in this workspace straight away. Translations and reviews they have already done keep their name on them." ### Confirm [#confirm] Select **Remove member**. ## What happens next [#what-happens-next] They lose access to the workspace at once, and a message names who was removed from which workspace. The removal is recorded in the audit log. They are emailed that they were removed, and the workspace's other admins and the account Owners receive a security email about it. Their translations, reviews and history stay as they are. Removing someone from one workspace frees their seat only if they reach no other workspace of the account. To take a person out of every workspace of the account at once, an account Owner can use **Remove from every workspace** on the account's **Owners & billing members** page. ## If it doesn't work [#if-it-doesnt-work] - **There is no Remove on an account Owner**: the Owners of the account are listed under **Account Owners**, not under **Who reaches what**, because they reach every workspace through the account. They are removed on the account's **Owners & billing members** page, which refuses to remove the last Owner. See [Account Owners and Billing members](https://transglot.ai/docs/administration/team/account-owners-and-billing-members). - **There is no Remove on anyone's row**: your role does not include removing members. - **The person is managed by your directory**: if your workspace uses SCIM provisioning, remove them in your identity provider as well, so your directory and the workspace stay in step. See [Set up SCIM provisioning](https://transglot.ai/docs/administration/security/set-up-scim-provisioning). ## Related [#related] - [Change a teammate's role](https://transglot.ai/docs/administration/team/change-a-teammates-role) - [The People page](https://transglot.ai/docs/administration/team/members-and-roles) - [Deploy tokens](https://transglot.ai/docs/developers/deploy-tokens) # Roles and what each one can do (https://transglot.ai/docs/administration/team/roles-and-what-each-one-can-do/) Every member of a workspace holds one role. A role is a set of permissions, not a rank on a ladder: Reviewer and Developer each add something different to what a Translator can do, and Admin holds both sets. The Owners of the account that holds the workspace are not given a role at all: they hold every permission through the account. Admins and account Owners see the same table in the app, under **What each role can do** on the **People** and **Roles** settings pages. It is built from the same rules the app enforces, and your workspace's custom roles appear in it as extra columns. ## The built-in roles [#the-built-in-roles] | Role | What it is for | | --- | --- | | **Translator** | Writes translations. Cannot approve their own work. | | **Reviewer** | Writes and approves translations, and reviews assigned work. Cannot add keys or run AI translation. | | **Developer** | Ships the integration: keys, languages, glossary and AI translation. | | **Admin** | Runs the workspace: members, projects, access and approvals. | | **Account owner** | Pays for this workspace through its account, and holds every permission in it. | ## Permissions by role [#permissions-by-role] Row labels match the app. | Permission | Translator | Reviewer | Developer | Admin | Account owner | | --- | :-: | :-: | :-: | :-: | :-: | | View the workspace | Yes | Yes | Yes | Yes | Yes | | View projects, keys and translations | Yes | Yes | Yes | Yes | Yes | | Edit translations | Yes | Yes | Yes | Yes | Yes | | Edit translations in scoped locales | Yes | Yes | Yes | Yes | Yes | | Approve (sign off) translations | No | Yes | No | Yes | Yes | | Approve translations in scoped locales | No | Yes | No | Yes | Yes | | View assignment work orders | No | Yes | No | Yes | Yes | | Review (approve or request changes on) assignments | No | Yes | No | Yes | Yes | | Add and remove languages | No | No | Yes | Yes | Yes | | Add and edit keys | No | No | Yes | Yes | Yes | | Run AI translation, QA and self-heal | No | No | Yes | Yes | Yes | | Manage in-context screenshots | No | No | Yes | Yes | Yes | | Manage glossary terms | No | No | Yes | Yes | Yes | | Create projects | No | No | No | Yes | Yes | | Edit project settings | No | No | No | Yes | Yes | | Delete a project | No | No | No | Yes | Yes | | Grant per-project access | No | No | No | Yes | Yes | | Assign translators | No | No | No | Yes | Yes | | Create and revoke API tokens | No | No | No | Yes | Yes | | Manage publishable keys | No | No | No | Yes | Yes | | Manage webhooks | No | No | No | Yes | Yes | | Manage the CMS connection | No | No | No | Yes | Yes | | Manage CDN delivery keys | No | No | No | Yes | Yes | | Invite, re-role and remove members | No | No | No | Yes | Yes | | Read the audit log | No | No | No | Yes | Yes | | Manage integrations | No | No | No | Yes | Yes | | Rename the workspace | No | No | No | Yes | Yes | | Delete the workspace | No | No | No | No | Yes | | Configure single sign-on and SCIM | No | No | No | No | Yes | | Require two-factor for everyone in the workspace | No | No | No | No | Yes | | Manage white-label branding | No | No | No | No | Yes | | See this workspace's AI words | Yes | Yes | Yes | Yes | Yes | A few rows need a note: - The rows marked "in scoped locales" can be narrowed to specific languages on a per-project grant. See [Grant access to one project](https://transglot.ai/docs/administration/team/grant-access-to-one-project). - The last four permission rows read "Account owners only. No workspace role can hold it." in the app. No built-in or custom role can carry them. - **See this workspace's AI words** is held by every member except one limited to some projects. - **Read the audit log** is a permission on every plan, but the log itself can be read on the Enterprise plan only. See [The audit log](https://transglot.ai/docs/administration/security/the-audit-log). - Submitting an assignment for review is not a role permission. The translator an assignment is given to submits it. ## Edit versus approve [#edit-versus-approve] The split worth understanding is between writing a translation and signing it off. A Translator or a Developer can edit a value but cannot approve it; the Reviewer is the role made for the second person. If you also turn on **Require a second reviewer** on the **People** page, nobody signs off a translation they wrote themselves, in any project, account Owners and admins included. ## Billing is not a workspace role [#billing-is-not-a-workspace-role] No workspace role, Admin included, can see or change the plan, the card or the invoices. Those belong to the account, and its Owners and Billing members manage them. See [Account Owners and Billing members](https://transglot.ai/docs/administration/team/account-owners-and-billing-members). ## Custom roles and project grants [#custom-roles-and-project-grants] - A custom role holds exactly the permissions ticked for it, and sits in the table beside the built-in role whose level it was given. See [Create a custom role](https://transglot.ai/docs/administration/team/create-a-custom-role). - A per-project grant gives a member a different role on one project, and can narrow them to specific languages. Admins and account Owners always have full access and cannot be narrowed this way. ## Related [#related] - [The People page](https://transglot.ai/docs/administration/team/members-and-roles) - [Change a teammate's role](https://transglot.ai/docs/administration/team/change-a-teammates-role) - [Grant access to one project](https://transglot.ai/docs/administration/team/grant-access-to-one-project) - [Invite a teammate](https://transglot.ai/docs/administration/team/invite-a-teammate) # Custom branding (https://transglot.ai/docs/administration/workspace/custom-branding/) **Who can do this:** the Owners of the account that holds the workspace. Custom branding (white-label) is available on the Enterprise plan. Branding is set per workspace and applies to every member of it. The three settings do not all appear in the same place, so check which is which before you promise a client anything: | Setting | Where it appears | | --- | --- | | **Accent colour** | Across the console, for everyone in this workspace. | | **Product name** | In the header of the printable quality scorecard, on scorecards you share by link, and in the scorecard CSV export in place of the workspace name. | | **Logo** | In the header of the printable quality scorecard and on scorecards you share by link. | The console header and the browser tab are not branded. ### Open Branding [#open-branding] Open **Settings**, choose the **Workspace** tab, then **Branding**. ### Set the product name [#set-the-product-name] Enter the **Product name**, for example `Acme Localization`. It can be up to 60 characters. ### Upload a logo [#upload-a-logo] Choose an image file under **Logo**, up to 1 MB. A preview appears. To take a logo off, select **Remove current logo**. ### Pick an accent colour [#pick-an-accent-colour] Turn on **Use a custom accent colour** and pick the colour. "Off keeps the colour the console ships with." ### Save [#save] Select **Save branding**. The message reads "Branding updated." The change is recorded in the audit log. ## If it doesn't work [#if-it-doesnt-work] - **"White-label the console"** with a **Contact sales** button: the workspace's account is not on the Enterprise plan. Enterprise is arranged with sales rather than bought at checkout. - **Branding is not in the Workspace settings menu**: you are not an Owner of the account that holds the workspace. - **The logo does not upload**: use an image file of 1 MB or less. ## Related [#related] - [The per-language scorecard](https://transglot.ai/docs/guides/quality/the-per-language-scorecard) - [Workspace settings](https://transglot.ai/docs/administration/workspace/workspace-settings) - [Your plan and seats](https://transglot.ai/docs/administration/billing/your-plan-and-seats) # Delete a workspace (https://transglot.ai/docs/administration/workspace/delete-a-workspace/) **Who can do this:** the Owners of the account that holds the workspace. No workspace role can delete it, Admin included, because it ends the workspace for everyone in it. The account keeps its plan. If this is the account's last workspace and the account pays for a plan, the card says so before you confirm: the account keeps billing with no workspaces. To stop billing, change the plan or [close the account](https://transglot.ai/docs/administration/billing/close-an-account). Before you start, export anything you need. When the countdown ends, the workspace and every project, translation, usage record and audit entry in it are removed permanently. ### Open General settings [#open-general-settings] Open **Settings**, choose the **Workspace** tab, then **General**. ### Start the deletion [#start-the-deletion] Under **Delete this workspace**, select **Delete workspace**. ### Type the workspace name [#type-the-workspace-name] Type the workspace's name into the **Workspace name** box exactly as it is written. ### Confirm [#confirm] Select **Delete workspace**. **Delete** on the workspace's row on the account's **Workspaces** page takes you to the same card. ## What happens next [#what-happens-next] - The workspace becomes read-only at once, for 30 days. Every member can still read everything and export, and nobody can change anything. A banner tells everyone in the workspace, because they lose it too. - The message reads "Deletion scheduled. Deleting the workspace does not cancel the subscription, so we have emailed you how to cancel it on the billing page." - The account's **Workspaces** page shows the workspace as "Scheduled for deletion on" a date. - When the countdown ends, the workspace and everything in it are removed permanently. It cannot be restored after that. ## Cancel the deletion [#cancel-the-deletion] Until the countdown ends, any Owner of the account can select **Cancel deletion** on the banner. The workspace is writable again straight away, and the message reads "Deletion cancelled. The workspace is no longer read-only." A cancellation can be refused when the workspace no longer fits the account's plan, for example when the account is on Free and already has its one active workspace. The message names what to upgrade or delete first. ## If it doesn't work [#if-it-doesnt-work] - **"Type the workspace name exactly to confirm."**: the name must match exactly. - **You do not see Delete this workspace**: you are not an Owner of the account that holds the workspace. - **Every change is refused during the countdown**: that is the read-only state, not a fault. ## Related [#related] - [Workspace settings](https://transglot.ai/docs/administration/workspace/workspace-settings) - [Delete a project](https://transglot.ai/docs/guides/projects/delete-a-project) - [Close an account](https://transglot.ai/docs/administration/billing/close-an-account) # Move a workspace to another account (https://transglot.ai/docs/administration/workspace/move-a-workspace-to-another-account/) **Who can do this:** a person who is an Owner of both accounts: the one the workspace is in now, and the one it moves to. Only accounts you own are offered as destinations. To move a workspace to an account you do not own, ask an Owner of that account to make you an Owner of it first. You can then move the workspace, and they can remove you afterwards. See [Account Owners and Billing members](https://transglot.ai/docs/administration/team/account-owners-and-billing-members). ### Start the move [#start-the-move] Open the workspace's **General** settings and select **Move to another account**, or select **Move** on the workspace's row on the account's **Workspaces** page. ### Choose the destination [#choose-the-destination] Under **Destination**, pick the account to move the workspace to. If you own no other account, the page offers **New account** so you can create one first. ### Read what changes [#read-what-changes] **What changes** lists what the move does: the plan the workspace runs on afterwards, the seats, projects and languages it brings against the destination's plan, and any features the destination's plan does not include. Tick each change to confirm it. ### Choose which credentials to revoke [#choose-which-credentials-to-revoke] **Credentials in** the workspace lists its tokens and keys. "Ticked credentials are revoked when" the workspace moves. Those whose creator loses access to the workspace start ticked. "Unchecked credentials keep working after" the move. ### Confirm the move [#confirm-the-move] Select **Move workspace**, type the workspace's name, then select **Move to** followed by the destination account's name. ## What happens next [#what-happens-next] The message names the workspace, the account it moved to, and the plan it now runs on, for example "Docs moved to Acme Inc. It now runs on Team." From then on: - The workspace runs on the destination account's plan, and the destination account pays for it. - "Words already used this cycle stay on" the old account's usage. Invoices already issued stay with the old account. - Features the destination's plan does not include stop working in the workspace once it moves. - If the workspace used single sign-on, enforcement for its email domain stops, and the domain must be verified again in the new account. - A workspace that was read-only because the old account was on Free becomes active, if the destination has room for it. The move is recorded in the workspace's audit log. ## If it doesn't work [#if-it-doesnt-work] The page explains why a move cannot go ahead yet. The common reasons: - **The destination's plan has no room**: its plan allows fewer workspaces, seats, projects or languages than the move needs. Upgrade the destination first. - **"... has an unpaid invoice ... Pay it before moving workspaces out."**: settle the old account's invoice on **Plan & billing**. - **"... is scheduled for deletion. Cancel the deletion first."**: cancel the workspace's deletion, then move it. - **The workspace is on the open source plan**: moving it would end the account's open source plan, so the page asks you to contact support to move the grant. The open source plan covers one workspace, so an account on it cannot receive another one either. - **"You can move a workspace only between two accounts you own."**: become an Owner of the destination account first. - **"You tried several moves in a minute. Wait a minute, then try again."** ## Related [#related] - [Workspace settings](https://transglot.ai/docs/administration/workspace/workspace-settings) - [Account Owners and Billing members](https://transglot.ai/docs/administration/team/account-owners-and-billing-members) - [Your plan and seats](https://transglot.ai/docs/administration/billing/your-plan-and-seats) # Workspace settings (https://transglot.ai/docs/administration/workspace/workspace-settings/) A workspace's **General** settings page holds what the workspace is called, where it posts, the account it belongs to, and the one way to close it. Open **Settings**, choose the **Workspace** tab, then **General**. **Who can use it:** every member can open the page. What each person can change depends on their role: | Card | Who can change it | | --- | --- | | **Workspace name** | Admins and account Owners (permission **Rename the workspace**) | | **Slack notifications** | Admins and account Owners (permission **Manage integrations**) | | **The account it belongs to** | Read-only for members. Account Owners also see **Open account** and **Move to another account**. | | **Delete this workspace** | Account Owners only | When nothing on the page is yours to change, it says: "Nothing here is yours to change. An admin can rename this workspace and connect Slack, and only an Owner of its account can delete it." ## Rename the workspace [#rename-the-workspace] ### Enter the new name [#enter-the-new-name] Under **Workspace name**, type the new name. ### Save it [#save-it] Select **Save name**. The message reads "Workspace renamed to" followed by the new name. The name is "Shown across the app, on your exports and in the emails this workspace sends." The workspace's web address does not change, so existing links, bookmarks and README badges keep working. The rename is recorded in the audit log. ## Connect Slack [#connect-slack] Paste a Slack incoming-webhook URL into **Slack incoming-webhook URL** to post batch, QA, comment and review events to a channel. Select **Save**, or **Save & send test** to check the channel straight away. **Disconnect** stops the channel receiving this workspace's alerts. See [Connect Slack](https://transglot.ai/docs/integrations/notifications/connect-slack) for the full setup. ## The account it belongs to [#the-account-it-belongs-to] This card names the account that pays for the workspace, its plan, and its Owners, for example "Acme Localization belongs to Acme Inc, on Team." Ask the Owners it lists about the plan, the card and the invoices. - **Open account** opens the account's settings, for its Owners and Billing members. - **Move to another account** starts moving the workspace to a different account you own. See [Move a workspace to another account](https://transglot.ai/docs/administration/workspace/move-a-workspace-to-another-account). ## Delete the workspace [#delete-the-workspace] **Delete this workspace** starts a countdown before the workspace is removed. See [Delete a workspace](https://transglot.ai/docs/administration/workspace/delete-a-workspace). ## Other workspace settings [#other-workspace-settings] The **Workspace** tab of **Settings** also holds: - **People** and **Roles**: see [The People page](https://transglot.ai/docs/administration/team/members-and-roles) and [Create a custom role](https://transglot.ai/docs/administration/team/create-a-custom-role). - **Security**: see [Require two-factor for a workspace](https://transglot.ai/docs/administration/security/require-two-factor-for-a-workspace). - **Single sign-on & SCIM**: see [Set up single sign-on](https://transglot.ai/docs/administration/security/set-up-single-sign-on). - **Branding**: see [Custom branding](https://transglot.ai/docs/administration/workspace/custom-branding). - **Audit log**: see [The audit log](https://transglot.ai/docs/administration/security/the-audit-log). ## Related [#related] - [Delete a workspace](https://transglot.ai/docs/administration/workspace/delete-a-workspace) - [Move a workspace to another account](https://transglot.ai/docs/administration/workspace/move-a-workspace-to-another-account) - [Workspaces and projects](https://transglot.ai/docs/concepts/workspaces-and-projects) # Cancel a batch (https://transglot.ai/docs/api-reference/batches/cancel-batch/) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Get a batch (https://transglot.ai/docs/api-reference/batches/get-batch/) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List batches (https://transglot.ai/docs/api-reference/batches/list-batches/) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Retry a batch's failed pairs (https://transglot.ai/docs/api-reference/batches/retry-failed-batch/) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Read one published version (https://transglot.ai/docs/api-reference/cdn/get-bundle-version/) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Read the latest published bundle (https://transglot.ai/docs/api-reference/cdn/get-latest-bundle/) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Publish a locale to the CDN (https://transglot.ai/docs/api-reference/cdn/publish-locale/) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Create a glossary term (https://transglot.ai/docs/api-reference/glossary/create-glossary-term/) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Delete a glossary term (https://transglot.ai/docs/api-reference/glossary/delete-glossary-term/) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Export the glossary (https://transglot.ai/docs/api-reference/glossary/export-glossary/) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Extract glossary candidates (https://transglot.ai/docs/api-reference/glossary/extract-glossary-candidates/) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Import a glossary file (https://transglot.ai/docs/api-reference/glossary/import-glossary/) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List glossary terms (https://transglot.ai/docs/api-reference/glossary/list-glossary-terms/) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Replace a glossary term (https://transglot.ai/docs/api-reference/glossary/update-glossary-term/) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # API reference (https://transglot.ai/docs/api-reference/) The reference is generated from the API's OpenAPI document, so each page shows the exact parameters, request body, responses and errors of one operation. For how the API behaves as a whole (authentication, errors, rate limits, versioning), start with the [REST API overview](https://transglot.ai/docs/developers/api). ## Base URL and authentication [#base-url-and-authentication] ```bash curl https://api.transglot.ai/v1/project \ -H "Authorization: Bearer $TRANSGLOT_TOKEN" ``` Every request under `/v1` sends a project [deploy token](https://transglot.ai/docs/developers/deploy-tokens) as a bearer token. The token decides which project the request acts on, so no URL carries a workspace or project identifier. Each operation needs one ability on the token, shown on its page. The two CDN read operations take a CDN key instead. Bodies are JSON unless an operation says otherwise. Errors use the problem format described in [Errors](https://transglot.ai/docs/developers/api/errors), and a rate-limited request answers `429` with a `Retry-After` header ([Rate limits](https://transglot.ai/docs/developers/api/rate-limits)). API access needs the Starter plan or higher. ## Sections [#sections] ## Download the spec [#download-the-spec] The OpenAPI 3.1 document behind these pages is published at [/openapi/transglot-v1.yaml](https://transglot.ai/docs/openapi/transglot-v1.yaml). Import it into an API client or a code generator to work against the API with typed requests. # Get the project (https://transglot.ai/docs/api-reference/project/get-project/) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Pull a locale as a file (https://transglot.ai/docs/api-reference/push-and-pull/pull/) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Push a locale file (https://transglot.ai/docs/api-reference/push-and-pull/push/) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Start a quality check (https://transglot.ai/docs/api-reference/quality/run-quality-check/) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Start a translation run (https://transglot.ai/docs/api-reference/translation/translate/) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Read one translation (https://transglot.ai/docs/api-reference/translations-single-values/get-translation/) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Write one translation (https://transglot.ai/docs/api-reference/translations-single-values/put-translation/) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Read many translations (https://transglot.ai/docs/api-reference/translations-single-values/read-translations/) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Create a webhook endpoint (https://transglot.ai/docs/api-reference/webhooks/create-webhook/) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Delete a webhook endpoint (https://transglot.ai/docs/api-reference/webhooks/delete-webhook/) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Get one event's catalog entry (https://transglot.ai/docs/api-reference/webhooks/get-webhook-event-sample/) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List webhook events (https://transglot.ai/docs/api-reference/webhooks/list-webhook-events/) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List webhook endpoints (https://transglot.ai/docs/api-reference/webhooks/list-webhooks/) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # batch.completed (https://transglot.ai/docs/api-reference/webhooks/webhook-batch-completed/) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # batch.failed (https://transglot.ai/docs/api-reference/webhooks/webhook-batch-failed/) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # glossary.updated (https://transglot.ai/docs/api-reference/webhooks/webhook-glossary-updated/) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # key.created (https://transglot.ai/docs/api-reference/webhooks/webhook-key-created/) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # key.deleted (https://transglot.ai/docs/api-reference/webhooks/webhook-key-deleted/) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # key.updated (https://transglot.ai/docs/api-reference/webhooks/webhook-key-updated/) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # language.added (https://transglot.ai/docs/api-reference/webhooks/webhook-language-added/) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # language.removed (https://transglot.ai/docs/api-reference/webhooks/webhook-language-removed/) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # qa.completed (https://transglot.ai/docs/api-reference/webhooks/webhook-qa-completed/) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # source_drift.repaired (https://transglot.ai/docs/api-reference/webhooks/webhook-source-drift-repaired/) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # translation.reviewed (https://transglot.ai/docs/api-reference/webhooks/webhook-translation-reviewed/) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # translation.updated (https://transglot.ai/docs/api-reference/webhooks/webhook-translation-updated/) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Authentication (https://transglot.ai/docs/developers/api/authentication/) Every `/v1` request authenticates with a **deploy token** sent as a bearer token. The token belongs to one project, and the project it belongs to is the project the request acts on. ```bash title="Authenticated request" curl https://api.transglot.ai/v1/project \ -H "Authorization: Bearer $TRANSGLOT_TOKEN" ``` ## Deploy tokens [#deploy-tokens] Owners and admins mint deploy tokens in the console under **Settings** → **Developer** → **API tokens**. See [Create a deploy token](https://transglot.ai/docs/developers/deploy-tokens) for the steps. - **Send the whole value.** A token looks like `42|tgl_` followed by a random string. The number before the `|` is part of the credential. - **Shown once.** The value appears once, at creation, and is stored hashed. If you lose it, revoke it and mint another. - **Project-owned.** A token keeps working after the person who minted it leaves the workspace. Revoke it explicitly; revoking takes effect on the next request. - **Expiry.** Choose **Never expires**, **30 days**, **90 days** or **365 days**. An expired token is refused with `401`, like a revoked one. - **Limited to your own access.** You can only give a token abilities you hold yourself on that project. transglot issues two other credentials that are not API tokens and do not work against `/v1`: | Credential | Looks like | What it can do | | --- | --- | --- | | Deploy token | `42\|tgl_...` | Call the API with the abilities it was given | | CDN key | `taicdn_...` | Read this project's published bundles from `GET /v1/cdn/...`, sent as `X-Transglot-Cdn-Key` or `?key=`. Nothing else | | Publishable key | `taip_...` | Open the in-context editor in a browser. See [Publishable keys](https://transglot.ai/docs/developers/in-context/publishable-keys) | ## Abilities [#abilities] A token carries one or more of five abilities. Each endpoint requires one, and abilities do not include each other: a `manage` token cannot read the glossary unless it also has `read`. | Ability | Grants | | --- | --- | | `push` | Upload locale files | | `pull` | Download locale files | | `manage` | Start and stop paid work (translation runs, quality checks, glossary extraction), publish to the CDN, and change project configuration (glossary terms, webhooks) | | `read` | Read single translation values and the glossary | | `write` | Write single translation values | ### Ability per endpoint [#ability-per-endpoint] | Endpoint | Ability | | --- | --- | | `GET /v1/project` | any of `push`, `pull`, `manage` | | `POST /v1/push` | `push` | | `GET /v1/pull` | `pull` | | `POST /v1/translate` | `manage` | | `GET /v1/batches` | any of `push`, `pull`, `manage` | | `GET /v1/batches/{batch}` | any of `push`, `pull`, `manage` | | `POST /v1/batches/{batch}/retry-failed` | `manage` | | `POST /v1/batches/{batch}/cancel` | `manage` | | `GET /v1/translations` | `read` | | `POST /v1/translations/batch` | `read` | | `PUT /v1/translations` | `write` | | `POST /v1/qa/run` | `manage` | | `POST /v1/cdn/publish` | `manage` | | `GET /v1/glossary`, `GET /v1/glossary/export` | `read` | | `POST /v1/glossary`, `PUT` and `DELETE /v1/glossary/{termId}`, `POST /v1/glossary/import`, `POST /v1/glossary/extract` | `manage` | | `GET /v1/webhooks`, `GET /v1/webhooks/events`, `GET /v1/webhooks/events/{event}/sample` | any of `push`, `pull`, `manage` | | `POST /v1/webhooks`, `DELETE /v1/webhooks/{webhook}` | `manage` | A CI job that only syncs files needs `push` and `pull`. Give `manage` only to the jobs that start paid work or change configuration: it is the difference between a leaked token that can read strings and one that can start translation runs. ## 401: not authenticated [#401-not-authenticated] A missing, malformed, unknown, revoked or expired token gets `401`: ```json title="401 Unauthorized" { "type": "/problems/unauthenticated", "title": "Unauthenticated", "status": 401, "detail": "Unauthenticated." } ``` The answer is the same for every reason, so a `401` never tells anyone whether a token existed. Check that the header is `Authorization: Bearer `, that you sent the whole value including the number and `|`, and that the token has not expired. ## 403: authenticated but refused [#403-authenticated-but-refused] A `403` means the token is valid and the request was refused. Branch on `type`: | `type` | Why | What to do | | --- | --- | --- | | `/problems/missing-ability` | The token lacks the ability the endpoint requires. `ability` names it | Mint a token with that ability | | `/problems/plan-feature-required` | The account's plan does not include API access | An account owner upgrades the plan; the same token then works | | `/problems/plan-limit` | The endpoint needs a plan feature the account does not have, for example quality checks | An account owner upgrades the plan | | `/problems/ai-paused`, `/problems/workspace-read-only`, `/problems/workspace-closed` | The workspace's standing refuses this kind of request | An account owner resolves it in the console | ```json title="403 missing-ability" { "type": "/problems/missing-ability", "title": "Missing ability", "status": 403, "detail": "The token is missing a required ability.", "ability": "manage" } ``` For an endpoint that accepts any of `push`, `pull` or `manage`, `ability` names the first of them, `push`. A `403` does not clear by waiting, so do not retry it. [Errors](https://transglot.ai/docs/developers/api/errors) lists every problem type. ## Related [#related] - [Create a deploy token](https://transglot.ai/docs/developers/deploy-tokens) - [Errors](https://transglot.ai/docs/developers/api/errors) - [Rate limits](https://transglot.ai/docs/developers/api/rate-limits) - [API reference](https://transglot.ai/docs/api-reference) # Errors (https://transglot.ai/docs/developers/api/errors/) Every error from `api.transglot.ai` is an [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807) problem document with `Content-Type: application/problem+json`. A client never has to parse prose to know what went wrong. ## The problem format [#the-problem-format] ```json title="422 Unprocessable Content" { "type": "/problems/locale-not-configured", "title": "Locale not configured", "status": 422, "detail": "Locale \"pt-PT\" is not configured on this project." } ``` | Member | What it is | | --- | --- | | `type` | `/problems/` followed by a slug. **Branch on this.** It is a stable identifier, not a URL to fetch | | `title` | The slug in words: hyphens become spaces and the first letter is capitalised, so `/problems/rate-limited` has the title `Rate limited` | | `status` | The HTTP status, repeated | | `detail` | A sentence for a person reading a log. Its wording can change; do not parse it | Some types add extension members beside these four, listed in the tables below. A `5xx` answer never carries internal detail: its `detail` is always `An unexpected error occurred.`, and an unexpected failure has the type `/problems/internal-error`. Retry a `5xx` with backoff. ## Error types [#error-types] ### Authentication and access [#authentication-and-access] | `type` | Status | When | Extensions | | --- | --- | --- | --- | | `/problems/unauthenticated` | 401 | No token, or an unknown, revoked or expired one | | | `/problems/missing-ability` | 403 | The token lacks the ability this endpoint requires | `ability` | | `/problems/plan-feature-required` | 403 | The account's plan does not include API access (or, on the CDN read endpoints, CDN delivery) | | | `/problems/plan-limit` | 403 | The endpoint needs a plan feature the account does not have: quality checks and glossary extraction need Team or higher; the glossary, webhooks and CDN publishing need Starter or higher | `resource`, `cap`, `upgrade_hint` | | `/problems/ai-paused` | 403 | The workspace may not spend on AI or take in new work right now. Reads and single-value writes still work | | | `/problems/workspace-read-only` | 403 | The workspace is read-only. Reads still work | | | `/problems/workspace-closed` | 403 | The workspace is not available | | | `/problems/free-account-held` | 403 | AI translation is paused for this workspace | | | `/problems/email-verification-required` | 403 | AI translation starts once an owner of the account confirms their email address | | ### Requests [#requests] | `type` | Status | When | Extensions | | --- | --- | --- | --- | | `/problems/not-found` | 404 | No such route, or the batch, term, key or endpoint is not in this project. Another project's ids answer `404`, never `403` | | | `/problems/method-not-allowed` | 405 | The path exists but not with this HTTP method | | | `/problems/validation-failed` | 422 | A field is missing or invalid, or a pushed file does not parse in the format you named | `errors`: field name to a list of messages | | `/problems/locale-not-configured` | 422 | The locale is not a language of this project. A push never adds languages | | | `/problems/active-batch` | 409 | A translation batch is already running for this project | `batch_id` of the running batch | ### Push and pull [#push-and-pull] | `type` | Status | When | Extensions | | --- | --- | --- | --- | | `/problems/sync-removals-requires-source` | 422 | `mode=sync_removals` on a target-locale push | | | `/problems/import-requires-target` | 422 | `import=repo` on a source-locale push | | | `/problems/metadata-requires-source` | 422 | A metadata sidecar on a target-locale push | | | `/problems/metadata-format-unsupported` | 422 | A metadata sidecar with a format that carries descriptions in the file itself | | | `/problems/metadata-invalid` | 422 | The sidecar is not valid, or was sent as a form field instead of a file part | `errors` (each with `path`, `code`, `message`, at most 50) and `error_count` | | `/problems/nested-key-collision` | 422 | A nested format cannot hold keys such as `checkout` and `checkout.submit` together | `keys` | | `/problems/format-cannot-serialize` | 422 | The format cannot carry this project's keys | `format` | ### Glossary, webhooks and CDN [#glossary-webhooks-and-cdn] | `type` | Status | When | Extensions | | --- | --- | --- | --- | | `/problems/glossary-term-exists` | 422 | An active term already uses this source term. Update that term instead | | | `/problems/glossary-import-failed` | 422 | The import file could not be read | | | `/problems/glossary-import-conflict` | 422 | Another write took a source term while the import ran. Retry | | | `/problems/webhook-url-not-allowed` | 422 | The endpoint URL is not a public HTTPS address on port 443 | | | `/problems/cdn-key-required` | 401 | A CDN read sent no key | | | `/problems/cdn-key-invalid` | 401 | The CDN key is unknown or has been revoked | | | `/problems/not-published` | 404 | The locale, version or format has no published bundle | | | `/problems/invalid-format` | 422 | The `format` query parameter on a CDN read is not a known format | | ### Limits [#limits] | `type` | Status | When | Extensions | | --- | --- | --- | --- | | `/problems/rate-limited` | 429 | Too many requests. See [Rate limits](https://transglot.ai/docs/developers/api/rate-limits) | Headers: `Retry-After`, `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` | | `/problems/quota-exceeded` | 429 | The account has no AI words left this cycle, or translation beyond the plan is paused | `quota`: `reason`, `scope`, `requested`, `resets_at` | | `/problems/extract-cap-reached` | 429 | The account, or this workspace's share of it, has used its glossary extractions for this cycle | `cap`: `kind`, `scope`, `resets_at` | A `quota-exceeded` answer looks like this. `quota.reason` is `plan_allowance`, `spend_cap` or `overage_paused`, and `resets_at` is `null` when the reset is not known: ```json title="429 quota-exceeded" { "type": "/problems/quota-exceeded", "title": "Quota exceeded", "status": 429, "detail": "The account this workspace belongs to has no AI words left for this cycle. They reset on 1 Nov 2026.", "quota": { "reason": "plan_allowance", "scope": "account", "requested": 4820, "resets_at": "2026-11-01T00:00:00+00:00" } } ``` ## Two answers that are not problem documents [#two-answers-that-are-not-problem-documents] - **A push whose translation run could not start.** `POST /v1/push` still applies the file and answers `200`. The body says why the run did not start: `quota_exceeded: true`, or a code in `translation_paused`. - **A conditional write that lost.** `PUT /v1/translations` with a stale `expected_hash` answers `409` with `{"error": "row-changed", "message": "...", "row": {...}}`, where `row` is the value as it stands now. ## Branching on errors [#branching-on-errors] Decide by status first, then by `type`. A `403` or a `422` does not change by retrying; a `429` and a `5xx` can. ```ts title="handle-problem.ts" type Problem = { type: string; title: string; status: number; detail: string; [extension: string]: unknown }; export async function startRun(token: string) { const res = await fetch('https://api.transglot.ai/v1/translate', { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ scope: 'missing' }), }); if (res.ok) return res.json(); // 202: the batch const problem = (await res.json()) as Problem; switch (problem.type) { case '/problems/active-batch': // Another run is going. Watch it instead of starting a second one. return { watch: problem.batch_id }; case '/problems/rate-limited': { const seconds = Number(res.headers.get('Retry-After') ?? '1'); await new Promise((resolve) => setTimeout(resolve, seconds * 1000)); return startRun(token); } case '/problems/quota-exceeded': // Lifts on its own at quota.resets_at (when set). Do not loop on it. throw new Error(`No AI words left: ${problem.detail}`); case '/problems/missing-ability': throw new Error(`This token needs the "${problem.ability}" ability.`); default: throw new Error(`${problem.status} ${problem.type}: ${problem.detail}`); } } ``` ## Related [#related] - [Authentication](https://transglot.ai/docs/developers/api/authentication) - [Rate limits](https://transglot.ai/docs/developers/api/rate-limits) - [API reference](https://transglot.ai/docs/api-reference) # REST API overview (https://transglot.ai/docs/developers/api/) The transglot API covers everything the command line tool does, and more. It is scoped to a single project: the deploy token you send decides which one, so no URL carries a workspace or project identifier. ## Base URL [#base-url] ```text title="Base URL" https://api.transglot.ai/v1 ``` Every endpoint lives under `/v1`. The version is in the path and there is no version header to send. See [Versioning](https://transglot.ai/docs/developers/api/versioning) for what can change inside `/v1`. ## Authentication [#authentication] Send a project [deploy token](https://transglot.ai/docs/developers/deploy-tokens) as a bearer token on every request: ```bash title="Your first request" curl https://api.transglot.ai/v1/project \ -H "Authorization: Bearer $TRANSGLOT_TOKEN" ``` ```json title="Response" { "id": 12, "name": "Acme Web", "slug": "acme-web", "source_locale": "en", "locales": [ { "locale": "de", "name": "German", "rtl": false, "is_source": false, "progress": { "total": 1204, "reviewed": 310, "human": 42, "ai": 820, "empty": 32, "translated": 1172, "humanBacked": 352, "percent": 97 } }, { "locale": "en", "name": "English", "rtl": false, "is_source": true, "progress": null } ], "keys_count": 1204, "message_syntax": "auto", "icu_dialect": "icu4c", "auto_translate": true, "first_run_pending": false } ``` Each endpoint requires one ability on the token, such as `pull` or `manage`. [Authentication](https://transglot.ai/docs/developers/api/authentication) lists them per endpoint. API access needs the Starter plan or higher. A Free workspace cannot mint a deploy token, and a token stops working if the account moves to a plan without API access. It works again after an upgrade, with nothing to re-issue. ## Content types [#content-types] | Direction | Format | | --- | --- | | Request bodies | JSON with `Content-Type: application/json`, except `POST /v1/push` (`multipart/form-data`) and `POST /v1/glossary/import` (the raw CSV or TBX file as the body) | | Responses | JSON, except `GET /v1/pull` and `GET /v1/glossary/export`, which return the file itself | | Errors | `application/problem+json`, described in [Errors](https://transglot.ai/docs/developers/api/errors) | A single resource comes back as a bare JSON object with no envelope, except the single-value reads and writes, which wrap theirs in `row` (or `rows`). Lists differ by endpoint: `GET /v1/batches` returns a bare array, while `GET /v1/glossary`, `GET /v1/webhooks` and `GET /v1/webhooks/events` wrap the list in `data`. Timestamps are ISO 8601 with an offset, for example `2026-10-12T09:41:00+00:00`. ## What the API covers [#what-the-api-covers] | Group | Endpoints | What it is for | | --- | --- | --- | | Project | `GET /v1/project` | The project's languages, their progress and its settings that affect a push | | Push and pull | `POST /v1/push`, `GET /v1/pull` | Upload a locale file, download a locale as a file | | Translation | `POST /v1/translate` | Start a Glossa translation run | | Batches | `GET /v1/batches`, `GET /v1/batches/{batch}`, `POST .../retry-failed`, `POST .../cancel` | Watch, retry and stop runs | | Translations (single values) | `GET /v1/translations`, `POST /v1/translations/batch`, `PUT /v1/translations` | Read and write individual values by key name | | Quality | `POST /v1/qa/run` | Start a quality check | | CDN | `POST /v1/cdn/publish`, `GET /v1/cdn/{project}/{locale}`, `GET /v1/cdn/{project}/{locale}/v{version}` | Publish a locale, then read the bundle with a CDN key | | Glossary | `GET`, `POST /v1/glossary`, `PUT`, `DELETE /v1/glossary/{termId}`, `GET /v1/glossary/export`, `POST /v1/glossary/import`, `POST /v1/glossary/extract` | Manage terms | | Webhooks | `GET`, `POST /v1/webhooks`, `DELETE /v1/webhooks/{webhook}`, `GET /v1/webhooks/events` | Subscribe endpoints to project events | Every endpoint, with its parameters, request bodies and response shapes, is in the [API reference](https://transglot.ai/docs/api-reference). ## A typical CI flow [#a-typical-ci-flow] ### Push the source file [#push-the-source-file] ```bash title="Push" curl https://api.transglot.ai/v1/push \ -H "Authorization: Bearer $TRANSGLOT_TOKEN" \ -F file=@locales/en.json \ -F format=json_flat \ -F locale=en ``` With automatic translation on, a push that changes source strings starts a translation run and returns it as `batch`. ### Wait for the run [#wait-for-the-run] ```bash title="Poll the batch" curl https://api.transglot.ai/v1/batches/4822 \ -H "Authorization: Bearer $TRANSGLOT_TOKEN" ``` Poll until `status` is `completed`, `completed_with_errors`, `failed` or `cancelled`, or subscribe to the `batch.completed` [webhook](https://transglot.ai/docs/developers/webhooks) instead of polling. ### Pull each locale [#pull-each-locale] ```bash title="Pull" curl "https://api.transglot.ai/v1/pull?format=json_flat&locale=fr" \ -H "Authorization: Bearer $TRANSGLOT_TOKEN" \ -D headers.txt -o locales/fr.json ``` Check `X-Entry-Count` in `headers.txt` before you commit the file: an untranslated locale comes back as a valid empty file. The [command line tool](https://transglot.ai/docs/developers/cli) and the [GitHub Action](https://transglot.ai/docs/developers/github-action) do all of this for you, with retries and rate-limit handling built in. ## Related [#related] # Rate limits (https://transglot.ai/docs/developers/api/rate-limits/) The API limits how many requests a credential can make per minute. Limits are counted per deploy token, so two tokens on the same project each have their own budget. ## The limits [#the-limits] | Requests | Limit | Counted per | | --- | --- | --- | | Every `/v1` endpoint not listed below | 60 per minute | Deploy token | | `POST /v1/push` | 300 per minute | Deploy token, in its own budget | | `POST /v1/batches/{batch}/cancel` | 60 per minute | Deploy token, in its own budget | | `GET /v1/cdn/{project}/{locale}` and `.../v{version}` | 300 per minute | CDN key and client IP address | Push and cancel draw on budgets of their own, so they never compete with the general one: - A repository upload is one push per file, so a project with many locale files can upload them all without touching the budget your status checks use. - Polling a batch never uses up the requests you need to cancel it. ## Headers [#headers] Responses from a limited endpoint carry the current state of the limit: | Header | On | Meaning | | --- | --- | --- | | `X-RateLimit-Limit` | Every response to an authenticated request | Requests allowed per minute | | `X-RateLimit-Remaining` | Every response to an authenticated request | Requests left in the current minute | | `Retry-After` | `429` only | Seconds to wait before the next request will be accepted | | `X-RateLimit-Reset` | `429` only | Unix time, in seconds, when the limit resets | When you exceed a limit, the answer is `429` with the problem type `/problems/rate-limited`: ```http title="429 Too Many Requests" HTTP/1.1 429 Too Many Requests Content-Type: application/problem+json Retry-After: 42 X-RateLimit-Limit: 60 X-RateLimit-Remaining: 0 X-RateLimit-Reset: 1791820920 {"type":"/problems/rate-limited","title":"Rate limited","status":429,"detail":"Too many requests."} ``` `/problems/quota-exceeded` and `/problems/extract-cap-reached` are also `429`, but they mean the account has used its AI allowance for the cycle. They carry no `Retry-After` and do not clear in seconds; `resets_at` in the body says when they lift. Branch on `type` before you retry. See [Errors](https://transglot.ai/docs/developers/api/errors). ## Backing off [#backing-off] Wait for `Retry-After` when it is present. When it is not (a `5xx`, or a dropped connection), back off exponentially with jitter, and give up after a few attempts. ```ts title="request-with-backoff.ts" const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); export async function requestWithBackoff(url: string, init: RequestInit, attempts = 5): Promise { for (let attempt = 0; ; attempt++) { const res = await fetch(url, init); const retryable = res.status >= 500 || (res.status === 429 && (await res.clone().json()).type === '/problems/rate-limited'); if (!retryable || attempt === attempts - 1) return res; const retryAfter = Number(res.headers.get('Retry-After')); const delay = retryAfter > 0 ? retryAfter * 1000 : Math.min(30_000, 1000 * 2 ** attempt) + Math.random() * 250; await sleep(delay); } } ``` Recent versions of curl honour `Retry-After` on a `429` when you ask them to retry: ```bash title="curl with retries" curl --retry 4 --retry-max-time 120 \ https://api.transglot.ai/v1/batches/4822 \ -H "Authorization: Bearer $TRANSGLOT_TOKEN" ``` ## Staying under the limits [#staying-under-the-limits] - **Poll with backoff.** When you watch a batch, start at one second and double the interval up to about fifteen. A run takes minutes, not seconds. - **Use webhooks instead of polling.** Subscribe an endpoint to `batch.completed` and `batch.failed` and you need no status requests at all. See [Webhooks](https://transglot.ai/docs/developers/webhooks). - **Read many values at once.** `POST /v1/translations/batch` reads up to 1,000 keys in one request where `GET /v1/translations` reads one. - **Revalidate instead of downloading.** Send the `ETag` you have as `If-None-Match` on `GET /v1/pull` and the CDN reads. A `304` is still a request, but it carries no body. - **One token per pipeline.** Each token has its own budget, so a busy CI pipeline does not slow down another job that uses a different token. The [command line tool](https://transglot.ai/docs/developers/cli) already waits for `Retry-After` and backs off while it polls. ## Related [#related] - [Errors](https://transglot.ai/docs/developers/api/errors) - [Authentication](https://transglot.ai/docs/developers/api/authentication) - [Connect your app to the CDN](https://transglot.ai/docs/guides/delivery/connect-your-app-to-the-cdn) # Versioning (https://transglot.ai/docs/developers/api/versioning/) The API is versioned by URL prefix. Everything published so far lives under `/v1`, which is the only prefix in use. There is no version header, no date pinning and nothing to configure. ## What can change inside /v1 [#what-can-change-inside-v1] `/v1` changes additively: - **New endpoints** can be added. - **New response fields** can be added to existing responses. - **New response headers** can be added. `X-Skipped-Keys` and `X-Missing-Source-Keys` on `GET /v1/pull` were added this way, rather than by changing the file body. A field you already parse does not change shape and does not disappear. If a change cannot be made additively, it ships under a new version prefix, and `/v1` keeps working. ## Writing a client that keeps working [#writing-a-client-that-keeps-working] Additive changes are only safe for a client that expects them: | Do | Because | | --- | --- | | Ignore JSON fields you do not recognise | New fields are added to existing responses | | Ignore headers you do not recognise | New headers are added the same way | | Branch on a problem's `type`, and fall back to its `status` for a type you do not know | New endpoints can bring problem types of their own | | Handle an unknown value in a field such as a batch's `status` or a webhook's `event` instead of failing | A client that degrades on the unexpected keeps running while you update it | | Never parse `detail` | It is a sentence for a person and its wording can change | | Subscribe webhooks to the events you handle, by name | A new event never reaches an endpoint that did not subscribe to it | ```ts title="Tolerant parsing" type Batch = { id: number; status: string; total: number; completed: number; failed: number }; function isFinal(batch: Batch): boolean { switch (batch.status) { case 'pending': case 'processing': return false; case 'completed': case 'completed_with_errors': case 'failed': case 'cancelled': return true; default: // A status this client does not know yet. Log it and keep polling. console.warn(`Unknown batch status ${batch.status}`); return false; } } ``` ## Deprecations and the changelog [#deprecations-and-the-changelog] Every change to `/v1` is recorded in the [API changelog](https://transglot.ai/api-changelog), dated, separately from product releases. Its deprecation register lists every published surface with the release that shipped it. Nothing has been deprecated or removed: the removal column reads "none scheduled" for every row. A removal would appear there with a date long before it happened, and the row would stay in the table afterwards. ## Related [#related] - [REST API overview](https://transglot.ai/docs/developers/api) - [Errors](https://transglot.ai/docs/developers/api/errors) - [API reference](https://transglot.ai/docs/api-reference) # Command reference (https://transglot.ai/docs/developers/cli/commands/) ```text transglot [options] ``` | Command | What it does | Token ability | | --- | --- | --- | | [`init`](#init) | Finds the locale files, signs in through the browser, sets up and translates. | **push**, **pull** or **manage** (with `TRANSGLOT_TOKEN`) | | [`push`](#push) | Uploads files per `sources` in `transglot.json`. | **push** | | [`pull`](#pull) | Downloads files per `pull` in `transglot.json`. | **pull** | | [`status [batchId]`](#status) | Prints the project summary and active batches, or one batch. | **push**, **pull** or **manage** | | [`cancel `](#cancel) | Stops a running translation batch. | **manage** | A batch is one translation run. Its id is the number the CLI prints after `#`. ## Global options [#global-options] | Option | Description | | --- | --- | | `--config ` | The configuration file. Default: `./transglot.json`. | | `--version`, `-v` | Prints the CLI version. | | `--help`, `-h` | Prints the help text. `transglot help` does the same. | Running `transglot` with no command prints the help text and exits with code 1. An unknown command or flag stops the CLI with exit code 1: ```text error: Unknown flag --locale for "push". error: hint: Run transglot --help for usage. ``` ## init [#init] Sets up the repository in the current folder: scans it for locale files, signs you in through the browser, writes `transglot.json`, uploads your strings and runs the first translation. The walk-through is on [The command line tool](https://transglot.ai/docs/developers/cli#set-up-a-repository). ```bash npx transglot@latest init ``` | Flag | Description | | --- | --- | | `--yes` | Accept the defaults without asking. | | `--no-browser` | Show the sign-in link without opening a browser. | | `--no-translate` | Upload, but leave the first translation for later. | | `--targets ` | Languages to translate into, for example `fr,de`. | | `--source ` | The source language, when the files cannot tell. | | `--path ` | The locale files, for example `messages/{locale}.json`. | | `--format ` | The file format. Read only together with `--path`. | | `--url
` | The transglot server. Default: `https://api.transglot.ai`. | | `--json` | Print events as JSON lines on stdout. | | `--force` | Set up again when `transglot.json` exists. | | `--confirm ` | The 2-digit number your browser shows when the browser and terminal are on different networks. | What `init` does depends on the folder and the environment: - **No `transglot.json`.** The full setup: scan, browser sign-in, write the file, upload, first translation. - **A `transglot.json` this machine has no link for.** `init` signs you in and links the folder on this machine only. It does not change the file and uploads nothing. - **A `transglot.json` this folder is already linked for.** `init` stops with `transglot.json already exists and this folder is linked on this machine.` Pass `--force` to scan again and rewrite the file. - **`TRANSGLOT_TOKEN` is set.** No browser and no upload. `init` reads the token's project, scans the repository for the layout, and writes `transglot.json` for that project. It stops if `transglot.json` exists unless you pass `--force`, and if the repository's source language differs from the project's. It ends with `Next: transglot push --wait && transglot pull`. When `transglot.json` names a server other than `https://api.transglot.ai`, `init` asks before it signs in or sends a token there. Outside a terminal it stops instead and tells you to pass that address with `--url`. Outside an interactive terminal (a script, CI, or a coding agent), and with `--yes` or `--json`, `init` asks nothing and takes every choice from flags. Browser sign-in never runs in CI: there, `init` stops with `Browser sign-in does not run in CI.` unless `TRANSGLOT_TOKEN` is set. ```bash title="Set up without questions" npx transglot@latest init --yes --path "src/locales/{locale}.json" --format json_nested --targets fr,de,ja ``` ## push [#push] Uploads the files the `sources` rules name. Each source file updates the project's keys and source text; a file for another locale uploads its existing translations. ```bash npx transglot@latest push --wait ``` | Flag | Description | | --- | --- | | `--wait` | Wait for the queued translation batch to finish. | | `--timeout ` | Maximum seconds to wait. Default: `600`. | | `--sync-removals` | Also delete keys on the server that are missing from the pushed source file. | Before the first upload, `push` plans every path and reads every file. A missing file, a file over 10 MiB, or a bad sidecar stops the run with nothing uploaded. Files then upload one locale at a time, up to four files at once. For each file it prints what changed: ```text push locales/en.json (en, json_nested) keys_created 12 keys_deleted 0 source_updated 3 translations_written 0 Batch: #4821 ``` `Batch: #` appears when the upload queued a translation batch, which happens when the project translates new strings automatically. Depending on the upload, `push` also lists `conflicts:`, `unknown keys:` and `warnings:`, and with a [metadata sidecar](https://transglot.ai/docs/developers/cli/configuration#key-metadata-sidecar) `descriptions_updated` and `max_lengths_updated`. `push` returns as soon as the batch is queued. With `--wait` it polls until the batch finishes, printing a progress line on each check: ```text waiting: batch #4821 processing 120/400 ``` If the batch fails or is cancelled, `push --wait` lists the failed keys and exits with code 4: ```text error: batch #4821 finished completed_with_errors (2 of 400 failed) error: - checkout.total (de): ``` When the word limit stops the batch, the files are still uploaded and `push` prints `error: quota exceeded: translation batch suppressed by monthly word quota` and exits with code 3. ### Removing keys [#removing-keys] `--sync-removals` deletes server keys the pushed source file no longer contains. It only applies to the source locale. The CLI refuses it when more than one file uploads for the same locale (each upload would delete the keys of the others), and when `TRANSGLOT_TOKEN` belongs to a different project from the one this folder is linked to. ## pull [#pull] Writes the files the `pull` rules name, one request per file. ```bash npx transglot@latest pull ``` | Flag | Description | | --- | --- | | `--wait` | Wait for active translation batches to finish before downloading. | | `--batch ` | Wait on one specific batch. Requires `--wait`. Use it to resume an interrupted wait. | | `--timeout ` | Maximum seconds to wait. Default: `600`. | | `--allow-empty` | Let an empty file overwrite an existing one. Refused by default. | For each file, `pull` prints one line: ```text written locales/fr.json unchanged locales/de.json ``` A file is rewritten only when its content changed, and the write is atomic, so an interrupted pull never leaves a half-written file. Missing folders are created. A pulled file holds what the project has for that locale, not one entry per key. A key nobody has translated yet is absent, and so is a key a `min_state` filter leaves out. On a project whose message syntax is ICU, a translation that is not valid ICU MessageFormat is left out too, so it cannot break your app at render time. `pull` names those under the file: ```text written messages/fr.json omitted 3 invalid ICU translation(s) for fr: cart.items, cart.total, and 1 more ``` Fix those translations in transglot and the next `pull` writes them. If your i18n library does not fall back to the source language for a missing key, pull the source locale too and merge it under each target. ### Empty files are not written over existing ones [#empty-files-are-not-written-over-existing-ones] If the server has no entries for a locale, `pull` refuses to replace an existing non-empty file and exits with code 1: ```text error: refusing to overwrite locales/fr.json with an empty bundle: the server returned 0 entries for this locale ``` This happens when a locale has not been translated yet, when `min_state: reviewed` matches nothing so far, or when a `push` is still translating. Run `push --wait` before `pull` rather than the bare pair. Writing to a path that does not exist yet is not affected. If you mean to clear the file, pass `--allow-empty`. ### Waiting [#waiting] With `--wait`, `pull` waits for every active batch of the project, or only `--batch `, before it downloads anything. If a batch failed or was cancelled, `pull` still writes the files, then exits with code 4. If the wait runs out, it exits with code 5 and prints the command that resumes it: ```text error: Timed out after 600s waiting for batch(es): #4821. error: hint: Resume with: transglot pull --wait --batch 4821 --timeout 600 ``` ## status [#status] Prints the project and its active batches. It always exits with code 0 unless the lookup itself fails. ```bash npx transglot@latest status ``` ```text Project Storefront (storefront) Source locale en Locales en, fr, de Keys 412 Active batches: #4821 processing 120/400 ``` With a batch id, it prints that batch, its per-language counts and, once it has finished, any failed keys: ```bash npx transglot@latest status 4821 ``` ```text Batch #4821 Status completed Progress 400/400 (400 ok, 0 failed) Started 2026-10-12T09:14:03+00:00 Finished 2026-10-12T09:16:41+00:00 ``` ## cancel [#cancel] Stops a running translation batch. It needs `TRANSGLOT_TOKEN` set to a token with the **manage** ability; the token stored by `init` cannot cancel. ```bash TRANSGLOT_TOKEN=tgl_xxx npx transglot@latest cancel 4821 ``` ```text Batch #4821 is cancelled. ``` Cancelling a batch that has already finished is not an error: the CLI prints `Batch #4821 had already finished (completed), nothing to cancel.` and exits with code 0, so a cleanup step can cancel defensively. ## Environment variables [#environment-variables] | Variable | Description | | --- | --- | | `TRANSGLOT_TOKEN` | A project token. When set, every command uses it instead of the token stored for this folder, and never opens a browser. `cancel` always needs it. | | `TRANSGLOT_URL` | The server address. Overrides `url` in `transglot.json`. | | `TRANSGLOT_CONFIG_DIR` | The absolute path of the folder that holds `credentials.json`. | | `TRANSGLOT_BROWSER` | `none` never opens a browser; any other value is the command that opens the sign-in link. The link is printed either way. | | `TRANSGLOT_INTERACTIVE` | `0` never asks a question; `1` asks even where the CLI would otherwise assume an automated environment. | | `NO_COLOR` | Turns coloured output off. Colour is also off outside a terminal and in CI. | ## Output [#output] Results go to stdout. Errors go to stderr, prefixed `error: `, followed by a `hint:` line that says what to do next. Scripts can rely on the lines `Batch: #`, `written ` and `unchanged `; the [GitHub Action](https://transglot.ai/docs/developers/github-action) reads them. Every command exits with one of the [documented exit codes](https://transglot.ai/docs/developers/cli/exit-codes). Pressing Ctrl+C prints `Cancelled.` and exits with code 130. # Configuration file (https://transglot.ai/docs/developers/cli/configuration/) The CLI reads `transglot.json` from the folder you run it in. `transglot init` writes one for you; this page describes every field so you can write or change it by hand. Pass `--config ` to read a file from somewhere else. ```json title="transglot.json" { "url": "https://api.transglot.ai", "sources": [ { "path": "locales/{locale}.json", "format": "json_nested" } ], "pull": [ { "path": "locales/{locale}.json", "format": "json_nested" } ] } ``` `push` uploads the files the `sources` rules name. `pull` writes the files the `pull` rules name. The two lists are independent, so you can pull into a different layout or format from the one you push. ## Top-level fields [#top-level-fields] The file must be a JSON object with only these three fields. Any other field stops the CLI with `Unknown config field "". Allowed fields: url, sources, pull.` `url` is required unless `TRANSGLOT_URL` is set. For transglot it is `https://api.transglot.ai`. ## Rule fields [#rule-fields] Each entry in `sources` and `pull` is a rule: one path template, one file format, and the locales it applies to. ### Locale placeholders [#locale-placeholders] `{locale}` is replaced by each locale the rule covers, everywhere it appears in the path. With `"locales": ["fr", "de"]`, the path `locales/{locale}/common.json` stands for `locales/fr/common.json` and `locales/de/common.json`. When a pull rule leaves `locales` out, `{locale}` is filled with each locale's id as transglot stores it, such as `fr` or `pt_BR`. If your files spell locales differently, for example `pt-BR`, list them in your spelling. transglot accepts any letter case and either `-` or `_` as the separator, so `"locales": ["fr", "pt-BR"]` writes `pt-BR.json` and still reads the project's `pt_BR` translations. A locale used in a path must be a plain tag of letters, digits, `-` and `_`. Every planned path must stay inside the folder you run the CLI in, symbolic links included. Two locales that would land on the same file are refused before anything is sent: a rule that covers more than one locale needs `{locale}` in its path. A sources rule can name a locale other than the source. `push` then uploads that file as existing translations for that locale. The locale must already be a language of the project: `push` never adds one, and answers `Locale "" is not configured on this project. Push never adds locales.` ### Formats [#formats] Every rule names one of these formats. `transglot init` detects it from your files. | Format | Default extension | Format | Default extension | | --- | --- | --- | --- | | `json_flat` | `.json` | `gettext_po` | `.po` | | `json_nested` | `.json` | `yaml` | `.yml` | | `android_xml` | `.xml` | `csv` | `.csv` | | `ios_strings` | `.strings` | `xlsx` | `.xlsx` | | `ios_stringsdict` | `.stringsdict` | `xliff_20` | `.xlf` | | `flutter_arb` | `.arb` | `java_properties` | `.properties` | | `laravel_php` | `.php` | `resx` | `.resx` | | `laravel_json` | `.json` | `markdown` | `.md` | | `xliff_12` | `.xlf` | `unity_csv` | `.csv` | | `ios_xcstrings` | `.xcstrings` | `unreal_po` | `.po` | `unity_csv` and `unreal_po` are the game engine formats (Unity string tables and Unreal `.po` catalogs). Each format has its own page under [File formats](https://transglot.ai/docs/developers/formats). ### Several rules [#several-rules] A project can have as many rules as you have files. List one rule per file family; the same locale can appear in several rules. ```json title="transglot.json" { "url": "https://api.transglot.ai", "sources": [ { "path": "locales/{locale}/common.json", "format": "json_nested" }, { "path": "locales/{locale}/checkout.json", "format": "json_nested" } ], "pull": [ { "path": "locales/{locale}/common.json", "format": "json_nested" }, { "path": "locales/{locale}/checkout.json", "format": "json_nested" } ] } ``` `push` uploads the files of one locale at a time, in the order the rules first mention each locale, so list your source locale first. Files of the same locale upload in parallel. With several source files for one locale you cannot use `push --sync-removals`: each upload would delete the keys the other files define. The CLI refuses that combination before it uploads anything. ## Key metadata sidecar [#key-metadata-sidecar] A key's description is the context translators and Glossa read, and its `max_length` is the limit a translation is checked against. Formats such as `json_flat`, `json_nested`, `android_xml` or `ios_strings` have nowhere to store either, so a sources rule can name a sidecar file that does (CLI 0.5.0 and later). ```json title="transglot.json" { "url": "https://api.transglot.ai", "sources": [ { "path": "messages/{locale}.json", "format": "json_nested", "metadata": "i18n/en.meta.json" } ], "pull": [ { "path": "messages/{locale}.json", "format": "json_nested" } ] } ``` ```json title="i18n/en.meta.json" { "cart.title": { "description": "Heading of the cart page", "max_length": 40 }, "cart.items": { "description": "Item count under the cart heading" }, "legacy.banner": { "description": null } } ``` - Keys are the project's key names, dotted the way the project stores them. - A field you leave out is not changed; `null` clears it. - `max_length` is a whole number from 1 to 100000. `description` is at most 2,000 characters. - The file is at most 1 MiB and 50,000 keys, and names each key once. `push` reads and checks the sidecar before it uploads anything, so a bad sidecar stops the run with nothing sent. The sidecar travels with the rule's source-locale upload, which goes after that locale's other files, so keys a sibling file creates exist by the time their descriptions arrive. The CLI refuses, as a configuration error: - `metadata` on a pull rule; - a sidecar path with a `{locale}` or any other `{…}` placeholder (a rule has exactly one sidecar); - a rule that plans no upload, or more than one, for the source locale; - a sidecar path outside the working folder, including through a symbolic link; - a format that carries descriptions in the file itself: `flutter_arb`, `xliff_12`, `ios_xcstrings`, `gettext_po`, `csv`, `xlsx`, `unity_csv` and `unreal_po`. ## Examples [#examples] Nested JSON with one folder per language, as i18next and most JavaScript libraries use. The locales are listed because the files spell regions with a hyphen. ```json title="transglot.json" { "url": "https://api.transglot.ai", "sources": [ { "path": "public/locales/{locale}/translation.json", "format": "json_nested" } ], "pull": [ { "path": "public/locales/{locale}/translation.json", "format": "json_nested", "locales": ["fr", "de", "pt-BR"] } ] } ``` The source file sits in `values/`, which has no locale in its name, so its rule names the path in full and pins its locale. A region such as `values-pt-rBR` uses Android's own spelling, so it gets a literal path too. ```json title="transglot.json" { "url": "https://api.transglot.ai", "sources": [ { "path": "app/src/main/res/values/strings.xml", "format": "android_xml", "locales": ["en"] } ], "pull": [ { "path": "app/src/main/res/values-{locale}/strings.xml", "format": "android_xml", "locales": ["fr", "de", "ja"] }, { "path": "app/src/main/res/values-pt-rBR/strings.xml", "format": "android_xml", "locales": ["pt-BR"] } ] } ``` `.strings` files in one `.lproj` folder per language. Add a second rule with `ios_stringsdict` for plural rules kept in `Localizable.stringsdict`. ```json title="transglot.json" { "url": "https://api.transglot.ai", "sources": [ { "path": "App/Resources/{locale}.lproj/Localizable.strings", "format": "ios_strings" } ], "pull": [ { "path": "App/Resources/{locale}.lproj/Localizable.strings", "format": "ios_strings", "locales": ["fr", "de", "zh-Hans"] } ] } ``` Laravel's JSON translation files, one per language in `lang/`. For PHP array files, use `laravel_php` with a path such as `lang/{locale}/messages.php`. ```json title="transglot.json" { "url": "https://api.transglot.ai", "sources": [ { "path": "lang/{locale}.json", "format": "laravel_json" } ], "pull": [ { "path": "lang/{locale}.json", "format": "laravel_json" } ] } ``` ARB files as `flutter gen-l10n` reads them. ARB carries key descriptions in the file itself, so a sidecar is not allowed here. ```json title="transglot.json" { "url": "https://api.transglot.ai", "sources": [ { "path": "lib/l10n/app_{locale}.arb", "format": "flutter_arb" } ], "pull": [ { "path": "lib/l10n/app_{locale}.arb", "format": "flutter_arb" } ] } ``` ## Validation errors [#validation-errors] The CLI checks the whole file before it sends a request and exits with code 1 on the first problem. The messages name the field, for example: ```text error: Config field "url" is required (or set TRANSGLOT_URL). error: Unknown config field "sources[0].file". Allowed fields: path, format, locales, min_state, metadata. error: Config field "pull[0].min_state" must be "human_edited" or "reviewed". error: Config field "pull[0].metadata" is only allowed on a sources rule. ``` See [Exit codes](https://transglot.ai/docs/developers/cli/exit-codes) for how to handle them in a pipeline. # Use the CLI in CI (https://transglot.ai/docs/developers/cli/continuous-integration/) In CI the CLI authenticates with a deploy token in the `TRANSGLOT_TOKEN` environment variable. It never opens a browser and never asks a question there. On GitHub, the [GitHub Action](https://transglot.ai/docs/developers/github-action) wraps the same steps; this page covers every other CI system. ## Before you start [#before-you-start] 1. **Commit `transglot.json`.** Run `npx transglot@latest init` once on your machine, or write the file by hand from the [configuration reference](https://transglot.ai/docs/developers/cli/configuration). 2. **Create a deploy token** with the **push** and **pull** abilities. Add **manage** only if the job also cancels batches. See [Deploy tokens](https://transglot.ai/docs/developers/deploy-tokens). 3. **Store it as a secret** named `TRANSGLOT_TOKEN` in your CI system. Never put the token in `transglot.json` or in the pipeline file. ## The two commands [#the-two-commands] Every pipeline runs the same pair: ```bash npx --yes transglot@latest push --wait npx --yes transglot@latest pull ``` `push --wait` uploads the source strings and waits until the translation batch finishes. `pull` then writes the translated files into the checkout. Without `--wait`, `pull` would run before anything is translated. What happens to the pulled files is up to the pipeline: commit them, open a merge request, or keep them as a build artifact. To make builds reproducible, pin a version instead of `@latest`, for example `npx --yes transglot@0.5.0`. The CLI needs Node.js 22 or later on the runner. ## Environment variables [#environment-variables] | Variable | Description | | --- | --- | | `TRANSGLOT_TOKEN` | Required. The deploy token. | | `TRANSGLOT_URL` | Optional. Overrides `url` from `transglot.json`. | | `NO_COLOR` | Optional. Colour is already off in CI. | The CLI recognises CI from the variables CI systems set, including `CI`, `GITLAB_CI`, `CIRCLECI`, `BUILDKITE`, `JENKINS_URL` and `BITBUCKET_BUILD_NUMBER`. In CI it asks nothing, prints no colour, and refuses browser sign-in with `Browser sign-in does not run in CI.` ## GitLab CI [#gitlab-ci] This job runs on the default branch and keeps the pulled files as an artifact. ```yaml title=".gitlab-ci.yml" translations: image: node:22 rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH script: - npx --yes transglot@latest push --wait - npx --yes transglot@latest pull artifacts: paths: - locales/ ``` Add `TRANSGLOT_TOKEN` under **Settings** → **CI/CD** → **Variables** in GitLab, masked and protected. Replace `locales/` with the folders your `pull` rules write to. ## CircleCI [#circleci] ```yaml title=".circleci/config.yml" version: 2.1 jobs: translations: docker: - image: cimg/node:22.20 steps: - checkout - run: name: Push source strings and wait for translations command: npx --yes transglot@latest push --wait - run: name: Pull translated files command: npx --yes transglot@latest pull - store_artifacts: path: locales workflows: localize: jobs: - translations: context: transglot filters: branches: only: main ``` Store `TRANSGLOT_TOKEN` in a CircleCI context (here named `transglot`) or in the project's environment variables. ## Plain shell [#plain-shell] Any runner with Node.js 22 and git can sync and commit in one script. This one commits only when a translation file changed. ```bash title="scripts/sync-translations.sh" #!/usr/bin/env bash set -euo pipefail : "${TRANSGLOT_TOKEN:?Set TRANSGLOT_TOKEN to a deploy token}" npx --yes transglot@latest push --wait npx --yes transglot@latest pull if [ -n "$(git status --porcelain)" ]; then git add -A locales git commit -m "chore(i18n): sync translations from transglot" git push fi ``` With `set -e`, the script stops at the first non-zero exit code. To keep going when the word limit is reached or some translations failed, branch on the exit code instead; see [Exit codes](https://transglot.ai/docs/developers/cli/exit-codes#react-to-codes-in-a-script). ## Recipes [#recipes] ### Push on every merge, pull on a schedule [#push-on-every-merge-pull-on-a-schedule] Uploading is quick; translation takes as long as the batch. Push without waiting on every merge, and pull in a scheduled job: ```bash # on merge to main npx --yes transglot@latest push # nightly npx --yes transglot@latest pull --wait ``` `pull --wait` waits for every active batch of the project before it downloads. ### Resume a wait that timed out [#resume-a-wait-that-timed-out] `--wait` gives up after 600 seconds by default and exits with code 5. The batch keeps running on the server. Raise the limit with `--timeout`, or pick up where the job stopped: ```bash npx --yes transglot@latest pull --wait --batch 4821 --timeout 1800 ``` The CLI prints the exact command, with the batch id, when the wait times out. ### Stop a batch when a job is cancelled [#stop-a-batch-when-a-job-is-cancelled] A cleanup step can stop a running batch. `cancel` needs a token with the **manage** ability and exits with code 0 even when the batch has already finished: ```bash npx --yes transglot@latest cancel "$BATCH_ID" ``` `push` prints the id as `Batch: #`. Read it from the output: ```bash BATCH_ID=$(npx --yes transglot@latest push | sed -n 's/^Batch: #\([0-9]*\).*/\1/p' | head -n 1) ``` ### Only translations someone has checked [#only-translations-someone-has-checked] To ship only translations a person edited or reviewed, add `min_state` to the pull rules in `transglot.json`: ```json title="transglot.json" { "url": "https://api.transglot.ai", "sources": [{ "path": "locales/{locale}.json", "format": "json_nested" }], "pull": [{ "path": "locales/{locale}.json", "format": "json_nested", "min_state": "reviewed" }] } ``` ## Troubleshooting [#troubleshooting] | Message | Cause | | --- | --- | | `TRANSGLOT_TOKEN is not set.` | The secret is not exposed to this job, or is misspelled. | | `The server did not accept TRANSGLOT_TOKEN.` (hint) | The token was revoked, has expired, or belongs to another server. | | `The token lacks the "pull" ability.` (hint) | Create a token with the abilities the job needs. | | `Config file not found: ...` | The job does not run in the folder that holds `transglot.json`. Pass `--config`. | | `refusing to overwrite ... with an empty bundle` | `pull` ran before anything was translated. Use `push --wait` first. | Every code the CLI exits with is listed in [Exit codes](https://transglot.ai/docs/developers/cli/exit-codes). # Exit codes (https://transglot.ai/docs/developers/cli/exit-codes/) Every CLI command exits with one of these codes. They are stable across the 0.x releases, so scripts can branch on them. | Code | Meaning | Typical causes | What to do | | --- | --- | --- | --- | | `0` | Success. | | Nothing. | | `1` | Configuration or usage. | `transglot.json` missing or invalid; a source file missing or over 10 MiB; an unknown command or flag; a batch id that does not exist; a locale that is not a language of the project; `pull` refusing to write an empty file over a full one; a choice `init` needs a flag for outside a terminal. | Fix the input the message names. Retrying does not help. | | `2` | Sign-in, token or access refused. | `TRANSGLOT_TOKEN` missing, revoked or expired; the token lacks the ability the command needs; the plan does not include API access; the workspace or account is suspended, read-only or closing; browser sign-in attempted in CI. | Read the hint: it names the missing ability or what to change. Replace the token or ask an admin. | | `3` | Word limit reached. | The upload went through but the translation batch was held back because the workspace's words are used up, or AI translation is paused for the account. | Your files are synced. Raise the limit or wait for the reset, then push again. | | `4` | A translation batch failed or was stopped. | With `--wait`, a batch finished as `failed` or `completed_with_errors`, or was cancelled. | The failed keys are listed with their reasons. Translations that did complete are still written by `pull`. Select **Retry failed** on the run in the project's **Runs** page to re-run only the strings that failed. | | `5` | Timed out while waiting. | `--wait` ran longer than `--timeout` (default 600 seconds). | The batch keeps running on the server. Resume with the command in the hint, for example `transglot pull --wait --batch 4821`. | | `6` | Server or network error. | The server could not be reached, answered with a server error or an unexpected response, redirected the request, or was still rate-limiting after two retries. | Retry later. Check that `url` points at `https://api.transglot.ai`. | | `130` | Interrupted. | You pressed Ctrl+C. | Nothing. | ## How the CLI picks a code [#how-the-cli-picks-a-code] - **Worst signal wins.** When one `push --wait` both hits the word limit and has a failed batch, it exits with `4`, because missing translations need attention first. - **`pull` writes first, then reports.** With `--wait`, a failed batch makes `pull` exit with `4`, but only after it has written every file it could. - **`status` and `cancel` are read-outs.** They exit with `0` whenever the request succeeds, including for a failed batch or one that had already finished. - **Rate limits are retried for you.** A `429` is retried up to two times, waiting as long as the server asks (at most 60 seconds), before the CLI gives up with `6`. Errors are printed on stderr as `error: ` and `error: hint: `. A token that lacks an ability, for example, reads: ```text error: hint: The token lacks the "push" ability. Issue a token that has it. ``` ## React to codes in a script [#react-to-codes-in-a-script] Treat `3` and `4` as warnings if a partial result is acceptable, and everything else as a failure: ```bash title="sync-translations.sh" #!/usr/bin/env bash set -u npx --yes transglot@latest push --wait code=$? case "$code" in 0) ;; 3) echo "Word limit reached: sources synced, nothing new translated." ;; 4) echo "Some translations failed: pulling what finished." ;; *) exit "$code" ;; esac npx --yes transglot@latest pull ``` The [GitHub Action](https://transglot.ai/docs/developers/github-action) follows the same idea: it pulls and commits after `0`, `3` and `4`, skips the pull after any other code, and fails the job with the worst code of the run. # The command line tool (https://transglot.ai/docs/developers/cli/) The command line tool is the npm package `transglot`. It reads a `transglot.json` file in your repository, uploads your source strings to a project, and writes the translations back into your locale files. It has no runtime dependencies. ## Requirements [#requirements] - **Node.js 22 or later.** On an older version the CLI stops before doing anything and prints `transglot needs Node.js 22 or later.` followed by the version it found. - **A plan with API access.** The CLI, the API and the GitHub Action start on the Starter plan. See [pricing](https://transglot.ai/pricing). - **An admin to approve the connection.** Connecting a repository creates a project token, so the login that approves it in the browser must be an admin of the workspace or an owner of its account. ## Install [#install] You can run the CLI without installing it, install it globally, or add it to your project. ```bash tab="npx" npx transglot@latest init ``` ```bash tab="Global" npm install -g transglot transglot init ``` ```bash tab="Dev dependency" npm install --save-dev transglot npx transglot init ``` Run it as `npx transglot@latest` rather than `npx transglot`. In a repository that lists an older transglot as a development dependency, npx runs that older version unless you ask for `@latest`. Check the installed version with `transglot --version`. ## Set up a repository [#set-up-a-repository] `init` connects a repository to a project, writes `transglot.json`, and runs the first translation. Run it in the root of your repository, on a machine with a browser. ### Scan the repository [#scan-the-repository] ```bash npx transglot@latest init ``` `init` finds your locale files and works out the source language. Nothing is sent to transglot yet. If it cannot tell which files are your locale files, pass the layout with `--path`, for example `--path messages/{locale}.json`. ### Sign in through the browser [#sign-in-through-the-browser] The terminal prints a link and a code such as `WDJB-MJHT`, and opens the link. The page is titled **Connect the CLI**. Sign in or sign up there. Under **Connect this terminal?**, check that the page shows the same code as your terminal, then select **Approve**. Approve only a request you started yourself; otherwise select **Decline**. When your browser and the terminal reach transglot from different networks (SSH, a container, a VPN that covers only one of them), the page shows **Type this number in your terminal**. Type the two digits into the terminal, or pass them as `--confirm`. ### Choose the workspace and project [#choose-the-workspace-and-project] If you have no workspace yet, the page asks you to name one under **Create your workspace**. If the workspace's account is on the Free plan, its owners and billing members are offered **Start the free trial** (or a paid plan when the account cannot have a trial) before they continue. Other members are told who can change the plan. Under **Choose the project for this repository**, connect an existing project or create the one the terminal proposed from your files. The page then says **Your terminal is connected**. ### Upload and translate [#upload-and-translate] Back in the terminal, `init` writes `transglot.json`, uploads your source strings and the translations you already have, shows how many words the first translation needs, and asks once before it starts. It then translates and writes the finished files into your repository. A file that holds strings the server does not have is kept, and the terminal says which and why. Commit `transglot.json`. `init` lists the files to commit when it finishes. If the repository already has a `transglot.json`, `init` only signs you in and links the folder on this machine. It does not change the file and uploads nothing, so an old branch never replaces your team's source text. Pass `--force` to scan the repository again and rewrite the file. ## Where the token is kept [#where-the-token-is-kept] Approving in the browser creates an ordinary project token with the **push** and **pull** abilities, named after your machine. It appears with the project's other tokens under **Settings** → **Developer**, where deleting it disconnects the terminal. See [Deploy tokens](https://transglot.ai/docs/developers/deploy-tokens). The CLI keeps the token in `credentials.json`, in the first of these folders that applies: 1. `TRANSGLOT_CONFIG_DIR`, when it is set (it must be an absolute path). 2. `$XDG_CONFIG_HOME/transglot`, when `XDG_CONFIG_HOME` is an absolute path. 3. `%LOCALAPPDATA%\transglot` on Windows. 4. `~/.config/transglot` otherwise. On macOS and Linux only your user can read the file. A token is only ever sent to the server it was created for. `init` also writes `.transglot/project.json` in the repository and adds `.transglot/` to `.gitignore`. That file links the folder to the token on this machine only, so a copy of the folder on another machine selects nothing. In a linked folder, `push`, `pull` and `status` use the stored token without any setup. ## Everyday commands [#everyday-commands] ```bash npx transglot@latest push --wait # upload source strings, wait for the translations npx transglot@latest pull # write translations into your locale files npx transglot@latest status # the project and its active runs ``` `push` returns as soon as a translation run is queued, so use `push --wait` before `pull`. See the [command reference](https://transglot.ai/docs/developers/cli/commands) for every command and flag. ## In CI [#in-ci] In CI there is no browser sign-in. Create a deploy token, store it as the `TRANSGLOT_TOKEN` secret, and the CLI uses it. While `TRANSGLOT_TOKEN` is set it always wins over a token stored on the machine, and the CLI never opens a browser. See [Use the CLI in CI](https://transglot.ai/docs/developers/cli/continuous-integration) and [The GitHub Action](https://transglot.ai/docs/developers/github-action). ## Troubleshooting [#troubleshooting] Run the command in your terminal again for a new code. The request was declined, or the terminal typed the wrong number twice. Run the command again for a new code. Connecting the CLI creates a project token, which needs an admin of the workspace. The page names the admins who can; ask one of them to connect the repository, or to create a deploy token for you. The CLI detected a CI environment. Store a deploy token as the `TRANSGLOT_TOKEN` secret instead. See [Use the CLI in CI](https://transglot.ai/docs/developers/cli/continuous-integration). This folder is not linked on this machine and no token is in the environment. Run `npx transglot@latest init` to sign in and link it, or export a deploy token as `TRANSGLOT_TOKEN`. ## Next steps [#next-steps] # Deploy tokens (https://transglot.ai/docs/developers/deploy-tokens/) A deploy token is a secret that authenticates one project. The CLI, the GitHub Action, the [REST API](https://transglot.ai/docs/developers/api), the [MCP server](https://transglot.ai/docs/mcp) and the [language server](https://transglot.ai/docs/developers/tools/language-server) all use one. Tokens start with `tgl_`, and the console calls them **API tokens**. A token belongs to the project, not to the person who created it. Removing that person from the workspace does not revoke their tokens; revoke them explicitly. The token list shows who created each one. ## Before you start [#before-you-start] - **Role.** Admins can create and revoke tokens, as can members whose custom role includes **Create and revoke API tokens**. - **Plan.** Tokens work on plans with API access, Starter and above. See [pricing](https://transglot.ai/pricing). If the account moves to a plan without it, tokens stay saved and work again after an upgrade. ## Abilities [#abilities] Each token carries one or more abilities, and every API request checks them. Give a token only what its job needs: a leaked token that can only `pull` reads strings, while one with `manage` can start translation runs that use your word allowance. | Ability | What it allows | Used by | | --- | --- | --- | | `push` | Upload source files and existing translations. Price and start the first translation run of a new project. | `transglot push`, `transglot init` | | `pull` | Download translated files. | `transglot pull` | | `manage` | Start, retry and cancel translation batches, run quality checks, publish to the CDN, change the glossary, and create or delete webhook endpoints. | `transglot cancel`, the MCP server's batch, quality and glossary tools | | `read` | Read single translations and the glossary. | The MCP server, your own scripts | | `write` | Write single translations. With `read`, it also lets your backend authorize the [in-context editor](https://transglot.ai/docs/developers/in-context). | The MCP server, your own backend | Any one of `push`, `pull` or `manage` also lets a token read the project's details and its batches, which `transglot status` and every `--wait` need. New tokens start with `push` and `pull` selected, which is what the CLI and the GitHub Action need. Add `manage` only for a job that cancels batches or starts translation runs itself. A token carries no language restriction and no role, so you can only give it access you hold on the project yourself. If your access is narrower, some abilities are not offered and the dialog says: "Some abilities are not listed: a token carries no language scope and no role, so it can only be given access you hold on this project yourself. An admin can mint the rest." ## Create a token [#create-a-token] ### Open Developer settings [#open-developer-settings] Open the project, then **Settings** → **Developer**. Under **API tokens**, select **New token**. ### Fill in the dialog [#fill-in-the-dialog] The **Create API token** dialog asks for three things: - **Name**: where the token will live, such as `ci-deploy` (up to 100 characters). - **Abilities**: one or more of `push`, `pull`, `manage`, `read` and `write`. - **Expiry**: **Never expires**, **30 days**, **90 days** or **365 days**. Select **Create token**. ### Copy the value [#copy-the-value] **Copy your new API token** shows the token once: "This token is shown only once. Store it in your CI secrets now. It cannot be recovered later." Select **Copy**, store it, then select **Done**. transglot keeps only a hash of the token, so nobody can show it to you again. If you close the dialog without copying, revoke the token and create another. ## Store the token [#store-the-token] Every transglot tool reads the token from the `TRANSGLOT_TOKEN` environment variable: | Tool | Where the token goes | | --- | --- | | CLI in CI | A CI secret exposed as `TRANSGLOT_TOKEN`. See [Use the CLI in CI](https://transglot.ai/docs/developers/cli/continuous-integration). | | GitHub Action | A repository secret named `TRANSGLOT_TOKEN`, passed to the action's `token` input. See [The GitHub Action](https://transglot.ai/docs/developers/github-action). | | MCP server | The `TRANSGLOT_TOKEN` entry in your MCP client's configuration. | | REST API | The `Authorization: Bearer ` header. See [Authentication](https://transglot.ai/docs/developers/api/authentication). | Never commit a token to `transglot.json` or to a workflow file. On a laptop you rarely need to create a token by hand: `transglot init` signs you in through the browser and creates a token with `push` and `pull`, named after your machine. It is listed with the project's other tokens. ## Track and revoke tokens [#track-and-revoke-tokens] The **API tokens** list shows each token's name and creator, its abilities, when it was **last used**, and when it **expires** (`never` for tokens without an expiry). The project's **Releases** page also charts calls per token over the last 14 days under **Token usage**, in its **Connect & SDK** view. To revoke a token, select **Revoke** on its row. The confirmation explains: "Any CLI or CI job authenticating with this token starts failing on its next run. The token value cannot be recovered, so a replacement has to be created and stored again." Type the token's name to continue, then select **Revoke token**. Revoking takes effect immediately. To rotate a token without downtime, create the replacement first, update the secret everywhere it is used, then revoke the old one. ## Other credentials [#other-credentials] A project has three other kinds of credential. They are created on the same **Developer** settings page, and none of them can replace a deploy token. | Credential | Looks like | Secret? | What it is for | | --- | --- | --- | --- | | Deploy token | `tgl_…` | Yes, shown once | The CLI, the GitHub Action, the REST API and the MCP server. | | [Publishable key](https://transglot.ai/docs/developers/in-context/publishable-keys) | `taip_…` | No. Safe to embed in client JavaScript, limited to the origins you allow, and listed so you can copy it again. | Loading the in-context editor SDK in your app. | | [CDN key](https://transglot.ai/docs/guides/delivery/connect-your-app-to-the-cdn) | `taicdn_…` | Yes, shown once | Read-only access to a project's published runtime bundles. | | [Webhook signing secret](https://transglot.ai/docs/developers/webhooks/verify-signatures) | | Yes, shown once | Verifying that a webhook delivery came from transglot. | See [Project developer settings](https://transglot.ai/docs/developers/project-developer-settings) for the whole page. # Android XML (https://transglot.ai/docs/developers/formats/android/) | Format | Identifier | Extension | | --- | --- | --- | | Android XML | `android_xml` | `.xml` | ```xml title="res/values-fr/strings.xml" Acme Bonjour, %1$s ! Appuyez sur \"Partager\" pour envoyer l\'invitation %d article %d articles ``` ## Keys [#keys] The root element must be ``. Each `` and `` child is a key named by its `name` attribute. - A missing `name`, or the same name twice, is an import error. - Other elements, such as ``, `` or ``, are skipped with a warning. - **`translatable="false"`** (in any letter case) marks a string Android never translates, such as an app name or a URL. transglot does not create a key for it, translate it, or count its words. ## Plurals [#plurals] `` becomes a plural key, one form per ``. Each quantity must be a category the locale uses: Android lint often suggests `zero` or `two` for English, but English has only `one` and `other`, so such an item is an import error. A category the file leaves out is stored empty. On export, a plural key is written as `` with an `` for every category of the locale, in order. ## Escaping [#escaping] Import decodes Android's string escapes: `\'`, `\"`, `\\`, `\n`, `\t`, `\@`, `\?` and `\uXXXX` (including surrogate pairs). An unknown escape loses its backslash. XML entities (`&`, `<`) decode as usual. Export applies the reverse, in this order: backslashes are doubled; newlines, `'` and `"` are escaped; `&`, `<` and `>` become entities; a value starting with `@` or `?` gets a leading backslash so Android does not read it as a resource reference. ## Markup inside strings [#markup-inside-strings] A string is read as its text content. Inline elements inside a ``, such as ``, `` or ``, are not kept: only the text between them is imported. On export, `<`, `>` and `&` in a translation are written as entities, so markup in a translation reaches Android as text. Split a string that needs inline styling into separate keys. ## Placeholders [#placeholders] Format arguments such as `%1$s`, `%d` and `%.2f` are kept exactly as written. See [Placeholders](https://transglot.ai/docs/developers/formats#placeholders). ## What the file must be [#what-the-file-must-be] - UTF-8. Other encodings, and files containing NUL bytes, are rejected. - No `DOCTYPE`. Android resource files never have one, and a file that does is rejected. - Comments are skipped on import and are not written on export. ## Export [#export] The file starts with an XML declaration and a `` root, four-space indentation, one `` or `` per key in byte order of the key names. An API pull names the file `strings-{locale}.xml` in its `Content-Disposition` header. ## Related [#related] - [Plural forms](https://transglot.ai/docs/guides/projects/plural-forms) - [File formats](https://transglot.ai/docs/developers/formats) # Apple (https://transglot.ai/docs/developers/formats/apple/) | Format | Identifier | Extension | Holds | | --- | --- | --- | --- | | Apple Strings | `ios_strings` | `.strings` | Simple strings | | Apple Stringsdict | `ios_stringsdict` | `.stringsdict` | Plural strings only | | Apple String Catalog | `ios_xcstrings` | `.xcstrings` | Simple and plural strings | With `.strings` and `.stringsdict`, keep simple strings in one and plural strings in the other, as Xcode does. A String Catalog holds both. ## Strings [#strings] ```text title="fr.lproj/Localizable.strings" /* Home screen title */ "home.title" = "Bon retour"; "home.greeting" = "Bonjour, %@ !"; "share.hint" = "Appuyez sur \"Partager\""; ``` - Each entry is `"key" = "value";`. Both sides must be double-quoted. - `/* ... */` and `//` comments are skipped, including in the middle of the file; the same characters inside a quoted string are kept. Comments are not imported as descriptions and are not written on export. - Escapes decoded: `\"`, `\\`, `\n`, `\t`, `\r`. Any other backslash sequence, including `\U` Unicode escapes, loses its backslash. - A duplicate key, a missing `=` or `;`, or an unterminated string or comment is an import error that names the problem. - The file must be UTF-8. Convert UTF-16 `.strings` files first, for example with `iconv -f UTF-16 -t UTF-8`. On export, each key is one line, `\\`, `\"` and newlines are escaped, and plural keys are left out (they belong in `.stringsdict`). ## Stringsdict [#stringsdict] ```xml title="fr.lproj/Localizable.stringsdict" items NSStringLocalizedFormatKey %#@value@ value NSStringFormatSpecTypeKey NSStringPluralRuleType NSStringFormatValueTypeKey d one %d article other %d articles ``` Each top-level `` is a plural key. Its `NSStringLocalizedFormatKey` must reference a variable (`%#@name@`, any name), and that variable's dictionary must be `NSStringPluralRuleType`, with one `` per plural category. - Each category must be one the locale uses. `zero` in an English file is an import error, because English has only `one` and `other`. - **One variable per entry.** When the format key references several variables (`%#@files@ in %#@folders@`), only the first variable's forms are imported, with a warning. - **The format key is not kept.** Export always writes `%#@value@` as the format key, a variable named `value`, and `NSStringFormatValueTypeKey` `d`. Text around the variable in your format key (`%#@count@ remaining`) is not carried; put it in each plural form instead. - The standard Apple `DOCTYPE` is accepted. A `DOCTYPE` with its own declarations is rejected. On export, simple keys are left out, so a project with no plural keys exports an empty plist. Export uses four-space indentation. ## String Catalog [#string-catalog] Xcode 15 String Catalogs are JSON files holding every language of a table: ```json title="Localizable.xcstrings" { "sourceLanguage": "en", "strings": { "home.title": { "comment": "Home screen title", "localizations": { "fr": { "stringUnit": { "state": "translated", "value": "Bon retour" } } } }, "items": { "localizations": { "fr": { "variations": { "plural": { "one": { "stringUnit": { "state": "translated", "value": "%lld article" } }, "other": { "stringUnit": { "state": "translated", "value": "%lld articles" } } } } } } } }, "version": "1.0" } ``` ### Import [#import] A catalog is read for one locale at a time: the locale you import picks the matching `localizations` entry (`pt-BR` and `pt_BR` match). Keys with no localization for that locale are skipped, which is normal for a catalog. When you import the catalog's `sourceLanguage` and a key has no explicit localization, the key itself is the value, as Xcode does. - `comment` becomes the key's description. - `variations.plural` becomes a plural key. Each category must be one the locale uses. - Device and width variations, and `substitutions`, are skipped with a warning. - The `state` of a string unit is not imported: an import never marks a translation as reviewed. - Only catalog `version` `1.0` is read. ### Export [#export] An export is a catalog with a single locale: `version` `1.0`, `sourceLanguage` set to the project's source locale, `comment` from the key's description, and every unit with state `translated`. It is not a replacement for a multi-language catalog in your Xcode project, which holds every language in one file. `transglot init` does not set up String Catalogs. ## Placeholders [#placeholders] Import and export keep format specifiers such as `%@`, `%d`, `%lld` and `%1$@` exactly as written. The [quality checks](https://transglot.ai/docs/concepts/quality-checks) compare the printf-style ones (`%d`, `%1$s`) between source and translation; see [Placeholders](https://transglot.ai/docs/developers/formats#placeholders) for the syntaxes they recognize. ## Related [#related] - [Plural forms](https://transglot.ai/docs/guides/projects/plural-forms) - [File formats](https://transglot.ai/docs/developers/formats) # Flutter ARB (https://transglot.ai/docs/developers/formats/flutter-arb/) | Format | Identifier | Extension | | --- | --- | --- | | Flutter ARB | `flutter_arb` | `.arb` | ARB is JSON with metadata members: `@@locale` names the file's locale, and `@key` holds metadata about `key`. ```json title="lib/l10n/app_fr.arb" { "@@locale": "fr", "greeting": "Bonjour, {name} !", "@greeting": { "description": "Shown at the top of the home screen." }, "itemCount": "{count, plural, one{{count} article} other{{count} articles}}" } ``` ## Keys [#keys] Each member whose name does not start with `@` is a key, and its name is the key name. Values must be strings. - `@@locale` is checked against the locale you are importing; a mismatch is accepted with a warning. Other `@@` members are ignored. - `@key.description` becomes the key's description. Other metadata, such as `placeholders`, is not read. - A `@key` with no matching key is accepted with a warning. ## Plurals [#plurals] A value shaped like an ICU plural message, `{variable, plural, category{...} ...}`, is imported as a plural key, with one form per category. Any variable name is accepted. Braces nest inside a form (`one{{count} article}`), and ICU apostrophe quoting is understood. - Every category must be one the locale uses. `few` in an English file is an import error. A category the file leaves out is stored empty. - A plural message with an exact-value selector, such as `=0{No items}`, is kept whole as an ordinary string, so its `=N` branches are not lost. The same goes for `select` messages. Either way the message is written back exactly as it came in. - A value that is not a plural message is an ordinary string. On export, a plural key is written as `{count, plural, ...}` with the locale's categories in order. The variable is always `count`. ## Placeholders [#placeholders] Placeholders such as `{name}` and `{count}` are kept as written. Inside plural forms, any other literal brace is written with ICU quoting (`'{'`, `'}'`), and an apostrophe is doubled (`''`), so a form can never break the message structure. Import decodes the same quoting. ## Export [#export] - `@@locale` comes first and is the exported locale. - Keys follow in byte order, each followed by `@key` with its `description` when the key has one. - Two-space indentation, one trailing newline. - Untranslated keys are left out. Because only `description` is written to `@key`, other metadata from your files (such as `placeholders` declarations) is not in an export. Keep your template ARB (the source-language file gen_l10n reads metadata from) as a file you edit yourself, and pull only the target locales. ## Runtime delivery [#runtime-delivery] `flutter_arb` is one of the four delivery formats the [runtime SDKs](https://transglot.ai/docs/developers/sdks) read, and the only one of them that carries plural keys. See [Delivery formats](https://transglot.ai/docs/developers/sdks/javascript#delivery-formats). ## Related [#related] - [Plural forms](https://transglot.ai/docs/guides/projects/plural-forms) - [File formats](https://transglot.ai/docs/developers/formats) # gettext PO (https://transglot.ai/docs/developers/formats/gettext/) | Format | Identifier | Extensions | | --- | --- | --- | | gettext PO | `gettext_po` | `.po`, `.pot` (exports use `.po`) | | Unreal PO | `unreal_po` | `.po` | Both formats share the PO syntax but key their entries differently. gettext keys on the source text; Unreal keys on an identity it stores in `msgctxt`. transglot never guesses Unreal PO: choose `unreal_po` explicitly for files from Unreal's Localization Dashboard. ## gettext PO [#gettext-po] ```po title="locale/fr.po" msgid "" msgstr "" "Language: fr\n" "Content-Type: text/plain; charset=UTF-8\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #. Shown on the home screen msgid "Welcome back" msgstr "Bon retour" msgctxt "menu" msgid "Open" msgstr "Ouvrir" msgid "%d item" msgid_plural "%d items" msgstr[0] "%d article" msgstr[1] "%d articles" ``` ### Keys [#keys] The `msgid` is the key, because gettext keys on the source text. An entry with a `msgctxt` is keyed `context|msgid`: the file above imports the keys `Welcome back`, `menu|Open` and `%d item`. On export, the key is split at the first unescaped `|` back into `msgctxt` and `msgid`. A literal `|` or `\` in a context, or a `|` in a `msgid` without context, is escaped with a backslash in the key name, so every key maps back to exactly one entry. ### What is imported [#what-is-imported] - `#.` extracted comments become the key's description. - Entries flagged `#, fuzzy` are skipped with a warning, as gettext tools treat them as unreliable. - Entries with an empty `msgstr` are untranslated and are skipped. So are obsolete `#~` entries. - `#:` references, `#|` previous strings and `#` translator comments are not imported. - The header's `Language` is checked against the locale you import; a mismatch is accepted with a warning. - Strings may span several quoted lines, and the C escape sequences (`\n`, `\t`, `\"`, `\\`, octal and hex escapes) decode. - A duplicate entry, a second header, or a `msgstr[N]` without `msgid_plural` is an error that names the line. ### Plurals [#plurals] An entry with `msgid_plural` becomes a plural key. transglot reads the header's `Plural-Forms` expression to work out which CLDR category each `msgstr[N]` belongs to: - A standard two-form file maps `msgstr[0]` to `one` and `msgstr[1]` to `other`. - A three-form file for Russian, Ukrainian, Polish and similar languages maps to `one`, `few` and `many`, and also fills `other` from `many`. - A form that has no CLDR category for the locale is dropped with a warning. A `Plural-Forms` expression transglot does not recognize falls back to first form as `one` and last as `other`, with a warning saying exactly what was mapped. - A file without a `Plural-Forms` header is read with the locale's standard convention. - A `msgstr[N]` beyond the declared `nplurals` is an error. On export, transglot writes the locale's standard `Plural-Forms` line and orders `msgstr[N]` to match it, so `msgfmt --check` accepts the file. `msgid_plural` is the source locale's plural text when there is one. ### Export [#export] Exports start with a header holding `Language`, `MIME-Version`, `Content-Type` (UTF-8), `Content-Transfer-Encoding` and `Plural-Forms`. There is no date in it, so pulling twice with no changes gives identical files. Each entry has its description as `#.` lines. A key whose name is empty cannot be exported, because it would collide with the header. Exports keep translations that are not valid ICU, because PO is a format for reviewers as well as apps. ## Unreal PO [#unreal-po] The `.po` files Unreal Engine's Localization Dashboard exports and imports (Export Text and Import Text). ```po title="Content/Localization/Game/fr/Game.po" msgid "" msgstr "" "Language: fr\n" "Content-Type: text/plain; charset=UTF-8\n" #. Key: MainMenu_Play #. SourceLocation: /Game/UI/MainMenu.MainMenu msgctxt "MainMenu,MainMenu_Play" msgid "Play" msgstr "Jouer" ``` ### Keys [#keys-1] - The key is the `msgctxt` (`Namespace,Key`), and `msgid` is the source text, not the key. - A file without `msgctxt` falls back to the `#. Key:` comment. Without either, the entry is keyed on its `msgid`, with a warning. - `#. SourceLocation:` and `#. InfoMetaData:` comments and `#:` references are Unreal's own scaffolding and are dropped. Any other `#.` line becomes the key's description. - Fuzzy entries are skipped with a warning; untranslated (empty `msgstr`) entries are skipped. - Escapes decoded: `\\`, `\"`, `\n`, `\t`, `\r`. Any other escape is kept as written, with a warning. ### Export [#export-1] Each entry is written as `msgctxt` (the key), `msgid` (the source text) and `msgstr` (the translation), with the description as `#.` lines, after a date-free header. Unreal handles plurals in the engine, not in PO files, so Unreal PO carries simple strings only and plural keys are left out of an export. If you need plural forms in a PO file, use `gettext_po`. ## Related [#related] - [XLIFF](https://transglot.ai/docs/developers/formats/xliff) - [Plural forms](https://transglot.ai/docs/guides/projects/plural-forms) - [File formats](https://transglot.ai/docs/developers/formats) # File formats (https://transglot.ai/docs/developers/formats/) transglot reads and writes twenty file formats. Each has an identifier, the string you put in the `format` field of a [CLI rule](https://transglot.ai/docs/developers/cli/configuration), in API requests, and in **Delivery format** for the [runtime SDKs](https://transglot.ai/docs/developers/sdks). ## Formats [#formats] | Format | Identifier | Extensions | Plurals | Nested keys | Descriptions | | --- | --- | --- | --- | --- | --- | | [JSON (flat)](https://transglot.ai/docs/developers/formats/json) | `json_flat` | `.json` | No | No | No | | [JSON (nested)](https://transglot.ai/docs/developers/formats/json) | `json_nested` | `.json` | No | Yes, split on `.` | No | | [Laravel JSON](https://transglot.ai/docs/developers/formats/laravel) | `laravel_json` | `.json` | No | No | No | | [Laravel PHP](https://transglot.ai/docs/developers/formats/laravel) | `laravel_php` | `.php` | No | No | No | | [YAML](https://transglot.ai/docs/developers/formats/yaml) | `yaml` | `.yml`, `.yaml` | No | Yes, split on `.` | No | | [Flutter ARB](https://transglot.ai/docs/developers/formats/flutter-arb) | `flutter_arb` | `.arb` | Yes, ICU plural messages | No | Yes | | [Android XML](https://transglot.ai/docs/developers/formats/android) | `android_xml` | `.xml` | Yes, `` | No | No | | [Apple Strings](https://transglot.ai/docs/developers/formats/apple#strings) | `ios_strings` | `.strings` | No | No | No | | [Apple Stringsdict](https://transglot.ai/docs/developers/formats/apple#stringsdict) | `ios_stringsdict` | `.stringsdict` | Plural keys only | No | No | | [Apple String Catalog](https://transglot.ai/docs/developers/formats/apple#string-catalog) | `ios_xcstrings` | `.xcstrings` | Yes | No | Yes | | [XLIFF 1.2](https://transglot.ai/docs/developers/formats/xliff) | `xliff_12` | `.xlf`, `.xliff` | No | No | Yes | | [XLIFF 2.0](https://transglot.ai/docs/developers/formats/xliff) | `xliff_20` | `.xlf`, `.xliff` | No | No | No | | [gettext PO](https://transglot.ai/docs/developers/formats/gettext) | `gettext_po` | `.po`, `.pot` | Yes, `msgid_plural` | No | Yes | | [Unreal PO](https://transglot.ai/docs/developers/formats/gettext#unreal-po) | `unreal_po` | `.po` | No | No | Yes | | [Java properties](https://transglot.ai/docs/developers/formats/java-properties) | `java_properties` | `.properties` | No | No | No | | [.NET resx](https://transglot.ai/docs/developers/formats/resx) | `resx` | `.resx` | No | No | No | | [CSV](https://transglot.ai/docs/developers/formats/spreadsheets) | `csv` | `.csv` | Yes, one row per form | No | Yes | | [Excel](https://transglot.ai/docs/developers/formats/spreadsheets) | `xlsx` | `.xlsx` | Yes, one row per form | No | Yes | | [Unity CSV](https://transglot.ai/docs/developers/formats/spreadsheets#unity-csv) | `unity_csv` | `.csv` | No | No | Yes | | [Markdown / MDX](https://transglot.ai/docs/developers/formats/markdown) | `markdown` | `.md`, `.mdx` | No | Not key-based | No | **Descriptions** means the file has a place for a note to translators, which becomes the key's description when you import and is written back when you export. In the other formats, keep descriptions in the console. ## How a format is chosen [#how-a-format-is-chosen] - **CLI and API.** Every CLI rule names its format, and the API takes it as a parameter. `transglot init` proposes one from your files. - **Browser upload.** transglot guesses from the file name and contents, and shows the guess with a control to change it before anything is imported. When it cannot tell, it asks rather than guessing. The guess uses the extension first, then the contents for extensions several formats share: - `.json`: `@@locale` means Flutter ARB; `sourceLanguage` with `strings` means an Apple String Catalog; any nested object means JSON (nested); a key containing a space means Laravel JSON; otherwise JSON (flat). - `.xml`: `` means Android, `` with `` means .NET resx, `` means Stringsdict. - `.xlf` and `.xliff`: the `version` on ``, or the namespace, picks 1.2 or 2.0. Unity CSV and Unreal PO are never guessed, because their files look like CSV and gettext PO. Choose them explicitly. ## One file, one locale [#one-file-one-locale] Every format is read and written one locale at a time. A file that holds several languages (an Apple String Catalog, a Unity string table) is read for the locale you are importing, and an export contains that one locale. ## Keys [#keys] Each format derives key names in its own way, described on its page. Two rules apply everywhere: - A key name appears once per file. A duplicate is an error, not a silent overwrite. - **Nested formats** (JSON (nested) and YAML) join nested object keys with `.` when you import (`{"home": {"title": ...}}` becomes `home.title`) and split on `.` when you export. A project that holds both `home` and `home.title` cannot be exported to them, because `home` would have to be both a string and an object. The export fails and names the colliding keys; publishing in such a format fails with the problem type `nested-key-collision`. Flat formats keep dots as part of the key. ## Plurals [#plurals] transglot stores a plural key as one form per CLDR plural category of each locale: `one` and `other` for English, `one`, `few`, `many` and `other` for Polish, and so on. See [Plural forms](https://transglot.ai/docs/guides/projects/plural-forms). - Formats with a plural construct (the ones marked in the table) read and write those forms. A category the file leaves out is stored empty. In most of them, a category the locale does not use, such as `few` in an English file, is an import error; each format's page says how it handles one. - Formats without one cannot carry a plural key. Exporting to them leaves plural keys out and counts them (the `X-Skipped-Keys` response header on an API pull). - In any format, a string can hold an ICU message such as `{count, plural, one {# item} other {# items}}`. It is stored as an ordinary string and written back unchanged. ## Placeholders [#placeholders] Placeholders are read and written exactly as they appear in the file; no format rewrites them. transglot recognizes these syntaxes, and its [quality checks](https://transglot.ai/docs/concepts/quality-checks) flag a translation that loses or alters one: | Syntax | Examples | | --- | --- | | Double braces | `{{name}}` | | Single braces (ICU, .NET, Python, Rails `%{name}`) | `{name}`, `{0}`, `{ user.name }` | | printf | `%s`, `%d`, `%1$s`, `%.2f`, `%%` | | HTML tags | ``, ``, `` | | Laravel parameters | `:name`, `:count` | ## What an export leaves out [#what-an-export-leaves-out] An export, a CLI pull, or an API pull contains only translated keys the format can carry. The API's pull response reports each kind of omission in its own header: | Header | Counts | | --- | --- | | `X-Omitted-Empty-Keys` | Keys with no translation yet in that locale. | | `X-Skipped-Keys` | Keys the format cannot carry, such as plural keys in JSON, or simple keys in Stringsdict. | | `X-Omitted-Invalid-Icu-Keys` | On a project whose message syntax is ICU, translations left out because they are not valid ICU. | The export screen in the console warns before you choose a format that would leave keys out, and blocks one that cannot produce a usable file for the project. ## Encoding [#encoding] Files are read as UTF-8. A leading byte order mark is ignored in the JSON, ARB, String Catalog, Apple Strings, YAML, Java properties, CSV and Unreal PO formats and in the XML-based formats. Other encodings are not supported: convert UTF-16 files (common for older Apple `.strings`) to UTF-8 first. ## Related [#related] - [Which file formats can I use?](https://transglot.ai/docs/guides/projects/which-file-formats-can-i-use) - [Import content](https://transglot.ai/docs/guides/projects/import-content) - [CLI configuration](https://transglot.ai/docs/developers/cli/configuration) # Java properties (https://transglot.ai/docs/developers/formats/java-properties/) | Format | Identifier | Extension | | --- | --- | --- | | Java properties | `java_properties` | `.properties` | The format `java.util.Properties` and `ResourceBundle` read, also used by Spring's `messages_{locale}.properties`. ```properties title="messages_fr.properties" # Home screen home.title=Bon retour home.greeting=Bonjour, {0} ! cart.summary = Vous avez {0} articles \ dans votre panier ``` ## Keys [#keys] Each logical line is one key. The whole left-hand side is the key name, dots included; keys are never nested. - The separator is `=`, `:` or the first run of whitespace. Whitespace around an `=` or `:` is ignored. - A line whose first non-blank character is `#` or `!` is a comment and is not imported. - A line ending in an odd number of backslashes continues on the next line. The backslash is dropped, and so is the next line's leading whitespace. - A key that appears twice is an error. ## Escapes [#escapes] Import decodes `\n`, `\t`, `\r`, `\f`, `\\`, `\uXXXX`, and the escaped separators `\=`, `\:`, `\#`, `\!` and `\ ` (space). Any other backslash is dropped, as Java does. A malformed `\u` escape is an error. The file must be UTF-8. Older files that store accented characters as raw Latin-1 bytes are rejected; files that use `\uXXXX` escapes for them are plain ASCII and read fine. ## Plurals [#plurals] Properties files have no plural construct. Plural keys are left out of an export. A `MessageFormat` choice pattern (`{0,choice,0#no files|1#one file|1<{0} files}`) is an ordinary string and is written back unchanged. ## Placeholders [#placeholders] `MessageFormat` arguments (`{0}`, `{1,number}`) and `%s`-style specifiers are kept as written. See [Placeholders](https://transglot.ai/docs/developers/formats#placeholders). ## Export [#export] Each key is written as `key=value` on its own line, in byte order. The output is pure ASCII, so it loads under a Latin-1 reader too: - Backslashes, tabs, newlines, carriage returns and form feeds are escaped, as are `=`, `:`, `#` and `!`, and spaces that are leading or inside the key. - Every character above U+007F, and the remaining control characters, is written as `\uXXXX` (characters outside the Basic Multilingual Plane as a surrogate pair). Comments are not kept. ## Related [#related] - [File formats](https://transglot.ai/docs/developers/formats) # JSON (https://transglot.ai/docs/developers/formats/json/) | Format | Identifier | Extension | | --- | --- | --- | | JSON (flat) | `json_flat` | `.json` | | JSON (nested) | `json_nested` | `.json` | Laravel's `lang/{locale}.json` files use the same flat shape under their own identifier, `laravel_json`. See [Laravel](https://transglot.ai/docs/developers/formats/laravel). ## JSON (flat) [#json-flat] A single object of string values. The key is the property name, dots and all. ```json title="locales/en.json" { "home.title": "Welcome back", "home.greeting": "Hello, {name}!", "cart.items_one": "{count} item", "cart.items_other": "{count} items" } ``` - Every value must be a string. A number, boolean or `null` is an import error. - A value that is an object or array is an error: `Key "home" holds a nested group, which the JSON (flat) format cannot read. This file needs JSON (nested).` ## JSON (nested) [#json-nested] Objects nest to any depth. On import, the path to each string is joined with `.` to form the key; on export, keys are split on `.` to rebuild the objects. ```json title="locales/en.json" { "home": { "title": "Welcome back", "greeting": "Hello, {name}!" }, "cart": { "items_one": "{count} item", "items_other": "{count} items" } } ``` This file imports the keys `home.title`, `home.greeting`, `cart.items_one` and `cart.items_other`. - Every leaf must be a string. - Arrays are read like objects whose keys are the indexes (`steps.0`, `steps.1`), and are written back as objects with those keys, not as arrays. - A property name that contains a dot is joined like any other segment, so `{"a.b": {"c": "x"}}` imports as `a.b.c` and exports as `{"a": {"b": {"c": "x"}}}`. - If two paths flatten to the same key (`{"a.b": "x", "a": {"b": "y"}}`), the import fails with a duplicate-key error. - A project where one key is a prefix of another (`home` and `home.title`) cannot be exported as nested JSON. See [Keys](https://transglot.ai/docs/developers/formats#keys). ## Plurals [#plurals] Neither JSON format has a plural construct. You have two options: - **Suffix keys**, as i18next does (`items_one`, `items_other`). transglot stores each as an ordinary key, so each suffix is translated separately. Add the suffixes the target language needs (`items_few`, `items_many`) as keys of their own. - **ICU messages** in a value, as react-intl and next-intl expect: `"{count, plural, one {# item} other {# items}}"`. The whole message is one string. Plural keys you create in transglot (with one form per category) are not written to JSON: an export leaves them out and counts them. Use [Flutter ARB](https://transglot.ai/docs/developers/formats/flutter-arb), [Android XML](https://transglot.ai/docs/developers/formats/android) or a [spreadsheet](https://transglot.ai/docs/developers/formats/spreadsheets) to carry them, or publish `flutter_arb` for the runtime SDKs. ## Placeholders [#placeholders] Values are read and written exactly as they are, so `{name}`, `{{name}}`, `%s` and ICU arguments survive a round trip. See [Placeholders](https://transglot.ai/docs/developers/formats#placeholders). ## Export [#export] Exports are UTF-8 with two-space indentation, keys in byte order, non-ASCII characters and slashes written as themselves rather than escaped, and one trailing newline. Untranslated keys are left out. JSON has no place for descriptions; keep them in the console. ## Related [#related] - [File formats](https://transglot.ai/docs/developers/formats) - [Runtime SDKs](https://transglot.ai/docs/developers/sdks) - [i18next backend](https://transglot.ai/docs/developers/sdks/i18next) # Laravel (https://transglot.ai/docs/developers/formats/laravel/) | Format | Identifier | Extension | Typical path | | --- | --- | --- | --- | | Laravel JSON | `laravel_json` | `.json` | `lang/{locale}.json` | | Laravel PHP | `laravel_php` | `.php` | `lang/{locale}/messages.php` | ## Laravel JSON [#laravel-json] The file Laravel's `__('...')` helper reads when you key strings by their source sentence. It is a flat JSON object, and transglot reads and writes it exactly like [JSON (flat)](https://transglot.ai/docs/developers/formats/json): every value must be a string, and nested objects are an error. ```json title="lang/fr.json" { "Welcome back": "Bon retour", "Hello, :name!": "Bonjour, :name !", "You have :count new messages.": "Vous avez :count nouveaux messages." } ``` The key is the source sentence. When transglot guesses the format of a `.json` file, a flat file with a space in any key is taken as Laravel JSON. ## Laravel PHP [#laravel-php] A PHP file returning a flat array of strings: ```php title="lang/fr/messages.php" 'Bon retour', 'greeting' => 'Bonjour, :name !', ]; ``` transglot never runs the file. It reads it with a strict scanner that accepts only this exact shape: - the file starts with ` 'value'`, separated by commas (a trailing comma is fine), - `\\` and `\'` as the only escapes. Anything else is an import error, including comments (such as the doc-comment header Laravel's own language files start with), double-quoted strings, nested arrays, constants, concatenation and function calls. A nested array fails with `Nested arrays are not supported in Laravel PHP files.` So Laravel PHP suits files you let transglot own. For Laravel's bundled files with nested arrays (such as `validation.php`), or files with comments, use Laravel JSON instead. The browser upload does not offer `.php` files; push them with the CLI or the API. On export, transglot writes the same shape, four-space indented, keys in byte order. An empty file is `return [];`. ## Plurals [#plurals] Neither format has a plural construct. Laravel's pipe syntax (`'{0} No messages|{1} One message|[2,*] :count messages'`) is an ordinary string to transglot and is stored and written back as one value. Plural keys created in transglot are left out of a Laravel export. ## Placeholders [#placeholders] `:name`-style parameters (and their `:Name` and `:NAME` variants) are written back exactly as they are, and the [quality checks](https://transglot.ai/docs/concepts/quality-checks) flag a translation that drops one. ## Related [#related] - [JSON](https://transglot.ai/docs/developers/formats/json) - [File formats](https://transglot.ai/docs/developers/formats) # Markdown / MDX (https://transglot.ai/docs/developers/formats/markdown/) | Format | Identifier | Extensions | | --- | --- | --- | | Markdown / MDX | `markdown` | `.md`, `.mdx` (exports use `.md`) | Markdown is a document, not a list of strings, so it works differently from every other format: transglot splits the document into blocks, one key per block, and rebuilds a document with the same structure on export. ## How a document becomes keys [#how-a-document-becomes-keys] Each block gets a key made of its type and its position in the document. This file: ````md title="docs/getting-started.md" --- title: Getting started --- # Getting started Install the **CLI** with [npm](https://npmjs.com). - Create a project - Push your files ```bash npx transglot push ``` | Command | What it does | | --- | --- | | push | Uploads files | > Tip: run pull after push. ```` imports as: | Key | Content | | --- | --- | | `fm:0` | The front matter, verbatim | | `h1:1` | `Getting started` | | `p:2` | `Install the **CLI** with [npm](https://npmjs.com).` | | `ul:3.0`, `ul:3.1` | Each list item | | `code:4` | The fenced code block, verbatim | | `td:5.0.0` ... `td:5.1.1` | Each table cell, by row and column | | `bq:6` | `Tip: run pull after push.` | - **Translatable blocks**: headings, paragraphs, list items, table cells and blockquote text. Inline markup (`**bold**`, links, `` `code` ``) stays inside the text, so translators keep it in place. - **Blocks kept verbatim**: YAML (`---`) or TOML (`+++`) front matter, fenced code blocks, raw HTML and MDX JSX blocks, and MDX `import` and `export` statements. They are stored with their exact text so export can put them back. Text inside front matter, such as a `title`, is part of that verbatim block. Because keys are positions, they only make sense for that document. Adding a paragraph near the top of the source shifts the keys of every block after it. ## Export [#export] Export rebuilds the document from the keys, in position order. The result has the same structure, written in a canonical style: - one blank line between blocks, LF line endings, no trailing whitespace, - ordered lists renumbered from 1, - table delimiter rows written as `---` (column alignment is dropped), - thematic breaks written as `---`. Running the output through transglot again gives the same document, but it is not byte-for-byte your original file. Only a project whose keys all came from a Markdown document can be exported as Markdown. The export screen blocks the format for any other project. ## Limitations [#limitations] - Setext headings (text underlined with `===` or `---`) are not recognized. They become a paragraph followed by a thematic break; use `#` headings. - Indented (four-space) code blocks are read as paragraph text. Use fenced code blocks. - Lazy continuation lines in lists and blockquotes become blocks of their own. - Hard-wrapped paragraphs keep their line breaks; they are not re-wrapped. - The file must be UTF-8, and an unclosed code fence is an import error naming the line it opened on. - Markdown has no plurals. ## Related [#related] - [Import content](https://transglot.ai/docs/guides/projects/import-content) - [File formats](https://transglot.ai/docs/developers/formats) # .NET resx (https://transglot.ai/docs/developers/formats/resx/) | Format | Identifier | Extension | | --- | --- | --- | | .NET resx | `resx` | `.resx` | ```xml title="Resources/Strings.fr.resx" text/microsoft-resx Bon retour Bonjour, {0} ! ``` ## Keys [#keys] The root element must be ``. Each string `` element is a key: `name` is the key name and the text of `` is the translation. - Only string resources are read. A `` with a `type` attribute (an image, an icon) or a `mimetype` attribute (base64 binary) is skipped. - ``, ``, `` and the `` block are skipped. - A `` without `name` or without ``, or a name that appears twice, is an error. - XML entities decode as usual, and `xml:space="preserve"` is honoured, so leading and trailing spaces in a value are kept. - `` elements are not imported. Keep descriptions in the console. - A `DOCTYPE` with its own declarations is rejected. ## Plurals [#plurals] ResX has no plural construct. Plural keys are left out of an export. ## Placeholders [#placeholders] Composite format items such as `{0}` and `{1:N2}` are kept as written. See [Placeholders](https://transglot.ai/docs/developers/formats#placeholders). ## Export [#export] An export is a minimal ResX file: the four standard `` entries (`resmimetype` `text/microsoft-resx`, `version` `2.0`, and the `System.Resources` reader and writer), then one `` per key in byte order, two-space indented. It has no `` block. Carriage returns are written as ` `, and control characters XML cannot represent are replaced with U+FFFD, so the file is always well-formed. Resources other than strings in your original file (images, binary data) are not part of an export, so keep them in a separate `.resx`. ## Related [#related] - [File formats](https://transglot.ai/docs/developers/formats) # Spreadsheets (https://transglot.ai/docs/developers/formats/spreadsheets/) | Format | Identifier | Extension | | --- | --- | --- | | CSV | `csv` | `.csv` | | Excel | `xlsx` | `.xlsx` | | Unity CSV | `unity_csv` | `.csv` | CSV and Excel share one table layout for one locale. Unity CSV is the multi-language sheet Unity's Localization package exports; transglot never guesses it, so choose `unity_csv` explicitly for those files. ## CSV and Excel [#csv-and-excel] ```csv title="fr.csv" "key","value","description" "home.title","Bon retour","Home screen title" "home.greeting","Bonjour, {name} !","" "items[one]","{count} article","Item count in the cart" "items[other]","{count} articles","" ``` ### Columns [#columns] | Column | Required | Holds | | --- | --- | --- | | `key` | Yes | The key name. | | `value` | Yes | The translation, or one plural form. | | `description` | No | The key's description. | - The first non-blank row is the header. Column names are matched without regard to case or surrounding spaces, in any order. Other columns are ignored with a warning; a header without `key` and `value` is an error. - Blank rows, and rows with an empty key, are skipped. - A key that appears twice is an error. - Excel files are read from the **first worksheet** only. Numbers, booleans and dates in cells are read as text. Very large sheets are refused above an import row limit. - CSV files follow RFC 4180: quoted or bare fields, commas, line breaks and doubled quotes (`""`) inside quoted fields, and CRLF, LF or CR line endings. A leading byte order mark (Excel writes one) is ignored. ### Plurals [#plurals] A table has no place for plural categories, so a plural key takes one row per category, with the category in square brackets after the key: `items[one]`, `items[other]`. The rows are combined into one plural key on import. - The bracketed suffix is read as a category only when it is one of the locale's plural categories. Any other bracketed suffix (`filters[advanced]`) is part of an ordinary key name. - A key used both on its own and with a category suffix is an error. - The description can be on any of the key's rows; the first non-empty one is used. On export it is written on the first row. - A category the sheet leaves out is stored empty. ### Formula safety [#formula-safety] A cell that starts with `=`, `+`, `-`, `@`, a tab or a carriage return can run as a formula when the file is opened in Excel or Google Sheets. On export, such a cell gets a leading apostrophe (`'=SUM(A1)`) so it opens as text, and on import that apostrophe is removed again, so values such as `+5 Gold` round-trip unchanged. ### Export [#export] - Columns are always `key`, `value`, `description`, in that order, one row per key (or per plural category) in byte order of the key names. - CSV exports quote every field, use LF line endings and UTF-8 without a byte order mark. - Excel exports write every value as a text cell. An Excel file differs byte for byte between exports (the file is a zip with timestamps), but its contents are the same. ## Unity CSV [#unity-csv] The string table CSV that Unity's Localization package (`com.unity.localization`) exports and imports: a `Key` column, an optional `Shared Comments` column, and one column per language, named `Language(code)`. ```csv title="UI Text.csv" Key,Shared Comments,English(en),French(fr) MainMenu_Play,Main menu button,Play,Jouer MainMenu_Quit,,Quit,Quitter ``` ### Import [#import] The sheet is read for the locale you import: - The value column is the one whose header ends in `(code)` for that locale, such as `French(fr)` for `fr`, or whose header is the locale code itself. Codes match regardless of case and of `-` or `_`. A sheet with no matching column is an error. - `Key` is the key name and is required. A key that appears twice is an error. - `Shared Comments` (or `Description`) becomes the key's description. - Other languages' columns and per-language comment columns are ignored, each reported once. - An empty cell for the locale is skipped, so it never clears an existing translation. ### Export [#export-1] An export has three columns, `Key`, `Shared Comments` and the locale code, which Unity's importer maps back to the language. Cells get the same formula protection as CSV. Unity models plurals as Smart Strings, which have no column in this sheet, so plural keys are left out; use `csv`, `xlsx` or `gettext_po` if you need plural forms in a file. ## Related [#related] - [Plural forms](https://transglot.ai/docs/guides/projects/plural-forms) - [File formats](https://transglot.ai/docs/developers/formats) # XLIFF (https://transglot.ai/docs/developers/formats/xliff/) | Format | Identifier | Extensions | | --- | --- | --- | | XLIFF 1.2 | `xliff_12` | `.xlf`, `.xliff` (exports use `.xlf`) | | XLIFF 2.0 | `xliff_20` | `.xlf`, `.xliff` (exports use `.xlf`) | XLIFF is bilingual: each unit carries the source text and its translation. transglot reads the translation for the locale you import and writes both sides on export, so a translator or tool receiving the file sees the source next to each string. When transglot guesses the format of an `.xlf` file, the `version` on `` (or the namespace) picks 1.2 or 2.0. ## XLIFF 1.2 [#xliff-12] ```xml title="fr.xlf" Welcome back Bon retour Home screen title ``` ### Import [#import] - The key is the `resname` attribute, or `id` when there is no `resname`. A unit with neither is an error, and so is a key that appears twice. - The value is the `` text. A unit with no ``, or an empty one, is skipped with a warning, except when you import the file's source language: then `` is the value. - `` elements become the key's description (several notes are joined with newlines). - Units inside `` elements are read. Other elements, such as `` and ``, are ignored. - `target-language` is checked against the locale you import; a mismatch is accepted with a warning. - The `state` attribute is not imported: an import never marks a translation as reviewed. ## XLIFF 2.0 [#xliff-20] ```xml title="fr.xlf" Welcome back Bon retour ``` ### Import [#import-1] - The XLIFF 2.0 namespace is required. A 1.2 document is rejected rather than read as 2.0. - The key is the `` `id`. A unit without one is an error, and so is a key that appears twice. - A unit with several `` elements is read as one string, the segments joined in order. - The value is the `` text; a unit with no `` is read from its ``. - Only units that are direct children of `` are read. Units inside `` elements are not imported. - ``, `` and elements from other namespaces (such as the metadata, matches or glossary modules) are ignored. XLIFF 2.0 files do not carry descriptions into transglot. ## Inline markup [#inline-markup] transglot stores plain text. A unit whose `` or `` contains inline elements (``, ``, ``, `` in 1.2; ``, ``, ``, ``, `` in 2.0) is skipped with a warning naming it, rather than imported with its placeholders silently removed. Text placeholders such as `%1$s` and `{name}` are ordinary text and are kept. CDATA and XML entities decode like any text. ## Plurals [#plurals] Neither version has a plural construct. Plural keys are left out of an XLIFF export, and an ICU plural message inside a unit is an ordinary string. ## Export [#export] - One unit per key, in byte order, with `` from the project's source locale and `` from the exported locale. 1.2 writes `id` and `resname` (both the key), `state="translated"`, and a `` with the key's description. 2.0 writes one `` per unit. - Language attributes are BCP 47 (`pt-BR`). - When a key has no source text, `` is empty rather than filled with the translation, so the file never labels translated text as source. - Carriage returns are written as ` `, and control characters XML cannot represent are replaced with U+FFFD, so the file is always well-formed. - Unlike files for apps, XLIFF exports keep translations that are not valid ICU, because a reviewer needs to see them. ## Security [#security] A `DOCTYPE` with its own declarations is rejected, external DTDs are never fetched, and entities are never expanded from them. ## Related [#related] - [gettext PO](https://transglot.ai/docs/developers/formats/gettext) - [File formats](https://transglot.ai/docs/developers/formats) # YAML (https://transglot.ai/docs/developers/formats/yaml/) | Format | Identifier | Extensions | | --- | --- | --- | | YAML | `yaml` | `.yml`, `.yaml` (exports use `.yml`) | The layout Rails I18n and similar libraries use: a single top-level key naming the locale, with the strings nested under it. ```yaml title="config/locales/fr.yml" fr: home: title: "Bon retour" greeting: "Bonjour, %{name} !" cart: items: one: "%{count} article" other: "%{count} articles" ``` ## Keys [#keys] Nested map keys are joined with `.`, below the locale root: the file above imports `home.title`, `home.greeting`, `cart.items.one` and `cart.items.other`. On export, keys are split on `.` to rebuild the maps, with the same rules as [JSON (nested)](https://transglot.ai/docs/developers/formats/json#json-nested): a project where one key is a prefix of another (`home` and `home.title`) cannot be exported as YAML, and two paths that flatten to the same key are an import error. ## What the file must contain [#what-the-file-must-contain] - **Exactly one root key**, the locale. A different root key than the locale you are importing is accepted with a warning; both `pt-BR` and `pt_BR` match `pt_BR`. An empty root (`fr:` with nothing under it) is a valid empty file. - **Maps and strings only.** Every leaf must be a string. A sequence (a YAML list, such as Rails' `day_names`) is an error, and so is an unquoted number or boolean (`count: 5`, `enabled: true`): quote such values if they are text. - Comments, either quote style, multi-line strings and anchors are all accepted. Alias expansion is bounded, so a file built to expand without limit is rejected. ## Plurals [#plurals] YAML has no plural construct of its own. Rails-style plurals (`items.one`, `items.other`) are ordinary nested keys to transglot, each translated as its own string. Add the categories a target language needs (`few`, `many`) as keys of their own. Plural keys you create in transglot are left out of a YAML export. ## Placeholders [#placeholders] `%{name}` (Rails), `{name}` and `%s` are kept exactly as written. See [Placeholders](https://transglot.ai/docs/developers/formats#placeholders). ## Export [#export] Exports are written so that they load the same way in every YAML parser: - The root key is the locale in hyphenated form (`pt-BR`, `zh-CN`), which is how Rails names locales. - Two-space indentation. - Every value is double-quoted, with `\n`, `\t`, `\r`, `\"` and `\\` escapes, so multi-line text never folds. - A key is written bare only when it is a plain identifier that no YAML parser would read as something else; keys such as `no`, `on`, `true` or digits are double-quoted. Comments and anchors from the file you imported are not kept. ## Related [#related] - [JSON](https://transglot.ai/docs/developers/formats/json) - [File formats](https://transglot.ai/docs/developers/formats) # The GitHub Action (https://transglot.ai/docs/developers/github-action/) The transglot GitHub Action runs the [CLI](https://transglot.ai/docs/developers/cli) in a workflow. On each run it pushes your source strings, waits for the translation batch to finish, pulls the translated files, and commits them to the branch the workflow ran on. Reference it as `transglot/transglot-action@v1`; the `v1` tag moves to each new release of the action. ## Before you start [#before-you-start] 1. **Commit `transglot.json`** to the repository. Run `npx transglot@latest init` in your checkout to write one, or see the [configuration reference](https://transglot.ai/docs/developers/cli/configuration). 2. **Create a deploy token** with the **push** and **pull** abilities. With **push** alone the action uploads strings but cannot bring translations back. See [Deploy tokens](https://transglot.ai/docs/developers/deploy-tokens). 3. **Add the token as a repository secret** named `TRANSGLOT_TOKEN`, under **Settings** → **Secrets and variables** → **Actions** in GitHub. The token never goes in the workflow file. ## Add the workflow [#add-the-workflow] Create `.github/workflows/transglot.yml`: ```yaml title=".github/workflows/transglot.yml" name: transglot on: push: branches: [main] jobs: localize: runs-on: ubuntu-latest permissions: contents: write # the action commits pulled translations back to a branch steps: - uses: actions/checkout@v7 - uses: transglot/transglot-action@v1 with: token: ${{ secrets.TRANSGLOT_TOKEN }} url: https://api.transglot.ai ``` The project's **Releases** page shows the same workflow in its **Connect & SDK** view, under **GitHub Action**: "Drop this into .github/workflows/transglot.yml and add TRANSGLOT_TOKEN to your repo secrets." `permissions: contents: write` is required for the default commit mode. Without it, the commit step fails with a 403 because the job's token is read-only. A commit made with the workflow's own `GITHUB_TOKEN` does not start another workflow run, so committing translations to `main` does not trigger this workflow again. ## Inputs [#inputs] The action accepts only these inputs. GitHub ignores an input the action does not declare and only prints a warning, so check the spelling if a setting seems to have no effect. ## Outputs [#outputs] | Output | Description | | --- | --- | | `batch_id` | The id of the translation batch the push queued. Empty when nothing needed translating. | | `files_changed` | How many files the pull rewrote. `0` when every file was already up to date. | ## What a run does [#what-a-run-does] 1. **Installs the CLI** on Node.js 22, once per run. 2. **Pushes** your source files (`transglot push`, with `--wait` unless `wait` is `"false"`). With `--wait`, the step waits up to 600 seconds. 3. **Pulls** the translated files (`transglot pull`). It pulls when the push succeeded, when the word limit held the batch back (your sources are still synced), and when the batch failed, because translations that did complete can still be pulled. After any other error it skips the pull. 4. **Commits or uploads** the pulled files, depending on `commit`. In `branch` mode it stages only the files the pull wrote, commits as `github-actions[bot]` with the message `chore(i18n): sync translations from transglot`, and pushes. When nothing changed, or every pulled file is ignored by `.gitignore`, it skips the commit. In `artifact` mode it uploads them as an artifact named `transglot-files`. 5. **Reports** the result. The job fails with the worst [CLI exit code](https://transglot.ai/docs/developers/cli/exit-codes) of the push and the pull. Because this step runs last, a failed batch still commits the translations that finished and sets both outputs before the job turns red. ## Examples [#examples] ### Upload files as an artifact [#upload-files-as-an-artifact] Use `commit: artifact` when the branch is protected, or to review the files before they land: ```yaml title=".github/workflows/transglot.yml" name: transglot on: push: branches: [main] jobs: localize: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: transglot/transglot-action@v1 with: token: ${{ secrets.TRANSGLOT_TOKEN }} commit: artifact ``` The run's summary page lists the `transglot-files` artifact. This mode needs no write permission. ### Open a pull request with the translations [#open-a-pull-request-with-the-translations] With `commit: none` the files stay in the workspace, so a later step can do anything with them. This workflow opens a pull request with a third-party action: ```yaml title=".github/workflows/transglot.yml" name: transglot on: push: branches: [main] jobs: localize: runs-on: ubuntu-latest permissions: contents: write pull-requests: write steps: - uses: actions/checkout@v7 - uses: transglot/transglot-action@v1 id: transglot with: token: ${{ secrets.TRANSGLOT_TOKEN }} commit: none - if: steps.transglot.outputs.files_changed != '0' uses: peter-evans/create-pull-request@v8 with: branch: transglot/translations commit-message: "chore(i18n): sync translations from transglot" title: Sync translations from transglot ``` ### Push now, pull on a schedule [#push-now-pull-on-a-schedule] Large batches can take longer than you want a merge job to run. Push without waiting on every merge, and pull on a schedule with the CLI's `pull --wait`, which waits for every active batch of the project: ```yaml title=".github/workflows/transglot.yml" name: transglot on: push: branches: [main] schedule: - cron: "0 3 * * *" jobs: push-strings: if: github.event_name == 'push' runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v7 with: node-version: 22 - run: npx --yes transglot@0.5.0 push env: TRANSGLOT_TOKEN: ${{ secrets.TRANSGLOT_TOKEN }} pull-translations: if: github.event_name == 'schedule' runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v7 with: node-version: 22 - run: npx --yes transglot@0.5.0 pull --wait --timeout 1800 env: TRANSGLOT_TOKEN: ${{ secrets.TRANSGLOT_TOKEN }} - run: | if [ -n "$(git status --porcelain)" ]; then git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git add -A git commit -m "chore(i18n): sync translations from transglot" git push fi ``` Setting `wait: "false"` on the action is not the same: it pulls straight after pushing, before anything is translated. A locale with no translations yet then stops the pull, because `pull` refuses to overwrite an existing file with an empty one. ### Use the outputs [#use-the-outputs] ```yaml - uses: transglot/transglot-action@v1 id: transglot with: token: ${{ secrets.TRANSGLOT_TOKEN }} - if: steps.transglot.outputs.batch_id != '' run: echo "Translated in batch ${{ steps.transglot.outputs.batch_id }}, ${{ steps.transglot.outputs.files_changed }} file(s) changed" ``` ## Troubleshooting [#troubleshooting] | What you see | Cause and fix | | --- | --- | | A 403 when the action pushes the commit | The job's token is read-only. Add `permissions: contents: write`, or use `commit: artifact`. | | Authentication errors in the log (exit code 2) | The `TRANSGLOT_TOKEN` secret is missing, misspelled, revoked or expired, or the token lacks **push** or **pull**. | | A configuration error (exit code 1) | `transglot.json` is missing from the checkout, or a path in it matches no file. | | The job reports a failed or cancelled batch (exit code 4) | The translations that did complete were still pulled and committed. Retry the failed strings from the project's **Runs** page. | | The job reports the word limit (exit code 3) | Your sources were uploaded; only the translation was held back. Raise the limit or wait for the reset. | | The job reports a timeout (exit code 5) | The batch was still running after 600 seconds. It finishes on its own; the next run pulls the result. | | The run succeeds but commits nothing | Nothing changed since the last run, or the pulled files are ignored by `.gitignore`. | | A setting has no effect | It is not one of the inputs above. GitHub ignores undeclared inputs. | # In-context editor (https://transglot.ai/docs/developers/in-context/) The in-context editor runs inside your own app. A reviewer holds ALT (Option on a Mac) to see which strings are editable, ALT+clicks one, and edits its translation in a panel on top of the page, without hunting for the key in the console. Saves go straight to your transglot project. This section is for the developer who embeds it. For what reviewers do once it is running, see [Connect the in-context editor](https://transglot.ai/docs/guides/quality/connect-the-in-context-editor). ## How it fits together [#how-it-fits-together] ### Your app marks its strings [#your-app-marks-its-strings] You wrap your existing translate function. Every string it returns gets an invisible watermark that maps it back to its key and locale. What users see does not change. ### The editor mounts with a publishable key [#the-editor-mounts-with-a-publishable-key] A publishable key (`taip_...`) is a public id that you ship in your page. From an allowed origin, it buys a short-lived, read-only session. See [Publishable keys](https://transglot.ai/docs/developers/in-context/publishable-keys). ### Your backend grants write access [#your-backend-grants-write-access] To let a reviewer save, your server asks transglot for a single-use nonce using a project token with write access, naming the keys this reviewer may change. The editor trades the nonce for a session that can write those keys. ### Reviewers edit in place [#reviewers-edit-in-place] With editing mode on, ALT+click opens the string under the pointer. A save updates the translation in transglot and the text on the page. ## What reviewers see [#what-reviewers-see] When editing mode is on, a floating bar sits on the page. It shows the locale being edited and each language's completion, lets the reviewer switch locale or add a language (when the session allows it), and has **Done** to leave editing mode. A read-only session is marked **Read only**. ALT+click on a marked string opens the editing panel: - the key, the **Source** text, and the **Target** for the current locale (one field per plural category for a plural key), - live checks on the draft, the same ones the console editor runs against the project's rules, such as placeholders, plural forms and glossary terms, - translation memory matches, each with **Use this**, - **Suggest with AI**, which asks Glossa for a translation, - **Revert** and **Save**. If someone else saved the same string after the panel opened, the save is refused and the panel shows **Their version** next to the reviewer's draft, with **Keep mine** to put their own wording back. A string a reviewer saves counts as edited by a person, so it is never overwritten by AI afterwards. Each save is attributed to the publishable key and to the reviewer label your app passes. When the session can write, the editor also captures a screenshot of the element before asking Glossa for a suggestion, and the capture lands in the project's [screenshot library](https://transglot.ai/docs/guides/quality/the-screenshot-library). Capture uses the optional `html2canvas` package; without it, suggestions still work from the text alone. ## Who should see it [#who-should-see-it] The editor is for reviewers. Do not show it to the public. The bar reports how complete each of your languages is, and anyone in editing mode can open every marked string on the page. The SDK cannot tell a reviewer from a visitor, or your staging site from production, so mounting is inert by default: nothing appears until your app turns editing mode on. Render the editor only for people your app knows may review translations, and either pass `enterOnMount` for them or call `session.enter()` from your own control. ## Requirements [#requirements] - The Starter plan or above. Creating publishable keys and opening sessions both need it. - An admin on the project to create the publishable key. - For saving: a project token with write access on your server. See [Deploy tokens](https://transglot.ai/docs/developers/deploy-tokens). - A browser. The editor works on any page; the watermark has to stay inside one text node, so a translation that renders as several elements (text with inline markup) can be opened but is not updated on the page after a save. ## Next steps [#next-steps] # Install the in-context editor (https://transglot.ai/docs/developers/in-context/install/) There are two packages: | Package | Use it for | | --- | --- | | `@transglot/react` | React 18 and 19 apps. A `TransglotProvider` component and a `useIncontextT` hook. | | `@transglot/incontext` | Any other page: Vue, Svelte, Angular, server-rendered HTML or plain scripts. `mountIncontextEditor` and `wrapT`. The React adapter is a thin layer over it. | Both need a [publishable key](https://transglot.ai/docs/developers/in-context/publishable-keys) whose allowed origins include the page's origin. `@transglot/react` is the in-context editor. To deliver published translations to your users, use [`@transglot/react-i18n`](https://transglot.ai/docs/developers/sdks/react). ## Install [#install] ```npm npm install @transglot/react ``` `@transglot/incontext` is installed with it. React and React DOM 18 or 19 are peer dependencies. ```npm npm install @transglot/incontext ``` The package is ESM only and has no dependencies. To capture screenshots for the [screenshot library](https://transglot.ai/docs/guides/quality/the-screenshot-library) when a reviewer asks for a suggestion, also install the optional peer dependency: ```npm npm install html2canvas ``` Without it, everything else works and suggestions use the text alone. ## React [#react] Render the provider only for people who may review translations, and mark strings with the `t` from `useIncontextT()`: ```tsx title="src/ReviewProvider.tsx" import { TransglotProvider } from '@transglot/react'; import type { ReactNode } from 'react'; import { i18n } from './i18n'; export function ReviewProvider({ user, locale, children }: { user: User; locale: string; children: ReactNode }) { if (!user.canReviewTranslations) { return children; } return ( i18n.t(key, ...args)} reviewer={user.email} writableKeys={user.reviewableKeys} getNonce={(keys) => fetch('/api/transglot-nonce', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ keys }), }) .then((response) => response.json()) .then((body) => body.nonce) } enterOnMount > {children} ); } ``` ```tsx title="src/Hero.tsx" import { useIncontextT } from '@transglot/react'; export function Hero() { const t = useIncontextT(); return

{t('home.hero.title')}

; } ``` `useIncontextT()` calls your `hostT` and adds the invisible watermark to every string it returns. The visible text is the same. It throws `useIncontextT() must be used inside a .` outside the provider, so when the provider is rendered only for reviewers, read the context yourself and fall back to your own `t` for everyone else: ```tsx title="src/useT.ts" import { TransglotContext } from '@transglot/react'; import { useContext } from 'react'; import { i18n } from './i18n'; export function useT() { const incontext = useContext(TransglotContext); return incontext ? incontext.t : i18n.t; } ``` `getNonce` is what lets reviewers save; without it the editor is read-only. It calls an endpoint on your own server, which you write: see [Your backend: the nonce endpoint](#your-backend-the-nonce-endpoint). ### `TransglotProvider` props [#transglotprovider-props] `publishableKey`, `url`, `getNonce`, `writableKeys`, `fetchImpl` and `client` are read once, on the first render. `enterOnMount`, `theme`, `consoleUrl` and `captureElement` are read when the editor mounts. Changing `locale` switches the editor's locale. unknown', default: 'returns the key' }, reviewer: { description: 'A label for the person reviewing, recorded with every save.', type: 'string' }, getNonce: { description: 'Returns a single-use write nonce from your backend. Called with writableKeys each time a session starts or is renewed. Omit it for a read-only editor.', type: '(keys: string[]) => Promise' }, writableKeys: { description: 'The keys this reviewer may change, passed to getNonce.', type: 'string[]' }, theme: { description: 'The editor\'s colour scheme. auto follows prefers-color-scheme.', type: "'light' | 'dark' | 'auto'", default: "'auto'" }, consoleUrl: { description: 'Where a reviewer who cannot add a language is sent instead.', type: 'string', default: 'the hosted console' }, onLocaleChange: { description: 'Called after the reviewer switches locale in the bar. Re-render your own i18n in the new locale here.', type: '(locale: string) => void | Promise' }, onRowsLanded: { description: 'Called with the rows the editor has read, after a locale switch and while new translations arrive, so you can update strings the watermark cannot reach.', type: '(rows: EditorRow[], locale: string) => void | Promise' }, onReady: { description: 'Called once with the editing session, before any editor UI is installed.', type: '(session: EditingSession) => void' }, captureElement: { description: 'Your own screenshot function, instead of the optional html2canvas.', type: '(element: HTMLElement) => Promise' }, fetchImpl: { description: 'The fetch implementation to use.', type: 'typeof fetch' }, client: { description: 'A client created with createIncontextClient, instead of url and publishableKey.', type: 'IncontextClient' }, children: { description: 'Your app.', type: 'ReactNode' }, }} /> ### Other exports [#other-exports] - `useTransglotContext()`: the provider's context value (`{ client, registry, t, locale, reviewer }`). Throws outside the provider. - `TransglotContext`: the React context itself, `null` outside the provider. - `createEditingSession`, and the types `TransglotProviderProps`, `TransglotContextValue`, `EditingSession`, `EditingSessionOptions`, `EditingState`, `LocaleSummary`, re-exported from `@transglot/incontext`. ## Any framework [#any-framework] `mountIncontextEditor` builds the whole editor. `wrapT` marks the strings your app renders, against the registry the editor returns: ```ts title="src/incontext.ts" import { mountIncontextEditor, wrapT } from '@transglot/incontext'; import { messages, render } from './app'; let locale = 'fr'; const editor = mountIncontextEditor({ url: 'https://api.transglot.ai', publishableKey: 'taip_your_publishable_key', locale, enterOnMount: currentUser.canReviewTranslations, reviewer: () => currentUser.email, writableKeys: currentUser.reviewableKeys, getNonce: (keys) => fetch('/api/transglot-nonce', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ keys }), }) .then((response) => response.json()) .then((body) => body.nonce), onLocaleChange: (next) => { locale = next; render(); }, }); export const t = wrapT( (key: string) => messages[locale]?.[key] ?? key, editor.registry, () => editor.session.state().locale, ); ``` Use the wrapped `t` wherever your app renders text. Only strings that went through it can be opened with ALT+click. Better still, load this module only for reviewers, so visitors never download the editor. ### Without a bundler [#without-a-bundler] The package is published as ES modules, so a page can load it from a CDN that serves npm packages as ESM, for example jsDelivr: ```html title="index.html"

``` Pin the version in the URL so an update never reaches your page unannounced. ### `mountIncontextEditor` options [#mountincontexteditor-options] string | undefined)' }, getNonce: { description: 'Returns a single-use write nonce from your backend. Omit it for a read-only editor.', type: '(keys: string[]) => Promise' }, writableKeys: { description: 'The keys this reviewer may change, passed to getNonce.', type: 'string[]' }, theme: { description: 'The editor\'s colour scheme, resolved once at mount.', type: "'light' | 'dark' | 'auto'", default: "'auto'" }, consoleUrl: { description: 'Where a reviewer who cannot add a language is sent instead.', type: 'string', default: 'the hosted console' }, onLocaleChange: { description: 'Called after a locale switch. Re-render your app in the new locale here; the editor updates the marked text it can reach, but only you can re-render your app.', type: '(locale: string) => void | Promise' }, onRowsLanded: { description: 'Called with the rows a read returned, after the editor has updated the marked text it found.', type: '(rows: EditorRow[], locale: string) => void | Promise' }, onReady: { description: 'Called once with the session, before any editor UI is installed.', type: '(session: EditingSession) => void' }, onError: { description: 'Called when starting the session, the read behind an ALT+click, or loading the locale list fails. When omitted, the editor shows the message in its own UI while editing mode is on.', type: '(error: IncontextError) => void' }, captureElement: { description: 'Your own screenshot function, instead of the optional html2canvas.', type: '(element: HTMLElement) => Promise' }, document: { description: 'The document to mount into.', type: 'Document', default: 'the global document' }, fetchImpl: { description: 'The fetch implementation to use.', type: 'typeof fetch' }, }} /> ### The returned handle [#the-returned-handle] | Member | Description | | --- | --- | | `session` | The editing session (below). | | `registry` | The registry to pass to `wrapT`. | | `client` | The API client the editor uses. | | `shadowRoot()` | The shadow root the editor's bar, panel and highlight render into, isolated from your page's styles. | | `destroy()` | Removes the editor and its listeners and leaves editing mode. Strings keep their watermarks, so a later mount can resolve them again. Safe to call more than once. | The `session` lets you build your own controls: ```ts editor.session.enter(); // turn editing mode on editor.session.leave(); // turn it off editor.session.state(); // { active, locale, sourceLocale, locales, canWrite, canAddLocale, ... } const unsubscribe = editor.session.subscribe((state) => updateMyToolbar(state)); await editor.session.setLocale('de'); ``` ### `wrapT(t, registry, locale)` [#wraptt-registry-locale] `wrapT(t: HostT, registry: Registry, locale: () => string): WrappedT` returns a function with the same arguments as `t`. Each string it returns is registered as a key and locale pair and gets the invisible watermark. A non-string return value passes through unmarked. `locale` is called on every call, so pass a getter that reads the editor's current locale, as in the examples. `createRegistry()` creates a registry yourself, if you want one before mounting; pass it as `registry`. ## Your backend: the nonce endpoint [#your-backend-the-nonce-endpoint] The publishable key ships in your page, so on its own it opens a read-only session. To let a reviewer save, the browser needs a single-use nonce that only your server can obtain: 1. The editor calls your `getNonce(keys)` with the keys in `writableKeys`, when the session starts and each time it is renewed. 2. Your `getNonce` calls an endpoint on your own server, for example `POST /api/transglot-nonce`. 3. That endpoint checks the signed-in user may review, then calls `POST https://api.transglot.ai/v1/incontext/session-nonce` with a **project API token that has the `write` ability** (a secret that stays on your server), the publishable key, and the key names this user may change. 4. transglot answers `{ "nonce": "...", "expires_at": "..." }`. Your endpoint returns the nonce to the browser, and `getNonce` resolves with it. 5. The editor sends the nonce with the publishable key when it opens the session, and the session can then save exactly those keys. ```ts title="server/routes/transglot-nonce.ts" app.post('/api/transglot-nonce', requireReviewer, async (req, res) => { const keys = keysThisUserMayEdit(req.user, req.body.keys); const response = await fetch('https://api.transglot.ai/v1/incontext/session-nonce', { method: 'POST', headers: { Authorization: `Bearer ${process.env.TRANSGLOT_TOKEN}`, 'Content-Type': 'application/json', Accept: 'application/json', }, body: JSON.stringify({ publishable_key: 'taip_your_publishable_key', keys, }), }); if (!response.ok) { res.json({ nonce: null }); return; } const { nonce } = await response.json(); res.json({ nonce }); }); ``` When `getNonce` resolves with `null`, the session is read-only: Save is disabled with the reason shown, and no screenshots are captured. A nonce can be used once and expires about a minute after it is issued, which is why the editor asks for a new one on every session. The request fields, the `add_locale` grant and the error responses are in [The nonce request](https://transglot.ai/docs/developers/in-context/publishable-keys#the-nonce-request). ## What can be edited in place [#what-can-be-edited-in-place] A watermark is appended to the string your `t` returns, so it only works while that string renders as one text node. - **A translation with inline markup** (`Stamped with the carrier time`) renders as several nodes. Reviewers can still save it, but the page is not updated afterwards; the editor tells the reviewer so. Split such sentences into one key per element if you want every part to update live. - **Attribute text** (`placeholder`, `title`, `alt`, `aria-label`) has no text node, so it is never marked and cannot be opened with ALT+click. While editing mode is on, ALT+click belongs to the editor: a click that does not land on a marked string opens nothing and does not reach your page's own handlers. The editor's bar and panel are exempt, and leaving editing mode gives the gesture back. ## Errors [#errors] Failures are `IncontextError` instances with a stable `slug`, a `message`, an optional `hint` (what to do), and `retryAfterSeconds` when the server sent `Retry-After`. Common slugs: | `slug` | Meaning | | --- | --- | | `origin-not-allowed` | The publishable key is unknown or deleted, the page's origin is not in its allowed origins, or the write nonce was already used or has expired. The server gives the same answer for all of these on purpose. | | `plan-limit`, `plan-feature-required` | The account's plan does not include the in-context editor. | | `network-error` | transglot could not be reached. | | `rate-limited` | Still rate limited after retries. | | `invalid-url` | `url` is not an https URL (or http on localhost). Thrown when the client is created. | A save refused because someone else saved the same string first is an `IncontextConflictError`, a subclass with the slug `row-changed` and a `row` property holding the version now on the server. The editor handles it by showing **Their version** to the reviewer. With `mountIncontextEditor`, pass `onError` to route failures into your own UI. The React provider does not take `onError`; the editor shows failures in its own UI. ## Related [#related] - [Publishable keys](https://transglot.ai/docs/developers/in-context/publishable-keys) - [Connect the in-context editor](https://transglot.ai/docs/guides/quality/connect-the-in-context-editor) - [The screenshot library](https://transglot.ai/docs/guides/quality/the-screenshot-library) # Publishable keys (https://transglot.ai/docs/developers/in-context/publishable-keys/) A publishable key is the id the in-context editor sends from the browser. It starts with `taip_`, it is meant to be public, and it is safe to ship in client code: on its own it can only open a short-lived, read-only session, and only from the origins you list on the key. Saving needs a second step that only your backend can perform. Publishable keys are available on the Starter plan and above, and only project admins can create or delete them. ## Create a key [#create-a-key] You can create a key in two places. Both create the same kind of key. 1. Open the project's **Settings** → **Developer**. 2. In the **Publishable keys** card, select **New publishable key**. 3. Enter a **Name** that identifies the site, such as "Marketing site". 4. Under **Allowed origins**, type each origin and select **Add** (or press Enter). 5. Select **Create key**, then **Copy** in **Your new publishable key**. 1. Open the project's **Screenshots**. 2. In the **Connection** card, enter a **Key name**, such as "Production web app". 3. In **Allowed origins (one per line)**, list each origin on its own line. 4. Select **Create publishable key**. The **SDK snippet** card on the same page is pre-filled with the project's key. Unlike API tokens and CDN keys, a publishable key stays visible in the list, so you can copy it again at any time. Each key shows its allowed origins and when it was last used. ## Allowed origins [#allowed-origins] The editor only starts a session when the browser's `Origin` matches one of the key's allowed origins exactly: - An origin is `scheme://host[:port]`: `https://app.example.com`, `http://localhost:5173`. There are no wildcards, so list every subdomain you need. - The scheme and port must match; the host is compared without regard to case. A path is ignored. - Browsers leave the default port out of `Origin`, so write `https://app.example.com`, not `https://app.example.com:443`. - List your development and staging origins too, or use a separate key for them. The usual way to organise this is one key per app or environment, so you can delete one without affecting the others. ## Sessions [#sessions] When the editor mounts, it exchanges the publishable key for a session: 1. The browser sends the key to `POST https://api.transglot.ai/v1/incontext/session`. 2. transglot checks that the key exists, that the request's origin is on its list, and that the account's plan includes the in-context editor. 3. It returns a session token for the key's project. Without a write nonce (below), the session can read strings but not change them. A session lasts about 15 minutes. The SDK renews it a minute before it expires (calling `getNonce` again, if you set it) and keeps the token in memory only: never in `localStorage` or `sessionStorage`. Session requests are rate-limited per key and client address. An unknown or deleted key, an origin that is not on the key's list, and a nonce that was already used or has expired all return the same `403` with the problem type `origin-not-allowed`. That is deliberate: nobody can use the answer to find out which keys exist. A plan without the in-context editor returns `403` `plan-limit`. ## Grant write access [#grant-write-access] Because the publishable key is in your page source, anyone can copy it. So it never grants write access. To let a reviewer save, your backend vouches for them with a **nonce**: ### Create a project token with write access [#create-a-project-token-with-write-access] In **Settings** → **Developer** → **API tokens**, create a token with the `write` ability. It is a secret: keep it on your server, for example as `TRANSGLOT_TOKEN`. See [Deploy tokens](https://transglot.ai/docs/developers/deploy-tokens). ### Add a nonce endpoint to your backend [#add-a-nonce-endpoint-to-your-backend] Your endpoint checks that the signed-in user may review translations, then asks transglot for a nonce naming the keys they may change: ```ts title="server/routes/transglot-nonce.ts" app.post('/api/transglot-nonce', requireReviewer, async (req, res) => { const keys = keysThisUserMayEdit(req.user, req.body.keys); const response = await fetch('https://api.transglot.ai/v1/incontext/session-nonce', { method: 'POST', headers: { Authorization: `Bearer ${process.env.TRANSGLOT_TOKEN}`, 'Content-Type': 'application/json', Accept: 'application/json', }, body: JSON.stringify({ publishable_key: 'taip_your_publishable_key', keys, }), }); if (!response.ok) { res.json({ nonce: null }); return; } const { nonce } = await response.json(); res.json({ nonce }); }); ``` Decide on the server which keys the user may edit; do not pass the browser's list through unchecked. ### Return the nonce from `getNonce` [#return-the-nonce-from-getnonce] Point the editor's `getNonce` option at your endpoint, and pass the keys in `writableKeys`. See [Install the in-context editor](https://transglot.ai/docs/developers/in-context/install). When `getNonce` returns `null`, the session is read-only. ### The nonce request [#the-nonce-request] `POST https://api.transglot.ai/v1/incontext/session-nonce`, authenticated with a project token that has the `write` ability. The response is `{ "nonce": "...", "expires_at": "..." }`. A nonce: - can be used once, - expires about a minute after it is issued, - works only with the publishable key it was issued for. | Status | Problem type | Cause | | --- | --- | --- | | `403` | `nonce-not-issued` | The publishable key is unknown, deleted, or belongs to another project. | | `422` | `unknown-translation-key` | One of `keys` does not exist in the project. | | `403` | `plan-feature-required` | The account's plan does not include the in-context editor. | A session token from the editor cannot request a nonce. Only a project token from your own backend can. ## Delete a key [#delete-a-key] In the **Publishable keys** card, select **Delete** next to the key and type its name to confirm. Every editor embedded with that key stops working at once, and its open sessions end. If the editor is in production, create a replacement key, deploy it, then delete the old one. ## Related [#related] - [Install the in-context editor](https://transglot.ai/docs/developers/in-context/install) - [Connect the in-context editor](https://transglot.ai/docs/guides/quality/connect-the-in-context-editor) - [Project developer settings](https://transglot.ai/docs/developers/project-developer-settings) # Developer overview (https://transglot.ai/docs/developers/) transglot keeps the strings of your app in a project. Everything on this page moves strings between that project and your code: up from your repository, back down as translated files, or straight into your running app. The API lives at `https://api.transglot.ai/v1`. The app, where you manage projects and create credentials, is at `https://app.transglot.ai`. The CLI, the GitHub Action, the API, webhooks, the in-context editor, CDN delivery and the MCP server need the Starter plan or above; see [pricing](https://transglot.ai/pricing). ## Choose your integration [#choose-your-integration] | You want to | Use | | --- | --- | | Connect a repository from your laptop and run the first translation | [`npx transglot@latest init`](https://transglot.ai/docs/developers/cli) | | Translate on every merge in GitHub and commit the results | [The GitHub Action](https://transglot.ai/docs/developers/github-action) | | Do the same in GitLab CI, CircleCI or any other pipeline | [The CLI in CI](https://transglot.ai/docs/developers/cli/continuous-integration) | | Read or write translations, start runs or manage the glossary from your own code | [The REST API](https://transglot.ai/docs/developers/api) | | Start a build or notify a team when translations change | [Webhooks](https://transglot.ai/docs/developers/webhooks) | | Ship translation fixes without releasing your app | [The runtime SDKs](https://transglot.ai/docs/developers/sdks) and [the CDN](https://transglot.ai/docs/guides/delivery/the-cdn) | | Let reviewers fix wording on the real page | [The in-context editor](https://transglot.ai/docs/developers/in-context) | | Ask an AI assistant to fill gaps or check a key in every language | [The MCP server](https://transglot.ai/docs/mcp) | | See missing translations while you edit message files | [The language server](https://transglot.ai/docs/developers/tools/language-server) | | Check that your files are supported | [File formats](https://transglot.ai/docs/developers/formats) | If nobody on your team writes code, you can still upload and download files in the app. See [Do I need a developer to set transglot up?](https://transglot.ai/docs/first-steps/do-i-need-a-developer-to-set-transglot-up) ## Authentication [#authentication] Each integration authenticates with one of three kinds of credential. All of them belong to a project and are created under **Settings** → **Developer** in that project. | Credential | Used by | Keep secret? | | --- | --- | --- | | [Deploy token](https://transglot.ai/docs/developers/deploy-tokens) (`tgl_…`) | The CLI, the GitHub Action, the REST API, the MCP server and the language server | Yes. Shown once, at creation. | | [Publishable key](https://transglot.ai/docs/developers/in-context/publishable-keys) (`taip_…`) | The in-context editor SDK in your app | No. Safe in client JavaScript, limited to the origins you allow. | | CDN key (`taicdn_…`) | Fetching published bundles from the CDN | Yes. Read-only, shown once. | A deploy token carries abilities (`push`, `pull`, `manage`, `read`, `write`) that limit what it can do. Give each token only the abilities its job needs. # Project developer settings (https://transglot.ai/docs/developers/project-developer-settings/) Open the project, then **Settings** → **Developer**. The page holds four sections, each with its own list and create dialog. Admins see all four; the page is not shown to members who cannot manage the project's tokens. | Section | Holds | Plan | | --- | --- | --- | | [API tokens](#api-tokens) | Deploy tokens for the CLI, CI, the REST API and the MCP server | Starter and above | | [Publishable keys](#publishable-keys) | Public keys that load the in-context editor in your app | Starter and above | | [Webhooks](#webhooks) | Endpoints that receive signed events | Starter and above | | [Runtime delivery (CDN)](#runtime-delivery-cdn) | Published locale URLs and the read-only keys that fetch them | Starter and above | See [pricing](https://transglot.ai/pricing) for what each plan includes. ## How secrets are handled [#how-secrets-are-handled] Deploy tokens, CDN keys and webhook signing secrets are shown once, in the dialog that appears right after you create them. transglot stores only a hash, so the value can never be shown again. Copy it straight into the place that uses it. If you lose one, create a replacement, roll it out, then delete the old one. Publishable keys are the exception. They are public by design, restricted by the origins you allow rather than by secrecy, and they stay listed so you can copy them again. ## API tokens [#api-tokens] "Project-owned tokens for the CLI and CI. The token value is shown only once, at creation." Select **New token**, then fill in **Name**, **Abilities** and **Expiry** in the **Create API token** dialog. The list shows each token's abilities, when it was last used and when it expires, with **Revoke** on every row. Abilities, expiry, storage and rotation are covered in [Deploy tokens](https://transglot.ai/docs/developers/deploy-tokens). ## Publishable keys [#publishable-keys] "Public `taip_` ids for the in-context editor SDK. Safe to embed in client JS. Each is bound to an allow-list of origins and only mints short-lived, origin-checked session tokens." Select **New publishable key**. In the **Create publishable key** dialog, give it a **Name** and list its **Allowed origins**: "Exact `scheme://host[:port]` origins allowed to mint a session (no wildcards)." Type each origin, such as `https://app.example.com`, and select **Add**, then **Create key**. **Your new publishable key** shows the key to pass to ``. The list shows each key's allowed origins and when it was last used, with **Delete** on every row. See [Publishable keys](https://transglot.ai/docs/developers/in-context/publishable-keys) and [Install the in-context editor](https://transglot.ai/docs/developers/in-context/install). ## Webhooks [#webhooks] "POST a signed event to your endpoint when something changes." Select **New endpoint**. In the **Create webhook endpoint** dialog, enter the **Endpoint URL**, choose the events, and select **Create endpoint**. **Copy your signing secret** then shows the secret once: "This secret is shown only once. Store it in your consumer now to verify the X-Transglot-Signature header. It cannot be recovered later." The list shows each endpoint's events and when it last delivered. Its **Active** switch pauses or resumes deliveries; events that happen while an endpoint is paused are not sent later. **Deliveries** opens **Recent deliveries**, with each attempt's status, response code, attempts and next retry. **Delete** removes the endpoint. If the account's plan does not include webhooks, or the workspace is frozen, the section says deliveries are paused. Your endpoints and their settings are kept, and delivery resumes once the workspace is back. See [Create a webhook endpoint](https://transglot.ai/docs/developers/webhooks/create-a-webhook-endpoint), [Webhook events](https://transglot.ai/docs/developers/webhooks/events) and [Verify signatures](https://transglot.ai/docs/developers/webhooks/verify-signatures). ## Runtime delivery (CDN) [#runtime-delivery-cdn] "Serve published strings over an edge-cacheable URL. Fix a prod typo without shipping a build." - **Freshness** decides when a version is published: **Manual: publish explicitly**, or **Auto: publish on every push**. - **Delivery format** is the file shape a bundle is served in. **Project default** follows the project's own export format. - Each locale shows its published version, **Publish now**, its **Latest URL** and, once published, its **Immutable URL**. Immutable `/v` URLs cache for a year; the latest URL revalidates with an ETag. **CDN keys**: "Read-only `taicdn_` keys authorize a CDN fetch. Pass one in the `X-Transglot-Cdn-Key` header (or `?key=` for browser fetches). Shown once on creation." Type a **Name** and select **New CDN key**. Deleting a key makes every fetch that uses it fail with a 401 straight away, so roll out a replacement first. On a plan without runtime delivery, the section shows "Runtime delivery unlocks on the Starter plan." Published versions and their URLs stay visible. See [The CDN](https://transglot.ai/docs/guides/delivery/the-cdn) and [Connect your app to the CDN](https://transglot.ai/docs/guides/delivery/connect-your-app-to-the-cdn). # Angular (https://transglot.ai/docs/developers/sdks/angular/) `@transglot/angular` connects [`@transglot/runtime`](https://transglot.ai/docs/developers/sdks/javascript) to Angular 17 or later, with standalone APIs and signals. Templates and `computed()` values that translate update when the locale changes or a new publish lands for the active locale. The package is published in the Angular Package Format (partial Ivy), so your build links it like any Angular library. ## Install [#install] ```npm npm install @transglot/angular ``` `@angular/core` is a peer dependency. `@transglot/runtime` is installed with the package, and `createClient`, `RuntimeError` and the `RuntimeClient` and `ClientOptions` types are re-exported from it. ## Minimal setup [#minimal-setup] Register the provider when you bootstrap the app: ```ts title="src/main.ts" import { bootstrapApplication } from '@angular/platform-browser'; import { provideTransglot } from '@transglot/angular'; import { AppComponent } from './app/app.component'; bootstrapApplication(AppComponent, { providers: [ provideTransglot({ baseUrl: 'https://api.transglot.ai', project: 42, cdnKey: 'taicdn_your_key', locale: 'en', authIn: 'query', }), ], }); ``` Translate with the `translate` pipe in templates, or with the service in code: ```ts title="src/app/header.component.ts" import { Component, computed, inject } from '@angular/core'; import { TransglotService, TranslatePipe } from '@transglot/angular'; @Component({ selector: 'app-header', standalone: true, imports: [TranslatePipe], template: `

{{ 'home.title' | translate }}

{{ 'cart.items' | translate: { count: 3 } }}

{{ greeting() }}

`, }) export class HeaderComponent { readonly i18n = inject(TransglotService); readonly greeting = computed(() => this.i18n.t('home.greeting', { name: 'Ada' })); toggle(): void { void this.i18n.setLocale(this.i18n.locale() === 'en' ? 'fr' : 'en'); } } ``` The service starts loading the initial locale when it is created and does not wait for it. Until the bundle arrives, translations return keys; bindings update when it lands. Pass `authIn: 'query'` when the client runs in a browser. The CDN does not answer CORS preflight requests, which also limits revalidation from a browser. See [In a browser](https://transglot.ai/docs/developers/sdks/javascript#in-a-browser). ## API [#api] ### `provideTransglot(config)` [#providetransglotconfig] `provideTransglot(config: TransglotConfig): EnvironmentProviders` `config` is either the runtime [`ClientOptions`](https://transglot.ai/docs/developers/sdks/javascript#options) or `{ client }` with a client you created. It registers the client under the `TRANSGLOT_CLIENT` token and `TransglotService`, one of each for the environment injector you add it to. Add it to `bootstrapApplication` for an app-wide instance. ### `TransglotService` [#transglotservice] ', }, t: { description: 'Translate a key in the active locale. Reads the service signals, so a template or computed() that calls it updates on a locale switch or a new bundle. Returns the key when it is missing. It is an arrow function, so you can destructure it.', type: '(key: string, params?: Record) => string', }, setLocale: { description: 'Load a locale, then switch to it.', type: '(locale: string) => Promise', }, loadLocale: { description: 'Fetch a locale, or revalidate it, without switching.', type: '(locale: string) => Promise', }, preload: { description: 'Load several locales without switching.', type: '(locales: string[]) => Promise', }, }} /> ### `TranslatePipe` [#translatepipe] `{{ key | translate }}` or `{{ key | translate: params }}`. A standalone pipe named `translate`: add `TranslatePipe` to a component's `imports`. It is impure, so it re-evaluates on every change detection pass, and it reads the locale signal, so zoneless and `OnPush` components update on a locale switch too. ### `TRANSGLOT_CLIENT` [#transglot_client] The `InjectionToken` holding the runtime client. Inject it when you need a client method the service does not expose, such as `refresh()` or `onChange()`: ```ts import { inject } from '@angular/core'; import { TRANSGLOT_CLIENT } from '@transglot/angular'; const client = inject(TRANSGLOT_CLIENT); await client.refresh(); ``` ## Errors [#errors] The service does not catch a failed first load, and `setLocale` rejects with the runtime's `RuntimeError` (see [Errors](https://transglot.ai/docs/developers/sdks/javascript#errors)). To handle the first load yourself, create the client, load the locale, then bootstrap with `{ client }`: ```ts title="src/main.ts" import { bootstrapApplication } from '@angular/platform-browser'; import { RuntimeError, createClient, provideTransglot } from '@transglot/angular'; import { AppComponent } from './app/app.component'; const client = createClient({ baseUrl: 'https://api.transglot.ai', project: 42, cdnKey: 'taicdn_your_key', locale: 'en', authIn: 'query', }); client .loadLocale('en') .catch((error: unknown) => { if (error instanceof RuntimeError) console.error(error.slug, error.hint); }) .finally(() => bootstrapApplication(AppComponent, { providers: [provideTransglot({ client })], }), ); ``` ## Related [#related] - [JavaScript runtime](https://transglot.ai/docs/developers/sdks/javascript) - [Runtime SDKs overview](https://transglot.ai/docs/developers/sdks) # Astro (https://transglot.ai/docs/developers/sdks/astro/) `@transglot/astro` fetches a locale's published bundle on the server, stores it on `Astro.locals`, and lets both your `.astro` markup and your React islands read it. Islands render translated HTML on the server and hydrate without fetching the seeded locale again. It supports Astro 4 and 5. The island entry uses React 18 or 19. ## Install [#install] ```npm npm install @transglot/astro ``` `@transglot/runtime` and `@transglot/react-i18n` are installed with it. For React islands, add Astro's React renderer: ```npm npm install @astrojs/react react react-dom ``` The package has three entry points: | Import | Use it in | Exports | | --- | --- | --- | | `@transglot/astro` | `astro.config`, `.astro` frontmatter, middleware, endpoints. Has no React import. | `transglot`, `getLocaleBundle`, `createTranslationMiddleware`, `readSnapshot`, `createSeededClient`, `DEFAULT_LOCALS_KEY`, `VIRTUAL_CONFIG_ID`, and `createClient`, `RuntimeError`, `parseBundle`, `interpolate` from the runtime | | `@transglot/astro/island` | React component files | `TransglotIsland`, and `TransglotProvider`, `useTranslations`, `T` from [`@transglot/react-i18n`](https://transglot.ai/docs/developers/sdks/react) | | `@transglot/astro/middleware` | Registered for you by the integration | `onRequest` | ## Setup [#setup] Add the integration to your Astro config: ```js title="astro.config.mjs" import react from '@astrojs/react'; import { transglot } from '@transglot/astro'; import { defineConfig } from 'astro/config'; export default defineConfig({ integrations: [ react(), transglot({ baseUrl: 'https://api.transglot.ai', project: 42, cdnKey: process.env.TRANSGLOT_CDN_KEY, defaultLocale: 'en', }), ], }); ``` The integration registers a middleware that runs before your own. For each request it picks a locale (Astro's `currentLocale` when you use Astro's i18n routing, otherwise `defaultLocale`), fetches that locale's bundle, and stores the snapshot on `Astro.locals.transglot`. If no locale resolves, it does nothing. ### Translate in `.astro` files [#translate-in-astro-files] Read the snapshot and build a seeded client. Its `t` returns translations straight away: ```astro title="src/pages/index.astro" --- import { createSeededClient, readSnapshot } from '@transglot/astro'; const snapshot = readSnapshot(Astro.locals); const { t } = createSeededClient({ snapshots: snapshot!, dev: false }); ---

{t('home.title')}

{t('cart.items', { count: 3 })}

``` ### Translate in a React island [#translate-in-a-react-island] Put `TransglotIsland` and the components that call `useTranslations` in the same React component, and hydrate that component. Astro renders children passed to an island from an `.astro` file as separate static HTML, outside the island's React tree, so they would not see the provider. ```tsx title="src/components/Menu.tsx" import { useTranslations } from '@transglot/astro/island'; export function Menu() { const { t, locale, setLocale } = useTranslations(); return (
); } ``` ```tsx title="src/components/MenuIsland.tsx" import type { TranslateSnapshot } from '@transglot/astro'; import { TransglotIsland } from '@transglot/astro/island'; import { Menu } from './Menu'; export default function MenuIsland({ snapshot }: { snapshot: TranslateSnapshot }) { return ( ); } ``` ```astro title="src/pages/index.astro" --- import { readSnapshot } from '@transglot/astro'; import MenuIsland from '../components/MenuIsland'; const snapshot = readSnapshot(Astro.locals)!; --- ``` The snapshot holds the bundle and its headers, not the CDN key, so passing it to an island does not expose the key. ## Switching locale in an island [#switching-locale-in-an-island] `setLocale` switches between locales the island already holds: - **Seed several locales.** Pass `snapshots` (an array) instead of `snapshot`. Switching between them needs no request. - **Pass a `loader`.** With CDN coordinates in `loader`, the island fetches a locale that was not seeded from the browser. The key is then part of the island's props and reaches the browser. Set `authIn: 'query'`, because the CDN does not answer the CORS preflight a header would need (see [In a browser](https://transglot.ai/docs/developers/sdks/javascript#in-a-browser)). Without a `loader`, switching to a locale that was not seeded keeps the current locale and logs a dev warning. The island builds its client once per mount; to give it a new snapshot, remount it with a React `key`. ## Freshness [#freshness] The middleware fetches the bundle each time it handles a request, and the seeded client does not poll. On pages rendered on demand, a new publish shows up on the next request. On prerendered pages, Astro runs the middleware while it builds, so the HTML keeps the bundle from build time until you rebuild. Every fetch goes to the CDN, which rate-limits reads per key and client address. For busy on-demand pages, wire your own middleware with a `fetchImpl` that caches responses. ## API [#api] ### `transglot(options)` [#transglotoptions] The Astro integration. Throws `[transglot] transglot() requires { baseUrl, project, cdnKey }.` when one of those is missing. The integration also exposes its options to your build as the virtual module `virtual:@transglot/astro/config` (the `VIRTUAL_CONFIG_ID` constant), which the built-in middleware reads. ### `createTranslationMiddleware(options)` [#createtranslationmiddlewareoptions] Builds the same middleware, for `src/middleware.ts`. Use it with `middleware: false` when you need your own locale resolution or a custom `fetch`: ```ts title="src/middleware.ts" import { createTranslationMiddleware } from '@transglot/astro'; export const onRequest = createTranslationMiddleware({ baseUrl: 'https://api.transglot.ai', project: 42, cdnKey: import.meta.env.TRANSGLOT_CDN_KEY, defaultLocale: 'en', resolveLocale: (context) => context.url?.searchParams.get('lang') ?? undefined, }); ``` The locale is the first of: `resolveLocale(context)`, Astro's `context.currentLocale`, `defaultLocale`. string | undefined' }, version: { description: 'Pin one immutable version.', type: 'number' }, authIn: { description: 'Where the key is sent.', type: "'header' | 'query'", default: "'header'" }, localsKey: { description: 'The Astro.locals property to store the snapshot under.', type: 'string', default: "'transglot'" }, fetchImpl: { description: 'The fetch implementation to use.', type: 'typeof fetch', default: 'globalThis.fetch' }, dev: { description: 'Log console warnings.', type: 'boolean', default: 'false' }, }} /> ### `readSnapshot(locals, localsKey?)` [#readsnapshotlocals-localskey] `readSnapshot(locals: Record, localsKey?: string): TranslateSnapshot | undefined` returns the snapshot the middleware stored, or `undefined` when it did not run or resolved no locale. ### `getLocaleBundle(options)` [#getlocalebundleoptions] `getLocaleBundle(options: GetLocaleBundleOptions): Promise` fetches one locale yourself, without the middleware. Its options are `baseUrl`, `project`, `cdnKey`, `locale`, and optionally `version`, `authIn`, `fetchImpl` and `dev` (default `false`). ```astro --- import { getLocaleBundle } from '@transglot/astro'; const snapshot = await getLocaleBundle({ baseUrl: 'https://api.transglot.ai', project: 42, cdnKey: import.meta.env.TRANSGLOT_CDN_KEY, locale: Astro.currentLocale ?? 'en', }); --- ``` `TranslateSnapshot` has the same fields as in the Next.js package. See [TranslateSnapshot](https://transglot.ai/docs/developers/sdks/nextjs#translatesnapshot). ### `createSeededClient(options)` [#createseededclientoptions] Returns a `RuntimeClient` filled synchronously from snapshots, with the options `snapshots`, `locale`, `loadSnapshot` and `dev`. It behaves as described in [createSeededClient](https://transglot.ai/docs/developers/sdks/nextjs#createseededclientoptions). ### `TransglotIsland` [#transglotisland] Pass exactly one of `snapshot` or `snapshots`. " }, children: { description: 'The React components that translate.', type: 'ReactNode' }, }} /> Inside it, `useTranslations()` and `T` work as described on the [React](https://transglot.ai/docs/developers/sdks/react#api) page. ## Errors [#errors] `getLocaleBundle` and the middleware reject with the runtime's `RuntimeError` (see [Errors](https://transglot.ai/docs/developers/sdks/javascript#errors)). A failure in the middleware fails the request, so Astro renders its error page. To degrade instead, wire your own middleware that catches it: ```ts title="src/middleware.ts" import { RuntimeError, getLocaleBundle } from '@transglot/astro'; import { defineMiddleware } from 'astro:middleware'; export const onRequest = defineMiddleware(async (context, next) => { try { context.locals.transglot = await getLocaleBundle({ baseUrl: 'https://api.transglot.ai', project: 42, cdnKey: import.meta.env.TRANSGLOT_CDN_KEY, locale: context.currentLocale ?? 'en', }); } catch (error) { if (!(error instanceof RuntimeError)) throw error; console.error(error.slug, error.hint); } return next(); }); ``` With this middleware, `readSnapshot` can return `undefined`, so check it before seeding a client. ## Related [#related] - [React](https://transglot.ai/docs/developers/sdks/react) - [Next.js](https://transglot.ai/docs/developers/sdks/nextjs) - [JavaScript runtime](https://transglot.ai/docs/developers/sdks/javascript) # i18next backend (https://transglot.ai/docs/developers/sdks/i18next/) `@transglot/i18next-backend` is a backend plugin for [i18next](https://www.i18next.com) 23 or later. i18next keeps doing interpolation, plurals and lookups; the plugin changes where the resources come from. It uses [`@transglot/runtime`](https://transglot.ai/docs/developers/sdks/javascript) for the request, so it shares its authentication, ETag revalidation, rate-limit retries and error types. Use it instead of `@transglot/react-i18n`, not with it: this package feeds i18next (and react-i18next on top of it), while [`@transglot/react-i18n`](https://transglot.ai/docs/developers/sdks/react) is transglot's own React adapter. ## Install [#install] ```npm npm install @transglot/i18next-backend ``` `i18next` is a peer dependency. `@transglot/runtime` is installed with the plugin. ## Minimal setup [#minimal-setup] ```ts title="src/i18n.ts" import { createTransglotBackend } from '@transglot/i18next-backend'; import i18next from 'i18next'; await i18next .use( createTransglotBackend({ baseUrl: 'https://api.transglot.ai', project: 42, cdnKey: 'taicdn_your_key', authIn: 'query', }), ) .init({ lng: 'en', fallbackLng: 'en', ns: ['translation'], defaultNS: 'translation', }); i18next.t('home.title'); ``` The plugin is also the package's default export, so you can register the class and pass its options through i18next's `backend` setting: ```ts title="src/i18n.ts" import TransglotBackend from '@transglot/i18next-backend'; import i18next from 'i18next'; await i18next.use(TransglotBackend).init({ lng: 'en', ns: ['translation'], backend: { baseUrl: 'https://api.transglot.ai', project: 42, cdnKey: 'taicdn_your_key', authIn: 'query', }, }); ``` When you pass options both ways, they are merged and the `backend` values win. Pass `authIn: 'query'` when i18next runs in a browser. The plugin fetches each locale once and revalidates it on later reads, and the CDN does not answer the CORS preflight that revalidation needs, so in a browser a later read of a locale already loaded (for example a namespace loaded on demand) fails. See [In a browser](https://transglot.ai/docs/developers/sdks/javascript#in-a-browser). ## How bundles map to i18next [#how-bundles-map-to-i18next] The CDN serves one bundle per locale. i18next asks for one language and one namespace at a time, and `namespaceMode` decides what each request gets: - **`single`** (default): every namespace gets the whole bundle. Use it when your app has one namespace, such as i18next's default `translation`. - **`perNamespace`**: the bundle's top-level keys are the namespaces, and a request for `common` gets the `common` object. Use it when you keep several namespaces in one project and call `t('common:save')`. A namespace with no matching top-level object gets an empty resource, so its keys fall back the way i18next normally handles missing keys. ```ts createTransglotBackend({ baseUrl: 'https://api.transglot.ai', project: 42, cdnKey: 'taicdn_your_key', namespaceMode: 'perNamespace', }); ``` The values reach i18next unchanged, with their nesting kept. The plugin does not flatten keys or rewrite placeholders, and it removes top-level `@`-prefixed members (ARB metadata). So: - **Interpolation** is i18next's: `{{name}}` placeholders in your strings work as they did in your files. - **Plurals** are i18next's suffix keys (`items_one`, `items_other`), which transglot stores and publishes as ordinary keys. ICU plural messages such as `{count, plural, one {...} other {...}}` are not interpreted by i18next and render as written; use [`@transglot/react-intl`](https://transglot.ai/docs/developers/sdks/react-intl) for ICU. - **Delivery format**: use `json_nested`, which matches i18next's default `keySeparator`, or `json_flat` with `keySeparator: false` in your i18next options. Set it in **Delivery format** on the project's **Runtime delivery (CDN)** card. ## Options [#options] ## API [#api] | Export | Description | | --- | --- | | `createTransglotBackend(options)` | Returns a configured `TransglotI18nextBackend` to pass to `i18next.use()`. | | `TransglotI18nextBackend` | The plugin class (also the default export). `new TransglotI18nextBackend(options?)`; options can come from i18next's `backend` setting instead. | | `bundleToResources(rawBody, namespace, mode?)` | Turns a bundle body into the resources for one namespace, as the plugin does. Throws `RuntimeError` (`unsupported-format`) for a body that is not a JSON object. | | `RuntimeError` | The error type, re-exported from the runtime. | Types: `TransglotBackendOptions`, `NamespaceMode`, `I18nextResources`. ## Errors [#errors] The plugin never throws from a read. A failed load is passed to i18next's read callback as a `RuntimeError`, so i18next's own retry and logging apply. The slugs are those of the [JavaScript runtime](https://transglot.ai/docs/developers/sdks/javascript#errors), plus: | `slug` | When | | --- | --- | | `backend-misconfigured` | `baseUrl`, `project` or `cdnKey` is missing after merging both option sources. Thrown when i18next initializes the plugin. | | `backend-uninitialized` | i18next asked for resources before it initialized the plugin. | | `unsupported-format` | The bundle is not a JSON object. Set a JSON-family delivery format. | ## Related [#related] - [JavaScript runtime](https://transglot.ai/docs/developers/sdks/javascript) - [react-intl](https://transglot.ai/docs/developers/sdks/react-intl) - [Runtime SDKs overview](https://transglot.ai/docs/developers/sdks) # Runtime SDKs (https://transglot.ai/docs/developers/sdks/) The runtime SDKs load your published translations over HTTP while your app runs, so a fixed typo or a new language reaches users without a new build. Every package reads the same CDN endpoint and builds on one client, [`@transglot/runtime`](https://transglot.ai/docs/developers/sdks/javascript). ## How runtime delivery works [#how-runtime-delivery-works] ### You publish a locale [#you-publish-a-locale] Publishing freezes a locale's current translations into a numbered version. You publish from the project's **Deploy** screen, from **Publish now** on the **Runtime delivery (CDN)** card, or from your pipeline. With **Freshness** set to **Auto: publish on every push**, every push publishes. See [Publish a locale](https://transglot.ai/docs/guides/delivery/publish-a-locale). A published bundle holds only the keys that have a translation in that locale, in the project's delivery format. ### The CDN serves it [#the-cdn-serves-it] Each published locale has two addresses on `https://api.transglot.ai`: | Address | Serves | Cache headers | | --- | --- | --- | | `/v1/cdn/{project}/{locale}` | The latest published version | `public`, a short `max-age`, `must-revalidate`, and an `ETag` | | `/v1/cdn/{project}/{locale}/v{n}` | Version `n`, which never changes | `public`, one year, `immutable` | The latest address is the one to use. Its short lifetime means a new publish reaches users within about a minute, and revalidating it is cheap: send the `ETag` back in `If-None-Match` and an unchanged bundle answers `304 Not Modified` with no body. A version address suits pinning during a rollout. transglot keeps a limited number of recent versions per locale, so an old version address eventually stops resolving. ### Your app reads it with a CDN key [#your-app-reads-it-with-a-cdn-key] Every read needs the project's CDN key, in the `X-Transglot-Cdn-Key` header or the `key` query parameter. A CDN key is read-only, belongs to one project, and reaches only that project's published bundles, never unpublished translations or another project's content. ## Get a CDN key [#get-a-cdn-key] Runtime delivery is available on the Starter plan and above. 1. Open the project's **Settings** → **Developer**. 2. In the **Runtime delivery (CDN)** card, under **CDN keys**, enter a **Name** (for example, the app or edge that will use it). 3. Select **New CDN key**, then **Copy**. The key starts with `taicdn_`. transglot stores only a hash, so it is shown once; if you lose it, create another. To revoke a key, select **Delete** next to it. Anything still using it starts receiving `401` responses immediately, including caches you do not control, so roll out its replacement first. The same card shows each locale's **Latest URL**. The number after `/v1/cdn/` is the project id the SDKs ask for. ## Choose a package [#choose-a-package] | Package | Use it for | Server rendering | | --- | --- | --- | | [`@transglot/runtime`](https://transglot.ai/docs/developers/sdks/javascript) | Any JavaScript runtime: browsers, Node.js 20+, React Native, workers | You call it | | [`@transglot/react-i18n`](https://transglot.ai/docs/developers/sdks/react) | React 18 and 19 | No, loads after mount | | [`@transglot/next`](https://transglot.ai/docs/developers/sdks/nextjs) | Next.js 14 and 15, App Router and Pages Router | Yes | | [`@transglot/vue`](https://transglot.ai/docs/developers/sdks/vue) | Vue 3.3 and later | No | | [`@transglot/svelte`](https://transglot.ai/docs/developers/sdks/svelte) | Svelte 5 | No | | [`@transglot/sveltekit`](https://transglot.ai/docs/developers/sdks/sveltekit) | SvelteKit 2 | Yes | | [`@transglot/angular`](https://transglot.ai/docs/developers/sdks/angular) | Angular 17 and later | No | | [`@transglot/solid`](https://transglot.ai/docs/developers/sdks/solid) | SolidJS 1.6 and later | No | | [`@transglot/astro`](https://transglot.ai/docs/developers/sdks/astro) | Astro 4 and 5, with React islands | Yes | | [`@transglot/i18next-backend`](https://transglot.ai/docs/developers/sdks/i18next) | Apps already on i18next or react-i18next | Through i18next | | [`@transglot/react-intl`](https://transglot.ai/docs/developers/sdks/react-intl) | Apps already on react-intl (FormatJS), and full ICU MessageFormat | You call it | | [`@transglot/pseudo`](https://transglot.ai/docs/developers/sdks/pseudo-localization) | Generating a pseudo-locale for testing; does not read the CDN | | `@transglot/react` is not a runtime SDK: it adds the in-context editor for reviewers. See [the in-context editor](https://transglot.ai/docs/developers/in-context). ## Delivery formats [#delivery-formats] The SDKs read the JSON-family delivery formats: `json_flat`, `json_nested`, `laravel_json` and `flutter_arb`. Choose one in **Delivery format** on the **Runtime delivery (CDN)** card; **Project default** follows the format you last pushed. Plural keys are published only in `flutter_arb`, as ICU plural messages; the other three leave them out. The [JavaScript runtime](https://transglot.ai/docs/developers/sdks/javascript#delivery-formats) page has the details. ## Things that apply to every package [#things-that-apply-to-every-package] - **Missing keys.** Looking up a key that is not in the bundle returns the key, or for the interop packages whatever i18next or react-intl do. No package falls back to another locale on its own. - **Browsers.** The CDN sends `Access-Control-Allow-Origin: *` on bundles but does not answer CORS preflight requests. In a browser, pass `authIn: 'query'`, and read [In a browser](https://transglot.ai/docs/developers/sdks/javascript#in-a-browser) for what that means for revalidation and errors. Server-side loading is not affected. - **Keys in client code.** A CDN key in a browser bundle is visible to anyone, and it can read only what you have published for that project. The server-rendering packages keep the key on the server unless you opt into browser-side loading. - **Rate limits.** CDN reads are rate-limited per key and client address. On `429`, the SDKs wait for `Retry-After` and retry twice before failing with `rate-limited`. A server that renders every request should cache bundles rather than fetch one per request. - **Errors.** Every package reports failures as `RuntimeError` with a stable `slug`. See [Errors](https://transglot.ai/docs/developers/sdks/javascript#errors). ## Without an SDK [#without-an-sdk] The CDN is plain HTTP, so any language can read it: ```bash curl -i \ -H "X-Transglot-Cdn-Key: taicdn_your_key" \ https://api.transglot.ai/v1/cdn/42/fr ``` The body is the bundle in the project's delivery format, with a matching `Content-Type`. The response also carries `ETag`, `X-Cdn-Version` (the published version number) and `X-Cdn-Format` (the format identifier). Add `?format=` to assert a format: if the version was published in a different one, the response is `404` with the problem type `not-published`, and an unknown identifier is `422` `invalid-format`. Errors are `application/problem+json` documents whose `type` is `/problems/`. ## Related [#related] - [The CDN](https://transglot.ai/docs/guides/delivery/the-cdn) - [Connect your app to the CDN](https://transglot.ai/docs/guides/delivery/connect-your-app-to-the-cdn) - [Why are my translations not reaching my app?](https://transglot.ai/docs/guides/delivery/why-are-my-translations-not-reaching-my-app) - [File formats](https://transglot.ai/docs/developers/formats) # JavaScript runtime (https://transglot.ai/docs/developers/sdks/javascript/) `@transglot/runtime` is the client every other runtime package builds on. It fetches a locale's published bundle, keeps it in memory, revalidates it with the bundle's ETag, interpolates parameters and picks plural forms with `Intl.PluralRules`. It has no dependencies and runs in Node.js 20 or later, in browsers, and in React Native. ## Install [#install] ```npm npm install @transglot/runtime ``` The package is ESM only. ## Before you start [#before-you-start] You need three things from the project's **Settings** → **Developer** page, in the **Runtime delivery (CDN)** card: - A **CDN key**. Under **CDN keys**, enter a **Name** and select **New CDN key**. The key starts with `taicdn_` and is shown once. - The **project id**: the number in the **Latest URL** shown for each locale (`https://api.transglot.ai/v1/cdn//`). - At least one **published** locale. A locale shows **not published** until you publish it. See [Publish a locale](https://transglot.ai/docs/guides/delivery/publish-a-locale). Runtime delivery is available on the Starter plan and above. ## Minimal setup [#minimal-setup] ```ts title="i18n.ts" import { createClient } from '@transglot/runtime'; export const i18n = createClient({ baseUrl: 'https://api.transglot.ai', project: 42, cdnKey: 'taicdn_your_key', locale: 'en', authIn: 'query', }); await i18n.loadLocale('en'); i18n.t('home.title'); // "Welcome" i18n.t('home.greeting', { name: 'Ada' }); // "Hello, Ada" i18n.t('cart.items', { count: 3 }); // "3 items" ``` Call `loadLocale` (or `setLocale`) before you translate. Until a locale is loaded, `t` returns the key you passed. ## In a browser [#in-a-browser] The CDN endpoint sends `Access-Control-Allow-Origin: *` on its bundle responses, but it does not answer CORS preflight requests. A cross-origin browser request that carries a custom header needs a preflight, so in a browser: - Set `authIn: 'query'`. The default, `'header'`, sends `X-Transglot-Cdn-Key`, which the browser will not send cross-origin. - Revalidating a locale the client already holds sends `If-None-Match`, which also needs a preflight. In a browser, `refresh()`, `refreshIntervalMs`, `onForeground` and `setLocale` back to a locale that is already loaded fail with `network-error` (the background triggers log it as a dev warning). To revalidate from a browser, serve `/v1/cdn/*` from your own origin through a reverse proxy to `https://api.transglot.ai` and set `baseUrl` to your origin, so the requests are same-origin. - Error responses carry no CORS header, so the browser hides them and every failure reaches you as `network-error`. None of this applies on a server, in React Native, or in the server-side helpers of the Next.js, SvelteKit and Astro packages. ## How delivery works [#how-delivery-works] The client reads two kinds of address on the CDN: | Address | What it serves | Caching | | --- | --- | --- | | `GET /v1/cdn/{project}/{locale}` | The latest published version of the locale | `public`, short `max-age`, `must-revalidate`, with an `ETag` | | `GET /v1/cdn/{project}/{locale}/v{n}` | One published version, which never changes | `public`, one year, `immutable` | The key travels in the `X-Transglot-Cdn-Key` header or the `key` query parameter. Every successful response carries: - `ETag`: a hash of the bundle body. When the client already holds a locale, it sends that value back in `If-None-Match`, and the CDN answers `304 Not Modified` with no body if nothing changed. - `X-Cdn-Version`: the published version number. - `X-Cdn-Format`: the delivery format the bundle was published in. The client uses it to parse the body. For the full picture of versions, freshness and what a CDN key can reach, see [The CDN](https://transglot.ai/docs/guides/delivery/the-cdn). ## Options [#options] `createClient(options: ClientOptions): RuntimeClient` Promise', default: 'setTimeout-based', }, dev: { description: 'Log console warnings for missing keys and unloaded locales, once per key.', type: 'boolean', default: "true unless process.env.NODE_ENV is 'production'", }, storage: { description: 'A persistent store for each locale\'s last good bundle, used for offline cold starts. See Persistent cache below.', type: 'StorageAdapter', }, refreshIntervalMs: { description: 'Revalidate the active locale in the background every N milliseconds. 0 or omitted turns the timer off.', type: 'number', }, onForeground: { description: 'Called once with a revalidate callback. Wire it to your "app became visible" event and return an unsubscribe function.', type: '(revalidate: () => void) => (() => void) | void', }, scheduler: { description: 'The interval functions the background refresh uses.', type: '{ setInterval(handler, ms): unknown; clearInterval(handle): void }', default: 'the global timers', }, }} /> ## Client methods [#client-methods] ) => string', }, 'loadLocale(locale)': { description: 'Fetch a locale into the cache, or revalidate it with its ETag if it is already there. Concurrent calls for the same locale share one request.', type: '(locale: string) => Promise', }, 'setLocale(locale)': { description: 'Load a locale, then make it the active one and notify onChange listeners.', type: '(locale: string) => Promise', }, 'preload(locales)': { description: 'Load several locales without changing the active one.', type: '(locales: string[]) => Promise', }, 'hydrate(locale?)': { description: 'Fill the cache for a locale (default: the active one) from storage, with no network call. Resolves true when a bundle is available, false otherwise or when no storage is set.', type: '(locale?: string) => Promise', }, 'refresh()': { description: 'Revalidate the active locale. The same as loadLocale(getLocale()).', type: '() => Promise', }, 'getLocale()': { description: 'The active locale.', type: '() => string', }, 'hasLocale(locale)': { description: 'Whether a locale is in the in-memory cache.', type: '(locale: string) => boolean', }, 'onChange(listener)': { description: 'Listen for a locale switch, or a new bundle landing for the active locale. Returns an unsubscribe function.', type: '(listener: (locale: string) => void) => () => void', }, 'stop()': { description: 'Stop the refreshIntervalMs timer and detach the onForeground hook. Safe to call more than once.', type: '() => void', }, }} /> ## Interpolation [#interpolation] Placeholders use single braces: `{name}`. A name can contain letters, digits, `_`, `.` and `-`, and whitespace inside the braces is ignored, so `{ user.name }` works. - A placeholder with no matching parameter, or one whose value is `null` or `undefined`, stays in the output as written, so a missing value is visible. - Substitution is a single pass. A value that itself contains `{x}` is inserted as plain text. ```ts i18n.t('greeting', { name: 'Ada' }); // "Hello, Ada" i18n.t('greeting'); // "Hello, {name}" ``` ## Plurals [#plurals] When an entry has plural forms, `t` reads `params.count`. If it is a number, the client asks `Intl.PluralRules` for the active locale which category applies (`one`, `few`, `many`, `other` and so on) and returns that form. If there is no form for the category, it falls back to `other`, then to the first form. If `count` is missing or not a number, it uses `other`. ```ts i18n.t('cart.items', { count: 1 }); // "1 item" i18n.t('cart.items', { count: 5 }); // "5 items" ``` Plural forms only reach the client in the `flutter_arb` delivery format, which carries them as ICU plural messages (`{count, plural, one {...} other {...}}`). The flat and nested JSON formats cannot represent plurals, so plural keys are left out of bundles published in those formats. See [Delivery formats](#delivery-formats). The client's plural support is narrower than full ICU: - The count always comes from the parameter named `count`, whatever variable the message names. - Only the CLDR categories (`zero`, `one`, `two`, `few`, `many`, `other`) are selected. Exact-value selectors such as `=0` are never chosen. - `#` inside a plural form is not replaced with the number. Write `{count}` instead. - In the JSON formats, a value that happens to be an ICU plural string is treated as plain text: `{count}` is interpolated and the rest stays as written. The client selects plural forms and interpolates placeholders. It does not implement the rest of ICU MessageFormat: a string with a `select`, `number` or `date` argument needs a formatter such as [`@transglot/react-intl`](https://transglot.ai/docs/developers/sdks/react-intl). ## Missing keys and locales [#missing-keys-and-locales] `t` never throws. For a key that is not in the active locale's bundle, it returns the key. With `dev` on, it logs one warning per key, prefixed `[transglot]`. A published bundle holds only the keys that have a translation in that locale. The client does not fall back to another locale. If you want the source text for an untranslated key, load the source locale too and choose between the two in your own code. ## Delivery formats [#delivery-formats] A bundle is served in the project's delivery format, set in **Delivery format** on the **Runtime delivery (CDN)** card. The client parses the JSON-family formats: | Format | Shape | Plurals | | --- | --- | --- | | `json_flat` | `{ "home.title": "Welcome" }` | Left out of the bundle | | `json_nested` | `{ "home": { "title": "Welcome" } }` | Left out of the bundle | | `laravel_json` | A flat object, like `json_flat` | Left out of the bundle | | `flutter_arb` | ARB, with `@`-prefixed metadata members | Yes, as ICU plural messages | Every format resolves to the same dotted key space, so `t('home.title')` reads the same key whether the bundle is flat or nested. Any other delivery format throws a `RuntimeError` with the slug `unsupported-format`. **Project default** follows the format of the files you last pushed, and falls back to `json_flat`. If your project pushes a non-JSON format (Android XML, for example), choose a JSON-family format in **Delivery format** and select **Save settings**. The setting applies to versions you publish after the change. ## Persistent cache [#persistent-cache] By default the cache lives in memory, so a fresh process starts empty. Pass a `storage` adapter and the client saves each locale's last good bundle, keyed per project and locale. On the next start, `hydrate()` serves it without a network call, and the next `loadLocale` revalidates it with the saved ETag. ```ts title="i18n.ts" import { createClient } from '@transglot/runtime'; export const i18n = createClient({ baseUrl: 'https://api.transglot.ai', project: 42, cdnKey: 'taicdn_your_key', locale: 'en', storage: { get: (key) => localStorage.getItem(key), set: (key, value) => localStorage.setItem(key, value), }, }); if (await i18n.hydrate()) { render(); // strings from the last session, available offline } await i18n.loadLocale('en'); ``` `StorageAdapter` has two methods, and either may return a promise: ```ts interface StorageAdapter { get(key: string): string | null | Promise; set(key: string, value: string): void | Promise; } ``` Persistence never breaks translation: a read or write that throws, or a stored value that does not parse, is ignored. ## Background refresh [#background-refresh] `refreshIntervalMs` revalidates the active locale on a timer, and `onForeground` lets you revalidate when the app comes back into view. Both run in the background and never reject; a failure is logged as a dev warning. When a new publish lands, `onChange` listeners are called, the same as after `setLocale`. ```ts title="i18n.ts" import { createClient } from '@transglot/runtime'; export const i18n = createClient({ baseUrl: 'https://api.transglot.ai', project: 42, cdnKey: 'taicdn_your_key', locale: 'en', refreshIntervalMs: 5 * 60_000, onForeground: (revalidate) => { const onVisible = () => { if (!document.hidden) revalidate(); }; document.addEventListener('visibilitychange', onVisible); return () => document.removeEventListener('visibilitychange', onVisible); }, }); i18n.onChange(() => render()); // On teardown: i18n.stop(); ``` In a browser, read [In a browser](#in-a-browser) before relying on background refresh. ## React Native [#react-native] React Native has no `localStorage` and no CORS, so the header default works. `AsyncStorage` already matches the adapter shape, and `AppState` gives you the foreground signal: ```ts title="i18n.ts" import AsyncStorage from '@react-native-async-storage/async-storage'; import { createClient } from '@transglot/runtime'; import { AppState } from 'react-native'; export const i18n = createClient({ baseUrl: 'https://api.transglot.ai', project: 42, cdnKey: 'taicdn_your_key', locale: 'en', storage: { get: AsyncStorage.getItem, set: AsyncStorage.setItem }, refreshIntervalMs: 15 * 60_000, onForeground: (revalidate) => { const subscription = AppState.addEventListener('change', (state) => { if (state === 'active') revalidate(); }); return () => subscription.remove(); }, }); await i18n.hydrate(); i18n.loadLocale('en').catch(() => { // Offline: the hydrated bundle keeps serving. }); ``` ## Errors [#errors] Network and HTTP failures reject with a `RuntimeError`: ```ts class RuntimeError extends Error { readonly slug: string; // stable identifier to branch on readonly hint?: string; // what to do about it readonly status?: number; // HTTP status, when the failure came from a response } ``` For an error response from the CDN, `slug` is the problem type from the response body and `message` is its `detail`. | `slug` | Status | Meaning | | --- | --- | --- | | `cdn-key-required` | 401 | No key was sent. | | `cdn-key-invalid` | 401 | The key is unknown, inactive or deleted. | | `not-found` | 404 | The project id does not match the key's project, or the locale is not configured on the project. | | `not-published` | 404 | The locale has never been published, or the pinned version no longer exists. | | `plan-feature-required` | 403 | The account's plan does not include runtime delivery. | | `workspace-closed` | 403 | The workspace is closed, so its bundles are not served. | | `rate-limited` | 429 | Still rate limited after the client's retries. | | `network-error` | | The request did not complete: DNS, connection, or a browser CORS block. | | `server-error` | any | The response was an error without a problem body. Check `baseUrl`. | | `unsupported-format` | | The bundle is not one of the JSON-family delivery formats, or is not valid JSON. | | `invalid-plural` | | A plural message in a `flutter_arb` bundle has unbalanced braces. | On a `429`, the client waits for the `Retry-After` interval (at most 60 seconds) and retries, up to two times, before it rejects. ```ts import { RuntimeError } from '@transglot/runtime'; try { await i18n.setLocale('fr'); } catch (error) { if (error instanceof RuntimeError && error.slug === 'not-published') { // keep the current locale and tell the user } else { throw error; } } ``` ## Lower-level exports [#lower-level-exports] The package also exports the pieces the client is made of, for adapters and tests: | Export | Signature | Purpose | | --- | --- | --- | | `parseBundle` | `(body: string, format?: string) => ParsedBundle` | Parse a bundle body into a flat map of entries. Without `format`, the shape is detected. Throws `RuntimeError` (`unsupported-format`). | | `interpolate` | `(template: string, params?: Record) => string` | The placeholder substitution `t` uses. | | `scanIcuPlural` | `(message: string) => IcuPlural \| null` | Read an ICU plural message into its variable and forms. Returns `null` for a plain string. | | `decodeIcu` | `(raw: string) => string` | Decode ICU apostrophe quoting in a message body. | Types: `RuntimeClient`, `ClientOptions`, `ChangeListener`, `StorageAdapter`, `Scheduler`, `Entry`, `ParsedBundle`, `IcuPlural`. ## Related [#related] - [Runtime SDKs overview](https://transglot.ai/docs/developers/sdks) - [Connect your app to the CDN](https://transglot.ai/docs/guides/delivery/connect-your-app-to-the-cdn) - [Why are my translations not reaching my app?](https://transglot.ai/docs/guides/delivery/why-are-my-translations-not-reaching-my-app) # Next.js (https://transglot.ai/docs/developers/sdks/nextjs/) `@transglot/next` fetches a locale's published bundle on the server, hands your page a serializable snapshot, and seeds a client provider from it. Server-rendered HTML contains translations, and the browser does not fetch the seeded locale again. It works with the App Router and the Pages Router, on Next.js 14 or 15 with React 18 or 19. ## Install [#install] ```npm npm install @transglot/next ``` `@transglot/runtime` and `@transglot/react-i18n` are installed with it. The package has two entry points: | Import | Runs on | Exports | | --- | --- | --- | | `@transglot/next` | Server and client. Has no React import, so it is safe in Server Components, route handlers and `getServerSideProps`. | `loadTranslations`, `createSeededClient`, and `createClient`, `RuntimeError`, `parseBundle`, `interpolate` from the runtime | | `@transglot/next/client` | Client. Marked `'use client'`. | `NextTranslateProvider`, and `TransglotProvider`, `useTranslations`, `T` from [`@transglot/react-i18n`](https://transglot.ai/docs/developers/sdks/react) | Keep the CDN key in a server environment variable, for example `TRANSGLOT_CDN_KEY`. The snapshot does not contain it. ## App Router [#app-router] Load the snapshot in a Server Component and render the provider around your tree: ```tsx title="app/[locale]/layout.tsx" import { loadTranslations } from '@transglot/next'; import { NextTranslateProvider } from '@transglot/next/client'; import type { ReactNode } from 'react'; export default async function LocaleLayout({ children, params, }: { children: ReactNode; params: Promise<{ locale: string }>; }) { const { locale } = await params; const snapshot = await loadTranslations({ baseUrl: 'https://api.transglot.ai', project: 42, cdnKey: process.env.TRANSGLOT_CDN_KEY!, locale, }); return ( {children} ); } ``` In Next.js 14, `params` is a plain object: type it as `{ locale: string }` and drop the `await`. Translate in Client Components with `useTranslations` or `T`: ```tsx title="app/[locale]/header.tsx" 'use client'; import { T, useTranslations } from '@transglot/next/client'; export function Header() { const { t } = useTranslations(); return (

{t('home.title')}

); } ``` The provider builds its client once per mount. The `key={locale}` remounts it when the route moves to another locale, so it picks up the new snapshot. ### In a Server Component [#in-a-server-component] Hooks do not run in Server Components. Build a seeded client from the snapshot and call its `t` directly: ```tsx title="app/[locale]/page.tsx" import { createSeededClient, loadTranslations } from '@transglot/next'; export default async function Page({ params }: { params: Promise<{ locale: string }> }) { const { locale } = await params; const snapshot = await loadTranslations({ baseUrl: 'https://api.transglot.ai', project: 42, cdnKey: process.env.TRANSGLOT_CDN_KEY!, locale, }); const { t } = createSeededClient({ snapshots: snapshot, dev: false }); return

{t('home.title')}

; } ``` ## Pages Router [#pages-router] ```tsx title="pages/index.tsx" import { loadTranslations, type TranslateSnapshot } from '@transglot/next'; import { NextTranslateProvider, useTranslations } from '@transglot/next/client'; import type { GetServerSideProps } from 'next'; type Props = { snapshot: TranslateSnapshot }; export const getServerSideProps: GetServerSideProps = async ({ locale }) => ({ props: { snapshot: await loadTranslations({ baseUrl: 'https://api.transglot.ai', project: 42, cdnKey: process.env.TRANSGLOT_CDN_KEY!, locale: locale ?? 'en', }), }, }); function Title() { const { t } = useTranslations(); return

{t('home.title')}

; } export default function Home({ snapshot }: Props) { return ( </NextTranslateProvider> ); } ``` ## Switching locale in the browser [#switching-locale-in-the-browser] `setLocale` from `useTranslations` switches between locales the provider already holds. You have two ways to give it more than one: - **Seed several locales.** Load a snapshot for each and pass them as `snapshots`. Switching between them needs no network request. ```tsx const snapshots = await Promise.all( ['en', 'fr', 'de'].map((locale) => loadTranslations({ baseUrl: 'https://api.transglot.ai', project: 42, cdnKey: process.env.TRANSGLOT_CDN_KEY!, locale, }), ), ); <NextTranslateProvider snapshots={snapshots} locale="en"> {children} </NextTranslateProvider>; ``` - **Pass a `loader`.** With CDN coordinates in `loader`, switching to a locale that was not seeded fetches it from the browser. The loader's CDN key is a prop, so it is sent to the browser. A CDN key is read-only and reaches only published bundles, but if you do not want it in the page, seed every locale instead. Set `authIn: 'query'`, because the CDN does not answer the CORS preflight a header would need (see [In a browser](https://transglot.ai/docs/developers/sdks/javascript#in-a-browser)). ```tsx <NextTranslateProvider snapshot={snapshot} loader={{ baseUrl: 'https://api.transglot.ai', project: 42, cdnKey: process.env.TRANSGLOT_CDN_KEY!, authIn: 'query', }} > {children} </NextTranslateProvider> ``` Without a `loader`, switching to a locale that was not seeded keeps the current locale and logs a dev warning. ## Freshness [#freshness] The seeded client does not poll and does not revalidate the seeded locale: its `refresh()` does nothing for a locale it already holds. New publishes reach users when the server runs `loadTranslations` again. How often that happens depends on how Next.js renders and caches the route. Next.js extends `fetch` with its own caching options. To decide how long the server reuses a bundle, pass a `fetchImpl` that sets them: ```ts const snapshot = await loadTranslations({ baseUrl: 'https://api.transglot.ai', project: 42, cdnKey: process.env.TRANSGLOT_CDN_KEY!, locale, fetchImpl: (input, init) => fetch(input, { ...init, next: { revalidate: 60 } }), }); ``` ## API [#api] ### `loadTranslations(options)` [#loadtranslationsoptions] `loadTranslations(options: LoadTranslationsOptions): Promise<TranslateSnapshot>` fetches one locale with the runtime client (the same URL, authentication, `429` retries and error mapping) and returns the raw body with its headers. <TypeTable type={{ baseUrl: { description: 'The transglot API origin, or a CDN or proxy in front of it.', type: 'string', required: true, }, project: { description: 'The numeric project id.', type: 'number | string', required: true, }, cdnKey: { description: 'The read-only CDN key.', type: 'string', required: true, }, locale: { description: 'The locale to fetch.', type: 'string', required: true, }, version: { description: 'Fetch one immutable version (/v{n}) instead of the latest publish.', type: 'number', }, authIn: { description: "Send the key in the X-Transglot-Cdn-Key header or the key query parameter.", type: "'header' | 'query'", default: "'header'", }, fetchImpl: { description: 'The fetch implementation to use.', type: 'typeof fetch', default: 'globalThis.fetch', }, dev: { description: 'Log console warnings.', type: 'boolean', default: 'false', }, }} /> ### `TranslateSnapshot` [#translatesnapshot] Every field is a JSON primitive, so the snapshot crosses the Server Component boundary and survives `getServerSideProps`. <TypeTable type={{ project: { description: 'The project id, as a string.', type: 'string', required: true }, locale: { description: 'The locale the snapshot holds.', type: 'string', required: true }, baseUrl: { description: 'The origin the bundle came from.', type: 'string', required: true }, format: { description: 'The X-Cdn-Format of the bundle.', type: 'string' }, version: { description: 'The X-Cdn-Version of the bundle.', type: 'string | null' }, etag: { description: 'The ETag of the bundle.', type: 'string | null' }, bundle: { description: 'The bundle body exactly as the CDN served it.', type: 'string', required: true }, }} /> ### `NextTranslateProvider` [#nexttranslateprovider] Pass exactly one of `snapshot` or `snapshots`. <TypeTable type={{ snapshot: { description: 'One snapshot to seed.', type: 'TranslateSnapshot' }, snapshots: { description: 'Several snapshots to seed, for switching without a request.', type: 'readonly TranslateSnapshot[]' }, locale: { description: 'The initial active locale.', type: 'string', default: 'the first snapshot\'s locale' }, loader: { description: 'CDN coordinates for fetching locales that were not seeded, from the browser.', type: "Omit<LoadTranslationsOptions, 'locale' | 'fetchImpl'>", }, children: { description: 'Your tree.', type: 'ReactNode' }, }} /> Inside the provider, `useTranslations()` and `T` work as described on the [React](https://transglot.ai/docs/developers/sdks/react#api) page. ### `createSeededClient(options)` [#createseededclientoptions] `createSeededClient(options: CreateSeededClientOptions): RuntimeClient` returns a client whose cache is filled synchronously from snapshots, so its first `t` call already returns translations. `NextTranslateProvider` uses it; you can use it directly in Server Components or with your own `TransglotProvider`. <TypeTable type={{ snapshots: { description: 'One snapshot or several.', type: 'TranslateSnapshot | readonly TranslateSnapshot[]', required: true }, locale: { description: 'The initial active locale.', type: 'string', default: 'the first snapshot\'s locale' }, loadSnapshot: { description: 'Called to fetch a locale that was not seeded. Without it, the client only knows the seeded locales.', type: '(locale: string) => Promise<TranslateSnapshot>', }, dev: { description: 'Log console warnings for missing keys and locales that were not seeded.', type: 'boolean', default: "true unless NODE_ENV is 'production'" }, }} /> The returned client has the full [`RuntimeClient`](https://transglot.ai/docs/developers/sdks/javascript#client-methods) shape. `hydrate(locale?)` resolves `true` when the locale is seeded, and `stop()` does nothing. ## Errors [#errors] `loadTranslations` rejects with the runtime's `RuntimeError`, with the slugs listed on the [JavaScript runtime](https://transglot.ai/docs/developers/sdks/javascript#errors) page. Catch it to render a fallback, or let it reach your error boundary. `createSeededClient` throws: - `Error('[transglot] createSeededClient() requires at least one snapshot.')` for an empty `snapshots` array. - `RuntimeError` with the slug `unsupported-format` when a snapshot body is not a JSON-family bundle. Set the project's **Delivery format** to `json_flat`, `json_nested`, `laravel_json` or `flutter_arb`. ## Related [#related] - [React](https://transglot.ai/docs/developers/sdks/react) - [JavaScript runtime](https://transglot.ai/docs/developers/sdks/javascript) - [Publish a locale](https://transglot.ai/docs/guides/delivery/publish-a-locale) # Pseudo-localization (https://transglot.ai/docs/developers/sdks/pseudo-localization/) `@transglot/pseudo` turns source strings into a pseudo-locale: every letter is swapped for an accented look-alike, the text is lengthened, and the value is wrapped in brackets. Run your app in that locale and two kinds of bug stand out: - Text that is not accented is hardcoded and never went through your i18n layer. - A label that is clipped, loses its closing `]]`, or wraps badly will break when a longer language arrives. Placeholders and markup are never changed, so the pseudo-locale still renders. The package is a library and a command-line tool, runs offline, and does not talk to transglot. ## Install [#install] ```npm npm install --save-dev @transglot/pseudo ``` It needs Node.js 20 or later and has no dependencies. ## Command line [#command-line] ```bash npx transglot-pseudo locales/en.json --out locales/en-XA.json ``` Given this source file: ```json title="locales/en.json" { "home": { "title": "Welcome back", "greeting": "Hello, {name}!" }, "cart.items": "{count, plural, one {# item} other {# items}}", "save": "Save <b>now</b>" } ``` it writes: ```json title="locales/en-XA.json" { "home": { "title": "[[Ŵéłçöɱáéíöüé ƀáçķ]]", "greeting": "[[Ĥéłáéíłö, {name}!]]" }, "cart.items": "[[{count, plural, one {# item} other {# items}}]]", "save": "[[Šááéíṽé <b>ñöŵ</b>]]" } ``` | Option | Default | Effect | | --- | --- | --- | | `<sourceFile>` | required | A JSON file, flat or nested. | | `--out <file>` | stdout | Write the result to a file instead of printing it, so the command also works in a pipeline. | | `--format json_nested` or `--format json_flat` | `json_nested` | `json_nested` transforms every string at any depth. `json_flat` transforms only the top-level values (strings and arrays of strings) and leaves nested objects untouched. | | `--expansion <n>` | `0.4` | How much longer to make the text, as a fraction: `0.4` adds about 40%. `0` turns lengthening off. | | `--no-brackets` | | Do not wrap values in `[[` and `]]`. | | `-h`, `--help` | | Print usage. | The command exits `0` on success, `1` when the source file cannot be read or is not valid JSON, and `2` on an unknown option, an invalid value, or a missing source file. Give the pseudo-locale a code no real user has. `en-XA` is the usual choice. Register it in your i18n setup like any other locale, switch to it, and click through your screens. Regenerate it when your source strings change; a CI step or pre-commit hook keeps it current. ## What is never transformed [#what-is-never-transformed] Only the readable text between placeholders and markup changes. These stay exactly as written: | Kind | Examples | | --- | --- | | HTML and XML tags | `<b>`, `</b>`, `<br/>`, `<a title="a>b">` | | Double-brace placeholders | `{{name}}` | | ICU plural, select and selectordinal blocks, kept whole including the text inside | `{count, plural, one {# item} other {# items}}` | | Single-brace placeholders | `{name}`, `{0}`, `{count, number}` | | printf-style placeholders | `%s`, `%d`, `%1$s`, `%.2f`, `%(name)s`, `%%` | | Colon parameters | `:name` | Lengthening adds accented vowels inside the longest run of text, so a value that is only a placeholder is not padded. ## Library [#library] ```ts import { pseudoLocalize, pseudoLocalizeJson } from '@transglot/pseudo'; pseudoLocalize('Save changes'); // "[[Šáṽé çáéíöüĥáñğéš]]" pseudoLocalize('Hi %s, you have :count items', { bracketOpen: '«', bracketClose: '»' }); // "«Ĥí %s, ýöüáéíöüáéí ĥáṽé :count íŧéɱš»" const pseudo = pseudoLocalizeJson(JSON.parse(source), { nested: true, expansion: 0.3 }); ``` ### `pseudoLocalize(value, options?)` [#pseudolocalizevalue-options] `pseudoLocalize(value: string, options?: PseudoOptions): string` transforms one string. <TypeTable type={{ expansion: { description: 'Fraction to lengthen the readable text by. 0 turns it off.', type: 'number', default: '0.4' }, brackets: { description: 'Wrap the result in the opening and closing brackets.', type: 'boolean', default: 'true' }, bracketOpen: { description: 'The opening bracket.', type: 'string', default: "'[['" }, bracketClose: { description: 'The closing bracket.', type: 'string', default: "']]'" }, }} /> ### `pseudoLocalizeJson(data, options?)` [#pseudolocalizejsondata-options] `pseudoLocalizeJson(data: unknown, options?: PseudoJsonOptions): unknown` transforms a parsed JSON tree. Numbers, booleans and `null` pass through unchanged. It takes the same options as `pseudoLocalize`, plus: <TypeTable type={{ nested: { description: 'Transform strings at every depth. When false, only top-level string values and arrays of strings change.', type: 'boolean', default: 'true' }, }} /> ### `tokenize(input)` [#tokenizeinput] `tokenize(input: string)` splits a string into the ordered runs of transformable text and protected placeholders that `pseudoLocalize` works from. Each token is `{ type: 'text' | 'protected', value: string }`. ## Related [#related] - [Runtime SDKs overview](https://transglot.ai/docs/developers/sdks) - [Quality checks](https://transglot.ai/docs/concepts/quality-checks) # react-intl (https://transglot.ai/docs/developers/sdks/react-intl/) `@transglot/react-intl` is a loader, not a provider. It fetches one locale's published bundle and returns it as the flat `{ id: message }` map that react-intl's `IntlProvider` and `createIntl` take. FormatJS does all the formatting, so your `FormattedMessage` and `formatMessage` call sites keep working and you get full ICU MessageFormat: `plural`, `select`, `selectordinal`, number, date and time arguments, and rich-text tags. It uses [`@transglot/runtime`](https://transglot.ai/docs/developers/sdks/javascript) for the request, so it shares its authentication, rate-limit retries and error types. ## Install [#install] ```npm npm install @transglot/react-intl react-intl ``` `react-intl` 6 or later is a peer dependency. `@transglot/runtime` is installed with the loader. ## Minimal setup [#minimal-setup] ```tsx title="src/main.tsx" import { loadMessages } from '@transglot/react-intl'; import { createRoot } from 'react-dom/client'; import { IntlProvider } from 'react-intl'; import { App } from './App'; const { locale, messages } = await loadMessages({ baseUrl: 'https://api.transglot.ai', project: 42, cdnKey: 'taicdn_your_key', locale: 'fr', authIn: 'query', }); createRoot(document.getElementById('root')!).render( <IntlProvider locale={locale} messages={messages} defaultLocale="en"> <App /> </IntlProvider>, ); ``` ```tsx title="src/App.tsx" import { FormattedMessage, useIntl } from 'react-intl'; export function App() { const intl = useIntl(); return ( <main> <h1>{intl.formatMessage({ id: 'home.title' })}</h1> <FormattedMessage id="cart.items" values={{ count: 3 }} /> </main> ); } ``` `loadMessages` runs anywhere `fetch` does: a Server Component, `getServerSideProps`, a route handler, or the browser. In a browser, pass `authIn: 'query'`; the CDN does not answer the CORS preflight that the header would trigger (see [In a browser](https://transglot.ai/docs/developers/sdks/javascript#in-a-browser)). Each call makes a fresh request and keeps no cache between calls, so it is not affected by the revalidation limit described there. ## How bundles become messages [#how-bundles-become-messages] The loader reshapes the bundle; it does not touch the strings: - Nested objects (`json_nested`) are flattened to dotted ids, so `{ "home": { "title": "..." } }` becomes `home.title`. - Flat keys (`json_flat`, `laravel_json`) are kept as they are. - Top-level `@`-prefixed members (ARB metadata in `flutter_arb`) are dropped. - Non-string values are converted with `String()`, and `null` values are skipped. Any of the four JSON-family delivery formats works. Plural keys you manage as plurals in transglot are only published in `flutter_arb`, where they arrive as ICU plural messages (`{count, plural, one {...} other {...}}`) that FormatJS formats. ICU strings stored as ordinary values reach FormatJS unchanged in every JSON format. If your project's **Message syntax** (project **Settings** → **Checks**) is ICU, a translation that is not valid ICU MessageFormat is left out of the published bundle rather than served. ## Missing messages [#missing-messages] A published bundle holds only the keys translated in that locale. For an id that is not in `messages`, react-intl renders the `defaultMessage` (or the id) and reports a missing-translation error through `onError`. To fall back to your source text instead, load the source locale too and merge it underneath: ```ts const [source, target] = await Promise.all([ loadMessages({ baseUrl, project, cdnKey, locale: 'en', authIn: 'query' }), loadMessages({ baseUrl, project, cdnKey, locale: 'fr', authIn: 'query' }), ]); const messages = { ...source.messages, ...target.messages }; ``` The source locale must be published too. ## API [#api] ### `loadMessages(options)` [#loadmessagesoptions] `loadMessages(options: LoadMessagesOptions): Promise<ReactIntlBundle>` <TypeTable type={{ baseUrl: { description: 'The transglot API origin, or a CDN or proxy in front of it.', type: 'string', required: true }, project: { description: 'The numeric project id.', type: 'number | string', required: true }, cdnKey: { description: 'The read-only CDN key.', type: 'string', required: true }, locale: { description: 'The locale to fetch. It is returned as the bundle locale.', type: 'string', required: true }, version: { description: 'Fetch one immutable version (/v{n}) instead of the latest publish.', type: 'number' }, authIn: { description: 'Send the key in the X-Transglot-Cdn-Key header or the key query parameter. Use query in a browser.', type: "'header' | 'query'", default: "'header'" }, fetchImpl: { description: 'The fetch implementation to use.', type: 'typeof fetch', default: 'globalThis.fetch' }, dev: { description: 'Log console warnings.', type: 'boolean', default: 'false' }, }} /> `ReactIntlBundle`: <TypeTable type={{ locale: { description: 'The locale, for IntlProvider locale.', type: 'string', required: true }, messages: { description: 'The flat id to message map, for IntlProvider messages.', type: 'IntlMessages', required: true }, format: { description: 'The X-Cdn-Format of the bundle.', type: 'string' }, version: { description: 'The X-Cdn-Version of the bundle.', type: 'string | null' }, etag: { description: 'The ETag of the bundle.', type: 'string | null' }, }} /> ### `bundleToMessages(rawBody)` [#bundletomessagesrawbody] `bundleToMessages(rawBody: string): IntlMessages` turns a bundle body into a messages map, as `loadMessages` does. Use it when you fetch the bundle yourself, for example through your own cache. Types: `LoadMessagesOptions`, `ReactIntlBundle`, `IntlMessages` (`{ readonly [id: string]: string }`). ## Errors [#errors] `loadMessages` rejects with the runtime's `RuntimeError`, which the package re-exports. The slugs are listed on the [JavaScript runtime](https://transglot.ai/docs/developers/sdks/javascript#errors) page. `bundleToMessages` throws a `RuntimeError` with the slug `unsupported-format` when the body is not a JSON object. ```ts import { RuntimeError, loadMessages } from '@transglot/react-intl'; try { bundle = await loadMessages({ baseUrl, project, cdnKey, locale, authIn: 'query' }); } catch (error) { if (error instanceof RuntimeError && error.slug === 'not-published') { bundle = { locale: 'en', messages: {} }; } else { throw error; } } ``` ## Related [#related] - [JavaScript runtime](https://transglot.ai/docs/developers/sdks/javascript) - [i18next backend](https://transglot.ai/docs/developers/sdks/i18next) - [Plural forms](https://transglot.ai/docs/guides/projects/plural-forms) # React (https://transglot.ai/docs/developers/sdks/react/) `@transglot/react-i18n` connects [`@transglot/runtime`](https://transglot.ai/docs/developers/sdks/javascript) to React. Components that translate re-render when the locale changes or a new publish lands for the active locale. <Callout type="info" title="Not the in-context editor"> `@transglot/react-i18n` delivers published translations to your users. `@transglot/react` is a different package: it adds the in-context editor for reviewers. See [Install the in-context editor](https://transglot.ai/docs/developers/in-context/install). </Callout> ## Install [#install] ```npm npm install @transglot/react-i18n ``` React 18 or 19 is a peer dependency. `@transglot/runtime` is installed with the package, and the package re-exports `createClient`, `RuntimeError` and the `RuntimeClient` and `ClientOptions` types, so one import is enough. ## Minimal setup [#minimal-setup] Wrap your app in `TransglotProvider` and pass the runtime client options: ```tsx title="src/main.tsx" import { TransglotProvider } from '@transglot/react-i18n'; import { createRoot } from 'react-dom/client'; import { App } from './App'; createRoot(document.getElementById('root')!).render( <TransglotProvider options={{ baseUrl: 'https://api.transglot.ai', project: 42, cdnKey: 'taicdn_your_key', locale: 'en', authIn: 'query', }} > <App /> </TransglotProvider>, ); ``` Then translate in any component below it: ```tsx title="src/Header.tsx" import { T, useTranslations } from '@transglot/react-i18n'; export function Header() { const { t, locale, setLocale } = useTranslations(); return ( <header> <h1>{t('home.title')}</h1> <p>{t('cart.items', { count: 3 })}</p> <T keypath="home.greeting" params={{ name: 'Ada' }} /> <button onClick={() => setLocale(locale === 'en' ? 'fr' : 'en')}>{locale}</button> </header> ); } ``` The provider starts loading the initial locale when it mounts. Until the bundle arrives, `t` returns keys; the tree re-renders with translations when it lands. <Callout type="warn" title="In the browser"> Pass `authIn: 'query'` when the client runs in a browser. The CDN does not answer CORS preflight requests, which also limits revalidation from a browser. See [In a browser](https://transglot.ai/docs/developers/sdks/javascript#in-a-browser). </Callout> ## API [#api] ### `TransglotProvider` [#transglotprovider] Pass exactly one of `options` or `client`. <TypeTable type={{ options: { description: 'Runtime client options. The provider creates the client from them.', type: 'ClientOptions', }, client: { description: 'A client you created with createClient. Use it to share one client, to add storage or background refresh, or in tests.', type: 'RuntimeClient', }, children: { description: 'Your app.', type: 'ReactNode', }, }} /> The provider reads `options` or `client` once, on its first render. To switch to a different client, remount the provider (for example with a React `key`). On unmount it stops listening to the client; it does not call `client.stop()`, so stop a client you created yourself. ### `useTranslations()` [#usetranslations] Returns `{ t, locale, setLocale, client }`. Throws `[transglot] useTranslations() must be used inside <TransglotProvider>.` when there is no provider above the component. <TypeTable type={{ t: { description: 'Translate a key in the active locale, with interpolation and plural selection. Returns the key when it is missing.', type: '(key: string, params?: Record<string, unknown>) => string', }, locale: { description: 'The active locale.', type: 'string', }, setLocale: { description: 'Load a locale, then switch to it. Every component using the hook re-renders.', type: '(locale: string) => Promise<void>', }, client: { description: 'The underlying runtime client, for preload, refresh and the rest of its methods.', type: 'RuntimeClient', }, }} /> ### `T` [#t] Renders a translation as a bare text node. Wrap it in an element yourself if you need one. <TypeTable type={{ keypath: { description: 'The translation key.', type: 'string', required: true, }, params: { description: 'Interpolation values, including count for plurals.', type: 'Record<string, unknown>', }, }} /> ### Other exports [#other-exports] - `TransglotContext`: the React context the provider fills. Its value is a `TransglotStore` (`{ client, subscribe, getSnapshot }`) shaped for `useSyncExternalStore`, or `null` outside a provider. - Types: `TransglotProviderProps`, `UseTranslations`, `TProps`, `TransglotStore`. ## Handling load errors [#handling-load-errors] The provider starts the first load and does not catch its failure. To handle a `RuntimeError` yourself, create the client, load the locale, and pass the client in: ```tsx title="src/main.tsx" import { createClient, RuntimeError, TransglotProvider } from '@transglot/react-i18n'; import { createRoot } from 'react-dom/client'; import { App } from './App'; const client = createClient({ baseUrl: 'https://api.transglot.ai', project: 42, cdnKey: 'taicdn_your_key', locale: 'en', authIn: 'query', }); try { await client.loadLocale('en'); } catch (error) { if (!(error instanceof RuntimeError)) throw error; console.error(error.slug, error.hint); } createRoot(document.getElementById('root')!).render( <TransglotProvider client={client}> <App /> </TransglotProvider>, ); ``` `setLocale` from the hook rejects with a `RuntimeError` in the same way, so catch it where you call it. The error slugs are listed on the [JavaScript runtime](https://transglot.ai/docs/developers/sdks/javascript#errors) page. ## Server rendering [#server-rendering] `TransglotProvider` loads translations after mount, so server-rendered HTML contains keys, not translations. For server rendering with React, use [`@transglot/next`](https://transglot.ai/docs/developers/sdks/nextjs) or [`@transglot/astro`](https://transglot.ai/docs/developers/sdks/astro), which fetch the bundle on the server and seed the client before the first render. ## Related [#related] - [JavaScript runtime](https://transglot.ai/docs/developers/sdks/javascript) - [Next.js](https://transglot.ai/docs/developers/sdks/nextjs) - [react-intl](https://transglot.ai/docs/developers/sdks/react-intl) # Solid (https://transglot.ai/docs/developers/sdks/solid/) `@transglot/solid` connects [`@transglot/runtime`](https://transglot.ai/docs/developers/sdks/javascript) to SolidJS 1.6 or later. JSX, memos and effects that call `t` recompute when the locale changes or a new publish lands for the active locale. ## Install [#install] ```npm npm install @transglot/solid ``` `solid-js` is a peer dependency. `@transglot/runtime` is installed with the package, and `createClient`, `RuntimeError` and the `RuntimeClient` and `ClientOptions` types are re-exported from it. The package ships its components as JSX for Solid's compiler to transform, so use it through a build that compiles Solid JSX in dependencies, such as Vite with `vite-plugin-solid`. ## Minimal setup [#minimal-setup] Wrap your app in `TransglotProvider` and pass the runtime client options: ```tsx title="src/index.tsx" import { TransglotProvider } from '@transglot/solid'; import { render } from 'solid-js/web'; import { App } from './App'; render( () => ( <TransglotProvider options={{ baseUrl: 'https://api.transglot.ai', project: 42, cdnKey: 'taicdn_your_key', locale: 'en', authIn: 'query', }} > <App /> </TransglotProvider> ), document.getElementById('root')!, ); ``` Then translate in any component below it: ```tsx title="src/Header.tsx" import { T, useTranslations } from '@transglot/solid'; export function Header() { const { t, locale, setLocale } = useTranslations(); return ( <header> <h1>{t('home.title')}</h1> <p>{t('cart.items', { count: 3 })}</p> <T keypath="home.greeting" params={{ name: 'Ada' }} /> <button onClick={() => void setLocale(locale() === 'en' ? 'fr' : 'en')}>{locale()}</button> </header> ); } ``` The provider starts loading the initial locale when it is created and does not wait for it. Until the bundle arrives, `t` returns keys; JSX that reads `t` updates when it lands. <Callout type="warn" title="In the browser"> Pass `authIn: 'query'` when the client runs in a browser. The CDN does not answer CORS preflight requests, which also limits revalidation from a browser. See [In a browser](https://transglot.ai/docs/developers/sdks/javascript#in-a-browser). </Callout> ## API [#api] ### `TransglotProvider` [#transglotprovider] Pass exactly one of `options` or `client`. The provider creates or adopts the client once. <TypeTable type={{ options: { description: 'Runtime client options. The provider creates the client from them.', type: 'ClientOptions' }, client: { description: 'A client you created with createClient.', type: 'RuntimeClient' }, children: { description: 'Your app.', type: 'JSX.Element' }, }} /> ### `useTranslations()` [#usetranslations] Returns a `Translation`. Throws `[transglot] useTranslations() must be used inside <TransglotProvider>.` outside a provider. <TypeTable type={{ t: { description: 'Translate a key in the active locale. Called inside JSX, a memo or an effect, it recomputes on a locale switch or a new bundle. Returns the key when it is missing.', type: '(key: string, params?: Record<string, unknown>) => string', }, locale: { description: 'An accessor for the active locale. Call it: locale().', type: '() => string' }, setLocale: { description: 'Load a locale, then switch to it.', type: '(locale: string) => Promise<void>' }, client: { description: 'The underlying runtime client.', type: 'RuntimeClient' }, }} /> ### `T` [#t] Renders a translation as text. <TypeTable type={{ keypath: { description: 'The translation key.', type: 'string', required: true }, params: { description: 'Interpolation values, including count for plurals.', type: 'Record<string, unknown>' }, }} /> ### `createTranslation(client)` [#createtranslationclient] `createTranslation(client: RuntimeClient): Translation` builds the same reactive object without a provider. Call it inside a reactive owner (a component or `createRoot`): it subscribes to the client and unsubscribes when the owner is cleaned up. ### Other exports [#other-exports] - `TransglotContext`: the Solid context the provider fills. - Types: `TransglotProviderProps`, `TProps`, `Translation`. ## Errors [#errors] The provider does not catch a failed first load, and `setLocale` rejects with the runtime's `RuntimeError` (see [Errors](https://transglot.ai/docs/developers/sdks/javascript#errors)). To handle the first load, create the client yourself and track the load with a resource: ```tsx title="src/index.tsx" import { TransglotProvider, createClient } from '@transglot/solid'; import { ErrorBoundary, Show, createResource } from 'solid-js'; import { render } from 'solid-js/web'; import { App } from './App'; const client = createClient({ baseUrl: 'https://api.transglot.ai', project: 42, cdnKey: 'taicdn_your_key', locale: 'en', authIn: 'query', }); render(() => { const [ready] = createResource(() => client.loadLocale('en').then(() => true)); return ( <ErrorBoundary fallback={(error) => <p>Translations could not load: {error.slug}</p>}> <Show when={ready()}> <TransglotProvider client={client}> <App /> </TransglotProvider> </Show> </ErrorBoundary> ); }, document.getElementById('root')!); ``` ## Related [#related] - [JavaScript runtime](https://transglot.ai/docs/developers/sdks/javascript) - [Runtime SDKs overview](https://transglot.ai/docs/developers/sdks) # Svelte (https://transglot.ai/docs/developers/sdks/svelte/) `@transglot/svelte` connects [`@transglot/runtime`](https://transglot.ai/docs/developers/sdks/javascript) to Svelte 5. It puts the client in component context and gives you a `t` store, so `{$t('key')}` re-renders when the locale changes or a new publish lands for the active locale. For SvelteKit with server rendering, use [`@transglot/sveltekit`](https://transglot.ai/docs/developers/sdks/sveltekit), which builds on this package. ## Install [#install] ```npm npm install @transglot/svelte ``` Svelte 5 is a peer dependency. `@transglot/runtime` is installed with the package, and `createClient`, `RuntimeError` and the `RuntimeClient` and `ClientOptions` types are re-exported from it. ## Minimal setup [#minimal-setup] Set the context once, in your root component: ```svelte title="src/App.svelte" <script lang="ts"> import { setTransglotContext } from '@transglot/svelte'; import Header from './Header.svelte'; setTransglotContext({ baseUrl: 'https://api.transglot.ai', project: 42, cdnKey: 'taicdn_your_key', locale: 'en', authIn: 'query', }); </script> <Header /> ``` Read it in any component below. `t` and `locale` are Svelte stores, so use the `$` prefix: ```svelte title="src/Header.svelte" <script lang="ts"> import { useTranslations } from '@transglot/svelte'; const { t, locale, setLocale } = useTranslations(); </script> <h1>{$t('home.title')}</h1> <p>{$t('cart.items', { count: 3 })}</p> <p>{$t('home.greeting', { name: 'Ada' })}</p> <button onclick={() => setLocale($locale === 'en' ? 'fr' : 'en')}>{$locale}</button> ``` `setTransglotContext` starts loading the initial locale and does not wait for it. Until the bundle arrives, `$t` returns keys; markup re-renders when it lands. <Callout type="warn" title="In the browser"> Pass `authIn: 'query'` when the client runs in a browser. The CDN does not answer CORS preflight requests, which also limits revalidation from a browser. See [In a browser](https://transglot.ai/docs/developers/sdks/javascript#in-a-browser). </Callout> ## API [#api] ### `setTransglotContext(source)` [#settransglotcontextsource] `setTransglotContext(source: TransglotSource): TranslationStore` Call it while a component initializes, as with Svelte's `setContext`. `source` is either the runtime [`ClientOptions`](https://transglot.ai/docs/developers/sdks/javascript#options) or `{ client }` with a client you created. It creates or adopts the client, puts a `TranslationStore` in context, starts loading the initial locale, and returns the store so the same component can use it. ### `useTranslations()` [#usetranslations] `useTranslations(): TranslationStore` Call it while a component initializes. Throws `[transglot] useTranslations() must be called under a component that ran setTransglotContext().` when no ancestor set the context. <TypeTable type={{ t: { description: 'A store holding the translate function. Use $t(key, params) in markup. Returns the key when it is missing.', type: 'Readable<(key: string, params?: Record<string, unknown>) => string>', }, locale: { description: 'A store holding the active locale.', type: 'Readable<string>', }, setLocale: { description: 'Load a locale, then switch to it.', type: '(locale: string) => Promise<void>', }, client: { description: 'The underlying runtime client.', type: 'RuntimeClient', }, }} /> ### `createTranslationStore(client)` [#createtranslationstoreclient] `createTranslationStore(client: RuntimeClient): TranslationStore` wraps a client in the same stores without touching context, for when you manage context yourself or use the stores outside components. The store subscribes to the client when it gets its first subscriber and unsubscribes when the last one leaves. Types: `TransglotSource`, `TranslationStore`, `TranslateFn`. ## Errors and server rendering [#errors-and-server-rendering] `setTransglotContext` does not catch a failed first load, and `setLocale` rejects with the runtime's `RuntimeError` (see [Errors](https://transglot.ai/docs/developers/sdks/javascript#errors)). To handle load errors, create the client, wait for the locale, then pass `{ client }`: ```svelte title="src/App.svelte" <script lang="ts"> import { createClient, setTransglotContext } from '@transglot/svelte'; import Header from './Header.svelte'; const client = createClient({ baseUrl: 'https://api.transglot.ai', project: 42, cdnKey: 'taicdn_your_key', locale: 'en', authIn: 'query', }); setTransglotContext({ client }); const ready = client.loadLocale('en'); </script> {#await ready then} <Header /> {:catch error} <p>Translations could not load: {error.slug}</p> {/await} ``` The context does not wait for translations, so server-rendered markup from this package contains keys. In SvelteKit, [`@transglot/sveltekit`](https://transglot.ai/docs/developers/sdks/sveltekit) loads the bundle in a `load` function and seeds the context before rendering. ## Related [#related] - [SvelteKit](https://transglot.ai/docs/developers/sdks/sveltekit) - [JavaScript runtime](https://transglot.ai/docs/developers/sdks/javascript) # SvelteKit (https://transglot.ai/docs/developers/sdks/sveltekit/) `@transglot/sveltekit` fetches a locale's published bundle in a SvelteKit `load` function and seeds the [`@transglot/svelte`](https://transglot.ai/docs/developers/sdks/svelte) context from it synchronously. Server-rendered markup contains translations, and hydration does not fetch the seeded locale again. It needs SvelteKit 2 and Svelte 5. ## Install [#install] ```npm npm install @transglot/sveltekit ``` `@transglot/svelte` and `@transglot/runtime` are installed with it, and the package re-exports what you need from both, so you import everything from `@transglot/sveltekit`. ## Setup [#setup] Add your CDN key to `.env` as `TRANSGLOT_CDN_KEY`, then load the snapshot in a server `load` function: ```ts title="src/routes/[locale]/+layout.server.ts" import { TRANSGLOT_CDN_KEY } from '$env/static/private'; import { loadTranslations } from '@transglot/sveltekit'; import type { LayoutServerLoad } from './$types'; export const load: LayoutServerLoad = async (event) => ({ snapshot: await loadTranslations(event, { baseUrl: 'https://api.transglot.ai', project: 42, cdnKey: TRANSGLOT_CDN_KEY, locale: event.params.locale, }), }); ``` Seed the context in the matching layout component: ```svelte title="src/routes/[locale]/+layout.svelte" <script lang="ts"> import { setTransglotContextFromSnapshot } from '@transglot/sveltekit'; let { data, children } = $props(); setTransglotContextFromSnapshot({ snapshots: data.snapshot }); </script> {@render children()} ``` Translate in any component below it: ```svelte title="src/routes/[locale]/+page.svelte" <script lang="ts"> import { useTranslations } from '@transglot/sveltekit'; const { t } = useTranslations(); </script> <h1>{$t('home.title')}</h1> <p>{$t('home.greeting', { name: 'Ada' })}</p> ``` The CDN key stays on the server: the snapshot carries the bundle and its headers, not the key. <Callout type="warn" title="Use a server load function"> Load the snapshot in `+layout.server.ts` or `+page.server.ts`. In a universal `+layout.ts`, the key ends up in your client code, the load runs in the browser on client-side navigation (where the CDN's CORS rules apply, see [In a browser](https://transglot.ai/docs/developers/sdks/javascript#in-a-browser)), and during server rendering SvelteKit refuses to let the loader read the `etag`, `x-cdn-format` and `x-cdn-version` response headers unless your `handle` hook allows them with `filterSerializedResponseHeaders`. </Callout> ## Switching locale [#switching-locale] `setTransglotContextFromSnapshot` runs once, when the layout component initializes. SvelteKit keeps a layout mounted when only its route parameters change, so navigating from `/en` to `/fr` does not reseed it. Either: - reload the page on a locale change, for example with `data-sveltekit-reload` on your language links, or - seed every locale you serve and switch with `setLocale`, which needs no network request between seeded locales: ```ts title="src/routes/+layout.server.ts" import { TRANSGLOT_CDN_KEY } from '$env/static/private'; import { loadTranslations } from '@transglot/sveltekit'; import type { LayoutServerLoad } from './$types'; export const load: LayoutServerLoad = async (event) => ({ snapshots: await Promise.all( ['en', 'fr', 'de'].map((locale) => loadTranslations(event, { baseUrl: 'https://api.transglot.ai', project: 42, cdnKey: TRANSGLOT_CDN_KEY, locale, }), ), ), }); ``` ```svelte title="src/routes/+layout.svelte" <script lang="ts"> import { setTransglotContextFromSnapshot } from '@transglot/sveltekit'; let { data, children } = $props(); const { locale, setLocale } = setTransglotContextFromSnapshot({ snapshots: data.snapshots, locale: 'en', }); </script> <button onclick={() => setLocale($locale === 'en' ? 'fr' : 'en')}>{$locale}</button> {@render children()} ``` Switching to a locale that was not seeded, without a `loadSnapshot`, keeps the current locale and logs a dev warning. ## Freshness [#freshness] The seeded client does not poll or revalidate. A new publish reaches users when a `load` function runs `loadTranslations` again, which happens on each server render of the route unless you cache it. ## API [#api] ### `loadTranslations(event, options)` [#loadtranslationsevent-options] `loadTranslations(event: LoadEventLike, options: LoadTranslationsOptions): Promise<TranslateSnapshot>` `event` is the `load` event, or any object with a `fetch` function; the loader uses `event.fetch`. It fetches one locale with the runtime client (the same URL, authentication, `429` retries and error mapping) and returns the raw body with its headers. <TypeTable type={{ baseUrl: { description: 'The transglot API origin, or a CDN or proxy in front of it.', type: 'string', required: true }, project: { description: 'The numeric project id.', type: 'number | string', required: true }, cdnKey: { description: 'The read-only CDN key.', type: 'string', required: true }, locale: { description: 'The locale to fetch.', type: 'string', required: true }, version: { description: 'Fetch one immutable version (/v{n}) instead of the latest publish.', type: 'number' }, authIn: { description: 'Send the key in the X-Transglot-Cdn-Key header or the key query parameter.', type: "'header' | 'query'", default: "'header'" }, dev: { description: 'Log console warnings.', type: 'boolean', default: 'false' }, }} /> `TranslateSnapshot` has the same fields as in the Next.js package: `project`, `locale`, `baseUrl`, `format`, `version`, `etag` and `bundle`. See [TranslateSnapshot](https://transglot.ai/docs/developers/sdks/nextjs#translatesnapshot). ### `setTransglotContextFromSnapshot(options)` [#settransglotcontextfromsnapshotoptions] `setTransglotContextFromSnapshot(options: CreateSeededClientOptions): TranslationStore` Call it while a component initializes. It builds a seeded client, sets the `@transglot/svelte` context with it, and returns the store (`{ t, locale, setLocale, client }`). <TypeTable type={{ snapshots: { description: 'One snapshot or several.', type: 'TranslateSnapshot | readonly TranslateSnapshot[]', required: true }, locale: { description: 'The initial active locale.', type: 'string', default: 'the first snapshot\'s locale' }, loadSnapshot: { description: 'Called to fetch a locale that was not seeded.', type: '(locale: string) => Promise<TranslateSnapshot>' }, dev: { description: 'Log console warnings for missing keys and locales that were not seeded.', type: 'boolean', default: "true unless NODE_ENV is 'production'" }, }} /> ### `createSeededClient(options)` [#createseededclientoptions] `createSeededClient(options: CreateSeededClientOptions): RuntimeClient` returns the seeded client on its own, with the same options. Its `refresh()` does nothing for a locale it holds, `hydrate(locale?)` resolves `true` for a seeded locale, and `stop()` does nothing. ### Re-exports [#re-exports] - From `@transglot/svelte`: `setTransglotContext`, `useTranslations`, `createTranslationStore`, and the types `TransglotSource`, `TranslationStore`, `TranslateFn`. See [Svelte](https://transglot.ai/docs/developers/sdks/svelte#api). - From `@transglot/runtime`: `createClient`, `RuntimeError`, `parseBundle`, `interpolate`, and the types `RuntimeClient`, `ClientOptions`. - Types: `LoadTranslationsOptions`, `LoadEventLike`, `TranslateSnapshot`, `CreateSeededClientOptions`. ## Errors [#errors] `loadTranslations` rejects with the runtime's `RuntimeError`; the slugs are listed on the [JavaScript runtime](https://transglot.ai/docs/developers/sdks/javascript#errors) page. Map it to a SvelteKit error in your `load` function: ```ts title="src/routes/[locale]/+layout.server.ts" import { TRANSGLOT_CDN_KEY } from '$env/static/private'; import { RuntimeError, loadTranslations } from '@transglot/sveltekit'; import { error } from '@sveltejs/kit'; import type { LayoutServerLoad } from './$types'; export const load: LayoutServerLoad = async (event) => { try { return { snapshot: await loadTranslations(event, { baseUrl: 'https://api.transglot.ai', project: 42, cdnKey: TRANSGLOT_CDN_KEY, locale: event.params.locale, }), }; } catch (err) { if (err instanceof RuntimeError && err.slug === 'not-found') { error(404, 'Unknown locale'); } throw err; } }; ``` Seeding throws `Error('[transglot] createSeededClient() requires at least one snapshot.')` for an empty array, and a `RuntimeError` with the slug `unsupported-format` when the project's delivery format is not one of `json_flat`, `json_nested`, `laravel_json` or `flutter_arb`. ## Related [#related] - [Svelte](https://transglot.ai/docs/developers/sdks/svelte) - [JavaScript runtime](https://transglot.ai/docs/developers/sdks/javascript) - [Publish a locale](https://transglot.ai/docs/guides/delivery/publish-a-locale) # Vue (https://transglot.ai/docs/developers/sdks/vue/) `@transglot/vue` connects [`@transglot/runtime`](https://transglot.ai/docs/developers/sdks/javascript) to Vue 3.3 or later. Every `t(...)` call in a template re-renders when the locale changes or a new publish lands for the active locale. ## Install [#install] ```npm npm install @transglot/vue ``` Vue is a peer dependency. `@transglot/runtime` is installed with the package, and `createClient`, `RuntimeError` and the `RuntimeClient` and `ClientOptions` types are re-exported from it. ## Minimal setup [#minimal-setup] Install the plugin with the runtime client options: ```ts title="src/main.ts" import { Transglot } from '@transglot/vue'; import { createApp } from 'vue'; import App from './App.vue'; createApp(App) .use(Transglot, { baseUrl: 'https://api.transglot.ai', project: 42, cdnKey: 'taicdn_your_key', locale: 'en', authIn: 'query', }) .mount('#app'); ``` Then translate in any component: ```vue title="src/components/AppHeader.vue" <script setup lang="ts"> import { useTranslations } from '@transglot/vue'; const { t, locale, setLocale } = useTranslations(); </script> <template> <h1>{{ t('home.title') }}</h1> <p>{{ t('cart.items', { count: 3 }) }}</p> <T keypath="home.cta" /> <T tag="span" keypath="home.greeting" :params="{ name: 'Ada' }" /> <button @click="setLocale(locale === 'en' ? 'fr' : 'en')">{{ locale }}</button> </template> ``` The plugin starts loading the initial locale when you install it and does not wait for it. Until the bundle arrives, `t` returns keys; templates re-render when it lands. <Callout type="warn" title="In the browser"> Pass `authIn: 'query'` when the client runs in a browser. The CDN does not answer CORS preflight requests, which also limits revalidation from a browser. See [In a browser](https://transglot.ai/docs/developers/sdks/javascript#in-a-browser). </Callout> ## API [#api] ### `app.use(Transglot, options)` [#appusetransglot-options] `options` is either the runtime [`ClientOptions`](https://transglot.ai/docs/developers/sdks/javascript#options), from which the plugin creates the client, or `{ client }` with a client you created. The plugin: - provides the client to the app, - registers `T` as a global component, - adds `$t` as a global property for the Options API, - starts loading the initial locale. ### `useTranslations()` [#usetranslations] Returns `{ t, locale, setLocale, client }`. Call it in `setup`. Throws `[transglot] useTranslations()/useT() must be used after app.use(Transglot, …).` when the plugin is not installed. <TypeTable type={{ t: { description: 'Translate a key in the active locale. Reactive: a template, computed or render function that calls it re-runs on a locale switch or a new bundle. Returns the key when it is missing.', type: '(key: string, params?: Record<string, unknown>) => string', }, locale: { description: 'The active locale. Read .value in script; templates unwrap it.', type: 'Readonly<Ref<string>>', }, setLocale: { description: 'Load a locale, then switch to it.', type: '(locale: string) => Promise<void>', }, client: { description: 'The underlying runtime client.', type: 'RuntimeClient', }, }} /> ### `useT()` [#uset] Returns only the reactive `t` from `useTranslations()`. ### `T` [#t] Renders a translation as a bare text node, or inside an element when you pass `tag`. <TypeTable type={{ keypath: { description: 'The translation key.', type: 'string', required: true }, params: { description: 'Interpolation values, including count for plurals.', type: 'Record<string, unknown>' }, tag: { description: 'An element to wrap the text in, such as span.', type: 'string' }, }} /> ### `$t` in the Options API [#t-in-the-options-api] ```vue <template> <h1>{{ $t('home.title') }}</h1> </template> ``` `$t` is reactive like `t`. The package does not declare it for TypeScript; if you use it in typed components, add the declaration yourself: ```ts title="src/transglot.d.ts" export {}; declare module 'vue' { interface ComponentCustomProperties { $t: (key: string, params?: Record<string, unknown>) => string; } } ``` ### Other exports [#other-exports] - `TRANSGLOT_KEY`: the injection key the plugin provides. Its value is a `TransglotContext` (`{ client, locale, revision }`). - Types: `TransglotOptions`, `UseTranslations`, `TransglotContext`. ## Errors and server rendering [#errors-and-server-rendering] The plugin does not catch a failed first load, and `setLocale` rejects with the runtime's `RuntimeError` (see [Errors](https://transglot.ai/docs/developers/sdks/javascript#errors)). The plugin does not wait for translations, so server-rendered output contains keys. To handle load errors, or to render translated HTML on the server, create the client yourself, wait for the locale, then install the plugin with it: ```ts title="src/main.ts" import { createClient, RuntimeError, Transglot } from '@transglot/vue'; import { createApp } from 'vue'; import App from './App.vue'; const client = createClient({ baseUrl: 'https://api.transglot.ai', project: 42, cdnKey: 'taicdn_your_key', locale: 'en', authIn: 'query', }); try { await client.loadLocale('en'); } catch (error) { if (!(error instanceof RuntimeError)) throw error; console.error(error.slug, error.hint); } createApp(App).use(Transglot, { client }).mount('#app'); ``` ## Related [#related] - [JavaScript runtime](https://transglot.ai/docs/developers/sdks/javascript) - [Runtime SDKs overview](https://transglot.ai/docs/developers/sdks) # Editor and IDE plugins (https://transglot.ai/docs/developers/tools/editor-and-ide-plugins/) transglot reaches your editor in two ways. Both are npm packages, both authenticate with a [deploy token](https://transglot.ai/docs/developers/deploy-tokens), and both can do only what the token's abilities allow. | Tool | Package | Works in | What you get | | --- | --- | --- | --- | | [Language server](https://transglot.ai/docs/developers/tools/language-server) | `@transglot/lsp` | Neovim, JetBrains IDEs (through LSP4IJ), Helix, Sublime Text, and any editor with an LSP client | Missing translations flagged as diagnostics, translations on hover and in CodeLens, and a code action that turns a hardcoded string into a key. | | [MCP server](https://transglot.ai/docs/mcp) | `@transglot/mcp` | Claude Code, Claude Desktop, Cursor, VS Code, Windsurf, and any other MCP client | An AI agent that reads and edits translations, syncs files, starts translation runs and manages the glossary. | ## Which one to use [#which-one-to-use] - **You want to see translations while you edit keys**: the language server. It reads your `transglot.json` and works on JSON message files. - **You want an assistant to do localization work for you**: the MCP server. Ask it to fill missing translations, check a key in every language, or add a glossary term. - **You want to sync files from a terminal or CI**: neither. Use the [CLI](https://transglot.ai/docs/developers/cli) or the [GitHub Action](https://transglot.ai/docs/developers/github-action). ## Set up [#set-up] <Cards> <Card title="Language server" href="/developers/tools/language-server" description="Install @transglot/lsp and configure Neovim, a JetBrains IDE, Helix or Sublime Text." /> <Card title="MCP server" href="/mcp" description="Add @transglot/mcp to your MCP client and pick the token abilities for your agent." /> </Cards> # Language server (https://transglot.ai/docs/developers/tools/language-server/) `@transglot/lsp` is a [Language Server Protocol](https://microsoft.github.io/language-server-protocol/) server. Any editor with an LSP client can start it and get the same features, configured once per editor. ## Features [#features] - **Missing translations as diagnostics.** In a JSON source file that one of your `sources` rules names, every key with no translation in a target locale is flagged: `"cart.title" is missing a translation in: fr, de`. - **Hover.** Hovering a key shows its translation in every target locale. - **CodeLens.** A summary line above each key, such as `fr: Bonjour · de: Hallo · es: Hola`. - **Extract to transglot key.** On a string literal in a `.js`, `.jsx`, `.ts` or `.tsx` file, the code action **Extract to transglot key** adds the string to your messages file under a generated key, replaces the literal with an i18n call such as `t('key')`, and offers **Push key to transglot** to upload the messages file. Diagnostics, hover and CodeLens work on the JSON formats: `json_flat`, `json_nested`, `laravel_json` and `flutter_arb`. The server downloads each target locale from the project, so it shows what the project holds rather than your local target files. ## Requirements [#requirements] - **Node.js 20 or later.** - **A deploy token** with the **pull** ability, plus **push** to upload extracted keys. See [Deploy tokens](https://transglot.ai/docs/developers/deploy-tokens). - **A `transglot.json`** in the workspace root. The server reads its `sources` rules to know which files are source files. See [Configuration file](https://transglot.ai/docs/developers/cli/configuration). Version 0.1.0 of the server cannot read a `transglot.json` whose rules use `metadata`, `unity_csv` or `unreal_po`. It then logs `transglot: could not load transglot.json` and shows no diagnostics. ## Install [#install] ```npm npm install -g @transglot/lsp ``` This puts a `transglot-lsp` command on your `PATH`. It speaks LSP over stdio. ## Settings [#settings] The server reads its settings from the `initializationOptions` your editor sends when it starts the server. They are read once, at start. <TypeTable type={{ serverUrl: { description: 'The transglot API address, https://api.transglot.ai. It must use https; plain http is accepted only for localhost.', type: 'string', required: true, }, token: { description: 'The deploy token. It is sent only to serverUrl and never logged.', type: 'string', required: true, }, configPath: { description: 'The path of transglot.json, relative to the workspace root.', type: 'string', default: '"transglot.json"', }, messagesFile: { description: 'The JSON messages file that Extract to transglot key writes to and pushes, for example src/messages/en.json. Needed for the code action only.', type: 'string', }, i18nFunction: { description: 'The function the code action calls in place of the string.', type: 'string', default: '"t"', }, format: { description: 'The file format of messagesFile, used when it is pushed.', type: 'string', default: '"json_flat"', }, }} /> Keep the token out of files you commit. The examples below read it from the `TRANSGLOT_TOKEN` environment variable where the editor allows it. ## Set up your editor [#set-up-your-editor] ### Neovim [#neovim] Neovim 0.11 and later configure language servers with `vim.lsp.config`: ```lua title="init.lua" vim.lsp.config('transglot', { cmd = { 'transglot-lsp' }, filetypes = { 'json', 'javascript', 'javascriptreact', 'typescript', 'typescriptreact' }, root_markers = { 'transglot.json', '.git' }, init_options = { serverUrl = 'https://api.transglot.ai', token = os.getenv('TRANSGLOT_TOKEN'), messagesFile = 'src/messages/en.json', i18nFunction = 't', format = 'json_flat', }, }) vim.lsp.enable('transglot') ``` Use your usual LSP keymaps: hover with `K`, code actions with `gra`, diagnostics in the location list. ### JetBrains IDEs [#jetbrains-ides] IntelliJ IDEA, WebStorm, PhpStorm and the other JetBrains IDEs (2023.2 and later) run external language servers through the free [LSP4IJ](https://plugins.jetbrains.com/plugin/23257-lsp4ij) plugin. <Steps> <Step> ### Install LSP4IJ [#install-lsp4ij] In **Settings** → **Plugins** → **Marketplace**, search for LSP4IJ, install it and restart the IDE. </Step> <Step> ### Add the server [#add-the-server] Open **Settings** → **Languages & Frameworks** → **Language Servers**, select **+** and choose **New Language Server**. Set: - **Name:** `transglot` - **Command:** `transglot-lsp` - **Mappings** → **File name patterns:** `*.json`, `*.js`, `*.jsx`, `*.ts`, `*.tsx` </Step> <Step> ### Add the settings [#add-the-settings] Under **Configuration** → **Initialization Options**, paste: ```json { "serverUrl": "https://api.transglot.ai", "token": "tgl_xxx", "messagesFile": "src/messages/en.json", "i18nFunction": "t", "format": "json_flat" } ``` Replace `tgl_xxx` with your token and select **Apply**. LSP4IJ's **LSP Consoles** tool window shows the server's log if nothing appears. </Step> </Steps> If the IDE cannot find `transglot-lsp`, use the full path that `which transglot-lsp` prints as the command. ### Helix [#helix] ```toml title="languages.toml" [language-server.transglot] command = "transglot-lsp" [language-server.transglot.config] serverUrl = "https://api.transglot.ai" token = "tgl_xxx" messagesFile = "src/messages/en.json" [[language]] name = "json" language-servers = ["vscode-json-language-server", "transglot"] [[language]] name = "typescript" language-servers = ["typescript-language-server", "transglot"] ``` Keep each language's existing servers in its `language-servers` list and add `transglot`. ### Sublime Text [#sublime-text] With the [LSP](https://packagecontrol.io/packages/LSP) package installed, open **Preferences** → **Package Settings** → **LSP** → **Settings** and add a client: ```json title="LSP.sublime-settings" { "clients": { "transglot": { "enabled": true, "command": ["transglot-lsp"], "selector": "source.json | source.js | source.jsx | source.ts | source.tsx", "initializationOptions": { "serverUrl": "https://api.transglot.ai", "token": "tgl_xxx", "messagesFile": "src/messages/en.json" } } } } ``` ### Other editors [#other-editors] Any LSP client that can start a stdio server works: run `transglot-lsp` and pass the [settings](#settings) as initialization options. ## Commands [#commands] The server registers `transglot.pushKey`, which pushes `messagesFile` to the project. **Extract to transglot key** offers it as **Push key to transglot** after it writes a key, and when it succeeds the editor shows how many keys were created. ## Troubleshooting [#troubleshooting] Messages from the server appear in your editor's LSP log: | Message | Cause | | --- | --- | | `transglot: not connected. Set serverUrl and token in the LSP initializationOptions.` | One of the two required settings is missing. | | `transglot: could not load transglot.json: ...` | The file is missing from the workspace root, is invalid, or uses a field or format version 0.1.0 does not read. | | `transglot: pull fr failed: ...` | The token lacks **pull**, or the locale is not a language of the project. | # Create a webhook endpoint (https://transglot.ai/docs/developers/webhooks/create-a-webhook-endpoint/) You need to be an Admin of the workspace or an Owner of its account, on the Starter plan or higher. Build and deploy the receiving endpoint first, so it can answer the first delivery. ## In the console [#in-the-console] <Steps> <Step> ### Open the project's developer settings [#open-the-projects-developer-settings] Open the project, then **Settings** → **Developer**. The **Webhooks** section lists the project's endpoints. </Step> <Step> ### Start a new endpoint [#start-a-new-endpoint] Select **New endpoint**. The **Create webhook endpoint** dialog opens. </Step> <Step> ### Enter the URL [#enter-the-url] Enter the **Endpoint URL**, for example `https://ci.example.com/hooks/transglot`. It must be an HTTPS URL on the default port (443) whose host resolves to a public address. </Step> <Step> ### Choose the events [#choose-the-events] Under **Events**, tick each event the endpoint should receive. They are grouped as **Batches**, **Keys**, **Translations**, **Project** and **Quality**. The [event reference](https://transglot.ai/docs/developers/webhooks/events) says when each one fires. </Step> <Step> ### Create it and copy the secret [#create-it-and-copy-the-secret] Select **Create endpoint**. **Copy your signing secret** appears with the secret, which starts with `whsec_`. Select **Copy** and store it where your receiver can read it, for example as an environment variable. It is shown only once and cannot be recovered later. </Step> </Steps> The endpoint is now listed with its **Events**, when it **Last delivered**, an **Active** switch, and **Deliveries** and **Delete** actions. <Callout type="warn" title="Lost the secret?"> There is no way to show it again. Delete the endpoint and create a new one, then update your receiver with the new secret. </Callout> ## With the API [#with-the-api] `POST /v1/webhooks` creates an endpoint with a deploy token that has the `manage` ability. The response is the only place the secret appears. ```bash title="Create an endpoint" curl https://api.transglot.ai/v1/webhooks \ -H "Authorization: Bearer $TRANSGLOT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "url": "https://ci.example.com/hooks/transglot", "events": ["batch.completed", "batch.failed"] }' ``` ```json title="201 Created" { "id": 8, "url": "https://ci.example.com/hooks/transglot", "events": ["batch.completed", "batch.failed"], "active": true, "created_at": "2026-10-12T10:02:19+00:00", "last_delivered_at": null, "secret": "whsec_Zq8mB3vN6xR1tY4uI7oP0aS2dF5gH8jK9lQ3wE6r" } ``` `GET /v1/webhooks` lists the project's endpoints (never their secrets), and `DELETE /v1/webhooks/{id}` removes one. `GET /v1/webhooks/events` returns the event catalog with a representative payload for each event, which is useful for building a field mapper. See the [API reference](https://transglot.ai/docs/api-reference). ## Pause or delete an endpoint [#pause-or-delete-an-endpoint] - **Pause.** Turn the **Active** switch off. Events stop being sent to it until you switch it back on. Events that happen while it is off are not sent later. - **Delete.** Select **Delete**, then **Delete endpoint**. The endpoint stops receiving deliveries immediately, and events that fire while it is gone are not queued or replayed. ## If it does not work [#if-it-does-not-work] | What you see | Why | | --- | --- | | `The webhook URL must use https.` | The URL is `http://`. Only HTTPS is accepted | | `The webhook URL port 8443 is not allowed.` | Only port 443 is accepted | | `The webhook URL resolves to a private or reserved address.` | The host resolves to a private, loopback or otherwise internal address. Endpoints must be publicly reachable | | `The webhook URL host could not be resolved.` | The host name has no DNS record | | **Paused: the account plan does not include webhooks.** | The account's plan no longer includes webhooks. Endpoints are kept, and deliveries resume after an upgrade | | **Deliveries are paused.** | The workspace cannot send data out right now. An account owner can see why in the console | Through the API, each of the first four comes back as `422` with the type `/problems/webhook-url-not-allowed` and that sentence as `detail`. Once deliveries are flowing, the endpoint's delivery log shows each attempt and what your endpoint answered. See [Retries and redelivery](https://transglot.ai/docs/developers/webhooks/retries-and-redelivery). ## Related [#related] - [Verify signatures](https://transglot.ai/docs/developers/webhooks/verify-signatures) - [Event reference](https://transglot.ai/docs/developers/webhooks/events) - [Project developer settings](https://transglot.ai/docs/developers/project-developer-settings) # Event reference (https://transglot.ai/docs/developers/webhooks/events/) An endpoint receives only the events it subscribed to. The event name arrives in the `X-Transglot-Event` header and again as `event` in the body. | Event | Fires when | Delivery | | --- | --- | --- | | [`batch.completed`](#batchcompleted) | A translation run finished: `completed` or `completed_with_errors` | Once per run | | [`batch.failed`](#batchfailed) | A translation run ended `failed` or `cancelled` | Once per run | | [`key.created`](#keycreated) | Keys were created | Coalesced, 30 s | | [`key.updated`](#keyupdated) | Keys changed name, description, plural flag or length limit | Coalesced, 30 s | | [`key.deleted`](#keydeleted) | Keys were deleted | Coalesced, 30 s | | [`translation.updated`](#translationupdated) | Translation values were written | Coalesced, 15 s | | [`translation.reviewed`](#translationreviewed) | Translations were approved | Coalesced, 30 s | | [`language.added`](#languageadded) | A target language was added | Once | | [`language.removed`](#languageremoved) | A target language was removed | Once | | [`glossary.updated`](#glossaryupdated) | Glossary terms changed | Coalesced, 30 s | | [`qa.completed`](#qacompleted) | A quality check finished scoring translations | Coalesced, 60 s | | [`source_drift.repaired`](#source_driftrepaired) | Translations were repaired after their source changed | Coalesced, 60 s | ## The envelope [#the-envelope] Every payload carries these fields: <TypeTable type={{ event: { description: 'The event name.', type: 'string', required: true }, delivered_at: { description: 'When the event was emitted, ISO 8601. A redelivery repeats the original value.', type: 'string', required: true }, 'project.id': { description: 'The numeric project id.', type: 'integer', required: true }, 'project.slug': { description: 'The project slug.', type: 'string', required: true }, }} /> The examples on this page are formatted for reading. The body you receive is compact JSON and its keys can come in a different order, so read fields by name. ## Coalesced events [#coalesced-events] A coalesced event is not delivered once per occurrence. The first occurrence opens a window (15, 30 or 60 seconds, depending on the event). Every further occurrence in that window adds to a counter. When the window closes, your endpoint receives one delivery for the whole window, and the next occurrence opens a new window. <TypeTable type={{ count: { description: 'How many times the event happened in the window: keys created, values written, translations checked, and so on.', type: 'integer', required: true }, samples: { description: 'Up to 20 examples of what changed, each at most 120 characters. Usually key names or key:locale pairs; a bulk action or a push adds one label for the whole action, such as bulk:de or push:en.', type: 'string[]', required: true }, window_seconds: { description: 'The length of the window.', type: 'integer', required: true }, }} /> `samples` is for display and logging. To act on exactly what changed, use the event as a trigger and fetch the current state, for example with [`GET /v1/pull`](https://transglot.ai/docs/api-reference) or [`POST /v1/translations/batch`](https://transglot.ai/docs/api-reference). ## batch.completed [#batchcompleted] A translation run reached a successful final state: `completed`, or `completed_with_errors` when some pairs failed. Delivered exactly once per run, whatever started it (a push, the console, `POST /v1/translate`, a language being added, a retry). `batch` has the same shape as [`GET /v1/batches/{batch}`](https://transglot.ai/docs/api-reference). ```json title="batch.completed" { "event": "batch.completed", "delivered_at": "2026-10-12T09:41:00+00:00", "project": { "id": 12, "slug": "acme-web" }, "batch": { "id": 4821, "status": "completed", "trigger": "push", "total": 240, "completed": 240, "failed": 0, "skipped": 12, "skipped_reasons": { "human_value_stale": 12 }, "tm_served": 37, "language_breakdown": { "fr": 120, "de": 120 }, "started_at": "2026-10-12T09:39:13+00:00", "finished_at": "2026-10-12T09:41:00+00:00" } } ``` <TypeTable type={{ 'batch.status': { description: 'completed or completed_with_errors.', type: 'string' }, 'batch.trigger': { description: 'What started the run: manual_missing, manual_selected, auto_edit, language_added, retry_failed or push.', type: 'string' }, 'batch.total': { description: 'Key and locale pairs sent for translation.', type: 'integer' }, 'batch.completed': { description: 'Pairs the run finished processing.', type: 'integer' }, 'batch.failed': { description: 'Pairs that failed. Retry them with POST /v1/batches/{batch}/retry-failed.', type: 'integer' }, 'batch.skipped': { description: 'Pairs the run covered and did not write. A translation you have edited or reviewed is never overwritten by AI.', type: 'integer' }, 'batch.skipped_reasons': { description: 'Skipped pairs by reason: human_value_stands, human_value_stale (a person\'s translation whose source has since changed; only a person can update it) or source_moved (the source changed during the run; running again fixes it). null when nothing was skipped.', type: 'object | null' }, 'batch.tm_served': { description: 'Pairs answered from translation memory, not counted in total.', type: 'integer' }, 'batch.language_breakdown': { description: 'Pairs per target locale.', type: 'object | null' }, 'batch.started_at': { description: 'ISO 8601.', type: 'string | null' }, 'batch.finished_at': { description: 'ISO 8601.', type: 'string | null' }, }} /> ## batch.failed [#batchfailed] A translation run ended `failed`, or was `cancelled` from the console or with `POST /v1/batches/{batch}/cancel`. Delivered exactly once per run, with the same payload shape as `batch.completed`. ```json title="batch.failed" { "event": "batch.failed", "delivered_at": "2026-10-12T09:41:00+00:00", "project": { "id": 12, "slug": "acme-web" }, "batch": { "id": 4821, "status": "failed", "trigger": "push", "total": 240, "completed": 191, "failed": 49, "skipped": 0, "skipped_reasons": null, "tm_served": 37, "language_breakdown": { "fr": 120, "de": 120 }, "started_at": "2026-10-12T09:39:13+00:00", "finished_at": "2026-10-12T09:41:00+00:00" } } ``` To see which pairs failed and why, call `GET /v1/batches/{batch}?include=failures`. ## key.created [#keycreated] Keys were created in the console or by a source-locale push. Coalesced over 30 seconds. `count` is the number of keys created; `samples` holds key names, and `push:<locale>` for a push. ```json title="key.created" { "event": "key.created", "delivered_at": "2026-10-12T09:50:31+00:00", "project": { "id": 12, "slug": "acme-web" }, "count": 14, "samples": ["push:en"], "window_seconds": 30 } ``` ## key.updated [#keyupdated] Keys changed name, description, plural flag or length limit: an edit in the console, a bulk rename (sample `bulk-rename`), or a source push whose metadata sidecar changed descriptions or length limits (sample `push-metadata:<locale>`). Coalesced over 30 seconds. ```json title="key.updated" { "event": "key.updated", "delivered_at": "2026-10-12T10:12:30+00:00", "project": { "id": 12, "slug": "acme-web" }, "count": 2, "samples": ["checkout.submit", "checkout.error.declined"], "window_seconds": 30 } ``` ## key.deleted [#keydeleted] Keys were deleted: one at a time in the console (sample: the key name), in bulk (sample `bulk`), or by a source push with `mode=sync_removals` (sample `push:<locale>`). Coalesced over 30 seconds. ```json title="key.deleted" { "event": "key.deleted", "delivered_at": "2026-10-12T10:20:04+00:00", "project": { "id": 12, "slug": "acme-web" }, "count": 31, "samples": ["bulk"], "window_seconds": 30 } ``` ## translation.updated [#translationupdated] Translation values were written. Coalesced over 15 seconds. `count` counts written values only: a pair a translation run left alone, because a person had already edited or reviewed it or because its source moved during the run, writes nothing and is not counted. | Source of the write | Sample | | --- | --- | | An edit in the console editor | `<key>:<locale>`, for example `checkout.submit:fr` | | A push (source or target) | `push:<locale>` | | A translation run | `batch:<batch id>:<locale>` | | Copying the source into a locale in bulk | `bulk-copy-source:<locale>` | ```json title="translation.updated" { "event": "translation.updated", "delivered_at": "2026-10-12T09:41:12+00:00", "project": { "id": 12, "slug": "acme-web" }, "count": 240, "samples": ["batch:4821:fr", "batch:4821:de"], "window_seconds": 15 } ``` <Callout type="info" title="Writes through the API"> A value written with `PUT /v1/translations` does not fire `translation.updated`. If your own integration writes values that way, you already know about the change. </Callout> ## translation.reviewed [#translationreviewed] Translations were approved in the review workflow. Coalesced over 30 seconds. `samples` holds `<key>:<locale>` for a single approval and `bulk:<locale>` for a bulk approval. ```json title="translation.reviewed" { "event": "translation.reviewed", "delivered_at": "2026-10-12T11:02:45+00:00", "project": { "id": 12, "slug": "acme-web" }, "count": 37, "samples": ["checkout.submit:fr", "bulk:de"], "window_seconds": 30 } ``` ## language.added [#languageadded] A target language was added to the project. Delivered once, as soon as the language exists. A translation run to fill it may follow and reports through the `batch.*` events. ```json title="language.added" { "event": "language.added", "delivered_at": "2026-10-12T12:00:00+00:00", "project": { "id": 12, "slug": "acme-web" }, "language": { "locale": "pt-BR" } } ``` ## language.removed [#languageremoved] A target language was removed from the project. Every translation in that locale and its version history were deleted with it, and neither can be recovered through the product. Delivered once. React to it by dropping the locale from your builds, your CDN configuration or your CMS. ```json title="language.removed" { "event": "language.removed", "delivered_at": "2026-10-12T12:30:00+00:00", "project": { "id": 12, "slug": "acme-web" }, "language": { "locale": "pt-BR", "translations_deleted": 1204 } } ``` `language.translations_deleted` tells you whether the locale held content or was an empty placeholder. ## glossary.updated [#glossaryupdated] Glossary terms were created, edited, approved, rejected, imported or deleted in the console. Coalesced over 30 seconds. `samples` holds source terms, and `import` for a file import, whose `count` is the number of terms it created or updated. ```json title="glossary.updated" { "event": "glossary.updated", "delivered_at": "2026-10-12T13:05:30+00:00", "project": { "id": 12, "slug": "acme-web" }, "count": 3, "samples": ["Checkout", "Acme"], "window_seconds": 30 } ``` Changes made through the glossary API endpoints do not fire this event. ## qa.completed [#qacompleted] A quality check finished scoring translations. A check is processed in groups, and the 60-second window collapses a whole check into one delivery. `count` is the number of translations checked. `samples` holds flagged `<key>:<locale>` pairs, at most one per group, so an empty `samples` means nothing was flagged. The findings themselves are on the translations: read them with `GET /v1/translations` or in the editor. ```json title="qa.completed" { "event": "qa.completed", "delivered_at": "2026-10-12T14:01:00+00:00", "project": { "id": 12, "slug": "acme-web" }, "count": 74, "samples": ["checkout.error.declined:de"], "window_seconds": 60 } ``` ## source_drift.repaired [#source_driftrepaired] Source strings changed, and the translations that depended on them were checked again and repaired, either as a suggestion or applied directly, according to the project's source-drift setting. Coalesced over 60 seconds. `count` is the number of translations repaired; `samples` holds `<key>:<locale>` pairs. ```json title="source_drift.repaired" { "event": "source_drift.repaired", "delivered_at": "2026-10-12T15:10:00+00:00", "project": { "id": 12, "slug": "acme-web" }, "count": 6, "samples": ["checkout.submit:fr", "checkout.submit:de"], "window_seconds": 60 } ``` ## Related [#related] - [Verify signatures](https://transglot.ai/docs/developers/webhooks/verify-signatures) - [Retries and redelivery](https://transglot.ai/docs/developers/webhooks/retries-and-redelivery) - [Create a webhook endpoint](https://transglot.ai/docs/developers/webhooks/create-a-webhook-endpoint) # Webhooks (https://transglot.ai/docs/developers/webhooks/) A webhook endpoint is an HTTPS URL of yours that transglot sends a `POST` to when something happens in a project: a translation run finishes, keys are created, a language is removed. Use webhooks to start a deploy when translations land, rebuild a static site, invalidate a cache or post to a channel, without polling the API. ## How it works [#how-it-works] - **Per project.** Endpoints belong to one project. A project can have several, each subscribed to its own set of events. - **You choose the events.** An endpoint receives only the events it subscribed to. There are twelve; see the [event reference](https://transglot.ai/docs/developers/webhooks/events). - **Signed.** Every delivery carries an `X-Transglot-Signature` header, an HMAC-SHA256 of the timestamp and body keyed with the endpoint's signing secret. [Verify it](https://transglot.ai/docs/developers/webhooks/verify-signatures) before you act on a delivery. - **Coalesced where it matters.** High-frequency events are counted over a short window and delivered once, carrying a `count` and up to 20 samples. A bulk edit of 5,000 strings becomes one request, not 5,000. - **Retried.** A delivery that does not get a `2xx` within 10 seconds is retried three more times over about six minutes. Every attempt is in the endpoint's delivery log, where you can redeliver it. See [Retries and redelivery](https://transglot.ai/docs/developers/webhooks/retries-and-redelivery). ## A delivery [#a-delivery] ```http title="POST https://ci.example.com/hooks/transglot" Content-Type: application/json User-Agent: transglot-webhooks/1 X-Transglot-Event: batch.completed X-Transglot-Delivery: 0f6d1c0e-6a4b-4f7e-9a51-3c2b8d7e4a19 X-Transglot-Timestamp: 1791820860 X-Transglot-Signature: sha256=5d1c3e8f0a2b4c6d8e0f1a3b5c7d9e1f3a5b7c9d1e3f5a7b9c1d3e5f7a9b1c3d {"batch":{"id":4821,"total":240,"failed":0,"status":"completed","skipped":12,"trigger":"push","completed":240,"tm_served":37,"started_at":"2026-10-12T09:39:13+00:00","finished_at":"2026-10-12T09:41:00+00:00","skipped_reasons":{"human_value_stale":12},"language_breakdown":{"de":120,"fr":120}},"event":"batch.completed","project":{"id":12,"slug":"acme-web"},"delivered_at":"2026-10-12T09:41:00+00:00"} ``` Every payload has the same three fields, `event`, `delivered_at` and `project` (`id` and `slug`), plus fields that depend on the event. The body is compact JSON and the order of its keys is not meaningful: read fields by name, and verify the signature against the raw bytes you received. ## Requirements [#requirements] - **Plan.** Webhooks need the Starter plan or higher. If the account's plan stops including them, deliveries stop until it does again; endpoints are kept. - **Permission.** Owners and admins create and manage endpoints. - **A public HTTPS URL.** Endpoints must be publicly reachable HTTPS URLs on port 443; private and internal addresses are refused. The address is checked when you create the endpoint and again before every delivery. ## Get started [#get-started] <Cards> <Card title="Create a webhook endpoint" href="/developers/webhooks/create-a-webhook-endpoint" description="In the console or with the API, and where the signing secret appears." /> <Card title="Event reference" href="/developers/webhooks/events" description="Every event, when it fires and its payload." /> <Card title="Verify signatures" href="/developers/webhooks/verify-signatures" description="Check a delivery is genuine, in Node.js, Python and PHP." /> <Card title="Retries and redelivery" href="/developers/webhooks/retries-and-redelivery" description="The retry schedule, the delivery log and manual redelivery." /> </Cards> ## Related [#related] - [Zapier, Make and n8n](https://transglot.ai/docs/integrations/automation/zapier-make-and-n8n) - [REST API overview](https://transglot.ai/docs/developers/api) # Retries and redelivery (https://transglot.ai/docs/developers/webhooks/retries-and-redelivery/) ## What counts as delivered [#what-counts-as-delivered] A delivery succeeds when your endpoint answers with any `2xx` status within 10 seconds. Everything else is a failed attempt: - any `3xx`, `4xx` or `5xx` status (redirects are not followed, so a `301` to a new address fails); - no answer within 10 seconds; - a connection or TLS error. Answer first and do slow work afterwards. A receiver that processes the event before responding risks the 10-second limit and a retry of something it already handled. ## The retry schedule [#the-retry-schedule] Each delivery gets four attempts: the first, then three retries. | Attempt | When | | --- | --- | | 1 | As soon as the event is emitted | | 2 | 10 seconds after attempt 1 fails | | 3 | 60 seconds after attempt 2 fails | | 4 | 5 minutes after attempt 3 fails | Every attempt of one delivery carries the same body and the same `X-Transglot-Delivery` id, with a fresh `X-Transglot-Timestamp` and `X-Transglot-Signature`. After the fourth failure, the delivery is marked `failed` and is not tried again. When a delivery fails for good, transglot emails the workspace's developers and admins, **A webhook endpoint is failing**, naming the endpoint, the event and the last error. You get at most one of these a day per endpoint. <Callout type="warn" title="Missed events are not queued"> Deliveries are not held for later. Once a delivery has failed, the event it carried reaches your endpoint only if you redeliver it from the log. The same is true for events that happen while an endpoint is paused or deleted: they are not sent afterwards. </Callout> ## Deliveries that are not attempted [#deliveries-that-are-not-attempted] | Situation | What happens | | --- | --- | | The endpoint was paused after the event was emitted | The delivery is marked `failed` without a request | | The endpoint was deleted | Nothing more is sent, and its delivery log is deleted with it | | The account's plan no longer includes webhooks, or the workspace cannot send data out | No delivery is created, and pending ones are marked `failed` without a request | | The endpoint's host now resolves to a private or internal address | The delivery is refused before sending, marked `failed` and not retried. transglot emails **A webhook endpoint was refused** | Endpoints must be publicly reachable HTTPS URLs; private and internal addresses are refused. The address is checked when the endpoint is created and again immediately before every attempt, so a host whose DNS later points inside a private network is still refused. ## The delivery log [#the-delivery-log] In the console, open **Settings** → **Developer**, find the endpoint under **Webhooks** and select **Deliveries**. **Recent deliveries** lists every attempt sequence for that endpoint, newest first: | Column | Shows | | --- | --- | | **Event** | The event name | | **Status** | `pending`, `delivering`, `succeeded` or `failed` | | **Code** | The HTTP status your endpoint returned on the latest attempt | | **Attempts** | How many attempts have been made | | **Next retry** | When the next attempt is due, while retries remain | | **When** | When it was delivered, or when it was first dispatched | Filter by **Delivery status** to see only failures, and select **Load more** for older entries. ## Redeliver an event [#redeliver-an-event] Select **Redeliver** on any entry in the log. transglot queues a new delivery of the same event to the same endpoint and confirms: "Redelivery queued. The new attempt appears at the top of this delivery log." A redelivery: - carries **byte-for-byte the same body** as the original, including its original `delivered_at`; - gets a **new** `X-Transglot-Delivery` id, a fresh timestamp and a fresh signature; - follows the same retry schedule; - fails without a request if the endpoint is paused, so switch it back on first. Redelivery is the quickest way to debug a receiver: fix it, then redeliver the failed entry instead of waiting for the event to happen again. ## Handling duplicates and order [#handling-duplicates-and-order] Design the receiver to tolerate the same event more than once and events out of order: - **Duplicates.** A retry repeats the same `X-Transglot-Delivery` id, so store the ids you have processed and skip repeats. A redelivery has a new id; when you redeliver on purpose, you usually want it processed. - **Order.** Deliveries are sent independently and can arrive in any order. Do not infer state from arrival order: read `batch.status` and `batch.finished_at`, or fetch the current state from the API. - **Coalesced events.** A `count` covers one window. Two windows of the same event are two separate deliveries. ```js title="Idempotent handler" const seen = new Set(); // use a database or cache with a TTL in production export async function handle(deliveryId, payload) { if (seen.has(deliveryId)) return; // a retry of something already processed seen.add(deliveryId); if (payload.event === 'batch.completed') { await triggerDeploy(payload.project.slug, payload.batch.id); // your own deploy step } } ``` ## Related [#related] - [Verify signatures](https://transglot.ai/docs/developers/webhooks/verify-signatures) - [Event reference](https://transglot.ai/docs/developers/webhooks/events) - [Create a webhook endpoint](https://transglot.ai/docs/developers/webhooks/create-a-webhook-endpoint) # Verify signatures (https://transglot.ai/docs/developers/webhooks/verify-signatures/) Anyone who learns your endpoint's URL can send it a request. The signature is what tells a genuine delivery from everything else, so verify it on every request and reject the ones that fail. ## The headers [#the-headers] | Header | Value | | --- | --- | | `X-Transglot-Signature` | `sha256=` followed by the lowercase hex HMAC-SHA256 of the signed message | | `X-Transglot-Timestamp` | Unix time in seconds when this attempt was signed, for example `1791820860` | | `X-Transglot-Event` | The event name, for example `batch.completed` | | `X-Transglot-Delivery` | A UUID for the delivery | ## How the signature is computed [#how-the-signature-is-computed] <Steps> <Step> ### Build the signed message [#build-the-signed-message] Join the `X-Transglot-Timestamp` value, a full stop, and the raw request body: ```text title="Signed message" 1791820860.{"batch":{"id":4821, ... },"event":"batch.completed", ... } ``` </Step> <Step> ### Compute the HMAC [#compute-the-hmac] Compute HMAC-SHA256 of that message, keyed with the endpoint's whole signing secret, including the `whsec_` prefix. Hex-encode the result in lowercase and prefix it with `sha256=`. </Step> <Step> ### Compare [#compare] Compare your value with `X-Transglot-Signature` using a constant-time comparison. If they differ, reject the request. </Step> </Steps> <Callout type="warn" title="Use the raw body"> Sign the exact bytes you received, before any JSON parsing. Parsing and re-serialising changes whitespace, key order and escaping, and the signature will not match. The body is UTF-8 JSON with slashes and non-ASCII characters unescaped. </Callout> ## Reject old timestamps [#reject-old-timestamps] Each attempt is signed when it is sent, so `X-Transglot-Timestamp` is always recent for a genuine delivery, including a retry or a redelivery. Reject a timestamp more than five minutes from your clock: that stops someone who captured a delivery from replaying it later. Because the timestamp is part of the signed message, it cannot be changed without breaking the signature. ## Verification code [#verification-code] Each function returns `true` only for a genuine, recent delivery. Store the secret in an environment variable such as `TRANSGLOT_WEBHOOK_SECRET`. ```js tab="Node.js" title="verify.mjs" import crypto from 'node:crypto'; const TOLERANCE_SECONDS = 300; export function verifyTransglotSignature(rawBody, timestamp, signature, secret) { if (!timestamp || !signature) return false; const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp)); if (!Number.isFinite(age) || age > TOLERANCE_SECONDS) return false; const expected = 'sha256=' + crypto .createHmac('sha256', secret) .update(`${timestamp}.`) .update(rawBody) .digest('hex'); const a = Buffer.from(expected); const b = Buffer.from(signature); return a.length === b.length && crypto.timingSafeEqual(a, b); } ``` ```python tab="Python" title="verify.py" import hashlib import hmac import time from typing import Optional TOLERANCE_SECONDS = 300 def verify_transglot_signature(raw_body: bytes, timestamp: Optional[str], signature: Optional[str], secret: str) -> bool: if not timestamp or not signature: return False try: age = abs(int(time.time()) - int(timestamp)) except ValueError: return False if age > TOLERANCE_SECONDS: return False expected = "sha256=" + hmac.new( secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256, ).hexdigest() return hmac.compare_digest(expected.encode(), signature.encode()) ``` ```php tab="PHP" title="verify.php" <?php function verifyTransglotSignature(string $rawBody, ?string $timestamp, ?string $signature, string $secret, int $tolerance = 300): bool { if ($timestamp === null || $signature === null || ! ctype_digit($timestamp)) { return false; } if (abs(time() - (int) $timestamp) > $tolerance) { return false; } $expected = 'sha256='.hash_hmac('sha256', $timestamp.'.'.$rawBody, $secret); return hash_equals($expected, $signature); } ``` ## Wiring it into a receiver [#wiring-it-into-a-receiver] The receiver has to hand the function the raw body. Then answer with a `2xx` quickly and do slow work afterwards: a delivery that takes longer than 10 seconds to answer counts as failed and is retried. ```js tab="Node.js" title="server.mjs (Express)" import express from 'express'; import { verifyTransglotSignature } from './verify.mjs'; const app = express(); // express.raw keeps req.body as the unparsed Buffer the signature covers. app.post('/hooks/transglot', express.raw({ type: 'application/json' }), (req, res) => { const genuine = verifyTransglotSignature( req.body, req.get('X-Transglot-Timestamp'), req.get('X-Transglot-Signature'), process.env.TRANSGLOT_WEBHOOK_SECRET, ); if (!genuine) return res.status(400).send('invalid signature'); const payload = JSON.parse(req.body.toString('utf8')); res.sendStatus(204); // Handle payload.event after answering. }); app.listen(3000); ``` ```python tab="Python" title="app.py (Flask)" import os from flask import Flask, abort, request from verify import verify_transglot_signature app = Flask(__name__) @app.post("/hooks/transglot") def transglot_webhook(): raw_body = request.get_data() # the unparsed bytes the signature covers if not verify_transglot_signature( raw_body, request.headers.get("X-Transglot-Timestamp"), request.headers.get("X-Transglot-Signature"), os.environ["TRANSGLOT_WEBHOOK_SECRET"], ): abort(400) payload = request.get_json() # Queue payload["event"] for processing, then answer. return "", 204 ``` ```php tab="PHP" title="webhook.php" <?php require __DIR__.'/verify.php'; $rawBody = file_get_contents('php://input'); // the unparsed bytes the signature covers $genuine = verifyTransglotSignature( $rawBody, $_SERVER['HTTP_X_TRANSGLOT_TIMESTAMP'] ?? null, $_SERVER['HTTP_X_TRANSGLOT_SIGNATURE'] ?? null, (string) getenv('TRANSGLOT_WEBHOOK_SECRET'), ); if (! $genuine) { http_response_code(400); exit; } $payload = json_decode($rawBody, true); // Queue $payload['event'] for processing, then answer. http_response_code(204); ``` ## Check a signature by hand [#check-a-signature-by-hand] To debug a mismatch, recompute the signature from a delivery you captured. Save the body exactly as received to `body.json`, then: ```bash title="Recompute with openssl" TIMESTAMP=1791820860 # the X-Transglot-Timestamp header printf '%s.%s' "$TIMESTAMP" "$(cat body.json)" \ | openssl dgst -sha256 -hmac "$TRANSGLOT_WEBHOOK_SECRET" \ | sed 's/^.*= /sha256=/' ``` The output should equal the `X-Transglot-Signature` header. If it does not, the usual causes are a body that was parsed and re-serialised before verifying, a secret copied without its `whsec_` prefix, or the secret of a different endpoint. ## Related [#related] - [Retries and redelivery](https://transglot.ai/docs/developers/webhooks/retries-and-redelivery) - [Event reference](https://transglot.ai/docs/developers/webhooks/events) - [Create a webhook endpoint](https://transglot.ai/docs/developers/webhooks/create-a-webhook-endpoint) # Add a progress badge to your README (https://transglot.ai/docs/guides/delivery/add-a-progress-badge-to-your-readme/) The badge is a small image that shows your project's name and how many languages it is translated into, for example **my-library | 12 languages**. The count is the project's target languages; the source language is not counted. It updates on its own when you add or remove a language. <Callout title="Open-source plan only"> The badge is part of the [open-source plan](https://transglot.ai/docs/administration/billing/open-source-plan). When a workspace is granted the plan, the badge is switched on for the projects the workspace has at that point. There is no setting in the console to switch it on or off for a single project. </Callout> ## What the badge makes public [#what-the-badge-makes-public] The badge shows exactly two things: the project's name and its number of target languages. It shows no strings, no translations, and nothing about your other projects. ## Add the badge [#add-the-badge] <Steps> <Step> ### Find your workspace and project slugs [#find-your-workspace-and-project-slugs] They are in the console address of the project: `https://app.transglot.ai/orgs/{workspace}/projects/{project}`. </Step> <Step> ### Build the badge address [#build-the-badge-address] ```text title="Badge address" https://api.transglot.ai/badge/{workspace}/{project}.svg ``` Open it in a browser to check it renders. </Step> <Step> ### Add it to your README [#add-it-to-your-readme] ```md title="README.md" [![Localized with transglot](https://api.transglot.ai/badge/acme/my-library.svg)](https://transglot.ai) ``` </Step> </Steps> ## Caching [#caching] The badge is cached for five minutes and revalidated with an `ETag`. After you add a language, it can take a few minutes to show through an image proxy such as GitHub's. ## If it doesn't work [#if-it-doesnt-work] - **The badge returns `404`.** A project without the badge switched on returns the same `404` as a project that does not exist, so nobody can use the badge to discover private project names. Check the slugs in the address, and that the workspace is on the open-source plan. - **The badge stopped working after a rename.** The badge address uses the project's slug. Changing the project's URL in its settings breaks badges that use the old one; update the address in your README. - **The badge disappeared.** If the open-source grant ends, the badge is switched off for the workspace's projects. # Connect your app to the CDN (https://transglot.ai/docs/guides/delivery/connect-your-app-to-the-cdn/) Your app fetches the published version of each language from the CDN when it needs it, instead of carrying translation files in its build. After that, publishing in transglot is enough to change what users see. <Callout title="Before you start"> You need a plan that includes runtime delivery (Starter and above), and an Owner or Admin to create the CDN key. Wiring up the app is a developer task. </Callout> ## Connect [#connect] <Steps> <Step> ### Publish at least one language [#publish-at-least-one-language] The CDN only serves published versions. Publish a language from **Releases** first; see [Publish a locale](https://transglot.ai/docs/guides/delivery/publish-a-locale). Until then every request for it returns `404 not-published`. </Step> <Step> ### Create a CDN key [#create-a-cdn-key] In the project's **Settings → Developer → Runtime delivery (CDN)**, enter a **Name** under **CDN keys** and select **New CDN key**. Copy the key from **Your new CDN key**: it is shown once. See [CDN keys](https://transglot.ai/docs/guides/delivery/the-cdn#cdn-keys). </Step> <Step> ### Note the project id [#note-the-project-id] The same card shows each language's **Latest URL**, for example `https://api.transglot.ai/v1/cdn/42/de`. The number after `/cdn/` is the project id the SDKs ask for. </Step> <Step> ### Install the runtime package [#install-the-runtime-package] Use the package for your framework (see [the runtime SDKs](https://transglot.ai/docs/developers/sdks)), or the framework-agnostic client: ```npm npm install @transglot/runtime ``` </Step> <Step> ### Configure it [#configure-it] Give the client the API address, the project id, the CDN key and the starting language: ```ts title="i18n.ts" import { createClient } from '@transglot/runtime'; export const i18n = createClient({ baseUrl: 'https://api.transglot.ai', project: 42, cdnKey: 'taicdn_...', locale: 'de', }); await i18n.loadLocale('de'); i18n.t('home.title'); ``` The client caches each language in memory and revalidates it with the `ETag`, so a repeat load that finds nothing new costs an empty `304`. Full options are on [the JavaScript SDK page](https://transglot.ai/docs/developers/sdks/javascript). </Step> <Step> ### Deploy your app [#deploy-your-app] From now on, a publish in transglot reaches users without a rebuild. </Step> </Steps> ## Choose latest or a pinned version [#choose-latest-or-a-pinned-version] By default the SDKs read the **latest** address, so each publish reaches users within about a minute (the latest address may be cached for up to 60 seconds). If you want a change to go live only when you ship, pin a version instead (the runtime client takes a `version` option). A pinned version never changes, so you move to a new one deliberately. See [Latest and versioned addresses](https://transglot.ai/docs/guides/delivery/the-cdn#latest-and-versioned-addresses). ## Things to know [#things-to-know] - **Use a JSON delivery format.** The runtime SDKs read `json_flat`, `json_nested`, `laravel_json` and `flutter_arb`. Set **Delivery format** in **Settings → Developer** if your project's default is something else. - **Published versions are sparse.** A key nobody has translated yet is not in the version. The runtime client returns the key itself for a missing string; to show your source text instead, load the source language underneath. The [SDK pages](https://transglot.ai/docs/developers/sdks) show how for each framework. - **The CDN key is read-only.** It can read what you already published and nothing else, so it can sit in client-side code. To keep it out of the browser altogether, fetch on your server. - **Do not use a deploy token here.** Deploy tokens are secrets with write access. The CDN only accepts CDN keys. ## If it doesn't work [#if-it-doesnt-work] | What you see | What to check | | --- | --- | | `404 not-published` | The language has not been published yet, or you pinned a version older than the 20 kept. | | `404 not-found` | The project id in the address is not the key's project, or the language is not in the project. | | `401 cdn-key-invalid` | The key was deleted or mistyped. Create a new one. | | `403 plan-feature-required` | The account's plan no longer includes runtime delivery. | | Old text after a publish | See [Why are my translations not reaching my app?](https://transglot.ai/docs/guides/delivery/why-are-my-translations-not-reaching-my-app) | # Publish a locale (https://transglot.ai/docs/guides/delivery/publish-a-locale/) Publishing takes one language as it stands right now, freezes it into a new numbered version, and makes that version the one the CDN serves. Your translations stay editable in transglot; the published version does not change after the fact. Each language is published on its own, so a finished language never waits for an unfinished one. <Callout title="Who can publish"> Owners and Admins, on a plan that includes runtime delivery (Starter and above). An Admin whose access is limited to some languages can publish those languages only. </Callout> ## Publish from the Releases screen [#publish-from-the-releases-screen] <Steps> <Step> ### Open Releases [#open-releases] Open the project and select **Releases** in the sidebar. </Step> <Step> ### Check the language is ready [#check-the-language-is-ready] In **Departures**, the language's **Status** should read **ready to publish** (or **published**, to release it again). If it reads **in progress**, **not translated** or **waiting for approval**, use the row's **Next** action to finish it first. See [The Releases screen](https://transglot.ai/docs/guides/delivery/the-deploy-screen#departures). </Step> <Step> ### Start the publish [#start-the-publish] Select **publish** in the language's row, or the **Publish German →** button in the page header (it names the language the page suggests). </Step> <Step> ### Confirm [#confirm] The confirmation, **Publish German?**, states the version number the language will freeze at and what goes into it. Select **Publish German**. </Step> </Steps> A notice confirms the result, for example **"de published as version 4."** The row's **Live** column and the **Release log** update, and your app starts receiving the new version (see [how quickly](https://transglot.ai/docs/guides/delivery/the-cdn#caching)). If nothing in the language has changed since its live version, no new version is made: the notice reads **"de is already live at version 3. Nothing has changed since it was published."** ## What goes into a version [#what-goes-into-a-version] | Included | Not included | | --- | --- | | Every string someone wrote, edited, uploaded or approved | Strings with no translation yet. Your app's runtime handles missing keys (see [the SDKs](https://transglot.ai/docs/developers/sdks)). | | AI drafts, unless the project holds them back (see below) | AI drafts, when the project holds them back | | Flagged strings: a review flag does not stop a publish | | Two more cases leave strings out: - If the project's **Message syntax** check setting is **ICU**, a translation that is not valid ICU MessageFormat is left out, and the notice says so: **"1 key is left out of the bundle because its translation is not valid ICU MessageFormat."** Fix the translation and publish again. - Strings the delivery format cannot represent are left out (for example, plural strings in a format with no plural support). See [Delivery settings](https://transglot.ai/docs/guides/delivery/the-cdn#delivery-settings) to choose the format. If a nested format cannot represent your key names (one key is both a value and a parent of another key), the publish is refused with **"de was not published."** followed by the reason, and the live version stays as it was. ## Choose whether AI drafts are published [#choose-whether-ai-drafts-are-published] An AI draft is a translation Glossa wrote that nobody has approved or edited yet. Each project has one rule for them: - **Yes, publish drafts too**: drafts, edits and approved strings all go in. This is the default. - **No, hold drafts back**: approved strings and the text your team wrote or uploaded go in. Drafts wait until someone approves or edits them. While a language still has drafts, its status reads **waiting for approval**. The first time someone publishes from **Releases**, the confirmation asks **"Can GLOSSA drafts go to your website before someone approves them?"** It is asked once per project. To change the answer later, select **Change** next to **Drafts** in the **This release** card. A change takes effect at each language's next publish; nothing that is live now is removed or rewritten. Only Owners and Admins with access to every language in the project can set the rule. ## Other ways to publish [#other-ways-to-publish] <Tabs items={['Settings → Developer', 'From CI', 'On every push']}> <Tab value="Settings → Developer"> **Settings → Developer → Runtime delivery (CDN)** lists every language in the project, including the source language, each with **Publish now**. This publishes the language as it stands, whatever its status on **Releases**, so check the language before you use it. </Tab> <Tab value="From CI"> Call the publish endpoint with a [deploy token](https://transglot.ai/docs/developers/deploy-tokens) that has the `manage` ability: ```bash title="Terminal" curl -X POST https://api.transglot.ai/v1/cdn/publish \ -H "Authorization: Bearer $TRANSGLOT_TOKEN" \ -H "Content-Type: application/json" \ -d '{"locale": "de"}' ``` The response includes `version`, `created` (`false` when nothing changed and the live version was kept), `key_count`, `omitted_invalid_icu_keys`, and the two delivery addresses, `immutable_url` and `latest_url`, so a pipeline can pin the new version or purge a cache in front of it. </Tab> <Tab value="On every push"> Set **Freshness** to **Auto: publish on every push** in **Settings → Developer**. Each push then publishes the language it uploaded, at the moment of the push. Translations that arrive later, such as the AI translation a push of source strings starts, reach your app at that language's next publish. </Tab> </Tabs> ## Roll back [#roll-back] Versions are immutable, and transglot keeps the 20 most recent versions of each language. To serve an earlier version, point your app at that version's address instead of the latest one. See [Latest and versioned addresses](https://transglot.ai/docs/guides/delivery/the-cdn#latest-and-versioned-addresses). ## If it doesn't work [#if-it-doesnt-work] - **"Not available on this account’s plan."**: runtime publishing needs Starter or above. Readiness and the release log still work. - **"A workspace admin publishes runtime versions."**: your role cannot publish. Ask an Owner or Admin. - **No source strings yet** or **No target languages yet**: there is nothing to publish. Import strings or add a language first. - **Your app still shows the old text**: see [Why are my translations not reaching my app?](https://transglot.ai/docs/guides/delivery/why-are-my-translations-not-reaching-my-app) # The CDN (https://transglot.ai/docs/guides/delivery/the-cdn/) The CDN serves the versions you [publish](https://transglot.ai/docs/guides/delivery/publish-a-locale) over plain HTTPS, so your app can load translations at runtime and a corrected string reaches production without a new build. It only ever serves published versions. Work in progress in the editor is never reachable through it. Runtime delivery is included on Starter and above. Owners and Admins manage it from **Settings → Developer → Runtime delivery (CDN)**. ## Latest and versioned addresses [#latest-and-versioned-addresses] Every published language has two addresses on the API host: ```text title="Delivery addresses" https://api.transglot.ai/v1/cdn/{project-id}/{locale} # latest https://api.transglot.ai/v1/cdn/{project-id}/{locale}/v{n} # one version ``` `{project-id}` is the project's numeric id. You do not need to look it up: **Settings → Developer → Runtime delivery (CDN)** shows each language's **Latest URL**, and its **Immutable URL** once it has been published, ready to copy. - The **latest** address always serves the newest published version of that language. Point your app here when you want every publish to reach users without a deploy. - A **versioned** address serves exactly that version, forever. Point your app here when you want to control when a change goes live, or to roll back to an earlier version. transglot keeps the 20 most recent versions of each language. A versioned address older than that returns `404`. ## Caching [#caching] | Address | `Cache-Control` | What it means for you | | --- | --- | --- | | Latest | `public, max-age=60, must-revalidate` | A cache may serve the previous version for up to a minute after a publish, then checks again. | | Versioned | `public, max-age=31536000, immutable` | Cached for a year and never rechecked, because its content can never change. | Every response carries an `ETag`. A client or cache that sends it back in `If-None-Match` gets an empty `304 Not Modified` when nothing has changed, so revalidating the latest address is cheap. Responses also carry `X-Cdn-Version` (the version served) and `X-Cdn-Format` (its file format). Published versions are public content meant to be fetched from any app, so responses allow every origin (`Access-Control-Allow-Origin: *`). Requests are rate limited per key and IP address. Let a CDN of your own, or the runtime SDK's cache, absorb repeat reads instead of fetching on every page view. ## CDN keys [#cdn-keys] Every request needs a **CDN key** for the project. It is the narrowest credential transglot issues: it reads this project's published versions and nothing else, and it cannot change anything. Send it in the `X-Transglot-Cdn-Key` header, or as a `?key=` query parameter where you cannot set headers. ```bash title="Terminal" curl https://api.transglot.ai/v1/cdn/42/de \ -H "X-Transglot-Cdn-Key: taicdn_..." ``` A CDN key is not a deploy token. Deploy tokens are secrets that push and pull strings; a CDN key only reads what you already chose to publish. ### Create a key [#create-a-key] <Steps> <Step> ### Open the delivery settings [#open-the-delivery-settings] Open the project's **Settings → Developer** and scroll to **Runtime delivery (CDN)**. </Step> <Step> ### Name the key [#name-the-key] Under **CDN keys**, enter a **Name** that says where the key is used, such as the CDN or app that will hold it. </Step> <Step> ### Create and copy it [#create-and-copy-it] Select **New CDN key**. The key is shown once, in **Your new CDN key**: copy it before you close the dialog. It is stored hashed and cannot be shown again; if you lose it, create another. </Step> </Steps> ### Delete a key [#delete-a-key] Select **Delete** next to the key and type its name to confirm. Anything still fetching with that key gets `401` straight away, including caches you do not control, so roll out a replacement first if the key serves production. ## Delivery settings [#delivery-settings] Two settings in **Runtime delivery (CDN)** apply to every language in the project. Change them, then select **Save settings**. | Setting | Options | | --- | --- | | **Freshness** | **Manual: publish explicitly** (the default): a version is made only when someone publishes. **Auto: publish on every push**: each push publishes the language it uploaded. | | **Delivery format** | **Project default**, which follows the format your project last pushed (or flat JSON if it has never pushed), or a specific format. | A version is published in one format. A new format applies from each language's next publish. The runtime SDKs read the JSON formats (`json_flat`, `json_nested`, `laravel_json`, `flutter_arb`); see [the SDKs](https://transglot.ai/docs/developers/sdks). You can ask for a specific format with `?format=`. If it is not the format the version was published in, the CDN returns `404` rather than converting it. ## Errors [#errors] Errors are JSON problem responses. The `type` field names the problem, for example `/problems/cdn-key-invalid`. | Status | Slug | Cause | | --- | --- | --- | | `401` | `cdn-key-required` | No key in the header or the query string. | | `401` | `cdn-key-invalid` | The key does not exist or was deleted. | | `404` | `not-found` | The project id is not the key's project, or the language is not in the project. | | `404` | `not-published` | The language has never been published, the version is older than the 20 kept, or the requested `format` is not the published one. | | `403` | `plan-feature-required` | The account's plan no longer includes runtime delivery. Your keys are kept and work again after an upgrade. | | `403` | `workspace-closed` | The workspace has been closed, so its published versions are no longer served. | The `403` responses are sent with `Cache-Control: no-store`, so a cache in front of the CDN never keeps them. ## Related [#related] <Cards> <Card title="Connect your app to the CDN" href="/guides/delivery/connect-your-app-to-the-cdn" description="Load published translations at runtime." /> <Card title="Publish a locale" href="/guides/delivery/publish-a-locale" description="Make a new version for the CDN to serve." /> <Card title="Why are my translations not reaching my app?" href="/guides/delivery/why-are-my-translations-not-reaching-my-app" description="Work through the usual causes." /> </Cards> # The Releases screen (https://transglot.ai/docs/guides/delivery/the-deploy-screen/) **Releases** is the project page that gets translations out of transglot and into your product. Open it from the project's sidebar. Everyone who can open the project can read it; publishing needs the Admin role or an account Owner, and a plan that includes runtime delivery (Starter and above). The page has two views, switched with the tabs under the title: **Releases** for the release work, and **Connect & SDK** for the setup snippets. ## The Releases view [#the-releases-view] ### The banners [#the-banners] Above the tables, a banner states where the project stands: | Banner | What it means | | --- | --- | | **"N locales are not ready to publish"** | At least one language still has something outstanding. Each row of the table says what. | | **"N locales have strings that need checking"** | Some strings carry a review flag. A flag does not stop a publish, so flagged strings go to your website as they stand. **Read them** opens the flagged strings in the editor. | | **"Every locale is ready to publish"** | Every target language would publish complete, and nothing is flagged. | If your role could publish but your plan cannot, a notice reads **"Runtime publishing unlocks on the Starter plan."** Readiness and the release log still work on every plan. ### Departures [#departures] **Departures** has one row per target language. The source language is not listed: you never ship the source to itself. Each row shows: | Column | What it shows | | --- | --- | | **Locale** | The language. | | **Coverage** | How much of the language is written, against the project's source strings. | | **Approved** | How many strings someone has approved. | | **Needs checking** | How many strings carry a review flag. | | **Status** | One of the five states below. | | **Live** | The version your app is being served, and when it was published. | | **Next** | The one action that moves the row forward. | The **Status** column uses five words: | Status | Meaning | Next | | --- | --- | --- | | **not translated** | Nothing is written yet (or the project has no source strings). | **translate** | | **in progress** | Some strings are written, not all. | **translate** | | **waiting for approval** | Everything is written, but this project holds AI drafts back from publishing and some strings are still drafts. | **approve** | | **ready to publish** | Everything that would go into a release is written. | **publish** (or **review** if strings are flagged) | | **published** | Ready, and a version is already live. | **publish** to release again | Whether AI drafts count towards a release is a project setting. See [Drafts](https://transglot.ai/docs/guides/delivery/publish-a-locale#choose-whether-ai-drafts-are-published). ### The release card [#the-release-card] The card beside the table puts one language forward. It prefers a language that has never been published, and it skips languages with flagged strings. It reads, for example, **"German is ready to publish"** with the next version number, or **"German is live at v3"** for a language already being served. When nothing can be offered it says why: **No source strings yet**, **No target languages yet**, or **No locale is ready to publish**, with a link to fix it. If you cannot publish, the card says so in place of the button: **"A workspace admin publishes runtime versions."** or **"Not available on this account’s plan."** The same language is offered in the page header as **Publish German →**. See [Publish a locale](https://transglot.ai/docs/guides/delivery/publish-a-locale). ### This release [#this-release] A summary of the project's delivery: - **Publishing mode**: **manual**, or **on every push**. Shown to Owners and Admins. - **Delivered as**: the file format the CDN serves. Shown to Owners and Admins. - **Locales live**: how many target languages have a published version. - **Last release**: when the most recent version was published. - **Never published**: how much written work has not reached your app yet. - **Drafts**: whether AI drafts go into a release, with **Change** for the people allowed to set it. Publishing mode and format are set in **Settings → Developer**. See [The CDN](https://transglot.ai/docs/guides/delivery/the-cdn#delivery-settings). ### Release log [#release-log] **Release log** lists the ten most recent publishes, newest first: the language and version, when it was published, how many keys it holds, its format, who published it (**system** for a publish made through the API or by a push), and a short content fingerprint. Two versions with the same fingerprint hold the same content. Before anything is published it reads **"No runtime versions published yet."** ## The Connect & SDK view [#the-connect--sdk-view] The setup half of the page. Every snippet is pre-filled with this project's real API address, and with a real credential right after you create one. | Section | What it gives you | | --- | --- | | **Connect & verify** | Whether the project has a deploy token yet, with a link to the project's setup steps. | | **One provider** | A React snippet that adds the in-context editor for your reviewers, pre-filled with the project's publishable key. See [Connect the in-context editor](https://transglot.ai/docs/guides/quality/connect-the-in-context-editor). | | **Command line (CLI)** | A `transglot.json` config file and the terminal commands to push your source strings and pull translations back. See [CLI](https://transglot.ai/docs/developers/cli). | | **GitHub Action** | A workflow file for `.github/workflows/transglot.yml`. See [GitHub Action](https://transglot.ai/docs/developers/github-action). | | **Tokens, keys, webhooks & CDN** | A pointer to **Settings → Developer**, where credentials and CDN delivery are managed. | | **Token usage** | API calls per deploy token over the last 14 days. Owners and Admins only. | | **API playground** | Runs read-only API requests against this project. Shown on plans with API access. | | **Event catalog** | Every webhook event with an example payload. See [Webhook events](https://transglot.ai/docs/developers/webhooks/events). | The CLI, the GitHub Action and the API authenticate with a deploy token, which needs a plan with API access (Starter and above). On the Free plan the page says so and points you to uploading files from the project's setup page instead. ## Related [#related] <Cards> <Card title="Publish a locale" href="/guides/delivery/publish-a-locale" description="Freeze a language into a version and serve it." /> <Card title="The CDN" href="/guides/delivery/the-cdn" description="Addresses, caching and CDN keys." /> <Card title="Create a deploy token" href="/developers/deploy-tokens" description="The credential the CLI, CI and API use." /> </Cards> # Why are my translations not reaching my app? (https://transglot.ai/docs/guides/delivery/why-are-my-translations-not-reaching-my-app/) The editor shows your translations as they are now. Your app shows whatever version it last received, from the CDN or from files in its build. When the two disagree, one of the causes below is almost always the reason. Work through them in order. <Accordions type="single"> <Accordion title="The language has never been published"> Editing a translation does not publish it. Open **Releases** and look at the language's **Live** column in **Departures**. If it has no version, there is nothing for your app to fetch, and the CDN answers `404 not-published`. [Publish it](https://transglot.ai/docs/guides/delivery/publish-a-locale). </Accordion> <Accordion title="You changed the translation after the last publish"> A published version never changes. Edits made after it are not in it, so publish the language again. When nothing has changed, the publish says the language is already live and keeps the current version. If the project's **Freshness** is **Auto: publish on every push**, each push publishes the language it uploaded at that moment. Translations that arrive afterwards, such as the AI translation a push of source strings starts, wait for that language's next publish. </Accordion> <Accordion title="The project holds AI drafts back"> If the project's **Drafts** rule is **No, hold drafts back**, a translation Glossa wrote goes into a version only once someone approves or edits it. The language's status reads **waiting for approval** while drafts remain. Approve them and publish again, or change the rule in the **This release** card on **Releases**. See [Choose whether AI drafts are published](https://transglot.ai/docs/guides/delivery/publish-a-locale#choose-whether-ai-drafts-are-published). </Accordion> <Accordion title="Your app is pinned to a version"> An app that requests a versioned address (`.../v3`) gets that version forever: that is what makes it safe to cache. Move it to the newer version, or to the latest address. See [Latest and versioned addresses](https://transglot.ai/docs/guides/delivery/the-cdn#latest-and-versioned-addresses). </Accordion> <Accordion title="A cache is still serving the previous version"> The latest address may be cached for up to 60 seconds after a publish, so give a new version about a minute. If you put your own CDN in front of transglot, check it honours the `Cache-Control` the origin sends rather than caching for longer. Your app's own storage, such as a persisted runtime cache, also serves the old version until it revalidates. </Accordion> <Accordion title="One string is missing, the rest are there"> A version only holds strings that have a translation. A key nobody has translated is absent, and the runtime client shows the key itself. Two other cases leave a string out: - The project's **Message syntax** is **ICU** and the translation is not valid ICU MessageFormat. The publish notice says how many keys were left out. Fix them in the editor and publish again. - The delivery format cannot represent the string, such as a plural string in a format without plural support. </Accordion> <Accordion title="Your app asks for a different format"> Each version is published in one format. A request with `?format=` naming another format returns `404`. Request the published format, which **Releases** shows under **Delivered as**, or change **Delivery format** in **Settings → Developer** and publish again. </Accordion> <Accordion title="You changed a setting, but the live version did not change"> Changes to **Delivery format**, **Message syntax** or the **Drafts** rule apply when a version is built. What is live keeps its content until the language's next publish. </Accordion> <Accordion title="Your app reads translation files from its build"> If your build pulls translations with the [CLI](https://transglot.ai/docs/developers/cli) or the [GitHub Action](https://transglot.ai/docs/developers/github-action) rather than reading the CDN at runtime, nothing changes in your app until you run that build again. Publishing affects only the CDN. </Accordion> <Accordion title="Every request fails"> Check the status code: - `401`: the CDN key is missing, mistyped or deleted. Use the `X-Transglot-Cdn-Key` header or `?key=`, and a key from this project. - `404 not-found`: the project id in the address does not belong to the key's project. - `403 plan-feature-required`: the account's plan no longer includes runtime delivery. The keys are kept and serve again after an upgrade. See [Errors](https://transglot.ai/docs/guides/delivery/the-cdn#errors) for the full list. </Accordion> </Accordions> ## Related [#related] <Cards> <Card title="Publish a locale" href="/guides/delivery/publish-a-locale" description="Make a new version for the CDN to serve." /> <Card title="Connect your app to the CDN" href="/guides/delivery/connect-your-app-to-the-cdn" description="Load published translations at runtime." /> </Cards> # Guides (https://transglot.ai/docs/guides/) These guides follow the work in the order it happens: set up a project and bring your strings in, translate and review them, keep quality up, then deliver the result to your app. Each page covers one screen or one task in the transglot console at [app.transglot.ai](https://app.transglot.ai). If you are new to transglot, start with the [Quickstart](https://transglot.ai/docs/quickstart) and [How transglot works](https://transglot.ai/docs/how-transglot-works). ## Guide areas [#guide-areas] <Cards> <Card title="Projects" href="/guides/projects/your-project" description="The project overview, languages, keys, plural forms, importing files and project activity." /> <Card title="Translating" href="/guides/translating/the-editor" description="The editor, AI translation with Glossa, review and approval, comments, history and assignments." /> <Card title="Quality" href="/guides/quality/quality-checks" description="Quality checks and scores, the glossary, brand voice, screenshots and self-heal." /> <Card title="Delivery" href="/guides/delivery/publish-a-locale" description="Publishing languages to the CDN, connecting your app, and the deploy screen." /> </Cards> ## Most-used tasks [#most-used-tasks] <Cards> <Card title="Import content" href="/guides/projects/import-content" description="Upload your language files and bring strings and translations into a project." /> <Card title="Add a target language" href="/guides/projects/add-a-target-language" description="Add a language and have the existing strings translated into it." /> <Card title="Translate with AI" href="/guides/translating/translate-with-ai" description="Have Glossa fill a language's missing translations, and see what it covers first." /> <Card title="Approve or flag a translation" href="/guides/translating/approve-or-flag-a-translation" description="Sign off translations one at a time or in bulk, or send them back." /> <Card title="Assign a language to a translator" href="/guides/translating/assign-a-language-to-a-translator" description="Hand a language to a translator or vendor as tracked work." /> <Card title="Publish a locale" href="/guides/delivery/publish-a-locale" description="Freeze a language into a version your app loads from the CDN." /> </Cards> ## Working from code [#working-from-code] Developers who push and pull strings from a repository can do most of this without opening the console. See the [CLI](https://transglot.ai/docs/developers/cli), the [GitHub Action](https://transglot.ai/docs/developers/github-action) and the [API](https://transglot.ai/docs/developers/api). # Add a target language (https://transglot.ai/docs/guides/projects/add-a-target-language/) Adding a target language needs the Developer role or above. Translators and Reviewers can work in a language but cannot add one. ## Add the language [#add-the-language] <Steps> <Step> ### Find the add row [#find-the-add-row] Open the project. The last row of the **Locales** table on the [overview](https://transglot.ai/docs/guides/projects/your-project) is **Add a language**. The same control is on **Settings**, then **Languages**. </Step> <Step> ### Pick the language [#pick-the-language] Type into **Search languages** to search by English name, native name or tag (for example "German", "Deutsch" or `de`), and pick it from the list. The source language and the languages already in the project are not offered. </Step> <Step> ### Add it [#add-it] Select **Add**. </Step> <Step> ### Confirm the translation [#confirm-the-translation] If the project has strings to translate into the new language and **Auto-translate** is on, the console asks first: **Add** *language* **and translate into it?** "Everything already in this project is translated into *language* as soon as it is added." The dialog shows how many words that is. Confirm to add the language and start the translation. </Step> </Steps> If you pick a regional form of a language the project already has, for example `pt-PT` when it has `pt-BR`, a warning says so before you add it. Both are allowed, but each is translated separately: no strings carry over between them. ## What happens next [#what-happens-next] The language joins the **Locales** table, and a message says what happened to the project's existing strings: | Message | Meaning | | --- | --- | | "*Language* added. GLOSSA is translating 240 strings into it. Reload the page to see its progress." | A translation run has started. | | "*Language* added. 12 strings came straight from translation memory, so they are in the language already." | Every string was reused from earlier translations, at no cost. | | "*Language* added. Nothing in the project was waiting to be translated into it." | There was nothing to translate. | | "*Language* added. Auto-translate is off for this project, so nothing has been translated into it yet." | Translate it from the editor with **Translate missing** when you are ready. | | "*Language* added. There are no source strings in the project yet, so nothing is being translated into this language." | Import your strings first. | | "*Language* added. There was not enough word quota left this cycle to translate the existing strings into this language." | The language was added, untranslated. | | "*Language* added. AI translation is paused for this project, so the existing strings were not translated into this language." | The language was added, untranslated. | | "*Language* added. Your access does not cover this locale, so the strings already in the project were left untranslated." | You can manage languages but are not scoped to this one. | Whatever you type or pick is stored as a standard BCP-47 tag, so `pt-br` and `pt-BR` are the same language. See [Languages and locales](https://transglot.ai/docs/concepts/languages-and-locales). ## Language limits [#language-limits] On the Free plan, an account can use a limited number of languages across all its projects, counting source languages; a language already used by another project in the account does not count again. Paid plans have no language limit. See [pricing](https://transglot.ai/pricing). ## If it doesn't work [#if-it-doesnt-work] - **"That language is already in this project."**: the language, or the same language under another spelling, is already a target or the source. - **"Unknown locale: use a BCP-47 tag like de, pt-BR, or zh-Hans."**: the tag is not a language transglot recognises. - **"The account this workspace belongs to has no room for another language on its plan."**: the account has reached its plan's language limit. Upgrade, or pick a language the account already uses. See [Change your plan](https://transglot.ai/docs/administration/billing/change-your-plan). - **Add a language** is not shown: you need the Developer role or above. ## Related [#related] - [Remove a language](https://transglot.ai/docs/guides/projects/remove-a-language) - [Translate with AI](https://transglot.ai/docs/guides/translating/translate-with-ai) - [Assign a language to a translator](https://transglot.ai/docs/guides/translating/assign-a-language-to-a-translator) # Add or edit a key (https://transglot.ai/docs/guides/projects/add-or-edit-a-key/) A key is the name your code asks for, such as `checkout.title`. Most keys arrive with your files, through an [import](https://transglot.ai/docs/guides/projects/import-content), the [CLI](https://transglot.ai/docs/developers/cli) or an integration, together with their source text. You can also create and edit keys by hand in the editor. Adding, editing and deleting keys needs the Developer role or above. <Callout type="info" title="Source text comes from your files"> The key dialog sets a key's name, description, plural setting and length limit. It does not hold source text: source strings arrive by import, push or integration, and the editor shows them read-only. A key you create by hand has no source text until a file that contains it is imported or pushed. See [Keys and strings](https://transglot.ai/docs/concepts/keys-and-strings). </Callout> ## Add a key [#add-a-key] <Steps> <Step> ### Open the dialog [#open-the-dialog] In the [editor](https://transglot.ai/docs/guides/translating/the-editor), select **New key**. In a project with no strings yet, the editor offers **Add one key by hand** instead. </Step> <Step> ### Fill in the fields [#fill-in-the-fields] | Field | What to enter | | --- | --- | | **Key name** | The name your code uses, for example `checkout.title`. Up to 500 characters, and unique in the project. | | **Description (context for translators & AI)** | What the string is and where it appears, for example "Headline shown on the checkout page". Translators see it in the editor, and Glossa reads it when translating. Up to 2,000 characters. | | **Plural key (one form per CLDR category)** | Turn on for a string that changes with a number, such as "1 item" and "3 items". See [Plural forms](https://transglot.ai/docs/guides/projects/plural-forms). | | **Max length (optional)** | The most characters a translation may have, for text that must fit a button or a label. | </Step> <Step> ### Create it [#create-it] Select **Create**. A message confirms "Key created." </Step> </Steps> ## Edit a key [#edit-a-key] <Steps> <Step> ### Open the key [#open-the-key] In the editor, switch to **Table** and select the key's name in the first column. The dialog opens as **Edit** *key name*. </Step> <Step> ### Change it [#change-it] Change any of the fields, then select **Save**. A message confirms "Key updated." </Step> </Steps> Things to know when editing: - **Renaming.** If your code or files still use the old name, the next import or push brings it back as a separate key. Rename it in your source files too. To rename many keys that share a prefix, use **Keys**, then **Rename prefix** in the editor's [bulk actions](https://transglot.ai/docs/guides/translating/bulk-actions). - **Descriptions from files.** If your files carry descriptions or comments for their strings, the next push or import of the source language replaces the description with the file's. - **Plural setting.** You can only switch a key between plural and simple while it has no translations. - **Length limit.** Changing **Max length (optional)** re-checks the key's translations: any that now run over the limit are flagged for review. ## Delete a key [#delete-a-key] <Steps> <Step> ### Open the key [#open-the-key-1] Open the key as described in [Edit a key](#edit-a-key). </Step> <Step> ### Delete it [#delete-it] Select **Delete key**. The confirmation reads "The key and every translation of it, in every language, are removed. Anything reading this key from your app falls back to the key name until you push a replacement." </Step> <Step> ### Confirm [#confirm] Select **Delete key**. A message confirms "Key deleted." </Step> </Steps> Deleting a key cannot be undone. To delete many keys at once, select them in the table and use **Delete** in the bulk bar. A member scoped to particular languages cannot delete a key that has translations in languages outside their access. ## If it doesn't work [#if-it-doesnt-work] - **"A key with that name already exists in this project."**: choose another name, or edit the existing key. - **"Cannot change the plural setting of a key that already has translations. Delete them first."**: the key already has translations, so its plural setting is fixed. - **New key** is not shown: you need the Developer role or above. ## Related [#related] - [Keys and strings](https://transglot.ai/docs/concepts/keys-and-strings) - [Plural forms](https://transglot.ai/docs/guides/projects/plural-forms) - [Act on many strings at once](https://transglot.ai/docs/guides/translating/bulk-actions) # Delete a project (https://transglot.ai/docs/guides/projects/delete-a-project/) Deleting a project needs the Admin role in the workspace, or Owner of its account. Someone whose admin access to the project is limited to some of its languages cannot delete it. <Callout type="warn" title="There is no undo and no grace period"> Every key, translation, version, screenshot, glossary term, webhook endpoint and API token in the project is removed immediately and permanently. Export or publish anything you want to keep first. The workspace's billing history is kept. </Callout> ## Before you start [#before-you-start] - Export the project's languages from **Settings**, then **Export**, or pull them with the [CLI](https://transglot.ai/docs/developers/cli), if you might need them again. - Anything that reads from the project stops working: CLI pushes and pulls with its tokens, webhooks, integrations and README badges. Translations your app has already built in keep working. ## Delete the project [#delete-the-project] <Steps> <Step> ### Open the project settings [#open-the-project-settings] Open the project, select **Settings**, then **General**. </Step> <Step> ### Start the deletion [#start-the-deletion] Under **Delete this project**, select **Delete project**. </Step> <Step> ### Type the name [#type-the-name] In the dialog, type the project's name in **Project name**, exactly as it appears. "This cannot be undone. Type *name* to confirm." </Step> <Step> ### Confirm [#confirm] Select **Delete project**. </Step> </Steps> ## What happens next [#what-happens-next] You return to the workspace, and a message confirms it: "Project “*name*” deleted." The workspace's Admins and the account's Owners get an email that the project was deleted, and the deletion is recorded in the workspace [audit log](https://transglot.ai/docs/administration/security/the-audit-log) and its activity feed. ## If it doesn't work [#if-it-doesnt-work] - **"Type the project name exactly to confirm."**: the name you typed does not match. Check capitals and spaces. - **Delete this project** is not shown: you are not an Admin of this workspace or an Owner of its account. ## Related [#related] - [Rename a project or change its URL](https://transglot.ai/docs/guides/projects/rename-a-project-or-change-its-url) - [Remove a language](https://transglot.ai/docs/guides/projects/remove-a-language) - [Delete a workspace](https://transglot.ai/docs/administration/workspace/delete-a-workspace) # Import content (https://transglot.ai/docs/guides/projects/import-content/) The browser upload is the quickest way to get strings into a project, and it works on every plan, including Free. You drop your language files on the project's **Get started** page, see what transglot detected, and nothing is written until you confirm. Uploading needs the Developer role or above. For other ways in, see [Other ways to bring strings in](#other-ways-to-bring-strings-in). ## Upload your files [#upload-your-files] <Steps> <Step> ### Open Get started [#open-get-started] On a new project, select **Add source strings →** on the [overview](https://transglot.ai/docs/guides/projects/your-project) or **Import source strings →** in the editor. On a project that already has strings, open **Releases**, then **Connect & SDK**, and select **Open the setup steps**. </Step> <Step> ### Add the files [#add-the-files] Under **Upload your language files**, drag your files onto **Drag your files here**, or select **Choose files**. You can upload several files at once, or a zip of your locales folder. Include the source-language file: only it can create keys. </Step> <Step> ### Check what was detected [#check-what-was-detected] transglot reads the files without writing anything and lists each one with its language, its format and how many strings it holds. - If a file's language could not be worked out, the row reads "Choose the language for this file." Pick it under **Language for** *file name*. - If a format was guessed wrong, or reads "Unrecognised format.", pick the right one from the format list on the row. **Detect** goes back to automatic detection. - If the files bring languages the project does not have yet, a line says which "will be added to this project." **Choose different files** starts again. </Step> <Step> ### Import [#import] Select **Import**. It stays unavailable while a file still needs its language, or when no file is ready. </Step> </Steps> ## What is written [#what-is-written] - **The source-language file** creates keys that do not exist yet and sets the source text of keys that do. The file wins: changed source text replaces what was there, and translations made from the old text are marked stale. - **Target-language files** write translations for keys that already exist, as **edited**. They never overwrite a translation that a person has edited or approved; that entry is skipped and reported. Keys that are only in a target file, and not in the source, are not created. - **Descriptions** and **plural forms** come across where the format carries them. See [Plural forms](https://transglot.ai/docs/guides/projects/plural-forms). - **Nothing is deleted.** An upload adds and updates; keys missing from your files stay in the project. Files are processed source first, whatever order you upload them in, so translations always land on keys that exist. ## After the upload [#after-the-upload] Small uploads finish while you wait. Larger ones run in the background with a progress bar, **Importing your files**, which "keeps running if you close this window or leave the page". You can cancel a running import; cancelling does not undo what has already been written. The first time strings arrive in a project, **Get started** asks whether to translate them now, and whether to keep translating new text automatically. After that, new and changed source text is translated as it arrives while **Auto-translate** is on. See [Translate with AI](https://transglot.ai/docs/guides/translating/translate-with-ai). Uploading the same files again is safe: identical values are recognised and left alone. ## If something was not imported [#if-something-was-not-imported] When some files could not be imported, the panel says "N files were not imported." and gives the reason for each: | Message | What to do | | --- | --- | | "None of these keys exist yet. Import your source language first." | Upload the source-language file, then the translations. | | "This language is outside your project grant." | Your access is limited to other languages. Ask an Admin, or upload only your languages. | | "*Languages* will not be added: your plan has no room for more languages in this project. Everything else below still imports." | Your plan's language limit is reached. See [Add a target language](https://transglot.ai/docs/guides/projects/add-a-target-language#language-limits). | | "Your role cannot add strings to this project. An admin or developer can, or they can grant you access." | Uploading needs the Developer role or above. | | "Those files are too large to upload here. Split them, or push from the CLI." | Upload fewer or smaller files at a time, or use the [CLI](https://transglot.ai/docs/developers/cli). | | "No language files were found in what you uploaded. A zip should contain the locale files themselves, not just folders." | Zip the files, not an empty folder structure. | | "The import stopped before it finished. Some of your strings may already be in the project, so check it before importing again." | Check the project; importing again is safe. | [My import did not bring everything across](https://transglot.ai/docs/resources/troubleshooting/my-import-did-not-bring-everything-across) covers more cases. ## Other ways to bring strings in [#other-ways-to-bring-strings-in] | Way | Best for | | --- | --- | | [CLI](https://transglot.ai/docs/developers/cli) `push` | Keeping a project in step with your codebase. Needs a deploy token, which starts on the Starter plan. | | [GitHub Action](https://transglot.ai/docs/developers/github-action) | Pushing on every merge. | | [Git and content integrations](https://transglot.ai/docs/integrations) | Syncing from a repository or a CMS. | | [Import from another tool](https://transglot.ai/docs/integrations/migrate/import-from-another-tool) | Moving a project, with its translations, from another localization platform. | See [Which file formats can I use?](https://transglot.ai/docs/guides/projects/which-file-formats-can-i-use) for the formats every route accepts. ## Related [#related] - [Add or edit a key](https://transglot.ai/docs/guides/projects/add-or-edit-a-key) - [Keys and strings](https://transglot.ai/docs/concepts/keys-and-strings) - [File formats](https://transglot.ai/docs/developers/formats) # Plural forms (https://transglot.ai/docs/guides/projects/plural-forms/) Languages count differently. English needs two forms of "1 item" and "3 items"; Polish needs more; Japanese needs one. transglot stores a plural string as one form per plural category, using the standard CLDR categories (`zero`, `one`, `two`, `few`, `many` and `other`), and each language gets exactly the categories its own grammar uses. ## Where plural keys come from [#where-plural-keys-come-from] A key is a plural key when its source file says so. These formats carry plurals as separate forms: | Format | How plurals are written | | --- | --- | | Android XML | `<plurals>` resources | | Apple `.stringsdict` and String Catalog (`.xcstrings`) | Plural variations | | Flutter ARB | ICU plural messages | | Gettext PO | `msgid_plural` entries | | CSV and XLSX | One row per form, named `key[category]` | You can also mark a key as plural by hand: turn on **Plural key (one form per CLDR category)** when you [add or edit a key](https://transglot.ai/docs/guides/projects/add-or-edit-a-key). A key can only be switched between plural and simple while it has no translations. Formats without plural forms, such as JSON and YAML, can still hold plurals as ICU messages inside an ordinary string, for example `{count, plural, one {# item} other {# items}}`. Those are simple keys whose text contains the plural, and the quality checks read them according to the project's **Message syntax** setting, under **Settings**, then **Checks**. See [File formats](https://transglot.ai/docs/developers/formats). ## Translating plurals [#translating-plurals] - **With Glossa.** An AI translation of a plural key writes every form the target language needs, and the automatic checks flag a translation with the wrong set of forms. - **By hand.** A plural key cannot be typed into a single box. In the editor, select **Edit forms** on the row in **Table** view, or **Edit plural forms** in **Side by side** view. ## Edit the forms [#edit-the-forms] <Steps> <Step> ### Open the plural dialog [#open-the-plural-dialog] In the [editor](https://transglot.ai/docs/guides/translating/the-editor), choose the language and open the string's plural forms. The dialog is titled *key*: *language* **plural forms**. </Step> <Step> ### Compare with the source [#compare-with-the-source] The source forms are shown at the top, one per category, "for comparison". </Step> <Step> ### Fill in each form [#fill-in-each-form] There is one box per category the target language uses, labelled with the category name. As you type, problems such as a missing placeholder are listed under the boxes. </Step> <Step> ### Save [#save] Select **Save**. </Step> </Steps> In the table, a plural string shows each form on its own line, labelled with its category, and its key carries a **plural** chip. ## Importing plurals [#importing-plurals] When you import or push a translation file, a plural entry is only written to a plural key, and a simple entry only to a simple key. An entry whose shape does not match the key is skipped and reported as a conflict, so a file can never silently turn a plural string into a simple one. ## Related [#related] - [Keys and strings](https://transglot.ai/docs/concepts/keys-and-strings) - [Quality checks](https://transglot.ai/docs/guides/quality/quality-checks) - [Which file formats can I use?](https://transglot.ai/docs/guides/projects/which-file-formats-can-i-use) # Project activity (https://transglot.ai/docs/guides/projects/project-activity/) The activity feed answers "what happened in this project, and who did it?". Everyone who can open the project can read it. ## Open the feed [#open-the-feed] The [project overview](https://transglot.ai/docs/guides/projects/your-project) shows the latest few events under **Recent activity**. Select **View all** to open the project's **Activity** page, which lists everything that happened, newest first, with who did it and when. **Load more** fetches older events. Events that no person started, such as a translation run that finished, are shown as done by **System**. ## What appears [#what-appears] | Area | Examples | | --- | --- | | Keys | A key created, keys deleted, renamed or given a character limit in bulk, source text copied into a language | | Languages | A language added or removed, with how many translations went with it | | Review | Translations approved, flagged or restored to an earlier version, a review requested | | Translation runs | A run finished, with what it translated, reused from memory or could not translate | | Quality | Self-heal and source drift repairs | | Assignments | Work assigned, started, submitted, withdrawn, approved or sent back | | Comments | A comment posted | | People and access | A member's access to the project granted, changed or removed | | Project | The project's web address changed, API tokens and webhooks created | | Delivery | A language published to the CDN | The feed records actions on the project, not every keystroke. To see each version of one translation, with **Restore** on each, use its [version history](https://transglot.ai/docs/guides/translating/version-history). ## Activity and the audit log [#activity-and-the-audit-log] The activity feed is a product history, meant for the people working on the project. The workspace [audit log](https://transglot.ai/docs/administration/security/the-audit-log) is the security record: an append-only trail of who was given access, whose role changed, which credentials were created, and destructive changes such as deleted projects and removed languages. Use the audit log when you need evidence; use the activity feed to follow the work. ## Related [#related] - [The project overview](https://transglot.ai/docs/guides/projects/your-project) - [Version history](https://transglot.ai/docs/guides/translating/version-history) - [The audit log](https://transglot.ai/docs/administration/security/the-audit-log) # Remove a language (https://transglot.ai/docs/guides/projects/remove-a-language/) Removing a language deletes its translations and their version history, not only the language. Keys, source text and every other language are untouched. Removing a language needs the Developer role or above. A member scoped to particular languages can only remove a language they have access to. <Callout type="warn" title="There is no undo"> The translations and their history are deleted, and adding the language again gives you an empty language. If you might want them back, export the language first from **Settings**, then **Export**, or pull it with the [CLI](https://transglot.ai/docs/developers/cli). </Callout> ## Remove the language [#remove-the-language] <Steps> <Step> ### Find the language [#find-the-language] Open the project. Find the language's row in the **Locales** table on the [overview](https://transglot.ai/docs/guides/projects/your-project), or on **Settings**, then **Languages**. </Step> <Step> ### Select Remove [#select-remove] Select **Remove** on its row. </Step> <Step> ### Confirm [#confirm] The dialog, **Remove** *language*?, says how many translated strings are deleted with it, or that nothing has been translated into it yet. For a language with 100 or more translated strings, type the language's name to confirm. Select **Remove** *language*. </Step> </Steps> ## What happens next [#what-happens-next] - The row disappears, and a message confirms it, such as "German removed, along with 240 translations." - Translation runs no longer include the language. - The workspace's Admins and the account's Owners, including you, get an email saying what was removed and how much went with it. The removal is recorded in the project's [activity](https://transglot.ai/docs/guides/projects/project-activity) and in the workspace [audit log](https://transglot.ai/docs/administration/security/the-audit-log). - Translations already published to the CDN or built into your app keep working until your next deploy. If this was a mistake, contact support the same day: a backup is the only remaining copy, and backups are not kept forever. See [Get help](https://transglot.ai/docs/resources/get-help). ## If it doesn't work [#if-it-doesnt-work] - **"The source language cannot be removed."**: a project always has exactly one source language. - **Remove** is not shown on a row: you need the Developer role or above, with access to that language. ## Related [#related] - [Add a target language](https://transglot.ai/docs/guides/projects/add-a-target-language) - [Languages and locales](https://transglot.ai/docs/concepts/languages-and-locales) - [Delete a project](https://transglot.ai/docs/guides/projects/delete-a-project) # Rename a project or change its URL (https://transglot.ai/docs/guides/projects/rename-a-project-or-change-its-url/) A project has two names. **Name** is what everyone sees across the console, and changing it moves nothing. **Web address** is the part of the console's page addresses that names the project, and changing it breaks links that use the old one. Admins and the account's Owners can change both. ## Rename the project [#rename-the-project] <Steps> <Step> ### Open the settings [#open-the-settings] Open the project, select **Settings**, then **General**. </Step> <Step> ### Edit the name [#edit-the-name] Under **Names**, edit **Name** ("The project's display name across the console."). Names can be up to 100 characters. </Step> <Step> ### Save [#save] Select **Save changes**. **Discard** puts the field back. </Step> </Steps> The new name shows everywhere the project is listed. A rename is not announced to anyone, because nothing outside the console changes. ## Change the web address [#change-the-web-address] <Callout type="warn" title="What a new address breaks"> Bookmarks and links to the project's pages in the console, links in emails already sent, export download links already shared, and README badges all use the old address and stop working. Translations delivered through the CDN and the API keep working. The old address is not held for you: another project in the workspace can take it later, and links that still use it would then lead to that project. </Callout> <Steps> <Step> ### Edit the web address [#edit-the-web-address] On **Settings**, then **General**, under **Names**, edit **Web address**. Under the field, **Web address after you save:** shows the address the project will have. </Step> <Step> ### Save [#save-1] Select **Save changes**. </Step> <Step> ### Confirm [#confirm] The dialog **Change this project's web address?** shows the current and the new address and what stops working. Select **Change address** to save, or cancel to go back without saving anything. </Step> </Steps> The project opens at its new address straight away. Because a new address breaks links for other people too, the change is announced: the workspace's Admins and the account's Owners, including you, get an email and a console notification naming the old and the new address. The change is also added to the project's [activity](https://transglot.ai/docs/guides/projects/project-activity) and recorded in the workspace [audit log](https://transglot.ai/docs/administration/security/the-audit-log). ### Address rules [#address-rules] A web address uses lowercase letters, numbers and hyphens only, up to 120 characters, and must be unique within the workspace. If you type anything else, the message under the field says so and, where it can, suggests an address that is allowed: for "My Website!" it suggests `my-website`. ## If it doesn't work [#if-it-doesnt-work] - **"Use lowercase letters, numbers and hyphens only, for example my-website."**: use the suggested address, or edit yours to match the rule. - **"That web address is already used by another project in this workspace."**: choose another address. - **"Use 120 characters or fewer for the web address."**: shorten it. - **General** is not offered under **Settings**: you are not an Admin of this workspace or an Owner of its account. ## Related [#related] - [The project overview](https://transglot.ai/docs/guides/projects/your-project) - [Add a progress badge to your README](https://transglot.ai/docs/guides/delivery/add-a-progress-badge-to-your-readme) - [Delete a project](https://transglot.ai/docs/guides/projects/delete-a-project) # Which file formats can I use? (https://transglot.ai/docs/guides/projects/which-file-formats-can-i-use/) Every format below works for bringing strings in (browser upload, CLI push, API) and for getting translations out (CLI pull, export, API). Placeholders and plural forms are kept intact, and the automatic checks flag a translation that breaks them before it reaches your app. | Ecosystem | Formats | Separate plural forms | | --- | --- | --- | | Web and JavaScript | [JSON (flat)](https://transglot.ai/docs/developers/formats/json), [JSON (nested)](https://transglot.ai/docs/developers/formats/json), [YAML](https://transglot.ai/docs/developers/formats/yaml) | No | | Apple | [iOS Strings](https://transglot.ai/docs/developers/formats/apple) (`.strings`), [iOS Stringsdict](https://transglot.ai/docs/developers/formats/apple) (`.stringsdict`), [Apple String Catalog](https://transglot.ai/docs/developers/formats/apple) (`.xcstrings`) | Stringsdict and String Catalog | | Android | [Android XML](https://transglot.ai/docs/developers/formats/android) | Yes | | Flutter | [Flutter ARB](https://transglot.ai/docs/developers/formats/flutter-arb) | Yes | | PHP and Laravel | [Laravel JSON](https://transglot.ai/docs/developers/formats/laravel), [Laravel PHP](https://transglot.ai/docs/developers/formats/laravel) | No | | Interchange | [XLIFF 1.2 and XLIFF 2.0](https://transglot.ai/docs/developers/formats/xliff), [gettext PO](https://transglot.ai/docs/developers/formats/gettext) | gettext PO | | .NET and Java | [.NET .resx](https://transglot.ai/docs/developers/formats/resx), [Java .properties](https://transglot.ai/docs/developers/formats/java-properties) | No | | Spreadsheets | [CSV and XLSX](https://transglot.ai/docs/developers/formats/spreadsheets) | Yes, one row per form | | Documents | [Markdown / MDX](https://transglot.ai/docs/developers/formats/markdown) | No | | Games | Unity CSV (Unity Localization string tables), Unreal PO (Unreal Engine Localization Dashboard) | No | ## Things to know [#things-to-know] - **Plurals in any format.** Formats without separate plural forms can still hold plurals as ICU messages inside a string, such as `{count, plural, one {# item} other {# items}}`. See [Plural forms](https://transglot.ai/docs/guides/projects/plural-forms). - **Laravel.** Use Laravel JSON (`lang/{locale}.json`) to bring Laravel strings in. Laravel PHP array files that start with the standard comment header cannot be read, so the browser upload does not offer `.php` files. - **Apple.** `.stringsdict` holds plural strings only; keep simple strings in `.strings`, or use a String Catalog for both. - **Descriptions.** Flutter ARB, XLIFF 1.2, Apple String Catalog, gettext PO, CSV, XLSX, Unity CSV and Unreal PO carry a description for each string, which becomes the key's description. In other formats, write descriptions in the console. See [Add or edit a key](https://transglot.ai/docs/guides/projects/add-or-edit-a-key). - **Detection.** transglot detects a file's format from its contents and name. When it guesses wrong in the browser upload, pick the right format on the file's row before importing. See [Import content](https://transglot.ai/docs/guides/projects/import-content). ## Related [#related] - [File formats reference](https://transglot.ai/docs/developers/formats) - [Plural forms](https://transglot.ai/docs/guides/projects/plural-forms) - [The command line tool](https://transglot.ai/docs/developers/cli) # The project overview (https://transglot.ai/docs/guides/projects/your-project/) The overview is the page a project opens on. It answers "where is this project up to?": how much is translated and approved in each language, what is running, and what to do next. Everyone who can open the project sees it. ## The header [#the-header] Under the project name, a line gives the source language, the number of target languages and the number of keys, for example `en → 3 locales · 1,204 keys`. When the project reused translations from memory this month, it adds how many words, as in "90 words from memory this month". On the right, **Open editor** opens the [editor](https://transglot.ai/docs/guides/translating/the-editor), and **Deploy →** opens the project's releases, where you [publish a locale](https://transglot.ai/docs/guides/delivery/publish-a-locale). A project with no keys yet shows **Add source strings →** instead, which opens the setup page where you [import content](https://transglot.ai/docs/guides/projects/import-content). ## The pipeline [#the-pipeline] A strip of five figures sums up the project's work, left to right. Each one opens the place where that work is done. | Figure | Counts | Opens | | --- | --- | --- | | **source** | Source strings (keys) in the project | The project | | **translate** | Target strings that have a translation, of all target strings | The editor, filtered to untranslated strings | | **review** | Translated strings flagged as needing a check by a person | The editor, filtered to strings that need checking | | **approve** | Translated strings a person has approved | The quality dashboard | | **deploy** | Target languages whose next publish would carry every string the project expects them to hold | Releases | ## Locales [#locales] The **Locales** table has one row per target language. A sentence at the top totals the project, for example "1,040 of 1,200 translated, 610 approved, 12 need checking". Rows are sorted by what blocks shipping: languages with strings that need checking first, then the least translated. | Column | Shows | | --- | --- | | **Locale** | The language name and its tag. | | **Coverage** | A bar of translated strings, and the same sentence for this language alone. | | **Approved** | Approved strings, out of the language's total. | | **Quality** | How many strings need checking, or **clear**. | | **State** | What is happening to the language now: "GLOSSA is translating, started 4 minutes ago", "queued to translate", "all keys translated", or how many strings are not translated. If the last run for the language failed, the state links to that run so you can see which strings failed. | | **Next** | The one thing to do next in this language, as a link into the editor. | The **Next** link is: - **Review** when the language has strings that need checking and you can review it. It opens the editor on exactly those strings. - **Fill gap** when strings are untranslated. It opens the editor on the untranslated strings. - **Approve** when everything is translated but some strings are not approved yet, and you can review. It opens the editor on the strings waiting for approval. - **Import** when the project has no source strings yet. Select anywhere on a row to follow its **Next** link. **Remove** at the end of a row deletes that language; see [Remove a language](https://transglot.ai/docs/guides/projects/remove-a-language). ### Add a language [#add-a-language] Below the rows, **Add a language** adds a target language: search by name, native name or tag, then select **Add**. See [Add a target language](https://transglot.ai/docs/guides/projects/add-a-target-language). ## What to do next [#what-to-do-next] The foot of the **Locales** table names the single most useful next step for you, worked out from the project's real state. While the project is being set up, it is the first unfinished setup step (add a target language, bring in source strings, run the first translation) with **Continue setup**. After that, it points at strings waiting for review first, then untranslated strings, with **Open editor**. When nothing is left that you can act on, it says nothing. ## Recent activity [#recent-activity] **Recent activity** lists the latest changes to the project. **Translation runs** opens the list of every AI translation run, with its counts and any strings that failed. **View all** opens the full [project activity](https://transglot.ai/docs/guides/projects/project-activity). ## If something looks wrong [#if-something-looks-wrong] - **"No target languages yet"**: the project has a source language but nothing to translate into. If you have the Developer role or above, add one in the row below. - Every language reads "waiting for source strings": the project has no keys yet. Select **Add source strings →**. - **Add a language** and **Remove** are not shown: managing languages needs the Developer role or above, with access to the language. - **Review** or **Approve** is missing on a row: you cannot review that language. Members with the Reviewer or Admin role, and account Owners, can. ## Related [#related] - [The editor](https://transglot.ai/docs/guides/translating/the-editor) - [Translation states](https://transglot.ai/docs/concepts/translation-states) - [Workspaces and projects](https://transglot.ai/docs/concepts/workspaces-and-projects) # Add a glossary term (https://transglot.ai/docs/guides/quality/add-a-glossary-term/) Owners, Admins and Developers can add terms, on Starter and above. ## Add a term in the console [#add-a-term-in-the-console] <Steps> <Step> ### Open the glossary [#open-the-glossary] Open the project and select **Glossary** in the sidebar. </Step> <Step> ### Open the form [#open-the-form] Select **Add term**. The **Add glossary term** dialog opens. </Step> <Step> ### Describe the term [#describe-the-term] - **Source term**: the word or phrase as it appears in your source language. Up to 255 characters. - **Notes** (optional): context for translators, such as what the term refers to. - Tick **Case-sensitive** if capitals are part of the term. - Tick **Do not translate** to keep the term exactly as written in every language, or **Forbidden** if the term must never appear in a translation. </Step> <Step> ### Save [#save] Select **Add term**. The notice reads **"Term added."** </Step> </Steps> The term applies from the next translation, and the next quality check checks existing translations against it. It does not retranslate what you already have. ## Set the wording for each language [#set-the-wording-for-each-language] The **Add glossary term** dialog sets the term and its rules. To fix the wording each language must use, such as `Kasse` for `Checkout` in German, import a CSV or TBX file with a column per language. Importing a term that is already in the glossary updates it rather than adding it twice, so you can add wording to terms you created in the console. See [Import and export a glossary](https://transglot.ai/docs/guides/quality/import-and-export-a-glossary). ## Add terms through the API [#add-terms-through-the-api] A [deploy token](https://transglot.ai/docs/developers/deploy-tokens) with the `manage` ability can create a term, wording included, with `POST https://api.transglot.ai/v1/glossary`: ```bash title="Terminal" curl -X POST https://api.transglot.ai/v1/glossary \ -H "Authorization: Bearer $TRANSGLOT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "source_term": "Checkout", "part_of_speech": "noun", "notes": "The payment page.", "translations": { "de": "Kasse", "fr": "Paiement" } }' ``` `case_sensitive`, `do_not_translate` and `forbidden` are optional booleans. A term marked do not translate or forbidden ignores `translations`. See the [API reference](https://transglot.ai/docs/api-reference). ## If it doesn't work [#if-it-doesnt-work] - **No Add term button**: your role cannot change the glossary. Ask a Developer, an Admin or an account Owner. - **"That term is already in this glossary."**: each source term appears once. To change an existing term's wording, import a file that includes it. - **A translation ignores the term**: check that the source text contains the term as written (with the same capitals, if the term is case-sensitive), and that **Use the glossary** is on in the project's [AI profile](https://transglot.ai/docs/guides/quality/brand-voice#the-ai-profile). With [enforcement](https://transglot.ai/docs/guides/quality/the-glossary#enforcement) set to **Off**, the checks do not flag a translation that departs from the term. # Brand voice (https://transglot.ai/docs/guides/quality/brand-voice/) A project's voice is how its translations should sound: how formal, how warm, which words you always use and which you never do. You set it once in the project's **Settings → Voice**, and it travels with every translation request for the project. Owners and Admins can change the voice. The style guide is available on every plan; inferring a guide from your work and the AI profile are included on Team and above. ## The style guide [#the-style-guide] ### How you sound [#how-you-sound] The **How you sound** card holds the guide itself: | Field | What it sets | | --- | --- | | **Formality** | How the product addresses the reader, from **very informal** to **very formal**. In French, for example, this is the choice between *tu* and *vous*. | | **Warmth** | How much the writing sounds like a person, from **clinical** to **very warm**. | | **Brand voice** | The product's personality in a line: the words you prefer and the ones you never use. | | **Regional notes** | Anything true of one market and not the others, such as currency, address order, or a word that means something else there. | | **Anything else worth saying** | Free-text guidance that does not fit the fields above. | ### Lines you never cross [#lines-you-never-cross] **Lines you never cross** holds two lists of rules: **Always** and **Never**. Type a rule and press Enter to add it. The card shows how many rules travel with each request and how long each can be. ### One voice is not enough [#one-voice-is-not-enough] **One voice is not enough** lets one language differ from the rest. Select **Every locale** to edit the base guide, or a language to edit its override. A language inherits the base guide and overrides only the fields you change, so German can be formal while Spanish is not, and both keep the same rules. ### Save [#save] Changes are not applied until you select **Save changes** in the bar at the bottom of the page. **Discard** throws them away. ## Infer the guide from your work [#infer-the-guide-from-your-work] Instead of writing the guide from scratch, select **Re-infer from your work** at the top of the page. Glossa reads the translations your team has already approved for the language you have selected (the first target language when **Every locale** is selected) and proposes a guide from them. The button reads **Reading your work…** while it works. What comes back is a draft, filled into the language's fields: **"Draft style guide inferred for de. Review it below, then save."** Nothing is applied until you correct it and select **Save changes**. Inferring uses your AI allowance and is included on Team and above. ## The AI profile [#the-ai-profile] On Team and above, the AI profile adds two things to the style guide. **Does it hold** holds example pairs: a source string, the translation you want matched, and an optional note on why it is right. Glossa is told to match them. One pair you have already signed off often teaches more than any adjective. The card shows how many pairs travel with each request. **What it is built from** shows what goes into every request, with three switches: | Switch | What it decides | | --- | --- | | **Use the glossary** | Whether matched [glossary](https://transglot.ai/docs/guides/quality/the-glossary) terms are given to Glossa for this project. | | **Use the style guide** | Whether the style guide on this page is applied. Turned off, the guide reaches no request. The **applied to every draft** pill at the top of the page switches the same thing. | | **Use semantic memory** | Whether Glossa may draw on similar translations a person has already reviewed. See [Translation memory](https://transglot.ai/docs/concepts/translation-memory). | With an AI profile in place, quality checks also re-read what comes back and flag a translation that has drifted from the register you set. See [Quality checks](https://transglot.ai/docs/guides/quality/quality-checks). To go back to no profile, select **Remove profile** and confirm. Translations made from the next run on no longer use the profile; strings already translated keep their wording. ## When changes apply [#when-changes-apply] The voice applies to translations made after you save. It does not rewrite existing translations. To bring older strings in line, retranslate them, or [run a quality check](https://transglot.ai/docs/guides/quality/run-a-quality-check) to find the ones that drifted. ## Related [#related] <Cards> <Card title="The glossary" href="/guides/quality/the-glossary" /> <Card title="AI translation settings" href="/guides/translating/ai-translation-settings" /> <Card title="Glossary and brand voice" href="/concepts/glossary-and-brand-voice" /> </Cards> # Configure checks and automation (https://transglot.ai/docs/guides/quality/configure-checks/) The project's check settings live in **Settings → Checks**, which **Configure** on the Quality page also opens. Owners and Admins can change them. Every setting saves the moment you change it. ## Confidence routing [#confidence-routing] **Route AI translations by confidence** is the first panel on the page. Each AI translation carries a confidence signal. Translations that pass the automatic checks and come back confident can be marked as not needing review, so your reviewers only see the ones worth their time. Everything else is flagged for review. Automatic checks always win: a translation with a broken placeholder, the wrong plural forms, or a glossary conflict is always sent to review no matter how confident it was. It is off by default and available on Team and above. When you switch it on, a **Threshold** field sets how confident a translation has to be. ## Checks & automation [#checks--automation] | Setting | Options | What it decides | | --- | --- | --- | | **Self-heal** | **Repair 12 strings** | Starts a one-off repair of the failing AI translations. See [Self-heal a translation](https://transglot.ai/docs/guides/quality/self-heal-a-translation). | | **Glossary enforcement** | **Off**, **Warn** (default), **Enforce** | How strictly the terminology check treats glossary terms. See below. | | **Message syntax** | **Auto** (default), **ICU**, **Legacy** | How the placeholder and plural checks read your strings. | | **ICU dialect** | **ICU4C** (default), **Flutter** | Which ICU rules apply to apostrophes. | | **Auto-heal mode** | **Off**, **Suggest** (default), **Apply** | What happens to an AI translation a quality check flags. | | **Source drift repair** | **Off**, **Suggest** (default), **Auto** | What happens to translations when their source string changes. | **Auto-heal mode** and **Source drift repair** run on Team and above. On other plans they can only be set to **Off**, and whatever is selected does nothing. ### Glossary enforcement [#glossary-enforcement] The glossary is always given to Glossa when it translates. Enforcement decides what the terminology check does when a translation still departs from it: - **Off**: terminology is not checked. - **Warn**: a translation that misses a required term is kept and flagged for review. - **Enforce**: a translation that misses a required term is treated as failing the check rather than only flagged. Under **Warn** and **Enforce**, a forbidden term in a translation always fails the check. You can also change enforcement from the [Glossary](https://transglot.ai/docs/guides/quality/the-glossary) page. ### Message syntax and ICU dialect [#message-syntax-and-icu-dialect] **Message syntax** tells the placeholder and plural checks how to read your strings: - **Auto** reads a string as ICU MessageFormat only when it holds a plural, select, ordinal, number, date or time argument. - **ICU** reads every string as ICU and checks apostrophes and tags strictly. - **Legacy** never reads ICU. On **ICU**, a translation that is not valid ICU MessageFormat is left out of what your app receives, from a [pull](https://transglot.ai/docs/developers/cli) straight away and from the CDN at the language's next [publish](https://transglot.ai/docs/guides/delivery/publish-a-locale). A version already live keeps its content until then. **ICU dialect** matters for apostrophes: **Flutter** reads an apostrophe as plain text, the way Flutter's own tooling does. Pushing a Flutter ARB file sets it for you. ### Auto-heal mode [#auto-heal-mode] After a quality check flags an AI translation, auto-heal decides what to do with it: - **Off**: nothing. The finding waits for a reviewer. - **Suggest**: a suggested fix is attached to the finding. A reviewer sees it under **Quality findings** in the editor and can select **Accept suggestion**. - **Apply**: Glossa writes a fixed translation, which must pass every automatic check before it is saved. A translation you have edited or reviewed is never overwritten by AI. Auto-heal only acts on translations Glossa wrote. See [Self-heal a translation](https://transglot.ai/docs/guides/quality/self-heal-a-translation). ### Source drift repair [#source-drift-repair] When someone changes a source string, the existing translations of it may no longer match. With source drift repair on, transglot re-checks those translations shortly after the change (several quick edits to the same string are checked once) and: - **Suggest**: attaches a candidate fix for a reviewer to accept. - **Auto**: rewrites the AI translations the change broke. A translation you have edited or reviewed is never overwritten by AI. This setting uses your AI allowance on every source edit that triggers it. See [What counts towards my usage](https://transglot.ai/docs/administration/billing/what-counts-towards-my-usage). # Connect the in-context editor (https://transglot.ai/docs/guides/quality/connect-the-in-context-editor/) The in-context editor lets a reviewer open any string where it appears in your app, read it in place, and, once editing is turned on, change it and capture a screenshot for the [screenshot library](https://transglot.ai/docs/guides/quality/the-screenshot-library). This page covers what an Owner or Admin sets up in transglot. Adding the editor to your app's code is a developer task, covered in [Install the in-context editor](https://transglot.ai/docs/developers/in-context/install). The in-context editor is included on Starter and above. ## Create a publishable key [#create-a-publishable-key] The editor identifies your project with a **publishable key**. It is designed to sit in your site's code, where anyone can read it, so it only works from the web addresses you list. <Steps> <Step> ### Open the Connection card [#open-the-connection-card] Open the project and select **Screenshots** in the sidebar. The key form is in the **Connection** card. (The same keys are listed in **Settings → Developer** under **Publishable keys**.) </Step> <Step> ### Name the key [#name-the-key] Enter a **Key name** that identifies the site, such as `Production web app`. Use one key per app you add the editor to. </Step> <Step> ### List the allowed origins [#list-the-allowed-origins] In **Allowed origins (one per line)**, list each address the editor will run on, with its scheme and any port: ```text title="Allowed origins" https://app.example.com https://staging.example.com ``` </Step> <Step> ### Create it [#create-it] Select **Create publishable key**. The **SDK snippet** card below is now pre-filled with the key. </Step> </Steps> Once the editor runs on your site, the key's status in **Connection** changes from **Never connected** to **Connected**, then shows when it was **Last seen**. ## Show the editor to reviewers only [#show-the-editor-to-reviewers-only] The editor cannot tell a reviewer from a visitor, so your app decides who gets it. The snippet on the **Screenshots** page, and under **One provider** in [Releases → Connect & SDK](https://transglot.ai/docs/guides/delivery/the-deploy-screen#the-connect--sdk-view), adds the editor only for signed-in users your app treats as reviewers. Keep that check when you adapt it. ## Turn on editing and screenshots [#turn-on-editing-and-screenshots] With the publishable key alone, the editor is **read-only**: reviewers can read strings in place, but saving is disabled, AI suggestions are not offered, and nothing is captured for the screenshot library. A key that anyone can copy from your page source must never be enough to change your translations. To let a reviewer edit, your own server vouches for them: 1. Your app passes the editor the strings this reviewer may change (`writableKeys`) and a function (`getNonce`) that asks **your** server for a one-time pass. 2. Your server calls `POST https://api.transglot.ai/v1/incontext/session-nonce` with a [deploy token](https://transglot.ai/docs/developers/deploy-tokens) that has the `write` ability, naming the publishable key and the strings, and returns the single-use nonce it gets back. The nonce expires about a minute after it is issued. 3. The editor trades the nonce for an editing session limited to those strings. The deploy token is a secret: keep it on your server, never in the page. The snippet in the console already includes `writableKeys` and `getNonce`, wired to a `/api/transglot-nonce` route on your side for you to implement. [Install the in-context editor](https://transglot.ai/docs/developers/in-context/install) has the server code. Once this is in place, **Save** works, AI suggestions appear, and the screenshots reviewers capture while editing reach the library. ## Delete a key [#delete-a-key] Select **Delete** next to the key in **Connection** and confirm. Every site using it stops loading the editor straight away. Create a replacement and roll it out first if the key is in use. ## If it doesn't work [#if-it-doesnt-work] | What you see | What to check | | --- | --- | | The editor does not load | The site's address must be in **Allowed origins** exactly, including `https://` and any port. Otherwise the editor is refused with **"This publishable key is not authorized for this origin."** | | Every field is read-only and Save is disabled | The editor has no `getNonce`, or your server did not return a nonce. That is the read-only default, not a fault: see [Turn on editing and screenshots](#turn-on-editing-and-screenshots). | | Nothing reaches the screenshot library | Same cause: screenshots are captured as part of an edit, so a read-only session never produces one. | | The key form is not shown | Only Owners and Admins can create publishable keys or see the existing ones. | | **"The in-context editor unlocks on the Starter plan."** | Your plan does not include the in-context editor. | ## Related [#related] <Cards> <Card title="In-context editor for developers" href="/developers/in-context" description="Install, configure and secure the editor in your app." /> <Card title="Publishable keys" href="/developers/in-context/publishable-keys" /> <Card title="The screenshot library" href="/guides/quality/the-screenshot-library" /> </Cards> # Import and export a glossary (https://transglot.ai/docs/guides/quality/import-and-export-a-glossary/) Import is the way to set the wording each language must use for a term, and to bring a termbase over from another tool. Export gives you the whole glossary as a file. Owners, Admins and Developers can import and export, on Starter and above. ## Import a file [#import-a-file] <Steps> <Step> ### Open the import [#open-the-import] On the project's **Glossary** page, select **Import CSV or TBX**. The **Import terms from a file** dialog explains the columns a CSV can have. </Step> <Step> ### Start from the template (optional) [#start-from-the-template-optional] Select **Download a CSV template** for a file with the right columns and one column per target language of this project. </Step> <Step> ### Choose your file [#choose-your-file] Select **Choose a file** and pick a CSV or TBX file of up to 20 MB. Nothing is imported yet: transglot reads the file and shows what it would change. </Step> <Step> ### Check the preview [#check-the-preview] **Import these changes?** counts the **New terms**, **Changed terms**, terms **Already the same**, and **Rows skipped**, and lists each change under **What changes**. Read the skipped rows and any columns the import will not read. </Step> <Step> ### Import [#import] Select **Import 12 terms**. The notice confirms what happened, for example **"Imported 12 terms: 9 added, 3 updated."** </Step> </Steps> ## CSV columns [#csv-columns] The first row is the header. Column names are not case-sensitive. | Column | Required | What it holds | | --- | --- | --- | | `source_term` | Yes | The term as it appears in your source text. Up to 255 characters. | | One column per language | No | Headed by the language code, such as `fr` or `de_DE`. Each cell holds the wording that language must use. | | `part_of_speech` | No | The kind of word, such as `noun`. Up to 30 characters. | | `notes` | No | A note for translators. Up to 2,000 characters. | | `case_sensitive` | No | `1` for yes, `0` for no. | | `do_not_translate` | No | `1` for yes, `0` for no. | | `forbidden` | No | `1` for yes, `0` for no. | ```csv title="glossary.csv" source_term,part_of_speech,case_sensitive,do_not_translate,forbidden,notes,de,fr Checkout,noun,0,0,0,The payment page.,Kasse,Paiement Acme Cloud,,1,1,0,Product name.,, ``` ## TBX files [#tbx-files] A TBX file exported from a terminology tool works too. Each entry needs a term in the project's source language; entries without one are skipped. ## How an import changes the glossary [#how-an-import-changes-the-glossary] - **A term already in the glossary is updated, not added twice.** This is how you add wording to terms you created in the console. - **An empty cell, or a column the file does not have, leaves what is already there.** Wording in languages the file leaves out is kept. - **Marking a term do not translate or forbidden removes its wording**, because those terms carry none. The preview warns you before you import. - **Importing does not approve a candidate.** A term waiting under **Review candidates** stays there. A term you rejected earlier stays rejected. - **Your language access applies.** If your access is limited to some languages and the file changes wording in others, the import is refused: **"This file changes wording in languages you do not have access to, so it cannot be imported."** Remove those changes from the file, or ask a project admin to import it. ## Export the glossary [#export-the-glossary] Select **Export** on the **Glossary** page to download every approved term as `glossary-{project}.csv`, in the same columns the import reads. The file can be edited and imported again. To export as TBX, use the API with a [deploy token](https://transglot.ai/docs/developers/deploy-tokens) that has the `read` ability: ```bash title="Terminal" curl "https://api.transglot.ai/v1/glossary/export?format=tbx" \ -H "Authorization: Bearer $TRANSGLOT_TOKEN" \ -o glossary.tbx ``` The API also accepts an import at `POST https://api.transglot.ai/v1/glossary/import`. See the [API reference](https://transglot.ai/docs/api-reference). ## If it doesn't work [#if-it-doesnt-work] - **"The columns in this file were not recognised, so no row can be read."**: the file has no `source_term` column. Check the header row. - **"These columns are not read"**: a column is neither one of the fixed fields nor a language code. Head each language column with its code, such as `fr` or `de_DE`. - **"That file is larger than 20 MB."**: split it into smaller files. - **"That file could not be read."**: check it is a CSV or TBX file. - **Rows skipped**: the preview gives each skipped row's number and reason, such as **"Row 7: no source term."** # Quality checks (https://transglot.ai/docs/guides/quality/quality-checks/) transglot checks translations for specific, nameable problems: a placeholder that went missing, a plural form the language needs, a glossary term that was not used, a sentence that changes the meaning. Each problem becomes a **finding** on the string, and a string with a serious finding is flagged for a person to check. ## What is checked [#what-is-checked] There are two kinds of check. The automatic checks follow fixed rules and cost nothing. The AI review reads the translation against its source the way a reviewer would. | Check | Kind | What it catches | | --- | --- | --- | | **Placeholders** | Automatic | A placeholder or variable from the source (such as `{name}` or `%s`) that is missing, extra or altered in the translation. | | **Plurals** | Automatic | A plural string that lacks a form its language needs. | | **Length** | Automatic | A translation longer than the maximum length set on its key. | | **Overflow** | Automatic, advisory | A translation so much longer than its source that it may not fit where the source fits. It is reported but never blocks. | | **Terminology** | Automatic | A [glossary](https://transglot.ai/docs/guides/quality/the-glossary) term translated differently from its required wording, or a forbidden term that appears. | | **Accuracy** | AI review | The translation does not say what the source says: an omission, an addition or a mistranslation. | | **Fluency** | AI review | Grammar, spelling or punctuation that is wrong in the target language. | | **Style** | AI review | Wording that is awkward, inconsistent, or off the project's [voice](https://transglot.ai/docs/guides/quality/brand-voice). | If the project's **Screenshot context** setting is on, the AI review also sees the screenshot captured for a key, so it can flag text that overflows the space it renders in. See [The screenshot library](https://transglot.ai/docs/guides/quality/the-screenshot-library). ## When the checks run [#when-the-checks-run] - **As each AI translation is written.** Every translation Glossa produces goes through the automatic checks before it is saved. Automatic checks always win: a translation with a broken placeholder, the wrong plural forms, or a glossary conflict is always sent to review no matter how confident it was. - **While you type in the editor.** The editor checks your draft against the same rules before you save it. - **When you run a quality check.** A full check runs the automatic checks and the AI review, and gives each translation a [quality score](https://transglot.ai/docs/guides/quality/what-does-the-quality-score-mean). See [Run a quality check](https://transglot.ai/docs/guides/quality/run-a-quality-check). - **When a source string changes**, if **Source drift repair** is on. See [Configure checks and automation](https://transglot.ai/docs/guides/quality/configure-checks). <Callout title="Plans"> The automatic checks run on every plan. The AI review, the quality score, the [scorecard](https://transglot.ai/docs/guides/quality/the-per-language-scorecard) and [self-heal](https://transglot.ai/docs/guides/quality/self-heal-a-translation) are included on Team and above. See [pricing](https://transglot.ai/pricing). </Callout> ## Severity [#severity] Every finding has one of three severities: **minor**, **major** or **critical**. Critical findings are counted as **blocking** on the Quality page. A translation is flagged for review when it has a finding of major severity or above, or when its quality score is low. A flag does not stop a translation from being published. It puts the string in front of a reviewer: flagged strings appear under **Needs checking** in the editor and on [Releases](https://transglot.ai/docs/guides/delivery/the-deploy-screen). See [Why was my translation flagged for review?](https://transglot.ai/docs/guides/translating/why-was-my-translation-flagged-for-review) ## The Quality page [#the-quality-page] Open the project and select **Quality** in the sidebar. Everyone who can open the project can read it. Before the first check has run, the page lists each language with how many of its translated strings are waiting to be scored, under **Nothing here has been checked yet**. After a check, the page header shows: - the **mqm · project average** score, with its change and trend over the last 12 weeks, - how many translated strings have been checked (**checked 120 of 140 translated strings**), - the count of blocking findings and warning findings, - how many strings need checking, and when the last check ran. Below the header: | Section | What it shows | | --- | --- | | **Every check, every locale** | A grid of languages against checks, with the number of findings in each cell. A language added since the last check reads **not scored**. Select a cell to filter the findings list to that language and check. | | **Findings** | The strings behind the selected cell: each finding's check, severity and message. | | **MQM dimensions** | The selected language's findings grouped into the five MQM categories. | | **All languages** | Each language's score. | The buttons in the header: - **Configure** opens the project's check settings (Owners and Admins). - **Re-run checks** starts a quality check. See [Run a quality check](https://transglot.ai/docs/guides/quality/run-a-quality-check). - **Fix 12 with Glossa** repairs the failing AI translations. See [Self-heal a translation](https://transglot.ai/docs/guides/quality/self-heal-a-translation). The **Scorecard** tab beside **Overview** opens the [per-language scorecard](https://transglot.ai/docs/guides/quality/the-per-language-scorecard). ## Findings in the editor [#findings-in-the-editor] In the editor, a string with findings shows how many it has. Open them to see **Quality findings** for that string: each finding's category, severity and message. When a finding comes with a suggested fix, **Accept suggestion** saves it as your own edit. # Review extracted glossary terms (https://transglot.ai/docs/guides/quality/review-extracted-glossary-terms/) An extraction reads your project's source strings, finds the terms that recur, and proposes the ones worth pinning as glossary candidates. Nothing is added on your behalf: a candidate is not enforced until someone approves it. <Callout title="Who can extract"> Owners, Admins and Developers, on Team and above. An extraction uses your AI allowance, and each plan includes a set number of extraction runs per cycle. </Callout> ## Run an extraction [#run-an-extraction] <Steps> <Step> ### Open the candidates [#open-the-candidates] Open the project's **Glossary** and select the **Review candidates** tab. </Step> <Step> ### Extract terms [#extract-terms] Select **Extract terms**. The notice reads **"Extraction queued. Terms appear under Review candidates as the run finds them."** </Step> </Steps> An extraction only reads source strings that changed since the last one, so running it again on an unchanged project finds nothing new and costs nothing. Terms already in the glossary, in any state, are never proposed again. When there is nothing new, the notice reads **"Nothing to extract. No new candidate terms were found in the source strings."** ## Review the candidates [#review-the-candidates] Candidates are listed under **Waiting for your decision**, most confident first. Each shows the term, its kind of word where one was found, and how confident the extraction is that it belongs in a glossary. - **Approve** moves the term to the **Terms** tab, where it is matched and enforced from the next translation: **"“Checkout” approved. It is now under Terms."** - **Reject** drops it: **"“Checkout” rejected. It will not be proposed again."** A candidate can carry suggested wording for your target languages. The candidate list does not show it; once the term is approved, its wording appears in the language columns of the **Terms** tab. Check it there, and to change it, [import a file](https://transglot.ai/docs/guides/quality/import-and-export-a-glossary) that includes the term. ## From the API [#from-the-api] A [deploy token](https://transglot.ai/docs/developers/deploy-tokens) with the `manage` ability can start an extraction with `POST https://api.transglot.ai/v1/glossary/extract`. A token with the `read` ability can list candidates with `GET https://api.transglot.ai/v1/glossary?status=pending`. See the [API reference](https://transglot.ai/docs/api-reference). ## If it doesn't work [#if-it-doesnt-work] - **"Nothing awaiting review"**: no extraction has run, or you have already decided on everything it found. - **"Candidate extraction unlocks on the Team plan."**: extraction is not on your plan. You can still [add terms](https://transglot.ai/docs/guides/quality/add-a-glossary-term) yourself. - **No Extract terms button**: your role cannot change the glossary. Ask a Developer, an Admin or an account Owner. # Run a quality check (https://transglot.ai/docs/guides/quality/run-a-quality-check/) A quality check runs the automatic checks and the AI review over your translations, gives each one a [quality score](https://transglot.ai/docs/guides/quality/what-does-the-quality-score-mean), and records what it finds. See [Quality checks](https://transglot.ai/docs/guides/quality/quality-checks) for what is checked. <Callout title="Who can run a check"> Owners, Admins and Developers, on Team and above. A check uses your AI allowance, and the confirmation tells you how much before anything starts. </Callout> ## From the Quality page [#from-the-quality-page] <Steps> <Step> ### Open Quality [#open-quality] Open the project and select **Quality** in the sidebar. </Step> <Step> ### Pick a language (optional) [#pick-a-language-optional] Select a language's row in **Every check, every locale** to check that language first. With no row selected, the check covers every language. </Step> <Step> ### Start the check [#start-the-check] Select **Re-run checks**. The button reads **Working out the cost…** while transglot counts what the check would cover. </Step> <Step> ### Confirm [#confirm] The confirmation, **Run the checks on German?**, shows how many strings and words the check covers, and what that leaves of this cycle's allowance. Under **Which languages**, choose **German only** or every language. Select the button that names the word count, for example **Check 1,240 words**, to start. </Step> </Steps> The check runs in the background, so you can leave the page. As it works through your strings, the grid, the findings and the scores fill in. When it finishes, the people on the project get a **QA finished** notification, unless they have turned **QA run completed** off in their [notification preferences](https://transglot.ai/docs/administration/account/notification-preferences). ## From the editor [#from-the-editor] <Steps> <Step> ### Open the language [#open-the-language] Open the **Editor** and choose the language to check. </Step> <Step> ### Run QA [#run-qa] Select **Run QA** in the toolbar and confirm. The button reads **Checking quality…** while the check starts. </Step> </Steps> Findings appear on each string in the editor: a string with findings shows how many, and opening them shows **Quality findings** for that string. ## What a check covers [#what-a-check-covers] A check only reads what has changed. A translation that has not changed, and whose source has not changed, since it was last checked is skipped, so running a second check soon after the first costs a fraction of the first. Findings never change your translations. They tell you what to look at. To have transglot fix what it finds, see [Self-heal a translation](https://transglot.ai/docs/guides/quality/self-heal-a-translation). If **Auto-heal mode** is on for the project, the confirmation says so: strings the check flags are then sent for a fresh translation afterwards, which spends their own words on top of the check. ## From the API [#from-the-api] A [deploy token](https://transglot.ai/docs/developers/deploy-tokens) with the `manage` ability can start a check with `POST https://api.transglot.ai/v1/qa/run`. See the [API reference](https://transglot.ai/docs/api-reference). ## If it doesn't work [#if-it-doesnt-work] - **No Re-run checks button**: your role cannot run checks, or your plan does not include the AI review. On plans without it, the Quality page reads **"Agentic QA scoring unlocks on the Team plan."** - **"Nothing to check. No string in German needs a check right now."**: every translation in that language has already been checked and nothing has changed since. - **"The figures for this run could not be loaded, so nothing was started."**: try again in a moment. - **The check is refused for your allowance**: the confirmation says so before anything starts, and nothing is charged. See [What counts towards my usage](https://transglot.ai/docs/administration/billing/what-counts-towards-my-usage). # Self-heal a translation (https://transglot.ai/docs/guides/quality/self-heal-a-translation/) Self-heal takes an AI translation that a [quality check](https://transglot.ai/docs/guides/quality/quality-checks) flagged and asks Glossa for a better one. It works in two ways: automatically after each quality check, following the project's **Auto-heal mode**, or as a one-off repair you start yourself. Self-heal is included on Team and above. ## What self-heal may and may not change [#what-self-heal-may-and-may-not-change] | Self-heal can | Self-heal never | | --- | --- | | Rewrite a translation Glossa wrote that a check flagged for review. | Touches a translation you have edited or reviewed. A translation you have edited or reviewed is never overwritten by AI. | | Attach a suggested fix to a finding, for a reviewer to accept. | Saves a rewrite that breaks an automatic check: one with a broken placeholder, missing plural forms or a forbidden glossary term is discarded. If no attempt passes, the translation is left as it was. | | Save its rewrite as a new version in the string's history. | Approves a translation. A repaired translation is still an AI draft until someone approves it. | Because every applied repair is saved as a version, you can restore the previous wording from the string's [version history](https://transglot.ai/docs/guides/translating/version-history). ## Automatic self-heal [#automatic-self-heal] Set **Auto-heal mode** in the project's **Settings → Checks** (Owners and Admins): - **Off**: flagged translations wait for a reviewer. - **Suggest** (the default): after a check, each flagged AI translation gets a suggested fix attached to its finding. Nothing in the translation changes. A reviewer opens **Quality findings** for the string in the editor and selects **Accept suggestion**, which saves the fix as their own edit. - **Apply**: after a check, Glossa rewrites each flagged AI translation, within the limits above. Automatic self-heal uses your AI allowance. When **Auto-heal mode** is on, the confirmation for a quality check says that flagged strings will be sent for a fresh translation afterwards. ## Repair the failing translations now [#repair-the-failing-translations-now] Owners, Admins and Developers can repair every flagged AI translation in one go, whatever the **Auto-heal mode**. <Steps> <Step> ### Start the repair [#start-the-repair] On the project's **Quality** page, select **Fix 12 with Glossa** (the number is how many flagged AI translations there are). Owners and Admins can also use **Repair 12 strings** in **Settings → Checks**. </Step> <Step> ### Read the confirmation [#read-the-confirmation] **Repair 12 strings?** states what will happen: Glossa re-translates the failing AI translations and overwrites their current values. It shows how many words that is and what it leaves of this cycle's allowance. If the project's **Auto-heal mode** is **Off**, it also says that running the repair overrides that setting this once. </Step> <Step> ### Confirm [#confirm] Select the button that names the word count, for example **Repair 84 words**. The repair runs in the background. </Step> </Steps> When it finishes, the project's Owners, Admins and Developers get a **Self-heal completed** notification. The repaired translations are not scored yet: [run a quality check](https://transglot.ai/docs/guides/quality/run-a-quality-check) to score them again. If your access is limited to some languages, the repair covers those languages only. ## If it doesn't work [#if-it-doesnt-work] - **No Fix with Glossa button**: your role cannot run repairs, or your plan does not include self-heal. - **Nothing left to fix** or **"There is nothing for a repair to rewrite right now."**: no AI translation is currently flagged. Flagged translations that someone edited or reviewed are not repaired, by design. - **A translation is still flagged after a repair**: no rewrite passed the automatic checks, so the original was kept. Fix it in the editor. # The glossary (https://transglot.ai/docs/guides/quality/the-glossary/) The glossary is the project's list of terms with fixed rules: your product name that is never translated, a feature name with an approved wording in each language, a word you never want to see. Glossary terms are matched against source text and enforced during translation and during quality checks; a translation that departs from a glossary term is flagged. Open the project and select **Glossary** in the sidebar. The glossary is included on Starter and above. ## What a term can say [#what-a-term-can-say] | Rule | What it means | | --- | --- | | **Wording per language** | The exact wording a language must use for the term, such as `Kasse` in German for `Checkout`. Set it by [importing a file](https://transglot.ai/docs/guides/quality/import-and-export-a-glossary) or through the API. | | **Do not translate** | The term stays exactly as written in every language, such as a brand or product name. It carries no wording. | | **Forbidden** | The term must never appear in a translation, such as a retired product name. It carries no wording. | | **Case-sensitive** | The term only matches with the same capitals, so `Pro` and `pro` are different terms. | | **Notes** | Context for translators, such as what the term refers to. | | **Kind of word** | The part of speech, such as noun. Set by import or the API. | ## How terms are used [#how-terms-are-used] - **During translation.** When a string contains a term, the term travels with the request, so Glossa uses the wording you pinned. - **In the checks.** The terminology check compares each translation with the glossary. See [Quality checks](https://transglot.ai/docs/guides/quality/quality-checks). - **In the editor.** The **Glossary** section of the editor's context panel lists the terms in the string you are on, with what each must become, so nobody has to remember the list. Adding or changing a term does not retranslate what you already have. [Run a quality check](https://transglot.ai/docs/guides/quality/run-a-quality-check) to find existing translations that conflict with it. ## Enforcement [#enforcement] The **Enforcement** control on the **Locked terms** card sets how strictly the terminology check treats the glossary: | Setting | A required term is missing | A forbidden term appears | | --- | --- | --- | | **Off** | Not checked | Not checked | | **Warn** (default) | Flagged for review | Fails the check | | **Enforce** | Fails the check | Fails the check | Owners and Admins can change it, here or in [Settings → Checks](https://transglot.ai/docs/guides/quality/configure-checks#glossary-enforcement). ## The Glossary page [#the-glossary-page] | Part | What it does | | --- | --- | | **Terms** tab | Every approved term, under **Locked terms**, with a column per language for its wording. **Filter by locale** narrows the list to one language. | | **Review candidates** tab | Terms an AI extraction proposed, waiting for approval. See [Review extracted glossary terms](https://transglot.ai/docs/guides/quality/review-extracted-glossary-terms). | | **Add term** | Adds a term. See [Add a glossary term](https://transglot.ai/docs/guides/quality/add-a-glossary-term). | | **Import CSV or TBX** and **Export** | Bring terms in from a file, or download them. See [Import and export a glossary](https://transglot.ai/docs/guides/quality/import-and-export-a-glossary). | Everyone who can open the project can read the glossary. Owners, Admins and Developers can add, import, export and delete terms. If your access is limited to some languages, you can change wording in those languages only. ## Delete a term [#delete-a-term] Select **Delete** on the term's row, then **Delete term** to confirm. The term stops being enforced from the next translation run. Wording already written into existing translations is left alone. ## Related [#related] <Cards> <Card title="Add a glossary term" href="/guides/quality/add-a-glossary-term" /> <Card title="Import and export a glossary" href="/guides/quality/import-and-export-a-glossary" /> <Card title="Brand voice" href="/guides/quality/brand-voice" /> </Cards> # The per-language scorecard (https://transglot.ai/docs/guides/quality/the-per-language-scorecard/) The scorecard turns the results of your [quality checks](https://transglot.ai/docs/guides/quality/quality-checks) into one score per language, in the industry-standard MQM vocabulary. Use it to compare languages, to see which kind of problem a language has, and to answer a procurement questionnaire or a renewal review without assembling the numbers by hand. Open the project, select **Quality**, then the **Scorecard** tab. Everyone who can open the project can read it. The figures are included on Team and above; on other plans the page keeps its layout with no figures, under **"The MQM quality scorecard unlocks on the Team plan."** ## What it shows [#what-it-shows] The scorecard re-expresses findings your quality checks already produced. It does not re-examine your translations, and reading or exporting it costs nothing from your AI allowance. **Per-language quality** has one row per target language: | Column | What it shows | | --- | --- | | **Language** | The target language. | | **MQM score** | The language's quality score, 0 to 100. See [What does the quality score mean?](https://transglot.ai/docs/guides/quality/what-does-the-quality-score-mean) | | **Checked of translated** | How many of the language's translated strings have been checked. A score covers only those. | | **% needing a check** | The share of translated strings flagged for review. | | **Avg confidence** | The average confidence of the language's AI translations. | Below the table: - **Auto-approve rate**, **Strings gate-scored** and **Mean confidence**: how much of the AI output was routed past review, for projects that route by confidence. See [Configure checks and automation](https://transglot.ai/docs/guides/quality/configure-checks#confidence-routing). - **Top MQM error dimensions**: where findings fall across Accuracy, Fluency, Terminology, Style and Locale convention. - **Quality over time**: each language's weekly score over the last 12 weeks. - A methodology note stating how the figures are computed, for the reader who wants to check them. ## Export the scorecard [#export-the-scorecard] <Tabs items={['PDF', 'CSV']}> <Tab value="PDF"> Select **Print / PDF** and choose your browser's save-as-PDF option. The console's navigation is left out of the printout, so what you get is the scorecard alone, with the project name and when it was generated. </Tab> <Tab value="CSV"> Select **Export CSV** (Owners and Admins). The file, `mqm-scorecard-{project}-{date}.csv`, has two blocks: 1. One row per language: `Language`, `Locale`, `MQM score`, `QA score`, `Checked`, `Total`, `% needs review`, `Avg confidence`. 2. One row per language with its number of findings in each MQM category. </Tab> </Tabs> On plans with [custom branding](https://transglot.ai/docs/administration/workspace/custom-branding), the printout carries your logo and product name, and the CSV is titled with your product name. Otherwise the CSV is titled with the workspace name. ## If it doesn't work [#if-it-doesnt-work] - **"No scored languages yet"**: the project has nothing to score yet. Translate some strings, then [run a quality check](https://transglot.ai/docs/guides/quality/run-a-quality-check). - **A language has no MQM score**: none of its strings has been checked yet. Run a check on that language. - **No Export CSV button**: exporting needs the Admin role, or an Owner of the account. # The screenshot library (https://transglot.ai/docs/guides/quality/the-screenshot-library/) A row in the editor does not tell you whether a translation fits its button. A screenshot does. The screenshot library collects the screenshots your reviewers capture with the [in-context editor](https://transglot.ai/docs/guides/quality/connect-the-in-context-editor) while they edit strings on your own site, each one tagged with the key it shows. Open the project and select **Screenshots** in the sidebar. Everyone who can open the project can browse it. Capturing new screenshots needs the in-context editor, which is included on Starter and above. ## What is on the page [#what-is-on-the-page] | Section | What it holds | | --- | --- | | **Connection** | The project's publishable keys, each with its status: **Connected**, **Last seen** with a time, or **Never connected**. Owners and Admins create keys here. See [Connect the in-context editor](https://transglot.ai/docs/guides/quality/connect-the-in-context-editor). | | **SDK snippet** | The code to add the in-context editor to your app, pre-filled with this project's key. | | **Screenshots can contain personal data.** | How screenshots are stored, and when they are deleted. | | **What your reviewers saw** | The gallery. Each screenshot is captioned with the key it shows, or **Untagged**. | ## Where screenshots are used [#where-screenshots-are-used] Screenshots are context, not content. They never change a translation. They are used: - **By reviewers**, to judge whether a translation fits the space and the situation. - **By Glossa**, when the project's **Screenshot context** setting is on (Settings → AI translation, Team and above). A key's screenshot then goes with its translation request, so the wording is written for where it appears, and [quality checks](https://transglot.ai/docs/guides/quality/quality-checks) can flag text that overflows the space it renders in. Screenshots add image cost to every request that uses them. - **In review sheets** exported for a person to work on, which include a link to each string's screenshot. ## Privacy and retention [#privacy-and-retention] Screenshots are taken of your live app, so they can show personal data. transglot strips the image metadata, stores screenshots privately, and serves them only through short-lived links. Every screenshot is deleted automatically 90 days after it is captured. ## Delete a screenshot [#delete-a-screenshot] Owners, Admins and Developers can delete a screenshot sooner, for example one that captured sensitive information. Select **Delete** on the screenshot, then **Delete screenshot** to confirm. The image is removed permanently. The keys it showed keep their translations but lose the visual context. ## If it doesn't work [#if-it-doesnt-work] - **"No screenshots yet"**: nobody has captured one. Screenshots are taken as part of an edit, so the in-context editor must be connected with editing turned on. A read-only session never captures one. See [Connect the in-context editor](https://transglot.ai/docs/guides/quality/connect-the-in-context-editor#turn-on-editing-and-screenshots). - **"The in-context editor unlocks on the Starter plan."**: screenshots already captured stay visible and can be deleted, but new captures need a plan that includes the in-context editor. # What does the quality score mean? (https://transglot.ai/docs/guides/quality/what-does-the-quality-score-mean/) The quality score is a 0-100 rating that transglot produces for a translation, alongside a list of specific findings, each with a severity. The scorecard groups those findings using the industry-standard MQM categories so they can be compared with other tools' reports. ## Where you see it [#where-you-see-it] - **On a string.** A checked translation carries its score and its findings. The editor's grid shows it as a colour-coded **MQM** chip, and the findings list on the Quality page labels it **passed**, **needs review** or **failed**. - **Per language and per project.** The [Quality](https://transglot.ai/docs/guides/quality/quality-checks) page shows an MQM score for each language and the project average (**mqm · project average**), with its trend over the last 12 weeks. - **On the scorecard.** The [per-language scorecard](https://transglot.ai/docs/guides/quality/the-per-language-scorecard) puts every language's score side by side, ready to export. ## The five categories [#the-five-categories] Every finding falls into one of the five MQM categories: | Category | What it covers | Checks that report it | | --- | --- | --- | | **Accuracy** | The translation does not say what the source says. | Accuracy | | **Fluency** | Grammar, spelling or punctuation is wrong in the target language. | Fluency | | **Terminology** | A term departs from your glossary. | Terminology | | **Style** | Awkward, off-register or inconsistent wording. | Style | | **Locale convention** | Placeholders, plural forms, length limits, and text that may not fit. | Placeholders, Plurals, Length, Overflow | ## Reading a score [#reading-a-score] - **Read the findings, not only the number.** Two translations can score the same for different reasons. The findings say what is wrong and how serious it is; the score is the summary you report. - **A score is not a gate.** A low score does not stop anything from being published. It flags the string for review, and a reviewer decides. - **A score is not a certification.** It is transglot's assessment, expressed in a shared vocabulary so it can be compared with other reports. - **Only checked translations have a score.** A language added since the last check reads **not scored**. [Run a quality check](https://transglot.ai/docs/guides/quality/run-a-quality-check) to score it. - **A score describes the translation as it was checked.** When a translation or its source changes, the next check scores it again. The quality score is included on Team and above. # AI translation settings (https://transglot.ai/docs/guides/translating/ai-translation-settings/) Each project has its own AI translation settings. Open the project, select **Settings**, then **AI translation**. Admins and the account's Owners can change them; everyone else sees them read-only. Changes save as soon as you make them and apply to the next translation run. Settings marked **Available on Team and above** are switched off and cannot be turned on while the workspace is on the Free or Starter plan. ## Drafting [#drafting] ### Auto-translate [#auto-translate] "Automatically queue translations when the source text changes." On by default. With it on, new and changed source strings are translated into every target language as soon as they arrive, whether they come from the CLI, an upload, the API or an integration, and adding a language translates the existing strings into it. With it off, nothing is translated until someone runs it from the editor. See [Translate with AI](https://transglot.ai/docs/guides/translating/translate-with-ai). A translation you have edited or reviewed is never overwritten by AI, whether a run was started automatically or by hand. ### Quality mode [#quality-mode] "Use the higher-quality model for every target language in this project." With Quality mode off, Glossa translates with **Glossa Flux**, the fast default model. With it on, every target language in the project is translated with **Glossa Deep**, the quality model. Use it for projects where wording matters more than speed, such as marketing copy or legal text. Team and above. Quality mode applies to runs started after you turn it on. Translations that already exist are not redone. ## Memory [#memory] ### Translation memory [#translation-memory] "Reuse an identical machine translation instantly instead of paying to translate it again." When the exact same source text has been translated before, transglot reuses that translation at no cost instead of translating it again. Exact-match reuse draws on an anonymised, shared pool as well as your own workspace's history. Under the switch, the page shows how many words this project reused this month. Turn it off and the project neither reuses translations from the pool nor adds its own to it: every string is translated fresh. See [Translation memory](https://transglot.ai/docs/concepts/translation-memory). ### Semantic translation memory [#semantic-translation-memory] "Find near-matching human-reviewed translations to suggest to reviewers and to ground the AI. Fuzzy matches are never applied automatically." With it on, transglot also finds previously reviewed translations of similar text and offers them as suggestions. A similar match is only ever a suggestion; it is never written to your project automatically. Reviewers see these matches in the editor under **Similar approved translations**, and Glossa uses them as reference when it translates. Team and above. Two settings refine it, and are available only while it is on: | Setting | What it does | | --- | --- | | **Minimum match** | "How close a fuzzy match must be before it is worth showing. A legal project may want nothing under 95%; a marketing one is glad of 75%." **Exact only** shows no similar matches. | | **Match sources** | Where similar matches come from: **All projects here** (every project in this workspace) or **This project only**. Choose **This project only** when this project's reviewed translations should not surface in other clients' work, or the other way round. | ## What the model can see [#what-the-model-can-see] ### Screenshot context [#screenshot-context] When a key has a screenshot, Glossa is shown it with the string, so it can see where the text appears and write something that fits. It also lets quality checks flag translations that overflow the space they render in. Runs that use screenshots cost more. Team and above. See [The screenshot library](https://transglot.ai/docs/guides/quality/the-screenshot-library). ## Related settings [#related-settings] These live on the project's other settings pages: - **Route AI translations by confidence**, under **Settings**, then **Checks**: lets confident translations that pass every automatic check skip review. See [Why was my translation flagged for review?](https://transglot.ai/docs/guides/translating/why-was-my-translation-flagged-for-review) - **Voice**: the project's brand voice, which Glossa follows. See [Brand voice](https://transglot.ai/docs/guides/quality/brand-voice). - **Glossary enforcement**, under **Checks**: how strictly glossary terms are enforced. See [The glossary](https://transglot.ai/docs/guides/quality/the-glossary). ## If it doesn't work [#if-it-doesnt-work] - A switch is greyed out with **Available on Team and above**: the account's plan does not include it. See [Change your plan](https://transglot.ai/docs/administration/billing/change-your-plan). - Every switch is greyed out: you are not an Admin of this workspace or an Owner of its account. - **Minimum match** and **Match sources** are greyed out: turn on **Semantic translation memory** first. ## Related [#related] - [Translate with AI](https://transglot.ai/docs/guides/translating/translate-with-ai) - [AI translation](https://transglot.ai/docs/concepts/ai-translation) - [Glossary and brand voice](https://transglot.ai/docs/concepts/glossary-and-brand-voice) # Approve or flag a translation (https://transglot.ai/docs/guides/translating/approve-or-flag-a-translation/) Approving is a reviewer's decision, separate from writing. Members with the Reviewer or Admin role, and the account's Owners, can approve and flag. A reviewer scoped to particular languages can only review those languages. Translators and Developers can write translations but not approve them; in **Side by side** they see "Approving is a reviewer’s right. Yours is to translate, so this string moves on when a reviewer reads it." ## Review one string [#review-one-string] <Tabs items={['Side by side', 'Table']}> <Tab value="Side by side"> <Steps> <Step> ### Open the strings to review [#open-the-strings-to-review] In the [editor](https://transglot.ai/docs/guides/translating/the-editor), choose the language. To see only what needs you, open **Filters** and tick **Needs checking** or **Waiting for approval**, or follow **Review** or **Approve** from the [project overview](https://transglot.ai/docs/guides/projects/your-project). </Step> <Step> ### Read it [#read-it] Read the translation against the source. If it needs a correction, make it and save it first. </Step> <Step> ### Decide [#decide] Select **Approve** to approve it and move to the next string, or **Flag for a check** to mark it as needing another look. `⌘` `Enter` also approves and moves on. </Step> </Steps> </Tab> <Tab value="Table"> <Steps> <Step> ### Select the row [#select-the-row] In the [editor](https://transglot.ai/docs/guides/translating/the-editor), switch to **Table** and select the row. </Step> <Step> ### Decide [#decide-1] In the context panel, under **Approval**, select **Approve** or **Flag**. `⌘` `Enter` approves and moves to the next row; `⌘` `Shift` `Enter` approves without moving. </Step> </Steps> </Tab> </Tabs> **Approve** is unavailable, with the reason shown on it, when: - you have an unsaved change: "Save your change first. Approve applies to the saved text." - the string has no translation: "Nothing to approve yet: this string has no translation." - it is already approved: "Already approved." When the source text changed after the translation was written, the editor warns "The source changed after this was written. Read it again before approving it." You can still approve it. ## Review many at once [#review-many-at-once] Tick the rows in **Table** view, then select **Approve** or **Flag** in the bar above the table. Approving more rows than fit on one screen asks you to confirm first. Rows that changed after you loaded them are skipped, never approved unseen. See [Act on many strings at once](https://transglot.ai/docs/guides/translating/bulk-actions). ## What happens next [#what-happens-next] | Action | Result | | --- | --- | | **Approve** | The translation becomes **approved** and any flag on it is cleared. Approving changes the sign-off, never the text. | | **Flag** | The translation is marked **needs checking**. An approved translation drops back to **edited**, because it is no longer signed off; a Glossa draft stays a draft. | Both are recorded in the string's [history](https://transglot.ai/docs/guides/translating/version-history) with your name, and in the project's [activity](https://transglot.ai/docs/guides/projects/project-activity). An approved translation is never overwritten by AI. If you publish to the CDN with drafts held back, approved strings are published and Glossa drafts wait until someone approves or edits them. See [Publish a locale](https://transglot.ai/docs/guides/delivery/publish-a-locale). ## Require a second reviewer [#require-a-second-reviewer] On Team plans and above, a workspace can stop anyone approving a translation they wrote themselves. An Admin turns on **Require a second reviewer** under **Policy** on the workspace's **People** page. It applies to every project, including to the account's Owners and Admins. With it on, **Approve** is unavailable on your own translations and reads "You wrote this one, so someone else has to approve it. Flag it to send it back." In a bulk approval, your own rows are skipped and counted as needing a second reviewer. ## Ask a reviewer to look [#ask-a-reviewer-to-look] Members who cannot approve can still ask for a review. In **Table** view, **Ask for a check** on a row flags it and notifies the project's reviewers. See [Translate a string](https://transglot.ai/docs/guides/translating/translate-a-string#ask-a-reviewer-to-check-it). ## If it doesn't work [#if-it-doesnt-work] - The **Approval** section and the bulk **Approve** and **Flag** buttons are missing: you do not have review access to this project or language. - "Someone else has to sign this off: this workspace requires a second reviewer.": the workspace requires a second reviewer and you wrote this translation. - "An empty translation cannot be approved: there is nothing there.": the string has no text to approve. ## Related [#related] - [Why was my translation flagged for review?](https://transglot.ai/docs/guides/translating/why-was-my-translation-flagged-for-review) - [Your assignments](https://transglot.ai/docs/guides/translating/your-assignments) - [Quality checks](https://transglot.ai/docs/guides/quality/quality-checks) # Assign a language to a translator (https://transglot.ai/docs/guides/translating/assign-a-language-to-a-translator/) An assignment is a work order: one language, one translator, a status that moves from assigned to approved, and a progress figure. Assigning also opens the language to the translator, so it is how you give a vendor or freelancer exactly the languages they work on and nothing else. Admins and the account's Owners can assign work. ## Before you start [#before-you-start] The translator must already be a member of the workspace. To bring in someone new, [invite them](https://transglot.ai/docs/administration/team/invite-a-teammate) first; for an outside vendor, use a project-scoped invite as described in [Onboard a vendor or freelancer](https://transglot.ai/docs/administration/team/onboard-a-vendor-or-freelancer). The project also needs at least one target language. ## Assign the work [#assign-the-work] <Steps> <Step> ### Open the project's assignments [#open-the-projects-assignments] In the project, select **Review assignments** in the navigation. The page is headed **Assignments**. </Step> <Step> ### Choose the translator [#choose-the-translator] Under **Assign to translator**, pick a person from **Translator** (it reads **Choose a translator** until you do). </Step> <Step> ### Set a due date (optional) [#set-a-due-date-optional] Pick a date under **Due date (optional)**. </Step> <Step> ### Tick the languages [#tick-the-languages] Tick each language to assign. Each language becomes its own assignment, tracked separately. </Step> <Step> ### Write a brief (optional) [#write-a-brief-optional] Under **Brief (optional)**, add anything the translator should know: "Tone, deadlines, anything the translator should know." </Step> <Step> ### Assign [#assign] Select **Assign to translator**. It stays unavailable until you have chosen a person and at least one language. </Step> </Steps> ## What happens next [#what-happens-next] - Each language appears under **Work orders** as **Assigned**, and a message confirms it: "Assigned de, fr to Ana." - The translator is notified in the console and by email, and the work appears on their **Assignments** page. See [Your assignments](https://transglot.ai/docs/guides/translating/your-assignments). - The translator can now open and write in the assigned languages. For a member whose access is limited to this project, the assignment adds exactly those languages to their access and nothing more. Assigning never takes access away, and it never changes anyone's role. The progress on each work order counts the language's **approved** strings against the project's keys, so it moves as reviewers approve the translator's work. ## Review the work [#review-the-work] When the translator selects **Submit for review**, you are notified, and the work order shows **Submitted for review**. Read the language in the editor and approve its strings (see [Approve or flag a translation](https://transglot.ai/docs/guides/translating/approve-or-flag-a-translation)), then, on the work order: - **Approve** signs off the work order. The confirmation says how many strings are not yet approved, if any: approving the work order does not approve them. "Approving is final: the work order cannot be reopened." The translator is told. - **Request changes** opens **Request changes to** *language*. Describe "Which strings, and what is wrong with them.", then select **Send request**. The work order shows **Changes requested** and the translator is told. Nobody can approve or send back a work order assigned to themselves. ## If it doesn't work [#if-it-doesnt-work] - **"Invite a translator to this workspace before assigning work."**: there is nobody you can assign yet. - **"Add a target language before assigning a translator."**: the project has no target language. - **"That member reviews this project. A reviewer cannot also be the assignee."**: pick someone else, or change who reviews the project. - **"Account owners and admins already have full access."**: Owners and Admins can already work in every language, so there is nothing to assign. - **"Ana already has an open assignment for de."**: that language is already assigned to them and not yet approved. Other languages in the same request are still assigned. - The **Assign to translator** form is not shown: you are not an Admin of this workspace or an Owner of its account. ## Related [#related] - [Your assignments](https://transglot.ai/docs/guides/translating/your-assignments) - [Grant access to one project](https://transglot.ai/docs/administration/team/grant-access-to-one-project) - [Members and roles](https://transglot.ai/docs/administration/team/members-and-roles) # Act on many strings at once (https://transglot.ai/docs/guides/translating/bulk-actions/) In the editor's **Table** view, every row has a checkbox. Tick the rows you want and a bar appears above the table with the actions you are allowed to take on them. The selection belongs to the language on screen: approving or translating acts on that language only. ## Select rows [#select-rows] <Steps> <Step> ### Narrow the table first [#narrow-the-table-first] Use **Filters** or the search box so the table shows the strings you mean to act on. See [Filter the editor](https://transglot.ai/docs/guides/translating/filter-the-editor). </Step> <Step> ### Tick the rows [#tick-the-rows] Tick rows one by one, or use the checkbox in the header row (**Select every key on this page**) to take every row that has loaded. </Step> <Step> ### Take every match, if you need to [#take-every-match-if-you-need-to] The table loads rows as you scroll. When every loaded row is selected and more rows match your filter, a banner reads **All N loaded rows are selected.** Select **Select all N matching this filter** to add the rest. </Step> </Steps> The bar shows how many rows are **selected**. **Clear** empties the selection. ## Actions [#actions] | Action | What it does | Who can use it | | --- | --- | --- | | **Translate** | Translates the selected strings with Glossa, after showing what the run covers. Anything a person has edited or approved is left as it is. See [Translate with AI](https://transglot.ai/docs/guides/translating/translate-with-ai). | Developer and above | | **Approve** | Approves every selected translation. | Reviewer, Admin, account Owner | | **Flag** | Marks every selected translation as needing a check. | Reviewer, Admin, account Owner | | **Copy source** | Fills the translation with the source text, for strings that must stay identical in every language, such as brand names, product codes and URLs. **Fill empty rows only** leaves existing translations alone; **Replace existing translations too** overwrites them. | Anyone who can edit the language | | **Keys**, then **Rename prefix** | Changes the start of every selected key name, for example from `checkout.` to `cart.`, then **Rename**. | Developer and above | | **Keys**, then **Character limit** | Sets a maximum length on every selected key with **Set**, or removes it with **Clear**. | Developer and above | | **Delete** | Deletes the selected keys and every translation of them, in every language. | Developer and above | The key actions (**Rename prefix**, **Character limit**, **Delete**) work on up to 200 keys at a time. ## What to expect [#what-to-expect] **Approving a large selection.** Approving more rows than fit on a screen asks first: "This approves every selected row, including rows nobody has opened. Rows changed since they were loaded, and rows you wrote yourself, are skipped." A large approval is sent in parts and shows its progress as it goes. When it finishes, a message such as "Approved 180. 4 changed since load." says how many rows were approved and why any were skipped. **Empty translations are never approved.** A selected row with no translation is skipped. **Rename is all or nothing.** If any new name would collide with a key that already exists, nothing is renamed and the bar says "Renaming would collide with an existing key:" followed by the name. **Changing a character limit re-checks the strings.** Translations that now run over the new limit are flagged for review. Raising a limit does not clear a flag that is already there. **Delete cannot be undone.** You type the number of keys to confirm. The workspace's Admins are notified that the keys were deleted, and the deletion is recorded in the project's [activity](https://transglot.ai/docs/guides/projects/project-activity). Apps that read a deleted key fall back to the key name. <Callout type="warn"> A member scoped to particular languages cannot delete a key that holds translations in a language outside their access, because deleting a key removes its translations in every language. </Callout> ## Related [#related] - [The editor](https://transglot.ai/docs/guides/translating/the-editor) - [Approve or flag a translation](https://transglot.ai/docs/guides/translating/approve-or-flag-a-translation) - [Add or edit a key](https://transglot.ai/docs/guides/projects/add-or-edit-a-key) # Comment on a string (https://transglot.ai/docs/guides/translating/comment-on-a-string/) Every key has one comment thread, shared across all of its languages. Anyone who can open the project can read, post and resolve comments, whatever their role. ## Post a comment [#post-a-comment] <Steps> <Step> ### Open the thread [#open-the-thread] In the [editor](https://transglot.ai/docs/guides/translating/the-editor), switch to **Table**. Under the key name, select **Comment**. When the key has open comments, the link shows how many instead. </Step> <Step> ### Write it [#write-it] Type into **Add a comment…**. Comments can be up to 5,000 characters. </Step> <Step> ### Mention someone (optional) [#mention-someone-optional] Select **Mention** and pick a teammate. Their name is added to your comment and they are notified when you post it. Only members who can open the project are offered, and only they are notified. </Step> <Step> ### Post [#post] Select **Comment**. </Step> </Steps> ## Who is notified [#who-is-notified] | Who | Notification | | --- | --- | | Everyone you mention | "*Your name* mentioned you", with the key name and the start of your comment. Sent in the console and by email. | | Everyone who has already commented on the key | "New comment on a string", in the console. | You are never notified of your own comment. If your workspace sends notifications to Slack, the comment is posted there too. Each person can change which of these they receive by email in their [notification preferences](https://transglot.ai/docs/administration/account/notification-preferences). ## Resolve, reopen and delete [#resolve-reopen-and-delete] - **Resolve** marks a comment as settled. **Reopen** brings it back. Anyone who can open the project can do both. - **Delete** removes a comment. You can delete your own comments; Admins and the account's Owners can delete anyone's. ## Find strings with open discussions [#find-strings-with-open-discussions] In the editor, open **Filters** and tick **Has unresolved comment** to see only the keys with a comment that has not been resolved. See [Filter the editor](https://transglot.ai/docs/guides/translating/filter-the-editor). ## If it doesn't work [#if-it-doesnt-work] - **"Could not post comment."**: the comment did not save. Your text is still in the box; try again. - A comment shows **Former member** as its author: the person who wrote it is no longer in the workspace. ## Related [#related] - [The editor](https://transglot.ai/docs/guides/translating/the-editor) - [Your notifications](https://transglot.ai/docs/administration/account/your-notifications) - [Connect Slack](https://transglot.ai/docs/integrations/notifications/connect-slack) # Filter the editor (https://transglot.ai/docs/guides/translating/filter-the-editor/) Filters, search and saved views work the same way in both editor views (**Side by side** and **Table**). In **Side by side** they decide which strings are in the queue; in **Table** they decide which rows are shown. ## Search [#search] Type into **Search keys and text…** to find strings by key name or by text. The search matches the text of the source and of translations, and tolerates small differences in key names. It returns the 200 best matches; if there are more, the table says **showing top 200: refine your search**. ## Filter [#filter] <Steps> <Step> ### Open the filters [#open-the-filters] In the editor, select **Filters**. Under **Filter keys**, the options are: | Filter | Shows | | --- | --- | | **Untranslated only** | Strings with no translation in this language yet. | | **State** | One of **GLOSSA drafts** (written by Glossa, not edited since), **Edited** or **Approved**. Select the same state again to remove it. | | **Waiting for approval** | Strings that are translated but not yet approved. | | **Needs checking** | Strings flagged for a person to check. | | **Has glossary issue** | Translations where a quality check found a glossary problem: a required term missing, or a forbidden one used. | | **Has unresolved comment** | Keys with an open comment thread. | | **Stale (source changed)** | Translations whose source text changed after they were written. | </Step> <Step> ### Combine them [#combine-them] Filters combine: **Needs checking** with **Has glossary issue** shows only the flagged strings that also break a glossary term. **Filters** shows how many are on. </Step> <Step> ### Clear them [#clear-them] **Clear filters** turns every filter off and keeps your search text. </Step> </Steps> The page address changes with your filters and language, so a filtered editor is a link you can send to a teammate. They see the same strings, limited to what their own access allows. ## Save a view [#save-a-view] A saved view keeps a filter combination, your search text and the language, under a name. <Steps> <Step> ### Set the filters [#set-the-filters] Set the language, the filters and any search text you want to keep. </Step> <Step> ### Name it [#name-it] Select **Save view**, type a name under **Name this view** (for example "Untranslated German"), then select **Save**. </Step> </Steps> Your views are listed under **Views**, under **Saved views**. Select a name to apply it; a view saved on another language switches the editor to that language. Select **✕** beside a view to delete it: only the saved filter is removed, never the strings it showed. Saved views are personal: each member sees only their own, per project. Saving a view under a name you already use replaces it. Saving a view needs edit access to the project; anyone who can open the editor can filter. ## Links from the project overview [#links-from-the-project-overview] The [project overview](https://transglot.ai/docs/guides/projects/your-project) opens the editor already filtered. On each language row, **Review** opens the strings that need checking, **Fill gap** the untranslated ones, and **Approve** the translated strings waiting for approval. The **translate** and **review** figures at the top of the project open the editor with the same filters. ## Related [#related] - [The editor](https://transglot.ai/docs/guides/translating/the-editor) - [Act on many strings at once](https://transglot.ai/docs/guides/translating/bulk-actions) - [Translation states](https://transglot.ai/docs/concepts/translation-states) # Keyboard shortcuts (https://transglot.ai/docs/guides/translating/keyboard-shortcuts/) Press `?` anywhere in the console, or `⌘/` (`Ctrl+/` on Windows and Linux), to open the shortcut list. In the editor, the **⌘** button in the toolbar opens the same list. It shows only the shortcuts your role can use: a translator who cannot approve is not shown the approve shortcut. On Windows and Linux, read `⌘` as `Ctrl` throughout. ## Editing a string [#editing-a-string] These work while you are typing in a translation. | Keys | Action | | --- | --- | | `⌘` `Enter` | Save and go to the next string | | `⌘` `Shift` `Enter` | Save without moving | | `Esc` | Cancel the edit and drop what you typed | Clicking away from a cell does not discard your text: the cell keeps it, marked as not saved, until you save it or press `Esc`. ## Moving around [#moving-around] | Keys | Action | | --- | --- | | `j` / `k` | Move down and up the queue | | `↑` / `↓` | Move between rows | | `←` / `→` | Move between columns | | `Alt` `↓` | Next untranslated string | | `Alt` `↑` | Previous untranslated string | | `Alt` `Shift` `↓` | Next string that needs checking | | `Alt` `Shift` `↑` | Previous string that needs checking | | `[` | Show or hide the context panel | ## In the context panel [#in-the-context-panel] These work when focus is not in a text field. | Keys | Action | Who can use it | | --- | --- | --- | | `⌘` `Enter` | Approve and go to the next string | Reviewers, Admins and account Owners | | `⌘` `Shift` `Enter` | Approve without moving | Reviewers, Admins and account Owners | | `Alt` `Enter` | Accept the Glossa suggestion on screen | Anyone who can edit the language | | `Alt` `1` to `9` | Insert the first to ninth similar translation, numbered as the panel lists them | Anyone who can edit the language | Memory matches use `Alt` rather than `Ctrl` with a number, because `Ctrl` and a number switches browser tabs on Windows and Linux. ## Anywhere in the console [#anywhere-in-the-console] | Keys | Action | | --- | --- | | `⌘` `K` | Open the command palette and [the assistant](https://transglot.ai/docs/guides/translating/the-assistant) | | `?` | Show the shortcut list | | `G`, then a letter | Go to a page. The shortcut list shows the letters for the pages in your navigation. | ## Related [#related] - [The editor](https://transglot.ai/docs/guides/translating/the-editor) - [Translate a string](https://transglot.ai/docs/guides/translating/translate-a-string) - [Approve or flag a translation](https://transglot.ai/docs/guides/translating/approve-or-flag-a-translation) # The assistant (https://transglot.ai/docs/guides/translating/the-assistant/) The assistant lives in the command palette. You tell it what you want, for example "translate everything missing in French, then run QA", and it answers with a plan: the steps it would take, in order, and what each one touches. Nothing happens until you run the plan. The assistant is available on Team plans and above, inside a project. It acts with your permissions: every step is checked against your role, your language access and your plan before it runs, exactly as if you had clicked the button yourself. ## Ask the assistant [#ask-the-assistant] <Steps> <Step> ### Open the command palette [#open-the-command-palette] Inside a project, press `⌘` `K` (`Ctrl` `K` on Windows and Linux). </Step> <Step> ### Switch to the assistant [#switch-to-the-assistant] Select **Ask the assistant**. The panel is headed **Ask about** *project name*. </Step> <Step> ### Describe what you want [#describe-what-you-want] Type into **Ask the assistant…** and press `Enter`. For example: "add a glossary term for Checkout", "add Japanese and translate it", or "open the QA dashboard". </Step> <Step> ### Review the plan [#review-the-plan] The assistant replies with a numbered plan of up to eight steps. Each step says what it will do and which strings or languages it covers. A step that translates or checks strings shows the words it would spend. </Step> <Step> ### Run it [#run-it] Select **Run** (or **Run plan**, or **Run** *N* **steps, spending** *words* when the plan spends). To change the plan, ask again in different words instead. </Step> </Steps> When the plan has run, the panel says which steps ran, such as "All 2 steps ran." or "1 of 2 steps ran.", with the reason under any step that did not. **Back to command palette** returns you to the ordinary search. ## What it can do [#what-it-can-do] | Step | Same as | | --- | --- | | Find keys by name or text | Searching in the [editor](https://transglot.ai/docs/guides/translating/the-editor) | | Open a project page: the editor, quality, glossary, screenshots, deploy or overview | Using the project navigation | | Create a key | **New key** in the editor ([Add or edit a key](https://transglot.ai/docs/guides/projects/add-or-edit-a-key)) | | Add a target language | [Add a target language](https://transglot.ai/docs/guides/projects/add-a-target-language) | | Translate missing or all strings, in one language or every language | [Translate with AI](https://transglot.ai/docs/guides/translating/translate-with-ai) | | Run the quality checks | [Run a quality check](https://transglot.ai/docs/guides/quality/run-a-quality-check) | | Add a glossary term, or change an existing one | [Add a glossary term](https://transglot.ai/docs/guides/quality/add-a-glossary-term) | | Repair Glossa translations that failed quality checks | [Self-heal a translation](https://transglot.ai/docs/guides/quality/self-heal-a-translation) | It does not approve translations, change settings, or delete anything. ## When a step does not run [#when-a-step-does-not-run] The assistant never claims a step ran when it did not. The reasons it gives include: - "Your role or plan does not allow this step." - "That language is outside your access to this project." - "Nothing to translate. No string is waiting for this run." - "Not run: a translation run is already in progress on this project. Try again when it finishes." - "Not run: this workspace does not have enough words left this cycle." - "Not run: the assistant has reached its limit for this billing cycle." If the panel says "The assistant needs a Team plan or higher (Quality Mode).", the account's plan does not include the assistant. See [Change your plan](https://transglot.ai/docs/administration/billing/change-your-plan). ## Related [#related] - [Keyboard shortcuts](https://transglot.ai/docs/guides/translating/keyboard-shortcuts) - [The editor](https://transglot.ai/docs/guides/translating/the-editor) - [Roles and what each one can do](https://transglot.ai/docs/administration/team/roles-and-what-each-one-can-do) # The editor (https://transglot.ai/docs/guides/translating/the-editor/) The editor is where translations are written, read and approved. It shows one target language at a time: every key in the project, its source text, and the translation in that language. Open it from a project with **Open editor**, or from any **Fill gap**, **Review** or **Approve** link on the project overview, which open it already filtered to the strings that need you. ## Choose a language [#choose-a-language] The strip at the top of the editor names the workspace, the project and the target language you are working in. Select the language to switch to another one; the list is searchable, and **Manage locales** at its foot takes you to the project's language settings. If the project has no target language yet, the editor says **Nothing to translate into yet.** Someone with the Developer role or above has to [add a target language](https://transglot.ai/docs/guides/projects/add-a-target-language) first. If it has languages but no source strings, the editor says **Nothing has been imported yet** and offers **Import source strings →** (see [Import content](https://transglot.ai/docs/guides/projects/import-content)) or, for Developers and above, **Add one key by hand**. ## Two ways to read a language [#two-ways-to-read-a-language] The toggle next to the language picks how the strings are laid out: | View | What it is for | | --- | --- | | **Side by side** | The default. One string at a time: the source above, a large box for the translation below, and the glossary terms, memory matches and suggestions for that string beside it. The queue on the left lists every string in your current filter. | | **Table** | Every key as a row, with the key name, the source text, the translation, a **Quality** column (on wider screens) and the string's state. Use it to scan, to select rows for bulk actions, and to open comments and plural forms. | **Queue** at the top left shows how many strings are in view. On narrow screens it opens the queue as a drawer. Move through the queue with `j` and `k`. ## Toolbar [#toolbar] | Control | What it does | Who sees it | | --- | --- | --- | | **Translate missing** | Translates the strings in this language that have no translation, or whose AI draft is out of date, with Glossa. It reads **Translating…** while a run is going. See [Translate with AI](https://transglot.ai/docs/guides/translating/translate-with-ai). | Developer and above | | **Run QA** | Runs the automatic quality checks. It reads **Checking quality…** while it works. See [Run a quality check](https://transglot.ai/docs/guides/quality/run-a-quality-check). | Developer and above | | **New key** | Adds a key by hand. See [Add or edit a key](https://transglot.ai/docs/guides/projects/add-or-edit-a-key). | Developer and above | | **⌘** | Opens the list of [keyboard shortcuts](https://transglot.ai/docs/guides/translating/keyboard-shortcuts). You can also press `?`. | Everyone | | **Filters**, **Views**, **Save view** | Narrow the strings in view and keep that combination. See [Filter the editor](https://transglot.ai/docs/guides/translating/filter-the-editor). | Everyone (saving a view needs edit access) | Both **Translate missing** and **Run QA** show what the run covers before anything starts. See [Translate with AI](https://transglot.ai/docs/guides/translating/translate-with-ai). ## String states [#string-states] Every translation carries one state, shown as a chip in the table, in the queue and in the context panel: | State | Meaning | | --- | --- | | **not translated** | No translation exists in this language yet. | | **draft** | Written by Glossa and not edited by a person since. | | **edited** | Written or changed by a person, or imported from a file. | | **approved** | A reviewer has approved it. | | **needs checking** | Flagged for a person to check, by an automatic check or by a teammate. See [Why was my translation flagged for review?](https://transglot.ai/docs/guides/translating/why-was-my-translation-flagged-for-review) | A second chip, **stale**, appears when the source text changed after the translation was written. A string can be approved and stale at the same time. [Translation states](https://transglot.ai/docs/concepts/translation-states) explains how strings move between states. ## The context panel [#the-context-panel] The panel on the right, headed **In context**, shows everything known about the string you are on. Press `[` to hide or show it. Depending on the string and your plan, it holds: - **Source**: the source text, with placeholders highlighted, the translation's state, and its quality score and confidence when it has them. - **Overflow watch**: the length of the translation against the key's maximum length, or a note that no limit is set. - **Glossary**: glossary terms found in the source and how each must be translated, marked **forbidden** or **do not translate** where that applies. - **Translation memory**: an exact match from earlier work, which you can take with **Use this ($0)**. - **Similar approved translations**: reviewed translations of similar source text, each with how close it is. **Use this** copies one into the translation. These are suggestions only. - **Ask Glossa**: **Suggest** asks Glossa for a fresh draft of this one string and shows whether it passes the placeholder, plural, glossary and length checks. Nothing is saved until you select **Accept**. Available on Starter and above. - **Quality findings**: each problem an automatic check found, with its category and severity. - **Approval**: **Approve** and **Flag**, for reviewers. See [Approve or flag a translation](https://transglot.ai/docs/guides/translating/approve-or-flag-a-translation). - **Concordance**: search earlier translations for a word or phrase, to see how your team has rendered it before. - **Provenance**: the translation's recent versions, newest first, with **Restore** on each. See [Version history](https://transglot.ai/docs/guides/translating/version-history). In **Side by side** view, the glossary terms, the memory match and the suggestions sit next to the translation box instead, and the context panel drops them so nothing appears twice. ## Translation runs [#translation-runs] While a translation run is going, a progress card above the strings shows how far it has got, as **Translating… (done/total)**, and rows fill in as strings land. Developers and above can **Stop** a run; strings already translated are kept. If a run finishes with failures, the card says how many strings failed, lists them under **Show the strings that failed**, and offers **Retry failed**, which re-runs only those strings. ## What you can do depends on your role [#what-you-can-do-depends-on-your-role] | Role | In the editor | | --- | --- | | Translator | Read every string, write translations in the languages they have access to, comment, save views, ask for a check. | | Reviewer | Everything a Translator can do, plus approve and flag. | | Developer | Everything a Translator can do, plus start AI translation and quality checks, and add, edit and delete keys. Developers do not approve. | | Admin and account Owner | Everything above. | A member scoped to particular languages can read the whole project but only write in, and review, the languages they were given. See [Roles and what each one can do](https://transglot.ai/docs/administration/team/roles-and-what-each-one-can-do). ## Related [#related] - [Translate a string](https://transglot.ai/docs/guides/translating/translate-a-string) - [Bulk actions](https://transglot.ai/docs/guides/translating/bulk-actions) - [Keyboard shortcuts](https://transglot.ai/docs/guides/translating/keyboard-shortcuts) # Translate a string (https://transglot.ai/docs/guides/translating/translate-a-string/) Anyone who can edit a language can write its translations: Translators, Reviewers, Developers, Admins and the account's Owners. A member scoped to particular languages can only write in the languages they were given; in any other language the editor reads **Read only. Translating this locale is not one of your rights here.** The editor writes translations only. Source text comes from your files, through an [import](https://transglot.ai/docs/guides/projects/import-content), the [CLI](https://transglot.ai/docs/developers/cli) or an integration. ## Write the translation [#write-the-translation] <Tabs items={['Side by side', 'Table']}> <Tab value="Side by side"> <Steps> <Step> ### Open the string [#open-the-string] In the [editor](https://transglot.ai/docs/guides/translating/the-editor), choose the language, then pick the string from the queue on the left. Search or [filter](https://transglot.ai/docs/guides/translating/filter-the-editor) the queue to find it, or press `Alt` `↓` to jump to the next untranslated string. </Step> <Step> ### Type the translation [#type-the-translation] Type into the box under the source. Glossary terms in the string, an exact memory match and Glossa's alternatives are listed beside it; select one to use it as your starting point. </Step> <Step> ### Save [#save] Select **Save**, or press `⌘` `Enter` to save and move to the next string (`⌘` `Shift` `Enter` saves without moving). **Discard**, or `Esc`, drops your change. </Step> </Steps> </Tab> <Tab value="Table"> <Steps> <Step> ### Open the cell [#open-the-cell] In the [editor](https://transglot.ai/docs/guides/translating/the-editor), switch to **Table**, find the row, and click its translation cell (or move to it and press `Enter`). </Step> <Step> ### Type the translation [#type-the-translation-1] The context panel on the right shows the string's glossary terms, memory matches and length limit while you type. </Step> <Step> ### Save [#save-1] Press `⌘` `Enter` to save and move to the next row, or `⌘` `Shift` `Enter` to save in place. `Esc` cancels. </Step> </Steps> </Tab> </Tabs> On Windows and Linux, use `Ctrl` for `⌘`. ## Start from a suggestion [#start-from-a-suggestion] You do not have to type from scratch. In the context panel, or beside the box in **Side by side**: - **Use this ($0)** copies an exact match from translation memory. - **Use this** under **Similar approved translations** copies a reviewed translation of similar source text. Check it before saving: the source it matched is shown above it. - **Suggest** asks Glossa for a fresh draft of this one string, checked against the placeholders, plural forms, glossary terms and length limit. **Accept** puts it in the box. Available on Starter and above. Whatever you take is saved as your edit, so you can change it before or after saving. ## What happens when you save [#what-happens-when-you-save] The string's state becomes **edited**, and your name is added to its [history](https://transglot.ai/docs/guides/translating/version-history). A translation you have edited or reviewed is never overwritten by AI: later translation runs skip it, even when its source text changes. While you type, the editor checks your draft against the source and lists problems under the box, such as a missing placeholder. If the key has a maximum length, a counter shows your length against it. ## Ask a reviewer to check it [#ask-a-reviewer-to-check-it] In **Table** view, **Ask for a check** under the row's state flags the translation as needing a check and notifies the project's reviewers ("requested your review"). It is offered to anyone who can edit the language, on translations that are not already flagged or approved. See [Approve or flag a translation](https://transglot.ai/docs/guides/translating/approve-or-flag-a-translation). ## Plural strings [#plural-strings] A key marked as plural has one form per plural category of the language. It cannot be typed into a single box: select **Edit forms** in the table, or **Edit plural forms** in **Side by side**. See [Plural forms](https://transglot.ai/docs/guides/projects/plural-forms). ## If it doesn't work [#if-it-doesnt-work] - **"Over the limit. Saving keeps your text and flags this row for review."**: your translation is longer than the key's maximum length. It saves, and the string is flagged so a reviewer sees it. - **"Someone else saved "…" while you were editing. Save again to replace it."**: a teammate saved this string after you opened it. Your text is kept; save again only if yours should win. - **"Save failed."**: the change did not reach the server. Your text stays in the box; try again. - The box is read-only: you do not have edit access to this language. Ask an Admin to [grant you access](https://transglot.ai/docs/administration/team/grant-access-to-one-project). ## Related [#related] - [The editor](https://transglot.ai/docs/guides/translating/the-editor) - [Keyboard shortcuts](https://transglot.ai/docs/guides/translating/keyboard-shortcuts) - [Why was my translation flagged for review?](https://transglot.ai/docs/guides/translating/why-was-my-translation-flagged-for-review) # Translate with AI (https://transglot.ai/docs/guides/translating/translate-with-ai/) Glossa, transglot's translation engine, drafts translations for you. **Glossa Flux** is the default model; projects with [Quality mode](https://transglot.ai/docs/guides/translating/ai-translation-settings#quality-mode) on use **Glossa Deep** instead. You can start a run yourself from the editor, and transglot also starts runs on its own when new source text arrives (see [Automatic translation](#automatic-translation)). Starting a run needs the Developer role or above. Translators and Reviewers can edit and approve what Glossa writes, but cannot start a run. ## Before you start [#before-you-start] Set up the project's [glossary](https://transglot.ai/docs/guides/quality/the-glossary) and [brand voice](https://transglot.ai/docs/guides/quality/brand-voice) first if you plan to use them. Glossa follows both on every run, and a translation that is already written is not redone when you change them later. ## Translate everything missing in a language [#translate-everything-missing-in-a-language] <Steps> <Step> ### Open the language [#open-the-language] In the [editor](https://transglot.ai/docs/guides/translating/the-editor), choose the target language. </Step> <Step> ### Start the run [#start-the-run] Select **Translate missing**. It covers every string in the language that has no translation, plus Glossa drafts whose source text has changed since they were written. </Step> <Step> ### Check what it covers [#check-what-it-covers] A dialog asks **Translate into** *language*? and shows how many strings and words the run covers, and how that compares with the words left in your plan this cycle. Under **Which languages**, keep *language* **only**, or choose the option that lists every language with strings waiting. </Step> <Step> ### Confirm [#confirm] Select **Translate**. **Cancel** starts nothing. </Step> </Steps> ## Translate selected strings [#translate-selected-strings] <Steps> <Step> ### Select the rows [#select-the-rows] In the editor's **Table** view, tick the strings you want translated. See [Act on many strings at once](https://transglot.ai/docs/guides/translating/bulk-actions). </Step> <Step> ### Start the run [#start-the-run-1] Select **Translate** in the bar above the table, check the figures in the dialog, and confirm. </Step> </Steps> ## What happens during a run [#what-happens-during-a-run] - **Translate missing** reads **Translating…**, and a progress card shows how many strings are done. Rows fill in as strings land, each in the **draft** state. - You can keep working. Leaving the page does not stop the run. - **Stop** ends the run early. Strings already translated are kept; the rest need starting again. - When the run ends, the card says what happened: how many strings were translated, how many came from translation memory, and how many failed. **Retry failed** re-runs only the failed strings. Every run, with what started it and which strings failed, is listed on the project's **Translation runs** page: select **Runs** in the project's navigation, or **Translation runs** on the [project overview](https://transglot.ai/docs/guides/projects/your-project). Only one run goes at a time in a project. Starting another while one is running is refused with "A translation run is already in progress on this project. Wait for it to finish, then start this one." ## What Glossa does and does not touch [#what-glossa-does-and-does-not-touch] **Your own work is safe.** A translation you have edited or reviewed is never overwritten by AI. A run skips it even when its source text has changed, and the end-of-run summary counts strings "left as they are because someone had already edited or reviewed them". **Repeated text is reused.** When the exact same source text has been translated before, transglot reuses that translation at no cost instead of translating it again. See [Translation memory](https://transglot.ai/docs/concepts/translation-memory). **Placeholders, plurals and terms are checked.** Glossary terms are matched against source text and enforced during translation and during quality checks; a translation that departs from a glossary term is flagged. Plural keys get every plural form the target language needs. See [Plural forms](https://transglot.ai/docs/guides/projects/plural-forms). **Review is routed for you, if you want.** Each AI translation carries a confidence signal. Translations that pass the automatic checks and come back confident can be marked as not needing review, so your reviewers only see the ones worth their time. Everything else is flagged for review. Automatic checks always win: a translation with a broken placeholder, the wrong plural forms, or a glossary conflict is always sent to review no matter how confident it was. This routing is off by default; turn it on with **Route AI translations by confidence** in the project's **Settings**, under **Checks** (Team and above). ## Automatic translation [#automatic-translation] With **Auto-translate** on (the default), transglot starts a run without anyone pressing a button when: - **Source text changes.** New or changed source strings that arrive by push, upload, API or an integration are translated into every target language. See [AI translation settings](https://transglot.ai/docs/guides/translating/ai-translation-settings#auto-translate). - **You add a language.** Everything already in the project is translated into the new language. The console asks first, with the word count: **Add** *language* **and translate into it?** See [Add a target language](https://transglot.ai/docs/guides/projects/add-a-target-language). When the first strings arrive in a new project, the project's setup page asks whether to translate them now, with the word count, and offers **Keep translating new text automatically**. Clearing that box, or choosing **Not now**, turns **Auto-translate** off for the project; you can turn it back on in the project's settings. Runs started automatically follow the same rules as runs you start: edited and approved translations are never overwritten, and memory is reused first. ## Usage [#usage] Your plan includes an allowance of AI translation each month. Usage is measured against that allowance, and what happens when you exceed it depends on whether usage-based billing is turned on for your workspace. Text reused from translation memory does not count against it. The confirmation dialog tells you before a run starts whether it fits. See [What counts towards my usage?](https://transglot.ai/docs/administration/billing/what-counts-towards-my-usage) and [pricing](https://transglot.ai/pricing). ## If it doesn't work [#if-it-doesnt-work] - **Translate missing** is not shown: you need the Developer role or above, or the project has no source strings yet. - **"Nothing to translate. No string in** *language* **is waiting for this run."**: every string in the language is translated and current, or has been edited by a person. - **"The figures for this run could not be loaded, so nothing was started. Try again in a moment."**: nothing was started or charged; try again. - A refusal that names your plan, payment or spend cap: AI translation is paused for the workspace's account. The message says why, and **Plan & billing** says what to do. See [Usage-based billing](https://transglot.ai/docs/administration/billing/usage-based-billing). - The run finished with failures: open the run on **Translation runs** to see which strings failed and why, then select **Retry failed**. ## Related [#related] - [AI translation settings](https://transglot.ai/docs/guides/translating/ai-translation-settings) - [AI translation](https://transglot.ai/docs/concepts/ai-translation) - [Why was my translation flagged for review?](https://transglot.ai/docs/guides/translating/why-was-my-translation-flagged-for-review) # Version history (https://transglot.ai/docs/guides/translating/version-history/) Every change to a translation is kept as a version: each Glossa draft, each edit, each approval and flag, and each restore. The editor shows a translation's recent versions in the context panel, so you can see how it got to where it is and undo a change without retyping it. ## See a translation's history [#see-a-translations-history] <Steps> <Step> ### Select the string [#select-the-string] In the [editor](https://transglot.ai/docs/guides/translating/the-editor), choose the language and select the string. </Step> <Step> ### Read the history [#read-the-history] In the context panel on the right, scroll to **Provenance**. It lists the ten most recent versions of the translation in this language, newest first, with how many it is showing. </Step> </Steps> Each version shows: - **What happened**, and who did it when the change was made by a person, for example "Edited by hand by Ana". - **When**, as a short age. Hover over it for the exact date and time. - **The text** of the translation at that point. - For a person's edit of a Glossa draft, how much of the draft they changed: **AI kept as-is**, or a figure such as **35% post-edited**. | Label | Meaning | | --- | --- | | **Glossa Flux draft**, **Glossa Deep draft** or **Glossa draft** | Glossa wrote this version. The name says which model, when it is known. | | **Edited by hand** | A person typed or changed the text. | | **From translation memory** | The text was reused from an earlier identical translation. | | **Approved** | A reviewer approved the translation. The text did not change. | | **Flagged** | Someone flagged the translation for a check. The text did not change. | | **Reverted** | Someone restored an earlier version. | | **In-context editor** | The change was made in the [in-context editor](https://transglot.ai/docs/developers/in-context). | | **Auto-repair** | An automatic fix from [self-heal](https://transglot.ai/docs/guides/quality/self-heal-a-translation). | If the panel says "No history yet. Every edit, approval and GLOSSA draft on this string will be listed here.", the translation has never been written in this language. ## Restore an earlier version [#restore-an-earlier-version] Anyone who can edit the language can restore a version. <Steps> <Step> ### Find the version [#find-the-version] In **Provenance**, find the version whose text you want back. </Step> <Step> ### Restore it [#restore-it] Select **Restore** under it. A message reads "Reverted." and the translation shows the restored text. </Step> </Steps> Restoring writes the old text back as a new version, labelled **Reverted**, so nothing in the history is lost and the restore itself can be undone the same way. The restored translation becomes **edited**. Restoring cannot be used to approve a translation: the only exception is a reviewer restoring a version that was itself approved, which comes back **approved**. Restoring never brings back a Glossa draft as a draft. If someone else saved the translation after you opened it, the restore is refused and the editor shows what they saved, so you can decide again with the current text in front of you. ## Where else changes are recorded [#where-else-changes-are-recorded] Edits, approvals, flags and restores also appear in the project's [activity](https://transglot.ai/docs/guides/projects/project-activity), alongside changes to keys, languages and the glossary. Use the activity feed to see what changed across a whole project; use **Provenance** to see one translation's story. ## Related [#related] - [Translate a string](https://transglot.ai/docs/guides/translating/translate-a-string) - [Approve or flag a translation](https://transglot.ai/docs/guides/translating/approve-or-flag-a-translation) - [Translation states](https://transglot.ai/docs/concepts/translation-states) # Why was my translation flagged for review? (https://transglot.ai/docs/guides/translating/why-was-my-translation-flagged-for-review/) A flagged translation shows **needs checking** in the editor and counts towards the **review** figure on the [project overview](https://transglot.ai/docs/guides/projects/your-project). The flag means a person should read it before you rely on it. It does not change the text. ## Find out why [#find-out-why] In the [editor](https://transglot.ai/docs/guides/translating/the-editor), select the string: - In **Table** view, a flagged row has a **Check** button under its state (or **QA** when it has findings but no flag). It opens **Quality findings** for that string, with each problem, its category, its severity and, where there is one, a suggested fix you can take with **Accept suggestion**. - In the context panel, **Quality findings** lists the same problems, and **Provenance** shows whether a person flagged it, and who. See [Version history](https://transglot.ai/docs/guides/translating/version-history). ## The reasons [#the-reasons] ### An automatic check found a problem [#an-automatic-check-found-a-problem] The [quality checks](https://transglot.ai/docs/guides/quality/quality-checks) flag a translation when they find a serious problem, when its quality score is low, or when it runs over the key's maximum length. Typical problems are a placeholder that is missing or changed, the wrong plural forms for the language, and a glossary term that was not followed. Glossary terms are matched against source text and enforced during translation and during quality checks; a translation that departs from a glossary term is flagged. Some problems are caught the moment a translation is saved, without a quality check: - A translation saved by hand that is longer than the key's maximum length. The editor warns before you save: "Over the limit. Saving keeps your text and flags this row for review." - A translation that already existed when someone lowered the key's maximum length, and is now too long. ### It was not confident enough to skip review [#it-was-not-confident-enough-to-skip-review] This applies only to projects with **Route AI translations by confidence** turned on, under **Settings**, then **Checks** (Team and above). It is off by default. Each AI translation carries a confidence signal. Translations that pass the automatic checks and come back confident can be marked as not needing review, so your reviewers only see the ones worth their time. Everything else is flagged for review. Automatic checks always win: a translation with a broken placeholder, the wrong plural forms, or a glossary conflict is always sent to review no matter how confident it was. With the setting off, Glossa's translations arrive as **draft** and are not flagged for confidence. Use the **Waiting for approval** filter to find drafts nobody has read yet. ### A person flagged it [#a-person-flagged-it] - A reviewer selected **Flag** or **Flag for a check**. See [Approve or flag a translation](https://transglot.ai/docs/guides/translating/approve-or-flag-a-translation). - A translator selected **Ask for a check**, which flags the string and notifies the project's reviewers. ## Clear the flag [#clear-the-flag] Fix the translation if it needs fixing, then have a reviewer approve it: approving clears the flag. Editing a translation does not clear its flag on its own, so a fixed string still reaches a reviewer. Only Reviewers, Admins and the account's Owners can approve. If you are a translator, the flag stays until a reviewer reads the string. ## Related [#related] - [What does the quality score mean?](https://transglot.ai/docs/guides/quality/what-does-the-quality-score-mean) - [Run a quality check](https://transglot.ai/docs/guides/quality/run-a-quality-check) - [Filter the editor](https://transglot.ai/docs/guides/translating/filter-the-editor) # Your assignments (https://transglot.ai/docs/guides/translating/your-assignments/) When an Admin assigns you a language, it becomes a work order with a status, a progress figure and, sometimes, a due date and a brief. You are notified in the console and by email. ## Find your work [#find-your-work] Select **Assignments** in the workspace navigation. **My assignments** lists every language assigned to you, across the workspace's projects, under **Assigned to you**: the language, its status, its progress and what you can do next. Only work assigned to you appears here. Inside a project, **Review assignments** in the project navigation shows the work orders on that project. Admins see every work order there; everyone else sees their own. **Progress** counts the language's approved strings against the project's keys, so it moves when a reviewer approves your translations, not when you save them. ## Work through an assignment [#work-through-an-assignment] <Steps> <Step> ### Start [#start] Select **Start**. The work order moves to **In progress** and the [editor](https://transglot.ai/docs/guides/translating/the-editor) opens on the assigned language. To come back to it later, select the language's name on the work order. </Step> <Step> ### Translate [#translate] Translate and correct the strings. **Filters**, then **Untranslated only**, shows what is left. See [Translate a string](https://transglot.ai/docs/guides/translating/translate-a-string). </Step> <Step> ### Submit for review [#submit-for-review] When you are done, select **Submit for review**. The confirmation lists anything still not translated or not approved, and says who will be told. The work order moves to **Submitted for review**. </Step> </Steps> Until a reviewer acts on it, you can select **Withdraw** to take it back: it returns to **In progress** and the people told about the submission are told it was withdrawn. ## Statuses [#statuses] | Status | Meaning | What you can do | | --- | --- | --- | | **Assigned** | Created, not started. | **Start**, or **Submit for review** | | **In progress** | You have started. | **Submit for review** | | **Submitted for review** | You have said it is done. | **Withdraw** | | **Changes requested** | A reviewer sent it back, with a note on what to change. | **Start**, or **Submit for review** | | **Approved** | A reviewer signed it off. This is final. | Nothing: the work order is closed. | Reviewers see **Approve** and **Request changes** on submitted work orders. Nobody can approve or send back their own. See [Assign a language to a translator](https://transglot.ai/docs/guides/translating/assign-a-language-to-a-translator#review-the-work). ## Who is told [#who-is-told] | Event | Who is notified | | --- | --- | | A language is assigned to you | You | | You submit or withdraw | The person who assigned it | | The work order is approved, or changes are requested | You | These notifications are emailed by default. Change that in your [notification preferences](https://transglot.ai/docs/administration/account/notification-preferences). ## If it doesn't work [#if-it-doesnt-work] - **"No assignments yet"**: nothing is assigned to you in this workspace. - **"Your access to** *language* **was removed."**: an Admin removed your access to that language or project after it was assigned. Ask them to restore it. - **"de is waiting for review. Withdraw it to keep working on it."**: you selected **Start** on submitted work. Withdraw it first. - **"de is not waiting for review, so there is nothing to withdraw. Reload the page to see where it stands."**: a reviewer acted on it before you withdrew it. ## Related [#related] - [Assign a language to a translator](https://transglot.ai/docs/guides/translating/assign-a-language-to-a-translator) - [Your notifications](https://transglot.ai/docs/administration/account/your-notifications) - [Onboard a vendor or freelancer](https://transglot.ai/docs/administration/team/onboard-a-vendor-or-freelancer) # Localize your App Store listing (https://transglot.ai/docs/integrations/app-stores/app-store-connect/) The App Store Connect connector is two-way. It reads your app's listing in its source locale, turns each field into a key in your project, and publishes reviewed translations back into each target App Store locale, creating the localization if it does not exist yet. It never writes your source locale. ## What it translates [#what-it-translates] One connection covers one app. You choose which listing fields to translate: | Field | App Store Connect surface | | --- | --- | | `name`, `subtitle` | App information | | `description`, `keywords`, `promotionalText`, `whatsNew`, `marketingUrl`, `supportUrl` | The app version | Only the fields you select become keys, and only those are ever written back. ## Requirements [#requirements] - **Plan:** Starter or above. - **Role:** Admin of the project's workspace, or an Owner of its account. - **In App Store Connect:** an API key. In **App Store Connect**, go to **Users and Access**, **Keys**, create a key, and download its `.p8` private key file. Note the key's **Key ID** and the **Issuer ID** shown on the same page. - **Your app's Apple ID:** the numeric ID under **App Information**, **General**, **Apple ID**. - **Target languages** added to your transglot project. ## Connect [#connect] <Steps> <Step> ### Open the CMS page [#open-the-cms-page] Open your project's settings, choose **CMS**, and pick **App Store Connect** in the **CMS** list. </Step> <Step> ### Enter the app and key [#enter-the-app-and-key] | Field | What to enter | | --- | --- | | **App ID** | Your app's numeric Apple ID, for example `1234567890` | | **Key ID** | The API key's ID | | **Issuer ID** | The issuer ID from the Keys page | | **Source App Store locale** | The App Store locale that holds your source listing, for example `en-US`. transglot only reads it | | **Private key (.p8)** | Paste the whole contents of the `.p8` file, from `-----BEGIN PRIVATE KEY-----` to `-----END PRIVATE KEY-----` | </Step> <Step> ### Map your locales [#map-your-locales] Under **Locale → App Store locale**, enter the App Store locale for each target language in the project, for example `fr-FR` for `fr`. A language you leave unmapped is skipped when publishing. </Step> <Step> ### Choose the fields [#choose-the-fields] Under **Content types**, select `listing`. Under **Translatable fields**, select the listing fields to translate. </Step> <Step> ### Connect, test and sync [#connect-test-and-sync] Select **Connect**, then **Test connection**, then **Sync from CMS** to import the listing. The private key is stored encrypted and never shown again. </Step> </Steps> ## Publish translations back [#publish-translations-back] Review the imported translations, then publish them with **Publish to CMS**, or turn on automatic publishing when a translation is reviewed. Only reviewed translations are published. [Connect a content platform](https://transglot.ai/docs/integrations/content-platforms) describes reviewing and publishing in full; it works the same way here. Apple accepts changes only while there is something editable: - Version fields (description, keywords, promotional text, release notes, marketing and support URLs) are written to the app version that is being prepared for submission. - `name` and `subtitle` are written to the app information that is editable with that version. transglot checks this before writing anything: if the fields you publish have nowhere editable to go, the publish fails without writing any locale. Importing still works on a live app with no version in preparation. ## Troubleshooting [#troubleshooting] <Accordions type="single"> <Accordion title="Publishing fails with "App Store Connect has no editable version""> The full message is "App Store Connect has no editable version. Create a new app version to publish store metadata (description, keywords, promotional text, release notes)." Create a new version of the app in App Store Connect, then publish again. </Accordion> <Accordion title="Publishing fails with "App Store Connect has no editable app information""> The full message is "App Store Connect has no editable app information. Create a new app version to publish the app name or subtitle." The app name and subtitle can only change alongside a new version. Create one, then publish again. </Accordion> <Accordion title="Test connection says the credentials were rejected"> Check that the **Key ID** and **Issuer ID** belong to the `.p8` key you pasted, that the key has not been revoked, and that it has access to the app. </Accordion> <Accordion title="A language was not published"> It has no App Store locale under **Locale → App Store locale**, or none of its translations are reviewed yet. </Accordion> </Accordions> ## Related [#related] - [Localize your Google Play listing](https://transglot.ai/docs/integrations/app-stores/google-play) - [Connect a content platform](https://transglot.ai/docs/integrations/content-platforms) - [Approve or flag a translation](https://transglot.ai/docs/guides/translating/approve-or-flag-a-translation) # Localize your Google Play listing (https://transglot.ai/docs/integrations/app-stores/google-play/) The Google Play connector is two-way. It reads your app's store listing in its source language, turns each field into a key in your project, and publishes reviewed translations back into each target language's listing. It never writes your source language. ## What it translates [#what-it-translates] One connection covers one app. You choose which listing fields to translate: `title`, `shortDescription` and `fullDescription`. Only the fields you select become keys, and only those are written back. Other parts of a listing, such as the promo video, are left as they are. ## Requirements [#requirements] - **Plan:** Starter or above. - **Role:** Admin of the project's workspace, or an Owner of its account. - **In Google Play Console:** a service account with the Android Publisher API enabled, granted access to your app. Download the service account's JSON key. - **Your app's package name**, for example `com.example.app`. - **Target languages** added to your transglot project. ## Connect [#connect] <Steps> <Step> ### Open the CMS page [#open-the-cms-page] Open your project's settings, choose **CMS**, and pick **Google Play** in the **CMS** list. </Step> <Step> ### Enter the app and key [#enter-the-app-and-key] | Field | What to enter | | --- | --- | | **Package name** | Your app's application ID, for example `com.example.app` | | **Source Play language** | The Play language that holds your source listing, for example `en-US`. transglot only reads it | | **Service account JSON** | Paste the whole contents of the JSON key file | </Step> <Step> ### Map your languages [#map-your-languages] Under **Locale → Play language**, enter the Play language for each target language in the project, for example `fr-FR` for `fr`. A language you leave unmapped is skipped when publishing. </Step> <Step> ### Choose the fields [#choose-the-fields] Under **Content types**, select `listing`. Under **Translatable fields**, select the listing fields to translate. </Step> <Step> ### Connect, test and sync [#connect-test-and-sync] Select **Connect**, then **Test connection**, then **Sync from CMS** to import the listing. The service account key is stored encrypted and never shown again. </Step> </Steps> ## Publish translations back [#publish-translations-back] Review the imported translations, then publish them with **Publish to CMS**, or turn on automatic publishing when a translation is reviewed. Only reviewed translations are published. [Connect a content platform](https://transglot.ai/docs/integrations/content-platforms) describes reviewing and publishing in full; it works the same way here. Each publish writes every target language inside one Google Play edit and commits it once all of them are written. If any language fails, the whole edit is discarded, so your listing never ends up half updated and no unfinished edit is left behind in Play Console. Importing never commits anything. ## Troubleshooting [#troubleshooting] <Accordions type="single"> <Accordion title="Test connection says the credentials were rejected"> Check that the JSON key is complete and has not been deleted in Google Cloud, and that the service account has been granted access to this app in Play Console. </Accordion> <Accordion title="The package name is refused"> Use the application ID exactly as it appears in Play Console: segments separated by dots, each starting with a letter, for example `com.example.app`. </Accordion> <Accordion title="A language was not published"> It has no Play language under **Locale → Play language**, or none of its translations are reviewed yet. </Accordion> </Accordions> ## Related [#related] - [Localize your App Store listing](https://transglot.ai/docs/integrations/app-stores/app-store-connect) - [Connect a content platform](https://transglot.ai/docs/integrations/content-platforms) - [Approve or flag a translation](https://transglot.ai/docs/guides/translating/approve-or-flag-a-translation) # Connect Jira (https://transglot.ai/docs/integrations/automation/connect-jira/) The Jira integration turns transglot's actionable events into Jira Cloud issues, one issue per event, in the project and issue type you choose. It is one-way: transglot writes to Jira and never reads from it, so your team can triage, move or close the issues without changing anything in transglot. ## Requirements [#requirements] - **Plan:** Team or above. On a lower plan the **Jira** card offers **Upgrade plan** instead of **Connect**. - **Role:** Admin of the workspace, or an Owner of its account. Other members can open the Jira page and see whether it is connected, but not change it. - **In Jira:** a Jira Cloud site, an Atlassian account that can create issues in the target project, and an API token for that account. ## Connect Jira [#connect-jira] <Steps> <Step> ### Create an API token [#create-an-api-token] Sign in to Atlassian as the account transglot should act as, then go to **id.atlassian.com**, **Security**, **API tokens** and create a token. Copy it: Atlassian shows it once. </Step> <Step> ### Open the Jira page [#open-the-jira-page] In your workspace, open **Integrations** and select **Connect** on the **Jira** card. </Step> <Step> ### Fill in the connection [#fill-in-the-connection] | Field | What to enter | | --- | --- | | **Jira site URL** | Your site address, for example `https://your-team.atlassian.net` | | **Atlassian account email** | The email of the account that owns the API token | | **API token** | The token you created. It is stored encrypted and never shown again | | **Project key** | The key of the Jira project issues should open in, for example `LOC` | | **Issue type** | The issue type to create. Leave it as `Task` unless your project uses another type | </Step> <Step> ### Save [#save] Select **Connect**. The confirmation reads "Jira connected." and the **Jira** card on the integrations page now shows **connected**. </Step> </Steps> ## What opens an issue [#what-opens-an-issue] transglot opens one issue each time one of these happens in the workspace: - A review is requested. - A source drift is repaired. - An assignment is created, or sent back for changes. - A translation batch fails, or finishes with some strings left untranslated. Comments, mentions and completed batches never open an issue, so the Jira project stays a list of things that need somebody. Each event opens its own issue: two failed batches produce two issues. Issue titles and descriptions are written in English and link back to the place in transglot where the work is. ## Change or disconnect [#change-or-disconnect] To change the project, issue type or account, open the Jira page, edit the fields and select **Save**. Leave **API token** blank to keep the stored token. To stop opening issues, select **Disconnect** and confirm **Disconnect Jira?**. Issues already created stay in Jira with their links back to transglot. ## Troubleshooting [#troubleshooting] <Accordions type="single"> <Accordion title=""A Jira API token is required to connect.""> The first connection needs a token. Paste the API token into **API token** and save again. </Accordion> <Accordion title="The connection saved but no issues appear"> Check that the Atlassian account whose token you used can create issues in that project, that the **Project key** is the project's key (not its name), and that the **Issue type** exists in that project. Remember that only the events listed above open issues. </Accordion> <Accordion title="The card shows Upgrade plan"> Jira is included on the Team plan and above. See [Plan requirements](https://transglot.ai/docs/integrations/plan-requirements). </Accordion> </Accordions> ## Related [#related] - [Connect Slack](https://transglot.ai/docs/integrations/notifications/connect-slack) - [Plan requirements](https://transglot.ai/docs/integrations/plan-requirements) - [Integrations overview](https://transglot.ai/docs/integrations) # Zapier, Make and n8n (https://transglot.ai/docs/integrations/automation/zapier-make-and-n8n/) Automation platforms connect to transglot in two directions, and you can use either or both: - **transglot to your flow.** transglot sends a signed event to your flow when something happens in a project: a batch finishes, translations are reviewed, a language is added, a quality check completes. Use it for "when a batch completes, post to the release channel" or "when a locale is reviewed, start a deploy", without polling. - **Your flow to transglot.** Your flow calls the public API to push source files, start a translation, read a batch's status or pull a translated file. Both directions authenticate with a [deploy token](https://transglot.ai/docs/developers/deploy-tokens), which belongs to one project. There is no project id to configure: the token is the project. ## Requirements [#requirements] - **Plan:** Starter or above. The API, deploy tokens and webhooks are not part of the Free plan. - **A deploy token** for the project, created by an Owner or Admin. Give it only the abilities your flow uses: | What the flow does | Ability needed | | --- | --- | | Read the project or a batch | any ability | | Push source files | `push` | | Pull translated files | `pull` | | Start a translation | `manage` | | Subscribe to events (turn a trigger on or off) | `manage` | ## n8n [#n8n] transglot publishes an n8n community node package, `n8n-nodes-transglot`. It adds two nodes: - **transglot**, an action node. Resource **Project**: **Get**, **Push Source**, **Translate**, **Pull**. Resource **Batch**: **Get**. - **transglot Trigger**, which starts a workflow when the project event you pick in **Event** fires. <Steps> <Step> ### Install the package [#install-the-package] In your n8n instance, go to **Settings**, **Community Nodes**, **Install**, enter `n8n-nodes-transglot` and confirm. On a self-hosted instance you can also install it into your custom nodes directory and restart n8n: ```bash npm install n8n-nodes-transglot ``` </Step> <Step> ### Create the credential [#create-the-credential] Add a **transglot API** credential: | Field | Value | | --- | --- | | **Base URL** | `https://api.transglot.ai` (the default) | | **API Token** | Your deploy token, starting `tgl_` | Select **Test**. A successful test confirms the URL, the token and the project without changing anything. </Step> <Step> ### Build the workflow [#build-the-workflow] Add a **transglot Trigger** node to react to events, or a **transglot** node to act on the project. When you activate a workflow that starts with **transglot Trigger**, the node subscribes to the event; when you deactivate it, the subscription is removed. </Step> </Steps> ## Zapier and Make [#zapier-and-make] Use the webhook and HTTP tools built into Zapier and Make. **To start a Zap or scenario from a transglot event:** 1. Create a trigger in Zapier or Make that gives you an incoming webhook URL. 2. In transglot, [create a webhook endpoint](https://transglot.ai/docs/developers/webhooks/create-a-webhook-endpoint) on the project with that URL, and choose the events that should start the flow. 3. Zapier and Make learn the payload shape from the first delivery they receive. To map fields before then, use the examples in the [event reference](https://transglot.ai/docs/developers/webhooks/events), or fetch one from `GET /v1/webhooks/events/{event}/sample`. Every delivery is signed. If your flow can run code, [verify the signature](https://transglot.ai/docs/developers/webhooks/verify-signatures) before acting on a delivery. **To act on transglot from a Zap or scenario,** add an HTTP request step that calls the API with the header `Authorization: Bearer <your deploy token>`. Common calls: | Goal | Request | | --- | --- | | Push a source file | `POST https://api.transglot.ai/v1/push` | | Start a translation | `POST https://api.transglot.ai/v1/translate` | | Check a batch | `GET https://api.transglot.ai/v1/batches/{id}` | | Pull a translated file | `GET https://api.transglot.ai/v1/pull` | See [the API reference](https://transglot.ai/docs/developers/api) for parameters and responses. ## Manage event subscriptions over the API [#manage-event-subscriptions-over-the-api] Any tool that can make HTTP requests can subscribe and unsubscribe itself, the way the n8n trigger does: | Request | What it does | | --- | --- | | `GET /v1/webhooks/events` | Lists the events you can subscribe to | | `GET /v1/webhooks/events/{event}/sample` | Returns a sample payload for one event | | `POST /v1/webhooks` | Subscribes a URL to one or more events. Send `url` and an `events` array. The response includes the signing `secret`, shown only this once | | `GET /v1/webhooks` | Lists this project's subscriptions | | `DELETE /v1/webhooks/{id}` | Removes a subscription | A subscription created this way is an ordinary webhook endpoint on the project, with the same signing, retries and delivery log as one created in the console. ## Troubleshooting [#troubleshooting] <Accordions type="single"> <Accordion title="The credential test or a request returns 401"> The token is wrong, expired or revoked. Create a new deploy token and update the credential. </Accordion> <Accordion title="Subscribing to an event returns 403"> Either the token lacks the `manage` ability, or the account's plan does not include webhooks. Webhooks need the Starter plan or above. </Accordion> <Accordion title="Subscribing returns 422 webhook-url-not-allowed"> transglot only delivers to public HTTPS addresses. The response says which rule the URL broke, for example "The webhook URL must use https." or "The webhook URL resolves to a private or reserved address." Use the webhook URL your automation platform gives you. </Accordion> </Accordions> ## Related [#related] - [Webhooks](https://transglot.ai/docs/developers/webhooks) - [Deploy tokens](https://transglot.ai/docs/developers/deploy-tokens) - [Connect Jira](https://transglot.ai/docs/integrations/automation/connect-jira) # Connect BigCommerce (https://transglot.ai/docs/integrations/content-platforms/bigcommerce/) <Callout type="info" title="Import only"> Nothing is written back to BigCommerce today: its per-language product write API is in beta and limited to Catalyst storefronts. Take the translations out through the [REST API](https://transglot.ai/docs/developers/api), the [transglot CLI](https://transglot.ai/docs/developers/cli) or the [CDN](https://transglot.ai/docs/guides/delivery/the-cdn). </Callout> | | | | --- | --- | | Direction | Import only | | Imported | Product `name` and `description` from the Catalog API | | Written back | Nothing | | Sync | **Sync from CMS** | | Plan | Starter and above | ## Before you start [#before-you-start] In your BigCommerce store's API settings, create a store-level API account with these scopes: - **Products**: read-only - **Store information**: read-only Copy the access token, and the store hash from the account's API path (`api.bigcommerce.com/stores/{hash}/…`). ## Connect [#connect] Open your project's settings, choose **CMS**, and pick **BigCommerce** in the **CMS** list. There is no URL to type. | Field | What to enter | | --- | --- | | **Store hash** | The store hash, letters and digits only, for example `abc123` | | **API token** | The store-level API account's access token | | **Content types** | `products` | | **Translatable fields** | `name`, `description`, or both | Select **Connect**, then **Test connection**. A successful test names the store. ## What gets imported [#what-gets-imported] transglot reads every product through the v3 Catalog API and imports the mapped fields that hold text. ## Troubleshooting [#troubleshooting] - `... the credentials were rejected.` The token is wrong or the API account is missing a scope above. - `... the store or product was not found, check the store hash.` Check **Store hash**. - `... the store is rate-limited, retry.` Wait and run the sync again. ## Related [#related] - [Connect a content platform](https://transglot.ai/docs/integrations/content-platforms) - [Plan requirements](https://transglot.ai/docs/integrations/plan-requirements) # Connect Confluence (https://transglot.ai/docs/integrations/content-platforms/confluence/) <Callout type="info" title="Import only"> Nothing is written back to Confluence. Confluence has no per-page translation: a translated page lives in a separate space or page, not in a field of the original. Take the translations out through the [REST API](https://transglot.ai/docs/developers/api), the [transglot CLI](https://transglot.ai/docs/developers/cli) or the [CDN](https://transglot.ai/docs/guides/delivery/the-cdn). </Callout> | | | | --- | --- | | Direction | Import only | | Imported | The title and body of every page in one space | | Written back | Nothing | | Sync | **Sync from CMS** | | Plan | Starter and above | ## Before you start [#before-you-start] - The connector works with Confluence sites on `atlassian.net`. - Create an API token for your Atlassian account in your Atlassian account's security settings. The account needs read access to the space. - Note the space key, which appears in the space URL (for example `/wiki/spaces/DOCS`). ## Connect [#connect] Open your project's settings, choose **CMS**, and pick **Confluence** in the **CMS** list. | Field | What to enter | | --- | --- | | **Atlassian site** | Only the site name, for example `acme` for `acme.atlassian.net` | | **Space key** | The key of the space holding the pages, for example `DOCS` | | **Email & API token** | Your Atlassian account email and the API token, as `you@example.com:api_token` | | **Content types** | `pages` | | **Translatable fields** | `title`, `body`, or both | Select **Connect**, then **Test connection**. A successful test names the space. ## What gets imported [#what-gets-imported] transglot reads every page in the space and imports its title and its body. The body is imported in Confluence's storage format, so the page markup comes with the text. ## Troubleshooting [#troubleshooting] - `The Confluence space [DOCS] was not found. Check the space key.` The key is wrong, or the account cannot see the space. - `... the credentials were rejected.` Check the email and token, entered as `email:token`. - `... the space or page was not found, check the site and space key.` Check **Atlassian site** holds only the site name. - `... Confluence is rate-limiting the request, retry.` Wait and run the sync again. ## Related [#related] - [Connect a content platform](https://transglot.ai/docs/integrations/content-platforms) - [Plan requirements](https://transglot.ai/docs/integrations/plan-requirements) # Connect Contentful (https://transglot.ai/docs/integrations/content-platforms/contentful/) | | | | --- | --- | | Direction | Two-way | | Imported | The source-locale value of the fields you map, on the content types you choose | | Written back | Each entry's target-locale field slots. The source locale is never written | | Sync | **Sync from CMS**, or a signed webhook to the **Inbound URL** | | Plan | Starter and above | ## Before you start [#before-you-start] - In Contentful, mark the fields you want translated as localized and add your target locales to the space. - Make sure the Contentful locale codes match the language codes in your transglot project. transglot reads the locale whose code is your project's source language, and writes each target language under its own code. - Create a Content Management token in your Contentful settings. It needs read and write access to the space. ## Connect [#connect] Open your project's settings, choose **CMS**, and pick **Contentful** in the **CMS** list. | Field | What to enter | | --- | --- | | **Space ID** | Your space ID, from your Contentful space settings | | **Environment** | The environment to use. Leave blank for `master` | | **Content Management token** | The token you created (it starts with `CFPAT-`) | | **Content types** | Content type IDs, comma separated, for example `blogPost, landingPage` | | **Translatable fields** | Field IDs, comma separated, for example `title, body` | | **Publish these locales** | The target locales to write back | Select **Connect**, then **Test connection**. A successful test names the space it reached. The test also checks that the environment exists. ## What gets imported [#what-gets-imported] For each entry of each content type, transglot reads the source-locale value of every mapped field. Only fields whose value is a plain string are imported; a field with another kind of value (Rich Text, a reference, a number) is skipped. When the entry has a `title` field, its value becomes the description on each of the entry's keys. ## How translations are written back [#how-translations-are-written-back] transglot reads the entry's current version, sets the target-locale slot of each mapped field, and saves the entry with Contentful's version check. Every other field and locale is kept as it was. transglot saves the translations to the entry, but it does not run Contentful's publish action. Publish the entry in Contentful when you want the translations live. ## Sync on content change [#sync-on-content-change] Once connected, the CMS page shows a **Sync on content change** card with an **Inbound URL**. A POST to that URL starts a sync without anyone pressing **Sync from CMS**. The CMS page suggests triggering it on `Entry.publish`. transglot accepts the request only when it is signed with the inbound signing secret shown once when you first connected: - `X-Transglot-Timestamp`: the current Unix time in seconds. A timestamp more than five minutes away from transglot's clock is refused. - `X-Transglot-Signature`: `sha256=` followed by the hex HMAC-SHA256 of the timestamp, a full stop, and the raw request body, keyed with the secret. This is the same scheme as transglot's outbound webhooks ([Verify signatures](https://transglot.ai/docs/developers/webhooks/verify-signatures)). To sync only the entries that changed, send a body that names them: ```json { "entries": [ { "type": "blogPost", "id": "6xRkQ2pZ" } ] } ``` The `type` is the content type ID and the `id` is the entry ID. A body that names no entries at all runs a full sync. A body with an `entries` list that holds no usable entry is accepted and does nothing. ## Troubleshooting [#troubleshooting] - `Invalid signature.` The request was unsigned, signed with an old secret, or its timestamp was outside the five-minute window. If you no longer have the secret, select **Rotate signing secret** and update your webhook. - `... the credentials were rejected.` The token is wrong or revoked. Paste a current Content Management token and save. - `... the space, environment or entry was not found, check the settings.` Check **Space ID** and **Environment**. - `... the entry changed underneath us (version conflict), retry.` Someone saved the entry in Contentful while transglot was writing it. Run **Publish to CMS** again. - A field never appears as a key: check the field ID, check it has a value in the source locale, and check it is a plain text field. ## Related [#related] - [Connect a content platform](https://transglot.ai/docs/integrations/content-platforms) - [Approve or flag a translation](https://transglot.ai/docs/guides/translating/approve-or-flag-a-translation) - [Plan requirements](https://transglot.ai/docs/integrations/plan-requirements) # Connect Contentstack (https://transglot.ai/docs/integrations/content-platforms/contentstack/) | | | | --- | --- | | Direction | Two-way | | Imported | The source-locale version of the fields you map, on the content types you choose | | Written back | Each entry's target-locale version. The source locale is never written | | Sync | **Sync from CMS** | | Plan | Starter and above | ## Before you start [#before-you-start] - Add your target languages to the stack. Contentstack locale codes must match the language codes in your transglot project. - Create a Management Token with read and write access in the stack's token settings. - Note the stack API key from the stack settings. It identifies the stack and is not a secret. ## Connect [#connect] Open your project's settings, choose **CMS**, and pick **Contentstack** in the **CMS** list. | Field | What to enter | | --- | --- | | **Data-hosting region** | Your stack's region: AWS-NA, AWS-EU, AWS-AU, AZURE-NA, AZURE-EU, GCP-NA or GCP-EU | | **Stack API key** | The stack API key, for example `blt0123456789abcdef` | | **Management token** | The Management Token you created | | **Content types** | Content type UIDs, comma separated | | **Translatable fields** | Field UIDs, comma separated | | **Publish these locales** | The target locales to write back | Select **Connect**, then **Test connection**. A successful test names the stack. ## What gets imported [#what-gets-imported] For each content type, transglot lists the entries in your source locale and imports the mapped fields that hold text. Each entry's `title` becomes the description on its keys. ## How translations are written back [#how-translations-are-written-back] Contentstack keeps a separate version of an entry for each locale. For each target locale, transglot reads that locale's version, replaces only the mapped fields with the reviewed values, and saves it. Saving a target locale for the first time localizes the entry into that locale. Every other field is kept as it was, and the source-locale version is never addressed. transglot saves the localized entries but does not publish them. Publish in Contentstack when you want the translations live. ## Troubleshooting [#troubleshooting] - `... the credentials were rejected.` The Management Token or stack API key is wrong, or the token lacks write access. - `... the stack, content type or entry was not found, check the settings.` Check **Data-hosting region**, **Stack API key** and the content type UIDs. - `... Contentstack is rate-limiting the request, retry.` Wait and run the sync or publish again. ## Related [#related] - [Connect a content platform](https://transglot.ai/docs/integrations/content-platforms) - [Plan requirements](https://transglot.ai/docs/integrations/plan-requirements) # Connect DatoCMS (https://transglot.ai/docs/integrations/content-platforms/datocms/) | | | | --- | --- | | Direction | Two-way | | Imported | The source-locale value of the localized fields you map, on the models you choose | | Written back | Each field's target-locale slot. The source locale and every other locale are kept | | Sync | **Sync from CMS** | | Plan | Starter and above | ## Before you start [#before-you-start] - Mark the fields you want translated as localized in each model. - Enable your target locales in the project's locale settings. DatoCMS locale codes must match the language codes in your transglot project. - Create an API token with read and write access to your records in the project's API token settings. ## Connect [#connect] Open your project's settings, choose **CMS**, and pick **DatoCMS** in the **CMS** list. There is no URL to type: the API token identifies the project. | Field | What to enter | | --- | --- | | **API token** | The token you created | | **Content types** | Model IDs (or model API keys), comma separated | | **Translatable fields** | Localized field API keys, comma separated | | **Publish these locales** | The target locales to write back | Select **Connect**, then **Test connection**. A successful test names the project. ## What gets imported [#what-gets-imported] For each record of each model, transglot reads the source-locale value of every mapped localized field and imports the ones that hold text. ## How translations are written back [#how-translations-are-written-back] DatoCMS treats a localized field's value as a whole, so writing one locale on its own would drop the others. transglot reads the record's current field values, adds the target-locale values, and writes the merged field back. The source locale and every other locale keep their values. ## Troubleshooting [#troubleshooting] - `... the credentials were rejected.` The token is wrong, revoked, or read-only. - `... the model or record was not found, check the settings.` Check the model IDs in **Content types**. - `... DatoCMS rejected the write, check the field/locale mapping.` A mapped field is not localized, or a target locale is not enabled in the project. ## Related [#related] - [Connect a content platform](https://transglot.ai/docs/integrations/content-platforms) - [Plan requirements](https://transglot.ai/docs/integrations/plan-requirements) # Connect Drupal (https://transglot.ai/docs/integrations/content-platforms/drupal/) | | | | --- | --- | | Direction | Two-way | | Imported | The source-language value of the fields you map, on the content types (node bundles) you choose | | Written back | Each node's target-language translation, which must already exist. The source language is never written | | Sync | **Sync from CMS** | | Per sync | Up to 2,500 nodes per content type | | Plan | Starter and above | ## Before you start [#before-you-start] - Enable the **JSON:API** and **Content Translation** modules, and the core **HTTP Basic Authentication** module. - Use URL language negotiation with a path prefix per language. transglot addresses each language as `/{language}/jsonapi/…`, using your project's language codes, so the path prefixes must match them. - Add the target-language translation to each node you want localized. JSON:API can update a translation but cannot create one. - Create a Drupal user with JSON:API read and write access to those nodes. - Your site must be reachable over https at a public address. ## Connect [#connect] Open your project's settings, choose **CMS**, and pick **Drupal** in the **CMS** list. | Field | What to enter | | --- | --- | | **Drupal site URL** | Your site root, for example `https://drupal.example.com` | | **User & password** | The user's credentials as `username:password` | | **Content types** | Node bundle machine names, comma separated, for example `article, page` | | **Translatable fields** | Field machine names, comma separated, for example `title, body` | | **Publish these locales** | The target languages to write back | Select **Connect**, then **Test connection**. The test reads one node of the first content type in your source language. ## What gets imported [#what-gets-imported] For each bundle, transglot reads the source-language nodes and imports the mapped fields that hold text. A formatted text field (such as `body`) is imported from its `value`. The node's `title` becomes the description on its keys. ## How translations are written back [#how-translations-are-written-back] For each target language, transglot reads the node's existing translation and updates only the mapped fields. A formatted text field keeps its text format and summary; only its value changes. The source-language node is never written. If the target translation does not exist, that entry fails with a clear error instead of being skipped silently. The rest of the publish continues. ## Troubleshooting [#troubleshooting] - `The fr translation of article #... does not exist in Drupal. Create it there first (JSON:API can update a translation but not create one).` Add the translation in Drupal, then publish again. - `... the credentials were rejected.` The username or password is wrong, HTTP Basic Authentication is not enabled, or the user lacks JSON:API write access. - `... the endpoint, node or translation was not found, check the base URL and content type.` Check **Drupal site URL**, the bundle names, and that language path prefixes match your project's language codes. - `... Drupal rejected the request body, check the field mapping.` A mapped field cannot take a text value, or does not exist on the bundle. ## Related [#related] - [Connect a content platform](https://transglot.ai/docs/integrations/content-platforms) - [Plan requirements](https://transglot.ai/docs/integrations/plan-requirements) # Connect Freshdesk (https://transglot.ai/docs/integrations/content-platforms/freshdesk/) | | | | --- | --- | | Direction | Two-way | | Imported | The title and description (body) of every solution article | | Written back | A translated article per target language. The primary article is never written | | Sync | **Sync from CMS** | | Plan | Starter and above | ## Before you start [#before-you-start] - Add your target languages to the help desk's supported languages first. Freshdesk rejects a translation for a language that is not enabled. - Copy your API key from your Freshdesk profile settings. The agent needs read and write access to solutions. ## Connect [#connect] Open your project's settings, choose **CMS**, and pick **Freshdesk** in the **CMS** list. | Field | What to enter | | --- | --- | | **Freshdesk domain** | Only the domain, for example `acme` for `acme.freshdesk.com` | | **API key** | Your Freshdesk API key | | **Content types** | `articles` | | **Translatable fields** | `title`, `description`, or both | | **Publish these locales** | The target languages to write back | Select **Connect**, then **Test connection**. A successful test names the help desk. ## What gets imported [#what-gets-imported] transglot walks your knowledge base (categories, then folders, then articles) and imports each article's title and description in the primary language. The description is the article body. ## How translations are written back [#how-translations-are-written-back] For each target language, transglot updates the translated article if it exists and creates it if it does not. Only the title and description are sent, and the primary article is never addressed. A translated article takes the primary article's status, so a translation is never published ahead of its source: a draft article gets a draft translation. Language codes are converted to Freshdesk's form, so `pt_BR` in transglot is published as `pt-BR`. ## Troubleshooting [#troubleshooting] - `... the translation language is not enabled on this help desk.` Add the language to the help desk's supported languages, then publish again. - `... the credentials were rejected.` The API key is wrong, or the agent lacks access to solutions. - `... the article or knowledge base was not found.` Check **Freshdesk domain**, and that the article still exists. - `... the help desk is rate-limited, retry.` Wait and run the sync or publish again. ## Related [#related] - [Connect a content platform](https://transglot.ai/docs/integrations/content-platforms) - [Plan requirements](https://transglot.ai/docs/integrations/plan-requirements) # Connect Ghost (https://transglot.ai/docs/integrations/content-platforms/ghost/) <Callout type="info" title="Import only"> Nothing is written back to Ghost. A Ghost post is single-language and has no per-language slot to write into. Take the translations out through the [REST API](https://transglot.ai/docs/developers/api), the [transglot CLI](https://transglot.ai/docs/developers/cli) or the [CDN](https://transglot.ai/docs/guides/delivery/the-cdn). </Callout> | | | | --- | --- | | Direction | Import only | | Imported | The `title` and `plaintext` of posts and pages | | Written back | Nothing | | Sync | **Sync from CMS** | | Plan | Starter and above | ## Before you start [#before-you-start] In Ghost admin, add a custom integration in the integration settings and copy its **Admin API key**. It has the form `{key-id}:{hex-secret}`. ## Connect [#connect] Open your project's settings, choose **CMS**, and pick **Ghost** in the **CMS** list. | Field | What to enter | | --- | --- | | **Ghost site URL** | Your Ghost site root, for example `https://blog.example.com`. transglot adds `/ghost/api/admin/…` itself. It must use https and be publicly reachable | | **Admin API key** | The custom integration's Admin API key | | **Content types** | `posts`, `pages`, or both | | **Translatable fields** | `title`, `plaintext`, or both | Select **Connect**, then **Test connection**. A successful test names the site. ## What gets imported [#what-gets-imported] transglot reads each post and page through the Ghost Admin API and imports its title and its plain-text body. The `plaintext` field is the body without formatting, so markup and cards are not carried into transglot. ## Troubleshooting [#troubleshooting] - The form refuses the key: an Admin API key is a 24-character hex ID, a colon, and a 64-character hex secret. A Content API key has a different form and will not work. - `The Ghost Admin API key is malformed (expected "id:hex-secret").` or `The Ghost Admin API secret is not valid hexadecimal.` Copy the Admin API key again from the custom integration. - `Failed verifying the Ghost Admin API key: the credentials were rejected.` The key is wrong or the integration was deleted. - `... the resource was not found, check the site URL.` Check **Ghost site URL** points at the site root. ## Related [#related] - [Connect a content platform](https://transglot.ai/docs/integrations/content-platforms) - [Plan requirements](https://transglot.ai/docs/integrations/plan-requirements) # Connect Help Scout (https://transglot.ai/docs/integrations/content-platforms/help-scout/) <Callout type="info" title="Import only"> Nothing is written back to Help Scout. Help Scout Docs models each language as a separate Docs site, not as a translation of an article. Take the translations out through the [REST API](https://transglot.ai/docs/developers/api), the [transglot CLI](https://transglot.ai/docs/developers/cli) or the [CDN](https://transglot.ai/docs/guides/delivery/the-cdn). </Callout> | | | | --- | --- | | Direction | Import only | | Imported | The name and text of every Docs article | | Written back | Nothing | | Sync | **Sync from CMS** | | Plan | Starter and above | ## Before you start [#before-you-start] Copy a **Docs API key** from the API keys section of your Help Scout profile. It must be a Docs API key, not a Mailbox API key. ## Connect [#connect] Open your project's settings, choose **CMS**, and pick **Help Scout** in the **CMS** list. There is no URL to type. | Field | What to enter | | --- | --- | | **Docs API key** | Your Help Scout Docs API key | | **Content types** | `articles` | | **Translatable fields** | `name`, `text`, or both | Select **Connect**, then **Test connection**. A successful test names the first collection it finds. ## What gets imported [#what-gets-imported] transglot walks your collections and their articles, and imports each article's name (its title) and text (its body). ## Troubleshooting [#troubleshooting] - `... the credentials were rejected.` The key is wrong, revoked, or is a Mailbox API key rather than a Docs API key. - `... the collection or article was not found.` The article was removed while the sync ran. Run **Sync from CMS** again. - `... Help Scout is rate-limiting the request, retry.` Wait and run the sync again. ## Related [#related] - [Connect a content platform](https://transglot.ai/docs/integrations/content-platforms) - [Plan requirements](https://transglot.ai/docs/integrations/plan-requirements) # Connect HubSpot (https://transglot.ai/docs/integrations/content-platforms/hubspot/) <Callout type="info" title="Import only"> Nothing is written back to HubSpot today: its per-language write path (creating a language variation of a published post, then editing it) is not supported yet. Take the translations out through the [REST API](https://transglot.ai/docs/developers/api), the [transglot CLI](https://transglot.ai/docs/developers/cli) or the [CDN](https://transglot.ai/docs/guides/delivery/the-cdn). </Callout> | | | | --- | --- | | Direction | Import only | | Imported | The `htmlTitle` and `postBody` of source blog posts | | Written back | Nothing | | Sync | **Sync from CMS** | | Plan | Starter and above | ## Before you start [#before-you-start] Create a private app in your HubSpot integration settings with the `content` scope, and copy its access token. ## Connect [#connect] Open your project's settings, choose **CMS**, and pick **HubSpot** in the **CMS** list. There is no URL to type: the token identifies your HubSpot account. | Field | What to enter | | --- | --- | | **Private app token** | The private app's access token (it starts with `pat-`) | | **Content types** | `blog-posts` | | **Translatable fields** | `htmlTitle`, `postBody`, or both | Select **Connect**, then **Test connection**. The test reads one blog post. ## What gets imported [#what-gets-imported] transglot reads your blog posts through the CMS Blogs API and imports each post's title and body. A post that is already a translation of another post (a language variation) is skipped, so a translation is never imported as source text. ## Troubleshooting [#troubleshooting] - `... the credentials were rejected.` The token is wrong, revoked, or the private app does not have the `content` scope. - `... the blog or post was not found.` The post was removed while the sync ran. Run **Sync from CMS** again. - `... HubSpot is rate-limiting the request, retry.` Wait and run the sync again. ## Related [#related] - [Connect a content platform](https://transglot.ai/docs/integrations/content-platforms) - [Plan requirements](https://transglot.ai/docs/integrations/plan-requirements) # Connect Hygraph (https://transglot.ai/docs/integrations/content-platforms/hygraph/) | | | | --- | --- | | Direction | Two-way | | Imported | The source-locale value of the localized fields you map, on the models you choose | | Written back | Each entry's target-locale localization, created if it does not exist yet. The source locale is never written | | Sync | **Sync from CMS** | | Plan | Starter and above | ## Before you start [#before-you-start] - Add your target locales in the project's locale settings and mark the fields you want translated as localized. Hygraph's locale API IDs must match the language codes in your transglot project. - Create a Permanent Auth Token with read and mutation permissions (including localizations) in the project's API access settings. ## Connect [#connect] Open your project's settings, choose **CMS**, and pick **Hygraph** in the **CMS** list. | Field | What to enter | | --- | --- | | **Content API endpoint URL** | Your project's High Performance or Content API URL, from the project's API access settings | | **Permanent Auth Token** | The token you created | | **Content types** | Singular model API IDs in PascalCase, comma separated, for example `Post, Author` | | **Translatable fields** | Localized field API IDs, comma separated | | **Publish these locales** | The target locales to write back | Select **Connect**, then **Test connection**. The test runs a minimal GraphQL query against the endpoint. transglot builds its queries from Hygraph's default names: the model `Post` is read through `posts` and `post`, and written with `updatePost`. Model, field and locale IDs may contain only letters, digits and underscores, and cannot start with a digit. ## What gets imported [#what-gets-imported] For each model, transglot queries the entries in your source locale and imports the mapped fields that hold text. When an entry has a `title` field, its value becomes the description on the entry's keys. ## How translations are written back [#how-translations-are-written-back] Hygraph localizes whole entries rather than single fields. transglot runs one update mutation per entry that upserts the target-locale localizations with the reviewed values. A target locale that does not exist on the entry yet is created; one that exists is updated. The source localization is never addressed. transglot does not run a publish mutation. Publish the entries in Hygraph when you want the translations live. ## Troubleshooting [#troubleshooting] - `... the credentials were rejected.` The token is wrong or lacks the permissions above. - `... the endpoint was not found, check the Content API URL.` Check **Content API endpoint URL**. - `Hygraph rejected the GraphQL request. Check the model, field and locale mapping.` A model or field ID does not exist, is not localized, or a locale is not set up in the project. - `The Hygraph connector cannot use the identifier [...] in a GraphQL query.` A model, field or locale ID contains characters other than letters, digits and underscores. ## Related [#related] - [Connect a content platform](https://transglot.ai/docs/integrations/content-platforms) - [Plan requirements](https://transglot.ai/docs/integrations/plan-requirements) # Connect a content platform (https://transglot.ai/docs/integrations/content-platforms/) A content platform connection reads text out of a system such as Contentful, Shopify or Zendesk Guide, turns each translatable field into a key in your project, and (for two-way platforms) writes the reviewed translations back into that system's own per-language slots. Each project holds one connection. You set it up on the project's **CMS** settings page. ## Who can do this [#who-can-do-this] Owners and admins of the project. Content platform connections are included on the Starter plan and above; see [Plan requirements](https://transglot.ai/docs/integrations/plan-requirements) and [pricing](https://transglot.ai/pricing). ## Two-way and import only [#two-way-and-import-only] Every connector is one of two kinds, and the CMS page tells you which before you connect. - **Two-way.** transglot imports your source content, and publishes reviewed translations back into the platform. It writes only the target-language slots and never touches your source content. - **Import only.** transglot imports your source content and you translate it, but nothing is written back, because the platform has no per-language place to write to. In place of the publish controls, the CMS page shows a card such as **Notion is import only**. For an import-only platform, you take the translations out another way: pull any locale as a localization file over the [REST API](https://transglot.ai/docs/developers/api) or the [transglot CLI](https://transglot.ai/docs/developers/cli), or publish it as a [CDN bundle](https://transglot.ai/docs/guides/delivery/the-cdn) your app reads at runtime. ## Every connector [#every-connector] | Platform | Direction | Translations are written to | | --- | --- | --- | | [Contentful](https://transglot.ai/docs/integrations/content-platforms/contentful) | Two-way | Each entry's localized field slots | | [Sanity](https://transglot.ai/docs/integrations/content-platforms/sanity) | Two-way | The target-language keys of field-level locale objects | | [Storyblok](https://transglot.ai/docs/integrations/content-platforms/storyblok) | Two-way | Each story's field-level translation keys | | [Webflow](https://transglot.ai/docs/integrations/content-platforms/webflow) | Two-way | Each CMS item's secondary-locale variants | | [Strapi](https://transglot.ai/docs/integrations/content-platforms/strapi) | Two-way | Each document's target-locale localization | | [DatoCMS](https://transglot.ai/docs/integrations/content-platforms/datocms) | Two-way | Each record's target-locale field slots | | [Hygraph](https://transglot.ai/docs/integrations/content-platforms/hygraph) | Two-way | Each entry's target-locale localization | | [Contentstack](https://transglot.ai/docs/integrations/content-platforms/contentstack) | Two-way | Each entry's target-locale version | | [Kontent.ai](https://transglot.ai/docs/integrations/content-platforms/kontent-ai) | Two-way | Each item's target-language variant | | [Prismic](https://transglot.ai/docs/integrations/content-platforms/prismic) | Import only | Not written back | | [WordPress](https://transglot.ai/docs/integrations/content-platforms/wordpress) | Two-way | One translation bundle per post, page or term | | [Drupal](https://transglot.ai/docs/integrations/content-platforms/drupal) | Two-way | Each node's existing target-language translation | | [Ghost](https://transglot.ai/docs/integrations/content-platforms/ghost) | Import only | Not written back | | [Notion](https://transglot.ai/docs/integrations/content-platforms/notion) | Import only | Not written back | | [Confluence](https://transglot.ai/docs/integrations/content-platforms/confluence) | Import only | Not written back | | [Shopify](https://transglot.ai/docs/integrations/content-platforms/shopify) | Two-way | Shopify translations in each secondary locale | | [WooCommerce](https://transglot.ai/docs/integrations/content-platforms/woocommerce) | Two-way | One translation bundle per product | | [Magento](https://transglot.ai/docs/integrations/content-platforms/magento) | Two-way | Each target store view | | [BigCommerce](https://transglot.ai/docs/integrations/content-platforms/bigcommerce) | Import only | Not written back | | [PrestaShop](https://transglot.ai/docs/integrations/content-platforms/prestashop) | Two-way | Each target language's field value | | [Zendesk Guide](https://transglot.ai/docs/integrations/content-platforms/zendesk-guide) | Two-way | Per-locale article translations | | [Intercom](https://transglot.ai/docs/integrations/content-platforms/intercom) | Two-way | Each article's per-locale translated content | | [Freshdesk](https://transglot.ai/docs/integrations/content-platforms/freshdesk) | Two-way | Per-language translated articles | | [Help Scout](https://transglot.ai/docs/integrations/content-platforms/help-scout) | Import only | Not written back | | [HubSpot](https://transglot.ai/docs/integrations/content-platforms/hubspot) | Import only | Not written back | | [Mailchimp](https://transglot.ai/docs/integrations/content-platforms/mailchimp) | Import only | Not written back | | [Klaviyo](https://transglot.ai/docs/integrations/content-platforms/klaviyo) | Import only | Not written back | App Store Connect and Google Play listings connect from the same CMS page. They have their own pages under [App stores](https://transglot.ai/docs/integrations/app-stores/app-store-connect). ## Connect a platform [#connect-a-platform] <Steps> <Step> ### Prepare the platform [#prepare-the-platform] Each connector page lists what to set up first: usually the target languages enabled on the platform side, and a token or key with read access (and write access, for a two-way platform). </Step> <Step> ### Open the CMS page [#open-the-cms-page] Open your project's settings and choose **CMS**. In the **CMS** list, pick the platform. The page shows a short explanation of how that connector works and what it needs. </Step> <Step> ### Fill in the connection [#fill-in-the-connection] Enter the address fields and the credential the form asks for, then choose the **Content types** and **Translatable fields** to import. For a two-way platform, tick the locales under **Publish these locales**. Select **Connect**. The credential is stored encrypted and never shown again; on later edits, leave it blank to keep the stored one. </Step> <Step> ### Test the connection [#test-the-connection] Select **Test connection**. On success the page reports **Connection verified.**, usually followed by the name of the site, space or store it reached. </Step> <Step> ### Run the first sync [#run-the-first-sync] Under **Sync**, select **Sync from CMS**. The import runs in the background; **Last sync** updates when it finishes. </Step> </Steps> The platform is fixed for the life of a connection. To switch platforms, disconnect and connect again. ## How imported content becomes keys [#how-imported-content-becomes-keys] Each sync reads the source-language value of every mapped field, for every entry of every selected content type. Each field becomes one key, named after the platform, the content type, the entry and the field (for example `wp:posts:42:title`). Because the name is stable, a later sync updates the same key instead of creating a duplicate. - Empty fields are skipped, so they never create a key. - The platform is the source of truth for source text. Edit source text in the platform: the next sync replaces the source text in transglot with what the platform holds. - One sync reads up to 5,000 entries in total. Some platforms read fewer per content type; their pages give the limit. Imported keys are ordinary keys. They go through translation memory, the glossary and quality checks like everything else. If **Auto-translate** is on in [AI translation settings](https://transglot.ai/docs/guides/translating/ai-translation-settings), new and changed source text is translated by Glossa as it arrives. A translation you have edited or reviewed is never overwritten by AI. ## Review, then publish back [#review-then-publish-back] Only translations in the **Reviewed** state are published. Review them in the [editor](https://transglot.ai/docs/guides/translating/the-editor) or with [Approve or flag a translation](https://transglot.ai/docs/guides/translating/approve-or-flag-a-translation). For a two-way platform, publishing sends: - only keys this connection imported, - only the fields in **Translatable fields** and the types in **Content types**, - only the locales ticked under **Publish these locales**. Your source locale is never published. There are two ways to publish: - **Publish to CMS** under **Sync** queues a publish straight away. - **Publish automatically whenever a translation is reviewed** queues a publish each time a translation in the project is reviewed. A publish is idempotent: running it again with nothing new reviewed writes the same values. One publish run writes up to 5,000 entries. If a single entry fails (for example because it was deleted on the platform), the rest of the run continues. ## Keep content in sync [#keep-content-in-sync] transglot does not poll your platform. New and changed content arrives when: - you select **Sync from CMS**, or - the platform notifies transglot of a change. Today that covers the [WordPress plugin](https://transglot.ai/docs/integrations/content-platforms/wordpress), which sends a change notification when you save, and [Contentful](https://transglot.ai/docs/integrations/content-platforms/contentful), whose CMS page shows an **Inbound URL** for a signed webhook. On the first save of a connection, the page shows **Your inbound signing secret: shown once**. A change notification must be signed with this secret, using the same scheme as transglot's outbound webhooks ([Verify signatures](https://transglot.ai/docs/developers/webhooks/verify-signatures)). An unsigned or stale request is refused with `Invalid signature.` If you lose the secret, select **Rotate signing secret**; the old secret stops working at once. ## Disconnect [#disconnect] At the bottom of the CMS page, select the disconnect button (it names your platform, for example **Disconnect Contentful**), then **Yes, disconnect**. This removes the stored credential and stops all syncing. Keys already imported stay in the project. ## If something goes wrong [#if-something-goes-wrong] <Accordions type="single"> <Accordion title="Connect is greyed out and the page says the connector is on every paid plan"> Your workspace is on the Free plan. Content platform connections start at Starter. An attempt to connect on Free is refused with `This integration is not enabled for your workspace.` </Accordion> <Accordion title="Test connection says the credentials were rejected"> The message ends in `the credentials were rejected.` The platform answered 401 or 403. Check the token is current and has the access the connector page lists, then paste it again and save. </Accordion> <Accordion title="Test connection says it could not reach the CMS"> `Could not reach the CMS. Check the site URL.` means the request never got an answer. For a self-hosted platform, the URL must use https and resolve to a public address. A URL that does not is refused with a message such as `The webhook URL must use https.` or `The webhook URL resolves to a private or reserved address.` </Accordion> <Accordion title="The sync finished but nothing new appeared"> Check that the content types and field IDs you entered match the platform exactly, that the fields hold text in your project's source language, and that your credential can read those types. Fields with no source-language value are skipped. </Accordion> <Accordion title="Sync and publish buttons are missing"> If the page says `Syncing and publishing cover every language on this connection, and your access is limited to a subset of them. An administrator can run these.`, your role is limited to some languages. Ask an admin whose access covers every language on the connection. </Accordion> <Accordion title="There is no Publish to CMS button"> The platform is import only. Take the translations out through the API, the CLI or the CDN instead. </Accordion> </Accordions> ## Related [#related] - [The integrations hub](https://transglot.ai/docs/integrations) - [Plan requirements](https://transglot.ai/docs/integrations/plan-requirements) - [Translation states](https://transglot.ai/docs/concepts/translation-states) - [Keys and strings](https://transglot.ai/docs/concepts/keys-and-strings) # Connect Intercom (https://transglot.ai/docs/integrations/content-platforms/intercom/) | | | | --- | --- | | Direction | Two-way | | Imported | Article title, description and body; collection name and description | | Written back | Each article's or collection's translated content for the target locales. The default-locale text is never written | | Sync | **Sync from CMS** | | Per sync | Up to 2,500 entries per content type | | Plan | Starter and above | ## Before you start [#before-you-start] - Enable your target languages in your Intercom Help Center settings. Intercom rejects a translation for a locale that is not enabled there, and transglot reports that error rather than hiding it. - Create an app in Intercom's developer settings and copy its workspace access token. ## Connect [#connect] Open your project's settings, choose **CMS**, and pick **Intercom** in the **CMS** list. There is no URL to type. | Field | What to enter | | --- | --- | | **Data-hosting region** | Where your workspace is hosted: US, EU or AU | | **Access token** | The workspace access token | | **Content types** | `articles`, `collections`, or both | | **Translatable fields** | `title`, `description` and `body` for articles; `name` and `description` for collections | | **Publish these locales** | The target languages to write back | Select **Connect**, then **Test connection**. A successful test names the workspace. ## What gets imported [#what-gets-imported] transglot reads each article's title, description and body, and each collection's name and description, as they appear in the default locale. ## How translations are written back [#how-translations-are-written-back] Intercom stores translations in each article's translated content, one entry per locale. transglot reads the article, adds or replaces the target locales, and saves only the translated content. Locales and fields transglot does not manage are kept, and the default-locale text is never in the request. Each new locale on an article takes the article's own author and state, so a translation of a published article is published too. Language codes are converted to Intercom's form, so `pt_BR` in transglot is published as `pt-BR`. ## Keeping in sync [#keeping-in-sync] Run **Sync from CMS** to pick up new and changed articles. Intercom's own webhooks cannot be verified by transglot's inbound signature check yet, so there is no automatic sync on change. ## Troubleshooting [#troubleshooting] - `... the credentials were rejected.` The access token is wrong or revoked, or the region does not match your workspace. - `... the article or help center was not found.` Check **Data-hosting region**, and that the article still exists. - `... the workspace is rate-limited, retry.` Wait and run the sync or publish again. - A locale fails to publish: check the language is enabled in your Help Center settings. ## Related [#related] - [Connect a content platform](https://transglot.ai/docs/integrations/content-platforms) - [Plan requirements](https://transglot.ai/docs/integrations/plan-requirements) # Connect Klaviyo (https://transglot.ai/docs/integrations/content-platforms/klaviyo/) <Callout type="info" title="Import only"> Nothing is written back to Klaviyo. A Klaviyo template is single-language and has no per-language slot to write into. Take the translations out through the [REST API](https://transglot.ai/docs/developers/api), the [transglot CLI](https://transglot.ai/docs/developers/cli) or the [CDN](https://transglot.ai/docs/guides/delivery/the-cdn). </Callout> | | | | --- | --- | | Direction | Import only | | Imported | Email template name, HTML and text | | Written back | Nothing | | Sync | **Sync from CMS** | | Per sync | Up to 500 templates | | Plan | Starter and above | ## Before you start [#before-you-start] Create a private API key in your Klaviyo account's API key settings, with the `templates:read` scope. ## Connect [#connect] Open your project's settings, choose **CMS**, and pick **Klaviyo** in the **CMS** list. There is no URL to type: the key identifies the account. | Field | What to enter | | --- | --- | | **Private API key** | Your private API key (it starts with `pk_`) | | **Content types** | `templates` | | **Translatable fields** | Any of `name`, `html`, `text` | Select **Connect**, then **Test connection**. ## What gets imported [#what-gets-imported] transglot reads the email templates in your account and imports the mapped fields: the template's name, and its HTML and text bodies. One sync reads up to 500 templates. ## Troubleshooting [#troubleshooting] - `... the credentials were rejected.` The key is wrong, revoked, or lacks the `templates:read` scope. A public API key (site ID) does not work. - `... the template was not found.` The template was removed while the sync ran. Run **Sync from CMS** again. - `... Klaviyo is rate-limiting the request, retry.` Wait and run the sync again. ## Related [#related] - [Connect a content platform](https://transglot.ai/docs/integrations/content-platforms) - [Plan requirements](https://transglot.ai/docs/integrations/plan-requirements) # Connect Kontent.ai (https://transglot.ai/docs/integrations/content-platforms/kontent-ai/) | | | | --- | --- | | Direction | Two-way | | Imported | The source-language variant of the elements you map, on the content types you choose | | Written back | Each item's target-language variant, created if it does not exist yet. The source variant is never written | | Sync | **Sync from CMS** | | Plan | Starter and above | ## Before you start [#before-you-start] - Add your target languages to the environment. Their codenames must match the language codes in your transglot project. - Create a Management API key in the environment's API key settings. - Note the environment ID. ## Connect [#connect] Open your project's settings, choose **CMS**, and pick **Kontent.ai** in the **CMS** list. There is no URL to type. | Field | What to enter | | --- | --- | | **Environment ID** | Your environment ID | | **Management API key** | The key you created | | **Content types** | Content type codenames, comma separated | | **Translatable fields** | Element codenames, comma separated | | **Publish these locales** | The target languages to write back | Select **Connect**, then **Test connection**. A successful test names the environment. ## What gets imported [#what-gets-imported] For each content type, transglot reads every item's source-language variant and imports the mapped elements whose value is text. ## How translations are written back [#how-translations-are-written-back] Each language is a separate variant of the same item. For each target language, transglot reads that variant, replaces only the mapped elements' values, and saves it. Every other element keeps its value. A target variant that does not exist yet is created with only the mapped elements. If the target variant is in the **Published** or **Scheduled** workflow step, it cannot be changed in place, so transglot first creates a new version of it and writes the translations into that new version. The source-language variant is never addressed. ## Troubleshooting [#troubleshooting] - `... the credentials were rejected.` The Management API key is wrong or revoked. - `... the environment, type, item or variant was not found, check the settings.` Check **Environment ID** and the content type codenames. - `... Kontent.ai is rate-limiting the request, retry.` Wait and run the sync or publish again. ## Related [#related] - [Connect a content platform](https://transglot.ai/docs/integrations/content-platforms) - [Plan requirements](https://transglot.ai/docs/integrations/plan-requirements) # Connect Magento (https://transglot.ai/docs/integrations/content-platforms/magento/) | | | | --- | --- | | Direction | Two-way | | Imported | Product name, description and short description, read from your source store view | | Written back | The same fields in each target store view you map. The source store view is never written | | Sync | **Sync from CMS** | | Per sync | Up to 2,500 products | | Plan | Starter and above | ## Before you start [#before-you-start] - Create a store view for each target language in your store configuration. - Create an integration in Magento's integration settings and activate it to get an access token with catalog read and write access. An admin token also works. - Your store must be reachable over https at a public address. ## Connect [#connect] Open your project's settings, choose **CMS**, and pick **Magento** in the **CMS** list. | Field | What to enter | | --- | --- | | **Magento store URL** | Your Magento host, for example `https://shop.example.com`. transglot adds `/rest/{storeView}/V1/…` itself | | **Source store-view code** | The store view that holds your source-language content. Leave it as `default` for most stores | | **Access token** | The integration access token | | **Content types** | `products` | | **Translatable fields** | Any of `name`, `description`, `short_description` | | **Locale → store view** | For each target language in your project, its Magento store-view code, for example `fr_fr` | | **Publish these locales** | The target languages to write back | Select **Connect**, then **Test connection**. The test runs a minimal product search, which checks both the address and the token. ## What gets imported [#what-gets-imported] transglot reads every product from the source store view, by SKU, and imports the mapped fields. `name` is the product's own field; `description` and `short_description` are read from its custom attributes. A product whose SKU contains a colon (`:`) is skipped. ## How translations are written back [#how-translations-are-written-back] For each target language, transglot writes the reviewed values to the product in that language's mapped store view. A store-view value overrides only that scope, so your source store view keeps its content. - A language with no store view under **Locale → store view** is skipped. - A language mapped to the source store view is skipped. - The reserved scopes `all` and `admin` cannot be used as targets. ## Troubleshooting [#troubleshooting] - `That URL did not return a Magento store (the products search was missing).` The address is not the Magento REST API root. - `... the credentials were rejected.` The token is wrong, the integration is not active, or it lacks catalog access. - `... the store, store view or product was not found, check the URL and store-view codes.` Check **Source store-view code** and the codes under **Locale → store view**. - A language never receives translations: check it has a store-view code mapped and is ticked under **Publish these locales**. ## Related [#related] - [Connect a content platform](https://transglot.ai/docs/integrations/content-platforms) - [Plan requirements](https://transglot.ai/docs/integrations/plan-requirements) # Connect Mailchimp (https://transglot.ai/docs/integrations/content-platforms/mailchimp/) <Callout type="info" title="Import only"> Nothing is written back to Mailchimp. A Mailchimp campaign is single-language and has no per-language slot to write into. Take the translations out through the [REST API](https://transglot.ai/docs/developers/api), the [transglot CLI](https://transglot.ai/docs/developers/cli) or the [CDN](https://transglot.ai/docs/guides/delivery/the-cdn). </Callout> | | | | --- | --- | | Direction | Import only | | Imported | Campaign subject line and preview text, and optionally the HTML and plain-text content | | Written back | Nothing | | Sync | **Sync from CMS** | | Per sync | Up to 2,500 campaigns | | Plan | Starter and above | ## Before you start [#before-you-start] Create an API key in your Mailchimp account's API key settings. It ends in your data center, for example `-us21`. ## Connect [#connect] Open your project's settings, choose **CMS**, and pick **Mailchimp** in the **CMS** list. There is no URL to type: transglot reads your data center from the end of the API key. | Field | What to enter | | --- | --- | | **API key** | Your Mailchimp API key, for example `0123…abcde-us21` | | **Content types** | `campaigns` | | **Translatable fields** | Any of `subject_line`, `preview_text`, `html`, `plain_text` | Select **Connect**, then **Test connection**. A successful test names the data center, for example **Mailchimp (us21)**. ## What gets imported [#what-gets-imported] transglot reads every campaign in the account. The subject line and preview text come from the campaign's settings. When you map `html` or `plain_text`, transglot also reads each campaign's content. If one campaign's content cannot be read (for example a campaign with no content yet), the sync still imports its subject line and preview text and moves on. ## Troubleshooting [#troubleshooting] - The form refuses the key: it must end in a data center suffix, such as `-us21`. Copy the whole key from Mailchimp. - `... the credentials were rejected.` The key is wrong or was revoked. - `... the campaign was not found, check the data center.` The key's suffix does not match your account's data center. - `... Mailchimp is rate-limiting the request, retry.` Wait and run the sync again. ## Related [#related] - [Connect a content platform](https://transglot.ai/docs/integrations/content-platforms) - [Plan requirements](https://transglot.ai/docs/integrations/plan-requirements) # Connect Notion (https://transglot.ai/docs/integrations/content-platforms/notion/) <Callout type="info" title="Import only"> Nothing is written back to Notion. A Notion page is single-language and has no per-language slot to write into. Take the translations out through the [REST API](https://transglot.ai/docs/developers/api), the [transglot CLI](https://transglot.ai/docs/developers/cli) or the [CDN](https://transglot.ai/docs/guides/delivery/the-cdn). </Callout> | | | | --- | --- | | Direction | Import only | | Imported | The title property and the text properties you map, on the pages of the databases you choose | | Written back | Nothing | | Sync | **Sync from CMS** | | Plan | Starter and above | ## Before you start [#before-you-start] - Create an internal integration in Notion's integration settings and copy its token. - Share each database you want to import with the integration. A database is not visible to the API until you do. ## Connect [#connect] Open your project's settings, choose **CMS**, and pick **Notion** in the **CMS** list. There is no URL to type. | Field | What to enter | | --- | --- | | **Integration token** | The internal integration token, for example `ntn_…` | | **Content types** | Database IDs, comma separated | | **Translatable fields** | Property names, comma separated, for example `Name, Summary` | Select **Connect**, then **Test connection**. A successful test names the integration's bot user. ## What gets imported [#what-gets-imported] For each page in each database, transglot imports the plain text of the properties you map. Only `title` and `rich_text` properties are read, and their formatting is not kept. The page body (its blocks) is not imported. ## Troubleshooting [#troubleshooting] - `Failed verifying the Notion token: the credentials were rejected.` The token is wrong or the integration was removed. - `... the database or page was not found, check it is shared with the integration.` Share the database with the integration in Notion, and check the database ID. - A property never appears as a key: check the name matches exactly and the property is a title or text property. ## Related [#related] - [Connect a content platform](https://transglot.ai/docs/integrations/content-platforms) - [Plan requirements](https://transglot.ai/docs/integrations/plan-requirements) # Connect PrestaShop (https://transglot.ai/docs/integrations/content-platforms/prestashop/) | | | | --- | --- | | Direction | Two-way | | Imported | Name, description and short description of products; name and description of categories, in your source language | | Written back | The same fields in each target language you map. The source language is never written | | Sync | **Sync from CMS** | | Per sync | Up to 2,500 entries per content type | | Plan | Starter and above | ## Before you start [#before-you-start] - Enable the webservice in PrestaShop's advanced parameters, and create a webservice key with read and write access to products and categories. - Add your target languages to the store and note each one's language ID. - Your store must be reachable over https at a public address. ## Connect [#connect] Open your project's settings, choose **CMS**, and pick **PrestaShop** in the **CMS** list. | Field | What to enter | | --- | --- | | **PrestaShop store URL** | Your store root, for example `https://shop.example.com`. transglot adds `/api/…` itself | | **Source language ID** | The PrestaShop language ID that holds your source content, usually `1` | | **Locale → language ID** | For each target language in your project, its PrestaShop language ID, for example `2` | | **Webservice key** | The webservice key | | **Content types** | `products`, `categories`, or both | | **Translatable fields** | Any of `name`, `description`, `description_short` | | **Publish these locales** | The target languages to write back | Select **Connect**, then **Test connection**. A successful test names the store's host. ## What gets imported [#what-gets-imported] transglot reads every product and category and imports the value of each mapped field in the source language ID. Categories have no short description, so that field is skipped for them. ## How translations are written back [#how-translations-are-written-back] A PrestaShop multilingual field holds one value per language ID. For each product or category, transglot reads the resource, replaces only the values for the target language IDs you mapped, and saves it. The source-language value and every other field are kept exactly as they were. - A language with no ID under **Locale → language ID** is skipped. - A language mapped to the source language ID is skipped. ## Troubleshooting [#troubleshooting] - `Failed verifying the PrestaShop webservice key: the credentials were rejected.` The key is wrong, disabled, or the webservice is off. - `... the store, resource or id was not found, check the URL and webservice key.` Check **PrestaShop store URL** and that the key has access to products and categories. - `... the store is rate-limited, retry.` Wait and run the sync or publish again. ## Related [#related] - [Connect a content platform](https://transglot.ai/docs/integrations/content-platforms) - [Plan requirements](https://transglot.ai/docs/integrations/plan-requirements) # Connect Prismic (https://transglot.ai/docs/integrations/content-platforms/prismic/) <Callout type="info" title="Import only"> Nothing is written back to Prismic. Its Content API is read only, and its Migration API cannot set the alternate language of an existing document. Take the translations out through the [REST API](https://transglot.ai/docs/developers/api), the [transglot CLI](https://transglot.ai/docs/developers/cli) or the [CDN](https://transglot.ai/docs/guides/delivery/the-cdn). </Callout> | | | | --- | --- | | Direction | Import only | | Imported | The top-level text fields you map, on published documents in your source locale | | Written back | Nothing | | Sync | **Sync from CMS** | | Plan | Starter and above | ## Before you start [#before-you-start] Generate a permanent access token in your Prismic repository's API and security settings. ## Connect [#connect] Open your project's settings, choose **CMS**, and pick **Prismic** in the **CMS** list. | Field | What to enter | | --- | --- | | **Prismic repository** | Only the repository name, for example `acme-repo`. transglot builds the `…cdn.prismic.io` address itself | | **Source locale** | The Prismic locale to import from, usually the master language, for example `en-us`. Leave blank to use your project's source language | | **Access token** | The permanent access token | | **Content types** | Custom type API IDs, comma separated | | **Translatable fields** | Top-level field API IDs, comma separated | Select **Connect**, then **Test connection**. A successful test names the repository. ## What gets imported [#what-gets-imported] transglot reads the published version of each document of each custom type, in the source locale, and imports the mapped top-level fields: - A Key Text field is imported as it is. - A Rich Text or Title field is imported as plain text, one line per block. Formatting is not kept. When a document has a `title` field, its text becomes the description on the document's keys. ## Troubleshooting [#troubleshooting] - `... the credentials were rejected.` The access token is wrong or revoked. - `... the repository or endpoint was not found, check the repository name.` Enter only the repository name, not a URL. - `The Prismic repository did not return a master ref.` The repository answered without a master ref (the pointer to its published content), so transglot has nothing to read. Check the repository name. - Nothing is imported: check **Source locale** matches the locale your documents are written in. ## Related [#related] - [Connect a content platform](https://transglot.ai/docs/integrations/content-platforms) - [The transglot CLI](https://transglot.ai/docs/developers/cli) - [Plan requirements](https://transglot.ai/docs/integrations/plan-requirements) # Connect Sanity (https://transglot.ai/docs/integrations/content-platforms/sanity/) | | | | --- | --- | | Direction | Two-way | | Imported | The source-language key of each mapped field, on published documents of the types you choose | | Written back | The target-language keys of the same fields. The source-language key is never written | | Sync | **Sync from CMS** | | Plan | Starter and above | ## Before you start [#before-you-start] - Your translatable fields must use field-level localization: an object keyed by language, such as `{ en: "…", fr: "…" }` (the `localeString` and `localeText` pattern). A plain string field has no per-language keys to read or write. - The language keys in those objects must match the language codes in your transglot project. - Create an API token with write access in your Sanity project's API settings. ## Connect [#connect] Open your project's settings, choose **CMS**, and pick **Sanity** in the **CMS** list. | Field | What to enter | | --- | --- | | **Project ID** | Your Sanity project ID | | **Dataset** | The dataset name, for example `production` | | **API version** | A dated version such as `v2021-06-07`. Leave blank for the default | | **API token** | The token you created | | **Content types** | Document type names, comma separated | | **Translatable fields** | Field names, comma separated | | **Publish these locales** | The target languages to write back | Select **Connect**, then **Test connection**. A successful test names the project and dataset. ## What gets imported [#what-gets-imported] transglot reads published documents only; drafts are not imported. For each mapped field it reads the value under your project's source language key. Localized Portable Text fields (an object keyed by language whose values are block arrays) are supported. Each one is imported as a single string per field, with markers that keep the block structure. ## How translations are written back [#how-translations-are-written-back] transglot patches only the target-language keys of each mapped field (for example `title.fr`) on the published document. The source-language key and every other language are left as they are. For a Portable Text field, transglot rebuilds the target-language blocks from a copy of the source blocks, with the reviewed text placed back in. If a block's translation cannot be placed back safely, that block keeps the source text, so the field always stays valid Portable Text. ## Troubleshooting [#troubleshooting] - `... the credentials were rejected.` The token is wrong, revoked, or has no write access. - `... the project or dataset was not found, check the settings.` Check **Project ID** and **Dataset**. - A field never appears as a key: check it uses the field-level localization pattern and has a value under your source language key on a published document. ## Related [#related] - [Connect a content platform](https://transglot.ai/docs/integrations/content-platforms) - [Plan requirements](https://transglot.ai/docs/integrations/plan-requirements) # Connect Shopify (https://transglot.ai/docs/integrations/content-platforms/shopify/) | | | | --- | --- | | Direction | Two-way | | Imported | The primary-locale value of the fields you choose, on products, collections and pages | | Written back | Shopify translations registered for each secondary locale. The primary-locale content is never changed | | Sync | **Sync from CMS** | | Per sync | Up to 2,500 resources per content type | | Plan | Starter and above | ## Before you start [#before-you-start] - Add your target languages to the store in Shopify's language settings. Their locale codes must match the language codes in your transglot project. - Create a custom app in your Shopify admin and install it to get an **Admin API access token**. Give it these scopes: `read_translations` and `write_translations`, plus `read_products` (for products and collections) and `read_content` (for pages). ## Connect [#connect] Open your project's settings, choose **CMS**, and pick **Shopify** in the **CMS** list. | Field | What to enter | | --- | --- | | **Store domain** | Only the store handle, for example `acme` for `acme.myshopify.com` | | **Admin API access token** | The custom app's token (it starts with `shpat_`) | | **Content types** | Any of `products`, `collections`, `pages` | | **Translatable fields** | Any of `title`, `body_html`, `handle`, `product_type` | | **Publish these locales** | The target languages to write back | Select **Connect**, then **Test connection**. A successful test names the store. ## What gets imported [#what-gets-imported] transglot reads Shopify's translatable content for each resource in the store's primary locale. A field that is not translatable on a given resource is skipped; for example, `product_type` exists only on products. Translating `handle` gives the resource a different URL handle in that language. Leave it out of **Translatable fields** if you want the same handle everywhere. ## How translations are written back [#how-translations-are-written-back] Shopify keeps each secondary-locale translation as a separate record from the primary content, pinned to a fingerprint (digest) of the source value. At publish time transglot reads each resource's current digests and registers the reviewed values for the target locales against them. Your primary locale is never registered, so it cannot be overwritten. ## Troubleshooting [#troubleshooting] - `... the credentials were rejected.` The token is wrong, or the app is missing one of the scopes above. - `... the shop or resource was not found, check the shop domain.` Enter only the store handle in **Store domain**. - `... the shop is unavailable (frozen or locked).` The store is paused or locked in Shopify. - `... the Shopify Admin API rejected the translation.` Shopify refused a value, for example because the target language has not been added to the store. - `... the shop is rate-limited, retry.` Wait and run the sync or publish again. ## Related [#related] - [Connect a content platform](https://transglot.ai/docs/integrations/content-platforms) - [Plan requirements](https://transglot.ai/docs/integrations/plan-requirements) # Connect Storyblok (https://transglot.ai/docs/integrations/content-platforms/storyblok/) | | | | --- | --- | | Direction | Two-way | | Imported | The base (source) value of the fields you map, on stories whose content type you choose | | Written back | Each field's `__i18n__` key for the target language. The base field is never written | | Sync | **Sync from CMS** | | Per sync | Up to 1,250 stories per content type | | Plan | Starter and above | ## Before you start [#before-you-start] - In the component schema, mark each field you want translated as translatable (field-level translation). - Enable your target languages in the space settings. Their language codes must match the language codes in your transglot project. - Create a personal access token in your Storyblok account settings. ## Connect [#connect] Open your project's settings, choose **CMS**, and pick **Storyblok** in the **CMS** list. | Field | What to enter | | --- | --- | | **Space ID** | Your numeric space ID, from the space's general settings | | **Region** | The data region your space is hosted in: EU, US, AP or CA | | **Personal access token** | The token you created | | **Content types** | The component names of the stories to import, comma separated | | **Translatable fields** | Field names, comma separated | | **Publish these locales** | The target languages to write back | Select **Connect**, then **Test connection**. A successful test names the space. ## What gets imported [#what-gets-imported] For every story whose content type is one you listed, transglot reads each mapped field from the story's top-level content. The base field (for example `content.title`) is the source. Fields with no text are skipped. ## How translations are written back [#how-translations-are-written-back] Storyblok stores a field-level translation as a separate key next to the base field: `content.title__i18n__fr` for French. transglot reads the story, sets those target-language keys for the mapped fields, and saves the story. The base field and any content transglot does not manage are kept as they were. transglot saves the story but does not publish it in Storyblok. Publish the story when you want the translations live. ## Troubleshooting [#troubleshooting] - `... the credentials were rejected.` The personal access token is wrong or revoked. - `... the space or story was not found, check the settings.` Check **Space ID** and **Region**. - A story is not imported: check its content type is in **Content types** and the mapped fields are text fields at the top level of the story. ## Related [#related] - [Connect a content platform](https://transglot.ai/docs/integrations/content-platforms) - [Plan requirements](https://transglot.ai/docs/integrations/plan-requirements) # Connect Strapi (https://transglot.ai/docs/integrations/content-platforms/strapi/) | | | | --- | --- | | Direction | Two-way | | Imported | The source-locale localization of the fields you map, on the content types you choose | | Written back | Each document's target-locale localization. The source locale is never written | | Sync | **Sync from CMS** | | Per sync | Up to 1,250 documents per content type | | Plan | Starter and above | ## Before you start [#before-you-start] - The connector targets **Strapi 5** (documents addressed by `documentId`). - Enable the Internationalization (i18n) plugin, add your target locales, and mark the fields you want translated as localized. Strapi's locale codes must match the language codes in your transglot project. - Your Strapi must be reachable over https at a public address. - Create an API token with read and write access (or a custom token scoped to your content types) in Strapi's API token settings. ## Connect [#connect] Open your project's settings, choose **CMS**, and pick **Strapi** in the **CMS** list. | Field | What to enter | | --- | --- | | **Strapi base URL** | Your Strapi instance root, for example `https://cms.example.com`. transglot adds `/api/…` itself | | **API token** | The token you created | | **Content types** | Plural API IDs of your collection types, comma separated, for example `articles, pages` | | **Translatable fields** | Attribute names, comma separated | | **Publish these locales** | The target locales to write back | Select **Connect**, then **Test connection**. The test reads one entry of the first content type in your source locale. ## What gets imported [#what-gets-imported] transglot lists each content type in your project's source locale and imports the mapped attributes that hold text. When a document has a `title` attribute, its value becomes the description on the document's keys. ## How translations are written back [#how-translations-are-written-back] Each locale is a separate localization of the same document. transglot writes each target locale with a request addressed to that locale, which creates the localization if it does not exist yet and updates it if it does. Other locales, including the source, are left as they were. ## Troubleshooting [#troubleshooting] - `... the credentials were rejected.` The token is wrong, expired, or lacks write access to the content type. - `... the endpoint or document was not found, check the base URL and content type.` Check **Strapi base URL** and that each content type is the plural API ID. - `The webhook URL must use https.` or `The webhook URL resolves to a private or reserved address.` transglot only connects to a public https address. ## Related [#related] - [Connect a content platform](https://transglot.ai/docs/integrations/content-platforms) - [Plan requirements](https://transglot.ai/docs/integrations/plan-requirements) # Connect Webflow (https://transglot.ai/docs/integrations/content-platforms/webflow/) | | | | --- | --- | | Direction | Two-way | | Imported | The primary-locale value of the fields you map, on the collections you choose | | Written back | Each item's secondary-locale variant. The primary locale is never written | | Sync | **Sync from CMS** | | Plan | Starter and above | ## Before you start [#before-you-start] - Add your target locales to the site in Webflow's localization settings. - For each existing item, add its secondary-locale variants in the CMS. The Webflow API can update a locale variant of an existing item but cannot create one. - Create a site API token with CMS read and write access in your site's settings. ## Connect [#connect] Open your project's settings, choose **CMS**, and pick **Webflow** in the **CMS** list. | Field | What to enter | | --- | --- | | **Site ID** | Your site ID, from the site's general settings or the URL of the site dashboard | | **API token** | The site API token you created | | **Content types** | Collection IDs, comma separated | | **Translatable fields** | Field slugs, comma separated | | **Publish these locales** | The target locales to write back | Select **Connect**, then **Test connection**. A successful test names the site. ## What gets imported [#what-gets-imported] For every item in each collection, transglot reads the primary-locale variant and imports the mapped fields that hold text. When an item has a `name` field, its value becomes the description on the item's keys. ## How translations are written back [#how-translations-are-written-back] In Webflow each locale is a separate variant of the same item. transglot matches each target language to one of the site's secondary locales (by the full locale tag first, then by the language alone) and updates only that variant's mapped fields. - A language with no matching secondary locale on the site is skipped. - The primary (source) variant is a separate record, and Webflow refuses writes to it through this path. - transglot updates the items but does not publish the site. Publish in Webflow when you want the translations live. ## Troubleshooting [#troubleshooting] - `... the credentials were rejected.` The token is wrong, revoked, or lacks CMS write access. - `... the site, collection or item was not found, check the settings.` Check **Site ID** and the collection IDs. - `... the item changed underneath us (conflict), retry.` Run **Publish to CMS** again. - Translations for one language never arrive: check that language is a secondary locale on the site and that the item has a variant for it. ## Related [#related] - [Connect a content platform](https://transglot.ai/docs/integrations/content-platforms) - [Plan requirements](https://transglot.ai/docs/integrations/plan-requirements) # Connect WooCommerce (https://transglot.ai/docs/integrations/content-platforms/woocommerce/) | | | | --- | --- | | Direction | Two-way for products. Categories are imported but not written back | | Imported | Name, description and short description of products; name and description of categories | | Written back | One translation bundle per product, in the `_transglot_translations` product meta. The product's own fields are never written | | Sync | **Sync from CMS** | | Plan | Starter and above | ## Before you start [#before-you-start] - In WooCommerce's advanced settings, create a REST API key with **Read/Write** permission and copy the consumer key and consumer secret. - Your store must be reachable over https at a public address. ## Connect [#connect] Open your project's settings, choose **CMS**, and pick **WooCommerce** in the **CMS** list. | Field | What to enter | | --- | --- | | **WooCommerce store URL** | Your store root, for example `https://store.example.com`. transglot adds `/wp-json/wc/v3/…` itself | | **Consumer key & secret** | The key and secret joined by a colon: `ck_…:cs_…` | | **Content types** | `products`, `categories`, or both | | **Translatable fields** | Any of `name`, `description`, `short_description` | | **Publish these locales** | The target languages to write back | Select **Connect**, then **Test connection**. The test checks that the address is a WooCommerce store and names it. ## What gets imported [#what-gets-imported] transglot reads the store's own (native) fields for each product and category. Categories have no short description, so that field is skipped for them. ## How translations are written back [#how-translations-are-written-back] WooCommerce has no built-in place for per-language product text, so transglot writes every reviewed target language into one meta key it owns on each product, `_transglot_translations`, shaped as language, then field, then value. It writes only that key, so the name, description and short description you wrote are never changed. Your theme or headless front end reads the bundle from product meta (for example with `get_post_meta()`). The leading underscore keeps the key out of the product's Custom Fields panel. Product categories have no meta channel in the WooCommerce REST API, so category translations are skipped at publish time with `WooCommerce categories require a term-meta capable endpoint; the translation was skipped.` This is expected and does not stop the rest of the publish. ## Troubleshooting [#troubleshooting] - `That URL did not return a WooCommerce store (the wc/v3 system status was missing).` The address is not a WooCommerce store, or its REST API is blocked. - `... the credentials were rejected.` The key or secret is wrong, or the key is read-only. - `... the endpoint was not found, check the store URL.` Check **WooCommerce store URL** is the store root. - `... the store is rate-limited, retry.` Wait and run the sync or publish again. ## Related [#related] - [Connect WordPress](https://transglot.ai/docs/integrations/content-platforms/wordpress) - [Connect a content platform](https://transglot.ai/docs/integrations/content-platforms) - [Plan requirements](https://transglot.ai/docs/integrations/plan-requirements) # Connect WordPress (https://transglot.ai/docs/integrations/content-platforms/wordpress/) | | | | --- | --- | | Direction | Two-way | | Imported | Title, excerpt and content of posts and pages; name and description of categories and tags | | Written back | One translation bundle per post, page or term, stored in post or term meta. The original fields are never written | | Sync | The transglot plugin notifies transglot when content changes, or **Sync from CMS** | | Plan | Starter and above | ## Two ways to connect [#two-ways-to-connect] - **With the transglot plugin for WordPress (recommended).** You paste one pairing code into the plugin. The plugin creates its own credential, registers the meta key translations are written to, renders the translations on per-language URLs, and tells transglot when content changes. - **With an application password only.** You enter the site URL and an application password on the CMS page. This imports and publishes over the WordPress REST API, but nothing on the site registers the meta key or renders the translations unless you build that yourself. ## Connect with the plugin [#connect-with-the-plugin] The plugin requires WordPress 6.6 or later and PHP 7.4 or later. It cannot run alongside another multilingual plugin (WPML, Polylang or TranslatePress), and on multisite it is activated per site. <Steps> <Step> ### Install the plugin [#install-the-plugin] In wp-admin, install and activate the transglot plugin (it appears in wp-admin as **Transglot**). </Step> <Step> ### Generate a pairing code [#generate-a-pairing-code] In transglot, open your project's settings, choose **CMS**, and pick **WordPress** in the **CMS** list. In **The Transglot plugin for WordPress** panel, select **Generate pairing code**. The code is shown once. It pairs one site and stops working after 15 minutes. **Generate another code** replaces it; **Revoke outstanding code** cancels a code nobody has used yet. </Step> <Step> ### Paste the code in WordPress [#paste-the-code-in-wordpress] In wp-admin, open **Transglot**, then **Setup**. Paste the code into **Your code** and select **Connect this site**. WordPress asks you to approve the application password the plugin creates for the connection. </Step> <Step> ### Finish the setup wizard [#finish-the-setup-wizard] The wizard continues with your languages, the URL format for each language and the language switcher. Its last screen checks the round trip: it writes a test bundle through the same REST route transglot uses and reads it back. </Step> </Steps> A new pairing publishes to every target language of the project that your access covers; you can change the list under **Publish these locales** afterwards. Pairing writes your project's source language, so the person generating the code needs access to the source language. Otherwise the panel says the code must be generated by an admin whose access covers it. ## Connect with an application password [#connect-with-an-application-password] | Field | What to enter | | --- | --- | | **WordPress site URL** | The site root, for example `https://blog.example.com`. transglot adds `/wp-json/…` itself. It must use https and be publicly reachable | | **Application password** | `username:application-password`, created under your WordPress user profile's application passwords | | **Content types** | Any of `posts`, `pages`, `categories`, `tags` | | **Translatable fields** | Any of `title`, `excerpt`, `content` (posts and pages) and `name`, `description` (categories and tags) | | **Publish these locales** | The target languages to write back | Select **Connect**, then **Test connection**. A successful test names the site. <Callout type="warn" title="Register the meta key yourself"> Without the plugin, your site must register the `transglot_translations` meta key for the REST API on every post type and taxonomy you publish to. WordPress drops a meta write for a key nothing has registered and still answers success, so the publish reports entries that the site never stored. </Callout> ## What gets imported [#what-gets-imported] transglot reads the raw (unrendered) value of each mapped field over the WordPress REST API. Empty fields are skipped. The REST API only exposes those fields. With the plugin, theme and plugin interface strings, menu labels, widget text, media alt text and SEO titles and descriptions reach your project through the plugin's own push instead. ## How translations are written back [#how-translations-are-written-back] Each publish writes one bundle per entry into the `transglot_translations` meta key: every reviewed target language, and in it every mapped field. Only that meta key is written, so the post's or term's own title and content are never changed. With the plugin, the bundle is rendered on per-language URLs with hreflang and per-language sitemaps. Without it, your theme or headless front end reads the bundle from post meta. After a publish, the panel shows a receipt such as **Last publish: 12 entries, 30 values**, with the number that could not be written. The plugin compares it against what the site actually stored, and its **Health** screen reports any difference. ## Sync on content change [#sync-on-content-change] With the plugin, saving, unpublishing, trashing or deleting a post, page or term sends a signed, batched notification. transglot then reads only the changed entries back. Nothing else is needed. If the site's notifications start failing after you select **Rotate signing secret**, pair the site again so the plugin receives the new secret. The old secret stops working straight away. ## Troubleshooting [#troubleshooting] - `That URL does not expose the WordPress REST API (wp/v2 namespace missing).` The REST API is disabled or blocked at that address. Check the URL and any security plugin. - `The posts listing reported ... entries but no usable X-WP-TotalPages header.` A cache or security plugin is stripping the `X-WP-*` response headers. Syncing would import only the first page, so transglot stops instead. Let those headers through for `/wp-json/` requests. - `... the credentials were rejected.` The application password is wrong or revoked, or the user cannot edit the content. - The publish receipt says some entries could not be written, and that some of those are entries this CMS cannot accept: those are skipped on purpose and are not a fault. - A pairing code is refused in the plugin: it has expired, been used, or been revoked. Generate a new one. - `This project is already connected to ...` The project already has a connection to another platform. Each project holds one connection, so disconnect the other platform before pairing. ## Related [#related] - [Connect a content platform](https://transglot.ai/docs/integrations/content-platforms) - [Plan requirements](https://transglot.ai/docs/integrations/plan-requirements) # Connect Zendesk Guide (https://transglot.ai/docs/integrations/content-platforms/zendesk-guide/) | | | | --- | --- | | Direction | Two-way | | Imported | Article title and body; section and category name and description | | Written back | A translation per target locale on each article, section or category. The source translation is never written | | Sync | **Sync from CMS** | | Per sync | Up to 1,500 entries per content type | | Plan | Starter and above | ## Before you start [#before-you-start] - Add each target language to your help center in Guide's language settings. Zendesk rejects a translation for a locale that is not enabled there, and transglot reports that error rather than hiding it. - Create an API token in Zendesk's API settings (Zendesk API, token access). You connect with an agent's email address and that token. ## Connect [#connect] Open your project's settings, choose **CMS**, and pick **Zendesk Guide** in the **CMS** list. | Field | What to enter | | --- | --- | | **Zendesk subdomain** | Only the subdomain, for example `acme` for `acme.zendesk.com` | | **Email + API token** | `email/token:api-token`, for example `agent@example.com/token:xxxxxxxx` | | **Content types** | Any of `articles`, `sections`, `categories` | | **Translatable fields** | `title` and `body` for articles; `name` and `description` for sections and categories | | **Publish these locales** | The target languages to write back | Select **Connect**, then **Test connection**. The test reads the help center's locales, which also proves Guide is enabled, and names the `….zendesk.com` address it reached. ## What gets imported [#what-gets-imported] transglot reads the source text of each article (title and body), and of each section and category (name and description). ## How translations are written back [#how-translations-are-written-back] Zendesk keeps each language as its own translation of the article (or section, or category). For each target locale, transglot updates the translation if it exists and creates it if it does not. An update changes only the fields being published, so a translation keeps its body when only the title changed. Section and category names and descriptions are published as the translation's title and body. Language codes are converted to Zendesk's form, so `pt_BR` in transglot is published as `pt-br`. A new translation needs both a title and a body. For articles, keep both `title` and `body` in **Translatable fields**. ## Troubleshooting [#troubleshooting] - `... the translation was rejected, title and body are required.` The first publish of a locale needs both fields reviewed. Review the missing one, then publish again. - `... the credentials were rejected.` Check the credential is in the form `email/token:api-token` and that token access is enabled. - `... the article or help center was not found.` Check **Zendesk subdomain**, and that the article still exists. - `... the account is rate-limited, retry.` Wait and run the sync or publish again. ## Related [#related] - [Connect a content platform](https://transglot.ai/docs/integrations/content-platforms) - [Plan requirements](https://transglot.ai/docs/integrations/plan-requirements) # Connect GitLab, Bitbucket or Azure Repos (https://transglot.ai/docs/integrations/git/connect-another-git-host/) transglot connects to GitLab, Bitbucket and Azure Repos with an access token you create on the repository side, plus a webhook that tells transglot when you push. On a push to the default branch, transglot extracts the changed source strings into your project, translates them, and opens one merge request (GitLab) or pull request (Bitbucket, Azure Repos) from its own `transglot/sync` branch. It never commits to your branches. | Host | Supported | Not supported | | --- | --- | --- | | GitLab | GitLab.com | Self-managed GitLab | | Bitbucket | Bitbucket Cloud | Bitbucket Server and Data Center | | Azure Repos | Azure DevOps Services (`dev.azure.com`) | Azure DevOps Server | For GitHub, use the [GitHub App](https://transglot.ai/docs/integrations/git/connect-github) instead. ## Requirements [#requirements] - **Plan:** Team or above. On a lower plan the host's card on the **Integrations** page offers **Upgrade plan** instead of **Connect**. - **Role:** Admin of the transglot workspace, or an Owner of its account. - **On the host:** permission to create an access token and a webhook (or Service Hook) on the repository. - **Your files:** one source locale file per repository, at a path that contains the locale code, for example `locales/en.json`. ## Connect a repository [#connect-a-repository] Open **Integrations** in your workspace and select **Connect** on the **GitLab**, **Bitbucket** or **Azure Repos** card. The page shows the URL your webhook must call: copy it from there. <Tabs items={['GitLab', 'Bitbucket', 'Azure Repos']}> <Tab value="GitLab"> <Steps> <Step> ### Create a project access token [#create-a-project-access-token] In your GitLab project, create a project access token with the `api` scope. It starts with `glpat-`. </Step> <Step> ### Add the webhook [#add-the-webhook] In the GitLab project, go to **Settings**, **Webhooks**. Add the **Webhook URL** shown on the transglot GitLab page, choose a **Secret token** (any long random value) and enable the **Push events** trigger. </Step> <Step> ### Fill in the connection [#fill-in-the-connection] Under **Connect a project**, enter: | Field | What to enter | | --- | --- | | **Project** | The transglot project this repository feeds | | **GitLab project ID (numeric)** | The numeric ID shown on the project's overview page in GitLab | | **Project path (group/project)** | For example `acme/website` | | **Default branch** | For example `main` | | **Project access token** | The token from step 1 | | **Webhook secret token** | The same value as the webhook's **Secret token** | | **Source path**, **Format**, **Source locale**, **Target locales (comma-separated, blank = all)** | See [Describe your files](#describe-your-files) | Select **Connect project**. </Step> </Steps> </Tab> <Tab value="Bitbucket"> <Steps> <Step> ### Create an access token [#create-an-access-token] Create a repository access token or a workspace access token in Bitbucket that can read the repository and open pull requests on it. </Step> <Step> ### Add the webhook [#add-the-webhook-1] In the repository, go to **Repository settings**, **Webhooks**. Add the **Webhook URL** shown on the transglot Bitbucket page, set a **Secret** (any long random value) and enable the **Repository push** trigger. </Step> <Step> ### Fill in the connection [#fill-in-the-connection-1] Under **Connect a repository**, enter: | Field | What to enter | | --- | --- | | **Project** | The transglot project this repository feeds | | **Workspace** | Your Bitbucket workspace, for example `acme` | | **Repo slug** | For example `website` | | **Repo UUID** | The repository's UUID, for example `{9970a9b6-2d86-...}` | | **Default branch** | For example `main` | | **Access token** | The token from step 1 | | **Webhook secret** | The same value as the webhook's **Secret** | | **Source path**, **Format**, **Source locale**, **Target locales (comma-separated, blank = all)** | See [Describe your files](#describe-your-files) | Select **Connect repository**. </Step> </Steps> </Tab> <Tab value="Azure Repos"> <Steps> <Step> ### Create a personal access token [#create-a-personal-access-token] In Azure DevOps, create a personal access token with the **Code (Read & Write)** scope. </Step> <Step> ### Add the Service Hook [#add-the-service-hook] In your Azure DevOps project, go to **Project settings**, **Service hooks**. Create a **Web Hooks** subscription on the **Code pushed** trigger and set its URL to the **Delivery URL** shown on the transglot Azure Repos page. Under **HTTP headers / Basic auth**, enter any username and a **password** you choose (any long random value). </Step> <Step> ### Fill in the connection [#fill-in-the-connection-2] Under **Connect a repository**, enter: | Field | What to enter | | --- | --- | | **Project** | The transglot project this repository feeds | | **Azure organization** | For example `contoso` | | **Azure project** | For example `Website` | | **Repository ID (GUID)** | The repository's ID | | **Default branch** | For example `main` | | **Personal access token** | The token from step 1 | | **Webhook secret** | The same value as the Service Hook's Basic auth **password** | | **Source path**, **Format**, **Source locale**, **Target locales (comma-separated, blank = all)** | See [Describe your files](#describe-your-files) | Select **Connect repository**. </Step> </Steps> </Tab> </Tabs> When it saves, transglot confirms with the repository name, and the connection appears under **Connected projects** (GitLab) or **Connected repositories**. Tokens and secrets are stored encrypted and never shown again. ## Describe your files [#describe-your-files] | Field | What to enter | | --- | --- | | **Source path** | Where your locale files live, with `{locale}` in place of the locale code, for example `locales/{locale}.json` | | **Format** | The file format, for example `json_nested`. See [file formats](https://transglot.ai/docs/developers/formats) | | **Source locale** | The locale your team writes in, for example `en` | | **Target locales (comma-separated, blank = all)** | The locales to deliver, for example `fr, de, es`. Leave it blank for every target language in the project | ## What happens on a push [#what-happens-on-a-push] 1. You push to the default branch, changing the source locale file. 2. transglot extracts the new and changed strings into the project, where translation memory, the glossary and quality checks apply to them. 3. If automatic translation is on for the project, the new strings are translated. A translation your team has edited or reviewed is never overwritten by AI. 4. transglot writes one file per target locale, at the same path pattern, to the `transglot/sync` branch and opens a merge or pull request. If one is already open, it is updated instead of a second one being opened. Each connection has its own webhook secret, and transglot checks it on every delivery: a delivery without the right secret is rejected, so one connection cannot be used to trigger another. ## Check what happened [#check-what-happened] The host's card on the **Integrations** page shows the last sync attempt, its status (**Completed**, **Nothing to sync**, **Not configured**, **Failed** or **Blocked**) and a sentence saying what happened. "We did not run this sync." means transglot did not start, and the sentence after it says why. "This sync ran and did not finish." means it started and hit a problem. A card whose last run failed carries a **failing** flag. ## Change or disconnect [#change-or-disconnect] To change a connection's files or locales, fill in the form again for the same repository and save. Leave the token and secret fields blank to keep the stored values. To stop syncing a repository, select **Disconnect** on its row and confirm. No new merge or pull requests are opened; anything already open stays open. Delete the webhook or Service Hook on the host as well. ## Troubleshooting [#troubleshooting] <Accordions type="single"> <Accordion title=""The source path must contain the {locale} placeholder.""> Write the path with `{locale}` where the locale code goes, for example `locales/{locale}.json`. </Accordion> <Accordion title=""This ... is already linked to another transglot workspace.""> A repository can feed only one transglot workspace. GitLab says "This GitLab project is already linked to another transglot workspace.", Bitbucket and Azure Repos say the same of the repository. Disconnect it in the other workspace first. </Accordion> <Accordion title="Pushing does nothing"> Check that the push went to the default branch named on the connection and changed the source locale file. Then check the webhook on the host: its URL must be the one on the transglot page, its secret must match the connection's, and the push trigger must be enabled. GitLab, Bitbucket and Azure DevOps each keep a delivery history for webhooks that shows failed calls. </Accordion> <Accordion title=""GitLab would not return ... Check the source path template.""> transglot could not read the source file at the pushed commit. Check the **Source path** and **Source locale**, and that the token can still read the repository. </Accordion> <Accordion title=""The default branch ... could not be found""> The **Default branch** on the connection does not exist, or the token can no longer read the repository. Correct the branch name, or create a new token and save it on the connection. </Accordion> <Accordion title="Strings arrive in the project but no merge or pull request appears"> The token can read but not write. Create a token with permission to push branches and open merge or pull requests, and save it on the connection. </Accordion> </Accordions> For a step-by-step diagnosis, see [Why is my repository sync not running?](https://transglot.ai/docs/integrations/git/why-is-my-repository-sync-not-running). ## Related [#related] - [Connect GitHub](https://transglot.ai/docs/integrations/git/connect-github) - [Plan requirements](https://transglot.ai/docs/integrations/plan-requirements) - [AI translation settings](https://transglot.ai/docs/guides/translating/ai-translation-settings) # Connect GitHub (https://transglot.ai/docs/integrations/git/connect-github/) The transglot GitHub App gives you continuous localization with no CI setup. When you push a change to your source locale file on the default branch, transglot extracts the new strings into your project, translates them, and opens a pull request with the translated files. You review and merge it like any other pull request. The direction is repository to project to pull request. transglot never commits to your branches: it writes only to its own `transglot/sync` branch and proposes the change from there. ## Requirements [#requirements] - **Plan:** Team or above. On a lower plan the **GitHub App** card offers **Upgrade plan** instead of **Install on GitHub**. - **Role:** Admin of the transglot workspace, or an Owner of its account. - **On GitHub:** permission to install a GitHub App on the account or organization that owns the repository. - **Your files:** one source locale file per repository, at a path that contains the locale code, for example `locales/en.json`. ## What the App can access [#what-the-app-can-access] On the repositories you grant it, the App asks GitHub for: | Permission | Why | | --- | --- | | Contents: read and write | Read your source locale file, and write the translated files to the `transglot/sync` branch | | Pull requests: read and write | Open and update the translation pull request | | Metadata: read | Required by GitHub for every App | The App authenticates as itself, so there is no token for you to create, paste or rotate. ## Connect a repository [#connect-a-repository] <Steps> <Step> ### Install the App [#install-the-app] In your workspace, open **Integrations** and select **Install on GitHub** on the **GitHub App** card, then **Install on GitHub** again on the GitHub App page. GitHub asks which account to install on and which repositories to grant. Grant only the repositories you want to localize. When GitHub sends you back, transglot confirms "GitHub App connected. Map a repository to a project below." </Step> <Step> ### Choose the repository and project [#choose-the-repository-and-project] Under **Repositories**, pick the **Repository** and the **Project** it should feed. Each repository feeds one project. </Step> <Step> ### Describe your files [#describe-your-files] | Field | What to enter | | --- | --- | | **Source path** | Where your locale files live, with `{locale}` in place of the locale code, for example `locales/{locale}.json` | | **Format** | The file format, for example `json_nested` or `android_xml`. See [file formats](https://transglot.ai/docs/developers/formats) | | **Source locale** | The locale your team writes in, for example `en` | | **Target locales (comma-separated, blank = all)** | The locales to deliver, for example `fr, de, es`. Leave it blank to deliver every target language in the project | | **Minimum state** | Which translations go into the pull request. **Any** includes every translation; **Human edited** or **Reviewed** leaves out translations that have not reached that state | </Step> <Step> ### Link it [#link-it] Select **Link repository**. transglot confirms it with the repository name, for example "acme/web linked.", and the repository appears in the list with its project, last run and pull request. </Step> </Steps> ## What happens on a push [#what-happens-on-a-push] 1. You **push** to the repository's default branch, and the push changes the source locale file (the **Source path** with your **Source locale** filled in). 2. transglot **extracts** the strings from that file into the project. New and changed strings become ordinary keys, so translation memory, the glossary and quality checks apply to them like any other content. 3. If the project has automatic translation on, the new strings are **translated**. A string your team has edited or reviewed is never overwritten by AI. 4. When the translation finishes, transglot opens a **pull request** from the `transglot/sync` branch with one file per target locale, written to the same path pattern. If a transglot pull request is already open, it is updated in place instead of a second one being opened. Pushes to other branches, and pushes that do not touch the source locale file, do nothing. If nothing new needed translating, or automatic translation is off, no pull request is opened. Translating new strings uses your plan's monthly AI translation allowance, as it does anywhere else in transglot. ## Check what happened [#check-what-happened] The GitHub App page lists **Recent deliveries**: every push transglot acted on, with its commit, its pull request and a status. | Status | Meaning | | --- | --- | | **Received** | The push matched your source file and extraction started | | **Translating** | New strings are being translated; the pull request follows | | **PR opened** | A new pull request was opened on `transglot/sync` | | **PR updated** | The open pull request was refreshed with the latest translations | | **Nothing to do** | Nothing new to translate, so no pull request. The detail says why, for example "No new source strings to translate." | | **Failed** | Something went wrong; the detail gives the reason | | **Blocked** | The run was held because of the workspace's account status; the detail says why | ## Unlink or uninstall [#unlink-or-uninstall] To stop syncing one repository, select **Unlink** on its row and confirm. No new pull requests are opened for it; anything already open stays open, and the GitHub App stays installed. To remove the App entirely, uninstall it from the account or organization's settings on GitHub. ## Troubleshooting [#troubleshooting] <Accordions type="single"> <Accordion title="The Repository list shows "No repositories granted""> The App was installed without access to any repository. Change the installation's repository access on GitHub, then reload the page. </Accordion> <Accordion title=""The source path must contain the {locale} placeholder.""> Write the path with `{locale}` where the locale code goes, for example `src/i18n/{locale}.json`, not `src/i18n/en.json`. </Accordion> <Accordion title=""This GitHub installation is already linked to another transglot workspace.""> A GitHub installation belongs to one transglot workspace. Unlink it there first, or install the App on a different GitHub account. </Accordion> <Accordion title=""This setup link has expired.""> The install round trip took longer than 15 minutes. Select **Install on GitHub** again. </Accordion> <Accordion title=""Nothing to do" with "Monthly word quota exceeded. Translation skipped.""> The strings were extracted, but the workspace has used its AI translation allowance for this cycle. See [usage-based billing](https://transglot.ai/docs/administration/billing/usage-based-billing). </Accordion> <Accordion title=""Failed" with "Source file ... not found" or "Parse error""> transglot could not find or read the file at the commit you pushed. Check the **Source path**, **Source locale** and **Format** on the link match the file in the repository. </Accordion> <Accordion title=""This installation is suspended on GitHub.""> Someone suspended the App on GitHub. No pull requests open until you resume it in the account or organization's GitHub settings. </Accordion> <Accordion title="The card shows coming soon and has no install button"> The GitHub App cannot be installed from this workspace. Use the [GitHub Action](https://transglot.ai/docs/developers/github-action) instead: it runs the transglot CLI in your workflow with a deploy token. </Accordion> </Accordions> For a step-by-step diagnosis, see [Why is my repository sync not running?](https://transglot.ai/docs/integrations/git/why-is-my-repository-sync-not-running). ## Related [#related] - [Connect GitLab, Bitbucket or Azure Repos](https://transglot.ai/docs/integrations/git/connect-another-git-host) - [The GitHub Action](https://transglot.ai/docs/developers/github-action) - [AI translation settings](https://transglot.ai/docs/guides/translating/ai-translation-settings) # Why is my repository sync not running? (https://transglot.ai/docs/integrations/git/why-is-my-repository-sync-not-running/) Start with what transglot recorded. For GitHub, open the GitHub App page from **Integrations** and read **Recent deliveries**. For GitLab, Bitbucket and Azure Repos, read the last sync panel on the host's card on the **Integrations** page. If a run is listed, its status and sentence usually name the cause. If nothing is listed, transglot never acted on the push: work through the checks below in order. <Steps> <Step> ### Was the push to the default branch? [#was-the-push-to-the-default-branch] Only pushes to the repository's default branch start a sync. A push to a feature branch, or a merge or pull request that is still open, does nothing until it lands on the default branch. </Step> <Step> ### Did the push change the source locale file? [#did-the-push-change-the-source-locale-file] transglot reacts only when the push changes the file at your **Source path** for your **Source locale**. With a source path of `locales/{locale}.json` and source locale `en`, that is `locales/en.json`. A push that changes only target locale files, or files elsewhere, does nothing. </Step> <Step> ### Is the repository linked to a project? [#is-the-repository-linked-to-a-project] Installing the GitHub App or adding a webhook is only half of it. The repository must also appear in the list on the integration's page, linked to a project. On GitHub, also check that the App was granted access to that repository. </Step> <Step> ### Is the webhook reaching transglot? (GitLab, Bitbucket, Azure Repos) [#is-the-webhook-reaching-transglot-gitlab-bitbucket-azure-repos] Open the webhook's delivery history on the host. Each delivery must go to the URL shown on the transglot page, carry the same secret you saved on the connection, and be for a push event. A rejected delivery means the secret does not match: set the same value on both sides. </Step> <Step> ### Does the account's plan include it? [#does-the-accounts-plan-include-it] Repository sync needs the Team plan or above. On a lower plan, pushes are not processed, and the GitLab, Bitbucket and Azure Repos cards say so, for example "The GitLab integration is not included on the current plan." See [Plan requirements](https://transglot.ai/docs/integrations/plan-requirements). </Step> <Step> ### Is automatic translation on? [#is-automatic-translation-on] Strings are extracted whatever your settings, but they are only translated automatically if the project has automatic translation turned on. Otherwise they wait in the project until someone translates them, and there is nothing new for a pull request. See [AI translation settings](https://transglot.ai/docs/guides/translating/ai-translation-settings). </Step> <Step> ### Is there AI translation allowance left? [#is-there-ai-translation-allowance-left] If the workspace has used its AI translation allowance for the cycle and usage-based billing is off, extraction still runs but translation is skipped. GitHub records "Monthly word quota exceeded. Translation skipped." See [usage-based billing](https://transglot.ai/docs/administration/billing/usage-based-billing). </Step> <Step> ### Can the token still write? (GitLab, Bitbucket, Azure Repos) [#can-the-token-still-write-gitlab-bitbucket-azure-repos] A token that expired or lost permissions fails on the way back, not on the way in: strings can arrive in the project while no merge or pull request ever appears. Create a new token with write access and save it on the connection. </Step> <Step> ### Is a pull request waiting for you? [#is-a-pull-request-waiting-for-you] The result is a merge or pull request from transglot's `transglot/sync` branch, never a commit to yours. Until someone merges it, nothing reaches your default branch. Look for an open request from `transglot/sync`. </Step> </Steps> ## Related [#related] - [Connect GitHub](https://transglot.ai/docs/integrations/git/connect-github) - [Connect GitLab, Bitbucket or Azure Repos](https://transglot.ai/docs/integrations/git/connect-another-git-host) - [AI translation settings](https://transglot.ai/docs/guides/translating/ai-translation-settings) # Integrations (https://transglot.ai/docs/integrations/) Integrations move strings into transglot from where they already live, and move translations back out. Each one is included in your plan once the plan reaches the integration's minimum: there are no add-ons to buy. ## Where to find them [#where-to-find-them] Open your workspace's **Settings** and choose **Integrations**. Every member can open the page; connecting and changing integrations is for Owners and Admins. The page has two parts: - **The app grid** lists what you can wire up now, each card with its real state, read from your actual tokens, endpoints and syncs rather than a checkbox: **connected**, **configured** (set up but not used yet), **available** or **coming soon**. Filter it with the search box, the category chips (**Notify**, **Pipeline**, **Context**, **Content**, **Data**) and the switch that toggles between **Showing everything** and **Installed only**. - **The Marketplace** lists every connector by family (**Content connectors**, **Design tools** and **Developer apps**) with what it does, whether it is **Import only**, and whether your plan includes it. Content platform and app store connections are set up per project, on the project's **CMS** settings page, rather than on the workspace's integrations page. ## Code repositories [#code-repositories] A push to your default branch extracts new strings into a project, and transglot answers with a pull or merge request holding the translations. Team plan and above. <Cards> <Card title="GitHub" href="/integrations/git/connect-github" description="Install the transglot GitHub App and link a repository to a project." /> <Card title="GitLab, Bitbucket and Azure Repos" href="/integrations/git/connect-another-git-host" description="Connect a repository with an access token and a webhook." /> <Card title="Repository sync not running" href="/integrations/git/why-is-my-repository-sync-not-running" description="Work through the checks for a push that produced nothing." /> </Cards> ## Content, app stores and storage [#content-app-stores-and-storage] Import the text you publish elsewhere, translate it, and publish reviewed translations back where the platform allows it. Starter plan and above. <Cards> <Card title="Content platforms" href="/integrations/content-platforms" description="CMS, e-commerce, help center and marketing tools, two-way or import only." /> <Card title="App Store Connect" href="/integrations/app-stores/app-store-connect" description="Localize your App Store listing into every App Store locale." /> <Card title="Google Play" href="/integrations/app-stores/google-play" description="Localize your Google Play store listing into every Play language." /> <Card title="Amazon S3 and Dropbox" href="/integrations/storage/connect-file-storage" description="Sync locale files that live in a bucket or a folder." /> </Cards> ## Notifications and automation [#notifications-and-automation] <Cards> <Card title="Slack" href="/integrations/notifications/connect-slack" description="Post batch, quality check, comment and review events into a channel. Every plan." /> <Card title="Jira" href="/integrations/automation/connect-jira" description="Open a Jira issue when something needs a person to act. Team plan and above." /> <Card title="Zapier, Make and n8n" href="/integrations/automation/zapier-make-and-n8n" description="Start flows from transglot events and drive transglot over the API. Starter plan and above." /> </Cards> ## Moving to transglot [#moving-to-transglot] <Cards> <Card title="Import from another tool" href="/integrations/migrate/import-from-another-tool" description="Bring keys and translations across from Lokalise, Phrase, Crowdin, Transifex, Weblate or POEditor." /> </Cards> ## Developer tools [#developer-tools] The integrations page also links the tools developers use from code and CI. They are documented in the developer section: <Cards> <Card title="CLI" href="/developers/cli" description="Push source files and pull translations from any repository or CI job." /> <Card title="GitHub Action" href="/developers/github-action" description="Run the CLI in a GitHub workflow with a deploy token." /> <Card title="Webhooks" href="/developers/webhooks" description="Receive signed events at your own endpoint." /> <Card title="MCP server" href="/mcp" description="Work with a project from an MCP client in natural language." /> <Card title="In-context editor" href="/developers/in-context" description="Let reviewers edit strings on the live page." /> </Cards> ## Plans [#plans] | Minimum plan | Integrations | | --- | --- | | Free | Slack, import from another tool | | Starter | Content platforms, App Store Connect, Google Play, Amazon S3, Dropbox, Zapier, Make and n8n | | Team | GitHub, GitLab, Bitbucket, Azure Repos, Jira | See [Plan requirements](https://transglot.ai/docs/integrations/plan-requirements) for what the page shows when your plan is below a minimum, and [pricing](https://transglot.ai/pricing) for what each plan includes. # Import from another localization tool (https://transglot.ai/docs/integrations/migrate/import-from-another-tool/) The importer is a one-time migration. It reads a project out of the tool you are leaving and writes its keys and translations into transglot. It only reads from the other tool and never writes back to it, and nothing stays connected after the run: the imported strings are ordinary transglot keys and translations. Your existing translations arrive as human-authored translations, so AI never overwrites them. The source language seeds the keys, and any gaps can be filled by AI translation afterwards if the project has automatic translation on. ## Requirements [#requirements] - **Plan:** any plan. An import that creates a new project needs room for another project on your plan, and new languages count towards your plan's language limit. - **Role:** Admin of the workspace, or an Owner of its account. - **A read API token** from the tool you are leaving. A read-only token is enough. | Tool | Where to find the token | | --- | --- | | Lokalise | Profile settings, then API tokens | | Phrase | Phrase Strings: Profile settings, then API tokens. Generate a token with read access | | Crowdin | Account settings, then API, then Personal Access Tokens | | Transifex | User settings, then API token. The token starts with `1/` | | Weblate | `hosted.weblate.org` only: Your profile, then API access | | POEditor | Account settings, then API access | Self-hosted Weblate is not supported by the importer. Export your files from Weblate and [import them as files](https://transglot.ai/docs/guides/projects/import-content) instead. ## Run an import [#run-an-import] <Steps> <Step> ### Source [#source] Open **Settings** for your workspace and choose **Import**. Under **Where it is now**, pick the tool, paste the token into **API token** and select **Verify the token**. Use **show** to check what you pasted. The token is used only for this import and is not kept after the run ends. </Step> <Step> ### What to read [#what-to-read] Pick the source project to import. For Transifex and Weblate you pick one resource or component at a time, listed as `Project > Resource`, because the same key name can repeat across them. Under **Import into**, choose **A new project**, or an existing project to merge into. An existing project keeps everything already translated and reviewed in it. Then select **Review this import**. </Step> <Step> ### Review [#review] Nothing has been written yet. The review shows four figures before you commit: | Figure | Meaning | | --- | --- | | **comes across** | Keys and translations that will be written, as human-authored | | **does not** | Content that stays in your old tool | | **stops the import** | Problems that must be cleared before the import can run | | **makes it smaller** | Limits that trim the import; it still runs | Select each figure to see its detail. </Step> <Step> ### Run [#run] Select **Start import**. The import runs in the background and you can leave the page. When it finishes, **What this run did** shows the **Keys created**, **Translations written** and **Languages landed**. </Step> </Steps> To stop a run early, select **Cancel this import**. Cancelling stops the rest of the import; it does not undo what has already landed. An import cannot be undone once it has finished. Every run is listed under **Import history**, newest first, with what it wrote and what it could not carry. ## What stops an import [#what-stops-an-import] <Accordions type="single"> <Accordion title=""The account this workspace belongs to has no room for another project. Import into an existing project instead.""> Your plan's project limit is reached. Choose an existing project under **Import into**, or [change your plan](https://transglot.ai/docs/administration/billing/change-your-plan). </Accordion> <Accordion title="The source languages do not match"> When you import into an existing project, the source project's source language must match it. The review names both languages. Pick a project with a matching source language, or import into a new one. </Accordion> </Accordions> ## What makes an import smaller [#what-makes-an-import-smaller] - **Languages beyond your plan.** If your plan has no room for every language in the source project, only the ones that fit are created, and the review says how many. - **Very large projects.** One import run carries a limited number of keys and of keys times languages. The review tells you how much will come across before you start. - **Untranslated strings** are not imported as translations: an empty translation is skipped. - **Keys transglot cannot carry as they are.** After the run, **What this run did** lists them: **Keys that did not come across** (names longer than transglot allows) and **Translations that did not come across** (a plural form that does not match the key already in the project), alongside counts such as **Plural keys skipped** and **Keys with a repeated name**. When you import into an existing project, translations already edited or reviewed there are kept, and the run lists them as **Left alone here**. ## Troubleshooting [#troubleshooting] <Accordions type="single"> <Accordion title="The token is rejected"> Check that it is a token for the tool you picked, that it has not expired, and that it can read the project you want. Picking a different tool clears the token field, so paste it again after switching. </Accordion> <Accordion title=""No projects were found for this token.""> The token is valid, but the account it belongs to holds nothing the importer can read. Check you pasted a token for the right account or workspace in the other tool. </Accordion> <Accordion title=""No imports yet""> This workspace has not run an import before. It is not an error. </Accordion> </Accordions> ## Related [#related] - [Import content from files](https://transglot.ai/docs/guides/projects/import-content) - [Which file formats can I use?](https://transglot.ai/docs/guides/projects/which-file-formats-can-i-use) - [My import did not bring everything across](https://transglot.ai/docs/resources/troubleshooting/my-import-did-not-bring-everything-across) # Connect Slack (https://transglot.ai/docs/integrations/notifications/connect-slack/) transglot posts workspace events into one Slack channel through a Slack incoming webhook: translation batches finishing or failing, quality checks, comments and review activity. Your team sees them where it already talks, without opening transglot. ## Requirements [#requirements] - **Plan:** any plan, including Free. - **Role:** Admin of the workspace, or an Owner of its account. - **In Slack:** permission to add an incoming webhook to the channel you want the messages in. ## Connect Slack [#connect-slack] <Steps> <Step> ### Create an incoming webhook in Slack [#create-an-incoming-webhook-in-slack] Follow Slack's guide to [incoming webhooks](https://api.slack.com/messaging/webhooks), choose the channel, and copy the webhook URL. It starts with `https://hooks.slack.com/services/`. </Step> <Step> ### Open the workspace's General settings [#open-the-workspaces-general-settings] In transglot, open your workspace's **Settings**, then **General**. Find the **Slack notifications** card. </Step> <Step> ### Paste the URL and save [#paste-the-url-and-save] Paste the URL into **Slack incoming-webhook URL**, then select **Save & send test**. transglot saves the URL and posts "transglot is connected" to the channel. Select **Save** instead if you do not want a test message. </Step> </Steps> The card now shows **Connected**, and the **Slack** card on the **Integrations** page reads **connected** with "Incoming webhook configured (stored encrypted)." ## How the URL is stored [#how-the-url-is-stored] The webhook URL works like a password: anyone holding it can post to your channel. transglot stores it encrypted and never sends it back to the browser. After saving, the field shows "•••• connected, paste a new URL to replace", and the URL cannot be read out of transglot, only replaced. ## Change the channel [#change-the-channel] Create a new incoming webhook for the other channel in Slack, paste its URL over the old one and select **Save**. ## Disconnect [#disconnect] Select **Disconnect** on the **Slack notifications** card and confirm. The channel stops receiving this workspace's events immediately. To reconnect later, paste a webhook URL again. ## Troubleshooting [#troubleshooting] <Accordions type="single"> <Accordion title="The URL is refused when you save"> transglot only posts to public HTTPS addresses. The message under the field names the rule the URL broke, for example "The webhook URL must use https." Copy the URL from Slack again, in full. </Accordion> <Accordion title="Saved, but no test message arrived"> The test message is sent in the background and usually arrives within seconds. If it never does, the webhook may have been removed or disabled in Slack: create a new one and paste its URL. </Accordion> <Accordion title="Save is greyed out"> **Save** and **Save & send test** stay disabled until the field holds a URL, and the card says "Paste a webhook URL to save". To clear a connected channel, use **Disconnect**. </Accordion> </Accordions> ## Related [#related] - [Connect Jira](https://transglot.ai/docs/integrations/automation/connect-jira) - [Notification preferences](https://transglot.ai/docs/administration/account/notification-preferences) - [Integrations overview](https://transglot.ai/docs/integrations) # Plan requirements (https://transglot.ai/docs/integrations/plan-requirements/) Integrations are not sold separately. Each one has a minimum plan, and once your account's plan meets it the integration is included at no extra charge. There is nothing to buy or switch on: an Owner or Admin connects it, and it works. ## Minimum plan for each integration [#minimum-plan-for-each-integration] | Integration | Minimum plan | | --- | --- | | [Slack](https://transglot.ai/docs/integrations/notifications/connect-slack) | Free (every plan) | | [Import from Lokalise, Phrase or Crowdin](https://transglot.ai/docs/integrations/migrate/import-from-another-tool) | Free (every plan), within your plan's project, language and key limits | | [Content platforms](https://transglot.ai/docs/integrations/content-platforms) (CMS, e-commerce, help center and marketing tools) | Starter | | [App Store Connect](https://transglot.ai/docs/integrations/app-stores/app-store-connect) and [Google Play](https://transglot.ai/docs/integrations/app-stores/google-play) | Starter | | [Amazon S3 and Dropbox](https://transglot.ai/docs/integrations/storage/connect-file-storage) | Starter | | [Zapier, Make and n8n](https://transglot.ai/docs/integrations/automation/zapier-make-and-n8n) (through the API and webhooks) | Starter | | [GitHub](https://transglot.ai/docs/integrations/git/connect-github) | Team | | [GitLab, Bitbucket and Azure Repos](https://transglot.ai/docs/integrations/git/connect-another-git-host) | Team | | [Jira](https://transglot.ai/docs/integrations/automation/connect-jira) | Team | Plans above a minimum include it too: an integration that needs Starter is also included on Team, Business and Enterprise. The OSS plan for open-source projects meets the same minimums as Team. For what each plan costs and includes, see [pricing](https://transglot.ai/pricing). ## How the integrations page shows it [#how-the-integrations-page-shows-it] Open **Integrations** in your workspace. Two parts of the page tell you where you stand: - **The app cards** at the top. A card whose minimum plan you have not reached carries the plan's name as a flag (for example **Team**), says "Requires the Team plan." and offers **Upgrade plan** instead of **Connect**. - **The Marketplace** section below, grouped into **Content connectors**, **Design tools** and **Developer apps**. Each card ends with **Included** when your plan covers it, or **Upgrade to enable** when it does not. **Upgrade plan** opens the account's **Plan & billing** page. If you are not allowed to change the account's plan, the card names the account Owners who can, instead of showing the button. ## If your plan drops below an integration's minimum [#if-your-plan-drops-below-an-integrations-minimum] The integration stops doing work: no new syncs, pull requests or Jira issues. Its settings stay saved. Move back to a plan that includes it and the next push or sync runs as usual. For GitLab, Bitbucket, Azure Repos, Amazon S3 and Dropbox, the card on the **Integrations** page shows the last sync attempt and why it did not run, for example "The GitLab integration is not included on the current plan." A sync can also be held for a reason that is not the plan, such as a hold on the account; the card names whichever reason applies. ## Related [#related] - [Change your plan](https://transglot.ai/docs/administration/billing/change-your-plan) - [Your plan and seats](https://transglot.ai/docs/administration/billing/your-plan-and-seats) - [Integrations overview](https://transglot.ai/docs/integrations) # Connect Amazon S3 or Dropbox (https://transglot.ai/docs/integrations/storage/connect-file-storage/) If your locale files live in cloud storage rather than in a code repository, connect the bucket or folder to a project. Each time you select **Sync now**, transglot reads the source locale file, adds its new and changed strings to the project, and writes one file per target locale back into the same place. Your source file is never overwritten. ## Requirements [#requirements] - **Plan:** Starter or above. On a lower plan the card offers **Upgrade plan** instead of **Connect**. - **Role:** Admin of the workspace, or an Owner of its account. - **Your files:** one source locale file whose name or path contains the locale code, for example `locales/en.json`. Each workspace holds one Amazon S3 connection and one Dropbox connection, and each feeds one project. ## Connect [#connect] <Tabs items={['Amazon S3', 'Dropbox']}> <Tab value="Amazon S3"> Amazon S3 connects with an access key you create in AWS. There is no sign-in step. <Steps> <Step> ### Create an access key [#create-an-access-key] In AWS IAM, create a user or role for transglot with an access key. Grant it only `s3:GetObject`, `s3:PutObject` and `s3:ListBucket` on the bucket and prefix that holds your locale files. </Step> <Step> ### Open the Amazon S3 page [#open-the-amazon-s3-page] In your workspace, open **Integrations** and select **Connect** on the **Amazon S3** card. </Step> <Step> ### Fill in Bucket & credentials [#fill-in-bucket--credentials] | Field | What to enter | | --- | --- | | **Project** | The transglot project the files feed | | **Region** | The bucket's region, for example `eu-west-1` | | **Bucket** | The bucket name, for example `my-localization-bucket` | | **Key prefix (blank = whole bucket)** | The folder inside the bucket, for example `app/locales` | | **Access key ID** | The key ID, starting `AKIA` | | **Secret access key** | The secret. It is stored encrypted and cannot be read back | | **Source file name** | The file name under the prefix, with `{locale}` in place of the locale code, for example `{locale}.json` | | **Format** | The file format. See [file formats](https://transglot.ai/docs/developers/formats) | | **Source locale** | The locale your team writes in, for example `en` | | **Target locales (comma-separated, blank = all)** | The locales to write, for example `fr, de, es`. Leave it blank for every target language in the project | Select **Save configuration**. The confirmation reads "Amazon S3 connected. Use Sync now to run the first sync." </Step> </Steps> </Tab> <Tab value="Dropbox"> Dropbox connects by signing in to your Dropbox account and granting access. <Steps> <Step> ### Connect your Dropbox account [#connect-your-dropbox-account] In your workspace, open **Integrations** and select **Connect** on the **Dropbox** card, then **Connect Dropbox**. Sign in to Dropbox and allow access. Back in transglot, the confirmation reads "Dropbox connected." </Step> <Step> ### Choose the folder and files [#choose-the-folder-and-files] | Field | What to enter | | --- | --- | | **Project** | The transglot project the files feed | | **Dropbox folder path** | The folder that holds your locale files. Leave it blank for the root of what transglot can see | | **Source file name** | The file name in that folder, with `{locale}` in place of the locale code, for example `locales/{locale}.json` | | **Format** | The file format. See [file formats](https://transglot.ai/docs/developers/formats) | | **Source locale** | The locale your team writes in, for example `en` | | **Target locales (comma-separated, blank = all)** | The locales to write, for example `fr, de, es`. Leave it blank for every target language in the project | Select **Save configuration**. </Step> </Steps> </Tab> </Tabs> ## Sync [#sync] Select **Sync now** on the connector's page. The sync runs in the background: 1. transglot downloads the source locale file and adds its new and changed strings to the project, where translation memory, the glossary and quality checks apply to them like any other content. If automatic translation is on for the project, the new strings are translated. 2. transglot writes one file per target locale, named from your **Source file name** with each locale filled in, next to the source file. A file whose content has not changed is left alone. Each sync writes the translations the project holds at that moment. Strings translated after the sync started reach your storage on the next **Sync now**. Syncing is manual: nothing runs on a schedule or when a file changes. The connector's card on the **Integrations** page shows the last sync, for example "Source pulled in; 3 target files written." ## Disconnect [#disconnect] Select **Disconnect** on the connector's page and confirm. - **Amazon S3:** the stored access key and secret are deleted and cannot be recovered. Objects in the bucket are untouched. - **Dropbox:** the stored access is deleted, so reconnecting means signing in to Dropbox again. Files already synced into transglot stay where they are. ## Troubleshooting [#troubleshooting] <Accordions type="single"> <Accordion title=""The source path must contain the {locale} placeholder.""> Write the **Source file name** with `{locale}` where the locale code goes, for example `{locale}.json`. </Accordion> <Accordion title=""The source object is not under the configured prefix." (S3)"> transglot looked for the source file at the **Key prefix** plus the **Source file name** with your **Source locale** filled in, and found nothing. Check all three, and that the region matches the bucket: the address transglot calls is built from the region and the bucket, so a wrong region reads as a missing file. </Accordion> <Accordion title=""The source file ... is not in the connected folder." (Dropbox)"> Check the **Dropbox folder path**, the **Source file name** and the **Source locale** together name a file that exists. </Accordion> <Accordion title=""Dropbox rejected the stored credentials. The integration needs reconnecting.""> Access was revoked on the Dropbox side. Select **Reconnect** and sign in again. </Accordion> <Accordion title=""The source locale ... is not one of this project's languages.""> The **Source locale** on the connection is not a language of the project. Set it to the project's source language, as a locale code such as `en`, and save again. </Accordion> <Accordion title="The card shows coming soon and has no Connect button"> That connector cannot be connected from this workspace. Use the [CLI](https://transglot.ai/docs/developers/cli) or the [API](https://transglot.ai/docs/developers/api) to push and pull files instead. </Accordion> </Accordions> ## Related [#related] - [Which file formats can I use?](https://transglot.ai/docs/guides/projects/which-file-formats-can-i-use) - [Plan requirements](https://transglot.ai/docs/integrations/plan-requirements) - [Integrations overview](https://transglot.ai/docs/integrations) # Frequently asked questions (https://transglot.ai/docs/resources/faq/) ## Before you start [#before-you-start] <Accordions type="single"> <Accordion title="What does transglot do?"> It keeps your product's text translated. You bring source strings in from a repository, a file or a content platform; Glossa drafts the translations; automatic checks and your reviewers catch what needs a person; and the finished languages go back out through your build, a pull request, the CDN or the content platform. See [How transglot works](https://transglot.ai/docs/how-transglot-works). </Accordion> <Accordion title="Is there a free plan?"> Yes. The Free plan includes one workspace, one project and a monthly allowance of AI translation, plus a one-time grant of welcome words. The paid plans are Starter, Team, Business and Enterprise. See [pricing](https://transglot.ai/pricing) for current allowances and prices. </Accordion> <Accordion title="Which plan do I need for the CLI, the API and integrations?"> Starter or above. The CLI, the GitHub Action, the API, the CDN, the glossary and the integrations all start on Starter. Quality mode, AI quality review and self-heal start on Team. Single sign-on and SCIM are on Enterprise. See [Plan requirements](https://transglot.ai/docs/integrations/plan-requirements). </Accordion> <Accordion title="Can I try a paid plan first?"> Yes. When you connect the CLI from a Free account, the browser offers a free trial of the Starter plan to the account's owners and billing members. Stripe asks for a card and nothing is charged that day. See the [Quickstart](https://transglot.ai/docs/quickstart). </Accordion> <Accordion title="Is there a plan for open source projects?"> Yes. Projects with an OSI-approved licence and a public repository can apply for the open source plan, which is reviewed by hand. See [Open source plan](https://transglot.ai/docs/administration/billing/open-source-plan). </Accordion> <Accordion title="Which AI translates my strings?"> Glossa, the transglot translation engine. Glossa Flux is the fast default model; Glossa Deep is the higher-quality model you get with Quality mode on Team and above. See [AI translation](https://transglot.ai/docs/concepts/ai-translation). </Accordion> <Accordion title="Is my content used to train AI models?"> No. transglot does not train any models on your content. The AI services it uses process your content under commercial terms, for translation only. See the [Trust Center](https://transglot.ai/trust-center). </Accordion> <Accordion title="Is translation memory shared with other customers?"> Exact-match reuse draws on an anonymised, shared pool as well as your own workspace's history. You can turn that off per project with the **Translation memory** setting; the project then neither reads from nor adds to the pool. Similar-match suggestions only ever come from your own workspace. See [Translation memory](https://transglot.ai/docs/concepts/translation-memory). </Accordion> <Accordion title="Which file formats can I use?"> Twenty, including JSON (flat and nested), YAML, XLIFF 1.2 and 2.0, gettext PO, Android XML, Apple strings, stringsdict and string catalogs, Flutter ARB, Laravel PHP and JSON, Java properties, .NET resx, Markdown, CSV and Excel. See [File formats](https://transglot.ai/docs/developers/formats). </Accordion> <Accordion title="Can I move from another localization tool?"> Yes. You can bring your keys and translations across from Lokalise, Phrase, Crowdin, Transifex, Weblate or POEditor with a read-only token. Your existing translations arrive as human-authored. See [Import from another tool](https://transglot.ai/docs/integrations/migrate/import-from-another-tool). </Accordion> <Accordion title="Do I need a developer to set it up?"> Only to connect your codebase. Uploading files, migrating from another tool and connecting a content platform need no code. See [Do I need a developer to set transglot up?](https://transglot.ai/docs/first-steps/do-i-need-a-developer-to-set-transglot-up) </Accordion> <Accordion title="Can I work with my own translators or an agency?"> Yes. Invite them with the Translator or Reviewer role, limit them to the projects and languages they work on, and hand them languages as assignments with a due date. See [Onboard a vendor or freelancer](https://transglot.ai/docs/administration/team/onboard-a-vendor-or-freelancer). </Accordion> <Accordion title="Can people translate without AI?"> Yes. Turn off **Auto-translate** for the project and nothing is translated until someone starts a run; your team can write every translation in the editor. See [AI translation settings](https://transglot.ai/docs/guides/translating/ai-translation-settings). </Accordion> <Accordion title="Do you support single sign-on?"> Yes, with SAML 2.0 and OpenID Connect identity providers, plus SCIM provisioning, on the Enterprise plan. See [Set up single sign-on](https://transglot.ai/docs/administration/security/set-up-single-sign-on). </Accordion> </Accordions> ## Once you are using transglot [#once-you-are-using-transglot] <Accordions type="single"> <Accordion title="Will AI overwrite translations my team wrote?"> No. A translation you have edited or reviewed is never overwritten by AI. Glossa only writes into strings that are empty or still a Glossa draft. See [Translation states](https://transglot.ai/docs/concepts/translation-states). </Accordion> <Accordion title="How is usage counted?"> In source words, once for each target language they are translated into. Reused translations and anything your team writes by hand do not count. See [Words and usage](https://transglot.ai/docs/concepts/words-and-usage). </Accordion> <Accordion title="What happens when we run out of words?"> Your plan includes an allowance of AI translation each month. Usage is measured against that allowance, and what happens when you exceed it depends on whether usage-based billing is turned on for your account. With it off, translation stops until the next cycle or until you upgrade; with it on, translation continues up to the spend cap you set. See [Usage-based billing](https://transglot.ai/docs/administration/billing/usage-based-billing). </Accordion> <Accordion title="Why was my translation flagged for review?"> A person flagged it, a quality check found a problem, or confidence routing sent it to review. See [Why was my translation flagged for review?](https://transglot.ai/docs/guides/translating/why-was-my-translation-flagged-for-review) </Accordion> <Accordion title="How do translations get into my app?"> Pull them into your build with the CLI or the GitHub Action, merge the pull request a connected repository opens, or publish them to the CDN and read them at runtime with an SDK. See [The CDN](https://transglot.ai/docs/guides/delivery/the-cdn) and [The runtime SDKs](https://transglot.ai/docs/developers/sdks). </Accordion> <Accordion title="I edited a translation. Why is my app still showing the old one?"> Editing and publishing are separate. Publish the language again, or run the build that pulls translations. See [My translations are not reaching my app](https://transglot.ai/docs/resources/troubleshooting/my-translations-are-not-reaching-my-app). </Accordion> <Accordion title="Can reviewers see strings in context?"> Yes. Screenshots show where a string appears, and on Starter and above the in-context editor lets reviewers edit translations on your running app. See [The screenshot library](https://transglot.ai/docs/guides/quality/the-screenshot-library) and [In-context editor](https://transglot.ai/docs/developers/in-context). </Accordion> <Accordion title="Can I export everything?"> Yes. A project's **Export** settings page builds one archive of the languages and formats you choose: files for your app in any supported format, or a bilingual sheet for a person to work on. The CLI's `pull` writes the languages named in your `transglot.json` to files. </Accordion> <Accordion title="What happens if we cancel?"> See [What happens if I cancel?](https://transglot.ai/docs/administration/billing/what-happens-if-i-cancel) </Accordion> </Accordions> Can't find your question? See [Get help](https://transglot.ai/docs/resources/get-help). # Get help (https://transglot.ai/docs/resources/get-help/) ## Check the docs first [#check-the-docs-first] - [Troubleshooting](https://transglot.ai/docs/resources/troubleshooting) covers the problems people run into most, with the fix for each. - [Frequently asked questions](https://transglot.ai/docs/resources/faq) answers questions about plans, data and setup. - The [Trust Center](https://transglot.ai/trust-center) answers security and procurement questions: subprocessors, retention, and the documents a reviewer asks for. ## Contact us [#contact-us] When you are signed in, the chat button in the corner of the console opens a conversation with the team. The team sees the account you are signed in with, so name the workspace and project and leave out your email address. To write by email instead, use the address that fits. There is no ticket form. | What you need | Write to | | --- | --- | | Support, billing, plans, invoices and product questions | [hello@transglot.ai](mailto:hello@transglot.ai) | | A security vulnerability | [security@transglot.ai](mailto:security@transglot.ai). See the [vulnerability disclosure policy](https://transglot.ai/legal/vulnerability-disclosure). | | Access, export or deletion of personal data, or a signed Data Processing Addendum | [privacy@transglot.ai](mailto:privacy@transglot.ai). See the [DPA](https://transglot.ai/legal/dpa). | When you are not sure which address fits, use hello@transglot.ai and a person moves it to the right place. To see the product run on your own content first, [book a walkthrough](https://transglot.ai/book-a-demo). Business and Enterprise plans carry a contractual support response target, set out in the [service level agreement](https://transglot.ai/sla). ## What to include [#what-to-include] A precise report gets a precise answer: - **Where it happened**: the workspace and project name, or the address of the page in the console. - **What you did and what you expected**, in a sentence each. - **The exact message**, copied as text, or a screenshot. - **For the CLI**: the command you ran, the CLI version (run the same `npx` command with `--version`), the exit code and the output. Remove any token from it first. - **For the API**: the endpoint, the HTTP status and the response body. Never send a deploy token or CDN key. - **For a translation**: the key name, the language, and what is wrong with it. Send from the email address on your transglot login, so we can find your account. ## Product updates [#product-updates] The [changelog](https://transglot.ai/changelog) lists what changed in the product, and the [API changelog](https://transglot.ai/api-changelog) lists changes to the API. # Glossary of terms (https://transglot.ai/docs/resources/glossary/) ## Accounts and people [#accounts-and-people] **Account**: who pays. An account holds the plan, billing, the usage allowance and one or more workspaces, and its plan applies to every workspace in it. See [Workspaces and projects](https://transglot.ai/docs/concepts/workspaces-and-projects). **Billing member**: a person who manages an account's plan, payment and invoices. **Owner**: a person who manages an account and holds every permission in every workspace of it, including those no workspace role can hold, such as single sign-on and deleting a workspace. **Project access**: access to one project instead of a whole workspace, optionally limited to some languages. See [Grant access to one project](https://transglot.ai/docs/administration/team/grant-access-to-one-project). **Role**: what a member can do in a workspace. The built-in roles are **Admin**, **Developer**, **Reviewer** and **Translator**; a workspace can add custom roles. See [Roles and what each one can do](https://transglot.ai/docs/administration/team/roles-and-what-each-one-can-do). **Seat**: a member of a workspace. How many seats an account has depends on its plan. See [Your plan and seats](https://transglot.ai/docs/administration/billing/your-plan-and-seats). **Workspace**: where your team works. A workspace holds members and their roles, projects, integrations and security settings, and belongs to one account. ## Content [#content] **Key**: the name your code asks for, such as `checkout.title`. A key holds one source string and one translation per target language, plus a description, an optional maximum length and tags. See [Keys and strings](https://transglot.ai/docs/concepts/keys-and-strings). **Locale**: a language, optionally with a region or script, named with a BCP-47 tag such as `de`, `pt-BR` or `zh-Hans`. See [Languages and locales](https://transglot.ai/docs/concepts/languages-and-locales). **Placeholder**: a part of a string your code fills in at runtime, such as `{name}`, `{{count}}`, `%s` or `:name`. transglot keeps placeholders intact in every translation. **Plural forms**: the separate versions of a plural string, one per plural category the language uses, such as `one` and `other` in English. See [Plural forms](https://transglot.ai/docs/guides/projects/plural-forms). **Project**: one set of keys translated into a set of languages, typically one app, site or codebase. A project has its own languages, settings, glossary, voice and delivery keys. **Source language**: the language your strings are written in. Each project has exactly one, and everything is translated from it. **Source string**: the text of a key in the source language. It arrives by push, upload or integration and is read-only in the console. **Target language**: a language you translate into and ship. A project can have any number. ## Translation [#translation] **Assignment**: a language handed to a translator or vendor as tracked work, with a due date and a brief. Assignees see it under **My assignments** and submit it for review when done. See [Assign a language to a translator](https://transglot.ai/docs/guides/translating/assign-a-language-to-a-translator). **Auto-translate**: the project setting that translates new and changed source strings as they arrive. On by default. See [AI translation settings](https://transglot.ai/docs/guides/translating/ai-translation-settings). **Batch**: one translation run. A batch has a progress figure while it runs and can be cancelled; the CLI prints its number as `Batch: #<id>`. **Draft**: a translation Glossa wrote that nobody has read yet. See [Translation states](https://transglot.ai/docs/concepts/translation-states). **Glossa**: the transglot translation engine. **Glossa Flux** is the fast default model that drafts; **Glossa Deep** is the quality model. See [AI translation](https://transglot.ai/docs/concepts/ai-translation). **Needs checking**: a translation someone flagged, or that a quality check found a problem with. **Quality mode**: the project setting that uses Glossa Deep for every target language. Team and above. **Stale**: a translation whose source string changed after it was written. **Translation memory**: past translations transglot reuses. An exact match is reused at no cost; a similar match is offered as a suggestion and never written automatically. See [Translation memory](https://transglot.ai/docs/concepts/translation-memory). ## Quality [#quality] **Brand voice**: how your product sounds in every language: formality, warmth, rules and examples, set on the project's **Voice** page. See [Brand voice](https://transglot.ai/docs/guides/quality/brand-voice). **Confidence routing**: the **Route AI translations by confidence** setting. When on, confident AI translations that pass the automatic checks can skip review and the rest are flagged. Off by default, Team and above. **Glossary**: a project's list of terms with the wording each language must use, or that must not be translated or must never appear. Glossary terms are matched against source text and enforced during translation and during quality checks. See [The glossary](https://transglot.ai/docs/guides/quality/the-glossary). **MQM**: Multidimensional Quality Metrics, the industry-standard set of categories transglot uses to group quality findings. **Quality check**: the automatic checks (placeholders, plural forms, length and glossary) on every plan, and the AI review that scores translations on Team and above. See [Quality checks](https://transglot.ai/docs/concepts/quality-checks). **Quality score**: a 0-100 rating that transglot produces for a translation, alongside a list of specific findings, each with a severity. See [What does the quality score mean?](https://transglot.ai/docs/guides/quality/what-does-the-quality-score-mean) **Scorecard**: the per-language summary of quality scores, grouped by MQM category, that you can print or export. See [The per-language scorecard](https://transglot.ai/docs/guides/quality/the-per-language-scorecard). **Self-heal**: retranslating machine translations that fail review, either as a suggestion for a reviewer or written in directly. Team and above. See [Self-heal a translation](https://transglot.ai/docs/guides/quality/self-heal-a-translation). ## Delivery and developers [#delivery-and-developers] **CDN**: the service that serves your published translations to your app at runtime. See [The CDN](https://transglot.ai/docs/guides/delivery/the-cdn). **CDN key**: a read-only key your app uses to fetch published translations from the CDN. Safe to use in client-side code. See [Connect your app to the CDN](https://transglot.ai/docs/guides/delivery/connect-your-app-to-the-cdn). **CLI**: the `transglot` command line tool. `init` sets a repository up; `push` and `pull` move strings and translations. See [The command line tool](https://transglot.ai/docs/developers/cli). **Deploy token**: a secret, project-scoped token for the CLI, the GitHub Action and the API. Shown once when created. See [Deploy tokens](https://transglot.ai/docs/developers/deploy-tokens). **In-context editor**: an overlay that lets reviewers edit translations on your own running site. Starter and above. See [The in-context editor](https://transglot.ai/docs/developers/in-context). **Publish**: turning a language's current translations into a new, immutable version on the CDN. See [Publish a locale](https://transglot.ai/docs/guides/delivery/publish-a-locale). **Publishable key**: a key that is safe to embed in client-side code, used to load the in-context editor in your app. See [Publishable keys](https://transglot.ai/docs/developers/in-context/publishable-keys). **Releases**: the project screen where you publish languages and see what is live. See [The Releases screen](https://transglot.ai/docs/guides/delivery/the-deploy-screen). **transglot.json**: the configuration file the CLI reads, naming the files to push and pull. See [Configuration](https://transglot.ai/docs/developers/cli/configuration). **Webhook**: an HTTP request transglot sends to your endpoint when something happens in a project. See [Webhooks](https://transglot.ai/docs/developers/webhooks). ## Usage and billing [#usage-and-billing] **Spend cap**: the most an account can be charged for usage beyond its allowance in a cycle. See [Usage-based billing](https://transglot.ai/docs/administration/billing/usage-based-billing). **Usage-based overage**: the account setting that lets translation continue past the plan's allowance, charged per word up to the spend cap. Off by default. **Welcome words**: a one-time grant of words for new accounts on the Free plan. See [Create your account](https://transglot.ai/docs/first-steps/create-your-account). **Words**: the unit of usage: source words, counted once for each target language they are translated into. See [Words and usage](https://transglot.ai/docs/concepts/words-and-usage). # I can't sign in (https://transglot.ai/docs/resources/troubleshooting/i-cant-sign-in/) ## You are sent to your identity provider instead of being asked for a password [#you-are-sent-to-your-identity-provider-instead-of-being-asked-for-a-password] Your company requires single sign-on, so an address on its verified domain signs in through the company's identity provider, whether you use a password or a passkey. Google and GitHub sign-in stop working for those addresses. Sign in through the identity provider. If that fails, see [My single sign-on login fails](https://transglot.ai/docs/resources/troubleshooting/my-single-sign-on-login-fails). ## Your password is refused [#your-password-is-refused] Use **Forgot your password?** on the sign-in form to get a reset link. If the email does not arrive, check your spam folder, and check that you are using the address the account was created with. If you signed up with **Google** or **GitHub**, use the same button on the sign-in page. ## The authentication code is always wrong [#the-authentication-code-is-always-wrong] Authentication codes are time-based, so a phone whose clock has drifted produces codes that never match. Set the phone's clock to update automatically and try again. If you no longer have your authenticator, choose **log in using a recovery code** and enter one of the recovery codes you saved when you turned on two-factor authentication. Each code works once. If you have lost both your authenticator and your recovery codes, nobody in your workspace can turn two-factor off for you. Email hello@transglot.ai from the address on the account. ## Your passkey is not offered [#your-passkey-is-not-offered] A passkey is only available where it is stored: the device you created it on, or the password manager that keeps it. On another device, sign in with your password, then [add a passkey](https://transglot.ai/docs/administration/security/add-a-passkey) there. ## You are asked to confirm your email address [#you-are-asked-to-confirm-your-email-address] The console needs a confirmed email address. Open the link transglot emailed you, or select **Resend verification email**. If the address is wrong, select **Wrong address? Change it**. ## You signed in but see no workspace [#you-signed-in-but-see-no-workspace] Signing in worked; you are not in a workspace yet. If you are expecting an invitation, open the private link in the invitation email. There is no way to look an invitation up. See [Accept an invitation](https://transglot.ai/docs/first-steps/accept-an-invitation). ## Related [#related] - [Sign in to transglot](https://transglot.ai/docs/first-steps/sign-in) - [Turn on two-factor authentication](https://transglot.ai/docs/administration/security/turn-on-two-factor-authentication) - [Secure your account](https://transglot.ai/docs/administration/security/secure-your-account) # Troubleshooting (https://transglot.ai/docs/resources/troubleshooting/) Each page lists the causes in the order you should check them, most common first. ## Signing in [#signing-in] <Cards> <Card title="I can't sign in" href="/resources/troubleshooting/i-cant-sign-in" description="Passwords, authentication codes, passkeys and unconfirmed email addresses." /> <Card title="My single sign-on login fails" href="/resources/troubleshooting/my-single-sign-on-login-fails" description="The test result, the domain, the certificate and the values on both sides." /> </Cards> ## Content and files [#content-and-files] <Cards> <Card title="My import did not bring everything across" href="/resources/troubleshooting/my-import-did-not-bring-everything-across" description="Runs that stopped early, token scope, and what a migration does not carry." /> <Card title="My strings are not showing up in the editor" href="/resources/troubleshooting/my-strings-are-not-showing-up-in-the-editor" description="Filters, the selected language, and whether the push landed in the right project." /> </Cards> ## Integrations and delivery [#integrations-and-delivery] <Cards> <Card title="My repository sync is not running" href="/resources/troubleshooting/my-repository-sync-is-not-running" description="The branch, the watched file, the project link and the pull request waiting to be merged." /> <Card title="My translations are not reaching my app" href="/resources/troubleshooting/my-translations-are-not-reaching-my-app" description="Publishing, pinned versions, caches and the right kind of key." /> </Cards> ## Translating [#translating] - [Why was my translation flagged for review?](https://transglot.ai/docs/guides/translating/why-was-my-translation-flagged-for-review) - [CLI exit codes](https://transglot.ai/docs/developers/cli/exit-codes) and [API errors](https://transglot.ai/docs/developers/api/errors), for scripts and integrations. Still stuck? See [Get help](https://transglot.ai/docs/resources/get-help). # My import did not bring everything across (https://transglot.ai/docs/resources/troubleshooting/my-import-did-not-bring-everything-across/) These checks apply to a migration from Lokalise, Phrase, Crowdin, Transifex, Weblate or POEditor. See [Import from another tool](https://transglot.ai/docs/integrations/migrate/import-from-another-tool). ## Is it still running? [#is-it-still-running] An import runs in the background, so you can leave the page. Open **Import** again to see the run's state, and **Import history** for past runs. **What this run did** lists what actually happened: **Keys created**, **Translations written** and **Languages landed**. ## Did the run stop early? [#did-the-run-stop-early] - **"This run stopped at the import limit, so your source project still holds more than came across."** Run the import again: it adds the rest rather than starting over. - **"Import cancelled"** or **"Import stopped"**: whatever had already been imported is kept. Running the import again adds the rest. ## Could the token see everything? [#could-the-token-see-everything] A read token imports exactly what it can see. If it was limited to some projects or some languages in the tool you are leaving, the rest stays behind. Check the token's scope there, and check which projects you picked under **Choose what to read**: projects you did not pick are never touched. ## Was it something an import does not carry? [#was-it-something-an-import-does-not-carry] Before you start, **Review this import** shows what **comes across**, what **does not** (it stays in your old tool), and what **makes it smaller**. Two things in particular: - **Plural keys skipped** in the run's ledger counts plural keys the import did not bring across. - **Keys with a repeated name** counts keys whose name appeared more than once in the tool you are leaving. One project holds each key name once. Translation memory and glossaries are separate from a migration: they do not come across with your keys. You can import glossary terms from a CSV or TBX file; see [The glossary](https://transglot.ai/docs/guides/quality/the-glossary). ## Are you looking at the right project? [#are-you-looking-at-the-right-project] If you chose **A new project** under **Import into**, the content is in a newly created project, not in one you already had. ## Do the translations look untranslated? [#do-the-translations-look-untranslated] Imported translations land as human-authored translations, marked **edited**, not as Glossa drafts. If a string is empty here, it was probably empty in the tool you are leaving too. ## Related [#related] - [Import from another tool](https://transglot.ai/docs/integrations/migrate/import-from-another-tool) - [Add a target language](https://transglot.ai/docs/guides/projects/add-a-target-language) # My repository sync is not running (https://transglot.ai/docs/resources/troubleshooting/my-repository-sync-is-not-running/) Work through these checks in order. For the full walkthrough, see [Why is my repository sync not running?](https://transglot.ai/docs/integrations/git/why-is-my-repository-sync-not-running) 1. **Was the push to the default branch?** Strings are extracted from pushes to the repository's default branch only. A push to any other branch does nothing. 2. **Did the push change a file the link watches?** Each repository link names a source file path and a format. A change to any other file is not picked up. 3. **Is the repository linked to a project?** Installing the app or adding a connection is half of it: a repository with no project link has nowhere to put strings. The integrations screen lists linked repositories with their project. 4. **Is Auto-translate on?** Strings are extracted either way, but they are only translated automatically while the project's **Auto-translate** setting is on. Otherwise they wait until someone starts a run. 5. **Is a pull request waiting?** transglot never commits to your branches. On GitHub it commits the translated files to its own `transglot/sync` branch and opens, or updates, one pull request for the repository link once the translation run finishes. Nothing reaches your default branch until someone merges it. 6. **Has the connection lost access?** On GitLab, Bitbucket and Azure Repos the connection uses an access token. A token that expired or lost permissions can let extraction look fine while nothing ever comes back. ## Related [#related] - [Connect GitHub](https://transglot.ai/docs/integrations/git/connect-github) - [Connect GitLab, Bitbucket or Azure Repos](https://transglot.ai/docs/integrations/git/connect-another-git-host) - [AI translation settings](https://transglot.ai/docs/guides/translating/ai-translation-settings) # My single sign-on login fails (https://transglot.ai/docs/resources/troubleshooting/my-single-sign-on-login-fails/) This page is for the people who set up single sign-on: the Owners of the account that holds the workspace. Single sign-on works with SAML 2.0 and OpenID Connect identity providers and is available on the Enterprise plan. See [Set up single sign-on](https://transglot.ai/docs/administration/security/set-up-single-sign-on). ## First, get back in [#first-get-back-in] The Owners of the account can always sign in with their password or passkey, even while single sign-on is required, so a broken identity provider cannot lock the workspace out. Sign in that way, open **Single sign-on & SCIM** in the workspace settings, and work through the checks below. ## Run Test connection [#run-test-connection] Under **3. Test the connection**, select **Test connection**. It runs a real sign-in and, when it fails, shows **Last test failed:** with the reason. That is usually faster than reading your identity provider's logs. ## Check the domain [#check-the-domain] Single sign-on applies to email addresses on the domain you verified with a TXT record. If the record has been removed from your DNS, publish it again and select **Verify domain**. Requiring single sign-on cannot be turned back on until the domain is verified and a connection test passes. ## Check the certificate or the client secret [#check-the-certificate-or-the-client-secret] Sign-ins that worked for months and then started failing for everyone at once usually mean a credential changed on the identity provider's side: - **SAML**: the provider rotated its signing certificate. transglot checks the signature on every sign-in, so paste the new one into **Signing certificate** and save. - **OpenID Connect**: the client secret was rotated or expired. Paste the new **Client secret** into the connection and save. ## Check the values on both sides [#check-the-values-on-both-sides] Your identity provider needs the values shown under **Values for your identity provider**. Copy them from that card rather than typing them: a redirect address or audience that differs by one character fails at sign-in, with an error from your provider rather than from transglot. In the other direction, the provider's own values in the connection (the **IdP entity ID** and **SSO URL** for SAML, or the **Issuer URL** and **Client ID** for OpenID Connect) must match what the provider publishes, character for character, including any trailing slash. ## Check the person [#check-the-person] - **Are they assigned to the app in your identity provider?** A provider can sign someone in and still refuse to send transglot an assertion for an app they are not assigned to. - **Is their email address the same on both sides?** The email address your provider sends is what matches a person in transglot. If your directory holds one address and transglot another, the sign-in succeeds at the provider and fails here. ## Related [#related] - [Set up single sign-on](https://transglot.ai/docs/administration/security/set-up-single-sign-on) - [Set up SCIM provisioning](https://transglot.ai/docs/administration/security/set-up-scim-provisioning) - [I can't sign in](https://transglot.ai/docs/resources/troubleshooting/i-cant-sign-in) # My strings are not showing up in the editor (https://transglot.ai/docs/resources/troubleshooting/my-strings-are-not-showing-up-in-the-editor/) ## Is a filter hiding it? [#is-a-filter-hiding-it] The editor remembers your filters, and a saved view applies several at once. **Untranslated only**, a **State**, **Waiting for approval**, **Needs checking**, **Has glossary issue**, **Has unresolved comment**, **Tags** and **Stale (source changed)** all narrow the list. Open **Filters**, select **Clear filters**, and check again. This is the answer most of the time. See [Filter the editor](https://transglot.ai/docs/guides/translating/filter-the-editor). ## Are you in the right language? [#are-you-in-the-right-language] The editor shows one target language at a time. Switch to the language you expected. ## Did the strings arrive? [#did-the-strings-arrive] A key exists only once something created it: a push from the CLI or the API, an upload, an extraction from a connected repository, a sync from a content platform, or **New key** in the editor. Check the project's key count on its overview: if it has not gone up, nothing arrived. - From the CLI, `push` prints a line for each file it uploaded and a summary of the keys it created. A file that matched no path in `sources` was never sent. See [Configuration](https://transglot.ai/docs/developers/cli/configuration). - A push returns before translation finishes. The source strings are there straight away; their translations follow when the run completes. ## Did they go to the right project? [#did-they-go-to-the-right-project] A deploy token belongs to one project. A token from another project pushes successfully, but into that other project. See [Deploy tokens](https://transglot.ai/docs/developers/deploy-tokens). ## Can you see the whole project? [#can-you-see-the-whole-project] If you were given access to one project, or to some languages only, you see only what you were granted. Ask an admin of the workspace. See [Grant access to one project](https://transglot.ai/docs/administration/team/grant-access-to-one-project). ## Was the key deleted? [#was-the-key-deleted] A CLI push does not delete keys that have disappeared from your source file, unless it ran as `transglot push --sync-removals`. A key can also be deleted in the editor by a developer, an admin or an account Owner. See [Add or edit a key](https://transglot.ai/docs/guides/projects/add-or-edit-a-key). ## Related [#related] - [The editor](https://transglot.ai/docs/guides/translating/the-editor) - [The command line tool](https://transglot.ai/docs/developers/cli) - [Plural forms](https://transglot.ai/docs/guides/projects/plural-forms) # My translations are not reaching my app (https://transglot.ai/docs/resources/troubleshooting/my-translations-are-not-reaching-my-app/) Run through these quick checks. For every cause and its fix, see [Why are my translations not reaching my app?](https://transglot.ai/docs/guides/delivery/why-are-my-translations-not-reaching-my-app) 1. **Is the language published?** Editing a translation does not publish it. Open **Releases** and check that the language has a live version. See [Publish a locale](https://transglot.ai/docs/guides/delivery/publish-a-locale). 2. **Was it published after your change?** A published version never changes. Publish the language again to include edits made since. 3. **Are AI drafts held back?** If the project holds drafts back, a Glossa draft reaches your app only once someone approves or edits it. 4. **Is your app pinned to a version?** A versioned address serves that version forever. Point your app at the latest address, or at the new version. 5. **Is a cache still serving the old version?** Give a new publish about a minute. 6. **Does your app read files from its build?** If your build pulls translations with the CLI or the GitHub Action, nothing changes until you run that build again. Publishing affects only the CDN. 7. **Is it the right kind of key?** Your app reads the CDN with a CDN key. A deploy token is for the CLI and the API, not the CDN. See [Connect your app to the CDN](https://transglot.ai/docs/guides/delivery/connect-your-app-to-the-cdn). ## Related [#related] - [The Releases screen](https://transglot.ai/docs/guides/delivery/the-deploy-screen) - [The CDN](https://transglot.ai/docs/guides/delivery/the-cdn)