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

@reforgium/presentia

v2.0.0

Published

Angular infrastructure library for i18n, route-aware namespace preload, theming, adaptive breakpoints, route state, and SEO

Readme

@reforgium/presentia

npm version License: MIT

Infrastructure package for Angular applications:

  • localization (LangService, lang pipe, reLang directive),
  • theming (ThemeService),
  • adaptive behavior (AdaptiveService, *reIfDevice),
  • route state helper (RouteWatcher),
  • SEO automation (SeoService, SeoRouteListener).

Release Highlights (2.0.0)

  • Grouped v2 provider via providePresentia(...).
  • Route-aware namespace preload with generated manifests, diagnostics, and strict failure handling.
  • Extended ThemeService with theme registry, configurable DOM strategy, and persistence adapter.
  • Breakpoint-first adaptive helpers with reIfDeviceAtLeast and reIfDeviceBetween.
  • Richer RouteWatcher state with merged/deepest selectors and route matching helpers.

Install

npm i @reforgium/presentia

Recommended Setup

For new integrations prefer:

  • providePresentia(...)
  • route namespace preload in blocking mode
  • theme.registry even if you currently use only light / dark
  • explicit adaptive.breakpoints when app breakpoints differ from defaults

Legacy integrations can keep provideReInit(...) temporarily, but it is now the compatibility path, not the recommended one.

Quick Start

import { bootstrapApplication } from '@angular/platform-browser';
import { providePresentia } from '@reforgium/presentia';

bootstrapApplication(AppComponent, {
  providers: [
    providePresentia({
      lang: {
        source: {
          url: '/assets/i18n',
          fromAssets: true,
          defaultLang: 'ru',
          fallbackLang: 'en',
          supportedLangs: ['de'],
        },
        preload: {
          global: ['layout', 'common'],
        },
        rendering: {
          placeholder: '...',
        },
        missingKeyHandler: (key, ctx) => `[${ctx.lang}] ${key}`,
      },
      theme: {
        defaultTheme: 'light',
        dom: { darkClassName: 'dark' },
      },
    }),
  ],
});

What You Get

Main provider:

  • providePresentia(config: PresentiaConfig)

providePresentia(...) is the new grouped config entry point. provideReInit(...) remains available as the legacy 1.x style provider.

Migration Snapshot

Typical 1.x -> v2 provider migration:

// 1.x
provideReInit({
  locale: {
    url: '/assets/i18n',
    isFromAssets: true,
    preloadNamespaces: ['layout', 'common'],
  },
  theme: {
    defaultTheme: 'light',
    darkThemePrefix: 're-dark',
  },
  breakpoints: {
    mobile: '(max-width: 719px)',
  },
});

// v2
providePresentia({
  lang: {
    source: {
      url: '/assets/i18n',
      fromAssets: true,
    },
    preload: {
      global: ['layout', 'common'],
    },
  },
  theme: {
    defaultTheme: 'light',
    dom: {
      darkClassName: 're-dark',
    },
  },
  adaptive: {
    breakpoints: {
      mobile: '(max-width: 719px)',
    },
  },
});

For a fuller mapping see V2-MIGRATION.md.

Localization exports:

  • LangService
  • LangPipe
  • LangDirective
  • LANG_CONFIG
  • LANG_PIPE_CONFIG
  • LANG_MISSING_KEY_HANDLER
  • PRESENTIA_ROUTE_NAMESPACES_DATA_KEY
  • Types: LocaleConfig, LangModel, LangDto, LangParams, LangKey, LangKeyRegistry, etc.

Theme exports:

  • ThemeService
  • Theme
  • THEME_CONFIG
  • themes

Adaptive exports:

  • AdaptiveService
  • IfDeviceDirective
  • DEVICE_BREAKPOINTS, defaultBreakpoints

Routes and SEO exports:

  • RouteWatcher
  • SeoService
  • SeoRouteListener

Legacy compatibility surface:

  • provideReInit(config: AppConfig)
  • darkThemePrefix
  • defaultThemeConfig
  • defaultThemePersistenceAdapter
  • RouteNamespaceDiagnosticsService

Provider Configuration

providePresentia

