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

@ng-linguo/linguo

v0.9.5

Published

A modern, signal-first i18n runtime for Angular 18+ — built on SignalStore, with a translator-safe slot syntax, ICU, and tree-shakeable HTTP loading.

Readme

ng-linguo

Signal-native internationalization for Angular. A modern, complete i18n toolkit for Angular 18+, built on SignalStore — an independent, from-scratch alternative to @ngx-translate/core and Transloco, reactive by default with zero RxJS plumbing in your components.

<!-- translators edit plain text; this renders a real Angular link -->
<p t="Read the [docs]documentation[/docs] to get started">
  <ng-template tFor="docs" let-text><a routerLink="/docs">{{ text }}</a></ng-template>
</p>

Status: pre-release (0.9.x) — published to npm and usable today. The runtime, the extraction CLI, and the full test suite are in place and green. APIs may still shift before 1.0.

Why ng-linguo

Writing code

  • Signals, not subscriptions. Translations are reactive through @ngrx/signals — switch language and the UI updates on its own, with no async pipe and no manual subscribe/unsubscribe.
  • Three ways to translate, one for each job. A t pipe for templates, a [t] directive for elements and rich text, and injectTranslate() for TypeScript — see which to use.
  • Zoneless-ready, SSR-friendly, tree-shakeable. No zone.js dependency, safe to render on the server, and the optional ICU and HTTP pieces live in separate entry points so you only ship what you import.

Writing translations

  • English is the key — no key files to maintain. You write real English in your components, and that text is the translation key. There are no home.header.title paths to invent, keep unique, and keep in sync, and nothing opaque for a translator (or an LLM) to guess at — they always see a full, meaningful sentence. For the rare clash, a context disambiguates (Play in a game vs. a music player).
  • Translators never see HTML. Named slots [name]…[/name] (a BBCode-like syntax) bind to your <ng-template>, so links, buttons, and bindings render as real Angular while the translation file stays plain text. Translated text is never inserted as HTML, so cross-site scripting (XSS) is impossible by design.
  • Correct grammar in every language (ICU MessageFormat 2, and MF1). Real plurals, select, and gendered text per locale — Polish gets four plural forms, English gets two, all from one message.

Shipping translations

  • A real, additive extraction pipeline. Extract your source strings to standard gettext .po files (works with Crowdin / Lokalise / Phrase) and compile them to runtime JSON. Re-running extraction is additive: new and changed strings merge in while every existing translation is kept.
  • Add a language in seconds. Add its code to the config and extract — with an AI translator wired up, filling it in is a single command (or a couple of clicks in the interactive CLI).
  • Translate with AI — your model, your key. Copy a ready-made prompt into any chat model, or point ng-linguo at a translator module that calls your own provider. ng-linguo writes the prompt (it teaches the model your context, slot tags, and plural rules) and merges the reply; your SDK and API key never leave your machine.
  • Automatable in CI. Every command runs non-interactively and is deterministic, so extraction and compilation drop straight into a pipeline.

Fast by default

  • The t pipe memoizes its result, injectTranslate() + computed() does zero work per change-detection pass, and ICU messages are compiled once and cached. See Performance.

Install

ng-linguo is two packages that do two different jobs at two different times — so you install both, once.

The runtime — what your app imports and ships to the browser:

npm i @ng-linguo/linguo @ngrx/signals

Requires Angular 18+. @ngrx/signals is a peer dependency — bring your own.

The CLI (@ng-linguo/extract) — the build-time tool that scans your source and produces the translation files the runtime loads. It's pure Node with zero Angular dependencies, so it installs as a dev dependency and never reaches the browser:

npm i -D @ng-linguo/extract

Its bin is linguo-extract; you run it with npx linguo-extract … (or from an npm script). Neither package depends on the other, so each stays minimal — the runtime renders translations in your app, the CLI generates them in your build.

Getting started

The fastest path from an empty Angular app to a translated one. Steps 1–3 get the runtime working; step 4 uses the CLI to generate the real translation files.

1. Configure the runtime

Add the providers to your app.config.ts (or bootstrapApplication). Pick a loader — loading is explicit, so nothing is fetched during DI setup.

Most apps load their translation JSON over HTTP:

import { provideTranslate } from '@ng-linguo/linguo';
import { createHttpLoader } from '@ng-linguo/linguo/http';
import { provideIcu } from '@ng-linguo/linguo/icu';
import { provideHttpClient } from '@angular/common/http';

export const appConfig = {
  providers: [
    provideHttpClient(),
    provideTranslate({
      defaultLang: 'en', // required: reported before load, and the fallback
      // optional: only matches a saved/browser language to one you ship.
      // This is a runtime concern — separate from the CLI's linguo.config.json.
      supportedLangs: ['en', 'pl', 'de'],
      // factory form: the loader is built in DI, so it can use HttpClient.
      // GETs /assets/i18n/<lang>.json by default.
      loader: () => createHttpLoader(),
    }),
    provideIcu(), // optional — enables ICU MessageFormat (defaults to MF2)
  ],
};

