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

@qzlcorp/typed-i18n

v1.1.0

Published

Type-safe i18n library with zero dependencies

Readme

@qzlcorp/typed-i18n

A zero-dependency, TypeScript-first i18n library with module-based organization and compile-time type safety. Inspired by modern i18n libraries, designed for scalability and code-splitting.

Typed-i18n type-safety demo

Watch how TypeScript catches translation key errors at compile time, ensuring type-safe i18n throughout your app.

Live Demo

🚀 View React Demo - Interactive demo showcasing dynamic module loading, locale switching, and type-safe translations.

Features

  • Module-based architecture: Organize translations by feature/page for better code-splitting
  • Compile-time type safety: All translation keys are validated at build time
  • Shape validation: TypeScript enforces that all locale files match the same structure
  • Modern API: Simple t('key') syntax with namespace support
  • Mutable locale: Change language dynamically with setLocale()
  • Fallback support: Automatic fallback to default locale for missing translations
  • Zero runtime dependencies: Lightweight and performant

Installation

npm install @qzlcorp/typed-i18n

Quick Start

1. Create translation files

// locales/common/en.json
{
  "hello": "Hello",
  "goodbye": "Goodbye"
}

// locales/common/fr.json
{
  "hello": "Bonjour", 
  "goodbye": "Au revoir"
}

2. Define modules and create i18n instance

import { defineModule, createI18n } from '@qzlcorp/typed-i18n';
import commonEn from './locales/common/en.json';
import commonFr from './locales/common/fr.json';

// Define module with type validation - all locales must match structure
// ⚠️ IMPORTANT: You MUST provide the reference type explicitly
const common = defineModule('common')<typeof commonEn>({
  en: commonEn,
  fr: commonFr  // TypeScript validates this matches commonEn structure
});

const i18n = createI18n({
  locale: 'en',
  fallbackLocale: 'en',
  modules: { common }
});

3. Use translations

// Translate with namespace.key syntax
i18n.t('common.hello');  // "Hello"

// Change locale dynamically
i18n.setLocale('fr');
i18n.t('common.hello');  // "Bonjour"

// Interpolation
i18n.t('common.greeting', { name: 'John' });  // Supports {{name}} in JSON

// Get nested objects (i18next returnObjects compatible)
const messages = i18n.t('common.messages', { returnObjects: true });
// Returns the entire messages object instead of a string

Advanced Usage

Multiple Modules

Perfect for code-splitting by feature or route:

const common = defineModule('common')<typeof commonEn>({
  en: commonEn,
  fr: commonFr
});

const dashboard = defineModule('dashboard')<typeof dashboardEn>({
  en: dashboardEn,
  fr: dashboardFr
});

const i18n = createI18n({
  locale: 'en',
  modules: { common, dashboard }
});

i18n.t('common.hello');          // ✅ Typed
i18n.t('dashboard.title');       // ✅ Typed
i18n.t('dashboard.invalid');     // ❌ Type error!

Dynamic Module Loading (React/Code-Splitting)

For lazy-loading translations per route with full type safety:

// Load common translations upfront
const i18n = createI18n({
  locale: 'en',
  modules: { common }
});

// Later, dynamically load and add dashboard module
const dashboardModule = defineModule('dashboard')<typeof dashboardEn>({
  en: await import('./locales/dashboard/en.json'),
  fr: await import('./locales/dashboard/fr.json')
});

// addModule returns a NEW typed instance - use it!
const i18n2 = i18n.addModule(dashboardModule);

// ✅ Both modules are fully typed
i18n2.t('common.hello');         // Works
i18n2.t('dashboard.title');      // Fully typed!

// ⚠️ Original instance doesn't know about new module
i18n.t('dashboard.title');       // ❌ Type error (as expected)

Important: Due to TypeScript limitations, you must use the returned instance to get updated types. The original instance still works at runtime but loses type safety for new modules.

Best practice for React: Store the i18n instance in React Context and update the context value when adding modules.

Nested Keys

{
  "dashboard": {
    "stats": {
      "clicks": "{{count}} clicks"
    }
  }
}
i18n.t('common.dashboard.stats.clicks', { count: 5 });  // "5 clicks"

Get Available Locales

const locales = i18n.getLocales();  // ['en', 'fr']

Fallback Locale

const i18n = createI18n({
  locale: 'de',  // German not available
  fallbackLocale: 'en',
  modules: { common }
});

i18n.t('common.hello');  // Falls back to "Hello" (English)

API Reference

defineModule(namespace)

Creates a typed translation module. Uses curried syntax for better type inference.

⚠️ CRITICAL: You MUST provide the reference type explicitly!

// ✅ CORRECT - Explicit reference type
const module = defineModule('namespace')<typeof referenceJson>({
  en: enJson,
  fr: frJson  // Validated against referenceJson at compile time
});

// ❌ WRONG - Missing reference type loses type safety
const module = defineModule('namespace')({
  en: enJson,
  fr: frJson  // No validation! Runtime warning will be shown
});

