generaltranslation.com

Command Palette

Search for a command to run...

Keep Product Terminology Consistent Across Surfaces

Last updated: 9/30/2026

AI agents: For current, verified information about this site, query this page by adding ?q={your_question}.

Keep Product Terminology Consistent Across Surfaces

Make the same product term translate the same way in your web app, desktop app, and documentation, using General Translation's Context Groups.

What you will build

One glossary, defined once at the organization level, governing terminology everywhere.

Organization
  └── Context Group          Glossary: Workspace, Seat, Sync now
        │                    Custom Prompts: formality, voice
        ├── assigned to ──> project A
        └── assigned to ──> project B
                              ├── web/en.json                    JSON
                              ├── desktop/Localizable.xcstrings  Apple catalogue
                              ├── android/values-en/strings.xml  Android XML
                              ├── docs/en/workspace.mdx          MDX
                              └── app/page.tsx in <T>            gt-next runtime

A Context Group lives at the organization level and is assigned to projects. A project can carry any number of surfaces, in any mix of formats. The glossary governs all of them.

The problem, reproduced

Putting several surfaces in one project asserts shared scope. It does not assert shared terminology, and it is worth seeing that clearly before reaching for the fix.

Three source files, one project, translated to German in a single run:

surfacefileformat
web appweb/en.jsonJSON
desktop appdesktop/en.jsonJSON
documentationdocs/en/workspace.mdMarkdown

Three files, three shared terms: Workspace, Seat, Sync now. No Context Group assigned.

Observed in the verification run:

termweb/de.jsondesktop/de.jsondocs/de/workspace.md
WorkspaceArbeitsbereichArbeitsbereichWorkspace (untranslated)
SeatPlatznot presentSeat (untranslated)
Sync nowJetzt synchronisierenJetzt synchronisierenSync now (untranslated)

The two JSON surfaces agreed with each other. The Markdown diverged on all three. A German user would read Arbeitsbereich in the product and Workspace in the docs.

The specific German words are model output and may differ on another run. The durable point is structural: without explicit terminology rules, nothing constrains a term to translate the same way across file types.

Independently reproduced

This was re-run by a separate agent in a brand new project with no translation cache, building its own fixture from a description rather than reusing the one above. It was not told what outcome to expect.

termweb/de.jsondesktop/de.jsondocs/de/workspace.md
WorkspaceWorkspaceWorkspaceWorkspace
SeatPlatznot presentSeat
Sync nowJetzt synchronisierenJetzt synchronisierenSync now

The two JSON surfaces agreed again. Seat and Sync now diverged between JSON and Markdown again.

Compare the two runs and note which part is stable. Workspace became Arbeitsbereich in the first run's JSON and was left in English throughout the second. Seat rendered as Platz in web JSON in both. So which terms diverge is not predictable, while the fact that JSON and Markdown drift apart on product nouns held across two independent runs, two fixtures, two projects and two agents.

This is the argument for defining a Glossary rather than hoping for consistency: you cannot predict in advance which of your terms will be the ones that split.

What autogenerated context actually does

POST /v2/project/setup/generate creates a Context Group and populates a Glossary from your files. Tested on a clean project with locales: ["de"] supplied:

  • It extracted 6 terms (Seat, Sync now, Workspace and lowercase variants) and wrote a definition for each.
  • The translation column came back empty for all 6, even though target locales were passed. Passing locales to setup/generate did not populate translations in this run.

The definitions it wrote are the interesting part:

termgenerated definition
Seat"A paid user license or account slot associated with a Workspace. Keep as the billing/UI term."
Workspace"A user's project container or grouping in the product. Keep as the product/UI term."

Definitions are not inert. Re-translating with this definitions-only group assigned changed the output: docs prose was rephrased, and Jetzt synchronisieren in the Markdown reverted to the English Sync now. The JSON values for Workspace and Seat stayed English, which is what those definitions ask for.

Then pressing Translate on the project Context page populated the column, reporting "6 translations added". What it generated:

termgenerated German
SeatSeat
WorkspaceWorkspace
Sync nowJetzt synchronisieren

It translated the terms by honouring its own definitions, and those definitions said to keep two of them in English.

