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

eslint-plugin-lingui-typescript

v1.16.9

Published

ESLint plugin for Lingui with TypeScript type-aware rules

Readme

eslint-plugin-lingui-typescript

npm version npm downloads CI OXLint compatible License: MIT

ESLint rules for Lingui that read TypeScript types instead of guessing. Your whitelist goes away.

Documentation

The whitelist treadmill

Every i18n linter that relies on pattern matching hits the same wall. It can't tell a CSS class from a button label, or a DOM event name from an error message. So you add them to the whitelist. Then someone calls document.createElement("div") and that gets flagged too. More entries. A new team member joins, hits the same false positives, adds the same entries.

// Pattern-based linters flag all of these as "missing translation"
document.createElement("div")                    // It's a DOM tag name
element.addEventListener("click", handler)       // It's an event name
fetch(url, { mode: "cors" })                     // It's a typed option
const status: "idle" | "loading" = "idle"        // It's a string literal union
<Box className="flex items-center" />            // It's a CSS class

The list grows. The problem stays.

When your linter reads types

A string flowing into keyof HTMLElementTagNameMap isn't user text. A variable typed as "idle" | "loading" | "error" doesn't need translation. TypeScript has had this information all along — this plugin reads it directly.

// TypeScript already has the context — this plugin uses it
document.createElement("div")                           // keyof HTMLElementTagNameMap
element.addEventListener("click", handler)              // keyof GlobalEventHandlersEventMap
fetch(url, { mode: "cors" })                            // RequestMode
date.toLocaleDateString("de-DE", { weekday: "long" })  // Intl.DateTimeFormatOptions

type Status = "idle" | "loading" | "error"
const status: Status = "loading"                        // String literal union

// Styling props and utility patterns — recognized out of the box
<Box containerClassName="flex items-center" />          // *ClassName, *Color, *Style
<Link rel="noopener noreferrer" />                      // Link rel attribute
<div className={clsx("px-4", "py-2")} />               // className utilities (clsx, cn)
<Calendar classNames={{ day: "bg-white" }} />           // Nested classNames objects
const colorClasses = { active: "bg-green-100" }         // *Classes, *Colors, *Styles
const successUrl = "Checkout redirect destination"      // *Url, *Slug, *Email names
const price = "1,00€"                                   // No letters = not user-facing
const letterSpacing = "-0.02em"                         // CSS numeric unit value
if (status === "active") {}                             // Binary comparison
if (error.message.includes("User not found")) {}        // String search on string receiver
const code = error.message.replace("User not found", "E_USER_NOT_FOUND") // replace(searchValue)
el.style.filter = "brightness(0.85)"                    // Direct style assignments
const data = { "United States": info }                  // Object keys (always structural)

// These actually need translation — and get reported
const message = "Welcome to our app"
<button>Save changes</button>

