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

@logistically/i18n

v1.2.0

Published

Enterprise-grade internationalization (i18n) library for NestJS microservices with RTL support, tree shaking, and performance optimizations

Readme

@logistically/i18n

Enterprise-grade internationalization (i18n) library for NestJS microservices with RTL support, tree shaking, and performance optimizations.

🚀 Features

  • Multi-locale support - Support for unlimited locales
  • RTL language support - Full support for Arabic, Hebrew, Persian, Urdu, and other RTL languages
  • Parameter interpolation - Dynamic content in translations
  • Number formatting - Locale-aware number formatting with proper numeral systems
  • Date formatting - Comprehensive date formatting with locale-specific numerals and calendars
  • Currency formatting - Native Intl API integration for currency formatting
  • Caching with TTL - Performance optimization
  • Statistics tracking - Monitor translation usage
  • Fallback strategies - Graceful handling of missing translations
  • Debug logging - Comprehensive logging for troubleshooting
  • Type safety - Full TypeScript support
  • Dependency injection - Seamless NestJS integration
  • Elegant exception handling - Translated exceptions with clean syntax
  • Enhanced decorators - Multi-source locale extraction (JWT, cookies, headers, query params)
  • GraphQL integration - Apollo Server plugin with automatic field translation
  • Performance testing - Comprehensive performance benchmarks
  • Integration testing - Real NestJS usage validation

📦 Installation

npm install @logistically/i18n

🎯 Quick Start

1. Setup Module

import { Module } from '@nestjs/common';
import { TranslationModule } from '@logistically/i18n';

@Module({
  imports: [
    TranslationModule.forRoot({
      serviceName: 'profile-service',
      defaultLocale: 'en',
      supportedLocales: ['en', 'fr', 'es', 'de', 'ar', 'he', 'fa', 'ur'],
      translationsPath: 'src/translations',
      debug: false,
      fallbackStrategy: 'default',
      cache: { enabled: true, ttl: 3600 },
      pluralization: { enabled: true },
      rtl: { enabled: true }
    })
  ]
})
export class AppModule {}

2. Create Translation Files

// src/translations/en.json
{
  "PROFILE.NOT_FOUND": "Profile not found: ${profileId}",
  "PROFILE.INVALID_TYPE": "Invalid profile type: ${profileType}",
  "VALIDATION.MAX_FILES": "Cannot upload more than ${maxFiles} files",
  "WELCOME.MESSAGE": "Welcome, ${userName}!",
  "ITEMS.COUNT": {
    "one": "1 item",
    "other": "${count} items"
  }
}

// src/translations/fr.json
{
  "PROFILE.NOT_FOUND": "Profil introuvable: ${profileId}",
  "PROFILE.INVALID_TYPE": "Type de profil invalide: ${profileType}",
  "VALIDATION.MAX_FILES": "Impossible de télécharger plus de ${maxFiles} fichiers",
  "WELCOME.MESSAGE": "Bienvenue, ${userName}!",
  "ITEMS.COUNT": {
    "one": "1 élément",
    "other": "${count} éléments"
  }
}

// src/translations/ar.json
{
  "PROFILE.NOT_FOUND": "الملف الشخصي غير موجود: ${profileId}",
  "PROFILE.INVALID_TYPE": "نوع الملف الشخصي غير صحيح: ${profileType}",
  "VALIDATION.MAX_FILES": "لا يمكن رفع أكثر من ${maxFiles} ملف",
  "WELCOME.MESSAGE": "مرحباً، ${userName}!",
  "ITEMS.COUNT": {
    "one": "عنصر واحد",
    "other": "${count} عناصر"
  }
}

3. Use in Services

import { Injectable } from '@nestjs/common';
import { TranslationService, TranslatedExceptions } from '@logistically/i18n';

@Injectable()
export class ProfileService {
  constructor(private translationService: TranslationService) {}

  async getProfile(profileId: string, locale: string = 'en') {
    const profile = await this.profileRepository.findById(profileId);
    
    if (!profile) {
      // 🎯 Elegant translated exception
      throw TranslatedExceptions.notFound('PROFILE.NOT_FOUND', {
        locale,
        params: { profileId }
      });
    }
    
    return profile;
  }

  // Basic translation
  getWelcomeMessage(locale: string, userName: string) {
    return this.translationService.translate('WELCOME.MESSAGE', locale, {
      userName
    });
  }

  // Pluralization
  getItemCount(locale: string, count: number) {
    return this.translationService.translatePlural('ITEMS.COUNT', count, locale, {
      count
    });
  }