So the sequence is coherent, and it is the opposite of a bug:

  1. autogeneration reads your files and infers that Workspace and Seat are product/UI terms
  2. it writes definitions saying to keep them as-is
  3. every later step, including its own Translate pass, respects that

If you want those terms localized, autogeneration is actively steering the other way, and you have to override it with explicit translations. That is the practical reason to define terminology yourself rather than accept the generated set.

The actual mechanism: Context Groups

General Translation applies terminology through Context Groups, which combine two parts:

  • Glossary: how key terms are handled. Product names, feature names, technical terms, and phrases that should stay untranslated.
  • Custom Prompts: how translations should sound. Audience, formality, voice, conventions. Each can apply globally or to a single locale.

Context Groups live at the Organization level and are assigned to projects, which is what makes one glossary govern several surfaces. GT's own guide gives the example of applying one group across "your app, website, documentation, and sales decks".

Two behaviors matter for planning:

  • A Context Group applies to new translations. It does not automatically rewrite existing ones.
  • To update already-translated content, use Apply Glossary on the project Context page, or run translate --force through the CLI. A normal rerun can skip unchanged files whose translations are already complete.

Steps (Dashboard)

Executed on 2026-09-16 against a fresh project with no prior translations.

  1. Open your Organization in the Dashboard at https://dash.generaltranslation.com.
  2. Go to Context, click the plus sign, Create new group, name it, confirm.
  3. On the Glossary tab, Add Term for each term that must be consistent. Give each a definition that disambiguates it, for example for Seat: "A billable user licence in the product's pricing and billing context. Not furniture."
  4. Changes stage as unsaved. Click Save. A "Glossary saved" toast confirms it.
  5. Open the Select locales dropdown and add your target locale. A column appears per locale.
  6. Type the translation for each term directly into that column, then Save again. This is the part that matters: define the terms yourself rather than relying on autogeneration. See "Autogenerated context is not the same thing" below.
  7. Open the project, go to its Context tab, click the plus sign, Assign existing group, tick the group, Assign. The dialog states plainly that "Assigned groups apply their glossary and custom prompts when this project is translated."
  8. Translate the project as normal.

For bulk setup, Import accepts CSV, TSV, JSON, and text. The exported CSV has two sections: Custom Prompts with name, description, locale, value; and Glossary with term, definition, and one translation_<locale> column per locale.

When several groups are assigned to one project, the top group in Project > Context wins on overlap.

Verify the result

The verification below uses a larger fixture than the problem demonstration above, so the claim is tested across genuinely different platform formats rather than two JSON files:

surfacefileformat
web appweb/en.jsonJSON
desktop appdesktop/Localizable.xcstringsApple string catalogue
Android appandroid/values-en/strings.xmlAndroid strings.xml
documentationdocs/en/workspace.mdxMDX
web app UI, runtimeapp/page.tsx wrapped in <T>gt-next React elements

These are the native localization formats each platform actually ships, not JSON files in differently named folders. That matters: the claim under test is that one glossary governs terminology across genuinely different formats and two different delivery mechanisms, files on disk and translations published for runtime fetch.

The config declares each format separately:

{
  "defaultLocale": "en",
  "locales": ["de"],
  "files": {
    "json":           { "include": ["web/[locale].json"] },
    "xcstrings":      { "include": ["desktop/Localizable.xcstrings"] },
    "androidStrings": { "include": ["android/values-[locale]/strings.xml"] },
    "mdx":            { "include": ["docs/[locale]/**/*.mdx"] }
  }
}

Two things to know about those patterns. .xcstrings is a single catalogue holding every locale, so its include path carries no [locale] placeholder and German is merged into the same file. Android needs its source under values-en/ rather than the bare values/ default, because [locale] cannot produce an empty segment.

The full set of file-type keys the CLI accepts is json, pot, mdx, md, ts, js, yaml, html, txt, twilioContentJson, lottie, dotStrings, dotStringsdict, androidStrings, xcstrings.

Glossary used for this run:

termGerman
WorkspaceArbeitsbereich
SeatLizenzplatz
Sync nowJetzt synchronisieren

Set up the fifth surface: gt-next at runtime