Macro verification works the same way. The plugin checks that t, Trans, and friends actually come from @lingui/* packages through TypeScript's symbol resolution:

import { t } from "@lingui/macro"
const label = t`Save`              // Recognized as Lingui

const t = (key: string) => map[key]
const label = t("save")            // Not confused with Lingui

Getting started

Requirements

  • Node.js >= 24, ESLint >= 9, TypeScript >= 5
  • typescript-eslint with type-aware linting enabled

Install

npm install --save-dev eslint-plugin-lingui-typescript

Configure

Add the recommended config to eslint.config.ts:

import eslint from "@eslint/js"
import tseslint from "typescript-eslint"
import linguiPlugin from "eslint-plugin-lingui-typescript"

export default [
  eslint.configs.recommended,
  ...tseslint.configs.strictTypeChecked,
  linguiPlugin.configs["flat/recommended"],
  {
    languageOptions: {
      parserOptions: {
        projectService: true,
        tsconfigRootDir: import.meta.dirname
      }
    }
  }
]

Or pick individual rules:

{
  plugins: {
    "lingui-ts": linguiPlugin
  },
  rules: {
    "lingui-ts/no-unlocalized-strings": "error",
    "lingui-ts/no-single-variables-to-translate": "error"
  }
}

That's it. DOM APIs, Intl methods, string literal unions, technical property/variable suffixes, comparisons, numeric strings, and string-search checks — all handled from the first run.

OXLint support

This plugin works with OXLint via its JavaScript plugin system. Eight of nine rules run natively in OXLint — no code changes, no wrapper, no adapter.

Add to your .oxlintrc.json:

{
  "jsPlugins": ["eslint-plugin-lingui-typescript"],
  "rules": {
    "lingui-typescript/no-nested-macros": "error",
    "lingui-typescript/no-single-variables-to-translate": "error",
    "lingui-typescript/no-single-tag-to-translate": "error",
    "lingui-typescript/t-call-in-function": "warn",
    "lingui-typescript/no-expression-in-message": "warn",
    "lingui-typescript/consistent-plural-format": "warn",
    "lingui-typescript/prefer-trans-in-jsx": "warn",
    "lingui-typescript/text-restrictions": ["error", { "rules": [] }]
  }
}

The only rule not supported in OXLint is no-unlocalized-strings — it uses TypeScript's type checker at runtime to distinguish UI text from technical strings. As OXLint's type-aware linting matures, this rule will follow.

Dual setup — run OXLint for speed, ESLint for full coverage:

oxlint . && eslint .

Rules

| Rule | Description | Recommended | Fixable | OXLint | |------|-------------|:-----------:|:-------:|:------:| | no-unlocalized-strings | Catches user-visible strings not wrapped in Lingui macros. Uses TypeScript types to skip technical strings automatically. | error | ✅ | *1 | | no-single-variables-to-translate | Prevents messages with only variables and no text — translators need context. | error | — | ✅ | | no-single-tag-to-translate | Prevents <Trans> wrapping a single JSX element without surrounding text. | error | — | ✅ | | no-nested-macros | Prevents nesting Lingui macros inside each other — nested macros produce broken catalogs. | error | — | ✅ | | no-expression-in-message | Keeps expressions simple inside messages. Complex logic goes into named variables. | warn | — | ✅ | | t-call-in-function | Keeps t macro calls inside functions where i18n is initialized. | warn | — | ✅ | | consistent-plural-format | Enforces consistent plural format — either # hash or ${var} template literals. | warn | ✅ | ✅ | | prefer-trans-in-jsx | Prefers <Trans> over {t`...`} in JSX for consistency. | warn | ✅ | ✅ | | text-restrictions | Enforces project-specific text patterns and restrictions. Requires configuration. | — | — | ✅ |

*1 Requires TypeScript's type checker to distinguish UI text from technical strings. OXLint's type-aware linting is in alpha — once stable, this rule will be supported too.

Branded types for edge cases

Automatic detection covers most strings, but some cases need a hint — custom loggers, analytics events, internal keys. The plugin exports branded types for exactly this:

import { unlocalized } from "eslint-plugin-lingui-typescript/types"

const logger = unlocalized({
  debug: (...args: unknown[]) => console.debug(...args),
  info: (...args: unknown[]) => console.info(...args),
  error: (...args: unknown[]) => console.error(...args),
})

logger.info("Server started on port", 3000)  // Not flagged
logger.error("Connection failed:", error)    // Not flagged

| Type | Use case | |------|----------| | UnlocalizedFunction<T> | Wrap functions/objects to ignore all string arguments | | unlocalized(value) | Helper for automatic type inference | | UnlocalizedText | Generic technical strings | | UnlocalizedLog | Logger message parameters | | UnlocalizedStyle | Style values (colors, fonts, spacing) | | UnlocalizedClassName | CSS class names | | UnlocalizedEvent | Analytics/tracking event names | | UnlocalizedKey | Storage keys, query keys | | UnlocalizedRecord<K> | Key-value maps (Record<K, UnlocalizedText>) |

As the plugin gets smarter, some brands become unnecessary. Enable reportUnnecessaryBrands to find the ones you can remove:

"lingui-ts/no-unlocalized-strings": ["error", { "reportUnnecessaryBrands": true }]

Full details in the no-unlocalized-strings docs.

Coming from eslint-plugin-lingui?

Drop-in alternative to eslint-plugin-lingui. Rule names are compatible.

What's different

| | eslint-plugin-lingui | This plugin | |---------|---------------------|--------------------------------| | How it works | Heuristics + manual whitelists | TypeScript type system | | String literal unions | Manual whitelist | Auto-detected | | DOM API strings | Manual whitelist | Auto-detected | | Intl method arguments | Manual whitelist | Auto-detected | | Styling props | Manual whitelist | Auto-detected | | Styling constants | Manual whitelist | Auto-detected | | Object keys | Manual whitelist | Auto-ignored | | Numeric strings | Manual whitelist | Auto-detected | | Custom ignores | ignoreFunctions only | ignoreFunctions, ignoreProperties, ignoreNames, branded types (unlocalized()) | | Macro verification | Name-based | Package-origin verification | | ESLint | v8 legacy config | v9 flat config |

Migration

  1. npm uninstall eslint-plugin-lingui
  2. npm install --save-dev eslint-plugin-lingui-typescript
  3. Switch to flat config:
    import linguiPlugin from "eslint-plugin-lingui-typescript"
    export default [
      // ...
      linguiPlugin.configs["flat/recommended"]
    ]
  4. Change rule prefix from lingui/ to lingui-ts/
  5. Review your ignore lists — most entries are no longer needed

Options mapping for no-unlocalized-strings

| Original | This plugin | Notes | |----------|-------------|-------| | useTsTypes | — | Always on | | ignore (regex array) | ignorePattern (single regex) | Simplified | | ignoreFunctions | ignoreFunctions | Console/Error auto-detected | | ignoreNames (regex) | ignoreNames | Plain strings only | | — | ignoreProperties | New: JSX attributes and object properties | | ignoreMethodsOnTypes | — | Handled by TypeScript |

Contributing

Contributions welcome. See the Contributing Guide and Code of Conduct.

Related

License

MIT


Built by Sebastian Software