Browse Source

Merge pull request #192 from RooVetGit/language_chooser

Add a preferred language dropdown
Matt Rubens 1 year ago
parent
commit
3248c9b1b7

+ 5 - 0
.changeset/cuddly-vans-judge.md

@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Add a preferred language dropdown

+ 162 - 1
.clinerules

@@ -1 +1,162 @@
-- Before attempting completion, always make sure that any code changes have test coverage and that the tests pass.
+# Code Quality Rules
+
+1. Test Coverage:
+   - Before attempting completion, always make sure that any code changes have test coverage
+   - Ensure all tests pass before submitting changes
+
+2. Git Commits:
+   - When finishing a task, always output a git commit command
+   - Include a descriptive commit message that follows conventional commit format
+
+3. Documentation:
+   - Update README.md when making significant changes, such as:
+     * Adding new features or settings
+     * Changing existing functionality
+     * Updating system requirements
+     * Adding new dependencies
+   - Include clear descriptions of new features and how to use them
+   - Keep the documentation in sync with the codebase
+   - Add examples where appropriate
+
+# Adding a New Setting
+
+To add a new setting that persists its state, follow these steps:
+
+## For All Settings
+
+1. Add the setting to ExtensionMessage.ts:
+   - Add the setting to the ExtensionState interface
+   - Make it required if it has a default value, optional if it can be undefined
+   - Example: `preferredLanguage: string`
+
+2. Add test coverage:
+   - Add the setting to mockState in ClineProvider.test.ts
+   - Add test cases for setting persistence and state updates
+   - Ensure all tests pass before submitting changes
+
+## For Checkbox Settings
+
+1. Add the message type to WebviewMessage.ts:
+   - Add the setting name to the WebviewMessage type's type union
+   - Example: `| "multisearchDiffEnabled"`
+
+2. Add the setting to ExtensionStateContext.tsx:
+   - Add the setting to the ExtensionStateContextType interface
+   - Add the setter function to the interface
+   - Add the setting to the initial state in useState
+   - Add the setting to the contextValue object
+   - Example:
+     ```typescript
+     interface ExtensionStateContextType {
+       multisearchDiffEnabled: boolean;
+       setMultisearchDiffEnabled: (value: boolean) => void;
+     }
+     ```
+
+3. Add the setting to ClineProvider.ts:
+   - Add the setting name to the GlobalStateKey type union
+   - Add the setting to the Promise.all array in getState
+   - Add the setting to the return value in getState with a default value
+   - Add the setting to the destructured variables in getStateToPostToWebview
+   - Add the setting to the return value in getStateToPostToWebview
+   - Add a case in setWebviewMessageListener to handle the setting's message type
+   - Example:
+     ```typescript
+     case "multisearchDiffEnabled":
+       await this.updateGlobalState("multisearchDiffEnabled", message.bool)
+       await this.postStateToWebview()
+       break
+     ```
+
+4. Add the checkbox UI to SettingsView.tsx:
+   - Import the setting and its setter from ExtensionStateContext
+   - Add the VSCodeCheckbox component with the setting's state and onChange handler
+   - Add appropriate labels and description text
+   - Example:
+     ```typescript
+     <VSCodeCheckbox 
+       checked={multisearchDiffEnabled} 
+       onChange={(e: any) => setMultisearchDiffEnabled(e.target.checked)}
+     >
+       <span style={{ fontWeight: "500" }}>Enable multi-search diff matching</span>
+     </VSCodeCheckbox>
+     ```
+
+5. Add the setting to handleSubmit in SettingsView.tsx:
+   - Add a vscode.postMessage call to send the setting's value when clicking Done
+   - Example:
+     ```typescript
+     vscode.postMessage({ type: "multisearchDiffEnabled", bool: multisearchDiffEnabled })
+     ```
+
+## For Select/Dropdown Settings
+
+1. Add the message type to WebviewMessage.ts:
+   - Add the setting name to the WebviewMessage type's type union
+   - Example: `| "preferredLanguage"`
+
+2. Add the setting to ExtensionStateContext.tsx:
+   - Add the setting to the ExtensionStateContextType interface
+   - Add the setter function to the interface
+   - Add the setting to the initial state in useState with a default value
+   - Add the setting to the contextValue object
+   - Example:
+     ```typescript
+     interface ExtensionStateContextType {
+       preferredLanguage: string;
+       setPreferredLanguage: (value: string) => void;
+     }
+     ```
+
+3. Add the setting to ClineProvider.ts:
+   - Add the setting name to the GlobalStateKey type union
+   - Add the setting to the Promise.all array in getState
+   - Add the setting to the return value in getState with a default value
+   - Add the setting to the destructured variables in getStateToPostToWebview
+   - Add the setting to the return value in getStateToPostToWebview
+   - Add a case in setWebviewMessageListener to handle the setting's message type
+   - Example:
+     ```typescript
+     case "preferredLanguage":
+       await this.updateGlobalState("preferredLanguage", message.text)
+       await this.postStateToWebview()
+       break
+     ```
+
+4. Add the select UI to SettingsView.tsx:
+   - Import the setting and its setter from ExtensionStateContext
+   - Add the select element with appropriate styling to match VSCode's theme
+   - Add options for the dropdown
+   - Add appropriate labels and description text
+   - Example:
+     ```typescript
+     <select
+       value={preferredLanguage}
+       onChange={(e) => setPreferredLanguage(e.target.value)}
+       style={{
+         width: "100%",
+         padding: "4px 8px",
+         backgroundColor: "var(--vscode-input-background)",
+         color: "var(--vscode-input-foreground)",
+         border: "1px solid var(--vscode-input-border)",
+         borderRadius: "2px"
+       }}>
+       <option value="English">English</option>
+       <option value="Spanish">Spanish</option>
+       ...
+     </select>
+     ```
+
+5. Add the setting to handleSubmit in SettingsView.tsx:
+   - Add a vscode.postMessage call to send the setting's value when clicking Done
+   - Example:
+     ```typescript
+     vscode.postMessage({ type: "preferredLanguage", text: preferredLanguage })
+     ```
+
+These steps ensure that:
+- The setting's state is properly typed throughout the application
+- The setting persists between sessions
+- The setting's value is properly synchronized between the webview and extension
+- The setting has a proper UI representation in the settings view
+- Test coverage is maintained for the new setting

