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

@apollo-deploy/aurebesh

v0.1.4

Published

Universal i18n, pluralization, smart inline syntax, and real-time FX currency engine for Next.js 16 apps

Readme

Aurebesh

"Aurebesh" — The ancient alphabet of the Star Wars galaxy, used by Jedi and Sith to preserve knowledge across worlds and generations.

TypeScript Next.js React License


What it covers

| Domain | Capability | |--------|-----------| | Translation | Namespace-scoped JSON files, dot-notation key lookup, SSR hydration, lazy HTTP loading | | Pluralization | CLDR plural rules (cardinal + ordinal), exact count, numeric intervals (2-5, 6+), gender/case inflection | | Inline shorthands | {{count : singular \| plural}} for plurals, {{selector : key: text \| fallback}} for selects | | Interpolation | {{token}} with HTML escaping, nested dot-path tokens, configurable delimiters | | Formatting | Locale-aware dates (4 presets + custom), times, relative time, numbers, percentages, lists | | Currency | ISO 4217 money formatting, zero-decimal detection, compact notation | | FX Engine | Live exchange rates (exchangerate.host), static fallback, failover chain, Next.js "use cache" integration, tag-based invalidation | | Locale detection | Accept-Language header parsing with quality weights, cookie-first with RSC/client parity | | Observability | Missing-key reporter (in-memory + pluggable), translation coverage reports for CI/CD | | React | useTranslation, useMoney, FormatService, locale persistence (localStorage + cookie), SSR hydration provider |


Architecture

@apollo-deploy/aurebesh
├── @apollo-deploy/aurebesh/config          → Locale constants, I18nConfig, createI18nConfig()
├── @apollo-deploy/aurebesh/format          → Pure Intl formatters (date, time, number, currency, relative, list)
├── @apollo-deploy/aurebesh/currency        → Price types, resolution policies, FX snapshots, provider chain
├── @apollo-deploy/aurebesh/react           → Client hooks & I18nProvider   ← "use client"
├── @apollo-deploy/aurebesh/server          → SSR loaders, locale detection, money(), fx-refresh handler  ← server-only
└── @apollo-deploy/aurebesh/observability   → Missing-key reporter interface + InMemoryMissingTranslationReporter

Server/client boundary is strictly enforced. @apollo-deploy/aurebesh/server imports server-only — any accidental client import fails at build time. @apollo-deploy/aurebesh/react carries "use client" directives.


Installation

bun install @apollo-deploy/aurebesh

Quick start

1. Create your app config

// lib/i18n.config.ts
import '@apollo-deploy/aurebesh/config';

export const i18nConfig = createI18nConfig(['settings', 'billing']);
//                                          ^— additional namespaces beyond 'common' + 'auth'

2. Lay out translation files

public/
  locales/
    en/
      common.json
      auth.json
      settings.json
    es/
      common.json
      auth.json
      settings.json
    fr/
      common.json
      auth.json
      settings.json

3. Wrap your root layout (RSC)

// app/layout.tsx
import { join } from 'node:path';
import '@apollo-deploy/aurebesh/react';
import '@apollo-deploy/aurebesh/server';
import '@apollo-deploy/aurebesh/currency';
import '@apollo-deploy/aurebesh/currency'; // re-exported from server
import { i18nConfig } from '@/lib/i18n.config';

ensureDefaultFxProviders(); // idempotent — safe to call on every render

export default async function RootLayout({ children }: { children: React.ReactNode }) {
  const i18nState = await getI18nServerState('en', i18nConfig.namespaces, {
    publicDir: join(process.cwd(), 'public'),
  });

  return (
    <html lang="en">
      <body>
        <I18nProvider config={i18nConfig} {...i18nState}>
          {children}
        </I18nProvider>
      </body>
    </html>
  );
}

4. Translate in client components

// components/Greeting.tsx
'use client';
import '@apollo-deploy/aurebesh/react';

export function Greeting({ name }: { name: string }) {
  const { t, locale, changeLocale, format } = useTranslation('common');

  return (
    <div>
      <h1>{t('greeting', { name })}</h1>
      <p>{t('itemCount', { count: 5 })}</p>
      <time>{format.date(new Date(), 'long')}</time>
      <button onClick={() => changeLocale('es')}>Español</button>
    </div>
  );
}

