🔍 Type-aware ESLint rules for Lingui — catch unlocalized strings with zero configuration using TypeScript's type system.
Traditional i18n linters rely on heuristics and manual whitelists to distinguish user-visible text from technical strings. This leads to false positives and constant configuration tweaking.
This plugin leverages TypeScript's type system to automatically recognize technical strings:
// ✅ Automatically ignored - TypeScript knows these are technical
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
// ✅ Automatically ignored - styling props, constants, and numeric strings
<Box containerClassName="flex items-center" /> // *ClassName, *Color, *Style, etc.
<div className={clsx("px-4", "py-2")} /> // className utilities (clsx, cn, etc.)
<Calendar classNames={{ day: "bg-white" }} /> // nested classNames objects
const colorClasses = { active: "bg-green-100" } // *Classes, *Colors, *Styles, etc.
const price = "1,00€" // No letters = technical
// ❌ Reported - actual user-visible text
const message = "Welcome to our app"
<button>Save changes</button>No configuration needed for DOM APIs, Intl methods, string literal unions, styling props, or numeric strings. TypeScript + smart heuristics handle it!
The plugin uses TypeScript's symbol resolution to verify that t, Trans, msg, etc. actually come from Lingui packages — not just any function with the same name:
import { t } from "@lingui/macro"
const label = t`Save` // ✅ Recognized as Lingui
// Your own function with the same name
const t = (key: string) => translations[key]
const label = t("save") // ❌ Not confused with Lingui- 🔍 Detects incorrect usage of Lingui translation macros
- 📝 Enforces simple, safe expressions inside translated messages
- 🎯 Detects missing localization of user-visible text
- 🧠 Zero-config recognition of technical strings via TypeScript types
- 🎨 Auto-ignores styling props (
*ClassName,*Color,*Style,*Icon,*Image,*Size,*Id) - 📦 Auto-ignores styling variables (
colorClasses,STATUS_COLORS,buttonStyles, etc.) - 🔧 Auto-ignores styling helper functions (
getStatusColor,getButtonClass, etc.) - 🔢 Auto-ignores numeric/symbolic strings without letters (
"1,00€","12:30") - 🏷️ Branded types for custom ignore patterns (loggers, analytics, etc.)
- 🔒 Verifies Lingui macros actually come from
@lingui/*packages (no false positives from similarly-named functions)
For cases not covered by automatic detection (like custom loggers or analytics), this plugin exports branded types you can use to mark strings as "no translation needed":
import { unlocalized } from "eslint-plugin-lingui-typescript/types"
// Wrap your logger to ignore all string arguments
function createLogger(prefix = "[App]") {
return unlocalized({
debug: (...args: unknown[]) => console.debug(prefix, ...args),
info: (...args: unknown[]) => console.info(prefix, ...args),
warn: (...args: unknown[]) => console.warn(prefix, ...args),
error: (...args: unknown[]) => console.error(prefix, ...args),
})
}
const logger = createLogger()
logger.info("Server started on port", 3000) // ✅ Automatically ignored
logger.error("Connection failed:", error) // ✅ Automatically ignored| Type | Use Case |
|---|---|
UnlocalizedFunction<T> |
Wrap functions/objects to ignore all string arguments |
unlocalized(value) |
Helper function for automatic type inference |
UnlocalizedText |
Generic technical strings |
UnlocalizedLog |
Logger message parameters (string only) |
UnlocalizedStyle |
Style values (colors, fonts, spacing) |
UnlocalizedClassName |
CSS class names |
UnlocalizedEvent |
Analytics/tracking event names |
UnlocalizedKey |
Storage keys, query keys |
See the no-unlocalized-strings documentation for detailed examples.
- Node.js ≥ 24
- ESLint ≥ 9
- TypeScript ≥ 5
typescript-eslintwith type-aware linting enabled
npm install --save-dev eslint-plugin-lingui-typescriptThis plugin requires TypeScript and type-aware linting. Configure your 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 configure rules manually:
{
plugins: {
"lingui-ts": linguiPlugin
},
rules: {
"lingui-ts/no-unlocalized-strings": "error",
"lingui-ts/no-single-variables-to-translate": "error"
}
}| Rule | Description | Recommended |
|---|---|---|
| no-unlocalized-strings | Detects user-visible strings not wrapped in Lingui macros. Uses TypeScript types to automatically ignore technical strings like string literal unions, DOM APIs, and Intl methods. | ✅ |
| no-single-variables-to-translate | Disallows messages that consist only of variables without surrounding text. Such messages provide no context for translators. | ✅ |
| no-single-tag-to-translate | Disallows <Trans> components that contain only a single JSX element without text. The wrapped element should have surrounding text for context. |
✅ |
| no-nested-macros | Prevents nesting Lingui macros inside each other (e.g., t inside <Trans>). Nested macros create invalid message catalogs and confuse translators. |
✅ |
| no-expression-in-message | Restricts embedded expressions to simple identifiers only. Complex expressions like ${user.name} or ${formatPrice(x)} must be extracted to named variables first. |
✅ |
| t-call-in-function | Ensures t macro calls are inside functions or class properties, not at module scope. Module-level calls execute before i18n is initialized and won't update on locale change. |
✅ |
| consistent-plural-format | Enforces consistent plural value format — either # hash syntax or ${var} template literals throughout the codebase. |
✅ |
| text-restrictions | Enforces project-specific text restrictions with custom patterns and messages. Requires configuration. | — |
This plugin is a TypeScript-focused alternative to the official eslint-plugin-lingui. Rule names are compatible where possible, making migration straightforward.
| Feature | eslint-plugin-lingui | eslint-plugin-lingui-typescript |
|---|---|---|
| Type-aware detection | ❌ Heuristics only | ✅ Uses TypeScript types |
| String literal unions | Manual whitelist | ✅ Auto-detected |
| DOM API strings | Manual whitelist | ✅ Auto-detected |
| Intl method arguments | Manual whitelist | ✅ Auto-detected |
Styling props (*ClassName, etc.) |
Manual whitelist | ✅ Auto-detected |
Styling constants (*_COLORS, etc.) |
Manual whitelist | ✅ Auto-detected |
Numeric strings ("1,00€") |
Manual whitelist | ✅ Auto-detected |
| Custom ignore patterns | ignoreFunctions only |
✅ Branded types (unlocalized()) |
| Lingui macro verification | Name-based only | ✅ Verifies package origin |
| ESLint version | 8.x | 9.x (flat config) |
| Config format | Legacy .eslintrc |
Flat config only |
-
Less configuration: TypeScript's type system automatically identifies technical strings — no need to maintain long whitelists of ignored functions and patterns.
-
Fewer false positives: Strings typed as literal unions (like
"loading" | "error") are automatically recognized as non-translatable. -
Modern ESLint: Built for ESLint 9's flat config from the ground up.
| eslint-plugin-lingui | eslint-plugin-lingui-typescript | Options |
|---|---|---|
lingui/no-unlocalized-strings |
lingui-ts/no-unlocalized-strings |
|
lingui/t-call-in-function |
lingui-ts/t-call-in-function |
✅ None |
lingui/no-single-variables-to-translate |
lingui-ts/no-single-variables-to-translate |
✅ None |
lingui/no-expression-in-message |
lingui-ts/no-expression-in-message |
✅ None |
lingui/no-single-tag-to-translate |
lingui-ts/no-single-tag-to-translate |
✅ None |
lingui/text-restrictions |
lingui-ts/text-restrictions |
✅ Compatible (rules), + minLength |
lingui/consistent-plural-format |
lingui-ts/consistent-plural-format |
✅ Compatible (style) |
lingui/no-trans-inside-trans |
lingui-ts/no-nested-macros |
✅ Extended (all macros) |
The no-unlocalized-strings rule has different options because TypeScript types replace most manual configuration:
| Original Option | This Plugin | Notes |
|---|---|---|
useTsTypes |
— | Always enabled (TypeScript required) |
ignore (array of regex) |
ignorePattern (single regex) |
Simplified |
ignoreFunctions |
ignoreFunctions |
✅ Simplified (Console/Error auto-detected) |
ignoreNames (with regex support) |
ignoreNames |
Simplified (no regex, plain strings only) |
| — | ignoreProperties |
New: separate option for JSX attributes and object properties |
ignoreMethodsOnTypes |
— | Not needed (TypeScript handles this automatically) |
What you can remove from your config:
useTsTypes: true— always enabled- Most
ignoreFunctionsentries for DOM APIs — auto-detected via types - Most
ignoreNamesentries for typed parameters — auto-detected via types - Most
ignorePropertiesentries (liketype,role,href) — auto-detected via types ignoreMethodsOnTypes— handled automatically
-
Remove the old plugin:
npm uninstall eslint-plugin-lingui
-
Install this plugin:
npm install --save-dev eslint-plugin-lingui-typescript
-
Update your ESLint config to flat config format (if not already):
// eslint.config.ts import linguiPlugin from "eslint-plugin-lingui-typescript" export default [ // ... other configs linguiPlugin.configs["flat/recommended"] ]
-
Update rule names in your config (change prefix from
lingui/tolingui-ts/). -
Review your ignore lists — many entries may no longer be needed thanks to type-aware detection.
Contributions are welcome! Please read our Contributing Guide and Code of Conduct before submitting a PR.
- Lingui – The excellent i18n library this plugin is built for. Provides powerful macros like
t,<Trans>, andpluralfor seamless internationalization. - eslint-plugin-lingui – The official Lingui ESLint plugin. Great for JavaScript projects; this plugin extends the concept with TypeScript type-awareness.
- typescript-eslint – The foundation that makes type-aware linting possible. This plugin builds on their excellent tooling.
Made with ❤️ by Sebastian Software