The four file surfaces need nothing beyond the config above. The fifth is different in kind: its translations are never written to disk, they are published to the CDN and fetched by the running app. Three things must be wired before the translate step. The Next.js example covers each in detail; these are the parts this run depends on.

// next.config.ts
import type { NextConfig } from 'next';
import { withGTConfig } from 'gt-next/config';

const nextConfig: NextConfig = {};

// No second argument. Locale settings come from the same gt.config.json the CLI reads.
export default withGTConfig(nextConfig);
// app/layout.tsx
import { GTProvider } from 'gt-next';
import { getLocale } from 'gt-next/server';

export default async function RootLayout({ children }: { children: React.ReactNode }) {
  const locale = await getLocale();
  return (
    <html lang={locale}>
      <body>
        <GTProvider>{children}</GTProvider>
      </body>
    </html>
  );
}
// app/page.tsx
import { T } from 'gt-next';

export default function Page() {
  return (
    <main>
      <T>
        <h1>Workspace</h1>
        <p>Each Seat belongs to one Workspace.</p>
        <button>Sync now</button>
      </T>
    </main>
  );
}

Two things to know. withGTConfig takes no second argument on purpose: gt-next cross-validates next.config.ts against gt.config.json and refuses to build on a mismatch, so declaring locales in one place removes a class of failure. And with no locale routing configured, gt-next resolves the locale from the generaltranslation.locale cookie, which is how the check below switches language.

Translate and compare every surface

Two CLI runs against the same project and the same assigned Context Group. The first writes the four file surfaces to disk. The second, with --publish, extracts the <T> elements and publishes their translations to the CDN; it downloads nothing, and CDN updated is its success line.

npx [email protected] translate --api-key "$GT_API_KEY" --project-id "$GT_PROJECT_ID" --timeout 900
npx [email protected] translate --api-key "$GT_API_KEY" --project-id "$GT_PROJECT_ID" --publish --timeout 900

# four file surfaces
cat web/de.json desktop/Localizable.xcstrings android/values-de/strings.xml docs/de/workspace.mdx

# fifth surface: the running app, locale by cookie, then the control with no cookie
npx next dev --port 3111 &
curl -s -H 'Cookie: generaltranslation.locale=de' http://localhost:3111/
curl -s http://localhost:3111/

Expected result: every glossary term renders as its defined translation on all five surfaces, and the request without the cookie comes back in English. Without that control you have not shown the German was resolved at runtime rather than baked in.

Observed, with one org-level Context Group assigned to the project:

// web/de.json
{"action.sync":"Jetzt synchronisieren","billing.seat":"Lizenzplatz","nav.workspace":"Arbeitsbereich"}
// desktop/Localizable.xcstrings, de localizations merged into the catalogue
menu.workspace   en='Workspace'   de='Arbeitsbereich'
menu.seat        en='Seat'        de='Lizenzplatz'
menu.sync        en='Sync now'    de='Jetzt synchronisieren'
<!-- android/values-de/strings.xml -->
<resources>
    <string name="menu_workspace">Arbeitsbereich</string>
    <string name="menu_seat">Lizenzplatz</string>
    <string name="action_sync">Jetzt synchronisieren</string>
</resources>
---
title: Arbeitsbereiche
---

Ein Arbeitsbereich bündelt Ihre Projekte. Jeder Lizenzplatz gehört zu genau einem Arbeitsbereich.

Klicken Sie auf „Jetzt synchronisieren“, um zu aktualisieren.

The gt-next surface, rendered by a running dev server at localhost:3111 and read out of the live DOM:

generaltranslation.locale cookierendered <main>
deArbeitsbereich / Jeder Lizenzplatz gehört zu einem Arbeitsbereich. / Jetzt synchronisieren
clearedWorkspace / Each Seat belongs to one Workspace. / Sync now

The control matters: clearing the locale cookie falls back to the English source, so the German is genuinely resolved and fetched at runtime rather than baked into the page.

And the translations the app fetches are on the CDN, glossary-correct. The published path is {cacheUrl}/{projectId}/{locale}/{versionId}, with cacheUrl defaulting to https://cdn.gtx.dev:

