Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions apps/desktop/electron.vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ export default defineConfig({
'@ai-sdk/openai-compatible',
'@ai-sdk/provider',
'@ai-sdk/provider-utils',
'@ai-sdk/mcp',
'@openrouter/ai-sdk-provider',
'@modelcontextprotocol/sdk',
'@tavily/core',
]
})],
build: {
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop/src/main/agent/agent-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { createOrGetWorktree } from '../ai/worktree';
import { findTaskWorktree } from '../worktree-paths';
import { readSettingsFile } from '../settings-utils';
import type { ProviderAccount } from '../../shared/types/provider-account';
import { getAppLanguage } from '../app-language';
import { tryLoadPrompt } from '../ai/prompts/prompt-loader';

/**
Expand Down Expand Up @@ -395,6 +396,7 @@ export class AgentManager extends EventEmitter {
baseURL: resolved.auth?.baseURL,
configDir: resolved.configDir,
oauthTokenFilePath: resolved.auth?.oauthTokenFilePath,
language: getAppLanguage(),
mcpOptions: {
context7Enabled: true,
memoryEnabled: !!process.env.GRAPHITI_MCP_URL,
Comment on lines +399 to 402

This comment was marked as outdated.

Expand Down Expand Up @@ -519,6 +521,7 @@ export class AgentManager extends EventEmitter {
baseURL: resolved.auth?.baseURL,
configDir: resolved.configDir,
oauthTokenFilePath: resolved.auth?.oauthTokenFilePath,
language: getAppLanguage(),
mcpOptions: {
context7Enabled: true,
memoryEnabled: !!process.env.GRAPHITI_MCP_URL,
Expand Down Expand Up @@ -627,6 +630,7 @@ export class AgentManager extends EventEmitter {
memoryEnabled: !!process.env.GRAPHITI_MCP_URL,
linearEnabled: !!process.env.LINEAR_API_KEY,
},
language: getAppLanguage(),
toolContext: {
cwd: effectiveCwd,
projectDir: effectiveProjectDir,
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/main/agent/agent-queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,7 @@ export class AgentQueueManager {
refresh,
enableCompetitorAnalysis,
abortSignal: abortController.signal,
language: config?.language,
},
(event: RoadmapStreamEvent) => {
switch (event.type) {
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/main/agent/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export interface AgentManagerEvents {
export interface RoadmapConfig {
model?: string; // Model shorthand (opus, sonnet, haiku)
thinkingLevel?: string; // Thinking level (low, medium, high)
language?: string; // Language code for generated content (e.g., 'en', 'pt-BR')
}

export interface TaskExecutionOptions {
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/main/ai/agent/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ export interface SerializableSessionConfig {
configDir?: string;
/** Pre-resolved path to OAuth token file for file-based OAuth providers (e.g., Codex). Worker-safe. */
oauthTokenFilePath?: string;
/** Language code for generated content (e.g., 'en', 'pt-BR') */
language?: string;
/** MCP options resolved from project settings (serialized for worker) */
mcpOptions?: {
context7Enabled?: boolean;
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/main/ai/agent/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ async function assemblePrompt(
specDir: session.specDir,
projectDir: session.projectDir,
projectInstructions: cachedProjectInstructions,
language: session.language,
});
}

Expand Down
9 changes: 8 additions & 1 deletion apps/desktop/src/main/ai/prompts/prompt-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,14 @@ export function injectContext(promptTemplate: string, context: PromptContext): s
);
}

// 5. Base prompt
// 5. Language instruction
if (context.language && context.language !== 'en') {
sections.push(
`## OUTPUT LANGUAGE\n\nYou MUST write ALL human-readable text content in **${context.language}**. This includes: titles, descriptions, rationale, acceptance criteria, user stories, phase names, status messages, and any text shown to the user. Keep JSON keys, code, file paths, technical identifiers, and enum values in English.\n\n---\n\n`
);
Comment on lines +248 to +250
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

For better readability and maintainability, you could use a single multi-line template literal here instead of concatenating multiple strings with +.

    sections.push(`## OUTPUT LANGUAGE

You MUST write ALL human-readable text content in **${context.language}**. This includes: titles, descriptions, rationale, acceptance criteria, user stories, phase names, status messages, and any text shown to the user. Keep JSON keys, code, file paths, technical identifiers, and enum values in English.

---

`);

}

