npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@vielzeug/i18nit

v1.2.0

Published

**I18nit** is a type-safe, lightweight internationalization library for TypeScript. Build multilingual applications with powerful pluralization, nested translations, and lazy loading—all in just 1.6 KB.

Readme

@vielzeug/i18nit

What is I18nit?

I18nit is a type-safe, lightweight internationalization library for TypeScript. Build multilingual applications with powerful pluralization, nested translations, and lazy loading—all in just 1.6 KB.

The Problem

Internationalization libraries are often heavy and complex:

  • i18next is feature-rich but adds 11KB+ to your bundle
  • react-intl is React-specific and requires setup
  • FormatJS has a steep learning curve
  • Manual translations lead to missing keys and runtime errors
  • Type safety requires extra tooling

The Solution

I18nit provides a simple, type-safe API using native browser APIs:

import { createI18n } from '@vielzeug/i18nit';

const i18n = createI18n({
  locale: 'en',
  messages: {
    en: {
      welcome: 'Welcome, {name}!',
      items: 'You have {count} item | You have {count} items',
    },
    es: {
      welcome: '¡Bienvenido, {name}!',
      items: 'Tienes {count} artículo | Tienes {count} artículos',
    },
  },
});

// Interpolation
i18n.t('welcome', { name: 'Alice' }); // "Welcome, Alice!"

// Automatic pluralization
i18n.t('items', { count: 1 }); // "You have 1 item"
i18n.t('items', { count: 5 }); // "You have 5 items"

✨ Features

  • Type-Safe – Full TypeScript support with generic types
  • Lightweight – 1.6 KB gzipped with zero dependencies
  • Universal Pluralization – 100+ languages via Intl.PluralRules API
  • Smart Array Handling – Auto-join with separators, length access, and safe indexing
  • Path Interpolation – Support for nested objects and array indices
  • Lazy Loading – Async locale loading with automatic caching
  • Namespaces – Organize translations by feature or module
  • Fallback Chain – Multiple fallback locales with automatic language variants
  • HTML Escaping – Built-in XSS protection
  • Number & Date Formatting – Locale-aware formatting with Intl API
  • Framework Agnostic – Works with React, Vue, Svelte, or vanilla JS

🆚 Comparison with Alternatives

| Feature | I18nit | i18next | react-intl | FormatJS | | ------------------- | -------------- | ------- | ---------- | ------------ | | Bundle Size (gzip) | ~1.6 KB | ~11KB | ~14KB | ~14KB | | TypeScript Support | ✅ First-class | ✅ Good | ✅ Good | ✅ Excellent | | Pluralization | ✅ Native Intl | ✅ ICU | ✅ ICU | ✅ ICU | | Nested Translations | ✅ Built-in | ✅ Yes | ⚠️ Limited | ✅ Yes | | Lazy Loading | ✅ Async | ✅ Yes | ⚠️ Manual | ✅ Yes | | Framework Agnostic | ✅ Yes | ✅ Yes | ❌ React | ❌ React | | Dependencies | 0 | 3 | 5 | 7 |

📦 Installation

# pnpm
pnpm add @vielzeug/i18nit
# npm
npm install @vielzeug/i18nit
# yarn
yarn add @vielzeug/i18nit

🚀 Quick Start

import { createI18n } from '@vielzeug/i18nit';

// Create instance with messages
const i18n = createI18n({
  locale: 'en',
  messages: {
    en: {
      greeting: 'Hello, {name}!',
      items: {
        zero: 'No items',
        one: 'One item',
        other: '{count} items',
      },
    },
    es: {
      greeting: '¡Hola, {name}!',
      items: {
        zero: 'Sin artículos',
        one: 'Un artículo',
        other: '{count} artículos',
      },
    },
  },
});

// Simple translation
i18n.t('greeting', { name: 'World' }); // "Hello, World!"

// Pluralization
i18n.t('items', { count: 0 }); // "No items"
i18n.t('items', { count: 1 }); // "One item"
i18n.t('items', { count: 5 }); // "5 items"

// Change locale
i18n.setLocale('es');
i18n.t('greeting', { name: 'Mundo' }); // "¡Hola, Mundo!"

📚 Core Concepts

Translation Keys

Translation keys support dot notation for nested organization:

