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

@mmstack/translate

v21.1.5

Published

**Type-Safe & modular localization for Modern Angular.**

Readme

@mmstack/translate

Type-Safe & modular localization for Modern Angular.

npm version License PRs Welcome

@mmstack/translate is an opinionated internationalization (i18n) library for Angular applications built with three core priorities:

  1. Maximum Type Safety: Catch errors related to missing keys or incorrect/missing parameters at compile time.
  2. Flexible Build Process: Works as a traditional multi-build solution (like @angular/localize) OR as a single-build runtime solution.
  3. Scalable Modularity: Organize translations into namespaces (typically aligned with feature libraries) and load them on demand.

It uses the robust FormatJS Intl runtime (@formatjs/intl) for ICU message formatting, and integrates with Angular's dependency injection and routing.

Features

  • End-to-End Type Safety: Compile-time checks for:
    • Translation key existence (within a namespace).
    • Correct parameter names and types.
    • Required vs. optional parameters based on ICU message.
    • Structural consistency check when defining non-default locales.
  • 🚀 Flexible Deployment: Support both multi-build (traditional) and single-build (runtime) scenarios.
  • 📦 Namespacing: Organize translations by feature/library (e.g., 'quotes', 'userProfile', 'common').
  • 🔄 Dynamic Language Switching (Optional): Change locales at runtime with automatic translation loading.
  • 🛣️ Route-Based Locale Support (Optional): Automatic locale detection and switching based on route parameters.
  • Lazy Loading: Load namespaced translations on demand using Route Resolvers.
  • Reactive API: Includes t.asSignal() for creating computed translation signals based on signal parameters.
  • 🌍 ICU Message Syntax: Uses FormatJS runtime for robust support of variables ({name}), plural, select, and selectordinal. (Note: Complex inline date/number formats are not the focus; use Angular's built-in Pipes/format functions & use the result as variables in your translation.)
  • 🔗 Shared Namespace Support: Define common translations (e.g., 'Save', 'Cancel') in one namespace and make them type-safely accessible from others.
  • 🛠️ Template Helpers: Includes abstract Translator pipe and Translate directive for easy, type-safe templating.

Comparison

While Angular offers excellent i18n solutions like @angular/localize and transloco, @mmstack/translate aims to fill a specific niche by supporting both traditional multi-build and modern single-build approaches.

| Feature | @mmstack/translate | @angular/localize | transloco | ngx-translate | | :----------------------- | :----------------------------------: | :----------------------: | :---------------------------: | :------------------------: | | Build Process | ✅ Single or Multi-Build | ❌ Multi-Build (Typical) | ✅ Single Build | ✅ Single Build | | Translation Timing | Runtime or Build Time | Compile Time | Runtime | Runtime | | Type Safety (Keys) | ✅ Strong (Inferred from structure) | 🟡 via extraction | 🟡 Tooling/TS Files | 🟡 OK Manual/Tooling | | Type Safety (Params) | ✅ Strong (Inferred from ICU) | ❌ None | 🟡 Manual | 🟡 Manual | | Locale Switching | ✅ Dynamic (Runtime) or Page refresh | 🔄 Page Refresh Required | ✅ Dynamic (Runtime) | ✅ Dynamic (Runtime) | | Lazy Loading | ✅ Built-in (Namespaces/Resolvers) | N/A (Compile Time) | ✅ Built-in (Scopes) | ✅ Yes (Custom Loaders) | | Namespacing/Scopes | ✅ Built-in | ❌ None | ✅ Built-in (Scopes) | 🟡 Manual (File Structure) | | ICU Support | ✅ Subset (via FormatJS Runtime) | ✅ Yes (Compile Time) | ✅ Yes (Runtime Intl/Plugins) | 🟡 Via Extensions | | Signal Integration | ✅ Good (t.asSignal()) | N/A | ✅ Good (translateSignal()) | ❌ Minimal/None | | Maturity / Community | ✨ New | Core Angular | ✅ Mature / Active | ✅ Mature |

Installation

Install the library & its peer dependency, @formatjs/intl.

npm install @mmstack/translate @formatjs/intl

Configuration

Default: Multi-Build Scenario (like @angular/localize)

By default, @mmstack/translate works like @angular/localize - it uses Angular's LOCALE_ID token and expects a page refresh for locale changes. This is ideal for traditional multi-build deployments where each locale has its own build artifact.

No special configuration needed! Just provide LOCALE_ID:

// app.config.ts
import { ApplicationConfig, LOCALE_ID } from '@angular/core';

export const appConfig: ApplicationConfig = {
  providers: [
    { provide: LOCALE_ID, useValue: 'en-US' }, // Set your locale
    // ... other providers
  ],
};

The library will use this LOCALE_ID value and work exactly like @angular/localize - requiring a full page refresh to change locales.

Single-Build with Runtime Translation Loading

If you want a single build that loads translations at runtime, use provideIntlConfig():

// app.config.ts
import { ApplicationConfig, LOCALE_ID } from '@angular/core';
import { provideIntlConfig } from '@mmstack/translate';

export const appConfig: ApplicationConfig = {
  providers: [
    { provide: LOCALE_ID, useValue: 'en-US' }, // Initial/fallback locale
    provideIntlConfig({
      defaultLocale: 'en-US',
      supportedLocales: ['en-US', 'sl-SI', 'de-DE', 'fr-FR'], // Validates locale switches
    }),
    // ... other providers
  ],
};

Optional Configuration:

provideIntlConfig({
  defaultLocale: 'en-US',
  supportedLocales: ['en-US', 'sl-SI', 'de-DE'],

  // Automatically detect and respond to locale route parameter changes, should correspond with actual param name example bellow
  localeParamName: 'locale',

  // Preload default locale for synchronous fallback (rarely needed)
  preloadDefaultLocale: true,
});

Usage

The core workflow involves defining namespaces, registering them (often via lazy loading), and then using the injected translation function (t), pipe, or directive.

1. Define Namespace & Translations

Define your default locale translations (e.g., 'en-US') as a const TypeScript object. Use createNamespace to process it and generate helpers.

// Example: packages/quote/src/lib/quote.namespace.ts
import { createNamespace } from '@mmstack/translate';

// Create the namespace definition object
const ns = createNamespace('quote', {
  pageTitle: 'Famous Quotes',
  greeting: 'Hello {name}!',
  detail: {
    authorLabel: 'Author',
  },
  errors: {
    minLength: 'Quote must be at least {min} characters long.',
  },
  stats: '{count, plural, one {# quote} other {# quotes}} available',
});

export default ns.translation;

export type QuoteLocale = (typeof ns)['translation'];

export const createQuoteTranslation = ns.createTranslation;

Define other locales in separate files (for lazy loading):

// packages/quote/src/lib/quote-sl.translation.ts
import { createQuoteTranslation } from './quote.namespace';

// Shape is type-safe (errors if you have missing or additional keys)
export default createQuoteTranslation('sl-SI', {
  pageTitle: 'Znani Citati',
  greeting: 'Zdravo {name}!',
  detail: {
    authorLabel: 'Avtor',
  },
  errors: {
    minLength: 'Citat mora imeti vsaj {min} znakov.', // Variables must match original
  },
  stats: '{count, plural, =1 {# citat} =2 {# citata} few {# citati} other {# citatov}} na voljo',
});

2. Register the Namespace & Load Translations

Use registerNamespace to prepare your namespace definition and obtain the injectNamespaceT function and the resolveNamespaceTranslation resolver function.

// Example: packages/quote/src/lib/quote.t.ts
import { registerNamespace } from '@mmstack/translate';

const r = registerNamespace(
  // Default locale's compiled translation (functions as fallback)
  () => import('./quote.namespace').then((m) => m.default),
  {
    // Map other locales to promise factories (dynamic imports)
    'sl-SI': () => import('./quote-sl.translation').then((m) => m.default),
    // Add more locales as needed...
  },
);

export const injectQuoteT = r.injectNamespaceT;
export const resolveQuoteTranslations = r.resolveNamespaceTranslation;

Add the resolver to your routes:

// quote.routes.ts
import { type Routes } from '@angular/router';
import { resolveQuoteTranslations } from './quote.t';

export const QUOTE_ROUTES: Routes = [
  {
    path: '',
    component: QuoteComponent,
    resolve: {
      translations: resolveQuoteTranslations, // Loads translations before component
    },
  },
];

2b. [OPTIONAL] Configure Type-Safe Pipe and/or Directive

import { Pipe, Directive } from '@angular/core';
import { Translator, Translate } from '@mmstack/translate';
import { type QuoteLocale } from './quote.namespace';

@Pipe({
  name: 'translate',
  standalone: true,
})
export class QuoteTranslator extends Translator<QuoteLocale> {}

@Directive({
  selector: '[translate]', // Input in Translate is named 'translate'
  standalone: true,
})
export class QuoteTranslate<TInput extends string> extends Translate<TInput, QuoteLocale> {}

3. Use Translations in Components

import { Component, signal } from '@angular/core';
import { injectQuoteT } from './quote.t';
import { QuoteTranslator, QuoteTranslate } from './quote.helpers';

@Component({
  selector: 'app-quote',
  standalone: true,
  imports: [QuoteTranslator, QuoteTranslate],
  template: `
    <!-- Pipe validates key & variables match -->
    <h1>{{ 'quote.pageTitle' | translate }}</h1>
    <!-- Non-pluralized params must be string -->
    <span>{{ 'quote.errors.minLength' | translate: { min: '5' } }}</span>

    <!-- Directive replaces textContent of element -->
    <h1 translate="quote.pageTitle"></h1>
    <span [translate]="['quote.errors.minLength', { min: '5' }]"></span>
  `,
})
export class QuoteComponent {
  protected readonly count = signal(0);
  private readonly t = injectQuoteT();

  // Static translation
  private readonly author = this.t('quote.detail.authorLabel');

  // Reactive translation with signal parameters
  private readonly stats = this.t.asSignal('quote.stats', () => ({
    count: this.count(), // Must match ICU parameter (type: number)
  }));
}

4. [OPTIONAL] Route-Based Locale Detection

For applications with locale-based routing (e.g., /en-US/quotes, /sl-SI/quotes), the library can automatically detect and switch locales.

Step 1: Configure locale parameter name

// app.config.ts
import { provideIntlConfig } from '@mmstack/translate';

export const appConfig: ApplicationConfig = {
  providers: [
    provideIntlConfig({
      defaultLocale: 'en-US',
      supportedLocales: ['en-US', 'sl-SI', 'de-DE'],
      localeParamName: 'locale', // Track this route parameter automatically
    }),
  ],
};

Step 2: Add route guard for validation

// app.routes.ts
import { Routes } from '@angular/router';
import { canMatchLocale } from '@mmstack/translate';

export const routes: Routes = [
  {
    path: ':locale',
    canMatch: [canMatchLocale()], // Validates & redirects invalid locales
    children: [
      {
        path: 'quotes',
        loadChildren: () => import('./quote/quote.routes').then((m) => m.QUOTE_ROUTES),
      },
      // ... other routes
    ],
  },
];

That's it! The library will:

  • Detect locale changes from route parameters
  • Load translations on demand for the new locale
  • Update all translation outputs reactively
  • Redirect invalid locales to the default

With prefix segments:

If your locale parameter isn't the first segment (e.g., /app/:locale/...):

{
  path: 'app/:locale',
  canMatch: [canMatchLocale(['app'])], // Validates second segment
  children: [...]
}

5. [OPTIONAL] Dynamic Language Switching

For applications that need runtime language switching without page refreshes (e.g., language selector in header), use injectDynamicLocale():

import { Component } from '@angular/core';
import { injectDynamicLocale } from '@mmstack/translate';

@Component({
  selector: 'app-language-switcher',
  template: `
    <select [value]="locale()" (change)="changeLanguage($event)">
      <option value="en-US">English</option>
      <option value="sl-SI">Slovenščina</option>
      <option value="de-DE">Deutsch</option>
    </select>

    @if (locale.isLoading()) {
      <div class="spinner">Loading translations...</div>
    }
  `,
})
export class LanguageSwitcherComponent {
  protected readonly locale = injectDynamicLocale();

  changeLanguage(event: Event) {
    const target = event.target as HTMLSelectElement;
    this.locale.set(target.value); // Automatically loads missing translations
  }
}

Features:

  • Validates against supportedLocales (if configured)
  • Automatically loads missing namespace translations
  • Provides isLoading() signal for UI feedback
  • Works with route-based locales

Important Note for Pure Pipes:

Due to Angular's memoization, pure pipes don't automatically react to locale changes. Solutions:

// Option 1: Pass locale as parameter (recommended)
{{ 'common.yes' | translate : locale() }}

// Option 2: Make pipe impure (not recommended for performance)
@Pipe({
  name: 'translate',
  pure: false,
})
export class QuoteTranslator extends Translator<QuoteLocale> {}

6. [OPTIONAL] Creating a Shared/Common Namespace

A shared namespace allows you to define common translations (e.g., 'Save', 'Cancel', 'Yes', 'No') once and use them type-safely across all other namespaces.

Step 1: Define a shared namespace

// packages/common/src/lib/common.namespace.ts
import { createNamespace } from '@mmstack/translate';

const ns = createNamespace('common', {
  yes: 'Yes',
  no: 'No',
  save: 'Save',
  cancel: 'Cancel',
  delete: 'Delete',
});

export default ns.translation;
export type CommonLocale = (typeof ns)['translation'];
export const createCommonTranslation = ns.createTranslation;

// Export this for other namespaces to use
export const createAppNamespace = ns.createMergedNamespace;

Step 2: Register the common namespace at the top level

// common.t.ts
import { registerNamespace } from '@mmstack/translate';

const r = registerNamespace(() => import('./common.namespace').then((m) => m.default), {
  'sl-SI': () => import('./common-sl.translation').then((m) => m.default),
});

export const injectCommonT = r.injectNamespaceT;
export const resolveCommonTranslations = r.resolveNamespaceTranslation;
// app.routes.ts - resolve at top level
export const routes: Routes = [
  {
    path: '',
    resolve: {
      common: resolveCommonTranslations, // Load common translations first
    },
    children: [
      // ... other routes
    ],
  },
];

Step 3: Use the shared namespace factory in other namespaces

// packages/quote/src/lib/quote.namespace.ts
import { createAppNamespace } from '@org/common'; // Your import path

const ns = createAppNamespace('quote', {
  pageTitle: 'Famous Quotes',
  // ... other translations
});

export default ns.translation;
// ... rest remains the same

Step 4: Access both namespaces in components

@Component({...})
export class QuoteComponent {
  private readonly t = injectQuoteT();

  // Access common namespace translations
  private readonly yesLabel = this.t('common.yes');
  private readonly saveLabel = this.t('common.save');

  // Access quote namespace translations
  private readonly title = this.t('quote.pageTitle');
}

Helper Functions

Core Injection Functions

injectDefaultLocale(): string
Returns the configured default locale or falls back to LOCALE_ID.

injectSupportedLocales(): string[]
Returns the array of supported locales or defaults to [defaultLocale].

injectIntl(): Signal<IntlShape>
Directly access the FormatJS Intl instance for advanced formatting needs.

import { injectIntl } from '@mmstack/translate';

const intl = injectIntl();
const formatted = intl().formatNumber(1234.56, {
  style: 'currency',
  currency: 'EUR',
});

injectDynamicLocale(): WritableSignal<string> & { isLoading: Signal<boolean> }
Inject a dynamic locale signal for runtime language switching.

Route Utilities

canMatchLocale(prefixSegments?: string[]): CanMatchFn
Route guard that validates locale parameters against supportedLocales and redirects invalid locales to the default.

Advanced: Architecture & Performance

Resource-Based Translation Loading

The library uses Angular's resource() API for efficient, reactive translation loading:

  • Automatic request deduplication
  • Built-in loading states
  • Cancellation support via AbortSignal
  • Better error handling

On-Demand Translation Loading

When switching locales dynamically, the library:

  1. Checks which namespaces need translations for the new locale
  2. Loads only the missing translations in parallel
  3. Updates all reactive outputs automatically
  4. Falls back to the default locale if unavailable

Remote / Unsafe Namespaces

For cases where you need to load translations from a remote API (where keys aren't known at compile-time), use registerRemoteNamespace. This provides an untyped experience but allows you to integrate dynamic content into the same system.

import { registerRemoteNamespace } from '@mmstack/translate';

// Returns an untyped t function: t('any.key')
const { injectNamespaceT: injectRemoteT } = registerRemoteNamespace('remote', () => fetch('/api/en').then((r) => r.json()), {
  'sl-SI': () => fetch('/api/sl').then((r) => r.json()),
});

// usage
const t = injectRemoteT();

// .asSignal variants also work
const value = t('remote.myKey');
const valueThatNeedsProps = t('remote.myOtherKey', {
  name: 'John',
});

Formatters

The library includes a set of reactive formatters that automatically adapt to the current locale. They are standalone functions that do not require dependency injection, making them easy to use anywhere.

Note: For reactivity, wrap them in a computed() if the input signals change or if you want them to react to dynamic locale changes.

Available formatters:

  • formatDate: Wraps Intl.DateTimeFormat
  • formatNumber: Wraps Intl.NumberFormat
  • formatCurrency: Wraps Intl.NumberFormat (currency style)
  • formatPercent: Wraps Intl.NumberFormat (percent style)
  • formatList: Wraps Intl.ListFormat
  • formatRelativeTime: Wraps Intl.RelativeTimeFormat
  • formatDisplayName: Wraps Intl.DisplayNames

Example:

import { computed, signal } from '@angular/core';
import { formatCurrency, formatDate } from '@mmstack/translate';

export class MyComponent {
  readonly price = signal(1234.56);
  readonly date = new Date();

  // Reacts to price changes OR locale changes
  readonly displayPrice = computed(() => formatCurrency(this.price(), 'EUR'));

  // Reacts to locale changes
  readonly displayDate = computed(() => formatDate(this.date));
}

Migration from Other Libraries

From @angular/localize

@mmstack/translate can work exactly like @angular/localize by default - no migration needed for the build process! Simply:

  1. Define your translations using createNamespace
  2. Register namespaces with resolvers
  3. Use the translation functions/pipes/directives

The main difference is the namespace organization and type safety.

From transloco/ngx-translate

If you're migrating from a runtime-only solution:

  1. Configure provideIntlConfig() with your supported locales
  2. Use localeParamName if you have route-based locales
  3. Use injectDynamicLocale() for programmatic locale switching
  4. Convert your translation JSON files to TypeScript using createNamespace
  5. Update component/template usage to use the type-safe APIs

Contributing

Contributions, issues, and feature requests are welcome! Please see CONTRIBUTING.md for details.

License

MIT © Mihael Mulec