diff --git a/packages/kilo-vscode/tests/unit/agent-manager-i18n.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-i18n.test.ts index 8edb485de35..691b8a7ab67 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-i18n.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-i18n.test.ts @@ -222,6 +222,105 @@ function findViolations(): Violation[] { return violations } +/** + * Detects arrow-function parameters that shadow the `t` i18n function, and + * JSX expressions that render `{t}` (the translation function itself) instead + * of the intended loop variable. + * + * Catches bugs like: + * .map((token) => {t}) — renders the i18n function, not `token` + * .map((t) => {t}) — shadows outer `t`, works by accident + */ +interface ShadowViolation { + file: string + line: number + text: string + context: string +} + +function findTranslationShadowViolations(): ShadowViolation[] { + const project = new Project({ + compilerOptions: { + jsx: 4, // JsxEmit.Preserve + jsxImportSource: "solid-js", + allowJs: true, + strict: false, + noEmit: true, + }, + skipAddingFilesFromTsConfig: true, + }) + + const results: ShadowViolation[] = [] + + for (const filePath of TSX_FILES) { + const source = project.addSourceFileAtPath(filePath) + const basename = path.basename(filePath) + + // Find arrow functions whose parameter list includes `t` AND whose body + // contains JSX — this shadows the outer `t` from useLanguage() in a + // rendering context, which is almost certainly a bug. + // Non-JSX callbacks (e.g. `.map((t) => t.id)`) are harmless data transforms. + source.getDescendantsOfKind(SyntaxKind.ArrowFunction).forEach((arrow) => { + for (const param of arrow.getParameters()) { + if (param.getName() === "t") { + const parent = arrow.getParent() + if (!parent) continue + // Allow top-level destructuring like `const { t } = useLanguage()` + if (Node.isVariableDeclaration(parent)) continue + // Only flag if the arrow body contains JSX elements + const hasJsx = + arrow.getDescendantsOfKind(SyntaxKind.JsxElement).length > 0 || + arrow.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement).length > 0 + if (!hasJsx) continue + results.push({ + file: basename, + line: param.getStartLineNumber(), + text: param.getText(), + context: "arrow parameter shadows i18n t() function in JSX context", + }) + } + } + }) + + // Find .map() calls where the callback has a parameter but JSX inside + // references `{t}` instead of the parameter name + source.getDescendantsOfKind(SyntaxKind.CallExpression).forEach((call) => { + const expr = call.getExpression() + if (!Node.isPropertyAccessExpression(expr)) return + if (expr.getName() !== "map") return + + const args = call.getArguments() + if (args.length === 0) return + const callback = args[0] + if (!Node.isArrowFunction(callback)) return + + const params = callback.getParameters() + if (params.length === 0) return + const paramName = params[0].getName() + + // If the param is already `t`, it's caught by the shadow check above. + // Here we check: param is NOT `t`, but JSX inside uses `{t}` — meaning + // the callback renders the i18n function instead of the loop variable. + if (paramName === "t") return + + callback.getDescendantsOfKind(SyntaxKind.JsxExpression).forEach((jsx) => { + const inner = jsx.getExpression() + if (!inner) return + if (Node.isIdentifier(inner) && inner.getText() === "t") { + results.push({ + file: basename, + line: jsx.getStartLineNumber(), + text: `{t} inside .map((${paramName}) => ...) — should be {${paramName}}`, + context: "JSX renders i18n function instead of map callback parameter", + }) + } + }) + }) + } + + return results +} + describe("Agent Manager i18n — no hardcoded strings", () => { const violations = findViolations() @@ -233,3 +332,15 @@ describe("Agent Manager i18n — no hardcoded strings", () => { expect(violations).toEqual([]) }) }) + +describe("Agent Manager i18n — no t() shadowing", () => { + const shadows = findTranslationShadowViolations() + + it("should not shadow the t() translation function in callbacks", () => { + if (shadows.length > 0) { + const report = shadows.map((v) => ` ${v.file}:${v.line} [${v.context}] ${v.text}`).join("\n") + expect(shadows, `Found ${shadows.length} t() shadow/misuse(s):\n${report}`).toEqual([]) + } + expect(shadows).toEqual([]) + }) +}) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index fd94ba96a83..440db6d1632 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -1100,7 +1100,7 @@ const AgentManagerContent: Component = () => { {t("agentManager.worktree.new")} {parseBindingTokens(kb().newWorktree ?? "").map((token) => ( - {t} + {token} ))} @@ -1109,8 +1109,8 @@ const AgentManagerContent: Component = () => { {t("agentManager.dialog.advanced")} - {parseBindingTokens(kb().advancedWorktree ?? "").map((t) => ( - {t} + {parseBindingTokens(kb().advancedWorktree ?? "").map((token) => ( + {token} ))}