const i18n = createI18n({
  messages: {
    en: {
      'user.profile.title': 'Profile',
      'user.settings.title': 'Settings',
      'admin.dashboard': 'Dashboard',
    },
  },
});

i18n.t('user.profile.title'); // "Profile"

Nested Message Objects

You can organize messages using nested objects for better structure:

const i18n = createI18n({
  locale: 'en',
  messages: {
    en: {
      // Flat structure
      welcome: 'Welcome!',
      
      // Nested structure - access with dot notation
      user: {
        greeting: 'Hello, {name}!',
        profile: {
          title: 'User Profile',
          settings: 'Profile Settings',
        },
      },
      
      // Deep nesting
      app: {
        navigation: {
          menu: {
            home: 'Home',
            about: 'About',
          },
        },
      },
    },
  },
});

// Access nested messages with dot notation
i18n.t('welcome');                    // "Welcome!"
i18n.t('user.greeting', { name: 'Alice' }); // "Hello, Alice!"
i18n.t('user.profile.title');         // "User Profile"
i18n.t('app.navigation.menu.home');   // "Home"

// Use with namespaces for cleaner code
const userNs = i18n.namespace('user');
userNs.t('greeting', { name: 'Bob' }); // "Hello, Bob!"
userNs.t('profile.title');             // "User Profile"

Variable Interpolation

Basic Interpolation

i18n.t('greeting', { name: 'Alice' });
// Template: "Hello, {name}!"
// Result: "Hello, Alice!"

Nested Object Access

i18n.t('message', { user: { name: 'Bob', role: 'Admin' } });
// Template: "User {user.name} is {user.role}"
// Result: "User Bob is Admin"

Array Index Access

i18n.t('friends', { friends: [{ name: 'Charlie' }, { name: 'Dave' }] });
// Template: "First friend: {friends[0].name}"
// Result: "First friend: Charlie"

Array Handling

Arrays can be intelligently formatted with various separators:

const i18n = createI18n({
  messages: {
    en: {
      shopping: 'Shopping list: {items}',
      guests: 'Invited: {names|and}',
      options: 'Choose: {choices|or}',
      path: 'Path: {folders| / }',
      count: 'You have {items.length} items',
    },
  },
});

// Default comma separator
i18n.t('shopping', { items: ['Apple', 'Banana', 'Orange'] });
// "Shopping list: Apple, Banana, Orange"

// Natural "and" lists (locale-aware via Intl.ListFormat – supports 100+ languages automatically)
i18n.t('guests', { names: ['Alice'] });
// "Invited: Alice"
i18n.t('guests', { names: ['Alice', 'Bob'] });
// "Invited: Alice and Bob"
i18n.t('guests', { names: ['Alice', 'Bob', 'Charlie'] });
// "Invited: Alice, Bob, and Charlie" (Oxford comma in English)

// Natural "or" lists (locale-aware via Intl.ListFormat – supports 100+ languages automatically)
i18n.t('options', { choices: ['Tea', 'Coffee', 'Juice'] });
// "Choose: Tea, Coffee, or Juice"

// Custom separators
i18n.t('path', { folders: ['home', 'user', 'documents'] });
// "Path: home / user / documents"

// Array length
i18n.t('count', { items: ['A', 'B', 'C'] });
// "You have 3 items"

Array Features:

  • {items} – Join with comma (, )
  • {items|and} – Natural "and" list with locale-aware conjunction (uses Intl.ListFormat – supports 100+ languages)
  • {items|or} – Natural "or" list with locale-aware conjunction (uses Intl.ListFormat – supports 100+ languages)
  • {items| – } – Custom separator (e.g., "A – B – C")
  • {items.length} – Array length
  • {items[0]} – Safe index access (returns empty if out of bounds)

Locale-Aware List Formatting:
The and and or separators use the built-in Intl.ListFormat API which automatically handles:

  • 100+ languages – Supports all languages available in the browser/runtime
  • Proper grammar – Oxford comma, locale-specific punctuation
  • Right-to-left languages – Arabic, Hebrew, etc.
  • Unicode CLDR standards – International standard for list formatting
  • No manual configuration – Zero maintenance required

Examples across languages:

  • English: "A, B, and C" (with Oxford comma)
  • Spanish: "A, B y C" (uses "y")
  • French: "A, B et C" (uses "et")
  • German: "A, B und C" (uses "und")
  • Japanese: "A、B、C" (uses Japanese comma)
  • Arabic: Proper RTL formatting with "و"
  • And 90+ more languages automatically!

