-
What can Roo do for you?
+
{t("chat:greeting")}
Thanks to the latest breakthroughs in agentic coding capabilities, I can handle complex
software development tasks step-by-step. With tools that let me create & edit files, explore
diff --git a/webview-ui/src/components/ui/combobox-primitive.tsx b/webview-ui/src/components/ui/combobox-primitive.tsx
index 13bad87aba..b2c3104a35 100644
--- a/webview-ui/src/components/ui/combobox-primitive.tsx
+++ b/webview-ui/src/components/ui/combobox-primitive.tsx
@@ -50,7 +50,7 @@ export type ComboboxType = "single" | "multiple"
export interface ComboboxBaseProps
extends React.ComponentProps,
- Omit, "value" | "defaultValue" | "onValueChange"> {
+ Omit, "value" | "defaultValue" | "onValueChange" | "children"> {
type?: ComboboxType | undefined
inputValue?: string
defaultInputValue?: string
diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx
index f7292e2721..d79f263e83 100644
--- a/webview-ui/src/context/ExtensionStateContext.tsx
+++ b/webview-ui/src/context/ExtensionStateContext.tsx
@@ -116,7 +116,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
enableCheckpoints: true,
checkpointStorage: "task",
fuzzyMatchThreshold: 1.0,
- vscodeLanguage: "en", // Default language code
+ language: "en", // Default language code
enableCustomModeCreation: true,
writeDelayMs: 1000,
browserViewportSize: "900x600",
diff --git a/webview-ui/src/i18n/TranslationContext.tsx b/webview-ui/src/i18n/TranslationContext.tsx
new file mode 100644
index 0000000000..e6115dae6a
--- /dev/null
+++ b/webview-ui/src/i18n/TranslationContext.tsx
@@ -0,0 +1,51 @@
+import React, { createContext, useContext, ReactNode, useEffect } from "react"
+import { useTranslation } from "react-i18next"
+import i18next, { loadTranslations } from "./setup"
+import { useExtensionState } from "@/context/ExtensionStateContext"
+
+// Create context for translations
+export const TranslationContext = createContext<{
+ t: (key: string, options?: Record) => string
+ i18n: typeof i18next
+}>({
+ t: (key: string) => key,
+ i18n: i18next,
+})
+
+// Translation provider component
+export const TranslationProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
+ // Initialize with default configuration
+ const { i18n } = useTranslation()
+ // Get the extension state directly - it already contains all state properties
+ const extensionState = useExtensionState()
+
+ // Load translations once when the component mounts
+ useEffect(() => {
+ try {
+ loadTranslations()
+ } catch (error) {
+ console.error("Failed to load translations:", error)
+ }
+ }, [])
+
+ useEffect(() => {
+ i18n.changeLanguage(extensionState.language)
+ }, [i18n, extensionState.language])
+
+ return (
+ ) => {
+ return i18n.t(key, options)
+ },
+ i18n,
+ }}>
+ {children}
+
+ )
+}
+
+// Custom hook for easy translations
+export const useAppTranslation = () => useContext(TranslationContext)
+
+export default TranslationProvider
diff --git a/webview-ui/src/i18n/__tests__/TranslationContext.test.tsx b/webview-ui/src/i18n/__tests__/TranslationContext.test.tsx
new file mode 100644
index 0000000000..0b4dbf8238
--- /dev/null
+++ b/webview-ui/src/i18n/__tests__/TranslationContext.test.tsx
@@ -0,0 +1,52 @@
+import React from "react"
+import { render } from "@testing-library/react"
+import "@testing-library/jest-dom"
+import TranslationProvider, { useAppTranslation } from "../TranslationContext"
+import { setupI18nForTests } from "../test-utils"
+
+// Mock the useExtensionState hook
+jest.mock("@/context/ExtensionStateContext", () => ({
+ useExtensionState: () => ({
+ language: "en",
+ }),
+}))
+
+// Mock component that uses the translation context
+const TestComponent = () => {
+ const { t } = useAppTranslation()
+ return (
+
+
{t("settings.autoApprove.title")}
+
{t("notifications.error", { message: "Test error" })}
+
+ )
+}
+
+describe("TranslationContext", () => {
+ beforeAll(() => {
+ // Initialize i18next with test translations
+ setupI18nForTests()
+ })
+
+ it("should provide translations via context", () => {
+ const { getByTestId } = render(
+
+
+ ,
+ )
+
+ // Check if translation is provided correctly
+ expect(getByTestId("translation-test")).toHaveTextContent("Auto-Approve")
+ })
+
+ it("should handle interpolation correctly", () => {
+ const { getByTestId } = render(
+
+
+ ,
+ )
+
+ // Check if interpolation works
+ expect(getByTestId("translation-interpolation")).toHaveTextContent("Operation failed: Test error")
+ })
+})
diff --git a/webview-ui/src/i18n/locales/ar/chat.json b/webview-ui/src/i18n/locales/ar/chat.json
new file mode 100644
index 0000000000..af09e7cf5e
--- /dev/null
+++ b/webview-ui/src/i18n/locales/ar/chat.json
@@ -0,0 +1,3 @@
+{
+ "greeting": "ماذا يمكن أن يفعل Roo من أجلك؟"
+}
diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json
new file mode 100644
index 0000000000..9f9d819936
--- /dev/null
+++ b/webview-ui/src/i18n/locales/ca/chat.json
@@ -0,0 +1,3 @@
+{
+ "greeting": "Què pot fer Roo per tu?"
+}
diff --git a/webview-ui/src/i18n/locales/cs/chat.json b/webview-ui/src/i18n/locales/cs/chat.json
new file mode 100644
index 0000000000..989655bf4b
--- /dev/null
+++ b/webview-ui/src/i18n/locales/cs/chat.json
@@ -0,0 +1,3 @@
+{
+ "greeting": "Co pro vás může Roo udělat?"
+}
diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json
new file mode 100644
index 0000000000..9047746c50
--- /dev/null
+++ b/webview-ui/src/i18n/locales/de/chat.json
@@ -0,0 +1,3 @@
+{
+ "greeting": "Was kann Roo für dich tun?"
+}
diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json
new file mode 100644
index 0000000000..1b742bb11f
--- /dev/null
+++ b/webview-ui/src/i18n/locales/en/chat.json
@@ -0,0 +1,3 @@
+{
+ "greeting": "What can Roo do for you?"
+}
diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json
new file mode 100644
index 0000000000..34b857fba7
--- /dev/null
+++ b/webview-ui/src/i18n/locales/es/chat.json
@@ -0,0 +1,3 @@
+{
+ "greeting": "¿Qué puedo hacer por ti?"
+}
diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json
new file mode 100644
index 0000000000..8a2e696594
--- /dev/null
+++ b/webview-ui/src/i18n/locales/fr/chat.json
@@ -0,0 +1,3 @@
+{
+ "greeting": "Que peut faire Roo pour vous ?"
+}
diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json
new file mode 100644
index 0000000000..5212c2026c
--- /dev/null
+++ b/webview-ui/src/i18n/locales/hi/chat.json
@@ -0,0 +1,3 @@
+{
+ "greeting": "Roo आपके लिए क्या कर सकता है?"
+}
diff --git a/webview-ui/src/i18n/locales/hu/chat.json b/webview-ui/src/i18n/locales/hu/chat.json
new file mode 100644
index 0000000000..f57221300c
--- /dev/null
+++ b/webview-ui/src/i18n/locales/hu/chat.json
@@ -0,0 +1,3 @@
+{
+ "greeting": "Mit tehet érted a Roo?"
+}
diff --git a/webview-ui/src/i18n/locales/it/chat.json b/webview-ui/src/i18n/locales/it/chat.json
new file mode 100644
index 0000000000..6ab48932bc
--- /dev/null
+++ b/webview-ui/src/i18n/locales/it/chat.json
@@ -0,0 +1,3 @@
+{
+ "greeting": "Cosa può fare Roo per te?"
+}
diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json
new file mode 100644
index 0000000000..d372814cee
--- /dev/null
+++ b/webview-ui/src/i18n/locales/ja/chat.json
@@ -0,0 +1,3 @@
+{
+ "greeting": "Rooは何をお手伝いできますか?"
+}
diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json
new file mode 100644
index 0000000000..a96e6d17a3
--- /dev/null
+++ b/webview-ui/src/i18n/locales/ko/chat.json
@@ -0,0 +1,3 @@
+{
+ "greeting": "Roo가 당신을 위해 무엇을 할 수 있을까요?"
+}
diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json
new file mode 100644
index 0000000000..b21e0047ed
--- /dev/null
+++ b/webview-ui/src/i18n/locales/pl/chat.json
@@ -0,0 +1,3 @@
+{
+ "greeting": "Co Roo może dla Ciebie zrobić?"
+}
diff --git a/webview-ui/src/i18n/locales/pt-br/chat.json b/webview-ui/src/i18n/locales/pt-br/chat.json
new file mode 100644
index 0000000000..9fcd1ad1e7
--- /dev/null
+++ b/webview-ui/src/i18n/locales/pt-br/chat.json
@@ -0,0 +1,3 @@
+{
+ "greeting": "O que o Roo pode fazer por você?"
+}
diff --git a/webview-ui/src/i18n/locales/pt/chat.json b/webview-ui/src/i18n/locales/pt/chat.json
new file mode 100644
index 0000000000..66bef0dff9
--- /dev/null
+++ b/webview-ui/src/i18n/locales/pt/chat.json
@@ -0,0 +1,3 @@
+{
+ "greeting": "O que o Roo pode fazer por si?"
+}
diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json
new file mode 100644
index 0000000000..b01532b168
--- /dev/null
+++ b/webview-ui/src/i18n/locales/ru/chat.json
@@ -0,0 +1,3 @@
+{
+ "greeting": "Что Roo может сделать для вас?"
+}
diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json
new file mode 100644
index 0000000000..8c160b2b77
--- /dev/null
+++ b/webview-ui/src/i18n/locales/tr/chat.json
@@ -0,0 +1,3 @@
+{
+ "greeting": "Roo sizin için ne yapabilir?"
+}
diff --git a/webview-ui/src/i18n/locales/zh-cn/chat.json b/webview-ui/src/i18n/locales/zh-cn/chat.json
new file mode 100644
index 0000000000..4e17fd3ca0
--- /dev/null
+++ b/webview-ui/src/i18n/locales/zh-cn/chat.json
@@ -0,0 +1,3 @@
+{
+ "greeting": "Roo能为您做什么?"
+}
diff --git a/webview-ui/src/i18n/locales/zh-tw/chat.json b/webview-ui/src/i18n/locales/zh-tw/chat.json
new file mode 100644
index 0000000000..13724748a9
--- /dev/null
+++ b/webview-ui/src/i18n/locales/zh-tw/chat.json
@@ -0,0 +1,3 @@
+{
+ "greeting": "Roo能為您做什麼?"
+}
diff --git a/webview-ui/src/i18n/setup.ts b/webview-ui/src/i18n/setup.ts
new file mode 100644
index 0000000000..678cdc1d49
--- /dev/null
+++ b/webview-ui/src/i18n/setup.ts
@@ -0,0 +1,54 @@
+import i18next from "i18next"
+import { initReactI18next } from "react-i18next"
+
+// Build translations object
+const translations: Record> = {}
+
+// Dynamically load locale files
+const localeFiles = import.meta.glob("./locales/**/*.json", { eager: true })
+
+// Process all locale files
+Object.entries(localeFiles).forEach(([path, module]) => {
+ // Extract language and namespace from path
+ // Example path: './locales/en/common.json' -> language: 'en', namespace: 'common'
+ const match = path.match(/\.\/locales\/([^/]+)\/([^/]+)\.json/)
+
+ if (match) {
+ const [, language, namespace] = match
+
+ // Initialize language object if it doesn't exist
+ if (!translations[language]) {
+ translations[language] = {}
+ }
+
+ // Add namespace resources to language
+ translations[language][namespace] = (module as any).default || module
+ }
+})
+
+console.log("Dynamically loaded translations:", Object.keys(translations))
+
+// Initialize i18next for React
+// This will be initialized with the VSCode language in TranslationProvider
+i18next.use(initReactI18next).init({
+ lng: "en", // Default language (will be overridden)
+ fallbackLng: "en",
+ debug: false,
+ interpolation: {
+ escapeValue: false, // React already escapes by default
+ },
+})
+
+export function loadTranslations() {
+ Object.entries(translations).forEach(([lang, namespaces]) => {
+ try {
+ Object.entries(namespaces).forEach(([namespace, resources]) => {
+ i18next.addResourceBundle(lang, namespace, resources, true, true)
+ })
+ } catch (error) {
+ console.warn(`Could not load ${lang} translations:`, error)
+ }
+ })
+}
+
+export default i18next
diff --git a/webview-ui/src/i18n/test-utils.ts b/webview-ui/src/i18n/test-utils.ts
new file mode 100644
index 0000000000..9abd4d9e06
--- /dev/null
+++ b/webview-ui/src/i18n/test-utils.ts
@@ -0,0 +1,37 @@
+import i18next from "i18next"
+import { initReactI18next } from "react-i18next"
+
+/**
+ * Sets up i18next for testing with pre-defined translations.
+ * Use this in test files to ensure consistent translation handling.
+ */
+export const setupI18nForTests = () => {
+ i18next.use(initReactI18next).init({
+ lng: "en",
+ fallbackLng: "en",
+ debug: false,
+ interpolation: {
+ escapeValue: false,
+ },
+ // Pre-define all translations needed for tests
+ resources: {
+ en: {
+ settings: {
+ autoApprove: {
+ title: "Auto-Approve",
+ },
+ },
+ common: {
+ notifications: {
+ error: "Operation failed: {{message}}",
+ },
+ },
+ chat: {
+ test: "Test",
+ },
+ },
+ },
+ })
+
+ return i18next
+}