From d095e6e4bd2c3e8799b341532f65152d4555932b Mon Sep 17 00:00:00 2001 From: Garik Khachatryan Date: Wed, 15 Oct 2025 17:14:11 -0700 Subject: [PATCH 1/7] add rule and tests --- src/rules/no-t-inside-trans-functions.ts | 69 +++++++++++++++++++ .../rules/no-t-inside-trans-functions.test.ts | 63 +++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 src/rules/no-t-inside-trans-functions.ts create mode 100644 tests/src/rules/no-t-inside-trans-functions.test.ts diff --git a/src/rules/no-t-inside-trans-functions.ts b/src/rules/no-t-inside-trans-functions.ts new file mode 100644 index 0000000..a4aeac9 --- /dev/null +++ b/src/rules/no-t-inside-trans-functions.ts @@ -0,0 +1,69 @@ +import { Rule } from 'eslint' +import { TSESTree } from '@typescript-eslint/utils' + +const rule: Rule.RuleModule = { + meta: { + type: 'problem', + docs: { + description: 'Disallow `t` function calls inside translation functions or components', + category: 'Best Practices', + recommended: false, + }, + messages: { + noTInsideTransFunctions: + '`t` function calls cannot be used inside `Trans`, `Plural` components or `plural` function calls.', + }, + schema: [], // No options for this rule + }, + + create: (context) => { + const sourceCode = context.sourceCode ?? context.getSourceCode() + + function isInsideTransFunction(node: TSESTree.Node): boolean { + let parent = node.parent + + while (parent) { + // Check for JSX elements: , + 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 + } + } + + parent = parent.parent + } + + return false + } + + return { + 'TaggedTemplateExpression[tag.name=t]'(node: any) { + if (isInsideTransFunction(node)) { + context.report({ + node, + messageId: 'noTInsideTransFunctions', + }) + } + }, + + 'CallExpression[callee.name=t]'(node: any) { + if (isInsideTransFunction(node)) { + context.report({ + node, + messageId: 'noTInsideTransFunctions', + }) + } + }, + } + }, +} + +export default rule diff --git a/tests/src/rules/no-t-inside-trans-functions.test.ts b/tests/src/rules/no-t-inside-trans-functions.test.ts new file mode 100644 index 0000000..1e095a9 --- /dev/null +++ b/tests/src/rules/no-t-inside-trans-functions.test.ts @@ -0,0 +1,63 @@ +import { RuleTester } from 'eslint' +import rule from '../../../src/rules/no-t-inside-trans-functions' + +const ruleTester = new RuleTester({ + languageOptions: { + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, +}) + +ruleTester.run('no-t-inside-trans-functions', rule, { + valid: [ + { + code: `const message = t\`Hello\``, + }, + { + code: `plural(count, { one: "one book", other: "There are many books" });`, + }, + { + code: `There are many books`, + }, + { + code: ``, + }, + // # is okay + { + code: ``, + }, + { + code: ``, + }, + ], + + invalid: [ + { + // Invalid: `t` inside `plural` + code: ` + plural(count, { + one: "one book", + other: t\`There are \${count} books\`, + }); + `, + errors: [{ messageId: 'noTInsideTransFunctions' }], + }, + { + // Invalid: `t` inside `Plural` + code: ``, + errors: [{ messageId: 'noTInsideTransFunctions' }], + }, + { + // Invalid: `t` inside `Trans` + code: ` + + {t\`Hello\`} + ; + `, + errors: [{ messageId: 'noTInsideTransFunctions' }], + }, + ], +}) From a51463dbb22bdb050025c1021dd6eb796eb5b377 Mon Sep 17 00:00:00 2001 From: Garik Khachatryan Date: Wed, 15 Oct 2025 17:15:08 -0700 Subject: [PATCH 2/7] add docs --- docs/rules/no-t-inside-trans-functions.md | 67 +++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 docs/rules/no-t-inside-trans-functions.md diff --git a/docs/rules/no-t-inside-trans-functions.md b/docs/rules/no-t-inside-trans-functions.md new file mode 100644 index 0000000..758c181 --- /dev/null +++ b/docs/rules/no-t-inside-trans-functions.md @@ -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 + + {t`Hello world`} + + +// ❌ Using t inside Plural component + + +// ❌ 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 +Hello world + +// ✅ Using Plural with direct strings or expressions + + +// ✅ 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. From 93167f8790153f2dcf6280f67ae55117804a9988 Mon Sep 17 00:00:00 2001 From: Garik Khachatryan Date: Wed, 15 Oct 2025 17:18:40 -0700 Subject: [PATCH 3/7] also for t inside t --- src/rules/no-t-inside-trans-functions.ts | 16 +++++++++++++++- .../rules/no-t-inside-trans-functions.test.ts | 4 ++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/rules/no-t-inside-trans-functions.ts b/src/rules/no-t-inside-trans-functions.ts index a4aeac9..430cca0 100644 --- a/src/rules/no-t-inside-trans-functions.ts +++ b/src/rules/no-t-inside-trans-functions.ts @@ -11,7 +11,7 @@ const rule: Rule.RuleModule = { }, messages: { noTInsideTransFunctions: - '`t` function calls cannot be used inside `Trans`, `Plural` components or `plural` function calls.', + '`t` function calls cannot be used inside `Trans`, `Plural` components, `plural` function calls, or other `t` calls.', }, schema: [], // No options for this rule }, @@ -38,6 +38,20 @@ const rule: Rule.RuleModule = { } } + // 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 + } + } + + // Check for nested t function calls: t('some text', { value: t('nested') }) + if (parent.type === 'CallExpression' && parent.callee.type === 'Identifier') { + if (parent.callee.name === 't') { + return true + } + } + parent = parent.parent } diff --git a/tests/src/rules/no-t-inside-trans-functions.test.ts b/tests/src/rules/no-t-inside-trans-functions.test.ts index 1e095a9..28baa84 100644 --- a/tests/src/rules/no-t-inside-trans-functions.test.ts +++ b/tests/src/rules/no-t-inside-trans-functions.test.ts @@ -59,5 +59,9 @@ ruleTester.run('no-t-inside-trans-functions', rule, { `, errors: [{ messageId: 'noTInsideTransFunctions' }], }, + { + code: `t\`some text \${t\`some other text\`}\``, + errors: [{ messageId: 'noTInsideTransFunctions' }], + }, ], }) From 32f1c5e7919a5361d464904c4673b451e1fccf09 Mon Sep 17 00:00:00 2001 From: Garik Khachatryan Date: Wed, 15 Oct 2025 17:33:29 -0700 Subject: [PATCH 4/7] forgot --- src/index.ts | 3 +- src/rules/no-t-inside-trans-functions.ts | 31 ++++++++++++------- .../rules/no-t-inside-trans-functions.test.ts | 14 ++++----- 3 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/index.ts b/src/index.ts index 6e19e62..20990c9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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' @@ -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 diff --git a/src/rules/no-t-inside-trans-functions.ts b/src/rules/no-t-inside-trans-functions.ts index 430cca0..90856aa 100644 --- a/src/rules/no-t-inside-trans-functions.ts +++ b/src/rules/no-t-inside-trans-functions.ts @@ -1,24 +1,30 @@ -import { Rule } from 'eslint' import { TSESTree } from '@typescript-eslint/utils' +import { createRule } from '../create-rule' -const rule: Rule.RuleModule = { +export const name = 'no-t-call-inside-trans-functions' +export const rule = createRule({ + name, meta: { - type: 'problem', docs: { description: 'Disallow `t` function calls inside translation functions or components', - category: 'Best Practices', - recommended: false, + recommended: 'error', }, messages: { - noTInsideTransFunctions: + default: '`t` function calls cannot be used inside `Trans`, `Plural` components, `plural` function calls, or other `t` calls.', }, - schema: [], // No options for this rule + schema: [ + { + type: 'object', + properties: {}, + additionalProperties: false, + }, + ], + type: 'problem' as const, }, + defaultOptions: [], create: (context) => { - const sourceCode = context.sourceCode ?? context.getSourceCode() - function isInsideTransFunction(node: TSESTree.Node): boolean { let parent = node.parent @@ -63,7 +69,7 @@ const rule: Rule.RuleModule = { if (isInsideTransFunction(node)) { context.report({ node, - messageId: 'noTInsideTransFunctions', + messageId: 'default', }) } }, @@ -72,12 +78,13 @@ const rule: Rule.RuleModule = { if (isInsideTransFunction(node)) { context.report({ node, - messageId: 'noTInsideTransFunctions', + messageId: 'default', }) } }, } }, -} +}) +// Export as default for compatibility with test export default rule diff --git a/tests/src/rules/no-t-inside-trans-functions.test.ts b/tests/src/rules/no-t-inside-trans-functions.test.ts index 28baa84..6f90719 100644 --- a/tests/src/rules/no-t-inside-trans-functions.test.ts +++ b/tests/src/rules/no-t-inside-trans-functions.test.ts @@ -1,5 +1,5 @@ -import { RuleTester } from 'eslint' -import rule from '../../../src/rules/no-t-inside-trans-functions' +import { RuleTester } from '@typescript-eslint/rule-tester' +import { rule, name } from '../../../src/rules/no-t-inside-trans-functions' const ruleTester = new RuleTester({ languageOptions: { @@ -11,7 +11,7 @@ const ruleTester = new RuleTester({ }, }) -ruleTester.run('no-t-inside-trans-functions', rule, { +ruleTester.run(name, rule, { valid: [ { code: `const message = t\`Hello\``, @@ -43,12 +43,12 @@ ruleTester.run('no-t-inside-trans-functions', rule, { other: t\`There are \${count} books\`, }); `, - errors: [{ messageId: 'noTInsideTransFunctions' }], + errors: [{ messageId: 'default' }], }, { // Invalid: `t` inside `Plural` code: ``, - errors: [{ messageId: 'noTInsideTransFunctions' }], + errors: [{ messageId: 'default' }], }, { // Invalid: `t` inside `Trans` @@ -57,11 +57,11 @@ ruleTester.run('no-t-inside-trans-functions', rule, { {t\`Hello\`} ; `, - errors: [{ messageId: 'noTInsideTransFunctions' }], + errors: [{ messageId: 'default' }], }, { code: `t\`some text \${t\`some other text\`}\``, - errors: [{ messageId: 'noTInsideTransFunctions' }], + errors: [{ messageId: 'default' }], }, ], }) From 319ec170f1de0268a46c3059998f45bd60b14865 Mon Sep 17 00:00:00 2001 From: Garik Khachatryan Date: Wed, 15 Oct 2025 19:09:21 -0700 Subject: [PATCH 5/7] ah not actually calling t func --- src/rules/no-t-inside-trans-functions.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/rules/no-t-inside-trans-functions.ts b/src/rules/no-t-inside-trans-functions.ts index 90856aa..8510fb6 100644 --- a/src/rules/no-t-inside-trans-functions.ts +++ b/src/rules/no-t-inside-trans-functions.ts @@ -51,13 +51,6 @@ export const rule = createRule({ } } - // Check for nested t function calls: t('some text', { value: t('nested') }) - if (parent.type === 'CallExpression' && parent.callee.type === 'Identifier') { - if (parent.callee.name === 't') { - return true - } - } - parent = parent.parent } @@ -73,15 +66,6 @@ export const rule = createRule({ }) } }, - - 'CallExpression[callee.name=t]'(node: any) { - if (isInsideTransFunction(node)) { - context.report({ - node, - messageId: 'default', - }) - } - }, } }, }) From 42f8cb753314828c10cb210dd9f8c64601fc9f96 Mon Sep 17 00:00:00 2001 From: Garik Khachatryan Date: Mon, 10 Nov 2025 15:22:11 -0800 Subject: [PATCH 6/7] change to no-nested-trans --- docs/rules/no-nested-trans.md | 77 ++++++++ src/helpers.ts | 21 +++ src/index.ts | 4 +- src/rules/no-nested-trans.ts | 139 ++++++++++++++ src/rules/no-t-inside-trans-functions.ts | 74 -------- tests/src/rules/no-nested-trans.test.ts | 175 ++++++++++++++++++ .../rules/no-t-inside-trans-functions.test.ts | 67 ------- 7 files changed, 414 insertions(+), 143 deletions(-) create mode 100644 docs/rules/no-nested-trans.md create mode 100644 src/rules/no-nested-trans.ts delete mode 100644 src/rules/no-t-inside-trans-functions.ts create mode 100644 tests/src/rules/no-nested-trans.test.ts delete mode 100644 tests/src/rules/no-t-inside-trans-functions.test.ts diff --git a/docs/rules/no-nested-trans.md b/docs/rules/no-nested-trans.md new file mode 100644 index 0000000..7c37c44 --- /dev/null +++ b/docs/rules/no-nested-trans.md @@ -0,0 +1,77 @@ +# no-nested-trans + +Disallow nested translation functions and components. + +Translation functions and components should not be nested inside each other. This includes: + +- Tagged template expressions: `t```, `msg``, `defineMessage`` +- Function calls: `t()`, `msg()`, `defineMessage()`, `plural()`, `select()`, `selectOrdinal()` +- JSX components: ``, ``, ` + +// Components inside components + +one item} other="many" /> + +// Function calls inside function calls +plural(count, { + one: "one book", + other: t`There are ${count} books` +}) + +select(gender, { + male: plural(count, { one: "one", other: "many" }), + other: "items" +}) + +// Nested tagged templates +t`Hello ${t`world`}` +msg`Hello ${plural(count, { one: "one", other: "many" })}` +``` + +✅ Examples of **correct** code for this rule: + +```tsx +// Standalone usage +const message = t`Hello` +const books = plural(count, { one: "one book", other: "many books" }) +const greeting = select(gender, { male: "He", female: "She", other: "They" }) + +// Components with static content +There are many books + + + * + */ +export const LinguiSelectComponentQuery = 'JSXElement[openingElement.name.name=Select]' + +export const LinguiSelectOrdinalComponentQuery = + 'JSXElement[openingElement.name.name=SelectOrdinal]' + export function isNativeDOMTag(str: string) { return DOM_TAGS.includes(str) } diff --git a/src/index.ts b/src/index.ts index 20990c9..f365332 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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 * as noNestedTransRule from './rules/no-nested-trans' import { ESLint, Linter } from 'eslint' import { FlatConfig, RuleModule } from '@typescript-eslint/utils/ts-eslint' @@ -19,7 +19,7 @@ const rules = { [textRestrictionsRule.name]: textRestrictionsRule.rule, [noTransInsideTransRule.name]: noTransInsideTransRule.rule, [consistentPluralFormatRule.name]: consistentPluralFormatRule.rule, - [noTCallInsideTransRule.name]: noTCallInsideTransRule.rule, + [noNestedTransRule.name]: noNestedTransRule.rule, } type RuleKey = keyof typeof rules diff --git a/src/rules/no-nested-trans.ts b/src/rules/no-nested-trans.ts new file mode 100644 index 0000000..0b53210 --- /dev/null +++ b/src/rules/no-nested-trans.ts @@ -0,0 +1,139 @@ +import { TSESTree } from '@typescript-eslint/utils' +import { createRule } from '../create-rule' +import { + LinguiTransQuery, + LinguiCallExpressionPluralQuery, + LinguiPluralComponentQuery, + LinguiCallExpressionSelectQuery, + LinguiCallExpressionSelectOrdinalQuery, + LinguiSelectComponentQuery, + LinguiSelectOrdinalComponentQuery, +} from '../helpers' + +export const name = 'no-nested-trans' +export const rule = createRule({ + name, + meta: { + docs: { + description: 'Disallow nested translation functions and components', + recommended: 'error', + }, + messages: { + default: + 'Translation functions and components cannot be nested inside each other. Found {{childType}} inside {{parentType}}.', + }, + schema: [ + { + type: 'object', + properties: {}, + additionalProperties: false, + }, + ], + type: 'problem' as const, + }, + defaultOptions: [], + + create: (context) => { + // All Lingui translation functions and components + const allLinguiQueries = [ + LinguiTransQuery, + LinguiPluralComponentQuery, + LinguiSelectComponentQuery, + LinguiSelectOrdinalComponentQuery, + LinguiCallExpressionPluralQuery, + LinguiCallExpressionSelectQuery, + LinguiCallExpressionSelectOrdinalQuery, + 'TaggedTemplateExpression[tag.name=t]', + 'TaggedTemplateExpression[tag.name=msg]', + 'TaggedTemplateExpression[tag.name=defineMessage]', + 'CallExpression[callee.name=t]', + 'CallExpression[callee.name=msg]', + 'CallExpression[callee.name=defineMessage]', + ].join(', ') + + function getNodeType(node: TSESTree.Node): string { + if (node.type === 'JSXElement') { + const jsxNode = node as TSESTree.JSXElement + if (jsxNode.openingElement.name.type === 'JSXIdentifier') { + return `<${jsxNode.openingElement.name.name}>` + } + } else if (node.type === 'TaggedTemplateExpression') { + const taggedNode = node as TSESTree.TaggedTemplateExpression + if (taggedNode.tag.type === 'Identifier') { + return `${taggedNode.tag.name}\`\`` + } + } else if (node.type === 'CallExpression') { + const callNode = node as TSESTree.CallExpression + if (callNode.callee.type === 'Identifier') { + return `${callNode.callee.name}()` + } + } + return 'translation function' + } + + function findParentTranslationFunction(node: TSESTree.Node): TSESTree.Node | null { + let parent = node.parent + while (parent) { + // Check for JSX elements (Trans, Plural, Select, SelectOrdinal) + if (parent.type === 'JSXElement') { + const jsxParent = parent as TSESTree.JSXElement + if (jsxParent.openingElement.name.type === 'JSXIdentifier') { + const tagName = jsxParent.openingElement.name.name + if (['Trans', 'Plural', 'Select', 'SelectOrdinal'].includes(tagName)) { + return parent + } + } + } + + // Check for function calls (plural, select, selectOrdinal, t, msg, defineMessage) + if (parent.type === 'CallExpression') { + const callParent = parent as TSESTree.CallExpression + if (callParent.callee.type === 'Identifier') { + const funcName = callParent.callee.name + if ( + ['plural', 'select', 'selectOrdinal', 't', 'msg', 'defineMessage'].includes(funcName) + ) { + return parent + } + } + } + + // Check for tagged template expressions (t``, msg``, defineMessage``) + if (parent.type === 'TaggedTemplateExpression') { + const taggedParent = parent as TSESTree.TaggedTemplateExpression + if (taggedParent.tag.type === 'Identifier') { + const tagName = taggedParent.tag.name + if (['t', 'msg', 'defineMessage'].includes(tagName)) { + return parent + } + } + } + + parent = parent.parent + } + return null + } + + return { + [`${allLinguiQueries}`](node: TSESTree.Node) { + const parentTranslationFunction = findParentTranslationFunction(node) + if (parentTranslationFunction) { + const childType = getNodeType(node) + const parentType = getNodeType(parentTranslationFunction) + + context.report({ + node, + messageId: 'default', + data: { + childType, + parentType, + }, + }) + } + }, + } + }, +}) + +// Export as default for compatibility with test +export default rule diff --git a/src/rules/no-t-inside-trans-functions.ts b/src/rules/no-t-inside-trans-functions.ts deleted file mode 100644 index 8510fb6..0000000 --- a/src/rules/no-t-inside-trans-functions.ts +++ /dev/null @@ -1,74 +0,0 @@ -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: , - 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 diff --git a/tests/src/rules/no-nested-trans.test.ts b/tests/src/rules/no-nested-trans.test.ts new file mode 100644 index 0000000..7071284 --- /dev/null +++ b/tests/src/rules/no-nested-trans.test.ts @@ -0,0 +1,175 @@ +import { RuleTester } from '@typescript-eslint/rule-tester' +import { rule, name } from '../../../src/rules/no-nested-trans' + +const ruleTester = new RuleTester({ + languageOptions: { + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, +}) + +ruleTester.run(name, rule, { + valid: [ + // Valid standalone usage + { + code: `const message = t\`Hello\``, + }, + { + code: `const message = t({ message: "Hello" })`, + }, + { + code: `const message = msg\`Hello\``, + }, + { + code: `const message = defineMessage\`Hello\``, + }, + { + code: `plural(count, { one: "one book", other: "There are many books" });`, + }, + { + code: `select(value, { male: "He", female: "She", other: "They" });`, + }, + { + code: `selectOrdinal(value, { one: "1st", two: "2nd", other: "#th" });`, + }, + { + code: `There are many books`, + }, + { + code: ``, + }, + { + code: ``, + errors: [{ messageId: 'default', data: { childType: 't``', parentType: 'He} other="They" />`, + errors: [{ messageId: 'default', data: { childType: '', parentType: '