Supported Path Formats

  • {name} – Simple variable
  • {user.name} – Nested object property
  • {items[0]} – Array index (safe – returns empty if out of bounds)
  • {items} – Array join with default separator
  • {items|and} – Array join with "and"
  • {items.length} – Array length
  • {data.items[0].value} – Mixed notation

Limitations:

  • Only numeric bracket notation [0], [123]
  • Quoted keys not supported ["key"]
  • Non-numeric brackets not supported [key]

Missing Variable Handling

Missing variables are automatically replaced with empty strings:

const i18n = createI18n({
  messages: { en: { msg: 'Hello, {name}!' } },
});

i18n.t('msg'); // "Hello, !"
i18n.t('msg', { name: 'Alice' }); // "Hello, Alice!"

Pluralization

Support for multiple plural forms based on locale-specific rules:

const i18n = createI18n({
  locale: 'en',
  messages: {
    en: {
      notifications: {
        zero: 'No notifications',
        one: 'One notification',
        other: '{count} notifications',
      },
    },
  },
});

i18n.t('notifications', { count: 0 }); // "No notifications"
i18n.t('notifications', { count: 1 }); // "One notification"
i18n.t('notifications', { count: 5 }); // "5 notifications"

Supported Plural Rules

i18nit uses the browser's built-in Intl.PluralRules API to automatically support pluralization for 100+ languages, including:

  • English (en): one, other
  • French (fr): one (0-1), other
  • Arabic (ar): zero, one, two, few, many, other
  • Polish (pl): one, few, many
  • Russian (ru): one, few, many, other
  • German (de): one, other
  • Chinese (zh): other
  • Japanese (ja): other
  • And 90+ more languages...

🔥 Advanced Features

Fallback Locales

Define fallback locales for missing translations:

const i18n = createI18n({
  locale: 'de-CH',
  fallback: ['de', 'en'],
  messages: {
    'de-CH': { greeting: 'Grüezi!' },
    de: { greeting: 'Hallo!', goodbye: 'Auf Wiedersehen!' },
    en: { greeting: 'Hello!', goodbye: 'Goodbye!', welcome: 'Welcome!' },
  },
});

i18n.t('greeting'); // "Grüezi!" (de-CH)
i18n.t('goodbye'); // "Auf Wiedersehen!" (de fallback)
i18n.t('welcome'); // "Welcome!" (en fallback)

Fallback Chain:

  1. Primary locale (e.g., de-CH)
  2. Base language (e.g., de from de-CH)
  3. First fallback locale
  4. Base of first fallback
  5. Continue through all fallbacks

Async Locale Loading

Load translations on-demand for better performance. Loaders receive the locale as a parameter, allowing you to reuse a single function:

// Define a reusable loader function
const loadLocale = async (locale: string) => {
  const response = await fetch(`/locales/${locale}.json`);
  return response.json();
};

const i18n = createI18n({
  locale: 'en',
  loaders: {
    fr: loadLocale,  // Loader receives 'fr' as parameter
    de: loadLocale,  // Loader receives 'de' as parameter
    es: loadLocale,  // Loader receives 'es' as parameter
  },
});

// Load a locale before using it
await i18n.load('fr');
i18n.setLocale('fr');
i18n.t('greeting'); // Uses loaded French messages

// Or use dynamic imports
const importLoader = async (locale: string) => {
  const module = await import(`./locales/${locale}.json`);
  return module.default;
};

i18n.register('it', importLoader);
await i18n.load('it');

// Preload at app startup await i18n.loadAll(['en', 'fr', 'de']);

// Or load explicitly await i18n.load('fr'); i18n.setLocale('fr'); i18n.t('greeting'); // Now uses French

// Register loader dynamically i18n.register('es', async () => { const module = await import('./locales/es.json'); return module.default; });

// Load and use await i18n.load('es'); i18n.t('greeting', undefined, { locale: 'es' });


**Features:**

- Concurrent requests are deduplicated
- Failed loads throw errors (can be caught)
- Locale is cached after loading
- Use `loadAll()` to preload multiple locales at once

### Namespaces

Organize translations by feature or module:

