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

@pikku/paraglide

v0.12.7

Published

Paraglide tooling for pikku apps — typed enum-lookup maps from `enum__*` message keys, and the i18n-debug mask locale.

Readme

@pikku/paraglide

Paraglide tooling for pikku apps.

Typed enum-lookup maps

Apps must not resolve i18n keys dynamically (mKey('status.' + value)) — dynamic lookups can't be type-checked or tree-shaken. Instead, enum-valued labels live under a reserved enum__<group>__<member> namespace in the message catalog, and this package generates a static, exhaustive lookup map from them:

// messages/en.json
{
  "enum__health__idle": "Idle",
  "enum__health__backlogged": "Backlogged",
  "enum__health__flowing": "Flowing",
}

generates src/i18n/i18n-enum.gen.ts:

// AUTO-GENERATED by @pikku/paraglide — do not edit.
import { m } from './messages.js'
import type { I18nString } from '@pikku/react'

export type I18nMessage = () => I18nString
export type EnumLabel<E extends string> = Record<E, I18nMessage>

export const health = {
  idle: m.enum__health__idle,
  backlogged: m.enum__health__backlogged,
  flowing: m.enum__health__flowing,
} satisfies EnumLabel<'idle' | 'backlogged' | 'flowing'>
export type HealthKey = keyof typeof health

App code then does a static, exhaustive lookup:

import { health } from './i18n/i18n-enum.gen.js'
const label = health[value]() // value: HealthKey

Because the value is an I18nMessage (a () => I18nString accessor), the label resolves at call time and tracks the active locale. Because the map is keyed by the member union, adding a new enum member is a type error until the catalog has its enum__health__<member> entry.

Reconciliation against the database

The database is the real source of truth for what an enum can be. When you point the generator at the pikku CLI's generated DB enums module (enums.gen.ts — Postgres native enums and SQLite CHECK (col IN (…)) constraints alike), each catalog group whose member set exactly matches a DB enum is typed against that enum:

import type { HealthStatus } from '#pikku/db/enums.gen'

export const health = {
  idle: m.enum__health__idle,
  // …
} satisfies EnumLabel<HealthStatus> // ← the DB enum, not the catalog union

EnumLabel<HealthStatus> is Record<HealthStatus, …>, so the label map is the reconciliation — no separate assertion:

  • catalog drops a DB member, or en.json is missing the key → the m.enum__… reference or the Record exhaustiveness fails tsc, naming the gap;
  • a DB enum that has no catalog group → by default a label map is generated for it referencing enum__<table>_<column>__<member> keys, so tsc tells you exactly which keys to add (set unmatchedDbEnums: 'warn' to only report instead);
  • a group with a member the DB doesn't have (a derived UI state) → a drift warning suggesting you make it a standalone message rather than an enum member.

Defining a label for an enum that's never rendered costs nothing — Paraglide compiles only the messages actually referenced, so unused labels are tree-shaken away.

Member naming

Members must be valid JS identifiers. Spell out leading digits (two_guests, not 2_guests); the generator quotes invalid members as a fallback but warns you to rename.

Vite plugin

Place it after paraglideVitePlugin (the generated file imports the compiled m). It regenerates on catalog edits in dev and only writes on change, so it never loops HMR.

import { paraglideVitePlugin } from '@inlang/paraglide-js'
import { paraglideEnums } from '@pikku/paraglide/vite'

export default defineConfig({
  plugins: [
    paraglideVitePlugin({
      project: './project.inlang',
      outdir: './src/paraglide',
    }),
    paraglideEnums({
      catalog: './messages/en.json',
      outFile: './src/i18n/i18n-enum.gen.ts',
      // optional: reconcile against the DB enum columns
      enumsFile: './packages/functions/.pikku/db/enums.gen.ts',
    }),
  ],
})

enumsImport (the specifier the generated file uses to import the DB types) defaults to a relative path from outFile to enumsFile; pass it explicitly to use a package mapping like #pikku/db/enums.gen.

CLI

For CI / non-Vite flows, run right after paraglide-js compile:

# paraglide-enums <catalog.json> <out.gen.ts> [messagesImport] [enums.gen.ts]
paraglide-enums ./messages/en.json ./src/i18n/i18n-enum.gen.ts ./messages.js ./packages/functions/.pikku/db/enums.gen.ts

The i18n-debug mask locale

tsc catches an invalid message, and the @pikku/mantine I18nNode gate catches a raw string literal on a gated prop. Neither sees a hardcoded string in plain JSX, an aria-label, an alt, a document.title, or anything handed to a non-Mantine component.

i18n-debug covers that gap: render every message as block glyphs, and whatever is still readable on screen never went through a message.

████ ███████        ← a message
Save changes        ← a hardcoded string

The mask is a locale, not a runtime wrapper. Wrapping the m namespace so each message pipes through a mask() touches every export, which is exactly what stops a bundler tree-shaking unused messages; it also adds a check to every call and forces every component to import m from the wrapper rather than from Paraglide. Masked text is just text, and rendering different text per locale is what Paraglide already does.

Place the plugin before paraglideVitePlugin — it writes into the catalog directory that Paraglide then compiles:

import { paraglideVitePlugin } from '@inlang/paraglide-js'
import { paraglideMaskLocale } from '@pikku/paraglide/vite'

export default defineConfig({
  plugins: [
    paraglideMaskLocale({
      catalog: './messages/en.json',
      locale: 'zz', // writes messages/zz.json
    }),
    paraglideVitePlugin({
      project: './project.inlang',
      outdir: './src/paraglide',
    }),
  ],
})

Switching is then one line in the locale bridge — see createLocaleStore in @pikku/react, whose debugLocale option does exactly this:

overwriteGetLocale(() => (isI18nDebug() ? 'zz' : activeLocale))

Two properties worth keeping:

  • Keep the locale out of the app's own supported list. That list drives URL prefixes, hreflang and any backend locale param, none of which should ever see it.
  • It costs the bundle effectively nothing. On a build the catalogue is deleted rather than written, so Paraglide compiles the locale to aliases of the base locale — one const per message, no duplicated strings.

{placeholders} are left intact: they are message inputs rather than copy, and mangling one changes the compiled function's signature. Whitespace is left alone too, so masked text keeps the shape of the original and a layout bug still looks like a layout bug.