Skip to content

Namespaces

Namespaces let you split translations into logical groups — like the default namespace, dashboard, and auth — so your app can load route- or feature-specific strings separately. This reduces initial bundle size when you lazy-load namespaces and keeps translation files manageable as your project grows.

A namespace is a named group of translation keys. The core does not prescribe a directory layout — namespaces are just identifiers, and a loader maps (locale, namespace) to a translation object however you choose.

That said, the official CDN loader (@comvi/plugin-fetch-loader) uses this URL convention: root locale files are the default namespace, and nested directories are additional namespaces. If you use the import-map form of registerLoader({ ... }), the map keys (en, en:dashboard, etc.) define the namespaces, and the file paths can mirror the same layout.

locales/
├── en.json # Default namespace for English
├── de.json # Default namespace for German
├── dashboard/
│ ├── en.json # "dashboard" namespace for English
│ └── de.json # "dashboard" namespace for German
├── auth/
│ ├── en.json # "auth" namespace for English
│ └── de.json # "auth" namespace for German
└── settings/
├── en.json # "settings" namespace for English
└── de.json # "settings" namespace for German

With a loader and route-based loading, a user who never visits the settings page does not need to download locales/settings/en.json.

Define your namespaces when creating the i18n instance:

src/i18n.ts
import { createI18n } from '@comvi/core';
import { FetchLoader } from '@comvi/plugin-fetch-loader';
const i18n = createI18n({
locale: 'en',
defaultNs: 'default',
ns: ['default', 'dashboard', 'auth', 'settings'],
})
.use(FetchLoader({
cdnUrl: 'https://cdn.comvi.io/your-distribution-id',
}));
OptionTypeDefaultDescription
defaultNsstring'default'Namespace used when no namespace is specified in t()
nsstring[]Namespaces to load during initialization. If not specified, only defaultNs is loaded. Use ns: [] to skip initial namespace loading

By default, t() looks up keys in the defaultNs. To reference a key in a different namespace, pass the ns option:

t('welcome', { ns: 'dashboard' }); // Key "welcome" from "dashboard" namespace
t('login.title', { ns: 'auth' }); // Key "login.title" from "auth" namespace
t('buttons.save'); // Uses defaultNs

Or scope an entire component with useI18n('dashboard') — see Per-Component Namespaces.

At runtime, Comvi does not use namespace:key shorthand for lookups. Use the ns option instead:

t('welcome', { ns: 'dashboard' }); // dashboard namespace
t('dashboard:welcome'); // literal key "dashboard:welcome" in defaultNs

Namespaces are loaded independently. By default, only the defaultNs is loaded during init(). Additional namespaces are loaded when you call addActiveNamespace() / addActiveNamespaces(), or when a framework-specific server helper explicitly loads them.

Specify which namespaces to load eagerly at init:

src/i18n.ts
const i18n = createI18n({
locale: 'en',
defaultNs: 'default',
ns: ['default', 'dashboard', 'auth'], // Load these during init()
})
.use(FetchLoader({
cdnUrl: 'https://cdn.comvi.io/your-distribution-id',
}));

Or load a namespace manually at runtime:

// Loads "settings" for the current locale
await i18n.addActiveNamespace('settings');
// Load multiple at once
await i18n.addActiveNamespaces(['settings', 'premium']);

Request deduplication: If multiple calls request the same (locale, namespace) pair simultaneously, the library makes a single loader call. Subsequent requests reuse the pending Promise.

Loader behavior: The loader is called per (locale, namespace) pair. For example, calling setLocaleAsync('de') while 'dashboard' is already active triggers a fetch for de:dashboard.

Retry on failure: If a namespace fails to load, it stays in the active set. The next locale switch retries it automatically — you do not need to call addActiveNamespace() again to recover.

In framework integrations, you can scope useI18n() to a specific namespace. This uses that namespace as the default for all t() calls within the component. It does not load the namespace by itself; load it during initialization, through your loader setup, or by calling addActiveNamespace().

src/views/DashboardView.vue
<script setup lang="ts">
import { useI18n } from '@comvi/vue';
// Scopes t() to the "dashboard" namespace
const { t, isLoading } = useI18n('dashboard');
</script>
<template>
<div v-if="isLoading">Loading...</div>
<div v-else>
<h1>{{ t('page.title') }}</h1> <!-- dashboard:page.title -->
<p>{{ t('stats.summary') }}</p> <!-- dashboard:stats.summary -->
</div>
</template>

You can still reference other namespaces from within a scoped component using the ns parameter:

const { t } = useI18n('dashboard');
t('page.title'); // "dashboard" namespace
t('buttons.save', { ns: 'default' }); // default namespace (explicit)
t('logout', { ns: 'auth' }); // "auth" namespace (explicit)

Load namespaces when a user navigates to a route, so translations are ready before the page renders.

src/router/index.ts
import { i18n } from '../i18n';
const router = createRouter({
routes: [
{
path: '/dashboard',
component: () => import('../views/DashboardView.vue'),
meta: { ns: 'dashboard' },
},
{
path: '/settings',
component: () => import('../views/SettingsView.vue'),
meta: { ns: 'settings' },
},
],
});
router.beforeEach(async (to) => {
const ns = to.meta.ns as string | undefined;
if (ns) {
await i18n.addActiveNamespace(ns);
}
});

If you bundle translations statically instead of fetching from CDN, pass each namespace with a locale:namespace key:

src/i18n.ts
import defaultEn from './locales/en.json';
import dashboardEn from './locales/dashboard/en.json';
import defaultDe from './locales/de.json';
import dashboardDe from './locales/dashboard/de.json';
const i18n = createI18n({
locale: 'en',
defaultNs: 'default',
translation: {
'en:default': defaultEn,
'en:dashboard': dashboardEn,
'de:default': defaultDe,
'de:dashboard': dashboardDe,
},
});

You do not need to list these namespaces in the ns option — every namespace present in translation is registered as active automatically. The ns option is only useful when you also have a loader and want it to fetch additional namespaces during init() (for example, eagerly loading dashboard for every locale via CDN while keeping the default namespace static).

Split namespaces where loading boundaries exist. Routes are a common boundary, but not the only one. A namespace can represent a route, a large feature area, or a section that should load independently.

Use the default namespace for shared strings. Button labels, validation messages, error text, and other UI primitives that appear across many screens are good candidates for the default namespace.

Use names that match how the app is organized. Pick a convention for namespace names and keep it consistent across files, loader config, and t(..., { ns }) calls.