Skip to content

Monorepo with shared namespaces

View .md

A monorepo with a web app, an admin dashboard and a marketing site has strings that belong to one app and strings, such as legal footers and error messages, that belong to all of them. One project with namespaces keeps the shared ones in one place, translated once, and lets each app load only what it needs.

Namespace Contents Consumed by
default Legal text, generic UI (Save, Cancel), error messages Every app
marketing Landing pages, pricing, blog UI Marketing site
app In-product strings for the main app Web app
admin Internal dashboard strings Admin dashboard

One project fits apps that ship in the same languages, to the same markets, on the same review schedule. Apps with independent language sets, such as an English-only admin next to a customer app in fifteen languages, or teams that must not see each other’s content, need separate projects; translation memory is organization-level, so they still share matches.

  1. Use default for the shared strings. It is served at the root of the CDN endpoint and stored as {languageTag}.json by the CLI. The dashboard cannot rename or delete it, and the REST API refuses with CANNOT_DELETE_DEFAULT, so treat it as the shared layer rather than trying to turn it into common.

  2. Add one namespace per app. On the Translations page, open the namespace selector, pick Create new namespace… and name it in the Add Namespace dialog. Match the repository layout where you can, so apps/web becomes app. A namespace that holds keys cannot be deleted, so pick names you can live with.

  3. Give each app its own .comvirc.json listing the namespaces it loads:

    apps/admin/.comvirc.json
    {
    "apiBaseUrl": "https://api.comvi.io",
    "translationsPath": "./src/locales",
    "fileTemplate": "{namespace}/{languageTag}.json",
    "format": "json",
    "namespaces": ["default", "admin"]
    }

    comvi pull writes only those namespaces and comvi push reads only them. --ns on the command line replaces the list rather than adding to it.

  4. Load the namespaces at runtime. Lookups do not cross namespaces: a key is found in the namespace you asked for, or in the same namespace under a fallback locale, and nowhere else. Pass an explicit namespace (t(key, { ns })) when the key is not in the app’s default one.

my-monorepo/
├── apps/
│ ├── web/
│ │ ├── .comvirc.json # namespaces: ["default", "app"]
│ │ └── src/locales/
│ ├── admin/
│ │ ├── .comvirc.json # namespaces: ["default", "admin"]
│ │ └── src/locales/
│ └── marketing/
│ ├── .comvirc.json # namespaces: ["default", "marketing"]
│ └── src/locales/
└── packages/
└── ui/ # shared components read the default namespace
  • Legal, brand and error text goes in default. It has to be identical everywhere.
  • Navigation, buttons and form labels go in default only if they are identical across apps in every language, not only in English. Otherwise per app.
  • Domain copy for checkout, admin actions or marketing headlines goes in the app’s own namespace.

Start app-specific and promote a key to default once a second app needs the same value: tick the rows on the Translations page and choose More → Change Namespace. Values, history and comments move with the key.

Translators see every namespace in the filter dropdown. Access is per project and, optionally, per language; there is no namespace-level permission, so scoping an agency to marketing is a briefing instruction and a filter they set themselves. Translation memory works across namespaces: “Payment failed” translated in app comes back as a match in marketing.

Content Deploy publishes one bundle per namespace and language for the namespaces ticked under Settings → Content Deploy. Tick every namespace your apps fetch; an unticked one is not deployed. A save in any namespace triggers a deploy of the whole selected set, and what keeps the apps apart is the fetch: only the marketing app requests the marketing bundles. See Content Deploy / CDN.

  • One namespace for everything. Every app then ships every string.
  • One namespace per page or component. The filter turns into a list nobody reads. Namespaces are for apps and major feature areas.
  • A namespace per language. Languages are columns inside a namespace. Splitting them breaks the side-by-side view and leaves translation memory nothing to match.
  • A shared string that drifted in one language. Someone edited it in one namespace and not the other. Translation memory shows the mismatch as a near match next time; a quarterly read of default catches the rest.