From ed6e073ededb7b1a34079f60e88c989d8266342f Mon Sep 17 00:00:00 2001 From: Sun-sunshine06 Date: Sat, 18 Apr 2026 19:49:09 +0800 Subject: [PATCH 1/2] feat(desktop): surface language toggle Signed-off-by: Sun-sunshine06 --- apps/desktop/package.json | 1 + apps/desktop/src/main/index.ts | 2 + apps/desktop/src/preload/index.ts | 5 +++ apps/desktop/src/renderer/src/App.tsx | 4 +- .../src/components/LanguageToggle.tsx | 41 +++++++++++++++++++ .../src/renderer/src/components/TopBar.tsx | 18 ++++---- apps/desktop/src/renderer/src/main.tsx | 19 ++++++--- .../src/renderer/src/onboarding/index.tsx | 6 ++- pnpm-lock.yaml | 3 ++ 9 files changed, 85 insertions(+), 14 deletions(-) create mode 100644 apps/desktop/src/renderer/src/components/LanguageToggle.tsx diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 46fbf9e5..ea1a3729 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -16,6 +16,7 @@ "@open-codesign/artifacts": "workspace:*", "@open-codesign/core": "workspace:*", "@open-codesign/exporters": "workspace:*", + "@open-codesign/i18n": "workspace:*", "@open-codesign/providers": "workspace:*", "@open-codesign/runtime": "workspace:*", "@open-codesign/shared": "workspace:*", diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index 33a94563..7d2a2d29 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -9,6 +9,7 @@ import { autoUpdater } from 'electron-updater'; import { scanDesignSystem } from './design-system'; import { BrowserWindow, app, dialog, ipcMain, shell } from './electron-runtime'; import { registerExporterIpc } from './exporter-ipc'; +import { registerLocaleIpc } from './locale-ipc'; import { getLogPath, getLogger, initLogger } from './logger'; import { getApiKeyForProvider, @@ -254,6 +255,7 @@ void app.whenReady().then(async () => { initLogger(); await loadConfigOnBoot(); registerIpcHandlers(); + registerLocaleIpc(); registerOnboardingIpc(); registerExporterIpc(() => mainWindow); setupAutoUpdater(); diff --git a/apps/desktop/src/preload/index.ts b/apps/desktop/src/preload/index.ts index b55abefa..ca3179b1 100644 --- a/apps/desktop/src/preload/index.ts +++ b/apps/desktop/src/preload/index.ts @@ -52,6 +52,11 @@ const api = { ipcRenderer.invoke('codesign:clear-design-system') as Promise, export: (payload: { format: ExportFormat; htmlContent: string; defaultFilename?: string }) => ipcRenderer.invoke('codesign:export', payload) as Promise, + locale: { + getSystem: () => ipcRenderer.invoke('locale:get-system') as Promise, + getCurrent: () => ipcRenderer.invoke('locale:get-current') as Promise, + set: (locale: string) => ipcRenderer.invoke('locale:set', locale) as Promise, + }, checkForUpdates: () => ipcRenderer.invoke('codesign:check-for-updates'), downloadUpdate: () => ipcRenderer.invoke('codesign:download-update'), installUpdate: () => ipcRenderer.invoke('codesign:install-update'), diff --git a/apps/desktop/src/renderer/src/App.tsx b/apps/desktop/src/renderer/src/App.tsx index 7259f8ad..d7458eed 100644 --- a/apps/desktop/src/renderer/src/App.tsx +++ b/apps/desktop/src/renderer/src/App.tsx @@ -1,3 +1,4 @@ +import { useT } from '@open-codesign/i18n'; import { useEffect, useMemo, useState } from 'react'; import { CommandPalette } from './components/CommandPalette'; import { PreviewPane } from './components/PreviewPane'; @@ -10,6 +11,7 @@ import { Onboarding } from './onboarding'; import { useCodesignStore } from './store'; export function App() { + const t = useT(); const config = useCodesignStore((s) => s.config); const configLoaded = useCodesignStore((s) => s.configLoaded); const loadConfig = useCodesignStore((s) => s.loadConfig); @@ -90,7 +92,7 @@ export function App() { if (!configLoaded) { return (
- Loading… + {t('common.loading')}
); } diff --git a/apps/desktop/src/renderer/src/components/LanguageToggle.tsx b/apps/desktop/src/renderer/src/components/LanguageToggle.tsx new file mode 100644 index 00000000..1c907277 --- /dev/null +++ b/apps/desktop/src/renderer/src/components/LanguageToggle.tsx @@ -0,0 +1,41 @@ +import { setLocale as applyLocale, getCurrentLocale, useT } from '@open-codesign/i18n'; +import type { Locale } from '@open-codesign/i18n'; +import { Globe } from 'lucide-react'; +import { useEffect, useState } from 'react'; + +function nextLocale(locale: Locale): Locale { + return locale === 'en' ? 'zh-CN' : 'en'; +} + +function localeLabel(locale: Locale): string { + return locale === 'zh-CN' ? 'ZH' : 'EN'; +} + +export function LanguageToggle() { + const t = useT(); + const [locale, setLocaleState] = useState(getCurrentLocale()); + + useEffect(() => { + setLocaleState(getCurrentLocale()); + }, []); + + async function handleToggle(): Promise { + const target = nextLocale(locale); + const persisted = window.codesign ? await window.codesign.locale.set(target) : target; + const applied = await applyLocale(persisted); + setLocaleState(applied); + } + + return ( + + ); +} diff --git a/apps/desktop/src/renderer/src/components/TopBar.tsx b/apps/desktop/src/renderer/src/components/TopBar.tsx index bed8a175..9abc3077 100644 --- a/apps/desktop/src/renderer/src/components/TopBar.tsx +++ b/apps/desktop/src/renderer/src/components/TopBar.tsx @@ -1,23 +1,26 @@ +import { useT } from '@open-codesign/i18n'; import { IconButton, Tooltip, Wordmark } from '@open-codesign/ui'; import { Command, Settings as SettingsIcon } from 'lucide-react'; import type { CSSProperties } from 'react'; import { useCodesignStore } from '../store'; +import { LanguageToggle } from './LanguageToggle'; import { ThemeToggle } from './ThemeToggle'; const dragStyle = { WebkitAppRegion: 'drag' } as CSSProperties; const noDragStyle = { WebkitAppRegion: 'no-drag' } as CSSProperties; export function TopBar() { + const t = useT(); const previewHtml = useCodesignStore((s) => s.previewHtml); const isGenerating = useCodesignStore((s) => s.isGenerating); const errorMessage = useCodesignStore((s) => s.errorMessage); const openSettings = useCodesignStore((s) => s.openSettings); const openCommandPalette = useCodesignStore((s) => s.openCommandPalette); - let crumb = 'Untitled design'; + let crumb = t('preview.noDesign'); if (errorMessage) crumb = 'Error'; - else if (isGenerating) crumb = 'Generating…'; - else if (previewHtml) crumb = 'Preview ready'; + else if (isGenerating) crumb = t('preview.loading.title'); + else if (previewHtml) crumb = t('preview.ready'); return (
- + / {crumb} @@ -33,14 +36,15 @@ export function TopBar() {
- + + - - + + diff --git a/apps/desktop/src/renderer/src/main.tsx b/apps/desktop/src/renderer/src/main.tsx index c4f2b183..e9c1867f 100644 --- a/apps/desktop/src/renderer/src/main.tsx +++ b/apps/desktop/src/renderer/src/main.tsx @@ -1,3 +1,4 @@ +import { initI18n } from '@open-codesign/i18n'; import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import { App } from './App'; @@ -5,9 +6,17 @@ import './index.css'; const container = document.getElementById('root'); if (!container) throw new Error('Root element #root not found'); +const root = createRoot(container); -createRoot(container).render( - - - , -); +async function bootstrap(): Promise { + const locale = window.codesign ? await window.codesign.locale.getCurrent() : undefined; + await initI18n(locale); + + root.render( + + + , + ); +} + +void bootstrap(); diff --git a/apps/desktop/src/renderer/src/onboarding/index.tsx b/apps/desktop/src/renderer/src/onboarding/index.tsx index fee392e4..04776131 100644 --- a/apps/desktop/src/renderer/src/onboarding/index.tsx +++ b/apps/desktop/src/renderer/src/onboarding/index.tsx @@ -1,6 +1,7 @@ import { PROVIDER_SHORTLIST, type SupportedOnboardingProvider } from '@open-codesign/shared'; import { Wordmark } from '@open-codesign/ui'; import { useState } from 'react'; +import { LanguageToggle } from '../components/LanguageToggle'; import { useCodesignStore } from '../store'; import { ChooseModel } from './ChooseModel'; import { PasteKey } from './PasteKey'; @@ -65,7 +66,10 @@ export function Onboarding() {
- +
+ + +
{step === 'welcome' ? ( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e15fcba9..2ab9dd75 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,9 @@ importers: '@open-codesign/exporters': specifier: workspace:* version: link:../../packages/exporters + '@open-codesign/i18n': + specifier: workspace:* + version: link:../../packages/i18n '@open-codesign/providers': specifier: workspace:* version: link:../../packages/providers From bca121ff87eeb3a96cb9220204cb2511a33dd853 Mon Sep 17 00:00:00 2001 From: Sun-sunshine06 Date: Sat, 18 Apr 2026 20:24:39 +0800 Subject: [PATCH 2/2] feat(desktop): localize workspace chrome Signed-off-by: Sun-sunshine06 --- apps/desktop/src/main/index.ts | 1 + .../src/components/CanvasErrorBar.tsx | 16 +--- .../src/components/CommandPalette.tsx | 58 +++++------ .../src/components/InlineCommentComposer.tsx | 15 +-- .../renderer/src/components/PreviewPane.tsx | 4 +- .../src/components/PreviewToolbar.tsx | 43 ++++++--- .../src/renderer/src/components/Sidebar.tsx | 35 +++---- .../src/renderer/src/components/TopBar.tsx | 8 +- .../src/renderer/src/preview/EmptyState.tsx | 38 +++----- .../src/renderer/src/preview/ErrorState.tsx | 12 ++- apps/desktop/src/renderer/src/store.ts | 57 +++++++---- packages/i18n/src/locales/en.json | 96 +++++++++++++++++-- packages/i18n/src/locales/zh-CN.json | 96 +++++++++++++++++-- 13 files changed, 335 insertions(+), 144 deletions(-) diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index 7d2a2d29..c694d8ee 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -33,6 +33,7 @@ function createWindow(): void { height: 820, minWidth: 960, minHeight: 640, + autoHideMenuBar: process.platform !== 'darwin', titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default', backgroundColor: BRAND.backgroundColor, show: false, diff --git a/apps/desktop/src/renderer/src/components/CanvasErrorBar.tsx b/apps/desktop/src/renderer/src/components/CanvasErrorBar.tsx index 5a7342e6..69a6d599 100644 --- a/apps/desktop/src/renderer/src/components/CanvasErrorBar.tsx +++ b/apps/desktop/src/renderer/src/components/CanvasErrorBar.tsx @@ -1,16 +1,9 @@ -/** - * CanvasErrorBar — slim red strip shown above the preview iframe when the - * sandbox runtime postMessages an IFRAME_ERROR. - * - * Loud surface (PRINCIPLES §10): every JS exception thrown inside the - * generated HTML lands here with file + line. Users can dismiss the bar - * (it clears the store slice) but errors are never auto-hidden. - */ - +import { useT } from '@open-codesign/i18n'; import { X } from 'lucide-react'; import { useCodesignStore } from '../store'; export function CanvasErrorBar() { + const t = useT(); const errors = useCodesignStore((s) => s.iframeErrors); const clear = useCodesignStore((s) => s.clearIframeErrors); if (errors.length === 0) return null; @@ -24,7 +17,8 @@ export function CanvasErrorBar() {
- Preview runtime error{errors.length > 1 ? ` (${errors.length})` : ''} + {t('preview.runtimeError')} + {errors.length > 1 ? ` (${errors.length})` : ''}
{latest} @@ -33,7 +27,7 @@ export function CanvasErrorBar() { diff --git a/apps/desktop/src/renderer/src/components/InlineCommentComposer.tsx b/apps/desktop/src/renderer/src/components/InlineCommentComposer.tsx index 007b4183..9ef56dea 100644 --- a/apps/desktop/src/renderer/src/components/InlineCommentComposer.tsx +++ b/apps/desktop/src/renderer/src/components/InlineCommentComposer.tsx @@ -1,3 +1,4 @@ +import { useT } from '@open-codesign/i18n'; import type { SelectedElement } from '@open-codesign/shared'; import { MessageSquareText, X } from 'lucide-react'; import { useState } from 'react'; @@ -16,6 +17,7 @@ interface InlineCommentComposerCardProps { } function InlineCommentComposerCard({ selectedElement }: InlineCommentComposerCardProps) { + const t = useT(); const clearCanvasElement = useCodesignStore((s) => s.clearCanvasElement); const applyInlineComment = useCodesignStore((s) => s.applyInlineComment); const isGenerating = useCodesignStore((s) => s.isGenerating); @@ -27,7 +29,7 @@ function InlineCommentComposerCard({ selectedElement }: InlineCommentComposerCar
- Comment on {selectedElement.tag} + {t('inlineComment.title')} {selectedElement.tag}

@@ -48,13 +50,12 @@ function InlineCommentComposerCard({ selectedElement }: InlineCommentComposerCar

- Clicked elements stay selected in the canvas. Describe the visual or content change you - want, and open-codesign will rewrite the artifact around that target. + {t('inlineComment.description')}