Core

Internationalization

Translate the admin and the public site with the Pluto i18n engine.

Pluto has its own small i18n engine. It has no @nuxtjs/i18n dependency. It translates the admin UI. With a separate scope, it also translates the public site.

The shipped languages are en (the default) and pt-BR. A new language is a new set of JSON files. It needs no code change.

The message format is compatible with vue-i18n. Use {name} for values and | for plural forms.

Locale files

Each layer, and the app, keeps its text in i18n/locales/<code>.json. The files of all layers merge by locale, with the base layer first and the app last. The app overrides the text of a layer.

Each layer owns one top-level namespace. It is the $meta.name of the layer.

NamespaceOwner
plutoCore. Shared words go in pluto.common.*.
supabase, supabase-shop, supabase-blogThe matching layer
docsThe @plutocms/docs layer
content.<typeName>.*The layer that declares the content type
site.*The text of your public site

Options

Set the options under plutoI18n in nuxt.config.ts. All are optional.

OptionDefaultMeaning
defaultLocaleenThe fallback, and the last choice of every resolve order.
adminLocalesEvery discovered localeThe languages that the admin offers.
siteLocalesEvery discovered localeThe languages that the public site can use.
siteLocaledefaultLocaleThe site language before a visitor picks one.

Restart the dev server after you add a new language, so the option defaults pick it up.

Two scopes

The admin and the site each have an active locale.

  • usePlutoI18n() is the admin scope. It is per user.
  • usePlutoSiteI18n() is the site scope. Your app owns it. It does not follow the admin language.

Both return { locale, locales, uiLocale, t, tx, te, n, d, setLocale }.

FunctionUse
t(key, params?)Translates a key. A number as the second argument is the plural count.
tx(value)Translates a key or a literal. It translates only when the value looks like a key and the key exists.
te(key)Tells if a key exists.
n(value, options?)Formats a number with Intl.
d(value, options?)Formats a date with Intl.

If a key is missing, t falls back to the active locale, then en, then the key itself. In development, a missing key warns once.

Plurals

A message with | has plural forms. Three forms are zero | one | other. Two forms are one | other. Use t(key, count). It also sets {count}.

Choose the first locale

  • Admin. The cookie pluto-admin-locale, then the browser language matched to adminLocales, then defaultLocale.
  • Site. The cookie pluto-site-locale, then the siteLocale option, then defaultLocale.

The server reads Accept-Language. The client reads navigator.languages.

The admin language of a user

The Settings page has a "Your preferences" card with a language select. The first choice is Automatic (browser). Each language shows in its own name.

A change switches the admin at once. The cookie pluto-admin-locale keeps the choice. If a backend layer registers a localeDriver, the choice is also saved there. Without a driver, the choice lives in the cookie only.

localeDriver: {
  load: async () => 'pt-BR', // or null for Automatic
  save: async (locale) => { /* store it */ }
}

Translate a component

  1. Call const { t, n, d } = usePlutoI18n() in <script setup>.
  2. Replace each literal with t('pluto.<area>.<name>').
  3. Pass a value as a parameter. Never build a sentence from pieces in code. The word order differs between languages.
  4. Add the key to every locale file. The files need the same keys, the same placeholders, and the same number of plural forms.
  5. Format numbers and dates with n() and d().
  6. Use a getter for useHead, so a language switch updates the title: useHead({ title: () => t('pluto.pages.settings') }).
  7. Read t() inside a template or a computed.

Content type text

A content type keeps English literals. Core looks for these keys and falls back to the literal:

  • content.<name>.label, content.<name>.labelPlural, content.<name>.new
  • content.<name>.fields.<field>.label, .description, .placeholder
  • content.<name>.fields.<field>.options.<value>
  • content.<name>.status.<value>

usePlutoContentText(type) resolves them. The layer that declares the type owns the keys.

Registry text

A registry entry holds a key or a literal. The consumer composables resolve it with tx(). A language switch updates the text.

Validation messages

validateContentPayload returns errors with a message in English, a code, and params. The codes are required, number, integer, min, max, boolean, option, and maxLength. Each code has a key pluto.validation.<code>.

Server errors

A server route throws createPlutoError({ statusCode, message, key, params?, data? }). The statusMessage stays English. The response also holds data.i18n = { key, params }.

On the client, usePlutoErrorText() returns (error, fallbackKey?) => string. It uses the translated data.i18n, then the response message, then the translated fallback key. The default fallback is pluto.errors.unknown.

Nuxt UI and the html tag

Nuxt UI has one locale for each page. PlutoRoot passes the admin locale to UApp on routes under /admin, and the site locale on the other routes.

PlutoRoot sets <html lang dir> from the admin locale on routes under /admin. On the other routes, it sets them from the site locale only when your app uses the site scope. That means the app sets plutoI18n.siteLocale or plutoI18n.siteLocales. Otherwise, the app keeps its own app.head.htmlAttrs.

Not translated

  • Text stored in the database, such as categories, availability names, and roles.
  • The 404 of the generated content pages.
  • Server logs.