mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-19 10:02:04 +08:00
feat: add language selector with VS Code language detection + user override
- Extension sends vscode.env.language to webview via ready message - LanguageProvider uses priority: user override → VS Code lang → browser → en - LanguageTab has a Select dropdown with all 16 locales + 'Auto' - Language preference stored in kilo-code.new.language VS Code setting - setLanguage message from webview saves preference to VS Code config
This commit is contained in:
@@ -338,6 +338,7 @@
|
||||
"name": "kilo-code",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"@kilocode/kilo-i18n": "workspace:*",
|
||||
"@kilocode/kilo-ui": "workspace:*",
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"eventsource": "^2.0.2",
|
||||
|
||||
@@ -114,6 +114,49 @@
|
||||
"configuration": {
|
||||
"title": "Kilo Code",
|
||||
"properties": {
|
||||
"kilo-code.new.language": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Override the UI language for Kilo Code (e.g. en, de, ja). Empty means use VS Code's display language.",
|
||||
"enum": [
|
||||
"",
|
||||
"en",
|
||||
"zh",
|
||||
"zht",
|
||||
"ko",
|
||||
"de",
|
||||
"es",
|
||||
"fr",
|
||||
"da",
|
||||
"ja",
|
||||
"pl",
|
||||
"ru",
|
||||
"ar",
|
||||
"no",
|
||||
"br",
|
||||
"th",
|
||||
"bs"
|
||||
],
|
||||
"enumDescriptions": [
|
||||
"Auto (VS Code language)",
|
||||
"English",
|
||||
"简体中文",
|
||||
"繁體中文",
|
||||
"한국어",
|
||||
"Deutsch",
|
||||
"Español",
|
||||
"Français",
|
||||
"Dansk",
|
||||
"日本語",
|
||||
"Polski",
|
||||
"Русский",
|
||||
"العربية",
|
||||
"Norsk",
|
||||
"Português (Brasil)",
|
||||
"ภาษาไทย",
|
||||
"Bosanski"
|
||||
]
|
||||
},
|
||||
"kilo-code.new.model.providerID": {
|
||||
"type": "string",
|
||||
"default": "kilo",
|
||||
|
||||
@@ -64,9 +64,12 @@ export class KiloProvider implements vscode.WebviewViewProvider {
|
||||
|
||||
// Re-send ready so the webview can recover after refresh.
|
||||
if (serverInfo) {
|
||||
const langConfig = vscode.workspace.getConfiguration("kilo-code.new")
|
||||
this.postMessage({
|
||||
type: "ready",
|
||||
serverInfo,
|
||||
vscodeLanguage: vscode.env.language,
|
||||
languageOverride: langConfig.get<string>("language"),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -193,6 +196,11 @@ export class KiloProvider implements vscode.WebviewViewProvider {
|
||||
case "requestAgents":
|
||||
await this.fetchAndSendAgents()
|
||||
break
|
||||
case "setLanguage":
|
||||
await vscode.workspace
|
||||
.getConfiguration("kilo-code.new")
|
||||
.update("language", message.locale || undefined, vscode.ConfigurationTarget.Global)
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -259,7 +267,13 @@ export class KiloProvider implements vscode.WebviewViewProvider {
|
||||
this.connectionState = this.connectionService.getConnectionState()
|
||||
|
||||
if (serverInfo) {
|
||||
this.postMessage({ type: "ready", serverInfo })
|
||||
const langConfig = vscode.workspace.getConfiguration("kilo-code.new")
|
||||
this.postMessage({
|
||||
type: "ready",
|
||||
serverInfo,
|
||||
vscodeLanguage: vscode.env.language,
|
||||
languageOverride: langConfig.get<string>("language"),
|
||||
})
|
||||
}
|
||||
|
||||
this.postMessage({ type: "connectionState", state: this.connectionState })
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Component, createSignal, createMemo, Switch, Match, onMount, onCleanup } from "solid-js"
|
||||
import { ThemeProvider } from "@kilocode/kilo-ui/theme"
|
||||
import { LanguageProvider } from "./context/language"
|
||||
import { DialogProvider } from "@kilocode/kilo-ui/context/dialog"
|
||||
import { MarkedProvider } from "@kilocode/kilo-ui/context/marked"
|
||||
import { DataProvider } from "@kilocode/kilo-ui/context/data"
|
||||
@@ -10,6 +9,7 @@ import { VSCodeProvider } from "./context/vscode"
|
||||
import { ServerProvider, useServer } from "./context/server"
|
||||
import { ProviderProvider } from "./context/provider"
|
||||
import { SessionProvider, useSession } from "./context/session"
|
||||
import { LanguageProvider } from "./context/language"
|
||||
import { ChatView } from "./components/chat"
|
||||
import SessionList from "./components/history/SessionList"
|
||||
import type { Message as SDKMessage, Part as SDKPart } from "@kilocode/sdk/v2"
|
||||
@@ -37,13 +37,6 @@ const DummyView: Component<{ title: string }> = (props) => {
|
||||
|
||||
/**
|
||||
* Bridge our session store to the DataProvider's expected Data shape.
|
||||
* Since the runtime data comes from the CLI backend, it's already in SDK format —
|
||||
* we just need to restructure it into the Data layout that DataProvider expects.
|
||||
*
|
||||
* Note: DiffComponentProvider and CodeComponentProvider are NOT included because
|
||||
* @pierre/diffs uses Vite's ?worker&url import syntax which is incompatible
|
||||
* with esbuild. Tool renderers that need diffs (edit, write, apply_patch)
|
||||
* will fall back to GenericTool display. See ui-implementation-plan.md §7.6.
|
||||
*/
|
||||
const DataBridge: Component<{ children: any }> = (props) => {
|
||||
const session = useSession()
|
||||
@@ -77,6 +70,19 @@ const DataBridge: Component<{ children: any }> = (props) => {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps children in LanguageProvider, passing server-side language info.
|
||||
* Must be below ServerProvider in the hierarchy.
|
||||
*/
|
||||
const LanguageBridge: Component<{ children: any }> = (props) => {
|
||||
const server = useServer()
|
||||
return (
|
||||
<LanguageProvider vscodeLanguage={server.vscodeLanguage} languageOverride={server.languageOverride}>
|
||||
{props.children}
|
||||
</LanguageProvider>
|
||||
)
|
||||
}
|
||||
|
||||
// Inner app component that uses the contexts
|
||||
const AppContent: Component = () => {
|
||||
const [currentView, setCurrentView] = createSignal<ViewType>("newTask")
|
||||
@@ -154,11 +160,11 @@ const AppContent: Component = () => {
|
||||
const App: Component = () => {
|
||||
return (
|
||||
<ThemeProvider defaultTheme="kilo-vscode">
|
||||
<LanguageProvider>
|
||||
<DialogProvider>
|
||||
<MarkedProvider>
|
||||
<VSCodeProvider>
|
||||
<ServerProvider>
|
||||
<DialogProvider>
|
||||
<VSCodeProvider>
|
||||
<ServerProvider>
|
||||
<LanguageBridge>
|
||||
<MarkedProvider>
|
||||
<ProviderProvider>
|
||||
<SessionProvider>
|
||||
<DataBridge>
|
||||
@@ -166,11 +172,11 @@ const App: Component = () => {
|
||||
</DataBridge>
|
||||
</SessionProvider>
|
||||
</ProviderProvider>
|
||||
</ServerProvider>
|
||||
</VSCodeProvider>
|
||||
</MarkedProvider>
|
||||
</DialogProvider>
|
||||
</LanguageProvider>
|
||||
</MarkedProvider>
|
||||
</LanguageBridge>
|
||||
</ServerProvider>
|
||||
</VSCodeProvider>
|
||||
</DialogProvider>
|
||||
</ThemeProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,30 +1,34 @@
|
||||
import { Component } from "solid-js"
|
||||
import { Select } from "@kilocode/kilo-ui/select"
|
||||
import { useLanguage, LOCALES, LOCALE_LABELS, type Locale } from "../../context/language"
|
||||
|
||||
const options = ["", ...LOCALES] as const
|
||||
type Option = "" | Locale
|
||||
|
||||
const LanguageTab: Component = () => {
|
||||
const language = useLanguage()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
background: "var(--vscode-editor-background)",
|
||||
border: "1px solid var(--vscode-panel-border)",
|
||||
"border-radius": "4px",
|
||||
padding: "16px",
|
||||
<div style={{ padding: "16px" }}>
|
||||
<p style={{ "font-size": "13px", "margin-bottom": "12px" }}>
|
||||
Choose the language for the Kilo Code UI. "Auto" uses your VS Code display language.
|
||||
</p>
|
||||
<Select
|
||||
options={[...options]}
|
||||
current={language.userOverride()}
|
||||
label={(opt: Option) => (opt === "" ? "Auto (VS Code language)" : LOCALE_LABELS[opt])}
|
||||
value={(opt: Option) => opt}
|
||||
onSelect={(opt) => {
|
||||
if (opt !== undefined) {
|
||||
language.setLocale(opt as Locale | "")
|
||||
}
|
||||
}}
|
||||
>
|
||||
<p
|
||||
style={{
|
||||
"font-size": "12px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
margin: 0,
|
||||
"line-height": "1.5",
|
||||
}}
|
||||
>
|
||||
<strong style={{ color: "var(--vscode-foreground)" }}>This section is not implemented yet.</strong> It will
|
||||
contain configuration options and explanatory text related to the selected settings category. During
|
||||
reimplementation, use this space to validate layout, spacing, scrolling behavior, and navigation state before
|
||||
wiring up real controls.
|
||||
</p>
|
||||
</div>
|
||||
variant="secondary"
|
||||
size="large"
|
||||
/>
|
||||
<p style={{ "font-size": "12px", color: "var(--vscode-descriptionForeground)", "margin-top": "8px" }}>
|
||||
Current: {LOCALE_LABELS[language.locale()]}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
* Language context
|
||||
* Provides i18n translations for kilo-ui components.
|
||||
* Merges UI translations from @opencode-ai/ui and Kilo overrides from @kilocode/kilo-i18n.
|
||||
* Follows the same pattern as the desktop app (packages/app/src/context/language.tsx).
|
||||
*
|
||||
* Locale priority: user override → VS Code display language → browser language → "en"
|
||||
*/
|
||||
|
||||
import { createMemo, ParentComponent } from "solid-js"
|
||||
import { createSignal, createMemo, createEffect, ParentComponent, Accessor } from "solid-js"
|
||||
import { I18nProvider } from "@kilocode/kilo-ui/context"
|
||||
import type { UiI18nKey, UiI18nParams } from "@kilocode/kilo-ui/context"
|
||||
import { dict as uiEn } from "@kilocode/kilo-ui/i18n/en"
|
||||
@@ -40,8 +41,9 @@ import { dict as kiloNo } from "@kilocode/kilo-i18n/no"
|
||||
import { dict as kiloBr } from "@kilocode/kilo-i18n/br"
|
||||
import { dict as kiloTh } from "@kilocode/kilo-i18n/th"
|
||||
import { dict as kiloBs } from "@kilocode/kilo-i18n/bs"
|
||||
import { useVSCode } from "./vscode"
|
||||
|
||||
type Locale =
|
||||
export type Locale =
|
||||
| "en"
|
||||
| "zh"
|
||||
| "zht"
|
||||
@@ -59,7 +61,45 @@ type Locale =
|
||||
| "th"
|
||||
| "bs"
|
||||
|
||||
// Merge UI + Kilo dicts (kilo overrides last)
|
||||
export const LOCALES: readonly Locale[] = [
|
||||
"en",
|
||||
"zh",
|
||||
"zht",
|
||||
"ko",
|
||||
"de",
|
||||
"es",
|
||||
"fr",
|
||||
"da",
|
||||
"ja",
|
||||
"pl",
|
||||
"ru",
|
||||
"ar",
|
||||
"no",
|
||||
"br",
|
||||
"th",
|
||||
"bs",
|
||||
]
|
||||
|
||||
export const LOCALE_LABELS: Record<Locale, string> = {
|
||||
en: "English",
|
||||
zh: "简体中文",
|
||||
zht: "繁體中文",
|
||||
ko: "한국어",
|
||||
de: "Deutsch",
|
||||
es: "Español",
|
||||
fr: "Français",
|
||||
da: "Dansk",
|
||||
ja: "日本語",
|
||||
pl: "Polski",
|
||||
ru: "Русский",
|
||||
ar: "العربية",
|
||||
no: "Norsk",
|
||||
br: "Português (Brasil)",
|
||||
th: "ภาษาไทย",
|
||||
bs: "Bosanski",
|
||||
}
|
||||
|
||||
// Merge UI + Kilo dicts (kilo overrides last, English base always present)
|
||||
const dicts: Record<Locale, Record<string, string>> = {
|
||||
en: { ...uiEn, ...kiloEn },
|
||||
zh: { ...uiEn, ...kiloEn, ...uiZh, ...kiloZh },
|
||||
@@ -79,61 +119,23 @@ const dicts: Record<Locale, Record<string, string>> = {
|
||||
bs: { ...uiEn, ...kiloEn, ...uiBs, ...kiloBs },
|
||||
}
|
||||
|
||||
function detectLocale(): Locale {
|
||||
if (typeof navigator !== "object") {
|
||||
return "en"
|
||||
function normalizeLocale(lang: string): Locale {
|
||||
const lower = lang.toLowerCase()
|
||||
if (lower.startsWith("zh")) {
|
||||
return lower.includes("hant") ? "zht" : "zh"
|
||||
}
|
||||
|
||||
const languages = navigator.languages?.length ? navigator.languages : [navigator.language]
|
||||
for (const language of languages) {
|
||||
if (!language) {
|
||||
continue
|
||||
}
|
||||
const lower = language.toLowerCase()
|
||||
if (lower.startsWith("zh")) {
|
||||
return lower.includes("hant") ? "zht" : "zh"
|
||||
}
|
||||
if (lower.startsWith("ko")) {
|
||||
return "ko"
|
||||
}
|
||||
if (lower.startsWith("de")) {
|
||||
return "de"
|
||||
}
|
||||
if (lower.startsWith("es")) {
|
||||
return "es"
|
||||
}
|
||||
if (lower.startsWith("fr")) {
|
||||
return "fr"
|
||||
}
|
||||
if (lower.startsWith("da")) {
|
||||
return "da"
|
||||
}
|
||||
if (lower.startsWith("ja")) {
|
||||
return "ja"
|
||||
}
|
||||
if (lower.startsWith("pl")) {
|
||||
return "pl"
|
||||
}
|
||||
if (lower.startsWith("ru")) {
|
||||
return "ru"
|
||||
}
|
||||
if (lower.startsWith("ar")) {
|
||||
return "ar"
|
||||
}
|
||||
if (lower.startsWith("no") || lower.startsWith("nb") || lower.startsWith("nn")) {
|
||||
return "no"
|
||||
}
|
||||
if (lower.startsWith("pt")) {
|
||||
return "br"
|
||||
}
|
||||
if (lower.startsWith("th")) {
|
||||
return "th"
|
||||
}
|
||||
if (lower.startsWith("bs")) {
|
||||
return "bs"
|
||||
for (const loc of LOCALES) {
|
||||
if (lower.startsWith(loc)) {
|
||||
return loc
|
||||
}
|
||||
}
|
||||
|
||||
// Special cases
|
||||
if (lower.startsWith("nb") || lower.startsWith("nn")) {
|
||||
return "no"
|
||||
}
|
||||
if (lower.startsWith("pt")) {
|
||||
return "br"
|
||||
}
|
||||
return "en"
|
||||
}
|
||||
|
||||
@@ -147,8 +149,39 @@ function resolveTemplate(text: string, params?: UiI18nParams) {
|
||||
})
|
||||
}
|
||||
|
||||
export const LanguageProvider: ParentComponent = (props) => {
|
||||
const locale = createMemo<Locale>(detectLocale)
|
||||
interface LanguageProviderProps {
|
||||
vscodeLanguage?: Accessor<string | undefined>
|
||||
languageOverride?: Accessor<string | undefined>
|
||||
}
|
||||
|
||||
export const LanguageProvider: ParentComponent<LanguageProviderProps> = (props) => {
|
||||
const vscode = useVSCode()
|
||||
const [userOverride, setUserOverride] = createSignal<Locale | "">("")
|
||||
|
||||
// Initialize from extension-side override
|
||||
createEffect(() => {
|
||||
const override = props.languageOverride?.()
|
||||
if (override) {
|
||||
setUserOverride(normalizeLocale(override))
|
||||
}
|
||||
})
|
||||
|
||||
// Resolved locale: user override → VS Code language → browser language → "en"
|
||||
const locale = createMemo<Locale>(() => {
|
||||
const override = userOverride()
|
||||
if (override) {
|
||||
return override
|
||||
}
|
||||
const vscodeLang = props.vscodeLanguage?.()
|
||||
if (vscodeLang) {
|
||||
return normalizeLocale(vscodeLang)
|
||||
}
|
||||
if (typeof navigator !== "undefined" && navigator.language) {
|
||||
return normalizeLocale(navigator.language)
|
||||
}
|
||||
return "en"
|
||||
})
|
||||
|
||||
const dict = createMemo(() => dicts[locale()] ?? dicts.en)
|
||||
|
||||
const t = (key: UiI18nKey, params?: UiI18nParams) => {
|
||||
@@ -156,5 +189,33 @@ export const LanguageProvider: ParentComponent = (props) => {
|
||||
return resolveTemplate(text, params)
|
||||
}
|
||||
|
||||
return <I18nProvider value={{ locale: () => locale(), t }}>{props.children}</I18nProvider>
|
||||
const setLocale = (next: Locale | "") => {
|
||||
setUserOverride(next)
|
||||
vscode.postMessage({ type: "setLanguage", locale: next })
|
||||
}
|
||||
|
||||
return (
|
||||
<LanguageContext.Provider value={{ locale, setLocale, userOverride }}>
|
||||
<I18nProvider value={{ locale: () => locale(), t }}>{props.children}</I18nProvider>
|
||||
</LanguageContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
// Expose locale + setLocale for the LanguageTab
|
||||
import { createContext, useContext } from "solid-js"
|
||||
|
||||
interface LanguageContextValue {
|
||||
locale: Accessor<Locale>
|
||||
setLocale: (locale: Locale | "") => void
|
||||
userOverride: Accessor<Locale | "">
|
||||
}
|
||||
|
||||
const LanguageContext = createContext<LanguageContextValue>()
|
||||
|
||||
export function useLanguage() {
|
||||
const ctx = useContext(LanguageContext)
|
||||
if (!ctx) {
|
||||
throw new Error("useLanguage must be used within a LanguageProvider")
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ interface ServerContextValue {
|
||||
profileData: Accessor<ProfileData | null>
|
||||
deviceAuth: Accessor<DeviceAuthState>
|
||||
startLogin: () => void
|
||||
vscodeLanguage: Accessor<string | undefined>
|
||||
languageOverride: Accessor<string | undefined>
|
||||
}
|
||||
|
||||
const ServerContext = createContext<ServerContextValue>()
|
||||
@@ -29,6 +31,8 @@ export const ServerProvider: ParentComponent = (props) => {
|
||||
const [error, setError] = createSignal<string | undefined>()
|
||||
const [profileData, setProfileData] = createSignal<ProfileData | null>(null)
|
||||
const [deviceAuth, setDeviceAuth] = createSignal<DeviceAuthState>(initialDeviceAuth)
|
||||
const [vscodeLanguage, setVscodeLanguage] = createSignal<string | undefined>()
|
||||
const [languageOverride, setLanguageOverride] = createSignal<string | undefined>()
|
||||
|
||||
onMount(() => {
|
||||
const unsubscribe = vscode.onMessage((message: ExtensionMessage) => {
|
||||
@@ -38,6 +42,12 @@ export const ServerProvider: ParentComponent = (props) => {
|
||||
setServerInfo(message.serverInfo)
|
||||
setConnectionState("connected")
|
||||
setError(undefined)
|
||||
if (message.vscodeLanguage) {
|
||||
setVscodeLanguage(message.vscodeLanguage)
|
||||
}
|
||||
if (message.languageOverride) {
|
||||
setLanguageOverride(message.languageOverride)
|
||||
}
|
||||
break
|
||||
|
||||
case "connectionState":
|
||||
@@ -110,6 +120,8 @@ export const ServerProvider: ParentComponent = (props) => {
|
||||
profileData,
|
||||
deviceAuth,
|
||||
startLogin,
|
||||
vscodeLanguage,
|
||||
languageOverride,
|
||||
}
|
||||
|
||||
return <ServerContext.Provider value={value}>{props.children}</ServerContext.Provider>
|
||||
|
||||
@@ -171,6 +171,8 @@ export interface ModelSelection {
|
||||
export interface ReadyMessage {
|
||||
type: "ready"
|
||||
serverInfo?: ServerInfo
|
||||
vscodeLanguage?: string
|
||||
languageOverride?: string
|
||||
}
|
||||
|
||||
export interface ConnectionStateMessage {
|
||||
@@ -367,6 +369,11 @@ export interface RequestAgentsMessage {
|
||||
type: "requestAgents"
|
||||
}
|
||||
|
||||
export interface SetLanguageRequest {
|
||||
type: "setLanguage"
|
||||
locale: string
|
||||
}
|
||||
|
||||
export type WebviewMessage =
|
||||
| SendMessageRequest
|
||||
| AbortRequest
|
||||
@@ -382,6 +389,7 @@ export type WebviewMessage =
|
||||
| WebviewReadyRequest
|
||||
| RequestProvidersMessage
|
||||
| RequestAgentsMessage
|
||||
| SetLanguageRequest
|
||||
|
||||
// ============================================
|
||||
// VS Code API type
|
||||
|
||||
Reference in New Issue
Block a user