+ 1 - 0
README.md

@@ -13,6 +13,7 @@ A fork of Cline, an autonomous coding agent, tweaked for more speed and flexibil
 - Option to use a larger 1280x800 browser
 - Quick prompt copying from history
 - OpenRouter compression support
+- Language selection for Cline's communication (English, Japanese, Spanish, French, German, and more)
 - Support for newer Gemini models (gemini-exp-1206, gemini-2.0-flash-exp, gemini-2.0-flash-thinking-exp-1219) and Meta 3, 3.1, and 3.2 models via AWS Bedrock
 - Runs alongside the original Cline
 

+ 2 - 2
src/core/Cline.ts

@@ -769,8 +769,8 @@ export class Cline {
 			throw new Error("MCP hub not available")
 		}
 
-		const { browserLargeViewport } = await this.providerRef.deref()?.getState() ?? {}
-		const systemPrompt = await SYSTEM_PROMPT(cwd, this.api.getModel().info.supportsComputerUse ?? false, mcpHub, this.diffStrategy, browserLargeViewport) + await addCustomInstructions(this.customInstructions ?? '', cwd)
+		const { browserLargeViewport, preferredLanguage } = await this.providerRef.deref()?.getState() ?? {}
+		const systemPrompt = await SYSTEM_PROMPT(cwd, this.api.getModel().info.supportsComputerUse ?? false, mcpHub, this.diffStrategy, browserLargeViewport) + await addCustomInstructions(this.customInstructions ?? '', cwd, preferredLanguage)
 
 		// If the previous API request's total token usage is close to the context window, truncate the conversation history to free up space for the new request
 		if (previousApiReqIndex >= 0) {

+ 10 - 2
src/core/prompts/system.ts

@@ -772,9 +772,17 @@ async function loadRuleFiles(cwd: string): Promise<string> {
     return combinedRules
 }
 
-export async function addCustomInstructions(customInstructions: string, cwd: string): Promise<string> {
+export async function addCustomInstructions(customInstructions: string, cwd: string, preferredLanguage?: string): Promise<string> {
     const ruleFileContent = await loadRuleFiles(cwd)
-    const allInstructions = [customInstructions.trim()]
+    const allInstructions = []
+
+    if (preferredLanguage) {
+        allInstructions.push(`You should always speak and think in the ${preferredLanguage} language.`)
+    }
+    
+    if (customInstructions.trim()) {
+        allInstructions.push(customInstructions.trim())
+    }
 
     if (ruleFileContent && ruleFileContent.trim()) {
         allInstructions.push(ruleFileContent.trim())

+ 30 - 0
src/core/webview/ClineProvider.ts

@@ -71,6 +71,7 @@ type GlobalStateKey =
 	| "alwaysAllowMcp"
 	| "browserLargeViewport"
 	| "fuzzyMatchThreshold"
+	| "preferredLanguage" // Language setting for Cline's communication
 
 export const GlobalFileNames = {
 	apiConversationHistory: "api_conversation_history.json",
@@ -622,6 +623,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 						await this.updateGlobalState("fuzzyMatchThreshold", message.value)
 						await this.postStateToWebview()
 						break
+					case "preferredLanguage":
+						await this.updateGlobalState("preferredLanguage", message.text)
+						await this.postStateToWebview()
+						break
 				}
 			},
 			null,
@@ -951,6 +956,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			taskHistory,
 			soundVolume,
 			browserLargeViewport,
+			preferredLanguage,
 		} = await this.getState()
 		
 		const allowedCommands = vscode.workspace
@@ -977,6 +983,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			allowedCommands,
 			soundVolume: soundVolume ?? 0.5,
 			browserLargeViewport: browserLargeViewport ?? false,
+			preferredLanguage: preferredLanguage ?? 'English',
 		}
 	}
 