  // Date formatting
  getFormattedDate(locale: string, date: Date) {
    return this.translationService.formatDateForLocale(date, locale, {
      format: 'full'
    });
  }

  // Number formatting
  getFormattedNumber(locale: string, number: number) {
    return this.translationService.formatNumberForLocale(number, locale);
  }

  // RTL text detection
  getTextDirection(text: string) {
    return this.translationService.getTextDirection(text);
  }
}

4. Use Enhanced Decorators

The library provides powerful decorators for extracting locale from multiple sources:

import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { 
  Locale, 
  LocaleFromJWT, 
  LocaleFromCookies, 
  LocaleFromHeaders, 
  LocaleFromQuery,
  TranslationParams 
} from '@logistically/i18n';

@Controller('profiles')
export class ProfileController {
  @Get(':id')
  async getProfile(
    @Param('id') id: string,
    @Locale() locale: string,                    // Multi-source (JWT > Cookies > Headers > Query)
    @LocaleFromJWT() jwtLocale: string | null,  // JWT token only
    @LocaleFromCookies() cookieLocale: string | null,  // Cookies only
    @LocaleFromHeaders() headerLocale: string | null,  // Headers only
    @LocaleFromQuery() queryLocale: string | null,     // Query params only
    @TranslationParams() params: any
  ) {
    return { 
      message: 'Profile loaded', 
      detectedLocale: locale,
      sources: { jwtLocale, cookieLocale, headerLocale, queryLocale },
      params 
    };
  }

  @Post('upload')
  async uploadFiles(
    @Body() body: any,
    @Locale() locale: string,
    @TranslationParams() params: any
  ) {
    const maxFiles = 10;
    if (body.files.length > maxFiles) {
      throw TranslatedExceptions.badRequest('VALIDATION.MAX_FILES', {
        locale,
        params: { maxFiles }
      });
    }
    return { success: true };
  }
}

Locale Sources Priority (for @Locale() decorator):

  1. JWT Token - Authorization: Bearer <token> (highest priority)
  2. Cookies - locale, language, lang cookies
  3. Headers - Accept-Language, X-Locale, Accept-Locale
  4. Query Parameters - ?locale=, ?language=, ?lang=
  5. Default - Falls back to configured default locale

5. GraphQL Integration

The library provides seamless GraphQL integration with Apollo Server:

import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { ApolloI18nPlugin, i18nSchemaExtensions } from '@logistically/i18n/graphql';
import { TranslationService } from '@logistically/i18n';

// Create translation service
const translationService = new TranslationService({
  serviceName: 'graphql-service',
  defaultLocale: 'en',
  supportedLocales: ['en', 'fr', 'es', 'ar'],
  translationsPath: 'src/translations'
});

// Create Apollo server with i18n plugin
const server = new ApolloServer({
  typeDefs: [yourSchema, i18nSchemaExtensions],
  resolvers: [yourResolvers],
  plugins: [
    new ApolloI18nPlugin({
      translationService,
      defaultLocale: 'en',
      supportedLocales: ['en', 'fr', 'es', 'ar']
    })
  ]
});

Use @i18n Directive

type User {
  id: ID!
  name: String! @i18n(key: "USER.NAME")
  bio: String @i18n(key: "USER.BIO")
  status: String! @i18n(key: "USER.STATUS")
}

type Product {
  id: ID!
  name: String! @i18n(key: "PRODUCT.NAME")
  description: String! @i18n(key: "PRODUCT.DESCRIPTION")
  status: String! @i18n(key: "PRODUCT.STATUS", params: { status: "status" })
}

Query with Automatic Translation

query GetUser($id: ID!) {
  user(id: $id) {
    id
    name  # Automatically translated based on Accept-Language header
    bio   # Automatically translated
    status # Automatically translated
  }
}

For detailed GraphQL integration guide, see GRAPHQL_GUIDE.md.

JWT Token Format:

{
  "sub": "user123",
  "locale": "fr",        // Preferred field name
  "language": "es",      // Alternative field name
  "lang": "de"          // Alternative field name
}

🎨 Advanced Usage

Exception Handling

import { TranslatedExceptions } from '@logistically/i18n';

// Different exception types
throw TranslatedExceptions.notFound('PROFILE.NOT_FOUND', {
  locale: 'fr',
  params: { profileId: '123' }
});

throw TranslatedExceptions.badRequest('VALIDATION.ERROR', {
  locale: 'en',
  params: { field: 'email' }
});

throw TranslatedExceptions.internalServerError('SYSTEM.ERROR', {
  locale: 'en',
  params: { service: 'database' }
});

// Custom HTTP status
throw TranslatedExceptions.http('CUSTOM.ERROR', 422, {
  locale: 'en',
  params: { reason: 'validation_failed' }
});

