# Localization

Run the same site in several languages, with a fallback when a translation is missing, and publish each language separately.

Localization lets one document exist in several languages. An editor switches language at
the top of the edit screen and writes the Spanish version of the same post, without
creating a second post.

## Availability

|            |                                                                 |
| ---------- | --------------------------------------------------------------- |
| Built in   | No plugin required                                              |
| Admin      | Language switcher on edit screens, plus a translations overview |
| Permission | The same access as editing the document                         |

## Turn it on

Declare your languages once, in `nextly.config.ts`:

```ts
import { defineConfig } from "nextly";

export default defineConfig({
  localization: {
    locales: [
      "en",
      { code: "es", label: "Español" },
      { code: "ar", label: "العربية", rtl: true },
      { code: "fr", label: "Français", fallbackLocale: "en" },
    ],
    defaultLocale: "en",
    fallback: true,
  },
});
```

A locale is either a bare code or an object. The object form carries:

| Key              | Meaning                                          |
| ---------------- | ------------------------------------------------ |
| `code`           | The locale code, e.g. `es`                       |
| `label`          | What the admin shows in the language switcher    |
| `rtl`            | Renders that language right-to-left              |
| `fallbackLocale` | Which language (or ordered list) to fall back to |

## Choose what gets translated

Mark a whole collection localized, then opt individual fields out:

```ts
import { defineCollection, text } from "nextly/config";

export const Posts = defineCollection({
  slug: "posts",
  localized: true,
  fields: [
    text({ name: "title" }),
    // Not worth translating: the same value in every language.
    text({ name: "externalId", localized: false }),
  ],
});
```

Fields inherit the collection's setting, so the common case is one flag.

### When a field stops being translated

Changing a translated field to shared keeps its content, for a collection, a Single or a field group, whether you change it in the Schema Builder or in code:

* The default language's value becomes the field's shared value.
* Every other language's value is copied to the localization archive (the `nextly_i18n_archive` table) before the translated column is removed.

In code, set `localized: false` on the field and run `nextly migrate:create`. It writes three files that `nextly migrate` runs in order: one that writes the other languages to the archive, the migration that gives the main table the field's column, and one that copies the default language's value into it and removes the translated column. Rolling them back with `nextly migrate:down` puts the translated field back, every language included. `nextly migrate --dry-run` shows all three before anything runs.

If the archive cannot be written, the change is refused and nothing is changed. A field marked Required or Unique cannot stop being translated in the same change: turn the setting off and save, make the field shared and save, then turn the setting back on. In code the steps are the same, with `migrate:create` after each.

To bring those values back, make the field translated again, then restore that field:

```bash
npx nextly i18n:restore --collection posts --field title
```

`--collection` takes the slug of a collection, Single or field group, and `--field` takes the field's name or its column name. `--locale es` restores one language only. The archive keeps the values until you remove them: add `--purge` to a restore to delete the rows it replayed. Deleting the collection, Single or field group also removes its archived values.

## Fallback chains

`fallback: true` means a missing translation falls back rather than rendering empty. A
locale can name its own chain — `fallbackLocale: ["es", "en"]` tries Spanish, then
English. Without a chain, the default locale is used.

## Publishing one language at a time

Per-locale publishing needs **two** flags, not one. `localized` gives you translations;
`status` gives you the draft/published lifecycle. Only with both does each language get its
own state:

```ts
import { defineCollection, text } from "nextly/config";

export const Posts = defineCollection({
  slug: "posts",
  localized: true,
  status: true,
  fields: [text({ name: "title" })],
});
```

With both set, publishing the Spanish version does not publish the French one, and a language
still being translated stays invisible while the others are live.

> [!WARNING]
> With `localized: true` alone you get translations and no lifecycle — there is nothing to
> publish per language, because the entity has no draft/published state at all.

> [!WARNING]
> This is per-locale **publishing**, not per-locale scheduling. A
> [content release](https://nextlyhq.com/docs/releases.md) schedules the whole document across every language;
> it cannot schedule one language on its own.

## Related

* [Draft and published status](https://nextlyhq.com/docs/guides/draft-published-status.md)
* [Content releases](https://nextlyhq.com/docs/releases.md) — publishing a set of documents together
* [Versions](https://nextlyhq.com/docs/versions.md) — history per document