Prefer to bundle translations (no network)? A loader is just an object with a load(lang) method, so a static import works too:

import en from './i18n/en.json';
import pl from './i18n/pl.json';

const dictionaries: Record<string, unknown> = { en, pl };

provideTranslate({
  defaultLang: 'en',
  loader: { load: (lang) => Promise.resolve(dictionaries[lang] ?? {}) },
});

2. Load a language at startup

The store never loads on its own. Call restoreLang() once, usually in your root component. It picks the startup language for you — persisted choice → browser preference → defaultLang — and loads it. Gate your UI on the isReady signal to avoid a flash of untranslated content:

import { Component, inject } from '@angular/core';
import { TranslateStore } from '@ng-linguo/linguo';

@Component({
  selector: 'app-root',
  template: `@if (store.isReady()) {
      <router-outlet />
    } @else {
      <app-splash />
    }`,
})
export class App {
  protected readonly store = inject(TranslateStore);
  constructor() {
    void this.store.restoreLang(); // resolve + load the startup language
  }
}

The active language is saved to localStorage (key ng-linguo.lang), and the browser's preferred language is used on the first visit — both on by default and SSR-safe (no-ops on the server). Set supportedLangs so a stored or browser value can be matched to a language you actually ship. To switch language later, call store.setLang('pl') (which also saves the choice). The full set of options — persistSelectedLanguage, restoreSelectedLanguage, persistKey, detectBrowserLanguage — is in Configuration.

3. Translate

In templates use the t pipe or the [t] directive; in TypeScript use injectTranslate().

<!-- plain text -->
{{ 'Save' | t }}

<!-- ICU placeholders & plurals -->
{{ 'Hello {$name}!' | t: { params: { name } } }}

<!-- context: same text, different translation -->
{{ 'Play' | t: { context: 'game' } }}

<!-- rich text: [tag] placeholders bound to your own templates (see the hero above) -->
<p t="[b]Warning:[/b] this cannot be undone">
  <ng-template tFor="b" let-text><strong>{{ text }}</strong></ng-template>
</p>
import { injectTranslate } from '@ng-linguo/linguo';

const t = injectTranslate();

// Reactive and efficient: recomputes only when `name()` or the active language
// changes. Prefer this for frequently-updated or looped bindings.
readonly greeting = computed(() => t('Hello {$name}!', { params: { name: this.name() } }));

Until you generate translation files (step 4), every string falls through to the English you wrote, so the app is fully usable from the first line of code.

4. Generate the translation files

The strings above are also your source catalog. Use the @ng-linguo/extract CLI to collect them and produce the JSON your loader serves:

npm i -D @ng-linguo/extract                   # one-time: install the CLI
npx linguo-extract init --locales en,pl,de    # create linguo.config.json
npx linguo-extract extract                    # scan source → en/pl/de .po catalogs
npx linguo-extract translate --all            # fill missing entries with AI (optional)
npx linguo-extract compile                    # .po → runtime JSON

That's the whole loop. See Translation workflow for the interactive menu, adding languages, and translating by hand.

Which API should I use?

All three resolve the same translations; they differ in where they run and what they can render.

| Use… | When | Notes | | ------------------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | [t] directive | Translating an element, rich text with slots, or hot lists | The most efficient option for the DOM. Re-renders via an effect() only when something changes. | | injectTranslate() | TypeScript, or a binding read inside a computed() | Zero work per change-detection pass. Best for frequently-updated or looped bindings. Slots → text. | | t pipe | Quick inline strings, attribute bindings | Convenient, but it's an impure pipe (see below). Slots degrade to plain text. |

A note on the t pipe. Angular re-evaluates an impure pipe on every change-detection pass. The t pipe has to be impure to react to a language switch (a pure pipe only re-runs when its input reference changes), so it memoizes aggressively to stay cheap. That's perfectly fine for ordinary templates — but in a long @for list or a hot binding, prefer the [t] directive or injectTranslate() + computed(), which do no per-pass work. The directive is also the only option that renders slot tags as real DOM; the pipe and injectTranslate() return a string, so slots collapse to their text.

Performance

  • The t pipe is memoized. It only re-translates when the key, params, context, or language actually change — so passing a fresh { params: … } object on every render is just a quick equality check, not a re-format.
  • injectTranslate() + computed() does no per-pass work — it recomputes only when its signal inputs change. Reach for it on hot paths.
  • ICU messages are compiled once and cached per (format, locale, message), so repeated formatting of the same pattern is a map lookup.

Translation workflow

@ng-linguo/extract is a pure-Node CLI (no Angular dependency, so it never drags the framework into your tooling) that turns your source into translation files and back. It reads a linguo.config.json — auto-discovered — listing your locales and paths. Install it once as a dev dependency; its bin is linguo-extract, so you invoke it with npx (or from an npm script):