RTL Language Support

// Check if locale is RTL
const isRTL = translationService.isRTLLocale('ar'); // true
const isRTL = translationService.isRTLLocale('he'); // true
const isRTL = translationService.isRTLLocale('en'); // false

// Get RTL information
const rtlInfo = translationService.getRTLInfo('ar');
// {
//   isRTL: true,
//   direction: "rtl",
//   script: "Arab",
//   name: "Arabic"
// }

// Get text direction for mixed content
const direction = translationService.getTextDirection('Hello مرحبا'); // "auto"
const direction = translationService.getTextDirection('مرحبا'); // "rtl"
const direction = translationService.getTextDirection('Hello'); // "ltr"

// Translation with RTL support
const result = translationService.translateWithRTL('PROFILE.NOT_FOUND', 'ar', { profileId: '123' });
// {
//   text: "الملف الشخصي غير موجود: 123",
//   rtl: { isRTL: true, direction: "rtl" }
// }

Number and Date Formatting

// Number formatting with locale-specific numerals
const formattedNumber = translationService.formatNumberForLocale(1234.56, 'ar');
// Output: "١٬٢٣٤٫٥٦" (Arabic numerals)

const formattedNumber = translationService.formatNumberForLocale(1234.56, 'he');
// Output: "קכד״קנו" (Hebrew numerals)

// Date formatting
const formattedDate = translationService.formatDateForLocale(new Date(), 'ar', {
  format: 'full'
});
// Output: "١٥ يناير ٢٠٢٤" (Arabic numerals)

// Date range formatting
const formattedRange = translationService.formatDateRangeForLocale(
  new Date('2024-01-15'), 
  new Date('2024-01-20'), 
  'he'
);
// Output: "15 ינואר 2024 - 20 ינואר 2024" (Hebrew numerals)

// Relative date formatting
const relativeDate = translationService.formatRelativeDate(new Date(), 'en');
// Output: "today", "yesterday", "in 2 days", etc.

Statistics and Monitoring

// Get translation statistics
const stats = translationService.getStats();
console.log(stats);
// {
//   totalRequests: 1000,
//   successfulTranslations: 950,
//   failedTranslations: 50,
//   cacheHits: 800,
//   cacheMisses: 200,
//   localeUsage: { en: 600, fr: 400 },
//   keyUsage: { 'PROFILE.NOT_FOUND': 100 }
// }

// Clear cache
translationService.clearCache();

// Reload translations
translationService.reloadTranslations();

Custom Configuration

TranslationModule.forRoot({
  serviceName: 'my-service',
  defaultLocale: 'en',
  supportedLocales: ['en', 'fr', 'es', 'ar'],
  translationsPath: 'src/translations',
  
  // Interpolation settings
  interpolation: {
    prefix: '{{',
    suffix: '}}'
  },
  
  // Fallback strategy
  fallbackStrategy: 'key', // 'key' | 'default' | 'throw'
  
  // Caching
  cache: {
    enabled: true,
    ttl: 3600 // 1 hour
  },
  
  // Statistics
  statistics: {
    enabled: true,
    trackKeyUsage: true,
    trackLocaleUsage: true
  },
  
  // RTL support
  rtl: {
    enabled: true,
    autoDetect: true,
    wrapWithMarkers: false,
    includeDirectionalInfo: true
  },
  
  // Pluralization
  pluralization: {
    enabled: true,
    formatNumbers: true,
    useDirectionalMarkers: true,
    validatePluralRules: true,
    trackPluralizationStats: true,
    ordinal: false,
    customRules: {}
  },
  
  // Debug mode
  debug: false
});

🧪 Testing

Unit Testing

import { Test } from '@nestjs/testing';
import { TranslationModule } from '@logistically/i18n';

describe('ProfileService', () => {
  let translationService: TranslationService;

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      imports: [
        TranslationModule.forRoot({
          serviceName: 'test-service',
          debug: true
        })
      ]
    }).compile();

    translationService = module.get<TranslationService>(TranslationService);
  });

  it('should translate correctly', () => {
    const result = translationService.translate('TEST.KEY', 'en', { name: 'John' });
    expect(result).toBe('Hello John!');
  });

  it('should handle RTL text', () => {
    const isRTL = translationService.isRTLLocale('ar');
    expect(isRTL).toBe(true);
  });
});

Integration Testing

import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import { TranslationModule } from '@logistically/i18n';
import { TranslationService } from '@logistically/i18n';
import * as request from 'supertest';