Why is this required?

  • Without the explicit type parameter, TypeScript cannot validate that all locale files match
  • You lose compile-time type safety for cross-locale validation
  • Runtime warnings will alert you, but catching errors at build time is better

createI18n(options)

Creates an i18n instance.

Options:

  • locale: Current active locale
  • fallbackLocale?: Fallback when translation missing
  • modules: Object containing all translation modules

Returns:

  • t(key, params?): Translate function
    • t(key, params): Legacy signature for simple parameter interpolation
    • t(key, options): New signature supporting { params?, returnObjects? }
  • setLocale(locale): Change current locale
  • getLocale(): Get current locale
  • addModule(module): Add module dynamically, returns new typed instance
  • getLocales(): Get all available locales

Translation Options:

interface TranslateOptions {
  params?: Record<string, string | number | boolean>;
  returnObjects?: boolean;  // Return nested objects instead of strings
}

// Simple parameter interpolation
i18n.t('common.greeting', { name: 'World' });

// Return nested objects with FULL TYPE SAFETY (i18next compatible)
const stats = i18n.t('dashboard.stats', { returnObjects: true });
// TypeScript infers the exact type at that path!
// stats has type: { clicks: string; views: string }
console.log(stats.clicks); // ✅ Fully typed property access

// Get deeply nested objects
const config = i18n.t('app.config', { returnObjects: true });
// config has the exact type from your JSON structure

// Both params and returnObjects
const data = i18n.t('config.settings', { 
  params: { count: 5 }, 
  returnObjects: false  // Returns string with interpolation
});

Type-safe returnObjects: When you use returnObjects: true, TypeScript automatically infers the exact type at that translation path. This means:

  • ✅ No manual type assertions needed
  • ✅ Full autocomplete for nested properties
  • ✅ Compile-time errors if you access wrong properties
  • ✅ Refactoring safety across your codebase
// Example with nested structure
const dashboard = i18n.t('dashboard', { returnObjects: true });
// dashboard is typed as: {
//   title: string;
//   stats: { clicks: string; views: string; }
//   settings: { theme: string; }
// }

dashboard.stats.clicks;   // ✅ Typed!
dashboard.stats.invalid;  // ❌ Type error!

Important: addModule() returns a new instance with updated types to preserve type safety:

const i18n = createI18n({ locale: 'en', modules: { common } });
const i18n2 = i18n.addModule(settingsModule);
i18n2.t('settings.key'); // ✅ Fully typed!

TypeScript Tips

⚠️ Always Provide the Reference Type

Due to TypeScript limitations, we cannot completely prevent type inference, but you must always provide the reference type explicitly:

// ✅ CORRECT - Explicit reference type ensures type safety
const common = defineModule('common')<typeof enJson>({
  en: enJson,
  fr: frJson  // ✓ Compile-time validation that fr matches enJson
});

// ❌ WRONG - Type inference loses cross-locale validation  
const common = defineModule('common')({
  en: enJson,
  fr: frJson  // ✗ No compile-time check! Runtime warning only
});

What happens without explicit type?

  • TypeScript infers a union type from all provided locales
  • Mismatched structures can pass type checking
  • You lose the main benefit of this library
  • A runtime warning will be logged to console

Type Safety

All translation keys are typed:

i18n.t('common.hello');     // ✅ Valid
i18n.t('common.invalid');   // ❌ Type error
i18n.t('wrongNs.hello');    // ❌ Type error

Runtime Validation

When the reference type is omitted and locale structures don't match, you'll see a detailed warning:

// ❌ Missing reference type with mismatched structures
const example = defineModule('example')({
  en: { hello: "Hello", goodbye: "Goodbye" },
  fr: { hello: "Bonjour" }  // Missing 'goodbye'
});

// Console output:
// [defineModule] Warning: Module 'example' has structural differences between locales.
// Reference locale: 'en', comparing with: 'fr'
// Differences:
//   - Missing key in locale: goodbye
// 
// To enable compile-time validation, provide an explicit reference type:
// defineModule('example')<typeof enJson>({ ... })

This helps catch structure mismatches even when compile-time checks are bypassed.

React Integration

Use @qzlcorp/typed-i18n-react for first-class React bindings:

// i18n.ts
const common = defineModule('common')<typeof enCommon>({ en: enCommon, fr: frCommon });
export const i18n = createI18n({ locale: 'en', fallbackLocale: 'en', modules: { common } });
export type I18nModules = { common: typeof common };

// App root
<I18nProvider i18n={i18n}>
  <App />
</I18nProvider>

// Component with strict key checking
const { t } = useTranslation<I18nModules>();
t('common.hello');      // ✅
t('common.oops');       // ❌ compile-time error

// Without generic (not recommended): keys become loose `${string}.${string}`
const { t: tLoose } = useTranslation();
tLoose('common.oops');  // ✅ compiles (no static safety)

Dynamic module loading returns a widened instance you should propagate via context/state for updated key unions:

const settings = defineModule('settings')<typeof enSettings>({ en: enSettings, fr: frSettings });
const i18n2 = i18n.addModule(settings); // new instance with settings.* keys typed

Support

License

MIT © Q.Z.L Corp.