From aaca0de452e46250288559079b9e30c39fc200a7 Mon Sep 17 00:00:00 2001 From: Sebastian Werner Date: Mon, 9 Dec 2024 14:03:53 +0100 Subject: [PATCH 01/24] fix: corrected support for interface prop names --- src/rules/no-unlocalized-strings.ts | 61 ++++++++++++++++++- .../src/rules/no-unlocalized-strings.test.ts | 6 ++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/rules/no-unlocalized-strings.ts b/src/rules/no-unlocalized-strings.ts index 222dad7..e3d1b16 100644 --- a/src/rules/no-unlocalized-strings.ts +++ b/src/rules/no-unlocalized-strings.ts @@ -332,6 +332,25 @@ export const rule = createRule({ return false } + function isInsideTypeContext(node: TSESTree.Node): boolean { + let parent = node.parent + + while (parent) { + switch (parent.type) { + case TSESTree.AST_NODE_TYPES.TSInterfaceDeclaration: + case TSESTree.AST_NODE_TYPES.TSTypeAliasDeclaration: + case TSESTree.AST_NODE_TYPES.TSPropertySignature: + case TSESTree.AST_NODE_TYPES.TSIndexSignature: + case TSESTree.AST_NODE_TYPES.TSTypeAnnotation: + case TSESTree.AST_NODE_TYPES.TSTypeLiteral: + return true + } + parent = parent.parent + } + + return false + } + const processTextNode = ( node: TSESTree.Literal | TSESTree.TemplateLiteral | TSESTree.JSXText, ) => { @@ -545,6 +564,36 @@ export const rule = createRule({ processTextNode(node) }, + // Add new visitors for interface-related nodes + 'TSInterfaceDeclaration :matches(Literal,TemplateLiteral)'( + node: TSESTree.Literal | TSESTree.TemplateLiteral, + ) { + // Mark all string literals in interfaces as visited to prevent them from being reported + visited.add(node) + }, + + 'TSTypeAliasDeclaration :matches(Literal,TemplateLiteral)'( + node: TSESTree.Literal | TSESTree.TemplateLiteral, + ) { + // Also handle type aliases similarly + visited.add(node) + }, + + 'TSPropertySignature :matches(Literal,TemplateLiteral)'( + node: TSESTree.Literal | TSESTree.TemplateLiteral, + ) { + // Handle property signatures in interfaces and type literals + visited.add(node) + }, + + 'TSIndexSignature :matches(Literal,TemplateLiteral)'( + node: TSESTree.Literal | TSESTree.TemplateLiteral, + ) { + // Handle index signatures + visited.add(node) + }, + + // Modify the existing Literal:exit visitor to check for interface contexts 'Literal:exit'(node: TSESTree.Literal) { if (visited.has(node)) return const trimmed = `${node.value}`.trim() @@ -556,11 +605,15 @@ export const rule = createRule({ return // Do not report this literal } - // New check: if the literal is inside an ignored property, do not report if (isInsideIgnoredProperty(node)) { return } + // Add check for interface and type contexts + if (isInsideTypeContext(node)) { + return + } + context.report({ node, messageId: 'default' }) }, @@ -574,11 +627,15 @@ export const rule = createRule({ 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 } + // Add check for interface and type contexts + if (isInsideTypeContext(node)) { + return + } + context.report({ node, messageId: 'default' }) }, diff --git a/tests/src/rules/no-unlocalized-strings.test.ts b/tests/src/rules/no-unlocalized-strings.test.ts index 0aa47dd..a39845d 100644 --- a/tests/src/rules/no-unlocalized-strings.test.ts +++ b/tests/src/rules/no-unlocalized-strings.test.ts @@ -363,6 +363,12 @@ ruleTester.run(name, rule, { foo.get("string with a spaces")`, options: [{ useTsTypes: true, ignoreMethodsOnTypes: ['Foo.get'] }], }, + { + code: "interface FieldLabelProps { 'htmlFor': string; }", + }, + { + code: "interface FieldInputProps { 'aria-required': boolean; }", + }, ], invalid: [ From d8936d462c62a266d914bd1f8a14983d4a21c33a Mon Sep 17 00:00:00 2001 From: Sebastian Werner Date: Mon, 9 Dec 2024 15:29:59 +0100 Subject: [PATCH 02/24] fix: enhance test suite and fix further issues --- src/rules/no-unlocalized-strings.ts | 72 ++++++++++++------- .../src/rules/no-unlocalized-strings.test.ts | 39 ++++++++++ 2 files changed, 85 insertions(+), 26 deletions(-) diff --git a/src/rules/no-unlocalized-strings.ts b/src/rules/no-unlocalized-strings.ts index e3d1b16..5a55ac4 100644 --- a/src/rules/no-unlocalized-strings.ts +++ b/src/rules/no-unlocalized-strings.ts @@ -250,6 +250,30 @@ export const rule = createRule({ } } + 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) + ) + } + + function isPropertyValue(node: TSESTree.Node): boolean { + const parent = node.parent + if (!parent) return false + + return ( + // Property value in object literal + (parent.type === TSESTree.AST_NODE_TYPES.Property && parent.value === node) || + // Property value in interface/type (string literal type) + parent.type === TSESTree.AST_NODE_TYPES.TSLiteralType + ) + } + /** * Helper function to determine if a node is inside an ignored property. */ @@ -335,6 +359,16 @@ export const rule = createRule({ function isInsideTypeContext(node: TSESTree.Node): boolean { let parent = node.parent + // Only ignore the node if it's being used as a property key + if (isPropertyKey(node)) { + return true + } + + // Add this check: If this is a property value, don't ignore it even in type contexts + if (isPropertyValue(node)) { + return false + } + while (parent) { switch (parent.type) { case TSESTree.AST_NODE_TYPES.TSInterfaceDeclaration: @@ -343,7 +377,10 @@ export const rule = createRule({ case TSESTree.AST_NODE_TYPES.TSIndexSignature: case TSESTree.AST_NODE_TYPES.TSTypeAnnotation: case TSESTree.AST_NODE_TYPES.TSTypeLiteral: - return true + // Only return true if we're a property key + if (isPropertyKey(node)) { + return true + } } parent = parent.parent } @@ -564,28 +601,6 @@ export const rule = createRule({ processTextNode(node) }, - // Add new visitors for interface-related nodes - 'TSInterfaceDeclaration :matches(Literal,TemplateLiteral)'( - node: TSESTree.Literal | TSESTree.TemplateLiteral, - ) { - // Mark all string literals in interfaces as visited to prevent them from being reported - visited.add(node) - }, - - 'TSTypeAliasDeclaration :matches(Literal,TemplateLiteral)'( - node: TSESTree.Literal | TSESTree.TemplateLiteral, - ) { - // Also handle type aliases similarly - visited.add(node) - }, - - 'TSPropertySignature :matches(Literal,TemplateLiteral)'( - node: TSESTree.Literal | TSESTree.TemplateLiteral, - ) { - // Handle property signatures in interfaces and type literals - visited.add(node) - }, - 'TSIndexSignature :matches(Literal,TemplateLiteral)'( node: TSESTree.Literal | TSESTree.TemplateLiteral, ) { @@ -601,16 +616,21 @@ export const rule = createRule({ 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 + return } if (isInsideIgnoredProperty(node)) { return } - // Add check for interface and type contexts - if (isInsideTypeContext(node)) { + // Only ignore type context for property keys + if (isInsideTypeContext(node) && !isPropertyValue(node)) { return } diff --git a/tests/src/rules/no-unlocalized-strings.test.ts b/tests/src/rules/no-unlocalized-strings.test.ts index a39845d..d2e638d 100644 --- a/tests/src/rules/no-unlocalized-strings.test.ts +++ b/tests/src/rules/no-unlocalized-strings.test.ts @@ -364,11 +364,41 @@ ruleTester.run(name, rule, { options: [{ useTsTypes: true, ignoreMethodsOnTypes: ['Foo.get'] }], }, { + name: 'basic interface with htmlFor attribute', code: "interface FieldLabelProps { 'htmlFor': string; }", }, { + name: 'interface with aria attribute', code: "interface FieldInputProps { 'aria-required': boolean; }", }, + { + name: 'type alias with multiple attributes', + code: ` + type ButtonProps = { + 'aria-pressed': boolean; + 'data-testid': string; + } + `, + }, + { + name: 'interface with nested type', + code: ` + interface ComplexProps { + details: { + 'nested-attr': string; + }; + } + `, + }, + { + name: 'interface with optional and readonly properties', + code: ` + interface Props { + readonly 'data-locked': string; + 'data-optional'?: string; + } + `, + }, ], invalid: [ @@ -488,6 +518,15 @@ ruleTester.run(name, rule, { }`, errors: [{ messageId: 'default' }, { messageId: 'default' }], }, + { + name: 'string literals in object literals should be translated', + code: ` + const props = { + 'data-testid': 'This should be translated' + }; + `, + errors: [{ messageId: 'default' }], + }, ], }) From e0619d5d08ee97a2314fce78c7f6236c170c53fb Mon Sep 17 00:00:00 2001 From: Sebastian Werner Date: Mon, 9 Dec 2024 15:36:22 +0100 Subject: [PATCH 03/24] fix: issue in string values in interfaces --- src/rules/no-unlocalized-strings.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/rules/no-unlocalized-strings.ts b/src/rules/no-unlocalized-strings.ts index 5a55ac4..6169061 100644 --- a/src/rules/no-unlocalized-strings.ts +++ b/src/rules/no-unlocalized-strings.ts @@ -269,8 +269,9 @@ export const rule = createRule({ return ( // Property value in object literal (parent.type === TSESTree.AST_NODE_TYPES.Property && parent.value === node) || - // Property value in interface/type (string literal type) - parent.type === TSESTree.AST_NODE_TYPES.TSLiteralType + // Property value in interface/type + (parent.type === TSESTree.AST_NODE_TYPES.TSPropertySignature && + parent.typeAnnotation === node.parent) ) } From db748204b8f55df409f7e96c648f526cb03ef689 Mon Sep 17 00:00:00 2001 From: Sebastian Werner Date: Mon, 9 Dec 2024 20:02:53 +0100 Subject: [PATCH 04/24] fix: simplified code --- src/rules/no-unlocalized-strings.ts | 31 ++----------------- .../src/rules/no-unlocalized-strings.test.ts | 2 +- 2 files changed, 4 insertions(+), 29 deletions(-) diff --git a/src/rules/no-unlocalized-strings.ts b/src/rules/no-unlocalized-strings.ts index 6169061..f8175ad 100644 --- a/src/rules/no-unlocalized-strings.ts +++ b/src/rules/no-unlocalized-strings.ts @@ -262,19 +262,6 @@ export const rule = createRule({ ) } - function isPropertyValue(node: TSESTree.Node): boolean { - const parent = node.parent - if (!parent) return false - - return ( - // Property value in object literal - (parent.type === TSESTree.AST_NODE_TYPES.Property && parent.value === node) || - // Property value in interface/type - (parent.type === TSESTree.AST_NODE_TYPES.TSPropertySignature && - parent.typeAnnotation === node.parent) - ) - } - /** * Helper function to determine if a node is inside an ignored property. */ @@ -360,16 +347,6 @@ export const rule = createRule({ function isInsideTypeContext(node: TSESTree.Node): boolean { let parent = node.parent - // Only ignore the node if it's being used as a property key - if (isPropertyKey(node)) { - return true - } - - // Add this check: If this is a property value, don't ignore it even in type contexts - if (isPropertyValue(node)) { - return false - } - while (parent) { switch (parent.type) { case TSESTree.AST_NODE_TYPES.TSInterfaceDeclaration: @@ -378,10 +355,8 @@ export const rule = createRule({ case TSESTree.AST_NODE_TYPES.TSIndexSignature: case TSESTree.AST_NODE_TYPES.TSTypeAnnotation: case TSESTree.AST_NODE_TYPES.TSTypeLiteral: - // Only return true if we're a property key - if (isPropertyKey(node)) { - return true - } + case TSESTree.AST_NODE_TYPES.TSLiteralType: + return true } parent = parent.parent } @@ -631,7 +606,7 @@ export const rule = createRule({ } // Only ignore type context for property keys - if (isInsideTypeContext(node) && !isPropertyValue(node)) { + if (isInsideTypeContext(node)) { return } diff --git a/tests/src/rules/no-unlocalized-strings.test.ts b/tests/src/rules/no-unlocalized-strings.test.ts index d2e638d..1505d03 100644 --- a/tests/src/rules/no-unlocalized-strings.test.ts +++ b/tests/src/rules/no-unlocalized-strings.test.ts @@ -525,7 +525,7 @@ ruleTester.run(name, rule, { 'data-testid': 'This should be translated' }; `, - errors: [{ messageId: 'default' }], + errors: [{ messageId: 'default' }, { messageId: 'default' }], }, ], }) From 7885c93e2ae6dd22f61396707054c1d5b02c718d Mon Sep 17 00:00:00 2001 From: Sebastian Werner Date: Mon, 9 Dec 2024 20:26:57 +0100 Subject: [PATCH 05/24] fix: updated test suite --- .../src/rules/no-unlocalized-strings.test.ts | 38 ++++++++++++++----- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/tests/src/rules/no-unlocalized-strings.test.ts b/tests/src/rules/no-unlocalized-strings.test.ts index 1505d03..c264147 100644 --- a/tests/src/rules/no-unlocalized-strings.test.ts +++ b/tests/src/rules/no-unlocalized-strings.test.ts @@ -364,15 +364,15 @@ ruleTester.run(name, rule, { options: [{ useTsTypes: true, ignoreMethodsOnTypes: ['Foo.get'] }], }, { - name: 'basic interface with htmlFor attribute', + name: 'interface property with html attribute', code: "interface FieldLabelProps { 'htmlFor': string; }", }, { - name: 'interface with aria attribute', + name: 'interface property with aria attribute', code: "interface FieldInputProps { 'aria-required': boolean; }", }, { - name: 'type alias with multiple attributes', + name: 'type alias with string literal properties', code: ` type ButtonProps = { 'aria-pressed': boolean; @@ -391,11 +391,24 @@ ruleTester.run(name, rule, { `, }, { - name: 'interface with optional and readonly properties', + name: 'interface with string literal type', code: ` interface Props { - readonly 'data-locked': string; - 'data-optional'?: string; + 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; } `, }, @@ -519,13 +532,20 @@ ruleTester.run(name, rule, { errors: [{ messageId: 'default' }, { messageId: 'default' }], }, { - name: 'string literals in object literals should be translated', + name: 'object literal properties should still be checked', code: ` const props = { - 'data-testid': 'This should be translated' + label: 'This should be translated' }; `, - errors: [{ messageId: 'default' }, { messageId: 'default' }], + errors: [{ messageId: 'default' }], + }, + { + name: 'regular string assignments should still be checked', + code: ` + let message = 'This should be translated'; + `, + errors: [{ messageId: 'default' }], }, ], }) From 4baebbd7b8203bbf5c9f71443a3c3cc3127df309 Mon Sep 17 00:00:00 2001 From: Sebastian Werner Date: Mon, 9 Dec 2024 20:34:15 +0100 Subject: [PATCH 06/24] fix: added more tests --- .../src/rules/no-unlocalized-strings.test.ts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/src/rules/no-unlocalized-strings.test.ts b/tests/src/rules/no-unlocalized-strings.test.ts index c264147..ed1442d 100644 --- a/tests/src/rules/no-unlocalized-strings.test.ts +++ b/tests/src/rules/no-unlocalized-strings.test.ts @@ -412,6 +412,42 @@ ruleTester.run(name, rule, { } `, }, + { + 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
+ {/* this creates an empty JSXText node */} +
+ } + `, + }, + { + name: 'property key in type literal', + code: ` + type Options = { + 'some-key': { + 'nested-key': string; + } + } + `, + }, ], invalid: [ From cd4f715be5db7bfadd0ffe3078532e089c8da683 Mon Sep 17 00:00:00 2001 From: Sebastian Werner Date: Mon, 9 Dec 2024 20:41:36 +0100 Subject: [PATCH 07/24] fix: more tests --- .../src/rules/no-unlocalized-strings.test.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/src/rules/no-unlocalized-strings.test.ts b/tests/src/rules/no-unlocalized-strings.test.ts index ed1442d..f3a1f7b 100644 --- a/tests/src/rules/no-unlocalized-strings.test.ts +++ b/tests/src/rules/no-unlocalized-strings.test.ts @@ -448,6 +448,30 @@ ruleTester.run(name, rule, { } `, }, + { + name: 'JSX with empty template literal in expression container', + code: 'function Component() { return
{``}
}', + }, + { + name: 'JSX with empty text node', + code: ` + function Component() { + return
+ +
+ } + `, + }, + { + name: 'JSX with empty string literal', + code: ` + function Component() { + return
+ {''} +
+ } + `, + }, ], invalid: [ From 5a69d1bf677fbd09e6510fbff745c2fd6da02b12 Mon Sep 17 00:00:00 2001 From: Sebastian Werner Date: Mon, 9 Dec 2024 20:52:55 +0100 Subject: [PATCH 08/24] fix: adding more tests --- .../src/rules/no-unlocalized-strings.test.ts | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/tests/src/rules/no-unlocalized-strings.test.ts b/tests/src/rules/no-unlocalized-strings.test.ts index f3a1f7b..58e2495 100644 --- a/tests/src/rules/no-unlocalized-strings.test.ts +++ b/tests/src/rules/no-unlocalized-strings.test.ts @@ -472,6 +472,50 @@ ruleTester.run(name, rule, { } `, }, + // 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 JSX text content + } + `, + }, ], invalid: [ @@ -607,6 +651,22 @@ ruleTester.run(name, rule, { `, errors: [{ messageId: 'default' }], }, + { + name: 'JSX with direct string literal child', + code: ` + function Component() { + return {'direct literal'} + } + `, + errors: [{ messageId: 'forJsxText' }], + }, + { + name: 'TSAs expression with string literal', + code: ` + const test = ('hello' as any as string); + `, + errors: [{ messageId: 'default' }], + }, ], }) From f073b133fd761cd23be0a20cc77fcdd800d3227f Mon Sep 17 00:00:00 2001 From: Sebastian Werner Date: Mon, 9 Dec 2024 21:00:56 +0100 Subject: [PATCH 09/24] fix: removed some dead paths --- src/rules/no-unlocalized-strings.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/rules/no-unlocalized-strings.ts b/src/rules/no-unlocalized-strings.ts index f8175ad..c5d5841 100644 --- a/src/rules/no-unlocalized-strings.ts +++ b/src/rules/no-unlocalized-strings.ts @@ -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: @@ -349,8 +348,6 @@ export const rule = createRule({ while (parent) { switch (parent.type) { - case TSESTree.AST_NODE_TYPES.TSInterfaceDeclaration: - case TSESTree.AST_NODE_TYPES.TSTypeAliasDeclaration: case TSESTree.AST_NODE_TYPES.TSPropertySignature: case TSESTree.AST_NODE_TYPES.TSIndexSignature: case TSESTree.AST_NODE_TYPES.TSTypeAnnotation: @@ -418,10 +415,6 @@ export const rule = createRule({ visited.add(node) }, - 'JSXElement > Literal'(node: TSESTree.Literal) { - processTextNode(node) - }, - 'JSXElement > JSXExpressionContainer > Literal'(node: TSESTree.Literal) { processTextNode(node) }, @@ -577,13 +570,6 @@ export const rule = createRule({ processTextNode(node) }, - 'TSIndexSignature :matches(Literal,TemplateLiteral)'( - node: TSESTree.Literal | TSESTree.TemplateLiteral, - ) { - // Handle index signatures - visited.add(node) - }, - // Modify the existing Literal:exit visitor to check for interface contexts 'Literal:exit'(node: TSESTree.Literal) { if (visited.has(node)) return From a4125c7c50a294ca10aac5432505972e9c23671e Mon Sep 17 00:00:00 2001 From: Sebastian Werner Date: Mon, 9 Dec 2024 21:02:53 +0100 Subject: [PATCH 10/24] fix: removed some dead paths --- src/rules/no-unlocalized-strings.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/rules/no-unlocalized-strings.ts b/src/rules/no-unlocalized-strings.ts index c5d5841..eaec7ff 100644 --- a/src/rules/no-unlocalized-strings.ts +++ b/src/rules/no-unlocalized-strings.ts @@ -613,11 +613,6 @@ export const rule = createRule({ return } - // Add check for interface and type contexts - if (isInsideTypeContext(node)) { - return - } - context.report({ node, messageId: 'default' }) }, From f1e3a79a5c83b3546a84e6d8d0a3e46f8d71eaaf Mon Sep 17 00:00:00 2001 From: Sebastian Werner Date: Mon, 9 Dec 2024 21:05:44 +0100 Subject: [PATCH 11/24] fix: cleanups --- src/rules/no-unlocalized-strings.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/rules/no-unlocalized-strings.ts b/src/rules/no-unlocalized-strings.ts index eaec7ff..fe3c57d 100644 --- a/src/rules/no-unlocalized-strings.ts +++ b/src/rules/no-unlocalized-strings.ts @@ -570,7 +570,6 @@ export const rule = createRule({ processTextNode(node) }, - // Modify the existing Literal:exit visitor to check for interface contexts 'Literal:exit'(node: TSESTree.Literal) { if (visited.has(node)) return const trimmed = `${node.value}`.trim() @@ -584,7 +583,7 @@ export const rule = createRule({ } if (isAssignedToIgnoredVariable(node, isIgnoredName)) { - return + return // Do not report this literal } if (isInsideIgnoredProperty(node)) { From 5a3d1f496f09a50804699bfe5f6bbb87ff4fc836 Mon Sep 17 00:00:00 2001 From: Sebastian Werner Date: Tue, 10 Dec 2024 10:44:02 +0100 Subject: [PATCH 12/24] refactor: full rework of the test suite --- .../src/rules/no-unlocalized-strings.test.ts | 778 ++++-------------- 1 file changed, 162 insertions(+), 616 deletions(-) diff --git a/tests/src/rules/no-unlocalized-strings.test.ts b/tests/src/rules/no-unlocalized-strings.test.ts index 58e2495..bea1cbc 100644 --- a/tests/src/rules/no-unlocalized-strings.test.ts +++ b/tests/src/rules/no-unlocalized-strings.test.ts @@ -1,14 +1,10 @@ import { rule, name, Option } from '../../../src/rules/no-unlocalized-strings' import { RuleTester } from '@typescript-eslint/rule-tester' -describe('', () => {}) - const ruleTester = new RuleTester({ languageOptions: { parserOptions: { - ecmaFeatures: { - jsx: true, - }, + ecmaFeatures: { jsx: true }, projectService: { allowDefaultProject: ['*.ts*'], }, @@ -16,754 +12,304 @@ const ruleTester = new RuleTester({ }, }) +const defaultError = [{ messageId: 'default' }] +const jsxTextError = [{ messageId: 'forJsxText' }] const upperCaseRegex = '^[A-Z_-]+$' -const ignoreUpperCaseName = { ignoreNames: [{ regex: { pattern: upperCaseRegex } }] } - -const errors = [{ messageId: 'default' }] // default errors +const ignoreUpperCaseName = { + ignoreNames: [ + { + regex: { pattern: upperCaseRegex }, + }, + ], +} -ruleTester.run(name, rule, { +ruleTester.run(name, rule, { valid: [ + // ==================== Basic i18n Usage ==================== { code: 'i18n._(t`Hello ${nice}`)', + name: 'allows i18n template literals with interpolation', }, - { code: 't(i18n)({ message: `Hello ${name}` })' }, { - name: 'should ignore non word strings', + code: 't(i18n)({ message: `Hello ${name}` })', + name: 'allows i18n function with template message', + }, + { + code: 'i18n._(`hello`)', + name: 'allows i18n with template literal', + }, + { + code: 'i18n._("hello")', + name: 'allows i18n with string literal', + }, + + // ==================== Non-Word Strings ==================== + { code: 'const test = "1111"', + name: 'ignores numeric strings', }, { - name: 'should ignore non word strings 2', code: 'const a = `0123456789!@#$%^&*()_+|~-=\\`[]{};\':",./<>?`;', + name: 'ignores special character strings', }, + + // ==================== Template Literals with Variables ==================== { - name: 'should ignore strings containing only variables', code: 'const t = `${BRAND_NAME}`', + name: 'allows template literal with single variable', }, { - name: 'should ignore strings containing few variables', code: 'const t = `${BRAND_NAME}${BRAND_NAME}`', + name: 'allows template literal with multiple variables', }, { - name: 'should ignore strings containing few variables with spaces', code: 'const t = ` ${BRAND_NAME} ${BRAND_NAME} `', + name: 'allows template literal with variables and spaces', }, + + // ==================== Ignored Functions ==================== { code: 'hello("Hello")', options: [{ ignoreFunctions: ['hello'] }], + name: 'allows whitelisted function calls', }, { code: 'new Error("hello")', options: [{ ignoreFunctions: ['Error'] }], + name: 'allows whitelisted constructor calls', }, { code: 'custom.wrapper()({message: "Hello!"})', options: [{ ignoreFunctions: ['custom.wrapper'] }], + name: 'allows nested whitelisted function calls', }, { - name: 'Should ignore calls using complex object.method expression', - code: 'console.log("Hello")', - options: [{ ignoreFunctions: ['console.log'] }], + code: 'getData().two.three.four("Hello")', + options: [{ ignoreFunctions: ['*.three.four'] }], + name: 'allows whitelisted methods with wildcards', }, + + // ==================== Console Methods ==================== { - name: 'Should ignore method calls using pattern', code: 'console.log("Hello"); console.error("Hello");', options: [{ ignoreFunctions: ['console.*'] }], + name: 'allows console methods with pattern matching', }, { - name: 'Should ignore methods multilevel', code: 'context.headers.set("Hello"); level.context.headers.set("Hello");', options: [{ ignoreFunctions: ['*.headers.set'] }], + name: 'allows multilevel method calls with wildcards', }, - { - name: 'Should ignore methods multilevel 2', - code: 'headers.set("Hello"); level.context.headers.set("Hello");', - options: [{ ignoreFunctions: ['*headers.set'] }], - }, - { - name: 'Should ignore methods with dynamic segment ', - code: 'getData().two.three.four("Hello")', - options: [{ ignoreFunctions: ['*.three.four'] }], - }, - { code: 'name === `Hello brat` || name === `Nice have`' }, - { code: 'switch(a){ case `a`: break; default: break;}' }, - { code: 'i18n._(`hello`);' }, - { code: 'const a = `absfoo`;', options: [{ ignore: ['foo'] }] }, - { code: 'const a = `fooabc`;', options: [{ ignore: ['^foo'] }] }, - { code: "name === 'Hello brat' || name === 'Nice have'" }, - { code: "switch(a){ case 'a': break; default: break;}" }, - { code: 'import name from "hello";' }, - { code: 'export * from "hello_export_all";' }, - { code: 'export { a } from "hello_export";' }, - { code: 'const a = require(["hello"]);', options: [{ ignoreFunctions: ['require'] }] }, - { code: 'const a = require(["hel" + "lo"]);', options: [{ ignoreFunctions: ['require'] }] }, - { code: 'const a = 1;' }, - { code: 'i18n._("hello");' }, - { code: 'const a = "absfoo";', options: [{ ignore: ['foo'] }] }, - { code: 'const a = "fooabc";', options: [{ ignore: ['^foo'] }] }, - // // JSX - { code: '
' }, - { code: '
' }, - { - name: 'Should ignore non-word strings in the JSX Text', - code: `+`, - }, - { - name: 'Should JSX Text if it matches the ignore option', - code: `foo`, - options: [{ ignore: ['^foo'] }], - }, - { code: '
' }, - { code: '
' }, - { code: '
{i18n._("foo")}
' }, - { code: '' }, - { code: '' }, - { code: '' }, - { code: '' }, - { code: '' }, - { code: '' }, - { - code: '', - }, - { - code: '', - }, - { - code: '', - }, - { - code: '', - }, - { - code: '
', - }, - { - code: '
', - }, - { code: '
' }, - { code: '
' }, - { code: '' }, - { code: '' }, - { code: '`, - }, - { code: '
', options: [{ ignoreNames: ['foo'] }] }, - { code: '
', options: [{ ignoreNames: ['foo'] }] }, - { - code: '
', - options: [{ ignoreNames: [{ regex: { pattern: 'className', flags: 'i' } }] }], - }, - { code: '
 
' }, - { code: "plural('Hello')" }, - { code: "select('Hello')" }, - { code: "selectOrdinal('Hello')" }, - { code: 'msg({message: `Hello!`})' }, - { - code: ``, - options: [{ ignoreNames: ['autoComplete'] }], - }, - { - code: 'const Wrapper = styled.a` cursor: pointer; ${(props) => props.isVisible && `visibility: visible;`}`', - }, + // ==================== JSX Attributes ==================== { - code: "const Wrapper = styled.a` cursor: pointer; ${(props) => props.isVisible && 'visibility: visible;'}`", + code: '
', + name: 'allows className attribute', }, { - code: `const test = { myProp: 'This is not localized' }`, - options: [{ ignoreNames: ['myProp'] }], + code: '
', + name: 'allows className with template literal', }, { - code: 'const test = { myProp: `This is not localized` }', - options: [{ ignoreNames: ['myProp'] }], - }, - { - code: `const test = { ['myProp']: 'This is not localized' }`, - options: [{ ignoreNames: ['myProp'] }], - }, - { - code: `const test = { wrapperClassName: 'This is not localized' }`, - options: [{ ignoreNames: [{ regex: { pattern: 'className', flags: 'i' } }] }], - }, - { - code: `MyComponent.displayName = 'MyComponent';`, - options: [{ ignoreNames: ['displayName'] }], - }, - { - code: 'class Form extends Component { displayName = "FormContainer" };', - options: [{ ignoreNames: ['displayName'] }], - }, - { - name: 'Respect the name of the parameter when a default is applied', - code: 'function Input({ intent = "none"}) {}', - options: [{ ignoreNames: ['intent'] }], - }, - { - name: "Should support ignoreNames when applied the 'as const' assertion", - code: 'const Shape = { CIRCLE: "circle" as const };', - options: [{ ignoreNames: [{ regex: { pattern: '^[A-Z0-9_-]+$' } }] }], - }, - { - name: 'Does not report when literal is assigned to an object property named in ignoreNames', - code: 'const x = { variant: "Hello!" }', - options: [{ ignoreNames: ['variant'] }], - }, - { - name: 'Does not report when template literal is assigned to an object property named in ignoreNames', - code: 'const x = { variant: `Hello ${"World"}` }', - options: [{ ignoreNames: ['variant'] }], - }, - { - name: 'Does not report with nullish coalescing inside object property named in ignoreNames', - code: 'const x = { variant: props.variant ?? "body" }', - options: [{ ignoreNames: ['variant'] }], - }, - { - name: 'Does not report with ternary operator inside object property named in ignoreNames', - code: 'const x = { variant: condition ? "yes" : "no" }', - options: [{ ignoreNames: ['variant'] }], - }, - { - name: 'computed keys should be ignored by default, StringLiteral', - code: `obj["key with space"] = 5`, - }, - { - name: 'computed keys should be ignored by default with TplLiteral', - code: `obj[\`key with space\`] = 5`, - }, - { - name: 'Supports default value assignment', - code: 'const variant = input || "body"', - options: [{ ignoreNames: ['variant'] }], - }, - { - name: 'Supports nullish coalescing operator', - code: 'const variant = input ?? "body"', - options: [{ ignoreNames: ['variant'] }], - }, - { - name: 'Supports ternary operator', - code: 'const value = condition ? "yes" : "no"', - options: [{ ignoreNames: ['value'] }], - }, - { - name: 'Supports default value assignment - template literal version', - code: 'const variant = input || `body`', - options: [{ ignoreNames: ['variant'] }], - }, - { - name: 'Supports nullish coalescing operator - template literal version', - code: 'const variant = input ?? `body`', - options: [{ ignoreNames: ['variant'] }], - }, - { - name: 'Supports ternary operator - template literal version', - code: 'const value = condition ? `yes` : `no`', - options: [{ ignoreNames: ['value'] }], - }, - { - name: 'Ignores literals in assignment expression after variable declaration', - code: `let variant; variant = input ?? "body";`, - options: [{ ignoreNames: ['variant'] }], - }, - { - name: 'Ignores literals in assignment expression to variable in ignoreNames', - code: `let variant; variant = "body";`, - options: [{ ignoreNames: ['variant'] }], - }, - { - name: 'Ignores literals assigned to object properties when property name is in ignoreNames', - code: `const obj = {}; obj.variant = "body";`, - options: [{ ignoreNames: ['variant'] }], - }, - { - name: 'Covers Literal in a nullish coalescing acceptable expression', - code: 'const variant = input ?? "Hello!";', - options: [{ ignoreNames: ['variant'] }], - }, - { - name: 'Covers TemplateLiteral in a ternary acceptable expression', - code: 'const variant = condition ? `Hello ${"World"}` : `Fallback`;', - options: [{ ignoreNames: ['variant'] }], - }, - { - code: `const test = "Hello!"`, - options: [{ ignoreNames: ['test'] }], - }, - { - code: `let test = "Hello!"`, - options: [{ ignoreNames: ['test'] }], - }, - { - code: `var test = "Hello!"`, - options: [{ ignoreNames: ['test'] }], - }, - { - code: 'const test = `Hello!`', - options: [{ ignoreNames: ['test'] }], + code: '
', + name: 'allows className with conditional', }, + + // ==================== SVG Elements ==================== { - code: `const wrapperClassName = "Hello!"`, - options: [{ ignoreNames: [{ regex: { pattern: 'className', flags: 'i' } }] }], + code: '', + name: 'allows SVG viewBox attribute', }, { - code: `const A_B = "Bar!"`, - options: [ignoreUpperCaseName], + code: '', + name: 'allows SVG path data', }, { - code: 'const FOO = `Bar!`', - options: [ignoreUpperCaseName], + code: '', + name: 'allows SVG circle attributes', }, - { code: 'var a = {["A_B"]: "hello world"};', options: [ignoreUpperCaseName] }, - { code: 'var a = {[A_B]: "hello world"};', options: [ignoreUpperCaseName] }, - { code: 'var a = {A_B: "hello world"};', options: [ignoreUpperCaseName] }, - { code: 'var a = {[`A_B`]: `hello world`};', options: [ignoreUpperCaseName] }, - { code: 'var a = {[A_B]: `hello world`};', options: [ignoreUpperCaseName] }, - { code: 'var a = {A_B: `hello world`};', options: [ignoreUpperCaseName] }, - { code: '
', filename: 'a.tsx' }, - { code: '
', filename: 'a.tsx' }, - { code: "var a: Element['nodeName']" }, - { code: "var a: Omit" }, - { code: `var a: 'abc' = 'abc'`, skip: true }, - { code: `var a: 'abc' | 'name' | undefined= 'abc'`, skip: true }, - { code: "type T = {name: 'b'} ; var a: T = {name: 'b'}", skip: true }, - { code: "function Button({ t= 'name' }: {t: 'name'}){} ", skip: true }, - { code: "type T = { t?: 'name'| 'abc'}; function Button({t='name'}:T){}", skip: true }, - { - code: `enum StepType { - Address = 'Address' - }`, - }, + // ==================== Translation Components ==================== { - code: `enum StepType { - Address = \`Address\` - }`, + code: 'Hello', + name: 'allows basic Trans component', }, - { - code: `const myMap = new Map(); - myMap.get("string with a spaces") - myMap.has("string with a spaces")`, - options: [{ useTsTypes: true, ignoreMethodsOnTypes: ['Map.get', 'Map.has'] }], + code: 'Hello', + name: 'allows nested Trans component', }, { - code: `interface Foo {get: (key: string) => string}; - (foo as Foo).get("string with a spaces")`, - options: [{ useTsTypes: true, ignoreMethodsOnTypes: ['Foo.get'] }], + code: "", + name: 'allows Plural component', }, { - code: `interface Foo {get: (key: string) => string}; - const foo: Foo; - foo.get("string with a spaces")`, - options: [{ useTsTypes: true, ignoreMethodsOnTypes: ['Foo.get'] }], + code: " " }, - { code: " " }, - ], - invalid: [ - { - code: 'Abc', - errors: [{ messageId: 'forJsxText' }], - }, - { - code: '{"Hello"}', - errors: [{ messageId: 'forJsxText' }], - }, - { - code: '{`Hello`}', - errors: [{ messageId: 'forJsxText' }], - }, - { - code: 'abc', - errors: [{ messageId: 'forJsxText' }], + code: 'class Form extends Component { property = "Something" };', + errors: defaultError, + name: 'detects unlocalized class property', }, + + // ==================== Uppercase Name Violations ==================== { - code: "{'abc'}", - errors: [{ messageId: 'forJsxText' }], + code: 'const lower_case = "Bar!"', + options: [ignoreUpperCaseName], + errors: defaultError, + name: 'detects lowercase name with ignoreUpperCaseName', }, { - code: '{`abc`}', - errors: [{ messageId: 'forJsxText' }], + code: 'const camelCase = "Bar!"', + options: [ignoreUpperCaseName], + errors: defaultError, + name: 'detects camelCase name with ignoreUpperCaseName', }, { - code: '{someVar === 1 ? `Abc` : `Def`}', - errors: [{ messageId: 'default' }, { messageId: 'default' }], + code: 'var obj = {lowercase_key: "hello world"};', + options: [ignoreUpperCaseName], + errors: defaultError, + name: 'detects lowercase property with ignoreUpperCaseName', }, ], }) -/** - * This test is covering the ignore regex proposed in the documentation - * This regex doesn't used directly in the code. - */ +// ==================== Default Ignore Regex Tests ==================== describe('Default ignore regex', () => { const regex = '^(?![A-Z])\\S+$' test.each([ - ['hello', true], - ['helloMyVar', true], - ['package.json', true], - ['./src/**/*.test*', true], - ['camel_case', true], - - // Start from capital letter - ['Hello', false], - // Multiword string (has space) - ['hello world', false], - ['Hello World', false], - ])('validate %s', (str, pass) => { - expect(new RegExp(regex).test(str)).toBe(pass) + ['hello', true, 'allows lowercase word'], + ['helloMyVar', true, 'allows camelCase'], + ['package.json', true, 'allows filenames'], + ['./src/**/*.test*', true, 'allows paths'], + ['camel_case', true, 'allows snake_case'], + ['Hello', false, 'blocks capitalized words'], + ['hello world', false, 'blocks strings with spaces'], + ['Hello World', false, 'blocks title case with spaces'], + ])('%s => %s (%s)', (input, expected, description) => { + expect(new RegExp(regex).test(input)).toBe(expected) }) }) From 61bd58063b7ca76232f2d71b57fb30f9b08edff7 Mon Sep 17 00:00:00 2001 From: Sebastian Werner Date: Tue, 10 Dec 2024 11:01:42 +0100 Subject: [PATCH 13/24] fix: enhance test suite --- .../src/rules/no-unlocalized-strings.test.ts | 149 ++++++++++-------- 1 file changed, 84 insertions(+), 65 deletions(-) diff --git a/tests/src/rules/no-unlocalized-strings.test.ts b/tests/src/rules/no-unlocalized-strings.test.ts index bea1cbc..34b1fab 100644 --- a/tests/src/rules/no-unlocalized-strings.test.ts +++ b/tests/src/rules/no-unlocalized-strings.test.ts @@ -27,271 +27,290 @@ ruleTester.run(name, rule, { valid: [ // ==================== Basic i18n Usage ==================== { - code: 'i18n._(t`Hello ${nice}`)', name: 'allows i18n template literals with interpolation', + code: 'i18n._(t`Hello ${nice}`)', }, { - code: 't(i18n)({ message: `Hello ${name}` })', name: 'allows i18n function with template message', + code: 't(i18n)({ message: `Hello ${name}` })', }, { - code: 'i18n._(`hello`)', name: 'allows i18n with template literal', + code: 'i18n._(`hello`)', }, { - code: 'i18n._("hello")', name: 'allows i18n with string literal', + code: 'i18n._("hello")', }, // ==================== Non-Word Strings ==================== { - code: 'const test = "1111"', name: 'ignores numeric strings', + code: 'const test = "1111"', }, { - code: 'const a = `0123456789!@#$%^&*()_+|~-=\\`[]{};\':",./<>?`;', name: 'ignores special character strings', + code: 'const a = `0123456789!@#$%^&*()_+|~-=\\`[]{};\':",./<>?`;', }, // ==================== Template Literals with Variables ==================== { - code: 'const t = `${BRAND_NAME}`', name: 'allows template literal with single variable', + code: 'const t = `${BRAND_NAME}`', }, { - code: 'const t = `${BRAND_NAME}${BRAND_NAME}`', name: 'allows template literal with multiple variables', + code: 'const t = `${BRAND_NAME}${BRAND_NAME}`', }, { - code: 'const t = ` ${BRAND_NAME} ${BRAND_NAME} `', name: 'allows template literal with variables and spaces', + code: 'const t = ` ${BRAND_NAME} ${BRAND_NAME} `', }, // ==================== Ignored Functions ==================== { + name: 'allows whitelisted function calls', code: 'hello("Hello")', options: [{ ignoreFunctions: ['hello'] }], - name: 'allows whitelisted function calls', }, { + name: 'allows whitelisted constructor calls', code: 'new Error("hello")', options: [{ ignoreFunctions: ['Error'] }], - name: 'allows whitelisted constructor calls', }, { + name: 'allows nested whitelisted function calls', code: 'custom.wrapper()({message: "Hello!"})', options: [{ ignoreFunctions: ['custom.wrapper'] }], - name: 'allows nested whitelisted function calls', }, { + name: 'allows whitelisted methods with wildcards', code: 'getData().two.three.four("Hello")', options: [{ ignoreFunctions: ['*.three.four'] }], - name: 'allows whitelisted methods with wildcards', }, // ==================== Console Methods ==================== { + name: 'allows console methods', code: 'console.log("Hello"); console.error("Hello");', options: [{ ignoreFunctions: ['console.*'] }], - name: 'allows console methods with pattern matching', }, { + name: 'allows multilevel methods', code: 'context.headers.set("Hello"); level.context.headers.set("Hello");', options: [{ ignoreFunctions: ['*.headers.set'] }], - name: 'allows multilevel method calls with wildcards', }, // ==================== JSX Attributes ==================== { - code: '
', name: 'allows className attribute', + code: '
', }, { - code: '
', name: 'allows className with template literal', + code: '
', }, { - code: '
', name: 'allows className with conditional', + code: '
', }, // ==================== SVG Elements ==================== { - code: '', name: 'allows SVG viewBox attribute', + code: '', }, { - code: '', name: 'allows SVG path data', + code: '', }, { - code: '', name: 'allows SVG circle attributes', + code: '', }, // ==================== Translation Components ==================== { - code: 'Hello', name: 'allows basic Trans component', + code: 'Hello', }, { - code: 'Hello', name: 'allows nested Trans component', + code: 'Hello', }, { - code: "", name: 'allows Plural component', + code: "", }, { - code: "", }, // ==================== TypeScript Types ==================== { - code: "interface FieldLabelProps { 'htmlFor': string; }", name: 'allows interface with HTML attributes', + code: "interface FieldLabelProps { 'htmlFor': string; }", }, { - code: "interface FieldInputProps { 'aria-required': boolean; }", name: 'allows interface with ARIA attributes', + code: "interface FieldInputProps { 'aria-required': boolean; }", }, { - code: `type ButtonVariant = 'primary' | 'secondary' | 'tertiary';`, name: 'allows string literal union types', + code: "type ButtonVariant = 'primary' | 'secondary' | 'tertiary';", }, { - code: `enum StepType { Address = 'Address' }`, name: 'allows enum with string values', + code: "enum StepType { Address = 'Address' }", }, - // ==================== Uppercase Variable Names ==================== + // ==================== Acceptable Expressions with Ignored Names ==================== + { + name: 'accepts TemplateLiteral in uppercase', + code: 'const MY_TEMPLATE = `Hello ${name}`', + options: [ignoreUpperCaseName], + }, + { + name: 'accepts LogicalExpression in uppercase', + code: 'const MY_LOGICAL = shouldGreet && "Hello"', + options: [ignoreUpperCaseName], + }, + { + name: 'accepts BinaryExpression in uppercase', + code: 'const MY_BINARY = "Hello" + count', + options: [ignoreUpperCaseName], + }, { - code: 'const A_B = "Bar!"', + name: 'accepts ConditionalExpression in uppercase', + code: 'const MY_CONDITIONAL = isGreeting ? "Hello" : count', options: [ignoreUpperCaseName], - name: 'allows uppercase snake case variable', }, { - code: 'const FOO = `Bar!`', + name: 'accepts UnaryExpression in uppercase', + code: 'const MY_UNARY = !"Hello"', options: [ignoreUpperCaseName], - name: 'allows uppercase variable with template literal', }, { - code: 'var a = {A_B: "hello world"};', + name: 'accepts TSAsExpression in uppercase', + code: 'const MY_AS = ("Hello" as string)', options: [ignoreUpperCaseName], - name: 'allows uppercase property name', }, { - code: 'var a = {["A_B"]: "hello world"};', + name: 'accepts complex expressions in uppercase', + code: 'const MY_COMPLEX = !("Hello" as string) || `World ${name}`', options: [ignoreUpperCaseName], - name: 'allows uppercase computed property', }, { - code: 'var a = {[A_B]: "hello world"};', + name: 'accepts LogicalExpression assignment in uppercase', + code: 'let MY_VAR; MY_VAR = shouldGreet && "Hello";', options: [ignoreUpperCaseName], - name: 'allows uppercase identifier in computed property', }, { - code: 'var a = {[`A_B`]: `hello world`};', + name: 'accepts conditional in uppercase property', + code: 'const obj = { MY_PROP: someCondition ? "Hello" : count };', + options: [ignoreUpperCaseName], + }, + { + name: 'accepts nullish coalescing in uppercase', + code: 'const MY_NULLISH = greeting ?? "Hello"', options: [ignoreUpperCaseName], - name: 'allows uppercase template literal in computed property', }, // ==================== Import/Export ==================== { - code: 'import name from "hello";', name: 'allows string literals in imports', + code: 'import name from "hello";', }, { - code: 'export * from "hello_export_all";', name: 'allows string literals in exports', + code: 'export * from "hello_export_all";', }, ], invalid: [ // ==================== Basic String Violations ==================== { + name: 'detects unlocalized string literal', code: 'const message = "Select tax code"', errors: defaultError, - name: 'detects unlocalized string literal', }, { + name: 'detects unlocalized export', code: 'export const text = "hello string";', errors: defaultError, - name: 'detects unlocalized export', }, { + name: 'detects unlocalized template literal', code: 'const a = `Hello ${nice}`', errors: defaultError, - name: 'detects unlocalized template literal', }, // ==================== JSX Violations ==================== { + name: 'detects unlocalized JSX text with HTML entity', code: '
hello  
', errors: jsxTextError, - name: 'detects unlocalized JSX text with HTML entity', }, { + name: 'detects unlocalized JSX expression', code: '
{"Hello World!"}
', errors: jsxTextError, - name: 'detects unlocalized JSX expression', }, // ==================== Non-Latin Character Support ==================== { + name: 'detects unlocalized Japanese text', code: 'const a = "こんにちは"', errors: defaultError, - name: 'detects unlocalized Japanese text', }, { + name: 'detects unlocalized Cyrillic text', code: 'const a = "Привет"', errors: defaultError, - name: 'detects unlocalized Cyrillic text', }, { + name: 'detects unlocalized Chinese text', code: 'const a = "添加筛选器"', errors: defaultError, - name: 'detects unlocalized Chinese text', }, // ==================== Component Attributes ==================== { + name: 'detects unlocalized alt text', code: 'some image', errors: defaultError, - name: 'detects unlocalized alt text', }, { + name: 'detects unlocalized ARIA label', code: '