# Add a locale end to end

Add a language on the platform, publish it through Content Deploy, and tell the app it exists. A redeploy is needed only when your URLs name each locale.

Canonical: https://comvi.io/docs/i18n/guides/add-a-locale

With a Comvi project wired through `cdnLoader()` as on [Connect to Comvi](/docs/i18n/connect-comvi),
and access to **Languages** and **Settings** on the platform, add a language and ship it to
users through the CDN. Your build changes only when the URL rules enumerate locales.

## Add the language on the platform

Open **Languages** and click **Add Language**. Codes are canonical BCP-47, such as `pt-BR`
or `zh-Hant-TW`, and the code the platform stores is the code your app configures, uppercase
region included. Machine translation fills the column, translation memory fills what the
organization has already translated from the same source, and a reviewer moves rows from
`Needs Review` to `Translated`. See [Languages](/docs/platform/languages) and
[Machine Translation](/docs/platform/machine-translation).

## Publish it

Open **Settings → Content Deploy** and check that the new language is ticked in the saved
selection. The dashboard starts with every locale ticked, so a new one is included unless you
narrowed the list before. The next auto-deploy publishes it, or **Deploy Now** pushes it at
once.

`Terminal`

```bash
curl https://cdn.comvi.io/<project-token>/pt-BR.json
```

The request returns the catalog once the deploy has landed. A 404 means the language is not in
the selection or the deploy is still in flight. A partly translated language ships as it is: the
CDN publishes exactly the locales you tick. See [CDN Deployment](/docs/platform/cdn).

## Tell the app the locale exists

`src/i18n/routing.ts`

```ts

export const routing = defineRouting({
locales: ['en', 'de', 'pt-BR'],
defaultLocale: 'en',
localePrefix: 'as-needed',
});
```

`nuxt.config.ts`

```ts

export default defineNuxtConfig({
modules: ['@comvi/nuxt'],
comvi: {
    defaultLocale: 'en',
    locales: [
      { code: 'en', name: 'English', iso: 'en-US' },
      { code: 'pt-BR', name: 'Português (Brasil)', iso: 'pt-BR' },
    ],
},
});
```

An i18n instance that fetches `locale + '.json'` for whatever locale is set needs nothing.
React, Vue, Svelte, Solid and vanilla apps add the code wherever their own switcher lists
options. An app that composes `localeDetector()` also adds the code to its `supportedLocales`:
without that option the detector truncates every detected and cached tag to its lowercased base
language, so a visitor who picked `pt-BR` comes back as `pt`, and `pt.json` does not exist. Next.js and Nuxt are different: both match the first
path segment against their `locales` list verbatim, so the new code goes into `defineRouting`
(and into `createNextI18n`, which spreads the same object) or into `comvi.locales`. Matching is
case-sensitive. With `pt-BR` in the list, `/pt-br/…` is a different route. On Nuxt the module
reads only `iso` and `dir`; `name` is yours, through
`useRuntimeConfig().public.comvi.localeObjects`.

> **One string, and the case is part of it**
>
> The loader builds the catalog path from the locale the i18n instance is set to, and the CDN
> object key is byte-exact: the file is `pt-BR.json`, and an instance set to `pt-br` asks for
> `pt-br.json`. The CDN answers an old spelling such as `pt_BR.json` with the canonical file;
> configure the code the platform shows you anyway. In JavaScript a locale code is a quoted
> key, `{ 'pt-BR': … }`.

## What needs a redeploy

| Change | Ships via | Needs a redeploy |
| --- | --- | --- |
| a translation's wording | CDN | no |
| a new key | CDN | no |
| a new language already in the routing list | CDN | no |
| a new language that must appear in URLs | CDN, plus source | yes |
| the default locale, or the prefix strategy | source | yes |

A right-to-left language such as `ar` or `he` publishes its catalog but does not set `dir` on
your document. Read the direction from the i18n instance and apply it; on Nuxt the module writes
`<html lang dir>` itself. See [Switch the locale](/docs/i18n/language-switching).

## How it works

Content Deploy is a saved selection of locales plus a pipeline that writes one JSON object per
locale and namespace to the CDN. Adding a language on **Languages** adds it to the project; it
reaches the CDN only when it is in the selection and a deploy runs. A selection somebody
narrowed earlier does not grow on its own: the new language stays unticked until a person
ticks it, and the CDN keeps answering 404 for it however complete the translations are. An
automatic deploy runs about 5 seconds after the last edit, and at most 1 minute into a
continuous stream of edits; **Deploy Now** skips the wait.

On the app side, `cdnLoader()` never holds a list of locales. When the i18n instance is set to
`pt-BR`, the loader asks for `{cdnUrl}/pt-BR.json` for the default namespace and
`{cdnUrl}/{namespace}/pt-BR.json` for every other one, and whatever comes back is the catalog.
That is why a plain React or Vue app learns about a new language without a build: the only
place the code lists locales is the switcher, and that list is yours. The two frameworks with
locale-prefixed URLs are the exception, because their middleware decides from `locales`
whether `/pt-BR/about` is a locale route or a 404, and nothing at runtime can grow that list.

A deploy publishes the values that pass the **Filter by Status** setting at that moment. With
**Translated** ticked, a `pt-BR` key still in review is left out of the file and the i18n
instance walks `fallbackLocale` for it at render time, as it does for a key whose value is
empty; with the default empty filter every
status ships, drafts included. Finishing the translations later changes the file on the CDN,
and the app picks it up on its next load, still with no redeploy.

## Read next

- [Switch the locale](/docs/i18n/language-switching): `setLocale`, persistence and direction.
- [CDN Deployment](/docs/platform/cdn): the selection, the deploy pipeline and cache invalidation.
- [Going from 2 to 20 languages](/docs/platform/recipes/scaling-languages): decisions that get harder to reverse.
