npm.io
2.2.0 • Published 3d agoCLI

@reforgium/presentia

Licence
MIT
Version
2.2.0
Deps
1
Size
192 kB
Vulns
0
Weekly
0

@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

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 { provideHttpClient } from '@angular/common/http';
import { bootstrapApplication } from '@angular/platform-browser';
import { providePresentia } from '@reforgium/presentia';

bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(),
    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: 'persist',
    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?: 'persist' | '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: 'persist',
    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

Keywords