providePresentia({
  lang: {
    source: {
      url: '/assets/i18n',
      fromAssets: true,
      defaultLang: 'ru',
      fallbackLang: 'en',
    },
    rendering: {
      placeholder: '...',
      missingValue: '--',
    },
    preload: {
      global: ['layout', 'common'],
      routes: {
        mode: 'blocking',
      },
    },
  },
  theme: {
    registry: ['light', 'dark'],
    defaultTheme: 'dark',
    persistence: 'localStorage',
    dom: {
      strategy: 'root-class',
      darkClassName: 're-dark',
    },
  },
  adaptive: {
    breakpoints: {
      mobile: '(max-width: 719px)',
      tablet: '(min-width: 720px) and (max-width: 1399px)',
      'desktop-s': '(min-width: 1400px) and (max-width: 1919px)',
      desktop: '(min-width: 1920px)',
    },
  },
});

Use providePresentia(...) for new integrations. Use provideReInit(...) only when you need the legacy flat config shape.

provideReInit

Legacy compatibility provider. Prefer providePresentia(...) for new integrations.

AppConfig includes:

  • locale: LocaleConfig (required)
  • theme?: ThemeConfig
  • breakpoints?: DeviceBreakpoints
  • langPipe?: LangPipeConfig
  • langMissingKeyHandler?: LangMissingKeyHandler

It also wires integration tokens from @reforgium/internal:

  • TRANSLATION
  • SELECTED_LANG
  • CHANGE_LANG
  • SELECTED_THEME
  • CHANGE_THEME
  • CURRENT_DEVICE

Localization

LocaleConfig

type LocaleConfig = {
  url: string;
  isFromAssets: boolean;
  defaultLang?: Langs;
  fallbackLang?: Langs;
  defaultValue?: string;
  kgValue?: 'kg' | 'ky';
  supportedLangs?: readonly string[];
  preloadNamespaces?: readonly string[];
  requestBuilder?: (ctx: {
    ns: string;
    lang: Langs;
    isFromAssets: boolean;
    baseUrl: string;
  }) => string;
  requestOptionsFactory?: (ctx: {
    ns: string;
    lang: Langs;
    isFromAssets: boolean;
    baseUrl: string;
  }) => {
    headers?: HttpHeaders | Record<string, string | string[]>;
    params?: HttpParams | Record<string, string | number | boolean | readonly (string | number | boolean)[]>;
    withCredentials?: boolean;
    responseType?: 'json' | 'text' | 'blob' | 'arraybuffer';
    context?: HttpContext;
    transferCache?: boolean | { includeHeaders?: string[] };
  };
  responseAdapter?: (response: unknown, ctx: {
    ns: string;
    lang: Langs;
    isFromAssets: boolean;
  }) => LangModel | LangDto[];
  batchRequestBuilder?: (ctx: {
    namespaces: readonly string[];
    lang: Langs;
    isFromAssets: boolean;
    baseUrl: string;
  }) => string;
  batchResponseAdapter?: (response: unknown, ctx: {
    namespaces: readonly string[];
    lang: Langs;
    isFromAssets: boolean;
  }) => Record<string, LangModel | LangDto[]>;
  namespaceCache?: {
    maxNamespaces?: number;
    ttlMs?: number;
  };
  routeNamespacePreload?: {
    mode?: 'blocking' | 'lazy';
    dataKey?: string;
    manifest?: Record<string, readonly string[]>;
    mergeStrategy?: 'append' | 'replace';
    onError?: 'continue' | 'throw';
    diagnostics?: boolean;
  };
};

Behavior Notes

  • Built-in languages: ru, kg, en.
  • Alias ky is accepted and normalized to internal kg.
  • supportedLangs lets you add custom language codes.
  • If key is missing, defaultValue is used (or missing key handler result).
  • defaultValue is a missing-key fallback, not a loading placeholder.
  • LangPipe.placeholder is used while a namespace is still loading.
  • Stale HTTP responses are ignored when language changes mid-flight.

Basic Usage

const lang = inject(LangService);

lang.setLang('en');
await lang.loadNamespace('layout');

const title = lang.get('layout.title');
const welcome = lang.get('layout.welcome', { name: 'John' });

API Mode With Custom Payload