Translation file format

Plain keys & nested keys

{
  "greeting": "Hello, {{name}}!",
  "navigation": {
    "home": "Home",
    "settings": "Settings"
  }
}
t('greeting', { name: 'Zara' })        // → "Hello, Zara!"
t('navigation.home')                   // → "Home"

Plural forms — flat sibling style (i18next-compatible)

{
  "itemCount": "{{count}} item",
  "itemCount_other": "{{count}} items",
  "itemCount_zero": "No items"
}
t('itemCount', { count: 0 })   // → "No items"
t('itemCount', { count: 1 })   // → "1 item"
t('itemCount', { count: 5 })   // → "5 items"

Plural forms — nested object style

{
  "messages": {
    "one":   "You have {{count}} message",
    "other": "You have {{count}} messages",
    "0":     "No messages",
    "2-5":   "A few messages ({{count}})",
    "6+":    "Many messages"
  }
}

Resolution priority: exact countintervalCLDR formother

Ordinal plurals

{
  "rank": {
    "one":   "{{count}}st place",
    "two":   "{{count}}nd place",
    "few":   "{{count}}rd place",
    "other": "{{count}}th place"
  }
}
t('rank', { count: 1, ordinal: true })  // → "1st place"
t('rank', { count: 3, ordinal: true })  // → "3rd place"

Gender/case inflection

{
  "role": {
    "male":   { "nominative": "actor",   "other": "actor" },
    "female": { "nominative": "actress", "other": "actress" },
    "other":  { "nominative": "performer" }
  }
}
t('role', { gender: 'female', case: 'nominative' }) // → "actress"

Inline plural and select shorthands

For plurals or gender variants embedded inside a longer sentence, use the inline shorthand syntax. Everything stays inside {{}} — the same delimiters used for plain variables.

Plural — 2 branches (singular | plural)

{
  "items":   "You have {{count}} {{count : item | items}}.",
  "results": "Found {{count : # result | # results}} matching your search."
}
t('items',   { count: 1 }) // → "You have 1 item."
t('items',   { count: 5 }) // → "You have 5 items."
t('results', { count: 0 }) // → "Found 0 results matching your search."
t('results', { count: 1 }) // → "Found 1 result matching your search."

Plural — 3 branches (zero | singular | plural)

When you need a distinct zero form, add a third branch:

{
  "messages": "{{count : No unread messages | # unread message | # unread messages}}"
}
t('messages', { count: 0 }) // → "No unread messages"
t('messages', { count: 1 }) // → "1 unread message"
t('messages', { count: 4 }) // → "4 unread messages"

# inside a branch is replaced with the actual count value.

Select (gender, status, any discrete value)

{
  "confirmed": "{{gender : male: He | female: She | They}} confirmed the order.",
  "status":    "Status: {{state : active: Active | paused: Paused | Unknown}}"
}
t('confirmed', { gender: 'male' })   // → "He confirmed the order."
t('confirmed', { gender: 'female' }) // → "She confirmed the order."
t('confirmed', { gender: 'other' })  // → "They confirmed the order."

Rules:

  • Branches with key: text format are named cases.
  • The last branch without : is the fallback (other).

React API

useTranslation(namespace?)

import '@apollo-deploy/aurebesh/react';

const { t, locale, changeLocale, isLoading, format } = useTranslation('settings');

| Return | Type | Description | |--------|------|-------------| | t | (key, opts?) => string | Translate a key, falls back to key on miss | | locale | SupportedLocale | Current active locale code | | changeLocale | (next: SupportedLocale) => Promise<void> | Switch locale + persist to cookie/localStorage | | isLoading | boolean | true while namespaces are loading over HTTP | | format | FormatService | Locale-bound formatting utilities |

FormatService methods

format.date(new Date(), 'long')                        // "January 15, 2024"
format.date(new Date(), 'short')                       // "1/15/24"
format.date(new Date(), { weekday: 'long', month: 'short' })
format.time(new Date())                                // "2:30 PM"
format.dateTime(new Date())                            // "Jan 15, 2024, 2:30 PM"
format.relative(new Date(Date.now() - 3600000))        // "1 hour ago"
format.number(1234567.89)                              // "1,234,567.89" (locale-aware)
format.currency(99.99, 'EUR')                          // "€99.99"
format.list(['apples', 'oranges', 'pears'])            // "apples, oranges, and pears"
format.list(['cats', 'dogs'], 'disjunction')           // "cats or dogs"

