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

i18n-keyless-angular

v3.8.1

Published

Keyless i18n for Angular 17+. Standalone, signals, Angular SSR.

Readme

i18n-keyless-angular

npm install i18n-keyless-angular
// app.config.ts
import { provideI18nKeyless } from "i18n-keyless-angular";
export const appConfig = { providers: [provideI18nKeyless({ API_KEY: "YOUR_API_KEY", languages: { primary: "fr", supported: ["fr", "en"] } })] };
<!-- any template: import I18nKeylessTextComponent / I18nKeylessTranslatePipe in the component -->
<h1><i18n-t>Bonjour le monde</i18n-t></h1>   <input [placeholder]="'Votre email' | t" />

That is the whole setup: write your strings in your primary language, the SDK translates them at runtime (once per string, then cached on the device). No keys, no JSON files. Get an API key at i18n-keyless.com.

The Angular package wraps i18n-keyless-core with the same feature set and the same semantics as i18n-keyless-react: same storage keys (an app migrating from React keeps its cache and its device id), same namespaces, same context / replace / forceTemporary / originLanguage options, same SSR model. Angular >= 17.1, standalone APIs, signals.

Table of contents

Setup

provideI18nKeyless(config) returns EnvironmentProviders. Put it in bootstrapApplication (or in app.config.ts). The config is the init config of i18n-keyless-react:

import { bootstrapApplication } from "@angular/platform-browser";
import { provideI18nKeyless } from "i18n-keyless-angular";

bootstrapApplication(AppComponent, {
  providers: [
    provideI18nKeyless({
      API_KEY: "YOUR_API_KEY",
      languages: {
        primary: "fr",           // the language your templates are written in
        supported: ["fr", "en"], // what the user can switch to
        fallback: "en",          // optional
      },
      // storage: window.localStorage  <- the browser default; see Storage below
      // onInit: (lang) => {}, onSetLanguage: (lang) => {}, defaultNamespace, debug, ssr,
      // API_URL, handleTranslate, getAllTranslations, sendTranslationsUsage
    }),
  ],
});

init runs when the injector is created, before the first component renders, and does not block bootstrap: the app renders in the primary language and re-renders into the current language as the cache (then the network) answers. Read I18nKeylessService.hydrated() or await whenHydrated() if you need that moment (a splash screen, for instance).

Three ways to translate

import { Component, inject } from "@angular/core";
import { I18nKeylessTextComponent, I18nKeylessTranslatePipe, I18nKeylessService } from "i18n-keyless-angular";

@Component({
  standalone: true,
  imports: [I18nKeylessTextComponent, I18nKeylessTranslatePipe],
  template: `
    <!-- 1. Component: the default. Re-renders on its own when the translation lands. -->
    <h1><i18n-t>Bonjour le monde</i18n-t></h1>

    <!-- 2. Pipe: where an element cannot go (attributes, <option>, <title>, a string). -->
    <input [placeholder]="'Votre email' | t" />
    <p>{{ "8 heures" | t: { context: "durée" } }}</p>

    <!-- 3. Service signal: a string computed in the class. -->
    <button>{{ label() }}</button>
  `,
})
export class HelloComponent {
  private readonly i18n = inject(I18nKeylessService);
  readonly label = this.i18n.translation("Envoyer");
}

<i18n-t> reads its projected text as the source (trimmed, so wrapped lines in a template are fine). It keeps the source in a hidden <span> and renders the translation next to it: that is what lets SSR hydration find the key again in the server HTML. A projected interpolation (<i18n-t>{{ user.firstName }}</i18n-t>) is followed too.

For a one-shot string outside change detection (a route title resolver, a toast, a file name), use I18nKeylessService.getTranslation(text, options) or the bare getTranslation export. Both are plain, non-reactive reads: the value does not update when the language changes.

Switch language

readonly i18n = inject(I18nKeylessService);

this.i18n.currentLanguage();        // Signal<Lang>
this.i18n.setCurrentLanguage("en"); // persists it, fetches its translations, signals update
this.i18n.getSupportedLanguages();  // ["fr", "en"]

AVAILABLE_LANGS and the Lang type are re-exported; resolveLang(navigator.language, { supported, fallback }) maps a BCP-47 tag onto a supported language.

The bundle (optional)

By default a dictionary is downloaded at boot and the API stays in the loop. To make the app independent of the API for every string it already knows, export the dictionaries at build time — the MCP export_bundle tool, or GET /translate/bundle — commit i18n-keyless/manifest.json plus one i18n-keyless/<namespace>/<lang>.json per dictionary, and pass them to provideI18nKeyless (or init) as bundle: { manifest, load }:

