Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions docs/rules/no-t-inside-trans-functions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# no-t-inside-trans-functions

Disallow `t` function calls inside translation functions or components.

## Rule Details

This rule prevents the use of `t` macro calls within `Trans`, `Plural` components or `plural` function calls. These contexts already handle internationalization, so using `t` inside them is redundant and can cause issues.

Examples of **incorrect** code for this rule:

```jsx
import { t, Trans, Plural, plural } from '@lingui/macro'

// ❌ Using t inside Trans component
<Trans>
{t`Hello world`}
</Trans>

// ❌ Using t inside Plural component
<Plural
value={count}
one="one book"
other={t`${count} books`}
/>

// ❌ Using t inside plural function
plural(count, {
one: "one book",
other: t`There are ${count} books`,
})
```

Examples of **correct** code for this rule:

```jsx
import { t, Trans, Plural, plural } from '@lingui/macro'

// ✅ Using t outside translation contexts
const message = t`Hello world`

// ✅ Using Trans without nested t calls
<Trans>Hello world</Trans>

// ✅ Using Plural with direct strings or expressions
<Plural
value={count}
one="one book"
other={`${count} books`}
/>

// ✅ Using plural with direct strings
plural(count, {
one: "one book",
other: "There are many books",
})
```

## Why?

- `Trans`, `Plural` components and `plural` function already provide internationalization functionality
- Nesting `t` calls inside these contexts is redundant and unnecessary
- It can lead to double-processing of translations
- It makes the code more complex and harder to maintain

## When Not To Use It

This rule should generally always be enabled when using Lingui, as nesting `t` calls is rarely the intended behavior.
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as tCallInFunctionRule from './rules/t-call-in-function'
import * as textRestrictionsRule from './rules/text-restrictions'
import * as noTransInsideTransRule from './rules/no-trans-inside-trans'
import * as consistentPluralFormatRule from './rules/consistent-plural-format'

import * as noTCallInsideTransRule from './rules/no-t-inside-trans-functions'
import { ESLint, Linter } from 'eslint'
import { FlatConfig, RuleModule } from '@typescript-eslint/utils/ts-eslint'

Expand All @@ -19,6 +19,7 @@ const rules = {
[textRestrictionsRule.name]: textRestrictionsRule.rule,
[noTransInsideTransRule.name]: noTransInsideTransRule.rule,
[consistentPluralFormatRule.name]: consistentPluralFormatRule.rule,
[noTCallInsideTransRule.name]: noTCallInsideTransRule.rule,
}

type RuleKey = keyof typeof rules
Expand Down
74 changes: 74 additions & 0 deletions src/rules/no-t-inside-trans-functions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { TSESTree } from '@typescript-eslint/utils'
import { createRule } from '../create-rule'

export const name = 'no-t-call-inside-trans-functions'
export const rule = createRule({
name,
meta: {
docs: {
description: 'Disallow `t` function calls inside translation functions or components',
recommended: 'error',
},
messages: {
default:
'`t` function calls cannot be used inside `Trans`, `Plural` components, `plural` function calls, or other `t` calls.',
},
schema: [
{
type: 'object',
properties: {},
additionalProperties: false,
},
],
type: 'problem' as const,
},
defaultOptions: [],

create: (context) => {
function isInsideTransFunction(node: TSESTree.Node): boolean {
let parent = node.parent

while (parent) {
// Check for JSX elements: <Trans>, <Plural>
if (parent.type === 'JSXElement' && parent.openingElement.name.type === 'JSXIdentifier') {
const tagName = parent.openingElement.name.name
if (tagName === 'Trans' || tagName === 'Plural') {
return true
}
}

// Check for function calls: plural()
if (parent.type === 'CallExpression' && parent.callee.type === 'Identifier') {
if (parent.callee.name === 'plural') {
return true
}
}

// Check for nested t calls: t`some text ${t`nested`}`
if (parent.type === 'TaggedTemplateExpression' && parent.tag.type === 'Identifier') {
if (parent.tag.name === 't') {
return true
}
}

parent = parent.parent
}

return false
}

return {
'TaggedTemplateExpression[tag.name=t]'(node: any) {
if (isInsideTransFunction(node)) {
context.report({
node,
messageId: 'default',
})
}
},
}
},
})

// Export as default for compatibility with test
export default rule
67 changes: 67 additions & 0 deletions tests/src/rules/no-t-inside-trans-functions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { RuleTester } from '@typescript-eslint/rule-tester'
import { rule, name } from '../../../src/rules/no-t-inside-trans-functions'

const ruleTester = new RuleTester({
languageOptions: {
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
},
})

ruleTester.run(name, rule, {
valid: [
{
code: `const message = t\`Hello\``,
},
{
code: `plural(count, { one: "one book", other: "There are many books" });`,
},
{
code: `<Trans>There are many books</Trans>`,
},
{
code: `<Plural value={count} one="one book" other="many books" />`,
},
// # is okay
{
code: `<Plural value={count} one="one book" other="# books" />`,
},
{
code: `<Plural value={count} one="one book" other={\`\${count} books\`} />`,
},
],

invalid: [
{
// Invalid: `t` inside `plural`
code: `
plural(count, {
one: "one book",
other: t\`There are \${count} books\`,
});
`,
errors: [{ messageId: 'default' }],
},
{
// Invalid: `t` inside `Plural`
code: `<Plural value={count} one="one book" other={t\`\${count} books\`} />`,
errors: [{ messageId: 'default' }],
},
{
// Invalid: `t` inside `Trans`
code: `
<Trans>
{t\`Hello\`}
</Trans>;
`,
errors: [{ messageId: 'default' }],
},
{
code: `t\`some text \${t\`some other text\`}\``,
errors: [{ messageId: 'default' }],
},
],
})