```typescript
const i18n = createI18n({
  messages: {
    en: {
      'auth.login.title': 'Login',
      'auth.login.button': 'Sign In',
      'auth.register.title': 'Register',
      'dashboard.welcome': 'Welcome back!',
    },
  },
});

// Create namespaced translator
const auth = i18n.namespace('auth.login');
auth.t('title'); // "Login"
auth.t('button'); // "Sign In"

const dashboard = i18n.namespace('dashboard');
dashboard.t('welcome'); // "Welcome back!"

HTML Escaping

Protect against XSS attacks with automatic HTML escaping:

const i18n = createI18n({
  messages: {
    en: {
      userContent: 'Comment: {content}',
    },
  },
});

// Enable escaping globally
const safeI18n = createI18n({
  escape: true,
  messages: { en: { html: '<script>alert("xss")</script>' } },
});

safeI18n.t('html');
// "&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;"

// Or per translation
i18n.t('userContent', { content: '<b>Bold</b>' }, { escape: true });
// "Comment: &lt;b&gt;Bold&lt;/b&gt;"

Number & Date Formatting

Locale-aware formatting using the Intl API:

const i18n = createI18n({ locale: 'en-US' });

// Number formatting
i18n.number(1234.56); // "1,234.56"
i18n.number(99.99, { style: 'currency', currency: 'USD' }); // "$99.99"
i18n.number(0.15, { style: 'percent' }); // "15%"

// Date formatting
const date = new Date('2024-01-15');
i18n.date(date); // "1/15/2024"
i18n.date(date, { dateStyle: 'long' }); // "January 15, 2024"
i18n.date(date, { timeStyle: 'short' }); // "12:00 AM"

// Timestamps
i18n.date(Date.now(), { dateStyle: 'medium' }); // "Jan 15, 2024"

// Custom locale
i18n.number(1234.56, undefined, 'de-DE'); // "1.234,56"
i18n.date(date, { dateStyle: 'short' }, 'fr'); // "15/01/2024"

Subscriptions

React to locale changes:

const i18n = createI18n({ locale: 'en' });

// Subscribe to locale changes
const unsubscribe = i18n.subscribe((locale) => {
  console.log('Locale changed to:', locale);
  // Update UI, reload data, etc.
});

i18n.setLocale('fr'); // Logs: "Locale changed to: fr"

// Unsubscribe when done
unsubscribe();

Use Cases:

  • Update UI when locale changes
  • Reload locale-specific data
  • Analytics/tracking
  • State management integration

Subscriptions

createI18n(config?)

Creates a new i18n instance.

type I18nConfig = {
  locale?: string; // Default: 'en'
  fallback?: string | string[]; // Fallback locale(s)
  messages?: Record<string, Messages>; // Initial translations
  loaders?: Record<string, () => Promise<Messages>>; // Async loaders
  escape?: boolean; // Global HTML escaping (default: false)
};

Translation Methods

t(key, vars?, options?)

Translate a key synchronously.

i18n.t('greeting'); // Simple
i18n.t('greeting', { name: 'Alice' }); // With variables
i18n.t('greeting', { name: 'Bob' }, { locale: 'fr', escape: true }); // With options

Options:

  • locale?: string – Override locale for this translation
  • escape?: boolean – Override HTML escaping

Locale Management

i18n.setLocale('fr'); // Change locale
i18n.getLocale(); // Get current locale
i18n.hasLocale('es'); // Check if locale exists
i18n.has('key'); // Check if key exists
i18n.has('key', 'fr'); // Check if key exists in locale
await i18n.hasAsync('key', 'es'); // Check with async loading

Message Management

// Add messages (merge)
i18n.add('en', { newKey: 'New value' });

// Set messages (replace)
i18n.set('en', { key: 'Value' });

// Get messages for locale
const messages = i18n.getMessages('en');

Async Loading

// Register loader
i18n.register('de', async () => import('./locales/de.json'));

// Load locale
await i18n.load('de');

Formatting

i18n.number(value, options?, locale?);
i18n.date(value, options?, locale?);

Namespace

const ns = i18n.namespace('auth');
ns.t('login.title');

Subscriptions

const unsubscribe = i18n.subscribe((locale) => {
  console.log('Locale changed:', locale);
});

Framework Integration

React

import { createI18n } from '@vielzeug/i18nit';
import { createContext, useContext, useState, useEffect } from 'react';

const I18nContext = createContext(null);

export function I18nProvider({ children, config }) {
  const [i18n] = useState(() => createI18n(config));
  const [locale, setLocale] = useState(i18n.getLocale());

  useEffect(() => {
    return i18n.subscribe(setLocale);
  }, [i18n]);

  return <I18nContext.Provider value={{ i18n, locale }}>{children}</I18nContext.Provider>;
}

export function useI18n() {
  const context = useContext(I18nContext);
  if (!context) throw new Error('useI18n must be used within I18nProvider');
  return context;
}

export function useTranslation(namespace?: string) {
  const { i18n } = useI18n();
  const ns = namespace ? i18n.namespace(namespace) : i18n;

  return {
    t: ns.t.bind(ns),
    locale: i18n.getLocale(),
    setLocale: i18n.setLocale.bind(i18n),
  };
}

// Usage
function MyComponent() {
  const { t, locale, setLocale } = useTranslation('dashboard');

  return (
    <div>
      <h1>{t('welcome')}</h1>
      <button onClick={() => setLocale('fr')}>Français</button>
    </div>
  );
}

Vue 3

import { createI18n } from '@vielzeug/i18nit';
import { ref, onUnmounted, Plugin } from 'vue';

const i18n = createI18n({ locale: 'en' });
const locale = ref(i18n.getLocale());

const unsubscribe = i18n.subscribe((newLocale) => {
  locale.value = newLocale;
});

export const i18nPlugin: Plugin = {
  install(app) {
    app.config.globalProperties.$t = i18n.t.bind(i18n);
    app.config.globalProperties.$i18n = i18n;

    app.provide('i18n', i18n);
    app.provide('locale', locale);
  },
};

// Composable
export function useI18n() {
  return {
    t: i18n.t.bind(i18n),
    locale,
    setLocale: (newLocale: string) => i18n.setLocale(newLocale),
  };
}

// Usage in component
<script setup>
import { useI18n } from './i18n';

const { t, locale, setLocale } = useI18n();
</script>

<template>
  <div>
    <h1>{{ t('welcome') }}</h1>
    <button @click="setLocale('fr')">Français</button>
  </div>
</template>

Svelte

import { createI18n } from '@vielzeug/i18nit';
import { writable } from 'svelte/store';

const i18n = createI18n({ locale: 'en' });
export const locale = writable(i18n.getLocale());

i18n.subscribe((newLocale) => {
  locale.set(newLocale);
});

export const t = i18n.t.bind(i18n);
export const setLocale = i18n.setLocale.bind(i18n);

// Usage
<script>
  import { t, setLocale } from './i18n';
</script>

<h1>{$t('welcome')}</h1>
<button on:click={() => setLocale('fr')}>Français</button>

Best Practices

1. Organize Translations by Feature

const messages = {
  en: {
    'auth.login.title': 'Login',
    'auth.register.title': 'Register',
    'dashboard.stats.users': 'Users',
    'dashboard.stats.revenue': 'Revenue',
  },
};

2. Use Namespaces for Large Apps

const authTranslations = i18n.namespace('auth');
const dashboardTranslations = i18n.namespace('dashboard');

3. Lazy Load Translations

const i18n = createI18n({
  loaders: {
    'en-US': () => import('./locales/en-US.json'),
    'es-ES': () => import('./locales/es-ES.json'),
  },
});

4. Type-Safe Translation Keys

type TranslationKeys = 'auth.login.title' | 'auth.register.title' | 'dashboard.welcome';

function t(key: TranslationKeys, vars?: Record<string, unknown>) {
  return i18n.t(key, vars);
}

TypeScript Support

Full TypeScript support with type inference:

import { createI18n, type Messages, type I18nConfig } from '@vielzeug/i18nit';

// Define your messages type
interface MyMessages extends Messages {
  greeting: string;
  items: {
    zero: string;
    one: string;
    other: string;
  };
}

const config: I18nConfig = {
  locale: 'en',
  messages: {
    en: {
      greeting: 'Hello!',
      items: { zero: 'No items', one: 'One item', other: '{count} items' },
    } satisfies MyMessages,
  },
};

const i18n = createI18n(config);

📖 Documentation

📄 License

MIT © Helmuth Saatkamp

🤝 Contributing

Contributions are welcome! Check our GitHub repository.

🔗 Links


Part of the Vielzeug ecosystem – A collection of type-safe utilities for modern web development.