diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index 4420af92..04e3394b 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -1,15 +1,12 @@ -import { Component, For, Show, createMemo, createEffect, createSignal, onMount, onCleanup } from "solid-js" +import { Component, For, Show, Suspense, createMemo, createEffect, createSignal, lazy, onMount, onCleanup } from "solid-js" import { Dialog } from "@kobalte/core/dialog" import { Toaster } from "solid-toast" import useMediaQuery from "@suid/material/useMediaQuery" import { Minimize2 } from "lucide-solid" import AlertDialog from "./components/alert-dialog" -import FolderSelectionView from "./components/folder-selection-view" import { showConfirmDialog } from "./stores/alerts" import InstanceTabs from "./components/instance-tabs" -import InstanceDisconnectedModal from "./components/instance-disconnected-modal" import InstanceShell from "./components/instance/instance-shell2" -import { SettingsScreen } from "./components/settings-screen" import { InstanceMetadataProvider } from "./lib/contexts/instance-metadata-context" import { initMarkdown } from "./lib/markdown" import { initGithubStars } from "./stores/github-stars" @@ -54,10 +51,16 @@ import { } from "./stores/sessions" import { getInstanceSessionIndicatorStatus } from "./stores/session-status" -import { openSettings } from "./stores/settings-screen" +import { openSettings, settingsOpen } from "./stores/settings-screen" const log = getLogger("actions") +const LazyFolderSelectionView = lazy(() => import("./components/folder-selection-view")) +const LazyInstanceDisconnectedModal = lazy(() => import("./components/instance-disconnected-modal")) +const LazySettingsScreen = lazy(() => + import("./components/settings-screen").then((module) => ({ default: module.SettingsScreen })), +) + const App: Component = () => { const { isDark } = useTheme() const { t } = useI18n() @@ -409,12 +412,16 @@ const App: Component = () => { return ( <> - + + + + + @@ -527,29 +534,37 @@ const App: Component = () => { } > - + }> + +
- { - setShowFolderSelection(false) - clearLaunchError() - }} - /> + }> + { + setShowFolderSelection(false) + clearLaunchError() + }} + /> +
- - - + + + + + + + import("../instance-welcome-view")) +const LazyInfoView = lazy(() => import("../info-view")) +const LazyCommandPalette = lazy(() => import("../command-palette")) +const LazyBackgroundProcessOutputDialog = lazy(() => + import("../background-process-output-dialog").then((module) => ({ default: module.BackgroundProcessOutputDialog })), +) +const LazyPermissionApprovalModal = lazy(() => import("../permission-approval-modal")) + interface InstanceShellProps { instance: Instance // Provided by App-level instance tabs; lets us pause heavy rendering @@ -834,7 +839,9 @@ const InstanceShell2: Component = (props) => { } >
- + }> + +
@@ -850,30 +857,49 @@ const InstanceShell2: Component = (props) => { class="instance-shell2 flex flex-col flex-1 min-h-0" data-instance-id={props.instance.id} > - }> + }> + + + } + > {sessionLayout} - hideCommandPalette(props.instance.id)} - commands={instancePaletteCommands()} - onExecute={props.onExecuteCommand} - /> + + + hideCommandPalette(props.instance.id)} + commands={instancePaletteCommands()} + onExecute={props.onExecuteCommand} + /> + + - + + + + + - setPermissionModalOpen(false)} - /> + + + setPermissionModalOpen(false)} + /> + + ) } diff --git a/packages/ui/src/lib/i18n/index.tsx b/packages/ui/src/lib/i18n/index.tsx index a63e53f1..f9949de2 100644 --- a/packages/ui/src/lib/i18n/index.tsx +++ b/packages/ui/src/lib/i18n/index.tsx @@ -2,11 +2,6 @@ import { createContext, createEffect, createMemo, createSignal, onCleanup, onMou import type { ParentComponent } from "solid-js" import { useConfig } from "../../stores/preferences" import { enMessages } from "./messages/en" -import { esMessages } from "./messages/es" -import { frMessages } from "./messages/fr" -import { ruMessages } from "./messages/ru" -import { jaMessages } from "./messages/ja" -import { zhHansMessages } from "./messages/zh-Hans" type Messages = Record @@ -16,13 +11,16 @@ export type Locale = "en" | "es" | "fr" | "ru" | "ja" | "zh-Hans" const SUPPORTED_LOCALES: readonly Locale[] = ["en", "es", "fr", "ru", "ja", "zh-Hans"] as const -const messagesByLocale: Record = { - en: enMessages, - es: esMessages, - fr: frMessages, - ru: ruMessages, - ja: jaMessages, - "zh-Hans": zhHansMessages, +const localeMessagesCache = new Map([["en", enMessages]]) +const localeMessagesPromises = new Map>() + +const localeLoaders: Record Promise> = { + en: async () => enMessages, + es: async () => (await import("./messages/es")).esMessages, + fr: async () => (await import("./messages/fr")).frMessages, + ru: async () => (await import("./messages/ru")).ruMessages, + ja: async () => (await import("./messages/ja")).jaMessages, + "zh-Hans": async () => (await import("./messages/zh-Hans")).zhHansMessages, } function normalizeLocaleTag(value: string): string { @@ -84,8 +82,45 @@ function translateFrom(messages: Messages, key: string, params?: TranslateParams } const [globalRevision, setGlobalRevision] = createSignal(0) -const initialGlobalLocale: Locale = detectNavigatorLocale() ?? "en" -let globalMessages: Messages = messagesByLocale[initialGlobalLocale] +let globalMessages: Messages = enMessages + +function getMessagesForLocale(locale: Locale): Messages { + return localeMessagesCache.get(locale) ?? enMessages +} + +async function loadLocaleMessages(locale: Locale): Promise { + const cached = localeMessagesCache.get(locale) + if (cached) { + return cached + } + + const pending = localeMessagesPromises.get(locale) + if (pending) { + return pending + } + + const loader = localeLoaders[locale] + const promise = loader() + .then((messages) => { + localeMessagesCache.set(locale, messages) + localeMessagesPromises.delete(locale) + return messages + }) + .catch((error) => { + localeMessagesPromises.delete(locale) + throw error + }) + + localeMessagesPromises.set(locale, promise) + return promise +} + +export async function preloadLocaleMessages(preferredLocale?: string | null): Promise { + const resolvedLocale = matchSupportedLocale(preferredLocale ?? undefined) ?? detectNavigatorLocale() ?? "en" + globalMessages = await loadLocaleMessages(resolvedLocale) + setGlobalRevision((value) => value + 1) + return resolvedLocale +} export function tGlobal(key: string, params?: TranslateParams): string { globalRevision() @@ -102,8 +137,7 @@ const I18nContext = createContext() export const I18nProvider: ParentComponent = (props) => { const { preferences } = useConfig() const [detectedLocale, setDetectedLocale] = createSignal("en") - - const previousMessages = globalMessages + const [resolvedLocale, setResolvedLocale] = createSignal("en") onMount(() => { const detected = detectNavigatorLocale() @@ -115,19 +149,42 @@ export const I18nProvider: ParentComponent = (props) => { return configured ?? detectedLocale() ?? "en" }) - const messages = createMemo(() => messagesByLocale[locale()]) + const messages = createMemo(() => getMessagesForLocale(resolvedLocale())) function t(key: string, params?: TranslateParams): string { return translateFrom(messages(), key, params) } createEffect(() => { - globalMessages = messages() - setGlobalRevision((value) => value + 1) + const nextLocale = locale() + let cancelled = false + + void loadLocaleMessages(nextLocale) + .then((loadedMessages) => { + if (cancelled) { + return + } + localeMessagesCache.set(nextLocale, loadedMessages) + setResolvedLocale(nextLocale) + globalMessages = loadedMessages + setGlobalRevision((value) => value + 1) + }) + .catch(() => { + if (cancelled) { + return + } + setResolvedLocale("en") + globalMessages = enMessages + setGlobalRevision((value) => value + 1) + }) + + onCleanup(() => { + cancelled = true + }) }) onCleanup(() => { - globalMessages = previousMessages + globalMessages = enMessages setGlobalRevision((value) => value + 1) }) diff --git a/packages/ui/src/main.tsx b/packages/ui/src/main.tsx index c0a1b4ff..cc6c6f1e 100644 --- a/packages/ui/src/main.tsx +++ b/packages/ui/src/main.tsx @@ -4,7 +4,7 @@ import { ThemeProvider } from "./lib/theme" import { ConfigProvider } from "./stores/preferences" import { InstanceConfigProvider } from "./stores/instance-config" import { runtimeEnv } from "./lib/runtime-env" -import { I18nProvider } from "./lib/i18n" +import { I18nProvider, preloadLocaleMessages } from "./lib/i18n" import { storage } from "./lib/storage" import "./index.css" import "@git-diff-view/solid/styles/diff-view-pure.css" @@ -32,14 +32,18 @@ async function bootstrap() { try { const uiConfig = await storage.loadConfigOwner("ui") const theme = (uiConfig as any)?.theme ?? "system" + const locale = (uiConfig as any)?.settings?.locale if (theme === "system") { document.documentElement.removeAttribute("data-theme") } else { document.documentElement.setAttribute("data-theme", theme) } + + await preloadLocaleMessages(typeof locale === "string" ? locale : undefined) } catch { // If config fails to load, fall back to CSS defaults. + await preloadLocaleMessages(undefined) } }