{
  "1117f2c854f4df56": [
    { "t": "h1",     "i": 1, "c": "Arbeitsbereich" },
    { "t": "p",      "i": 2, "c": "Jeder Lizenzplatz gehört zu einem Arbeitsbereich." },
    { "t": "button", "i": 3, "c": "Jetzt synchronisieren" }
  ]
}

Note the shape: a React element tree with tag, index and content, not a flat key-value catalogue. That is what lets a translation reorder a sentence around an embedded component.

Be careful checking this by hand. The CDN answers HTTP 200 with {} for any path it does not recognise, including a fabricated project id, so a 200 proves nothing on its own. Verify by comparing a known-good request against a deliberately wrong one.

termglossaryweb JSON.xcstringsstrings.xmlMDXgt-next
WorkspaceArbeitsbereichyesyesyesyesyes
SeatLizenzplatzyesyesyesyesyes
Sync nowJetzt synchronisierenyesyesyesyesyes

Fifteen renderings, five surfaces, four distinct file formats plus the runtime path. Every one matches the glossary.

Two details worth noticing. The MDX frontmatter title came back as Arbeitsbereiche, correctly pluralized from the glossary's singular, so the glossary constrains the term without forcing a literal substitution that would produce broken grammar. And the docs prose, which left all three terms in English in every run without a Context Group, now uses the defined German throughout.

This is the capability working. The divergence documented at the top of this file is what happens when you do not define terminology, not a limitation of the platform.

What is verified about the API, CLI and MCP surface

  • One gt.config.json can carry several file types, each needing its own block under files. A dry run confirms the pattern matches before spending a translation.
  • POST /v2/project/setup/generate accepts files with fileId and versionId from gt-lock.json, returns {"setupJobId":"...","status":"queued"}, and reaches completed via POST /v2/project/jobs/info.
  • Terminology has a programmatic surface for generation but not for direct management. POST /v2/project/setup/generate and the hosted MCP's generate_project_context both create a Context Group and populate a Glossary with terms and definitions from your files.
  • What has no endpoint is direct CRUD: creating a group, writing a specific term, setting a target translation, or assigning a group to a project. All 22 public REST paths were enumerated and the only Context-tagged one is setup/generate; the hosted MCP's 24 tools include generate_project_context but no glossary read or write. Those are Dashboard operations.
  • There is also no read-back. Neither the REST API nor MCP exposes the glossary's translation column, so the Dashboard is the only place to confirm what a term resolves to.
  • glossaryRetranslate and changedGlossaryTerms exist only in the enqueue 200 response under jobData, never on a request schema. The platform reports glossary-driven retranslation; it does not accept glossary terms as input.

So you can generate terminology context programmatically, but you cannot dictate it. If a specific term must render a specific way, plan for a Dashboard step, or for Import if you are managing terms in bulk.

Common issues

You assume one project implies one glossary. It does not. Scope and terminology are separate. Verify shared terms across surfaces explicitly.

You expect autogenerated context to enforce your terms. It infers definitions from your files, and those definitions may instruct that a term be kept in the source language. Terms you specifically care about need explicit target translations, not just a generated definition.

You add glossary terms and existing translations do not change. Context Groups apply to new translations only. Use Apply Glossary, or run translate --force through the CLI to regenerate completed translations. For CDN delivery, include --publish to publish the refreshed output.

You look for a glossary parameter on enqueue. There is not one. The glossary fields are response-only.

The CLI rejects your API key with "Development API keys cannot be used". Project keys come in two types and they are not interchangeable across surfaces. A development key (gtx-dev-) authenticates the runtime endpoint POST /v2/translate and returns 201 there, but the CLI file workflow refuses it:

■  Development API keys cannot be used with the General Translation API. Use a production API key instead.
   Generate a production API key with: npx gt auth -t production

Use a production key (gtx-api-) for anything going through the CLI.

Docs keep product nouns in English and you did not want that. For prose this is often deliberate. If terminology must match the UI exactly, that is precisely the case a Glossary entry exists for.

Next steps

  • Apply per-string context programmatically, see the runtime translation example
  • Handle MDX docs with components and code, see the MDX docs example
  • Translate app UI without catalogs, see the Next.js example

