Skip to content

Commit 36d40dc

Browse files
committed
fix(no-unlocalized-strings): ignore React directives
Ignore React directives "use client" and "use server" which were incorrectly flagged as unlocalized strings. Supports both file-level directives and "use server" inside async function bodies.
1 parent b464dfd commit 36d40dc

File tree

2 files changed

+32
-16
lines changed

2 files changed

+32
-16
lines changed

src/rules/no-unlocalized-strings.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -393,12 +393,14 @@ ruleTester.run("no-unlocalized-strings", noUnlocalizedStrings, {
393393
{ code: 'import name from "hello"', filename: "test.tsx" },
394394
{ code: 'export * from "hello_export_all"', filename: "test.tsx" },
395395

396-
// JavaScript/React directive prologues
397-
{ code: '"use strict"', filename: "test.tsx" },
396+
// React directives
398397
{ code: '"use client"', filename: "test.tsx" },
399398
{ code: '"use server"', filename: "test.tsx" },
400399
{ code: '"use client"\nimport React from "react"', filename: "test.tsx" },
401400
{ code: '"use server"\nexport async function action() {}', filename: "test.tsx" },
401+
// "use server" inside function body (server actions)
402+
{ code: 'async function myAction() { "use server"; return null }', filename: "test.tsx" },
403+
{ code: 'const action = async () => { "use server"; return null }', filename: "test.tsx" },
402404

403405
// =========================================================================
404406
// Branded types with __linguiIgnore

src/rules/no-unlocalized-strings.ts

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -853,35 +853,49 @@ function isInsideStylingConstant(node: TSESTree.Node): boolean {
853853
// Syntax Context Checks (non-user-facing locations)
854854
// ============================================================================
855855

856-
/** Known JavaScript/React directive prologues */
857-
const DIRECTIVE_PROLOGUES = new Set(["use strict", "use client", "use server"])
856+
/** React directive strings */
857+
const REACT_DIRECTIVES = new Set(["use client", "use server"])
858858

859859
/**
860-
* Checks if a string literal is a JavaScript directive prologue.
860+
* Checks if a string literal is a React directive.
861861
*
862-
* Directive prologues are special string literals at the start of a script/module:
863-
* - "use strict" - JavaScript strict mode
864-
* - "use client" - React Server Components client boundary
865-
* - "use server" - React Server Components server actions
862+
* React directives are special string literals:
863+
* - "use client" - marks a client component boundary (file level)
864+
* - "use server" - marks server actions (file level or inside async functions)
866865
*/
867-
function isDirectivePrologue(node: TSESTree.Node): boolean {
866+
function isReactDirective(node: TSESTree.Node): boolean {
868867
if (node.type !== AST_NODE_TYPES.Literal || typeof node.value !== "string") {
869868
return false
870869
}
871870

872-
// Check if this is a known directive
873-
if (!DIRECTIVE_PROLOGUES.has(node.value)) {
871+
if (!REACT_DIRECTIVES.has(node.value)) {
874872
return false
875873
}
876874

877-
// Directive prologues must be expression statements at the program level
875+
// Must be wrapped in an expression statement
878876
const parent = node.parent
879877
if (parent.type !== AST_NODE_TYPES.ExpressionStatement) {
880878
return false
881879
}
882880

883881
const grandparent = parent.parent
884-
return grandparent.type === AST_NODE_TYPES.Program
882+
883+
// File-level directive
884+
if (grandparent.type === AST_NODE_TYPES.Program) {
885+
return true
886+
}
887+
888+
// Function-level directive (e.g., "use server" inside async function)
889+
if (grandparent.type === AST_NODE_TYPES.BlockStatement) {
890+
const functionParent = grandparent.parent
891+
return (
892+
functionParent.type === AST_NODE_TYPES.FunctionDeclaration ||
893+
functionParent.type === AST_NODE_TYPES.FunctionExpression ||
894+
functionParent.type === AST_NODE_TYPES.ArrowFunctionExpression
895+
)
896+
}
897+
898+
return false
885899
}
886900

887901
/**
@@ -1387,8 +1401,8 @@ export const noUnlocalizedStrings = createRule<[Options], MessageId>({
13871401
return
13881402
}
13891403

1390-
// JavaScript/React directive prologues: "use strict", "use client", "use server"
1391-
if (isDirectivePrologue(node)) {
1404+
// React directives: "use client", "use server"
1405+
if (isReactDirective(node)) {
13921406
return
13931407
}
13941408

0 commit comments

Comments
 (0)