useMoney()

'use client';
import '@apollo-deploy/aurebesh/react';

function PricingCard({ price }: { price: Price }) {
  const money = useMoney();

  return (
    <div>
      {/* AUTO: converts USD → EUR via FX snapshot */}
      <span>{money(price, { target: 'EUR' })}</span>

      {/* FIXED: no conversion, renders base currency as-is */}
      <span>{money({ ...price, policy: 'FIXED' })}</span>

      {/* Regional override: uses price.regional['EU'] if present */}
      <span>{money(price, { region: 'EU' })}</span>
    </div>
  );
}

Requires rates to be passed to <I18nProvider> from the server. Throws a dev-mode error when rates are missing for AUTO policy.

I18nProvider

<I18nProvider
  config={i18nConfig}
  initialLocale="en"
  initialMessages={{ common: { ... }, auth: { ... } }}  // SSR hydration
  rates={rateSnapshot}                                   // FX snapshot for useMoney
  missingKeyReporter={reporter}                          // optional observability
>
  {children}
</I18nProvider>

Server API

getI18nServerState(locale, namespaces, opts)

Load translation files from disk for SSR hydration. Skips missing files gracefully.

import '@apollo-deploy/aurebesh/server';

const state = await getI18nServerState('en', ['common', 'auth', 'settings'], {
  publicDir: join(process.cwd(), 'public'),
  pathTemplate: 'locales/{{locale}}/{{namespace}}.json', // default
});
// → { initialLocale: 'en', initialMessages: { common: {...}, auth: {...}, settings: {...} } }

resolveRequestLocale(input, config)

Detect the correct locale from an incoming request (proxy/middleware use case).

import '@apollo-deploy/aurebesh/server';

// In proxy.ts (Next.js 16)
const locale = resolveRequestLocale(
  {
    cookieLocale: request.cookies.get('apollo.locale')?.value,
    acceptLanguage: request.headers.get('accept-language'),
  },
  i18nConfig, // { supportedLocales, defaultLocale }
);

Follows cookie-first strategy — cookie wins over Accept-Language header. Falls back to defaultLocale when no supported match is found.

detectLocaleFromAcceptLanguage(header, opts)

Parse an Accept-Language header with full quality-weight (q=) support.

detectLocaleFromAcceptLanguage('fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7', {
  supportedLocales: ['en', 'es', 'fr'],
  defaultLocale: 'en',
}); // → 'fr'

money(price, opts) — server-side

Resolve and format a price against cached FX rates in a single call.

import '@apollo-deploy/aurebesh/server';

const price: Price = {
  base: { amount: 99.99, currency: 'USD' },
  regional: { EU: { amount: 84.99, currency: 'EUR' } },
};

await money(price, { target: 'EUR', locale: 'de-DE' }); // "84,99 €"
await money(price, { region: 'EU', locale: 'de-DE' });  // "84,99 €"  (regional override)
await money(price, { locale: 'ja-JP', target: 'JPY' }); // "¥15,000"

Currency & FX Engine

Price types

interface Price {
  base: { amount: number; currency: string }; // ISO 4217, decimal units
  regional?: Record<string, PriceAmount>;      // keyed by region code, e.g. 'EU'
  policy?: 'AUTO' | 'FIXED' | 'DISPLAY_ONLY';
}

| Policy | Behaviour | |--------|-----------| | AUTO (default) | Convert base amount to target currency via live FX rates | | FIXED | Never convert — always render the base amount as-is (compliance use case) | | DISPLAY_ONLY | Show base currency without conversion (informational display) |

FX providers

import {
  ExchangeRateHostProvider,
  StaticFallbackProvider,
  MultiProvider,
  registerProvider,
  ensureDefaultFxProviders,
} from '@apollo-deploy/@apollo-deploy/aurebesh/currency';

// Option A: use the built-in defaults (recommended)
ensureDefaultFxProviders();
// Registers: 'exchange-rate-host', 'static-fallback', 'multi:host+fallback'

