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
32 changes: 31 additions & 1 deletion packages/desktop/src/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import TemplateModal from "./components/template-modal";
import WorkspacePicker from "./components/workspace-picker";
import CreateWorkspaceModal from "./components/create-workspace-modal";
import McpAuthModal from "./components/mcp-auth-modal";
import LanguagePickerModal from "./components/language-picker-modal";
import OnboardingView from "./pages/onboarding";
import DashboardView from "./pages/dashboard";
import SessionView from "./pages/session";
Expand Down Expand Up @@ -67,7 +68,7 @@ import {
groupMessageParts,
isTauriRuntime,
} from "./utils";
import { currentLocale, setLocale, t, type Language } from "../i18n";
import { currentLocale, setLocale, t, type Language, initLocale } from "../i18n";
import {
isWindowsPlatform,
lastUserModelFromMessages,
Expand Down Expand Up @@ -129,6 +130,7 @@ export default function App() {
const [rememberModeChoice, setRememberModeChoice] = createSignal(false);
const [tab, setTab] = createSignal<DashboardTab>("home");
const [themeMode, setThemeMode] = createSignal<ThemeMode>(getInitialThemeMode());
const [languagePickerOpen, setLanguagePickerOpen] = createSignal(false);

const [engineSource, setEngineSource] = createSignal<"path" | "sidecar">(
isTauriRuntime() ? "sidecar" : "path"
Expand Down Expand Up @@ -767,6 +769,19 @@ export default function App() {
setModelPickerOpen(true);
}

function openLanguagePicker() {
setLanguagePickerOpen(true);
}

function closeLanguagePicker() {
setLanguagePickerOpen(false);
}

function handleLanguageSelect(language: Language) {
setLocale(language);
setLanguagePickerOpen(false);
}

function applyModelSelection(next: ModelRef) {
if (modelPickerTarget() === "default") {
setDefaultModel(next);
Expand Down Expand Up @@ -1177,6 +1192,9 @@ export default function App() {


onMount(async () => {
// Initialize i18n locale from localStorage
initLocale();

const modePref = readModePreference();
if (modePref) {
setRememberModeChoice(true);
Expand Down Expand Up @@ -1712,6 +1730,11 @@ export default function App() {
onResetStartupPreference: () => clearModePreference(),
themeMode: themeMode(),
setThemeMode,
currentLanguage: currentLocale(),
languagePickerOpen: languagePickerOpen(),
openLanguagePicker,
closeLanguagePicker,
handleLanguageSelect,
pendingPermissions: pendingPermissions(),
events: events(),
safeStringify,
Expand Down Expand Up @@ -1873,6 +1896,13 @@ export default function App() {
onScopeChange={setTemplateDraftScope}
/>

<LanguagePickerModal
open={languagePickerOpen()}
currentLanguage={currentLocale()}
onSelect={handleLanguageSelect}
onClose={closeLanguagePicker}
/>

<WorkspacePicker
open={workspaceStore.workspacePickerOpen()}
workspaces={workspaceStore.filteredWorkspaces()}
Expand Down
102 changes: 58 additions & 44 deletions packages/desktop/src/app/components/language-picker-modal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { For, Show } from "solid-js";
import { CheckCircle2, Circle } from "lucide-solid";
import { CheckCircle2, Circle, X } from "lucide-solid";
import { LANGUAGE_OPTIONS, type Language, t, currentLocale } from "../../i18n";
import Button from "./button";

export type LanguagePickerModalProps = {
open: boolean;
Expand All @@ -10,55 +11,68 @@ export type LanguagePickerModalProps = {
};

export default function LanguagePickerModal(props: LanguagePickerModalProps) {
// Use reactive translation that updates when locale changes
const translate = (key: string) => t(key, currentLocale());

return (
<Show when={props.open}>
<div class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div class="bg-zinc-900 rounded-2xl p-6 w-full max-w-md border border-zinc-800 shadow-xl">
<h3 class="text-lg font-medium text-white mb-4">{translate("settings.language")}</h3>
<div class="fixed inset-0 z-50 bg-gray-1/60 backdrop-blur-sm flex items-center justify-center p-4">
<div class="bg-gray-2 border border-gray-6/70 w-full max-w-md rounded-2xl shadow-2xl overflow-hidden">
<div class="p-6">
<div class="flex items-start justify-between gap-4 mb-5">
<div>
<h3 class="text-lg font-semibold text-gray-12">{translate("settings.language")}</h3>
<p class="text-sm text-gray-11 mt-1">{translate("settings.language.description")}</p>
</div>
<Button variant="ghost" class="!p-2 rounded-full" onClick={props.onClose}>
<X size={16} />
</Button>
</div>

<div class="space-y-2">
<For each={LANGUAGE_OPTIONS}>
{(option) => (
<button
class={`w-full p-3 rounded-xl text-left transition-all ${
props.currentLanguage === option.value
? "bg-zinc-800 text-white border-2 border-zinc-700"
: "bg-zinc-950 text-zinc-400 hover:bg-zinc-900 border-2 border-transparent"
}`}
onClick={() => {
props.onSelect(option.value);
props.onClose();
}}
>
<div class="flex items-center justify-between gap-2">
<div class="flex-1">
<div class="font-medium text-sm">{option.nativeName}</div>
<Show when={option.label !== option.nativeName}>
<div class="text-xs text-zinc-500 mt-0.5">{option.label}</div>
</Show>
</div>
<div class="text-zinc-500">
<Show
when={props.currentLanguage === option.value}
fallback={<Circle size={14} />}
>
<CheckCircle2 size={14} class="text-emerald-400" />
</Show>
</div>
</div>
</button>
)}
</For>
</div>
<div class="space-y-2">
<For each={LANGUAGE_OPTIONS}>
{(option) => {
const isActive = () => props.currentLanguage === option.value;

return (
<button
class={`w-full text-left rounded-xl border px-4 py-3 transition-colors ${
isActive()
? "border-gray-6/20 bg-gray-12/5"
: "border-gray-6/70 bg-gray-1/40 hover:bg-gray-1/60"
}`}
onClick={() => {
props.onSelect(option.value);
}}
>
<div class="flex items-center justify-between gap-3">
<div class="flex-1 min-w-0">
<div class="font-medium text-sm text-gray-12">{option.nativeName}</div>
<Show when={option.label !== option.nativeName}>
<div class="text-xs text-gray-10 mt-0.5">{option.label}</div>
</Show>
</div>
<div class="text-gray-10 shrink-0">
<Show
when={isActive()}
fallback={<Circle size={14} />}
>
<CheckCircle2 size={14} class="text-green-11" />
</Show>
</div>
</div>
</button>
);
}}
</For>
</div>

<button
class="mt-4 w-full py-2 text-sm text-zinc-500 hover:text-white transition-colors"
onClick={props.onClose}
>
{translate("common.cancel")}
</button>
<div class="mt-5 flex justify-end">
<Button variant="outline" onClick={props.onClose}>
{translate("common.cancel")}
</Button>
</div>
</div>
</div>
</div>
</Show>
Expand Down
Loading