// 6. Base prompt
sections.push(promptTemplate);

return sections.join('');
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/main/ai/prompts/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export interface PromptContext {
recoveryHints?: string[];
/** Phase-specific planning retry context */
planningRetryContext?: string;
/** Language code for generated content (e.g., 'en', 'pt-BR') */
language?: string;
}

// =============================================================================
Expand Down
35 changes: 25 additions & 10 deletions apps/desktop/src/main/ai/runners/roadmap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
*/

import { streamText, stepCountIs } from 'ai';
import { randomUUID } from 'node:crypto';
import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from 'node:fs';
import { join } from 'node:path';

import { withFileLock } from '../../utils/file-lock';
import { createSimpleClient } from '../client/factory';
import type { SimpleClientResult } from '../client/types';
import { buildToolRegistry } from '../tools/build-registry';
Expand Down Expand Up @@ -51,6 +53,8 @@ export interface RoadmapConfig {
enableCompetitorAnalysis?: boolean;
/** Abort signal for cancellation */
abortSignal?: AbortSignal;
/** Language code for generated content (e.g., 'en', 'pt-BR') */
language?: string;
}

/** Result of a roadmap phase */
Expand Down Expand Up @@ -103,6 +107,7 @@ async function runDiscoveryPhase(
client: SimpleClientResult,
abortSignal?: AbortSignal,
onStream?: RoadmapStreamCallback,
language?: string,
): Promise<RoadmapPhaseResult> {
const discoveryFile = join(outputDir, 'roadmap_discovery.json');
const projectIndexFile = join(outputDir, 'project_index.json');
Expand All @@ -121,7 +126,10 @@ async function runDiscoveryPhase(
const loadedDiscoveryPrompt = tryLoadPrompt('roadmap_discovery');

for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
const contextBlock = `\n\n---\n\n## CONTEXT (injected by runner)\n\n**Project Directory**: ${projectDir}\n**Project Index**: ${projectIndexFile}\n**Output Directory**: ${outputDir}\n**Output File**: ${discoveryFile}\n\nUse the paths above when reading input files and writing output.`;
const languageInstruction = language && language !== 'en'
? `\n\n**LANGUAGE**: You MUST write ALL human-readable text content in ${language}. This includes: project descriptions, pain points, goals, vision statements, value propositions, success metrics, feature names, known gaps, differentiators, market position, and constraints. Keep JSON keys, technical terms (framework names, language names), and file paths in English.`
: '';
const contextBlock = `\n\n---\n\n## CONTEXT (injected by runner)\n\n**Project Directory**: ${projectDir}\n**Project Index**: ${projectIndexFile}\n**Output Directory**: ${outputDir}\n**Output File**: ${discoveryFile}\n\nUse the paths above when reading input files and writing output.${languageInstruction}`;
Comment on lines +129 to +132
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider extracting duplicated language instruction string.

The language instruction text is nearly identical between discovery and features phases. While the enum value lists differ slightly, the core pattern is duplicated. Consider extracting to a helper function for maintainability.

♻️ Suggested refactor
function buildLanguageInstruction(language: string | undefined, enumExamples: string): string {
  if (!language || language === 'en') return '';
  return `\n\n**LANGUAGE**: You MUST write ALL human-readable text content in ${language}. This includes: ${enumExamples}. Keep JSON keys, technical terms (framework names, language names), file paths, and enum values in English.`;
}

// Usage in discovery:
const languageInstruction = buildLanguageInstruction(
  language,
  'project descriptions, pain points, goals, vision statements, value propositions, success metrics, feature names, known gaps, differentiators, market position, and constraints'
);

// Usage in features:
const featuresLanguageInstruction = buildLanguageInstruction(
  language,
  'feature titles, descriptions, rationales, acceptance criteria, user stories, phase names, phase descriptions, milestone titles, milestone descriptions, vision statement, and audience descriptions'
);

Also applies to: 265-268

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/ai/runners/roadmap.ts` around lines 129 - 132,
Duplicate multilingual instruction strings (languageInstruction used in the
discovery phase and the similar features-phase text) should be extracted into a
single helper function to avoid duplication and ease maintenance; add a function
(e.g., buildLanguageInstruction(language: string | undefined, enumExamples:
string): string) that returns '' for undefined/'en' and otherwise composes the
LANGUAGE block with the provided enumExamples, then replace the inline
constructions used to set languageInstruction and the features-phase equivalent
with calls to buildLanguageInstruction (pass the appropriate enumExamples for
discovery vs features) so callers like the discovery contextBlock and the
features context block reuse the single helper.


const prompt = loadedDiscoveryPrompt
? loadedDiscoveryPrompt + contextBlock
Expand All @@ -140,7 +148,7 @@ Your task:

The JSON must contain at minimum: project_name, target_audience, product_vision, key_features, technical_stack, and constraints.

Do NOT ask questions. Make educated inferences and create the file.`;
Do NOT ask questions. Make educated inferences and create the file.${languageInstruction}`;

const discoveryUserPrompt = 'Analyze the project and create the discovery document. Use the available tools to explore the codebase, then write your findings as JSON to the output file specified in the context above.';

Expand Down Expand Up @@ -217,6 +225,7 @@ async function runFeaturesPhase(
client: SimpleClientResult,
abortSignal?: AbortSignal,
onStream?: RoadmapStreamCallback,
language?: string,
): Promise<RoadmapPhaseResult> {
const roadmapFile = join(outputDir, 'roadmap.json');
const discoveryFile = join(outputDir, 'roadmap_discovery.json');
Expand Down Expand Up @@ -253,7 +262,10 @@ The following ${preservedFeatures.length} features already exist and will be pre
Generate NEW features that complement these, do not duplicate them:
${preservedInfo}\n`;
}
const featuresContextBlock = `\n\n---\n\n## CONTEXT (injected by runner)\n\n**Discovery File**: ${discoveryFile}\n**Project Index**: ${projectIndexFile}\n**Output File**: ${roadmapFile}\n${preservedSection}\nUse the paths above when reading input files and writing output. Write the complete roadmap JSON to the Output File path.`;
const featuresLanguageInstruction = language && language !== 'en'
? `\n\n**LANGUAGE**: You MUST write ALL human-readable text content in ${language}. This includes: feature titles, descriptions, rationales, acceptance criteria, user stories, phase names, phase descriptions, milestone titles, milestone descriptions, vision statement, and audience descriptions. Keep JSON keys, technical terms, file paths, and enum values (must/should/could/wont, low/medium/high, idea/planned) in English.`
: '';
const featuresContextBlock = `\n\n---\n\n## CONTEXT (injected by runner)\n\n**Discovery File**: ${discoveryFile}\n**Project Index**: ${projectIndexFile}\n**Output File**: ${roadmapFile}\n${preservedSection}\nUse the paths above when reading input files and writing output. Write the complete roadmap JSON to the Output File path.${featuresLanguageInstruction}`;

const prompt = loadedFeaturesPrompt
? loadedFeaturesPrompt + featuresContextBlock
Expand All @@ -272,7 +284,7 @@ Based on the discovery data:
6. Map dependencies

Output the complete roadmap as valid JSON to ${roadmapFile}.
The JSON must contain: vision, target_audience (object with "primary" key), phases (array), and features (array with at least 3 items each with id, title, description, priority, complexity, impact, phase_id, status, acceptance_criteria, and user_stories).`;
The JSON must contain: vision, target_audience (object with "primary" key), phases (array), and features (array with at least 3 items each with id, title, description, priority, complexity, impact, phase_id, status, acceptance_criteria, and user_stories).${featuresLanguageInstruction}`;

const featuresUserPrompt = 'Read the discovery data and generate a complete roadmap with prioritized features. Write the roadmap JSON to the output file specified in the context above.';

Expand Down Expand Up @@ -330,13 +342,15 @@ The JSON must contain: vision, target_audience (object with "primary" key), phas
}