@@ -1072,6 +1079,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			soundVolume,
 			browserLargeViewport,
 			fuzzyMatchThreshold,
+			preferredLanguage,
 		] = await Promise.all([
 			this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
 			this.getGlobalState("apiModelId") as Promise<string | undefined>,
@@ -1112,6 +1120,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			this.getGlobalState("soundVolume") as Promise<number | undefined>,
 			this.getGlobalState("browserLargeViewport") as Promise<boolean | undefined>,
 			this.getGlobalState("fuzzyMatchThreshold") as Promise<number | undefined>,
+			this.getGlobalState("preferredLanguage") as Promise<string | undefined>,
 		])
 
 		let apiProvider: ApiProvider
@@ -1170,6 +1179,27 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			soundVolume,
 			browserLargeViewport: browserLargeViewport ?? false,
 			fuzzyMatchThreshold: fuzzyMatchThreshold ?? 1.0,
+			preferredLanguage: preferredLanguage ?? (() => {
+				// Get VSCode's locale setting
+				const vscodeLang = vscode.env.language;
+				// Map VSCode locale to our supported languages
+				const langMap: { [key: string]: string } = {
+					'en': 'English',
+					'es': 'Spanish',
+					'fr': 'French',
+					'de': 'German',
+					'it': 'Italian',
+					'pt': 'Portuguese',
+					'zh': 'Chinese',
+					'ja': 'Japanese',
+					'ko': 'Korean',
+					'ru': 'Russian',
+					'ar': 'Arabic',
+					'hi': 'Hindi'
+				};
+				// Return mapped language or default to English
+				return langMap[vscodeLang.split('-')[0]] ?? 'English';
+			})(),
 		}
 	}
 

+ 20 - 2
src/core/webview/__tests__/ClineProvider.test.ts

@@ -73,7 +73,8 @@ jest.mock('vscode', () => ({
         onDidCloseTextDocument: jest.fn(() => ({ dispose: jest.fn() }))
     },
     env: {
-        uriScheme: 'vscode'
+        uriScheme: 'vscode',
+        language: 'en'
     }
 }))
 
