Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: no-unlocalized-strings corrected support for interface prop names #95

Merged
Merged
Show file tree
Hide file tree
Changes from 11 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
47 changes: 40 additions & 7 deletions src/rules/no-unlocalized-strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ function createMatcher(patterns: MatcherDef[]) {

function isAcceptableExpression(node: TSESTree.Node): boolean {
switch (node.type) {
case TSESTree.AST_NODE_TYPES.Literal:
case TSESTree.AST_NODE_TYPES.TemplateLiteral:
case TSESTree.AST_NODE_TYPES.LogicalExpression:
case TSESTree.AST_NODE_TYPES.BinaryExpression:
Expand Down Expand Up @@ -250,6 +249,18 @@ export const rule = createRule<Option[], string>({
}
}

function isPropertyKey(node: TSESTree.Node): boolean {
const parent = node.parent
if (!parent) return false

return (
// Property in object literal
(parent.type === TSESTree.AST_NODE_TYPES.Property && parent.key === node) ||
// Property in interface/type
(parent.type === TSESTree.AST_NODE_TYPES.TSPropertySignature && parent.key === node)
)
}

/**
* Helper function to determine if a node is inside an ignored property.
*/
Expand Down Expand Up @@ -332,6 +343,24 @@ export const rule = createRule<Option[], string>({
return false
}

function isInsideTypeContext(node: TSESTree.Node): boolean {
let parent = node.parent

while (parent) {
switch (parent.type) {
case TSESTree.AST_NODE_TYPES.TSPropertySignature:
case TSESTree.AST_NODE_TYPES.TSIndexSignature:
case TSESTree.AST_NODE_TYPES.TSTypeAnnotation:
case TSESTree.AST_NODE_TYPES.TSTypeLiteral:
case TSESTree.AST_NODE_TYPES.TSLiteralType:
return true
}
parent = parent.parent
}

return false
}

const processTextNode = (
node: TSESTree.Literal | TSESTree.TemplateLiteral | TSESTree.JSXText,
) => {
Expand Down Expand Up @@ -386,10 +415,6 @@ export const rule = createRule<Option[], string>({
visited.add(node)
},

'JSXElement > Literal'(node: TSESTree.Literal) {
processTextNode(node)
},

'JSXElement > JSXExpressionContainer > Literal'(node: TSESTree.Literal) {
processTextNode(node)
},
Expand Down Expand Up @@ -552,15 +577,24 @@ export const rule = createRule<Option[], string>({

if (isTextWhiteListed(trimmed)) return

// If this is a property key and the property name is ignored, skip it
if (isPropertyKey(node) && isIgnoredName(String(node.value))) {
return
}

if (isAssignedToIgnoredVariable(node, isIgnoredName)) {
return // Do not report this literal
}

// New check: if the literal is inside an ignored property, do not report
if (isInsideIgnoredProperty(node)) {
return
}

// Only ignore type context for property keys
if (isInsideTypeContext(node)) {
return
}

context.report({ node, messageId: 'default' })
},

Expand All @@ -574,7 +608,6 @@ export const rule = createRule<Option[], string>({
return // Do not report this template literal
}

// New check: if the template literal is inside an ignored property, do not report
if (isInsideIgnoredProperty(node)) {
return
}
Expand Down
185 changes: 185 additions & 0 deletions tests/src/rules/no-unlocalized-strings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,159 @@ ruleTester.run<string, Option[]>(name, rule, {
foo.get("string with a spaces")`,
options: [{ useTsTypes: true, ignoreMethodsOnTypes: ['Foo.get'] }],
},
{
name: 'interface property with html attribute',
code: "interface FieldLabelProps { 'htmlFor': string; }",
},
{
name: 'interface property with aria attribute',
code: "interface FieldInputProps { 'aria-required': boolean; }",
},
{
name: 'type alias with string literal properties',
code: `
type ButtonProps = {
'aria-pressed': boolean;
'data-testid': string;
}
`,
},
{
name: 'interface with nested type',
code: `
interface ComplexProps {
details: {
'nested-attr': string;
};
}
`,
},
{
name: 'interface with string literal type',
code: `
interface Props {
message: 'This is a type';
variant: 'primary' | 'secondary';
}
`,
},
{
name: 'type alias with string literal union',
code: "type ButtonVariant = 'primary' | 'secondary' | 'tertiary';",
},
{
name: 'interface with optional property using string literal type',
code: `
interface Props {
type?: 'success' | 'error';
message: string;
}
`,
},
{
name: 'type with index signature using string literal',
code: `
type Dict = {
[K in 'foo' | 'bar']: string;
}
`,
},
{
name: 'interface with index signature using string literal',
code: `
interface Dict {
['some-key']: string;
}
`,
},
{
name: 'JSX with empty text',
code: `
function Component() {
return <div>
{/* this creates an empty JSXText node */}
</div>
}
`,
},
{
name: 'property key in type literal',
code: `
type Options = {
'some-key': {
'nested-key': string;
}
}
`,
},
{
name: 'JSX with empty template literal in expression container',
code: 'function Component() { return <div>{``}</div> }',
},
{
name: 'JSX with empty text node',
code: `
function Component() {
return <div>

</div>
}
`,
},
{
name: 'JSX with empty string literal',
code: `
function Component() {
return <div>
{''}
</div>
}
`,
},
// For isAcceptableExpression coverage (Image 1)
{
name: 'logical expression in ignored name assignment',
code: `const MY_CONST = foo && 'some string';`,
options: [ignoreUpperCaseName],
},
{
name: 'unary expression in ignored name assignment',
code: `const MY_CONST = !'some string';`,
options: [ignoreUpperCaseName],
},
{
name: 'TSAsExpression in ignored name assignment',
code: `const MY_CONST = ('some string' as string);`,
options: [ignoreUpperCaseName],
},
{
name: 'type with string literal in index signature',
code: `
type Test = {
['literal key']: string;
}
`,
},
{
name: 'type annotation with literal type',
code: `let x: 'foo' | 'bar';`,
},
{
name: 'type literal with string literal',
code: `
type Test = {
prop: 'literal value';
}
`,
},
{
name: 'JSX text content',
code: `
function Component() {
return <Trans>JSX text content</Trans>
}
`,
},
],

invalid: [
Expand Down Expand Up @@ -482,6 +635,38 @@ ruleTester.run<string, Option[]>(name, rule, {
}`,
errors: [{ messageId: 'default' }, { messageId: 'default' }],
},
{
name: 'object literal properties should still be checked',
code: `
const props = {
label: 'This should be translated'
};
`,
errors: [{ messageId: 'default' }],
},
{
name: 'regular string assignments should still be checked',
code: `
let message = 'This should be translated';
`,
errors: [{ messageId: 'default' }],
},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possible the duplication of the cases, we already have these:

    { code: 'const a = `Hello ${nice}`', errors },
    { code: 'export const a = `hello string`;', errors },
    { code: "const ge = 'Select tax code'", errors },
    { code: 'const a = `Foo`;', errors },

    { code: 'var a = {foo: "Bar"};', errors },
    { code: `const test = { text: 'This is not localized' }`, errors },

I know the test suite for this rule is a bit mess, let's not make it even more messier ) Either delete old duplicating cases, or not add a new ones.

Also i tried to somehow group / sort them, so it's easier to spot group of functionality. Like all variants of prop assignments together. All variants of JSX text together, etc.

Thanks!

{
name: 'JSX with direct string literal child',
code: `
function Component() {
return <Trans>{'direct literal'}</Trans>
}
`,
errors: [{ messageId: 'forJsxText' }],
},
{
name: 'TSAs expression with string literal',
code: `
const test = ('hello' as any as string);
`,
errors: [{ messageId: 'default' }],
},
],
})

Expand Down
Loading