npm i -D @ng-linguo/extract   # install once; the bin is `linguo-extract`
npx linguo-extract init       # create/edit linguo.config.json
npx linguo-extract extract    # scan source → <locale>.po catalogs (additive)
npx linguo-extract translate  # fill missing entries with AI (needs a translator)
npx linguo-extract compile    # .po catalogs → runtime <locale>.json

The interactive menu

New to the tool? Run it with no command to open a guided menu that walks through every step — extract, compile, translate, run the full pipeline — and includes a BIOS-style settings editor where each config field carries an inline description:

npx linguo-extract        # guided menu (also creates/edits the config)

Everything the menu does is also a flag-driven command, so you can graduate to scripts whenever you like.

Extraction is additive

extract scans your .ts and .html for the t pipe, the [t] directive, injectTranslate() calls, and mark(), then merges the results into your existing .po catalogs. New strings are added, removed ones are dropped, and every translation you already have is preserved — entries are matched by their source text plus context. Re-running it is safe and idempotent.

(Need to keep a documentation sample or fixture out of the scan? Wrap it in linguo-ignore-start / linguo-ignore-end comments.)

Adding a language

Adding a locale is a couple of steps — or a couple of clicks in the menu:

npx linguo-extract init --locales en,pl,de,fr   # add `fr` to the config
npx linguo-extract extract                       # seeds fr.po with the source strings
npx linguo-extract translate --locale fr         # fill it in with AI…
# …or: npx linguo-extract copyprompt fr          # …or copy a prompt for any chat model
npx linguo-extract compile                       # produce fr.json

Translating with AI

Because the source strings are full English sentences (not opaque keys), an LLM has all the context it needs. ng-linguo writes a self-contained prompt that teaches the model your context notes, slot tags, and plural rules, and only ever sends entries that are still missing. Two ways to run it:

  • Clipboard (no key, no config): npx linguo-extract copyprompt pl copies the prompt; paste it into any chat model and save the reply over pl.po.
  • Automatic: point the translator config field at a small module that calls your AI provider. ng-linguo builds the prompt and merges the reply; your SDK and API key stay yours. See the @ng-linguo/extract README for a copy-paste module (OpenAI, Anthropic, or any provider).

In CI

Every command runs non-interactively and deterministically, so the pipeline drops into CI as-is:

npx linguo-extract extract          # fails the build if it errors; idempotent otherwise
npx linguo-extract translate --all  # optional: fill any gaps (needs a translator)
npx linguo-extract compile

init is scriptable too: npx linguo-extract init --locales en,pl,de --out public/i18n.

Configuration

ng-linguo has two independent configs that don't overlap by accident: this runtime config (passed to provideTranslate, shipped in your browser bundle) and the build-time linguo.config.json (read only by the Node CLI). The runtime never reads the CLI's file. The one thing both name is the locale list — supportedLangs here vs. locales there — and supportedLangs is optional: it exists purely to match a saved or browser-preferred language to one you actually ship.

provideTranslate(options)

| Option | Type | Default | What it does | | ------------------------- | ---------------------------------------------- | --------------------------- | ----------------------------------------------------------------------------------------------------------------- | | defaultLang | string | (required) | The language reported before anything loads, and the guaranteed fallback. | | loader | TranslationLoader \| () => TranslationLoader | (required) | How translations are fetched. The factory form runs in DI, so the loader can inject services (e.g. HttpClient). | | supportedLangs | string[] | (none) | Languages you ship. Used to match a persisted/browser value; browser detection is skipped when omitted. | | persistSelectedLanguage | boolean | true | Save the active language to localStorage when it changes. SSR-safe. | | restoreSelectedLanguage | boolean | = persistSelectedLanguage | Read the saved language back on startup (inside restoreLang()). SSR-safe. | | persistKey | string | 'ng-linguo.lang' | The localStorage key used for the saved language. | | detectBrowserLanguage | boolean | true | On first run, match navigator.languages against supportedLangs. SSR-safe. |

provideIcu({ defaultFormat }) accepts 'mf2' (default) or 'mf1'. createHttpLoader({ prefix, suffix }) defaults to /assets/i18n/ + .json, fetching ${prefix}${lang}${suffix}. A loader is just an object with a load(lang): Promise<Record<string, string>> method, so any source works.

Packages & entry points

| Import | What it gives you | | -------------------------- | -------------------------------------------------------------------------------------------------- | | @ng-linguo/linguo | TranslateStore, provideTranslate, the t pipe, the [t] directive, injectTranslate, mark | | @ng-linguo/linguo/icu | provideIcu — ICU MessageFormat 1 + 2 | | @ng-linguo/linguo/http | createHttpLoaderHttpClient-backed loader | | @ng-linguo/extract | build-time extraction/translate/compile CLI (pure Node) | | @ng-linguo/eslint-plugin | lint config so the a11y linter trusts empty [t] elements |

Contributing

This is an Nx + pnpm monorepo. CLAUDE.md is the source of truth for architecture, code style, testing, and release conventions — read it first.

pnpm install
pnpm nx run-many -t lint test build   # the full suite (what CI runs)
pnpm nx serve playground              # the demo app

License

MIT