eslint-plugin-lingui-typescript
v1.16.9
Published
ESLint plugin for Lingui with TypeScript type-aware rules
Maintainers
Readme
eslint-plugin-lingui-typescript
ESLint rules for Lingui that read TypeScript types instead of guessing. Your whitelist goes away.
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 classThe 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 LinguiGetting started
Requirements
- Node.js >= 24, ESLint >= 9, TypeScript >= 5
typescript-eslintwith type-aware linting enabled
Install
npm install --save-dev eslint-plugin-lingui-typescriptConfigure
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
npm uninstall eslint-plugin-linguinpm install --save-dev eslint-plugin-lingui-typescript- Switch to flat config:
import linguiPlugin from "eslint-plugin-lingui-typescript" export default [ // ... linguiPlugin.configs["flat/recommended"] ] - Change rule prefix from
lingui/tolingui-ts/ - 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
- Lingui — The i18n library this plugin is built for
- eslint-plugin-lingui — The official Lingui ESLint plugin
- typescript-eslint — Makes type-aware linting possible
- OXLint — High-performance linter with JS plugin support
License
Built by Sebastian Software
