You're reading docs for Nextly Alpha. APIs may change between releases.

Localization

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 inNo plugin required
AdminLanguage switcher on edit screens, plus a translations overview
PermissionThe same access as editing the document

Turn it on

Declare your languages once, in nextly.config.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:

KeyMeaning
codeThe locale code, e.g. es
labelWhat the admin shows in the language switcher
rtlRenders that language right-to-left
fallbackLocaleWhich language (or ordered list) to fall back to

Choose what gets translated

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

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:

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:

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.

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.

This is per-locale publishing, not per-locale scheduling. A content release schedules the whole document across every language; it cannot schedule one language on its own.