if (missing.length === 0 && featureCount >= 3) {
// Merge preserved features — atomic write via temp file + rename
// Merge preserved features — atomic write with file lock to prevent races
if (preservedFeatures.length > 0) {
data.features = mergeFeatures(data.features as Record<string, unknown>[], preservedFeatures);
const merged = JSON.stringify(data, null, 2);
const tmpFile = `${roadmapFile}.tmp.${process.pid}`;
writeFileSync(tmpFile, merged, 'utf-8');
renameSync(tmpFile, roadmapFile);
await withFileLock(roadmapFile, async () => {
const tmpFile = `${roadmapFile}.tmp.${process.pid}.${randomUUID()}`;
writeFileSync(tmpFile, merged, 'utf-8');
renameSync(tmpFile, roadmapFile);
});
}
return { phase: 'features', success: true, outputs: [roadmapFile], errors: [] };
}
Expand Down Expand Up @@ -441,6 +455,7 @@ export async function runRoadmapGeneration(
thinkingLevel = 'medium',
refresh = false,
abortSignal,
language = 'en',
} = config;

const outputDir = config.outputDir ?? join(projectDir, '.auto-claude', 'roadmap');
Expand Down Expand Up @@ -475,7 +490,7 @@ export async function runRoadmapGeneration(
// Phase 1: Discovery
onStream?.({ type: 'phase-start', phase: 'discovery' });
const discoveryResult = await runDiscoveryPhase(
projectDir, outputDir, refresh, client, abortSignal, onStream,
projectDir, outputDir, refresh, client, abortSignal, onStream, language,
);
phases.push(discoveryResult);
onStream?.({ type: 'phase-complete', phase: 'discovery', success: discoveryResult.success });
Expand All @@ -491,7 +506,7 @@ export async function runRoadmapGeneration(
// Phase 2: Feature Generation
onStream?.({ type: 'phase-start', phase: 'features' });
const featuresResult = await runFeaturesPhase(
projectDir, outputDir, refresh, client, abortSignal, onStream,
projectDir, outputDir, refresh, client, abortSignal, onStream, language,
);
phases.push(featuresResult);
onStream?.({ type: 'phase-complete', phase: 'features', success: featuresResult.success });
Expand Down
3 changes: 3 additions & 0 deletions apps/desktop/src/main/ipc-handlers/roadmap-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { safeSendToRenderer } from "./utils";
import { writeFileWithRetry, readFileWithRetry } from "../utils/atomic-file";
import { withFileLock } from "../utils/file-lock";
import { getActiveProviderFeatureSettings } from "./feature-settings-helper";
import { getAppLanguage } from "../app-language";

/**
* Read roadmap feature settings using per-provider resolution
Expand Down Expand Up @@ -217,6 +218,7 @@ export function registerRoadmapHandlers(
const config: RoadmapConfig = {
model: featureSettings.model,
thinkingLevel: featureSettings.thinkingLevel,
language: getAppLanguage(),
};

debugLog("[Roadmap Handler] Generate request:", {
Expand Down Expand Up @@ -279,6 +281,7 @@ export function registerRoadmapHandlers(
const config: RoadmapConfig = {
model: featureSettings.model,
thinkingLevel: featureSettings.thinkingLevel,
language: getAppLanguage(),
};

debugLog("[Roadmap Handler] Refresh request:", {
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/renderer/components/AddFeatureDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export function AddFeatureDialog({
onFeatureAdded,
defaultPhaseId
}: AddFeatureDialogProps) {
const { t } = useTranslation('dialogs');
const { t } = useTranslation(['dialogs', 'common']);

// Form state
const [title, setTitle] = useState('');
Expand Down Expand Up @@ -283,7 +283,7 @@ export function AddFeatureDialog({
<SelectContent>
{Object.entries(ROADMAP_PRIORITY_LABELS).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
{t(`common:${label}`)}
</SelectItem>
))}
</SelectContent>
Expand Down
13 changes: 8 additions & 5 deletions apps/desktop/src/renderer/components/RoadmapKanbanView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
sortableKeyboardCoordinates,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { useTranslation } from 'react-i18next';
import { Plus, Inbox, Eye, Calendar, Play, Check } from 'lucide-react';
import { ScrollArea } from './ui/scroll-area';
import { Badge } from './ui/badge';
Expand Down Expand Up @@ -76,6 +77,7 @@ function DroppableStatusColumn({
onArchive,
isOver
}: DroppableStatusColumnProps) {
const { t } = useTranslation('common');
const { setNodeRef } = useDroppable({
id: column.id
});
Expand Down Expand Up @@ -110,7 +112,7 @@ function DroppableStatusColumn({
{getStatusIcon(column.icon)}
</div>
<h2 className="font-semibold text-sm text-foreground">
{column.label}
{t(column.label)}
</h2>
<span className="column-count-badge">
{features.length}
Expand Down Expand Up @@ -138,16 +140,16 @@ function DroppableStatusColumn({
<div className="h-8 w-8 rounded-full bg-primary/20 flex items-center justify-center mb-2">
<Plus className="h-4 w-4 text-primary" />
</div>
<span className="text-sm font-medium text-primary">Drop here</span>
<span className="text-sm font-medium text-primary">{t('roadmap.kanban.dropHere')}</span>
</>
) : (
<>
<Inbox className="h-6 w-6 text-muted-foreground/50" />
<span className="mt-2 text-sm font-medium text-muted-foreground/70">
No features
{t('roadmap.kanban.noFeatures')}
</span>
<span className="mt-0.5 text-xs text-muted-foreground/50">
Drag features here
{t('roadmap.kanban.dragFeaturesHere')}
</span>
</>
)}
Expand Down Expand Up @@ -181,6 +183,7 @@ export function RoadmapKanbanView({
onSave,
onArchive
}: RoadmapKanbanViewProps) {
const { t } = useTranslation('common');
const [activeFeature, setActiveFeature] = useState<RoadmapFeature | null>(null);
const [overColumnId, setOverColumnId] = useState<string | null>(null);

Expand Down Expand Up @@ -282,7 +285,7 @@ export function RoadmapKanbanView({
// Get status label for a feature (for display in drag overlay)
const getStatusLabelForFeature = (feature: RoadmapFeature) => {
const statusColumn = ROADMAP_STATUS_COLUMNS.find((c) => c.id === feature.status);
return statusColumn?.label || 'Unknown Status';
return statusColumn ? t(statusColumn.label) : t('roadmap.unknownStatus');
};

return (
Expand Down
16 changes: 8 additions & 8 deletions apps/desktop/src/renderer/components/SortableFeatureCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export function SortableFeatureCard({
variant="outline"
className={cn('text-[10px] px-1.5 py-0', ROADMAP_PRIORITY_COLORS[feature.priority])}
>
{ROADMAP_PRIORITY_LABELS[feature.priority]}
{t(ROADMAP_PRIORITY_LABELS[feature.priority])}
</Badge>
{phaseName && (
<Tooltip>
Expand All @@ -102,7 +102,7 @@ export function SortableFeatureCard({
</Badge>
</TooltipTrigger>
<TooltipContent>
Phase: {phaseName}
{t('roadmap.phaseLabel', { phase: phaseName })}
</TooltipContent>
</Tooltip>
)}
Expand All @@ -117,7 +117,7 @@ export function SortableFeatureCard({
</Badge>
</TooltipTrigger>
<TooltipContent>
This feature addresses competitor pain points
{t('roadmap.competitorInsight.tooltip')}
</TooltipContent>
</Tooltip>
)}
Expand Down Expand Up @@ -191,13 +191,13 @@ export function SortableFeatureCard({
variant="outline"
className={cn('text-[10px] px-1.5 py-0', ROADMAP_COMPLEXITY_COLORS[feature.complexity])}
>
{feature.complexity}
{t(`roadmap.complexity.${feature.complexity}`)}
</Badge>
<Badge
variant="outline"
className={cn('text-[10px] px-1.5 py-0', ROADMAP_IMPACT_COLORS[feature.impact])}
>
{feature.impact}
{t(`roadmap.impact.${feature.impact}`)}
</Badge>
Comment on lines 196 to 201
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Impact badge lost context and can be confused with complexity.

Rendering only roadmap.impact.<level> makes the badge ambiguous (e.g., two “Low” badges). Use the label template key for clearer output.

🎯 Suggested patch
-            {t(`roadmap.impact.${feature.impact}`)}
+            {t('roadmap.impact.label', { impact: t(`roadmap.impact.${feature.impact}`) })}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Badge
variant="outline"
className={cn('text-[10px] px-1.5 py-0', ROADMAP_IMPACT_COLORS[feature.impact])}
>
{feature.impact}
{t(`roadmap.impact.${feature.impact}`)}
</Badge>
<Badge
variant="outline"
className={cn('text-[10px] px-1.5 py-0', ROADMAP_IMPACT_COLORS[feature.impact])}
>
{t('roadmap.impact.label', { impact: t(`roadmap.impact.${feature.impact}`) })}
</Badge>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/components/SortableFeatureCard.tsx` around lines
196 - 201, The badge text is ambiguous because it renders only
t(`roadmap.impact.${feature.impact}`); update the Badge content to use the label
template key so it provides context (e.g., use the "label" key under
roadmap.impact with the impact level injected or a dedicated label key like
roadmap.impact.label.<level>), referencing the Badge component,
ROADMAP_IMPACT_COLORS, feature.impact and the t() call in
SortableFeatureCard.tsx to locate the change.

{/* Show vote count if from external source */}
{feature.votes !== undefined && feature.votes > 0 && (
Expand All @@ -212,7 +212,7 @@ export function SortableFeatureCard({
</Badge>
</TooltipTrigger>
<TooltipContent>
{feature.votes} votes from user feedback
{t('roadmap.votesFromFeedback', { count: feature.votes })}
</TooltipContent>
</Tooltip>
)}
Expand All @@ -224,11 +224,11 @@ export function SortableFeatureCard({
variant="outline"
className="text-[10px] px-1.5 py-0 text-orange-500 border-orange-500/30"
>
{feature.source?.provider === 'canny' ? 'Canny' : 'External'}
{feature.source?.provider === 'canny' ? 'Canny' : t('roadmap.externalSource')}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Hardcoded brand name 'Canny' bypasses localization.

While 'Canny' is a proper noun (company name), the conditional logic shows inconsistent treatment—other external sources use t('roadmap.externalSource') but Canny gets a hardcoded string. Consider using a translation key like t('roadmap.providers.canny') for consistency, even if the value is the same across locales.

♻️ Suggested fix
-                  {feature.source?.provider === 'canny' ? 'Canny' : t('roadmap.externalSource')}
+                  {t(`roadmap.providers.${feature.source?.provider}`, { defaultValue: t('roadmap.externalSource') })}

Then add to locale files:

"roadmap": {
  "providers": {
    "canny": "Canny"
  }
}

As per coding guidelines: "All frontend user-facing text must use react-i18next translation keys."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/components/SortableFeatureCard.tsx` at line 227,
The hardcoded provider label in SortableFeatureCard.tsx bypasses i18n; replace
the conditional that currently renders {feature.source?.provider === 'canny' ?
'Canny' : t('roadmap.externalSource')} with a translation lookup (e.g., use
t('roadmap.providers.canny') when feature.source?.provider === 'canny') and
fallback to t('roadmap.externalSource') for others, and add the corresponding
"roadmap.providers.canny" key to the locale files so the provider name is
localized consistently.

</Badge>
</TooltipTrigger>
<TooltipContent>
Imported from {feature.source?.provider}
{t('roadmap.importedFrom', { provider: feature.source?.provider })}
</TooltipContent>
</Tooltip>
)}
Expand Down
Loading
Loading