Skip to content

@mapomodule/i18n

Interface translations for Mapo. Ships the en and it catalogs used by every UIKit and Form component, and wires @nuxtjs/i18n with admin-friendly defaults so your app can translate its own strings through the same useI18n().

Two different "i18n"

This page is about the UI language — the labels of buttons, dialogs and error messages. Translating your content (a model with per-language fields) is a separate feature: see Translated fields and MapoDetailLangSwitch.

Installation

The module is installed by the mapomodule meta-package, so a standard setup needs nothing at all:

ts
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ["@nuxt/ui", "mapomodule"],
});

@nuxtjs/i18n is installed for you with strategy: 'no_prefix' (no /it/… URL prefixes — an admin panel is not indexed), browser-language detection and cookie persistence.

Configuration

Everything lives under mapo.i18n:

ts
export default defineNuxtConfig({
  modules: ["@nuxt/ui", "mapomodule"],

  mapo: {
    i18n: {
      // Default UI locale, also the fallback for missing keys. Default: 'en'.
      defaultLocale: "it",

      // Persist the detected browser language in a cookie. Default: true.
      detectBrowserLanguage: true,
    },
  },
});
OptionTypeDefaultDescription
defaultLocalestring'en'Startup locale and fallbackLocale for keys a locale doesn't define.
localesMapoLocale[]en + itThe locales your app offers. See Adding a language.
detectBrowserLanguagebooleantrueDetect on first visit and persist the choice in the i18n_redirected cookie.
i18nRecord<string, unknown>{}Escape hatch forwarded verbatim to @nuxtjs/i18n; wins over Mapo defaults.

Using translations in your pages

useI18n() is auto-imported by @nuxtjs/i18n — Mapo strings live under the mapo.* namespace, your own strings wherever you put them:

vue
<script setup lang="ts">
const { t } = useI18n();
</script>

<template>
  <!-- your own key -->
  <h1>{{ t("dashboard.title") }}</h1>

  <!-- a Mapo built-in, reused for consistency -->
  <UButton>{{ t("mapo.save") }}</UButton>
</template>

Outside <script setup>

useI18n() requires an active component instance, so it cannot be used inside a Pinia store or a plain function. Use useMapoT() instead — it resolves the translator from the Nuxt app and falls back to returning the key itself if i18n is not installed. It is auto-imported, so no import statement is needed:

ts
export const useThingStore = defineStore("thing", () => {
  async function remove(thing: Thing) {
    const t = useMapoT();
    const ok = await useConfirmStore().ask({
      title: t("mapo.delete"),
      message: t("mapo.confirmDelete"),
    });
    // …
  }
});

Overriding a Mapo string

Ship a locale file in your app with the same key. Project messages are merged on top of the module ones, so you only redefine what you want to change:

json
// i18n/locales/it.json
{
  "mapo": {
    "save": "Registra",
    "listTable": {
      "noItems": "Nessun risultato per questa ricerca"
    }
  }
}

Register the file through locales:

ts
mapo: {
  i18n: {
    defaultLocale: 'it',
    locales: [
      { code: 'en', language: 'en-US', name: 'English', file: 'en.json' },
      { code: 'it', language: 'it-IT', name: 'Italiano', file: 'it.json' },
    ],
  },
}

Everything you don't override keeps the Mapo default: the merge is deep, key by key, so a partial file is perfectly valid.

Adding a language

Add the locale plus a file with your own strings. Mapo has no catalog for it, so its own strings fall back to defaultLocale:

ts
mapo: {
  i18n: {
    defaultLocale: 'en',
    locales: [
      { code: 'en', language: 'en-US', name: 'English' },
      { code: 'it', language: 'it-IT', name: 'Italiano' },
      { code: 'fr', language: 'fr-FR', name: 'Français', file: 'fr.json' },
    ],
  },
}

To translate the Mapo strings too, copy the mapo block from en.json into your fr.json and translate the values.

The language switcher

<MapoLangSwitcher> lists the configured locales and switches the UI language, persisting the choice in the cookie. Drop it in your topbar:

vue
<!-- app.vue -->
<template>
  <NuxtLayout>
    <template #topbar:right>
      <MapoLangSwitcher />
      <MapoThemeToggle />
    </template>
    <NuxtPage />
  </NuxtLayout>
</template>
PropTypeDefaultDescription
flagsbooleantruePrefix each entry with the flag emoji derived from the locale's language tag.
size'xs' | 'sm' | 'md' | 'lg' | 'xl''sm'Forwarded to the underlying USelectMenu.

A locale with no region in its language tag (or no language at all) simply renders without a flag.

Writing message keys

Mapo catalogs follow the vue-i18n message syntax. Two things are worth knowing:

Interpolation uses named placeholders:

json
{ "totalItems": "{total} items" }
ts
t("mapo.listTable.totalItems", { total: 42 });

Pluralization uses the pipe | to separate singular and plural forms. The count is passed as the third argument:

json
{ "nItems": "{n} item | {n} items" }
ts
t("mapo.repeater.nItems", { n: count }, count);

The format is {placeholder} singular form | {placeholder} plural form. Always use a consistent placeholder name ({n} is recommended) and always include both forms, even if the singular seems redundant. This ensures consistency across locales and makes it clear to translators where the count will be substituted.

Pluralization best practices

When writing new Mapo strings or overriding existing ones:

  1. Always use both singular and plural forms, separated by |, even if one seems trivial. Some languages have complex plural rules (e.g., Czech, Polish, Russian with multiple forms).

  2. Use {n} as the placeholder name for the count. Avoid {number}, {count}, or other names — consistency helps translators and makes code review easier.

  3. Always pass the count as the third argument to t():

    ts
    // ✅ Correct
    t("mapo.repeater.nItems", { n: items.length }, items.length);
    
    // ❌ Wrong — translator gets no signal that this is plural
    t("mapo.repeater.nItems", { n: items.length });
  4. When translating, keep the | separator and use the same placeholder name in both forms — the plural engine relies on it.

@ must be escaped

vue-i18n reads @:key as a linked message. A literal @ anywhere in a value (an email, an npm scope) makes the compiler fail with Invalid linked format (error code: 10) — and the failure takes down the whole catalog, not just that key. Wrap it in a literal:

json
{ "requiresUikit": "Requires {'@'}mapomodule/uikit" }

What is already translated

Every string rendered by @mapomodule/uikit and @mapomodule/form goes through t(): list tables and filters, detail pages and their buttons, the form field messages, the Media Manager, the Menu Manager, login, and the confirm/snack feedback. Keys are grouped by component under mapo.* — e.g. mapo.listTable.*, mapo.mediaUploader.*, mapo.menuTreeview.* — with generic labels (mapo.save, mapo.delete, mapo.search…) shared across components.

Using your own @nuxtjs/i18n setup

If your app already declares @nuxtjs/i18n in modules[], Mapo detects it and steps aside: it only contributes its message catalogs through the i18n:registerModule hook and leaves your configuration untouched. Note that @nuxt/ui must still come before mapomodule in modules[].

Released under the MIT License.