provideReInit({
  locale: {
    url: '/api/i18n',
    isFromAssets: false,
    requestBuilder: ({ ns, lang, baseUrl }) => `${baseUrl}/${ns}?language=${lang}`,
    requestOptionsFactory: () => ({
      headers: { 'x-tenant-id': 'tenant-1' },
      withCredentials: true,
      responseType: 'json',
    }),
    responseAdapter: (response) => (response as { data: LangDto[] }).data,
  },
});

Batch Namespace Loading

provideReInit({
  locale: {
    url: '/api/i18n',
    isFromAssets: false,
    batchRequestBuilder: ({ lang, namespaces }) =>
      `/api/i18n/batch?language=${lang}&ns=${namespaces.join(',')}`,
    batchResponseAdapter: (response) => response as Record<string, LangDto[]>,
  },
});

const lang = inject(LangService);
await lang.loadNamespaces(['layout', 'common', 'breadcrumbs']);

Route Namespace Preload

Use this when first-paint localization must be ready before page activation. presentia patches router config automatically via providePresentia(...) or provideReInit(...), so no extra resolver wiring is required in the consumer app.

Behavior summary:

  • mode: 'blocking' waits for route namespaces before route activation.
  • mode: 'lazy' starts namespace loading without delaying navigation.
  • onError: 'continue' keeps navigation alive if preload fails.
  • onError: 'throw' cancels navigation by rethrowing the preload error.

Route data mode:

providePresentia({
  lang: {
    source: {
      url: '/assets/i18n',
      fromAssets: true,
    },
    preload: {
      routes: {},
    },
  },
});

export const routes: Routes = [
  {
    path: 'profile',
    component: ProfilePage,
    data: {
      presentiaNamespaces: ['profile', 'layout', 'common'],
    },
  },
];

Manifest mode:

const PRESENTIA_ROUTE_NAMESPACES = {
  '/': ['home', 'layout', 'common'],
  '/profile': ['profile', 'layout', 'common'],
  '/users/:id': ['users', 'layout', 'common'],
} as const;

providePresentia({
  lang: {
    source: {
      url: '/assets/i18n',
      fromAssets: true,
    },
    preload: {
      routes: {
        manifest: PRESENTIA_ROUTE_NAMESPACES,
        mode: 'blocking',
      },
    },
    diagnostics: {
      lateNamespaceLoads: true,
    },
  },
});

Legacy 1.x projects can keep using provideReInit(...), but new integrations should prefer providePresentia(...).

Generate the manifest in a consumer app:

npx presentia-gen-namespaces

Default paths:

  • --routes src/app/app.routes.ts
  • --project src
  • --tsconfig tsconfig.json
  • --out src/app/presentia-route-namespaces.ts

Explicit paths:

npx presentia-gen-namespaces \
  --routes src/app/app.routes.ts \
  --project src \
  --tsconfig tsconfig.json \
  --locales src/assets/locales \
  --out src/app/presentia-route-namespaces.ts

The public CLI is presentia-gen-namespaces. Repository scripts such as libs/presentia/scripts/generate-sandbox-namespaces.mjs are internal helpers for local demo verification only.

Best-effort extraction sources:

  • route data lang keys such as title: 'profile.title'
  • route data.presentiaNamespaces
  • {{ 'namespace.key' | lang }}
  • reLang / reLangKey
  • reLangAttrs
  • lang.get('namespace.key')
  • lang.observe('namespace.key')
  • imported local standalone components from component imports: [...]

When --locales is provided, generator validates discovered keys and namespaces against real locale JSON files to reduce false positives.

Optional report mode:

npx presentia-gen-namespaces \
  --locales src/assets/locales \
  --report src/app/presentia-route-namespaces.report.json \
  --print-report

This emits a JSON report with static-analysis gaps such as unresolved lazy imports or unsupported presentiaNamespaces expressions.

Options:

  • mode: 'blocking' | 'lazy' - waits for namespaces before route activation, or starts loading without blocking.
  • dataKey - custom route-data field name instead of default presentiaNamespaces.
  • manifest - generated or handwritten route -> namespaces[] map.
  • mergeStrategy: 'append' | 'replace' - merge manifest and route-data namespaces, or let route data override manifest entries.
  • onError: 'continue' | 'throw' - in blocking mode either keep navigation alive on preload failure, or cancel it by rethrowing the load error.
  • diagnostics - in dev mode warns when a namespace starts loading only after route activation.