// Option B: bring your own provider chain
const liveProvider  = new ExchangeRateHostProvider({ apiKey: process.env.FX_KEY });
const fallback      = new StaticFallbackProvider({
  table: { USD: { USD: 1, EUR: 0.91, GBP: 0.78 } },
});
const chain         = new MultiProvider([liveProvider, fallback], 'my-chain');
registerProvider(liveProvider);
registerProvider(fallback);
registerProvider(chain);

Custom provider

Implement the CurrencyProvider interface to add any data source:

import '@apollo-deploy/aurebesh/currency';

class MyProvider implements CurrencyProvider {
  readonly name = 'my-provider';

  async fetchRates(base: string): Promise<RateSnapshot> {
    const data = await myApi.getRates(base);
    return {
      base,
      rates: data.rates,
      fetchedAt: Date.now(),
      source: this.name,
    };
  }
}

FX caching (Next.js "use cache")

getCachedRates uses the Next.js 16 Cache Components system:

import '@apollo-deploy/aurebesh/currency'; // server-only

// Read (cached 30 min, stale 15 min, expires 24h):
const snapshot = await getCachedRates('USD', 'multi:host+fallback');

// Invalidate when you know rates have changed:
invalidateFxRatesForBase('USD');
invalidateFxRates(); // invalidate ALL fx-rates:* entries

Requires cacheComponents: true in your app's next.config.ts.

FX refresh route handler

// app/api/admin/fx-refresh/route.ts
import '@apollo-deploy/aurebesh/server';
import { verifyAdminToken } from '@/lib/auth';

export const POST = createFxRefreshHandler({
  authorize: async (request) => {
    const token = request.headers.get('x-admin-token');
    return verifyAdminToken(token);
  },
  defaultBase: 'USD',
  defaultWarm: true, // pre-warm cache after invalidation
});

POST to /api/admin/fx-refresh with optional JSON body:

{ "scope": "base", "base": "USD", "warm": true }

Observability

Missing-key reporter

import '@apollo-deploy/aurebesh/observability';

const reporter = new InMemoryMissingTranslationReporter();

<I18nProvider config={i18nConfig} missingKeyReporter={reporter}>
  {children}
</I18nProvider>

// Later — e.g. in an admin panel or CI check:
const metrics = reporter.snapshot(); // sorted by hit count descending
// [
//   { key: 'billing.invoiceTitle', locale: 'es', namespace: 'billing', hits: 42, ... },
//   ...
// ]
reporter.clear();

Custom reporter (send to your monitoring backend)

import '@apollo-deploy/aurebesh/observability';

class DatadogMissingKeyReporter implements MissingTranslationReporter {
  report(event: MissingTranslationEvent): void {
    datadogRum.addError(new Error(`Missing i18n key: ${event.key}`), {
      locale: event.locale,
      namespace: event.namespace,
    });
  }
}

Translation coverage reports

Use getTranslationCoverageFromDisk in CI to gate deployments on translation completeness.

// scripts/check-translations.ts
import { join } from 'node:path';
import '@apollo-deploy/aurebesh/server';

const report = await getTranslationCoverageFromDisk({
  publicDir: join(process.cwd(), 'public'),
  locales: ['en', 'es', 'fr'],
  namespaces: ['common', 'auth', 'settings'],
  referenceLocale: 'en', // 'en' is the source of truth
});

console.log(`Overall coverage: ${(report.summary.coverage * 100).toFixed(1)}%`);

for (const locale of report.locales) {
  if (locale.coverage < 0.9) {
    console.error(`${locale.locale}: ${(locale.coverage * 100).toFixed(1)}% — below threshold`);
    process.exit(1);
  }
}

Report shape:

{
  generatedAt: 1713456789000,
  referenceLocale: 'en',
  namespaces: ['common', 'auth', 'settings'],
  summary: {
    localeCount: 3,
    referenceKeyCount: 280,
    translatedKeyCount: 252,
    missingKeyCount: 28,
    extraKeyCount: 4,
    coverage: 0.9,              // 0–1 ratio
  },
  locales: [
    {
      locale: 'es',
      coverage: 0.94,
      namespaces: [
        {
          locale: 'es',
          namespace: 'settings',
          coverage: 0.88,
          missingKeys: ['billing.planName', 'billing.nextRenewal'],
          extraKeys: ['legacy.oldKey'],
          missingFile: false,
          ...
        }
      ]
    }
  ]
}