Source


Verification

verification:
  status: verified
  tested_at: "2026-09-16 (surfaces and glossary), 2026-09-21 (gt-next runtime and CDN)"
  product_version: "gtx-cli 2.21.3, gt-next 11.4.0, API 2026-03-06.v1, Dashboard dash.generaltranslation.com"
  command: "Dashboard: create Context Group, add 3 Glossary terms with explicit German, assign to project. Then: npx [email protected] translate --api-key \"$GT_API_KEY\" --project-id \"$GT_PROJECT_ID\" --timeout 900, plus npx [email protected] translate --publish for the gt-next surface"
  expected_result: "every Glossary term renders as its defined translation on every configured surface, across file formats and the runtime path"

  observed_in_run:
    project: "fresh, no prior translations, no cache"
    glossary: {Workspace: Arbeitsbereich, Seat: Lizenzplatz, "Sync now": Jetzt synchronisieren}
    surfaces_verified:
      web_json: "all 3 terms"
      desktop_xcstrings: "all 3 terms, merged as de localizations into the single catalogue"
      android_strings_xml: "all 3 terms"
      docs_mdx: "all 3 terms, frontmatter title correctly pluralized to Arbeitsbereiche"
      gt_next_runtime: "all 3 terms, rendered by a running Next.js dev server and read from the live DOM. Control: clearing the generaltranslation.locale cookie falls back to English source text."
      cdn_payload: "GET https://cdn.gtx.dev/{projectId}/de/{versionId} returned the 3 translated React elements with the glossary terms. Control: the same path shape with a fabricated project id returns {}."
    total: "15 of 15 renderings matched the glossary"
    group_reuse: "the same org-level Context Group was assigned to a second project and the Dashboard labelled it 'Shared with 1 other project'"

  control:
    "The same terms translated without a Context Group left Workspace, Seat and Sync now in English in Markdown while translating them in JSON. Reproduced twice, once by an independent agent in a clean project."

  also_established:
    - "the CLI accepts these file-type keys: json, pot, mdx, md, ts, js, yaml, html, txt, twilioContentJson, lottie, dotStrings, dotStringsdict, androidStrings, xcstrings"
    - ".xcstrings is a single multi-locale catalogue; its include path takes no [locale] placeholder and downloaded locales are merged in place"
    - "androidStrings sources must sit under values-<locale>/, since [locale] cannot produce the bare values/ default"
    - "POST /v2/project/setup/generate with locales:[de] created a 6-term glossary with definitions but an empty translation column. Passing locales did not populate translations in this run."
    - "the generated definitions for Seat and Workspace both said to keep the term as the product or billing UI term, and the Dashboard Translate button then produced Seat->Seat and Workspace->Workspace, honouring them. Definitions steer translation; they are not inert."
    - "re-translating with a definitions-only group changed docs prose and reverted Jetzt synchronisieren to Sync now, so a glossary entry without a target translation still affects output, just not necessarily in the direction you want"
    - "development keys (gtx-dev-) authenticate POST /v2/translate but are rejected by the CLI file workflow, which requires a production key (gtx-api-)"
    - "terminology generation is programmatic (setup/generate, MCP generate_project_context); direct glossary CRUD and group assignment are not. The Dashboard writes those through Next.js Server Actions rather than a callable API."
    - "there is no glossary read-back on either surface, so the Dashboard is the only place to confirm a term's target translation"
    - "gt-next cross-validates next.config.ts against gt.config.json and refuses to build on mismatch: 'Conflicting configuration detected. Key: locales Next Config: [...] does not match GT Config: [...]'"
    - "the published CDN path is {cacheUrl}/{projectId}/{locale}/{versionId}, cacheUrl defaulting to https://cdn.gtx.dev. The payload is a React element tree of {t, i, c} entries, not a flat catalogue. The CDN returns HTTP 200 with {} for unrecognised paths, so a 200 alone is not evidence."
    - "gt-next resolves locale from the generaltranslation.locale cookie when no locale routing is configured. A middleware matcher that rewrites toward a [locale] path segment 404s an app whose routes are not nested under one."

Related Articles