Namespace Cache Policy

provideReInit({
  locale: {
    url: '/assets/i18n',
    isFromAssets: true,
    namespaceCache: {
      maxNamespaces: 20,
      ttlMs: 10 * 60 * 1000,
    },
  },
});

Cache control methods:

  • lang.evictNamespace('layout')
  • lang.clearNamespaceCache()

Copy-Paste Recipes

1. Assets JSON (simple frontend-only i18n)

provideReInit({
  locale: {
    url: '/assets/locales',
    isFromAssets: true,
    defaultLang: 'en',
    fallbackLang: 'en',
    preloadNamespaces: ['layout', 'common'],
  },
});

Expected files:

  • /assets/locales/layout.en.json
  • /assets/locales/common.en.json

2. Flat API DTO (LangDto[])

provideReInit({
  locale: {
    url: '/api/i18n',
    isFromAssets: false,
    requestBuilder: ({ ns, lang, baseUrl }) => `${baseUrl}/${ns}?language=${lang}`,
    // default parser already supports LangDto[]
  },
});

Expected payload:

[
  { "namespace": "layout", "code": "layout.title", "localization": "Dashboard" }
]

3. Envelope API DTO ({ data: [...] })

provideReInit({
  locale: {
    url: '/api/i18n',
    isFromAssets: false,
    responseAdapter: (response) => (response as { data: LangDto[] }).data,
  },
});

Expected payload:

{
  "data": [{ "namespace": "layout", "code": "layout.title", "localization": "Dashboard" }]
}

4. Multi-tenant API with headers and params

provideReInit({
  locale: {
    url: '/api/i18n',
    isFromAssets: false,
    requestOptionsFactory: ({ lang }) => ({
      headers: { 'x-tenant-id': 'tenant-1' },
      params: { region: 'eu', language: lang },
      withCredentials: true,
    }),
    requestBuilder: ({ ns, baseUrl }) => `${baseUrl}/${ns}`,
  },
});

5. Batch endpoint (/batch) for many namespaces

provideReInit({
  locale: {
    url: '/api/i18n',
    isFromAssets: false,
    batchRequestBuilder: ({ namespaces, lang, baseUrl }) =>
      `${baseUrl}/batch?language=${lang}&ns=${namespaces.join(',')}`,
    batchResponseAdapter: (response) =>
      response as Record<string, LangDto[]>,
  },
});

const lang = inject(LangService);
await lang.loadNamespaces(['layout', 'common', 'breadcrumbs']);

LangPipe

  • Name: lang
  • Standalone, impure pipe (pure: false)
  • Internal cache with TTL and max size
  • Configurable placeholder while namespace is loading
<h1>{{ 'layout.title' | lang }}</h1>
<p>{{ 'users.welcome' | lang: { name: userName, count: 12 } }}</p>

LangPipeConfig (via LANG_PIPE_CONFIG or provideReInit.langPipe):

  • ttlMs?: number
  • maxCacheSize?: number
  • placeholder?: string | ((query: string) => string)

reLang Directive

Auto-localizes text and selected attributes.

Supported modes:

  • 'all'
  • 'only-content'
  • 'only-placeholder'
  • 'only-label'
  • 'only-title'

Examples:

<button reLang title="common.save">common.save</button>
<button [reLang]="'only-title'" title="common.cancel">Cancel</button>
<input reLang [langForAttr]="'aria-label'" aria-label="forms.username" />
<div [reLang]="{ mode: 'only-content', textKey: 'layout.title' }"></div>

Typed Lang Keys (Opt-in)

LangService.get() and LangService.observe() support typed keys via module augmentation.

Generate keys from locale JSON in consumer app:

npx presentia-gen-lang-keys --locales src/assets/locales --out src/types/presentia-lang-keys.d.ts

This generates:

  • declare module '@reforgium/presentia'
  • interface LangKeyRegistry { keys: 'layout.title' | 'common.save' | ... }

After generation, invalid keys in get()/observe() are compile-time errors.

Theme