Locale detection in middleware / proxy

// proxy.ts (Next.js 16 — replaces middleware.ts)
import { NextRequest, NextResponse } from 'next/server';
import '@apollo-deploy/aurebesh/server';
import { i18nConfig } from '@/lib/i18n.config';

export function proxy(request: NextRequest) {
  const locale = resolveRequestLocale(
    {
      cookieLocale: request.cookies.get('apollo.locale')?.value ?? null,
      acceptLanguage: request.headers.get('accept-language'),
    },
    i18nConfig,
  );

  const response = NextResponse.next();
  response.headers.set('x-locale', locale);
  return response;
}

Config reference

import '@apollo-deploy/aurebesh/config';

const config = createI18nConfig(['settings', 'billing', 'audit']);
// config.namespaces → ['common', 'auth', 'settings', 'billing', 'audit']
// config.defaultLocale → 'en'
// config.supportedLocales → ['en', 'es', 'fr']
// config.interpolation.prefix → '{{'
// config.interpolation.suffix → '}}'
// config.interpolation.escapeValue → true  (HTML-escape by default)
// config.pluralization.simplifyPluralSuffix → true  (_plural maps to 'other')
// config.loadPath → '/locales/{{locale}}/{{namespace}}.json'

Adding a new supported locale

Edit src/config/locales.ts:

export const SUPPORTED_LOCALES = [
  { code: 'en', name: 'English',  nativeName: 'English',    direction: 'ltr' },
  { code: 'es', name: 'Spanish',  nativeName: 'Español',     direction: 'ltr' },
  { code: 'fr', name: 'French',   nativeName: 'Français',    direction: 'ltr' },
  // add here:
  { code: 'de', name: 'German',   nativeName: 'Deutsch',     direction: 'ltr' },
  { code: 'ar', name: 'Arabic',   nativeName: 'العربية',     direction: 'rtl' },
] as const;

Module exports

| Import path | Environment | Contents | |-------------|------------|---------| | @apollo-deploy/aurebesh | Both | Root types: TranslateFn, FormatService, UseTranslationResult + all config re-exports | | @apollo-deploy/aurebesh/config | Both | I18nConfig, LocaleConfig, SupportedLocale, SUPPORTED_LOCALES, DEFAULT_LOCALE, BASE_NAMESPACES, baseI18nConfig, createI18nConfig | | @apollo-deploy/aurebesh/react | Client only | I18nProvider, useTranslation, useMoney, buildFormatService, getPersistedLocale, persistLocale | | @apollo-deploy/aurebesh/server | Server only | getI18nServerState, loadMessagesFromDisk, resolveRequestLocale, detectLocaleFromAcceptLanguage, money, resolveMoney, refreshFxRates, createFxRefreshHandler, getTranslationCoverageFromDisk | | @apollo-deploy/aurebesh/format | Both | formatDate, formatTime, formatDateTime, formatRelative, formatNumber, formatPercent, formatList, formatMoney | | @apollo-deploy/aurebesh/currency | Both (server for rates) | Price, PriceAmount, PricePolicy, RateSnapshot, RateInput, resolvePrice, pickRateSnapshot, isRateSnapshot, ExchangeRateHostProvider, StaticFallbackProvider, MultiProvider, registerProvider, ensureDefaultFxProviders, getCachedRates, invalidateFxRates | | @apollo-deploy/aurebesh/observability | Both | MissingTranslationReporter, MissingTranslationEvent, MissingTranslationMetric, InMemoryMissingTranslationReporter |


Requirements

| Dependency | Version | |------------|---------| | Node.js | 20+ | | TypeScript | 5.x | | Next.js | 16+ (peer dep) | | React | 18+ or 19+ (peer dep) |

cacheComponents: true must be set in next.config.ts for FX rate caching to function.


Contributing

# Install
pnpm install

# Typecheck
pnpm -F aurebesh build

# Tests
pnpm -F aurebesh test

# Watch mode
pnpm -F aurebesh test:watch

Translation JSON files live in each app's public/locales/ directory — the package itself ships no translations.


License

MIT