@@ -235,6 +236,7 @@ describe('ClineProvider', () => {
         
         const mockState: ExtensionState = {
             version: '1.0.0',
+            preferredLanguage: 'English',
             clineMessages: [],
             taskHistory: [],
             shouldShowAnnouncement: false,
@@ -248,7 +250,7 @@ describe('ClineProvider', () => {
             alwaysAllowBrowser: false,
             uriScheme: 'vscode',
             soundEnabled: false,
-            diffEnabled: false
+            diffEnabled: false,
         }
         
         const message: ExtensionMessage = { 
@@ -300,6 +302,22 @@ describe('ClineProvider', () => {
         expect(state).toHaveProperty('diffEnabled')
     })
 
+    test('preferredLanguage defaults to VSCode language when not set', async () => {
+        // Mock VSCode language as Spanish
+        (vscode.env as any).language = 'es-ES';
+        
+        const state = await provider.getState();
+        expect(state.preferredLanguage).toBe('Spanish');
+    });
+
+    test('preferredLanguage defaults to English for unsupported VSCode language', async () => {
+        // Mock VSCode language as an unsupported language
+        (vscode.env as any).language = 'unsupported-LANG';
+        
+        const state = await provider.getState();
+        expect(state.preferredLanguage).toBe('English');
+    });
+
     test('diffEnabled defaults to true when not set', async () => {
         // Mock globalState.get to return undefined for diffEnabled
         (mockContext.globalState.get as jest.Mock).mockReturnValue(undefined)

+ 1 - 0
src/shared/ExtensionMessage.ts

@@ -55,6 +55,7 @@ export interface ExtensionState {
 	diffEnabled?: boolean
 	browserLargeViewport?: boolean
 	fuzzyMatchThreshold?: number
+	preferredLanguage: string
 }
 
 export interface ClineMessage {

+ 1 - 0
src/shared/WebviewMessage.ts

@@ -40,6 +40,7 @@ export interface WebviewMessage {
 		| "toggleToolAlwaysAllow"
 		| "toggleMcpServer"
 		| "fuzzyMatchThreshold"
+		| "preferredLanguage"
 	text?: string
 	disabled?: boolean
 	askResponse?: ClineAskResponse

+ 39 - 0
webview-ui/src/components/settings/SettingsView.tsx

@@ -40,6 +40,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
 		allowedCommands,
 		fuzzyMatchThreshold,
 		setFuzzyMatchThreshold,
+		preferredLanguage,
+		setPreferredLanguage,
 	} = useExtensionState()
 	const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
 	const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
@@ -67,6 +69,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
 			vscode.postMessage({ type: "diffEnabled", bool: diffEnabled })
 			vscode.postMessage({ type: "browserLargeViewport", bool: browserLargeViewport })
 			vscode.postMessage({ type: "fuzzyMatchThreshold", value: fuzzyMatchThreshold ?? 1.0 })
+			vscode.postMessage({ type: "preferredLanguage", text: preferredLanguage })
 			onDone()
 		}
 	}
@@ -136,6 +139,42 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
 				</div>
 
 				<div style={{ marginBottom: 5 }}>
+					<div style={{ marginBottom: 15 }}>
+						<label style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>Preferred Language</label>
+						<select
+							value={preferredLanguage}
+							onChange={(e) => setPreferredLanguage(e.target.value)}
+							style={{
+								width: "100%",
+								padding: "4px 8px",
+								backgroundColor: "var(--vscode-input-background)",
+								color: "var(--vscode-input-foreground)",
+								border: "1px solid var(--vscode-input-border)",
+								borderRadius: "2px",
+								height: "28px"
+							}}>
+							<option value="English">English</option>
+							<option value="Spanish">Spanish - Español</option>
+							<option value="French">French - Français</option>
+							<option value="German">German - Deutsch</option>
+							<option value="Italian">Italian - Italiano</option>
+							<option value="Portuguese">Portuguese - Português</option>
+							<option value="Chinese">Chinese - 中文</option>
+							<option value="Japanese">Japanese - 日本語</option>
+							<option value="Korean">Korean - 한국어</option>
+							<option value="Russian">Russian - Русский</option>
+							<option value="Arabic">Arabic - العربية</option>
+							<option value="Hindi">Hindi - हिन्दी</option>
+						</select>
+						<p style={{
+							fontSize: "12px",
+							marginTop: "5px",
+							color: "var(--vscode-descriptionForeground)",
+						}}>
+							Select the language that Cline should use for communication.
+						</p>
+					</div>
+
 					<VSCodeTextArea
 						value={customInstructions ?? ""}
 						style={{ width: "100%" }}

+ 4 - 0
webview-ui/src/context/ExtensionStateContext.tsx

@@ -33,6 +33,8 @@ export interface ExtensionStateContextType extends ExtensionState {
 	setDiffEnabled: (value: boolean) => void
 	setBrowserLargeViewport: (value: boolean) => void
 	setFuzzyMatchThreshold: (value: number) => void
+	preferredLanguage: string
+	setPreferredLanguage: (value: string) => void
 }
 
 const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
@@ -48,6 +50,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
 		soundVolume: 0.5,
 		diffEnabled: false,
 		fuzzyMatchThreshold: 1.0,
+		preferredLanguage: 'English',
 	})
 	const [didHydrateState, setDidHydrateState] = useState(false)
 	const [showWelcome, setShowWelcome] = useState(false)
@@ -153,6 +156,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
 		setDiffEnabled: (value) => setState((prevState) => ({ ...prevState, diffEnabled: value })),
 		setBrowserLargeViewport: (value) => setState((prevState) => ({ ...prevState, browserLargeViewport: value })),
 		setFuzzyMatchThreshold: (value) => setState((prevState) => ({ ...prevState, fuzzyMatchThreshold: value })),
+		setPreferredLanguage: (value) => setState((prevState) => ({ ...prevState, preferredLanguage: value })),
 	}
 
 	return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>