ThemeService:

  • theme() current theme (Theme = BaseTheme | string)
  • isLight()
  • isDark()
  • is(theme | theme[])
  • switch(theme?) explicit switch or toggle
const theme = inject(ThemeService);
theme.switch('dark');

ThemeConfig:

  • registry?: readonly Theme[]
  • defaultTheme?: Theme
  • darkThemePrefix?: string
  • dom?: { strategy?: 'root-class' | 'data-attribute'; rootSelector?: string; darkThemePrefix?: string; attributeName?: string; themeClassPrefix?: string; classNameBuilder?: (theme) => string }

providePresentia(...).theme additionally supports:

  • persistence?: 'localStorage' | 'none'
  • persistenceAdapter?: PersistenceAdapter
  • dom.darkClassName as a consumer-friendly alias for darkThemePrefix
  • dom.classPrefix as a consumer-friendly alias for themeClassPrefix

Example:

providePresentia({
  lang: {
    source: {
      url: '/assets/i18n',
      fromAssets: true,
    },
  },
  theme: {
    registry: ['light', 'dark', 'sepia'],
    defaultTheme: 'light',
    persistence: 'localStorage',
    dom: {
      strategy: 'root-class',
      darkClassName: 're-dark',
      classPrefix: 're-theme-',
    },
  },
});

With that config:

  • dark still applies the legacy dark class (re-dark)
  • every registered theme also gets a prefixed class such as re-theme-light, re-theme-dark, re-theme-sepia
  • for fully custom naming, use dom.classNameBuilder

Adaptive

AdaptiveService provides reactive signals:

  • device() -> 'desktop' | 'desktop-s' | 'tablet' | 'mobile'
  • breakpoint() alias for the current device bucket
  • width()
  • height()
  • isMobile()
  • isTablet()
  • isDesktopSmall()
  • isDesktop()
  • isPortrait()
  • is(device | device[])
  • isAtLeast(device)
  • isBetween(min, max)

*reIfDevice structural directive:

<div *reIfDevice="'desktop'">Desktop only</div>
<div *reIfDevice="['mobile', 'tablet']">Mobile and tablet</div>
<div *reIfDevice="'mobile'; inverse: true">Hidden on mobile</div>
<div *reIfDeviceAtLeast="'tablet'">Tablet and larger</div>
<div *reIfDeviceBetween="['tablet', 'desktop']">Tablet to desktop</div>

Breakpoints are configurable via DeviceBreakpoints.

Route State Helper

RouteWatcher gives reactive route state:

  • params()
  • deepestParams()
  • query()
  • data()
  • mergedData()
  • url()
  • routePattern()
  • fragment()
  • state()
  • selectData<T>(key)
  • selectData<T>(key, 'merged')
  • selectParam(key)
  • selectParam(key, 'deepest')
  • matchesPath(path | regexp)

Use when you want route-derived UI state without manual router subscriptions.

SEO

SeoService

Methods:

  • setTitle
  • setDescription
  • setKeywords
  • setRobots
  • setCanonical
  • setOg
  • setTwitter
  • setJsonLd

SeoRouteListener

Auto-applies SEO from route data on navigation:

const seoRoute = inject(SeoRouteListener);
seoRoute.init('https://example.com');

Expected route data keys:

  • title
  • description
  • robots
  • canonical
  • og
  • twitter
  • jsonld

Performance Notes

  • Use preloadNamespaces for first-screen keys.
  • Use loadNamespaces + batch hooks for chatty APIs.
  • Use namespaceCache limits to control memory in long-lived sessions.
  • Keep LangPipe.maxCacheSize realistic for your screen complexity.

Troubleshooting

If some texts appear untranslated until reload:

  • Ensure keys are valid (namespace.key format).
  • Confirm namespace is loaded for current language.
  • Check API adapter (responseAdapter) returns correct shape.
  • Verify route/lazy components do not call get() before namespace load if you require strict first render.
  • Prefer observe() / lang pipe for reactive updates.

If custom language is ignored:

  • Add it to supportedLangs.
  • Pass lowercase code or rely on normalization.

If ky/kg behavior is unexpected:

  • ky input is normalized to internal kg.
  • currentLang() returns kgValue when language is kg.

License

MIT