describe('Translation Integration Tests', () => {
  let app: INestApplication;
  let translationService: TranslationService;

  beforeAll(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [TranslationModule.forRoot({
        serviceName: 'integration-test',
        translationsPath: './test-translations',
        defaultLocale: 'en',
        supportedLocales: ['en', 'fr', 'es'],
        pluralization: { enabled: true },
        rtl: { enabled: true },
        cache: { enabled: true, ttl: 3600 },
        debug: true
      })],
    }).compile();

    app = moduleFixture.createNestApplication();
    translationService = moduleFixture.get<TranslationService>(TranslationService);
    await app.init();
  });

  afterAll(async () => {
    await app.close();
  });

  it('should handle HTTP requests with locale detection', async () => {
    const response = await request(app.getHttpServer())
      .get('/test/translate/test-key')
      .set('accept-language', 'fr')
      .expect(200);

    expect(response.body).toBeDefined();
  });
});

Performance Testing

import { Test, TestingModule } from '@nestjs/testing';
import { TranslationModule } from '@logistically/i18n';

describe('Translation Performance Tests', () => {
  let translationService: TranslationService;

  beforeAll(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [TranslationModule.forRoot({
        serviceName: 'performance-test',
        translationsPath: './test-translations',
        defaultLocale: 'en',
        supportedLocales: ['en', 'fr', 'es'],
        pluralization: { enabled: true },
        rtl: { enabled: true },
        cache: { enabled: true, ttl: 3600 },
        debug: false
      })],
    }).compile();

    translationService = moduleFixture.get<TranslationService>(TranslationService);
  });

  it('should handle 1000 translations within 100ms', () => {
    const startTime = performance.now();
    
    for (let i = 0; i < 1000; i++) {
      translationService.translate('welcome', 'en', { name: `User${i}` });
    }
    
    const endTime = performance.now();
    const duration = endTime - startTime;
    
    expect(duration).toBeLessThan(100);
  });
});

📊 Performance

The library has been extensively performance tested and optimized:

Performance Benchmarks

| Operation | Volume | Time | Performance | |-----------|--------|------|-------------| | Mixed Text Detection | 1,000 | 0.09ms | 12,500,000 ops/sec | | Cache Hits | 1,000 | 0.26ms | 4,347,826 ops/sec | | Basic Translations | 10,000 | 3.50ms | 2,857,143 ops/sec | | RTL Text Detection | 1,000 | 0.26ms | 3,846,154 ops/sec | | Concurrent Translations | 100 | 0.14ms | 714,286 ops/sec |

Production-Ready Features

  • High load handling - 4,000 operations in 50ms
  • Sustained performance - Consistent under stress
  • Memory efficient - 23.5MB for 40k operations
  • Cache effective - 1MB cache for 1,000 entries
  • Zero memory leaks - Stable memory usage

🔧 Configuration Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | serviceName | string | - | Service name for key prefixing | | defaultLocale | string | 'en' | Default locale | | supportedLocales | string[] | ['en', 'fr', 'es', 'de', 'ar'] | Supported locales | | translationsPath | string | 'src/translations' | Path to translation files | | debug | boolean | false | Enable debug logging | | interpolation.prefix | string | '${' | Interpolation prefix | | interpolation.suffix | string | '}' | Interpolation suffix | | fallbackStrategy | 'key' \| 'default' \| 'throw' | 'default' | Fallback strategy | | cache.enabled | boolean | true | Enable caching | | cache.ttl | number | 3600 | Cache TTL in seconds | | statistics.enabled | boolean | true | Enable statistics tracking | | statistics.trackKeyUsage | boolean | true | Track key usage statistics | | statistics.trackLocaleUsage | boolean | true | Track locale usage statistics | | rtl.enabled | boolean | true | Enable RTL language support | | rtl.autoDetect | boolean | true | Auto-detect RTL text content | | rtl.wrapWithMarkers | boolean | false | Wrap text with directional markers | | rtl.includeDirectionalInfo | boolean | true | Include RTL info in metadata | | pluralization.enabled | boolean | true | Enable pluralization | | pluralization.formatNumbers | boolean | true | Enable number formatting | | pluralization.useDirectionalMarkers | boolean | true | Use RTL directional markers | | pluralization.validatePluralRules | boolean | true | Validate plural rule structure | | pluralization.trackPluralizationStats | boolean | true | Track pluralization statistics | | pluralization.ordinal | boolean | false | Enable ordinal pluralization | | pluralization.customRules | Record<string, (count: number) => string> | {} | Custom plural rules |

📚 Documentation

📖 Guides

🤝 Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes
  4. Add tests
  5. Submit a pull request

📄 License

MIT License - see LICENSE file for details.