import manifest from "./i18n-keyless/manifest.json";

provideI18nKeyless({
  API_KEY,
  languages: { primary: "fr", supported: ["fr", "en"] },
  bundle: { manifest, load: (namespace, lang) => import(`./i18n-keyless/${namespace}/${lang}.json`) },
});

load is called once per (namespace, lang) the manifest covers, so a dynamic import() ships only the language actually rendered. A namespace the manifest covers in the current language is seeded from the file instead of fetched, at boot and on a language switch, with the manifest's cursor — so the API is only called for a key the bundle does not have (UGC, a new screen) and for the delta that follows. Storage always wins over the bundle when it holds a newer slice in the same language (e.g. after a human review from the dashboard). Re-run the export before each release.

Per-translation options

Inputs of <i18n-t> and the second argument of the pipe, translate(), translation() and getTranslation():

  • context: disambiguates meaning. <i18n-t context="heure">8 heures</i18n-t> and <i18n-t context="durée">8 heures</i18n-t> are two translations, stored as key__context.
  • replace: interpolation. The keys include the delimiters: <i18n-t [replace]="{ '{name}': user.name }">{{ 'Bonjour {name}' }}</i18n-t>. (Wrap a literal { in an interpolation: Angular reads a bare { as an ICU block.)
  • count: plurals. One form with {count}; every language gets the forms it needs, the primary one too: <i18n-t [count]="n">{{ '{count} articles' }}</i18n-t>. [ordinal]="true" for a rank.
  • select: a closed set of values that changes the wording: <i18n-t [select]="{ gender: user.gender }">Il est connecté</i18n-t>.
  • namespace: a fetch/storage partition for large projects. unpersistedNamespace: memory only.
  • forceTemporary: override the AI translation from code, per language.
  • originLanguage: user generated content written in another language than the primary one.
  • debug: logs the resolution of that one string.

Translation status

A missing translation renders the source text and swaps in once it lands — fine for most UI, but for user generated content (a comment written in another language) you may want to show a spinner, a blur, or the source text instead, while it is on its way. TranslationStatus is "ready" | "pending" | "unavailable": ready when the text rendered is final (the key's own language, or a cell that satisfies count / select), pending while a translate-on-miss for it is queued and not yet settled, unavailable otherwise (not initialized, a server runtime, or a settle that did not bring the cell). The SDK only exposes the status — it never decides what to render.

@Component({
  standalone: true,
  imports: [I18nKeylessTextComponent, I18nKeylessTranslationStatusPipe],
  template: `
    <!-- <i18n-t> mirrors it onto data-i18n-status: style it in CSS. -->
    <i18n-t>{{ comment.text }}</i18n-t>

    <!-- or read it directly, e.g. to render your own placeholder -->
    @if (('Bonjour' | tStatus) === 'pending') {
      <span class="skeleton"></span>
    }
  `,
})

i18n-t[data-i18n-status="pending"] { opacity: 0.5 } styles every instance without touching a template. On the component, status() (Signal<TranslationStatus>) is available too. The t pipe has a tStatus counterpart (I18nKeylessTranslationStatusPipe, impure like t, same request-memo). I18nKeylessService has translationStatus(text, options) (a signal, sharing translate()'s resolution and request-on-miss) and getTranslationStatus(text, options) (one-shot, non-reactive, no side effect). The bare getTranslationStatus export is the one-shot form outside a component (store, then the runWithI18nKeyless scope).

The service

I18nKeylessService is providedIn: "root":

| Member | Type | Notes | | --- | --- | --- | | currentLanguage | Signal<Lang> | the request scope's language under SSR, else the store's | | translations | Signal<Translations> | flat map for currentLanguage | | hydrated | Signal<boolean> | true once storage has been read | | config | Signal<I18nConfig> | | | currentLanguage$, translations$ | Observable | rxjs bridges (toObservable) | | translate(text, options) | string | reactive: call it in a template, computed or effect | | translation(text, options) | Signal<string> | computed(() => translate(...)) | | translationStatus(text, options) | Signal<TranslationStatus> | reactive, shares translate()'s resolution | | getTranslation(text, options) | string | one-shot, non-reactive | | getTranslationStatus(text, options) | TranslationStatus | one-shot, non-reactive, no side effect | | setCurrentLanguage(lang) | Promise<void> | | | whenHydrated() | Promise<void> | | | clearStorageAndStore() | Promise<void> | wipes the cache, keeps the device id |

The store behind it is module-scoped (one per process) and exported as i18nKeylessStore (currentLanguage, translations, config, hydrated signals plus getState() / setState()), for tests and advanced use.

SSR

Two modes, as in the React package (see docs/SSR.md):

  1. Primary-language SSR: nothing to do. provideI18nKeyless works on the server (storage defaults to an in-memory adapter, init runs once per process, usage analytics are never sent from a server). The server renders the primary language and the client re-translates after hydration.
  2. Localized SSR: render any language on the server, for indexable ?lang=xx or /{lang}/… URLs. provideI18nKeylessServer is the counterpart of <I18nKeylessProvider>: <i18n-t>, the pipe and the service read its { lang, translations } first and fall back to the store when it is absent.
// app.config.server.ts
import { inject, REQUEST } from "@angular/core";
import { provideI18nKeylessServer, getServerTranslations, resolveLang } from "i18n-keyless-angular";

const serverConfig: ApplicationConfig = {
  providers: [
    provideServerRendering(),
    // A factory runs in injection context as an APP_INITIALIZER: Angular waits for it.
    provideI18nKeylessServer(async () => {
      const url = new URL(inject(REQUEST)?.url ?? "http://localhost/");
      const lang = resolveLang(url.searchParams.get("lang"), { supported: ["fr", "en"], fallback: "fr" })!;
      return { lang, translations: await getServerTranslations(lang), primary: "fr" }; // cached per process
    }),
  ],
};

Hand the same { lang, translations } to the client (TransferState, or a JSON script tag you serialize) and provide it there too: in the browser provideI18nKeylessServer seeds the store synchronously (hydrateFromServer), so the first client render matches the server HTML and later reads agree with it. In provider mode the language is the scope's lang: drive it from the URL, setCurrentLanguage is for SPA mode. primary (≥ 3.6.1) is the language the source strings are written in: when the scope carries it, the resolution never reads the store's primary, so a store that has not run provideI18nKeyless yet cannot make a request in the real primary language look like a request for the source strings. Optional: it defaults to the store's primary.

runWithI18nKeyless({ lang, translations }, () => renderApplication(...)), getRequestScope() and getUsedTranslationsSnapshot() are exported as well, for the imperative getTranslation path inside a server render (AsyncLocalStorage, Node >= 20.10).

Translate-on-miss never runs from <i18n-t> or the pipe on the server (a server render is read-only, like <I18nKeylessText>); the imperative getTranslation still queues a miss, outside Angular's zone so the response is never delayed by it.

Storage

Browser default: window.localStorage. Server default: an in-memory adapter. Any object with getItem / setItem / removeItem (sync or async) is a valid storage: sessionStorage, an idb-keyval wrapper, Capacitor Preferences, Ionic Storage. The keys are the ones of the React package (i18n-keyless-translations, i18n-keyless-current-language, i18n-keyless-user-id, ...).

Change detection: why the pipe is impure

A pure pipe is memoized by Angular on its input values, and 'Bonjour' never changes; the translation does (it lands asynchronously, the language switches). So t is pure: false: its transform runs on every check of its template, where it reads the store signals. That read is tracked by the template's reactive consumer, so a signal change marks the view for refresh even under OnPush and under zoneless change detection, with no ChangeDetectorRef.markForCheck() and no subscription. The cost per check is one map read and a string compare; the side effects (translate-on-miss, usage) are memoized per (language, key) in the pipe instance.

<i18n-t> is OnPush with a computed text; it only re-renders when its own string changes. Signals also make both work without zone.js (provideZonelessChangeDetection).

Differences from the React package

  • storage is optional in the browser (defaults to localStorage); React throws.
  • I18nKeylessService.getTranslation reads the DI request scope, which a plain function in React cannot; the bare getTranslation export behaves like React's (store, then ALS scope).
  • Translate-on-miss and usage recording from translate() / the pipe are memoized per (language, key) for the life of the instance, where React's <T> fires once per mount.
  • The sdk header says angular-client in a browser and angular-server on the server: the API bills a *-server label by its connection and every other label by its device id.

Building and publishing

The package compiles with ngc from @angular/compiler-cli (a devDependency) in prepublishOnly, with angularCompilerOptions.compilationMode: "partial" in tsconfig.json. The published dist is therefore in Ivy partial format (ɵɵngDeclareComponent), which is what an Angular CLI application built with AOT (the default) links at build time: install from npm and ng build works as is.

Building from source: rm -rf dist && npx ngc -p tsconfig.json (after i18n-keyless-core). A consumer that installs from a source checkout without that build has no dist in partial format and needs JIT ("aot": false in angular.json).

Tests: npx vitest run (vitest + jsdom + Angular JIT; see __tests__/setup.ts).