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.
What Are Namespaces?
Section titled “What Are Namespaces?”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 GermanWith a loader and route-based loading, a user who never visits the settings page does not need to download locales/settings/en.json.
Configuration
Section titled “Configuration”Define your namespaces when creating the i18n instance:
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', }));| Option | Type | Default | Description |
|---|---|---|---|
defaultNs | string | 'default' | Namespace used when no namespace is specified in t() |
ns | string[] | — | Namespaces to load during initialization. If not specified, only defaultNs is loaded. Use ns: [] to skip initial namespace loading |
Using Namespaced Keys
Section titled “Using Namespaced Keys”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" namespacet('login.title', { ns: 'auth' }); // Key "login.title" from "auth" namespacet('buttons.save'); // Uses defaultNsOr 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 namespacet('dashboard:welcome'); // literal key "dashboard:welcome" in defaultNsLoading Specific Namespaces
Section titled “Loading Specific Namespaces”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:
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 localeawait i18n.addActiveNamespace('settings');
// Load multiple at onceawait 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.
Per-Component Namespaces
Section titled “Per-Component Namespaces”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().
<script setup lang="ts">import { useI18n } from '@comvi/vue';
// Scopes t() to the "dashboard" namespaceconst { 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>import { useI18n } from '@comvi/react';
function Dashboard() { // Scopes t() to the "dashboard" namespace const { t, isLoading } = useI18n('dashboard');
if (isLoading) return <div>Loading...</div>;
return ( <div> <h1>{t('page.title')}</h1> {/* dashboard:page.title */} <p>{t('stats.summary')}</p> {/* dashboard:stats.summary */} </div> );}'use client';import { useI18n } from '@comvi/next/client';
function Dashboard() { // Scopes t() to the "dashboard" namespace const { t, isLoading } = useI18n('dashboard');
if (isLoading) return <div>Loading...</div>;
return ( <div> <h1>{t('page.title')}</h1> {/* dashboard:page.title */} <p>{t('stats.summary')}</p> {/* dashboard:stats.summary */} </div> );}<script setup lang="ts">// Scopes t() to the "dashboard" namespaceconst { 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>import { useI18n } from '@comvi/solid';
function Dashboard() { // Scopes t() to the "dashboard" namespace const { t, isLoading } = useI18n('dashboard');
if (isLoading()) return <div>Loading...</div>;
return ( <div> <h1>{t('page.title')}</h1> {/* dashboard:page.title */} <p>{t('stats.summary')}</p> {/* dashboard:stats.summary */} </div> );}<script lang="ts">import { useI18n } from '@comvi/svelte';
// Scopes t() to the "dashboard" namespaceconst { t, isLoading } = useI18n('dashboard');</script>
{#if $isLoading} <div>Loading...</div>{:else} <div> <h1>{$t('page.title')}</h1> <!-- dashboard:page.title --> <p>{$t('stats.summary')}</p> <!-- dashboard:stats.summary --> </div>{/if}import { i18n } from './i18n';
// Load "dashboard" namespaceawait i18n.addActiveNamespace('dashboard');
const heading = document.createElement('h1');heading.textContent = i18n.t('page.title', { ns: 'dashboard' });
const summary = document.createElement('p');summary.textContent = i18n.t('stats.summary', { ns: 'dashboard' });
container.append(heading, summary);You can still reference other namespaces from within a scoped component using the ns parameter:
const { t } = useI18n('dashboard');
t('page.title'); // "dashboard" namespacet('buttons.save', { ns: 'default' }); // default namespace (explicit)t('logout', { ns: 'auth' }); // "auth" namespace (explicit)Lazy Loading Namespaces on Route Change
Section titled “Lazy Loading Namespaces on Route Change”Load namespaces when a user navigates to a route, so translations are ready before the page renders.
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); }});import { createBrowserRouter } from 'react-router-dom';import { i18n } from './i18n';import { Dashboard } from './pages/Dashboard';import { Settings } from './pages/Settings';
export const router = createBrowserRouter([ { path: '/dashboard', Component: Dashboard, loader: async () => { await i18n.addActiveNamespace('dashboard'); return null; }, }, { path: '/settings', Component: Settings, loader: async () => { await i18n.addActiveNamespace('settings'); return null; }, },]);Static Translations with Namespaces
Section titled “Static Translations with Namespaces”If you bundle translations statically instead of fetching from CDN, pass each namespace with a locale:namespace key:
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).
Best Practices
Section titled “Best Practices”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.