Unverified 提交 b1c4aa28 authored 作者: Will Chen's avatar Will Chen 提交者: GitHub

Add i18n internationalization support with language selector (#2450)

## Summary - Set up i18n infrastructure using i18next with locale files for English (chat, common, errors, home, settings namespaces) - Add LanguageSelector component to the settings page for users to switch languages - Add language preference field to the app schema and integrate i18next provider in the app layout and renderer ## Test plan - Verify the app builds and starts without errors - Navigate to Settings and confirm the Language Selector is visible and functional - Confirm English translations load correctly across all namespaces 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/dyad-sh/dyad/pull/2450"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end --> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Broad UI text refactor plus new runtime language switching could surface missing keys, incorrect namespaces, or layout regressions, though changes are largely non-functional and localized to presentation. > > **Overview** > Adds app-wide internationalization via `i18next`/`react-i18next`, including a new `src/i18n` initialization with bundled namespaces and locale resources. > > Introduces a persisted `language` setting (validated by `LanguageSchema`) plus a new `LanguageSelector` UI, and syncs the active i18n language at startup in `RootLayout`. > > Migrates many user-facing strings across chat, settings, integrations, dialogs, banners, and preview panel components to use `t()` translation keys (with interpolation/plurals) instead of hardcoded English, and adds an i18n design doc. Dependencies are updated in `package.json`/lockfile. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit a996131500b0f99ea766036084972f9863aca81d. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Add app-wide internationalization with i18next and a language selector in Settings. The chosen language persists and updates the UI instantly; ships with English, Simplified Chinese (zh-CN), and Brazilian Portuguese (pt-BR), aligned with the Linear issue, and migrates UI text across the app. - **New Features** - Initialize i18next/react-i18next with namespaces (common, settings, chat, home, errors) before render; sync to UserSettings.language on startup. - Add LanguageSelector in Settings → General showing only completed locales; saves validated values and switches UI language. - Include complete en, zh-CN, pt-BR translations, Intl-based date/number/relative-time helpers, and docs/i18n.md. - **Refactors** - Replace hardcoded strings with t() across 50+ components. - Validate settings.language via LanguageSchema.safeParse; defer changeLanguage to layout sync to avoid duplicates; add error handling for updateSettings; extract constants in formatRelativeTime. <sup>Written for commit b670b0489415ca966db1f70f0a1ad3111a455538. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: 's avatarClaude Opus 4.5 <noreply@anthropic.com> Co-authored-by: 's avatarclaude[bot] <41898282+claude[bot]@users.noreply.github.com>
上级 a6baaef1
...@@ -26,6 +26,8 @@ Make sure you run this once after doing `npm install` because it will make sure ...@@ -26,6 +26,8 @@ Make sure you run this once after doing `npm install` because it will make sure
npm run init-precommit npm run init-precommit
``` ```
**Note:** Running `npm install` may update `package-lock.json` with version changes or peer dependency flag removals. If rebasing or performing git operations, commit these changes first to avoid "unstaged changes" errors.
## Pre-commit checks ## Pre-commit checks
RUN THE FOLLOWING CHECKS before you do a commit. RUN THE FOLLOWING CHECKS before you do a commit.
......
差异被折叠。
...@@ -57,6 +57,7 @@ ...@@ -57,6 +57,7 @@
"geist": "^1.3.1", "geist": "^1.3.1",
"glob": "^11.0.2", "glob": "^11.0.2",
"html-to-image": "^1.11.13", "html-to-image": "^1.11.13",
"i18next": "^25.8.0",
"isomorphic-git": "^1.30.1", "isomorphic-git": "^1.30.1",
"jotai": "^2.12.2", "jotai": "^2.12.2",
"jsonrepair": "^3.13.1", "jsonrepair": "^3.13.1",
...@@ -70,6 +71,7 @@ ...@@ -70,6 +71,7 @@
"posthog-js": "^1.236.3", "posthog-js": "^1.236.3",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-i18next": "^16.5.4",
"react-konva": "^19.2.1", "react-konva": "^19.2.1",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"react-resizable-panels": "^2.1.7", "react-resizable-panels": "^2.1.7",
...@@ -14489,6 +14491,15 @@ ...@@ -14489,6 +14491,15 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/html-parse-stringify": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
"license": "MIT",
"dependencies": {
"void-elements": "3.1.0"
}
},
"node_modules/html-to-image": { "node_modules/html-to-image": {
"version": "1.11.13", "version": "1.11.13",
"resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz", "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz",
...@@ -14638,6 +14649,37 @@ ...@@ -14638,6 +14649,37 @@
"url": "https://github.com/sponsors/typicode" "url": "https://github.com/sponsors/typicode"
} }
}, },
"node_modules/i18next": {
"version": "25.8.0",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.8.0.tgz",
"integrity": "sha512-urrg4HMFFMQZ2bbKRK7IZ8/CTE7D8H4JRlAwqA2ZwDRFfdd0K/4cdbNNLgfn9mo+I/h9wJu61qJzH7jCFAhUZQ==",
"funding": [
{
"type": "individual",
"url": "https://locize.com"
},
{
"type": "individual",
"url": "https://locize.com/i18next.html"
},
{
"type": "individual",
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
}
],
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.28.4"
},
"peerDependencies": {
"typescript": "^5"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/iconv-lite": { "node_modules/iconv-lite": {
"version": "0.6.3", "version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
...@@ -19710,6 +19752,33 @@ ...@@ -19710,6 +19752,33 @@
"react": ">=16.13.1" "react": ">=16.13.1"
} }
}, },
"node_modules/react-i18next": {
"version": "16.5.4",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.4.tgz",
"integrity": "sha512-6yj+dcfMncEC21QPhOTsW8mOSO+pzFmT6uvU7XXdvM/Cp38zJkmTeMeKmTrmCMD5ToT79FmiE/mRWiYWcJYW4g==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.28.4",
"html-parse-stringify": "^3.0.1",
"use-sync-external-store": "^1.6.0"
},
"peerDependencies": {
"i18next": ">= 25.6.2",
"react": ">= 16.8.0",
"typescript": "^5"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
},
"typescript": {
"optional": true
}
}
},
"node_modules/react-is": { "node_modules/react-is": {
"version": "17.0.2", "version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
...@@ -22758,7 +22827,7 @@ ...@@ -22758,7 +22827,7 @@
"version": "5.9.2", "version": "5.9.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"dev": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
...@@ -23880,6 +23949,15 @@ ...@@ -23880,6 +23949,15 @@
} }
} }
}, },
"node_modules/void-elements": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/watchpack": { "node_modules/watchpack": {
"version": "2.5.1", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz",
......
...@@ -94,6 +94,7 @@ ...@@ -94,6 +94,7 @@
"geist": "^1.3.1", "geist": "^1.3.1",
"glob": "^11.0.2", "glob": "^11.0.2",
"html-to-image": "^1.11.13", "html-to-image": "^1.11.13",
"i18next": "^25.8.0",
"isomorphic-git": "^1.30.1", "isomorphic-git": "^1.30.1",
"jotai": "^2.12.2", "jotai": "^2.12.2",
"jsonrepair": "^3.13.1", "jsonrepair": "^3.13.1",
...@@ -107,6 +108,7 @@ ...@@ -107,6 +108,7 @@
"posthog-js": "^1.236.3", "posthog-js": "^1.236.3",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-i18next": "^16.5.4",
"react-konva": "^19.2.1", "react-konva": "^19.2.1",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"react-resizable-panels": "^2.1.7", "react-resizable-panels": "^2.1.7",
......
...@@ -36,3 +36,8 @@ Actions performed using the default `GITHUB_TOKEN` (including labels added by `g ...@@ -36,3 +36,8 @@ Actions performed using the default `GITHUB_TOKEN` (including labels added by `g
```bash ```bash
gh api repos/dyad-sh/dyad/issues/{PR_NUMBER}/labels -f "labels[]=label-name" gh api repos/dyad-sh/dyad/issues/{PR_NUMBER}/labels -f "labels[]=label-name"
``` ```
## Rebase conflict resolution tips
- When resolving conflicts in i18n-related commits, watch for duplicate constant definitions that conflict with imports from `@/lib/schemas` (e.g., `DEFAULT_ZOOM_LEVEL`)
- If both sides of a conflict have valid imports/hooks, keep both and remove any duplicate constant redefinitions
...@@ -18,6 +18,8 @@ import { selectedComponentsPreviewAtom } from "@/atoms/previewAtoms"; ...@@ -18,6 +18,8 @@ import { selectedComponentsPreviewAtom } from "@/atoms/previewAtoms";
import { chatInputValueAtom } from "@/atoms/chatAtoms"; import { chatInputValueAtom } from "@/atoms/chatAtoms";
import { usePlanEvents } from "@/hooks/usePlanEvents"; import { usePlanEvents } from "@/hooks/usePlanEvents";
import { useZoomShortcuts } from "@/hooks/useZoomShortcuts"; import { useZoomShortcuts } from "@/hooks/useZoomShortcuts";
import i18n from "@/i18n";
import { LanguageSchema } from "@/lib/schemas";
export default function RootLayout({ children }: { children: ReactNode }) { export default function RootLayout({ children }: { children: ReactNode }) {
const { refreshAppIframe } = useRunApp(); const { refreshAppIframe } = useRunApp();
...@@ -62,6 +64,16 @@ export default function RootLayout({ children }: { children: ReactNode }) { ...@@ -62,6 +64,16 @@ export default function RootLayout({ children }: { children: ReactNode }) {
return () => {}; return () => {};
}, [settings?.zoomLevel]); }, [settings?.zoomLevel]);
// Sync i18n language with persisted user setting
useEffect(() => {
const parsed = LanguageSchema.safeParse(settings?.language);
const language = parsed.success ? parsed.data : "en";
if (i18n.language !== language) {
i18n.changeLanguage(language);
}
}, [settings?.language]);
// Global keyboard listener for refresh events // Global keyboard listener for refresh events
useEffect(() => { useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => { const handleKeyDown = (event: KeyboardEvent) => {
......
...@@ -2,6 +2,7 @@ import { useSettings } from "@/hooks/useSettings"; ...@@ -2,6 +2,7 @@ import { useSettings } from "@/hooks/useSettings";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { showInfo } from "@/lib/toast"; import { showInfo } from "@/lib/toast";
import { useTranslation } from "react-i18next";
export function AutoApproveSwitch({ export function AutoApproveSwitch({
showToast = true, showToast = true,
...@@ -9,6 +10,7 @@ export function AutoApproveSwitch({ ...@@ -9,6 +10,7 @@ export function AutoApproveSwitch({
showToast?: boolean; showToast?: boolean;
}) { }) {
const { settings, updateSettings } = useSettings(); const { settings, updateSettings } = useSettings();
const { t } = useTranslation("settings");
return ( return (
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Switch <Switch
...@@ -22,7 +24,7 @@ export function AutoApproveSwitch({ ...@@ -22,7 +24,7 @@ export function AutoApproveSwitch({
} }
}} }}
/> />
<Label htmlFor="auto-approve">Auto-approve</Label> <Label htmlFor="auto-approve">{t("workflow.autoApprove")}</Label>
</div> </div>
); );
} }
import { useSettings } from "@/hooks/useSettings"; import { useSettings } from "@/hooks/useSettings";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { useTranslation } from "react-i18next";
import { showInfo } from "@/lib/toast"; import { showInfo } from "@/lib/toast";
...@@ -10,6 +11,7 @@ export function AutoFixProblemsSwitch({ ...@@ -10,6 +11,7 @@ export function AutoFixProblemsSwitch({
showToast?: boolean; showToast?: boolean;
}) { }) {
const { settings, updateSettings } = useSettings(); const { settings, updateSettings } = useSettings();
const { t } = useTranslation("settings");
return ( return (
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Switch <Switch
...@@ -25,7 +27,7 @@ export function AutoFixProblemsSwitch({ ...@@ -25,7 +27,7 @@ export function AutoFixProblemsSwitch({
} }
}} }}
/> />
<Label htmlFor="auto-fix-problems">Auto-fix problems</Label> <Label htmlFor="auto-fix-problems">{t("workflow.autoFixProblems")}</Label>
</div> </div>
); );
} }
...@@ -3,9 +3,11 @@ import { Label } from "@/components/ui/label"; ...@@ -3,9 +3,11 @@ import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { toast } from "sonner"; import { toast } from "sonner";
import { ipc } from "@/ipc/types"; import { ipc } from "@/ipc/types";
import { useTranslation } from "react-i18next";
export function AutoUpdateSwitch() { export function AutoUpdateSwitch() {
const { settings, updateSettings } = useSettings(); const { settings, updateSettings } = useSettings();
const { t } = useTranslation("settings");
if (!settings) { if (!settings) {
return null; return null;
...@@ -31,7 +33,7 @@ export function AutoUpdateSwitch() { ...@@ -31,7 +33,7 @@ export function AutoUpdateSwitch() {
}); });
}} }}
/> />
<Label htmlFor="enable-auto-update">Auto-update</Label> <Label htmlFor="enable-auto-update">{t("general.autoUpdate")}</Label>
</div> </div>
); );
} }
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate, useRouterState } from "@tanstack/react-router"; import { useNavigate, useRouterState } from "@tanstack/react-router";
import { formatDistanceToNow } from "date-fns"; import { formatDistanceToNow } from "date-fns";
...@@ -34,6 +35,7 @@ import { ChatSearchDialog } from "./ChatSearchDialog"; ...@@ -34,6 +35,7 @@ import { ChatSearchDialog } from "./ChatSearchDialog";
import { useSelectChat } from "@/hooks/useSelectChat"; import { useSelectChat } from "@/hooks/useSelectChat";
export function ChatList({ show }: { show?: boolean }) { export function ChatList({ show }: { show?: boolean }) {
const { t } = useTranslation("chat");
const navigate = useNavigate(); const navigate = useNavigate();
const [selectedChatId, setSelectedChatId] = useAtom(selectedChatIdAtom); const [selectedChatId, setSelectedChatId] = useAtom(selectedChatIdAtom);
const [selectedAppId] = useAtom(selectedAppIdAtom); const [selectedAppId] = useAtom(selectedAppIdAtom);
...@@ -115,7 +117,7 @@ export function ChatList({ show }: { show?: boolean }) { ...@@ -115,7 +117,7 @@ export function ChatList({ show }: { show?: boolean }) {
await invalidateChats(); await invalidateChats();
} catch (error) { } catch (error) {
// DO A TOAST // DO A TOAST
showError(`Failed to create new chat: ${(error as any).toString()}`); showError(t("failedCreateChat", { error: (error as any).toString() }));
} }
} else { } else {
// If no app is selected, navigate to home page // If no app is selected, navigate to home page
...@@ -126,7 +128,7 @@ export function ChatList({ show }: { show?: boolean }) { ...@@ -126,7 +128,7 @@ export function ChatList({ show }: { show?: boolean }) {
const handleDeleteChat = async (chatId: number) => { const handleDeleteChat = async (chatId: number) => {
try { try {
await ipc.chat.deleteChat(chatId); await ipc.chat.deleteChat(chatId);
showSuccess("Chat deleted successfully"); showSuccess(t("chatDeleted"));
// If the deleted chat was selected, navigate to home // If the deleted chat was selected, navigate to home
if (selectedChatId === chatId) { if (selectedChatId === chatId) {
...@@ -137,7 +139,7 @@ export function ChatList({ show }: { show?: boolean }) { ...@@ -137,7 +139,7 @@ export function ChatList({ show }: { show?: boolean }) {
// Refresh the chat list // Refresh the chat list
await invalidateChats(); await invalidateChats();
} catch (error) { } catch (error) {
showError(`Failed to delete chat: ${(error as any).toString()}`); showError(t("failedDeleteChat", { error: (error as any).toString() }));
} }
}; };
...@@ -176,7 +178,7 @@ export function ChatList({ show }: { show?: boolean }) { ...@@ -176,7 +178,7 @@ export function ChatList({ show }: { show?: boolean }) {
className="overflow-y-auto h-[calc(100vh-112px)]" className="overflow-y-auto h-[calc(100vh-112px)]"
data-testid="chat-list-container" data-testid="chat-list-container"
> >
<SidebarGroupLabel>Recent Chats</SidebarGroupLabel> <SidebarGroupLabel>{t("recentChats")}</SidebarGroupLabel>
<SidebarGroupContent> <SidebarGroupContent>
<div className="flex flex-col space-y-4"> <div className="flex flex-col space-y-4">
<Button <Button
...@@ -185,7 +187,7 @@ export function ChatList({ show }: { show?: boolean }) { ...@@ -185,7 +187,7 @@ export function ChatList({ show }: { show?: boolean }) {
className="flex items-center justify-start gap-2 mx-2 py-3" className="flex items-center justify-start gap-2 mx-2 py-3"
> >
<PlusCircle size={16} /> <PlusCircle size={16} />
<span>New Chat</span> <span>{t("newChat")}</span>
</Button> </Button>
<Button <Button
onClick={() => setIsSearchDialogOpen(!isSearchDialogOpen)} onClick={() => setIsSearchDialogOpen(!isSearchDialogOpen)}
...@@ -194,16 +196,16 @@ export function ChatList({ show }: { show?: boolean }) { ...@@ -194,16 +196,16 @@ export function ChatList({ show }: { show?: boolean }) {
data-testid="search-chats-button" data-testid="search-chats-button"
> >
<Search size={16} /> <Search size={16} />
<span>Search chats</span> <span>{t("searchChats")}</span>
</Button> </Button>
{loading ? ( {loading ? (
<div className="py-3 px-4 text-sm text-gray-500"> <div className="py-3 px-4 text-sm text-gray-500">
Loading chats... {t("loadingChats")}
</div> </div>
) : chats.length === 0 ? ( ) : chats.length === 0 ? (
<div className="py-3 px-4 text-sm text-gray-500"> <div className="py-3 px-4 text-sm text-gray-500">
No chats found {t("noChatsFound")}
</div> </div>
) : ( ) : (
<SidebarMenu className="space-y-1"> <SidebarMenu className="space-y-1">
...@@ -226,7 +228,7 @@ export function ChatList({ show }: { show?: boolean }) { ...@@ -226,7 +228,7 @@ export function ChatList({ show }: { show?: boolean }) {
> >
<div className="flex flex-col w-full"> <div className="flex flex-col w-full">
<span className="truncate"> <span className="truncate">
{chat.title || "New Chat"} {chat.title || t("newChat")}
</span> </span>
<span className="text-xs text-gray-500"> <span className="text-xs text-gray-500">
{formatDistanceToNow(new Date(chat.createdAt), { {formatDistanceToNow(new Date(chat.createdAt), {
...@@ -262,19 +264,19 @@ export function ChatList({ show }: { show?: boolean }) { ...@@ -262,19 +264,19 @@ export function ChatList({ show }: { show?: boolean }) {
className="px-3 py-2" className="px-3 py-2"
> >
<Edit3 className="mr-2 h-4 w-4" /> <Edit3 className="mr-2 h-4 w-4" />
<span>Rename Chat</span> <span>{t("renameChat")}</span>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => onClick={() =>
handleDeleteChatClick( handleDeleteChatClick(
chat.id, chat.id,
chat.title || "New Chat", chat.title || t("newChat"),
) )
} }
className="px-3 py-2 text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-950/50 focus:bg-red-50 dark:focus:bg-red-950/50" className="px-3 py-2 text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-950/50 focus:bg-red-50 dark:focus:bg-red-950/50"
> >
<Trash2 className="mr-2 h-4 w-4" /> <Trash2 className="mr-2 h-4 w-4" />
<span>Delete Chat</span> <span>{t("deleteChat")}</span>
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
......
import { useState, useRef, useEffect, useCallback } from "react"; import { useState, useRef, useEffect, useCallback } from "react";
import { useTranslation } from "react-i18next";
import { useAtomValue, useSetAtom } from "jotai"; import { useAtomValue, useSetAtom } from "jotai";
import { import {
chatMessagesByIdAtom, chatMessagesByIdAtom,
...@@ -35,6 +36,7 @@ export function ChatPanel({ ...@@ -35,6 +36,7 @@ export function ChatPanel({
isPreviewOpen, isPreviewOpen,
onTogglePreview, onTogglePreview,
}: ChatPanelProps) { }: ChatPanelProps) {
const { t } = useTranslation("chat");
const messagesById = useAtomValue(chatMessagesByIdAtom); const messagesById = useAtomValue(chatMessagesByIdAtom);
const setMessagesById = useSetAtom(chatMessagesByIdAtom); const setMessagesById = useSetAtom(chatMessagesByIdAtom);
const [isVersionPaneOpen, setIsVersionPaneOpen] = useState(false); const [isVersionPaneOpen, setIsVersionPaneOpen] = useState(false);
...@@ -184,7 +186,7 @@ export function ChatPanel({ ...@@ -184,7 +186,7 @@ export function ChatPanel({
> >
<ArrowDown className="h-4 w-4" /> <ArrowDown className="h-4 w-4" />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>Scroll to bottom</TooltipContent> <TooltipContent>{t("scrollToBottom")}</TooltipContent>
</Tooltip> </Tooltip>
</div> </div>
)} )}
......
import { useTranslation } from "react-i18next";
import React from "react"; import React from "react";
import { import {
AlertDialog, AlertDialog,
...@@ -19,31 +20,25 @@ interface CommunityCodeConsentDialogProps { ...@@ -19,31 +20,25 @@ interface CommunityCodeConsentDialogProps {
export const CommunityCodeConsentDialog: React.FC< export const CommunityCodeConsentDialog: React.FC<
CommunityCodeConsentDialogProps CommunityCodeConsentDialogProps
> = ({ isOpen, onAccept, onCancel }) => { > = ({ isOpen, onAccept, onCancel }) => {
const { t } = useTranslation(["home", "common"]);
return ( return (
<AlertDialog open={isOpen} onOpenChange={(open) => !open && onCancel()}> <AlertDialog open={isOpen} onOpenChange={(open) => !open && onCancel()}>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Community Code Notice</AlertDialogTitle> <AlertDialogTitle>{t("home:communityCodeNotice")}</AlertDialogTitle>
<AlertDialogDescription className="space-y-3"> <AlertDialogDescription className="space-y-3">
<p> <p>{t("home:communityCodeWarning")}</p>
This code was created by a Dyad community member, not our core <p>{t("home:communityCodeRisk")}</p>
team. <p>{t("home:communityCodeReview")}</p>
</p>
<p>
Community code can be very helpful, but since it's built
independently, it may have bugs, security risks, or could cause
issues with your system. We can't provide official support if
problems occur.
</p>
<p>
We recommend reviewing the code on GitHub first. Only proceed if
you're comfortable with these risks.
</p>
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel onClick={onCancel}>Cancel</AlertDialogCancel> <AlertDialogCancel onClick={onCancel}>
<AlertDialogAction onClick={onAccept}>Accept</AlertDialogAction> {t("common:cancel")}
</AlertDialogCancel>
<AlertDialogAction onClick={onAccept}>
{t("common:accept")}
</AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
......
import { useTranslation } from "react-i18next";
import React, { useState } from "react"; import React, { useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
...@@ -33,6 +34,7 @@ export function CreateAppDialog({ ...@@ -33,6 +34,7 @@ export function CreateAppDialog({
onOpenChange, onOpenChange,
template, template,
}: CreateAppDialogProps) { }: CreateAppDialogProps) {
const { t } = useTranslation(["home", "common"]);
const setSelectedAppId = useSetAtom(selectedAppIdAtom); const setSelectedAppId = useSetAtom(selectedAppIdAtom);
const [appName, setAppName] = useState(""); const [appName, setAppName] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
...@@ -84,27 +86,27 @@ export function CreateAppDialog({ ...@@ -84,27 +86,27 @@ export function CreateAppDialog({
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[425px]"> <DialogContent className="sm:max-w-[425px]">
<DialogHeader> <DialogHeader>
<DialogTitle>Create New App</DialogTitle> <DialogTitle>{t("home:createNewApp")}</DialogTitle>
<DialogDescription> <DialogDescription>
{`Create a new app using the ${template?.title} template.`} {t("home:createAppUsingTemplate", { template: template?.title })}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className="grid gap-4 py-4"> <div className="grid gap-4 py-4">
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="appName">App Name</Label> <Label htmlFor="appName">{t("home:appName")}</Label>
<Input <Input
id="appName" id="appName"
value={appName} value={appName}
onChange={(e) => setAppName(e.target.value)} onChange={(e) => setAppName(e.target.value)}
placeholder="Enter app name..." placeholder={t("home:enterAppName")}
className={nameExists ? "border-red-500" : ""} className={nameExists ? "border-red-500" : ""}
disabled={isSubmitting} disabled={isSubmitting}
/> />
{nameExists && ( {nameExists && (
<p className="text-sm text-red-500"> <p className="text-sm text-red-500">
An app with this name already exists {t("home:appNameAlreadyExists")}
</p> </p>
)} )}
</div> </div>
...@@ -117,7 +119,7 @@ export function CreateAppDialog({ ...@@ -117,7 +119,7 @@ export function CreateAppDialog({
onClick={() => onOpenChange(false)} onClick={() => onOpenChange(false)}
disabled={isSubmitting} disabled={isSubmitting}
> >
Cancel {t("common:cancel")}
</Button> </Button>
<Button <Button
type="submit" type="submit"
...@@ -127,7 +129,7 @@ export function CreateAppDialog({ ...@@ -127,7 +129,7 @@ export function CreateAppDialog({
{isSubmitting && ( {isSubmitting && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="mr-2 h-4 w-4 animate-spin" />
)} )}
{isSubmitting ? "Creating..." : "Create App"} {isSubmitting ? t("common:creating") : t("home:createApp")}
</Button> </Button>
</DialogFooter> </DialogFooter>
</form> </form>
......
...@@ -9,10 +9,12 @@ import { ...@@ -9,10 +9,12 @@ import {
} from "@/components/ui/select"; } from "@/components/ui/select";
import type { ChatMode } from "@/lib/schemas"; import type { ChatMode } from "@/lib/schemas";
import { isDyadProEnabled, getEffectiveDefaultChatMode } from "@/lib/schemas"; import { isDyadProEnabled, getEffectiveDefaultChatMode } from "@/lib/schemas";
import { useTranslation } from "react-i18next";
export function DefaultChatModeSelector() { export function DefaultChatModeSelector() {
const { settings, updateSettings, envVars } = useSettings(); const { settings, updateSettings, envVars } = useSettings();
const { isQuotaExceeded, isLoading: isQuotaLoading } = useFreeAgentQuota(); const { isQuotaExceeded, isLoading: isQuotaLoading } = useFreeAgentQuota();
const { t } = useTranslation("settings");
if (!settings) { if (!settings) {
return null; return null;
...@@ -53,7 +55,7 @@ export function DefaultChatModeSelector() { ...@@ -53,7 +55,7 @@ export function DefaultChatModeSelector() {
htmlFor="default-chat-mode" htmlFor="default-chat-mode"
className="text-sm font-medium text-gray-700 dark:text-gray-300" className="text-sm font-medium text-gray-700 dark:text-gray-300"
> >
Default Chat Mode {t("workflow.defaultChatMode")}
</label> </label>
<Select <Select
value={effectiveDefault} value={effectiveDefault}
...@@ -89,7 +91,7 @@ export function DefaultChatModeSelector() { ...@@ -89,7 +91,7 @@ export function DefaultChatModeSelector() {
</Select> </Select>
</div> </div>
<div className="text-sm text-gray-500 dark:text-gray-400"> <div className="text-sm text-gray-500 dark:text-gray-400">
The chat mode used when creating new chats. {t("workflow.defaultChatModeDescription")}
</div> </div>
</div> </div>
); );
......
import { useTranslation } from "react-i18next";
import React from "react"; import React from "react";
import { Trash2, Loader2 } from "lucide-react"; import { Trash2, Loader2 } from "lucide-react";
import { import {
...@@ -28,6 +29,7 @@ export function DeleteConfirmationDialog({ ...@@ -28,6 +29,7 @@ export function DeleteConfirmationDialog({
trigger, trigger,
isDeleting = false, isDeleting = false,
}: DeleteConfirmationDialogProps) { }: DeleteConfirmationDialogProps) {
const { t } = useTranslation(["home", "common"]);
return ( return (
<AlertDialog> <AlertDialog>
{trigger ? ( {trigger ? (
...@@ -37,29 +39,32 @@ export function DeleteConfirmationDialog({ ...@@ -37,29 +39,32 @@ export function DeleteConfirmationDialog({
className={buttonVariants({ variant: "ghost", size: "icon" })} className={buttonVariants({ variant: "ghost", size: "icon" })}
data-testid="delete-prompt-button" data-testid="delete-prompt-button"
disabled={isDeleting} disabled={isDeleting}
title={`Delete ${itemType.toLowerCase()}`} title={`${t("common:delete")} ${itemType.toLowerCase()}`}
> >
<Trash2 className="h-4 w-4" /> <Trash2 className="h-4 w-4" />
</AlertDialogTrigger> </AlertDialogTrigger>
)} )}
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Delete {itemType}</AlertDialogTitle> <AlertDialogTitle>
{t("home:deleteItemTitle", { itemType })}
</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
Are you sure you want to delete "{itemName}"? This action cannot be {t("home:deleteItemConfirmation", { itemName })}
undone.
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel> <AlertDialogCancel disabled={isDeleting}>
{t("common:cancel")}
</AlertDialogCancel>
<AlertDialogAction onClick={onDelete} disabled={isDeleting}> <AlertDialogAction onClick={onDelete} disabled={isDeleting}>
{isDeleting ? ( {isDeleting ? (
<> <>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="mr-2 h-4 w-4 animate-spin" />
Deleting... {t("common:deleting")}
</> </>
) : ( ) : (
"Delete" t("common:delete")
)} )}
</AlertDialogAction> </AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
......
import { useTranslation } from "react-i18next";
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
...@@ -27,6 +28,7 @@ export function ForceCloseDialog({ ...@@ -27,6 +28,7 @@ export function ForceCloseDialog({
onClose, onClose,
performanceData, performanceData,
}: ForceCloseDialogProps) { }: ForceCloseDialogProps) {
const { t } = useTranslation(["home", "common"]);
const formatTimestamp = (timestamp: number) => { const formatTimestamp = (timestamp: number) => {
return new Date(timestamp).toLocaleString(); return new Date(timestamp).toLocaleString();
}; };
...@@ -37,19 +39,16 @@ export function ForceCloseDialog({ ...@@ -37,19 +39,16 @@ export function ForceCloseDialog({
<AlertDialogHeader> <AlertDialogHeader>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<AlertTriangle className="h-5 w-5 text-yellow-500" /> <AlertTriangle className="h-5 w-5 text-yellow-500" />
<AlertDialogTitle>Force Close Detected</AlertDialogTitle> <AlertDialogTitle>{t("home:forceCloseDetected")}</AlertDialogTitle>
</div> </div>
<AlertDialogDescription render={<div />}> <AlertDialogDescription render={<div />}>
<div className="space-y-4 pt-2 text-muted-foreground"> <div className="space-y-4 pt-2 text-muted-foreground">
<div className="text-base"> <div className="text-base">{t("home:forceCloseDescription")}</div>
The app was not closed properly the last time it was running.
This could indicate a crash or unexpected termination.
</div>
{performanceData && ( {performanceData && (
<div className="rounded-lg border bg-muted/50 p-4 space-y-3"> <div className="rounded-lg border bg-muted/50 p-4 space-y-3">
<div className="font-semibold text-sm text-foreground"> <div className="font-semibold text-sm text-foreground">
Last Known State:{" "} {t("home:lastKnownState")}{" "}
<span className="font-normal text-muted-foreground"> <span className="font-normal text-muted-foreground">
{formatTimestamp(performanceData.timestamp)} {formatTimestamp(performanceData.timestamp)}
</span> </span>
...@@ -59,18 +58,22 @@ export function ForceCloseDialog({ ...@@ -59,18 +58,22 @@ export function ForceCloseDialog({
{/* Process Metrics */} {/* Process Metrics */}
<div className="space-y-2"> <div className="space-y-2">
<div className="font-medium text-foreground"> <div className="font-medium text-foreground">
Process Metrics {t("home:processMetrics")}
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-muted-foreground">Memory:</span> <span className="text-muted-foreground">
{t("home:memory")}
</span>
<span className="font-mono"> <span className="font-mono">
{performanceData.memoryUsageMB} MB {performanceData.memoryUsageMB} MB
</span> </span>
</div> </div>
{performanceData.cpuUsagePercent !== undefined && ( {performanceData.cpuUsagePercent !== undefined && (
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-muted-foreground">CPU:</span> <span className="text-muted-foreground">
{t("home:cpu")}
</span>
<span className="font-mono"> <span className="font-mono">
{performanceData.cpuUsagePercent}% {performanceData.cpuUsagePercent}%
</span> </span>
...@@ -84,7 +87,7 @@ export function ForceCloseDialog({ ...@@ -84,7 +87,7 @@ export function ForceCloseDialog({
performanceData.systemCpuPercent !== undefined) && ( performanceData.systemCpuPercent !== undefined) && (
<div className="space-y-2"> <div className="space-y-2">
<div className="font-medium text-foreground"> <div className="font-medium text-foreground">
System Metrics {t("home:systemMetrics")}
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
{performanceData.systemMemoryUsageMB !== undefined && {performanceData.systemMemoryUsageMB !== undefined &&
...@@ -92,7 +95,7 @@ export function ForceCloseDialog({ ...@@ -92,7 +95,7 @@ export function ForceCloseDialog({
undefined && ( undefined && (
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-muted-foreground"> <span className="text-muted-foreground">
Memory: {t("home:memory")}
</span> </span>
<span className="font-mono"> <span className="font-mono">
{performanceData.systemMemoryUsageMB} /{" "} {performanceData.systemMemoryUsageMB} /{" "}
...@@ -103,7 +106,7 @@ export function ForceCloseDialog({ ...@@ -103,7 +106,7 @@ export function ForceCloseDialog({
{performanceData.systemCpuPercent !== undefined && ( {performanceData.systemCpuPercent !== undefined && (
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-muted-foreground"> <span className="text-muted-foreground">
CPU: {t("home:cpu")}
</span> </span>
<span className="font-mono"> <span className="font-mono">
{performanceData.systemCpuPercent}% {performanceData.systemCpuPercent}%
...@@ -120,7 +123,9 @@ export function ForceCloseDialog({ ...@@ -120,7 +123,9 @@ export function ForceCloseDialog({
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogAction onClick={onClose}>OK</AlertDialogAction> <AlertDialogAction onClick={onClose}>
{t("common:ok")}
</AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
......
import { useState, useEffect, useCallback, useRef } from "react"; import { useState, useEffect, useCallback, useRef } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Github, Github,
...@@ -75,6 +76,7 @@ function ConnectedGitHubConnector({ ...@@ -75,6 +76,7 @@ function ConnectedGitHubConnector({
triggerAutoSync, triggerAutoSync,
onAutoSyncComplete, onAutoSyncComplete,
}: ConnectedGitHubConnectorProps) { }: ConnectedGitHubConnectorProps) {
const { t } = useTranslation(["home", "common"]);
const [isSyncing, setIsSyncing] = useState(false); const [isSyncing, setIsSyncing] = useState(false);
const [syncError, setSyncError] = useState<string | null>(null); const [syncError, setSyncError] = useState<string | null>(null);
const [syncSuccess, setSyncSuccess] = useState<boolean>(false); const [syncSuccess, setSyncSuccess] = useState<boolean>(false);
...@@ -98,7 +100,9 @@ function ConnectedGitHubConnector({ ...@@ -98,7 +100,9 @@ function ConnectedGitHubConnector({
await ipc.github.disconnect({ appId }); await ipc.github.disconnect({ appId });
refreshApp(); refreshApp();
} catch (err: any) { } catch (err: any) {
setDisconnectError(err.message || "Failed to disconnect repository."); setDisconnectError(
err.message || t("integrations.github.failedDisconnectRepo"),
);
} finally { } finally {
setIsDisconnecting(false); setIsDisconnecting(false);
} }
......
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Github } from "lucide-react"; import { Github } from "lucide-react";
import { useSettings } from "@/hooks/useSettings"; import { useSettings } from "@/hooks/useSettings";
import { showSuccess, showError } from "@/lib/toast"; import { showSuccess, showError } from "@/lib/toast";
export function GitHubIntegration() { export function GitHubIntegration() {
const { t } = useTranslation(["home", "common"]);
const { settings, updateSettings } = useSettings(); const { settings, updateSettings } = useSettings();
const [isDisconnecting, setIsDisconnecting] = useState(false); const [isDisconnecting, setIsDisconnecting] = useState(false);
...@@ -16,14 +18,12 @@ export function GitHubIntegration() { ...@@ -16,14 +18,12 @@ export function GitHubIntegration() {
githubUser: undefined, githubUser: undefined,
}); });
if (result) { if (result) {
showSuccess("Successfully disconnected from GitHub"); showSuccess(t("integrations.github.disconnected"));
} else { } else {
showError("Failed to disconnect from GitHub"); showError(t("integrations.github.failedDisconnect"));
} }
} catch (err: any) { } catch (err: any) {
showError( showError(err.message || t("integrations.github.errorDisconnect"));
err.message || "An error occurred while disconnecting from GitHub",
);
} finally { } finally {
setIsDisconnecting(false); setIsDisconnecting(false);
} }
...@@ -39,10 +39,10 @@ export function GitHubIntegration() { ...@@ -39,10 +39,10 @@ export function GitHubIntegration() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300"> <h3 className="text-sm font-medium text-gray-700 dark:text-gray-300">
GitHub Integration {t("integrations.github.title")}
</h3> </h3>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1"> <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
Your account is connected to GitHub. {t("integrations.github.connected")}
</p> </p>
</div> </div>
...@@ -53,7 +53,9 @@ export function GitHubIntegration() { ...@@ -53,7 +53,9 @@ export function GitHubIntegration() {
disabled={isDisconnecting} disabled={isDisconnecting}
className="flex items-center gap-2" className="flex items-center gap-2"
> >
{isDisconnecting ? "Disconnecting..." : "Disconnect from GitHub"} {isDisconnecting
? t("common:disconnecting")
: t("integrations.github.disconnect")}
<Github className="h-4 w-4" /> <Github className="h-4 w-4" />
</Button> </Button>
</div> </div>
......
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useSettings } from "@/hooks/useSettings";
import { Language, LanguageSchema } from "@/lib/schemas";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
const DEFAULT_LANGUAGE: Language = "en";
/**
* Language labels shown in their native script so users can always
* find their language regardless of the current UI language.
* Only languages with completed translations are listed here.
*/
const LANGUAGE_OPTIONS: { value: Language; nativeLabel: string }[] = [
{ value: "en", nativeLabel: "English" },
{ value: "zh-CN", nativeLabel: "简体中文" },
{ value: "pt-BR", nativeLabel: "Português (Brasil)" },
// Additional languages will be added as translations are completed:
// { value: "ja", nativeLabel: "日本語" },
// { value: "ko", nativeLabel: "한국어" },
// { value: "es", nativeLabel: "Español" },
// { value: "fr", nativeLabel: "Français" },
// { value: "de", nativeLabel: "Deutsch" },
];
export function LanguageSelector() {
const { t } = useTranslation("settings");
const { settings, updateSettings } = useSettings();
const currentLanguage: Language = useMemo(() => {
const parsed = LanguageSchema.safeParse(settings?.language);
return parsed.success ? parsed.data : DEFAULT_LANGUAGE;
}, [settings?.language]);
const handleChange = async (value: Language | null) => {
if (!value) return;
try {
await updateSettings({ language: value });
// Language change is handled by the useEffect in layout.tsx
// after settings are successfully persisted
} catch (error) {
console.error("Failed to update language setting:", error);
// Settings update failed, so no language change will occur
}
};
return (
<div className="space-y-2">
<div className="flex flex-col gap-1">
<Label htmlFor="language">{t("general.language")}</Label>
<p className="text-sm text-muted-foreground">
{t("general.languageDescription")}
</p>
</div>
<Select value={currentLanguage} onValueChange={handleChange}>
<SelectTrigger id="language" className="w-[220px]">
<SelectValue placeholder="Select language" />
</SelectTrigger>
<SelectContent>
{LANGUAGE_OPTIONS.map((lang) => (
<SelectItem key={lang.value} value={lang.value}>
{lang.nativeLabel}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
}
...@@ -8,6 +8,7 @@ import { ...@@ -8,6 +8,7 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { MAX_CHAT_TURNS_IN_CONTEXT } from "@/constants/settings_constants"; import { MAX_CHAT_TURNS_IN_CONTEXT } from "@/constants/settings_constants";
import { useTranslation } from "react-i18next";
interface OptionInfo { interface OptionInfo {
value: string; value: string;
...@@ -49,6 +50,7 @@ const options: OptionInfo[] = [ ...@@ -49,6 +50,7 @@ const options: OptionInfo[] = [
export const MaxChatTurnsSelector: React.FC = () => { export const MaxChatTurnsSelector: React.FC = () => {
const { settings, updateSettings } = useSettings(); const { settings, updateSettings } = useSettings();
const { t } = useTranslation("settings");
const handleValueChange = (value: string) => { const handleValueChange = (value: string) => {
if (value === "default") { if (value === "default") {
...@@ -74,14 +76,14 @@ export const MaxChatTurnsSelector: React.FC = () => { ...@@ -74,14 +76,14 @@ export const MaxChatTurnsSelector: React.FC = () => {
htmlFor="max-chat-turns" htmlFor="max-chat-turns"
className="text-sm font-medium text-gray-700 dark:text-gray-300" className="text-sm font-medium text-gray-700 dark:text-gray-300"
> >
Maximum number of chat turns used in context {t("ai.maxChatTurns")}
</label> </label>
<Select <Select
value={currentValue} value={currentValue}
onValueChange={(v) => v && handleValueChange(v)} onValueChange={(v) => v && handleValueChange(v)}
> >
<SelectTrigger className="w-[180px]" id="max-chat-turns"> <SelectTrigger className="w-[180px]" id="max-chat-turns">
<SelectValue placeholder="Select turns" /> <SelectValue placeholder={t("ai.selectMaxChatTurns")} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{options.map((option) => ( {options.map((option) => (
......
import { useEffect } from "react"; import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { ipc } from "@/ipc/types"; import { ipc } from "@/ipc/types";
import { toast } from "sonner"; import { toast } from "sonner";
...@@ -10,6 +11,7 @@ import { useTheme } from "@/contexts/ThemeContext"; ...@@ -10,6 +11,7 @@ import { useTheme } from "@/contexts/ThemeContext";
import { NeonDisconnectButton } from "@/components/NeonDisconnectButton"; import { NeonDisconnectButton } from "@/components/NeonDisconnectButton";
export function NeonConnector() { export function NeonConnector() {
const { t } = useTranslation("home");
const { settings, refreshSettings } = useSettings(); const { settings, refreshSettings } = useSettings();
const { lastDeepLink, clearLastDeepLink } = useDeepLink(); const { lastDeepLink, clearLastDeepLink } = useDeepLink();
const { isDarkMode } = useTheme(); const { isDarkMode } = useTheme();
...@@ -18,7 +20,7 @@ export function NeonConnector() { ...@@ -18,7 +20,7 @@ export function NeonConnector() {
const handleDeepLink = async () => { const handleDeepLink = async () => {
if (lastDeepLink?.type === "neon-oauth-return") { if (lastDeepLink?.type === "neon-oauth-return") {
await refreshSettings(); await refreshSettings();
toast.success("Successfully connected to Neon!"); toast.success(t("integrations.neon.connectedSuccess"));
clearLastDeepLink(); clearLastDeepLink();
} }
}; };
...@@ -30,7 +32,9 @@ export function NeonConnector() { ...@@ -30,7 +32,9 @@ export function NeonConnector() {
<div className="flex flex-col space-y-4 p-4 border bg-white dark:bg-gray-800 max-w-100 rounded-md"> <div className="flex flex-col space-y-4 p-4 border bg-white dark:bg-gray-800 max-w-100 rounded-md">
<div className="flex flex-col items-start justify-between"> <div className="flex flex-col items-start justify-between">
<div className="flex items-center justify-between w-full"> <div className="flex items-center justify-between w-full">
<h2 className="text-lg font-medium pb-1">Neon Database</h2> <h2 className="text-lg font-medium pb-1">
{t("integrations.neon.database")}
</h2>
<Button <Button
variant="outline" variant="outline"
onClick={() => { onClick={() => {
...@@ -43,7 +47,7 @@ export function NeonConnector() { ...@@ -43,7 +47,7 @@ export function NeonConnector() {
</Button> </Button>
</div> </div>
<p className="text-sm text-gray-500 dark:text-gray-400 pb-3"> <p className="text-sm text-gray-500 dark:text-gray-400 pb-3">
You are connected to Neon Database {t("integrations.neon.connectedToNeon")}
</p> </p>
<NeonDisconnectButton /> <NeonDisconnectButton />
</div> </div>
...@@ -54,9 +58,11 @@ export function NeonConnector() { ...@@ -54,9 +58,11 @@ export function NeonConnector() {
return ( return (
<div className="flex flex-col space-y-4 p-4 border bg-white dark:bg-gray-800 max-w-100 rounded-md"> <div className="flex flex-col space-y-4 p-4 border bg-white dark:bg-gray-800 max-w-100 rounded-md">
<div className="flex flex-col items-start justify-between"> <div className="flex flex-col items-start justify-between">
<h2 className="text-lg font-medium pb-1">Neon Database</h2> <h2 className="text-lg font-medium pb-1">
{t("integrations.neon.database")}
</h2>
<p className="text-sm text-gray-500 dark:text-gray-400 pb-3"> <p className="text-sm text-gray-500 dark:text-gray-400 pb-3">
Neon Database has a good free tier with backups and up to 10 projects. {t("integrations.neon.freeTier")}
</p> </p>
<div <div
onClick={async () => { onClick={async () => {
...@@ -71,7 +77,7 @@ export function NeonConnector() { ...@@ -71,7 +77,7 @@ export function NeonConnector() {
className="w-auto h-10 cursor-pointer flex items-center justify-center px-4 py-2 rounded-md border-2 transition-colors font-medium text-sm dark:bg-gray-900 dark:border-gray-700" className="w-auto h-10 cursor-pointer flex items-center justify-center px-4 py-2 rounded-md border-2 transition-colors font-medium text-sm dark:bg-gray-900 dark:border-gray-700"
data-testid="connect-neon-button" data-testid="connect-neon-button"
> >
<span className="mr-2">Connect to</span> <span className="mr-2">{t("integrations.neon.connectTo")}</span>
<NeonSvg isDarkMode={isDarkMode} /> <NeonSvg isDarkMode={isDarkMode} />
</div> </div>
</div> </div>
......
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { toast } from "sonner"; import { toast } from "sonner";
import { useSettings } from "@/hooks/useSettings"; import { useSettings } from "@/hooks/useSettings";
...@@ -7,6 +8,7 @@ interface NeonDisconnectButtonProps { ...@@ -7,6 +8,7 @@ interface NeonDisconnectButtonProps {
} }
export function NeonDisconnectButton({ className }: NeonDisconnectButtonProps) { export function NeonDisconnectButton({ className }: NeonDisconnectButtonProps) {
const { t } = useTranslation("home");
const { updateSettings, settings } = useSettings(); const { updateSettings, settings } = useSettings();
const handleDisconnect = async () => { const handleDisconnect = async () => {
...@@ -14,10 +16,10 @@ export function NeonDisconnectButton({ className }: NeonDisconnectButtonProps) { ...@@ -14,10 +16,10 @@ export function NeonDisconnectButton({ className }: NeonDisconnectButtonProps) {
await updateSettings({ await updateSettings({
neon: undefined, neon: undefined,
}); });
toast.success("Disconnected from Neon successfully"); toast.success(t("integrations.neon.disconnected"));
} catch (error) { } catch (error) {
console.error("Failed to disconnect from Neon:", error); console.error("Failed to disconnect from Neon:", error);
toast.error("Failed to disconnect from Neon"); toast.error(t("integrations.neon.failedDisconnect"));
} }
}; };
...@@ -32,7 +34,7 @@ export function NeonDisconnectButton({ className }: NeonDisconnectButtonProps) { ...@@ -32,7 +34,7 @@ export function NeonDisconnectButton({ className }: NeonDisconnectButtonProps) {
className={className} className={className}
size="sm" size="sm"
> >
Disconnect from Neon {t("integrations.neon.disconnect")}
</Button> </Button>
); );
} }
import { useTranslation } from "react-i18next";
import { useSettings } from "@/hooks/useSettings"; import { useSettings } from "@/hooks/useSettings";
import { NeonDisconnectButton } from "@/components/NeonDisconnectButton"; import { NeonDisconnectButton } from "@/components/NeonDisconnectButton";
export function NeonIntegration() { export function NeonIntegration() {
const { t } = useTranslation("home");
const { settings } = useSettings(); const { settings } = useSettings();
const isConnected = !!settings?.neon?.accessToken; const isConnected = !!settings?.neon?.accessToken;
...@@ -14,10 +16,10 @@ export function NeonIntegration() { ...@@ -14,10 +16,10 @@ export function NeonIntegration() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300"> <h3 className="text-sm font-medium text-gray-700 dark:text-gray-300">
Neon Integration {t("integrations.neon.title")}
</h3> </h3>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1"> <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
Your account is connected to Neon. {t("integrations.neon.connected")}
</p> </p>
</div> </div>
......
...@@ -5,9 +5,11 @@ import { useSettings } from "@/hooks/useSettings"; ...@@ -5,9 +5,11 @@ import { useSettings } from "@/hooks/useSettings";
import { showError, showSuccess } from "@/lib/toast"; import { showError, showSuccess } from "@/lib/toast";
import { ipc } from "@/ipc/types"; import { ipc } from "@/ipc/types";
import { FolderOpen, RotateCcw, CheckCircle, AlertCircle } from "lucide-react"; import { FolderOpen, RotateCcw, CheckCircle, AlertCircle } from "lucide-react";
import { useTranslation } from "react-i18next";
export function NodePathSelector() { export function NodePathSelector() {
const { settings, updateSettings } = useSettings(); const { settings, updateSettings } = useSettings();
const { t } = useTranslation("settings");
const [isSelectingPath, setIsSelectingPath] = useState(false); const [isSelectingPath, setIsSelectingPath] = useState(false);
const [nodeStatus, setNodeStatus] = useState<{ const [nodeStatus, setNodeStatus] = useState<{
version: string | null; version: string | null;
...@@ -103,9 +105,7 @@ export function NodePathSelector() { ...@@ -103,9 +105,7 @@ export function NodePathSelector() {
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<div className="flex gap-2"> <div className="flex gap-2">
<Label className="text-sm font-medium"> <Label className="text-sm font-medium">{t("general.nodePath")}</Label>
Node.js Path Configuration
</Label>
<Button <Button
onClick={handleSelectNodePath} onClick={handleSelectNodePath}
...@@ -115,7 +115,9 @@ export function NodePathSelector() { ...@@ -115,7 +115,9 @@ export function NodePathSelector() {
className="flex items-center gap-2" className="flex items-center gap-2"
> >
<FolderOpen className="w-4 h-4" /> <FolderOpen className="w-4 h-4" />
{isSelectingPath ? "Selecting..." : "Browse for Node.js"} {isSelectingPath
? t("general.selecting")
: t("general.browseForNode")}
</Button> </Button>
{isCustomPath && ( {isCustomPath && (
...@@ -126,7 +128,7 @@ export function NodePathSelector() { ...@@ -126,7 +128,7 @@ export function NodePathSelector() {
className="flex items-center gap-2" className="flex items-center gap-2"
> >
<RotateCcw className="w-4 h-4" /> <RotateCcw className="w-4 h-4" />
Reset to Default {t("general.resetToDefault")}
</Button> </Button>
)} )}
</div> </div>
...@@ -135,7 +137,9 @@ export function NodePathSelector() { ...@@ -135,7 +137,9 @@ export function NodePathSelector() {
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<span className="text-xs font-medium text-gray-500 dark:text-gray-400"> <span className="text-xs font-medium text-gray-500 dark:text-gray-400">
{isCustomPath ? "Custom Path:" : "System PATH:"} {isCustomPath
? t("general.customPath")
: t("general.systemPath")}
</span> </span>
{isCustomPath && ( {isCustomPath && (
<span className="px-2 py-0.5 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded"> <span className="px-2 py-0.5 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded">
...@@ -160,7 +164,7 @@ export function NodePathSelector() { ...@@ -160,7 +164,7 @@ export function NodePathSelector() {
) : ( ) : (
<div className="flex items-center gap-1 text-yellow-600 dark:text-yellow-400"> <div className="flex items-center gap-1 text-yellow-600 dark:text-yellow-400">
<AlertCircle className="w-4 h-4" /> <AlertCircle className="w-4 h-4" />
<span className="text-xs">Not found</span> <span className="text-xs">{t("general.notFound")}</span>
</div> </div>
)} )}
</div> </div>
...@@ -170,13 +174,10 @@ export function NodePathSelector() { ...@@ -170,13 +174,10 @@ export function NodePathSelector() {
{/* Help Text */} {/* Help Text */}
<div className="text-sm text-gray-500 dark:text-gray-400"> <div className="text-sm text-gray-500 dark:text-gray-400">
{nodeStatus.isValid ? ( {nodeStatus.isValid ? (
<p>Node.js is properly configured and ready to use.</p> <p>{t("general.nodeConfigured")}</p>
) : ( ) : (
<> <>
<p> <p>{t("general.nodeSelectFolder")}</p>
Select the folder where Node.js is installed if it's not in your
system PATH.
</p>
</> </>
)} )}
</div> </div>
......
import { useTranslation } from "react-i18next";
// @ts-ignore // @ts-ignore
import openAiLogo from "../../assets/ai-logos/openai-logo.svg"; import openAiLogo from "../../assets/ai-logos/openai-logo.svg";
// @ts-ignore // @ts-ignore
...@@ -39,6 +40,7 @@ export function ProBanner() { ...@@ -39,6 +40,7 @@ export function ProBanner() {
} }
export function ManageDyadProButton({ className }: { className?: string }) { export function ManageDyadProButton({ className }: { className?: string }) {
const { t } = useTranslation("home");
return ( return (
<Button <Button
variant="outline" variant="outline"
...@@ -52,13 +54,14 @@ export function ManageDyadProButton({ className }: { className?: string }) { ...@@ -52,13 +54,14 @@ export function ManageDyadProButton({ className }: { className?: string }) {
}} }}
> >
<Wallet aria-hidden="true" className="w-5 h-5" /> <Wallet aria-hidden="true" className="w-5 h-5" />
Manage Dyad Pro {t("proBanner.manageDyadPro")}
<ArrowUpRight aria-hidden="true" className="w-5 h-5" /> <ArrowUpRight aria-hidden="true" className="w-5 h-5" />
</Button> </Button>
); );
} }
export function SetupDyadProButton() { export function SetupDyadProButton() {
const { t } = useTranslation("home");
return ( return (
<Button <Button
variant="outline" variant="outline"
...@@ -69,12 +72,13 @@ export function SetupDyadProButton() { ...@@ -69,12 +72,13 @@ export function SetupDyadProButton() {
}} }}
> >
<KeyRound aria-hidden="true" /> <KeyRound aria-hidden="true" />
Already have Dyad Pro? Add your key {t("proBanner.alreadyHavePro")}
</Button> </Button>
); );
} }
export function AiAccessBanner() { export function AiAccessBanner() {
const { t } = useTranslation("home");
return ( return (
<div <div
className="w-full py-2 sm:py-2.5 md:py-3 rounded-lg bg-gradient-to-br from-white via-indigo-50 to-sky-100 dark:from-indigo-700 dark:via-indigo-700 dark:to-indigo-900 flex items-center justify-center relative overflow-hidden ring-1 ring-inset ring-black/5 dark:ring-white/10 shadow-sm cursor-pointer transition-all duration-200 hover:shadow-md hover:-translate-y-[1px]" className="w-full py-2 sm:py-2.5 md:py-3 rounded-lg bg-gradient-to-br from-white via-indigo-50 to-sky-100 dark:from-indigo-700 dark:via-indigo-700 dark:to-indigo-900 flex items-center justify-center relative overflow-hidden ring-1 ring-inset ring-black/5 dark:ring-white/10 shadow-sm cursor-pointer transition-all duration-200 hover:shadow-md hover:-translate-y-[1px]"
...@@ -95,14 +99,14 @@ export function AiAccessBanner() { ...@@ -95,14 +99,14 @@ export function AiAccessBanner() {
<div className="relative z-10 text-center flex flex-col items-center gap-0.5 sm:gap-1 md:gap-1.5 px-4 md:px-6 pr-6 md:pr-8"> <div className="relative z-10 text-center flex flex-col items-center gap-0.5 sm:gap-1 md:gap-1.5 px-4 md:px-6 pr-6 md:pr-8">
<div className="mt-0.5 sm:mt-1 flex items-center gap-2 sm:gap-3 justify-center"> <div className="mt-0.5 sm:mt-1 flex items-center gap-2 sm:gap-3 justify-center">
<div className="text-xl font-semibold tracking-tight text-indigo-900 dark:text-indigo-100"> <div className="text-xl font-semibold tracking-tight text-indigo-900 dark:text-indigo-100">
Access leading AI models with one plan {t("proBanner.accessLeadingModels")}
</div> </div>
<button <button
type="button" type="button"
aria-label="Subscribe to Dyad Pro" aria-label="Subscribe to Dyad Pro"
className="inline-flex items-center rounded-md bg-white/90 text-indigo-800 hover:bg-white shadow px-3 py-1.5 text-xs sm:text-sm font-semibold focus:outline-none focus:ring-2 focus:ring-white/50" className="inline-flex items-center rounded-md bg-white/90 text-indigo-800 hover:bg-white shadow px-3 py-1.5 text-xs sm:text-sm font-semibold focus:outline-none focus:ring-2 focus:ring-white/50"
> >
Get Dyad Pro {t("proBanner.getDyadPro")}
</button> </button>
</div> </div>
...@@ -141,6 +145,7 @@ export function AiAccessBanner() { ...@@ -141,6 +145,7 @@ export function AiAccessBanner() {
} }
export function SmartContextBanner() { export function SmartContextBanner() {
const { t } = useTranslation("home");
return ( return (
<div <div
className="w-full py-2 sm:py-2.5 md:py-3 rounded-lg bg-gradient-to-br from-emerald-50 via-emerald-100 to-emerald-200 dark:from-emerald-700 dark:via-emerald-700 dark:to-emerald-900 flex items-center justify-center relative overflow-hidden ring-1 ring-inset ring-emerald-900/10 dark:ring-white/10 shadow-sm cursor-pointer transition-all duration-200 hover:shadow-md hover:-translate-y-[1px]" className="w-full py-2 sm:py-2.5 md:py-3 rounded-lg bg-gradient-to-br from-emerald-50 via-emerald-100 to-emerald-200 dark:from-emerald-700 dark:via-emerald-700 dark:to-emerald-900 flex items-center justify-center relative overflow-hidden ring-1 ring-inset ring-emerald-900/10 dark:ring-white/10 shadow-sm cursor-pointer transition-all duration-200 hover:shadow-md hover:-translate-y-[1px]"
...@@ -162,10 +167,10 @@ export function SmartContextBanner() { ...@@ -162,10 +167,10 @@ export function SmartContextBanner() {
<div className="mt-0.5 sm:mt-1 flex items-center gap-2 sm:gap-3 justify-center"> <div className="mt-0.5 sm:mt-1 flex items-center gap-2 sm:gap-3 justify-center">
<div className="flex flex-col items-center text-center"> <div className="flex flex-col items-center text-center">
<div className="text-xl font-semibold tracking-tight text-emerald-900 dark:text-emerald-100"> <div className="text-xl font-semibold tracking-tight text-emerald-900 dark:text-emerald-100">
Up to 3x cheaper {t("proBanner.upTo3xCheaper")}
</div> </div>
<div className="text-sm sm:text-base mt-1 text-emerald-700 dark:text-emerald-200/80"> <div className="text-sm sm:text-base mt-1 text-emerald-700 dark:text-emerald-200/80">
by using Smart Context {t("proBanner.byUsingSmartContext")}
</div> </div>
</div> </div>
<button <button
...@@ -173,7 +178,7 @@ export function SmartContextBanner() { ...@@ -173,7 +178,7 @@ export function SmartContextBanner() {
aria-label="Get Dyad Pro" aria-label="Get Dyad Pro"
className="inline-flex items-center rounded-md bg-white/90 text-emerald-800 hover:bg-white shadow px-3 py-1.5 text-xs sm:text-sm font-semibold focus:outline-none focus:ring-2 focus:ring-white/50" className="inline-flex items-center rounded-md bg-white/90 text-emerald-800 hover:bg-white shadow px-3 py-1.5 text-xs sm:text-sm font-semibold focus:outline-none focus:ring-2 focus:ring-white/50"
> >
Get Dyad Pro {t("proBanner.getDyadPro")}
</button> </button>
</div> </div>
</div> </div>
...@@ -182,6 +187,7 @@ export function SmartContextBanner() { ...@@ -182,6 +187,7 @@ export function SmartContextBanner() {
} }
export function TurboBanner() { export function TurboBanner() {
const { t } = useTranslation("home");
return ( return (
<div <div
className="w-full py-2 sm:py-2.5 md:py-3 rounded-lg bg-gradient-to-br from-rose-50 via-rose-100 to-rose-200 dark:from-rose-800 dark:via-fuchsia-800 dark:to-rose-800 flex items-center justify-center relative overflow-hidden ring-1 ring-inset ring-rose-900/10 dark:ring-white/5 shadow-sm cursor-pointer transition-all duration-200 hover:shadow-md hover:-translate-y-[1px]" className="w-full py-2 sm:py-2.5 md:py-3 rounded-lg bg-gradient-to-br from-rose-50 via-rose-100 to-rose-200 dark:from-rose-800 dark:via-fuchsia-800 dark:to-rose-800 flex items-center justify-center relative overflow-hidden ring-1 ring-inset ring-rose-900/10 dark:ring-white/5 shadow-sm cursor-pointer transition-all duration-200 hover:shadow-md hover:-translate-y-[1px]"
...@@ -203,10 +209,10 @@ export function TurboBanner() { ...@@ -203,10 +209,10 @@ export function TurboBanner() {
<div className="mt-0.5 sm:mt-1 flex items-center gap-2 sm:gap-3 justify-center"> <div className="mt-0.5 sm:mt-1 flex items-center gap-2 sm:gap-3 justify-center">
<div className="flex flex-col items-center text-center"> <div className="flex flex-col items-center text-center">
<div className="text-xl font-semibold tracking-tight text-rose-900 dark:text-rose-100"> <div className="text-xl font-semibold tracking-tight text-rose-900 dark:text-rose-100">
Generate code 4–10x faster {t("proBanner.generateCode4x")}
</div> </div>
<div className="text-sm sm:text-base mt-1 text-rose-700 dark:text-rose-200/80"> <div className="text-sm sm:text-base mt-1 text-rose-700 dark:text-rose-200/80">
with Turbo Models & Turbo Edits {t("proBanner.withTurboModels")}
</div> </div>
</div> </div>
<button <button
...@@ -214,7 +220,7 @@ export function TurboBanner() { ...@@ -214,7 +220,7 @@ export function TurboBanner() {
aria-label="Get Dyad Pro" aria-label="Get Dyad Pro"
className="inline-flex items-center rounded-md bg-white/90 text-rose-800 hover:bg-white shadow px-3 py-1.5 text-xs sm:text-sm font-semibold focus:outline-none focus:ring-2 focus:ring-white/50" className="inline-flex items-center rounded-md bg-white/90 text-rose-800 hover:bg-white shadow px-3 py-1.5 text-xs sm:text-sm font-semibold focus:outline-none focus:ring-2 focus:ring-white/50"
> >
Get Dyad Pro {t("proBanner.getDyadPro")}
</button> </button>
</div> </div>
</div> </div>
......
...@@ -15,6 +15,7 @@ import { Skeleton } from "./ui/skeleton"; ...@@ -15,6 +15,7 @@ import { Skeleton } from "./ui/skeleton";
import { Alert, AlertDescription, AlertTitle } from "./ui/alert"; import { Alert, AlertDescription, AlertTitle } from "./ui/alert";
import { AlertTriangle } from "lucide-react"; import { AlertTriangle } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
...@@ -37,6 +38,7 @@ import { CreateCustomProviderDialog } from "./CreateCustomProviderDialog"; ...@@ -37,6 +38,7 @@ import { CreateCustomProviderDialog } from "./CreateCustomProviderDialog";
export function ProviderSettingsGrid() { export function ProviderSettingsGrid() {
const navigate = useNavigate(); const navigate = useNavigate();
const { t } = useTranslation(["settings", "common"]);
const [isDialogOpen, setIsDialogOpen] = useState(false); const [isDialogOpen, setIsDialogOpen] = useState(false);
const [editingProvider, setEditingProvider] = const [editingProvider, setEditingProvider] =
useState<LanguageModelProvider | null>(null); useState<LanguageModelProvider | null>(null);
...@@ -75,7 +77,9 @@ export function ProviderSettingsGrid() { ...@@ -75,7 +77,9 @@ export function ProviderSettingsGrid() {
if (isLoading) { if (isLoading) {
return ( return (
<div className="p-6"> <div className="p-6">
<h2 className="text-lg font-medium mb-6">AI Providers</h2> <h2 className="text-lg font-medium mb-6">
{t("settings:ai.providers")}
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{[1, 2, 3, 4, 5].map((i) => ( {[1, 2, 3, 4, 5].map((i) => (
<Card key={i} className="border-border"> <Card key={i} className="border-border">
...@@ -93,12 +97,14 @@ export function ProviderSettingsGrid() { ...@@ -93,12 +97,14 @@ export function ProviderSettingsGrid() {
if (error) { if (error) {
return ( return (
<div className="p-6"> <div className="p-6">
<h2 className="text-lg font-medium mb-6">AI Providers</h2> <h2 className="text-lg font-medium mb-6">
{t("settings:ai.providers")}
</h2>
<Alert variant="destructive"> <Alert variant="destructive">
<AlertTriangle className="h-4 w-4" /> <AlertTriangle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle> <AlertTitle>{t("common:error")}</AlertTitle>
<AlertDescription> <AlertDescription>
Failed to load AI providers: {error.message} {t("settings:ai.failedToLoadProviders", { message: error.message })}
</AlertDescription> </AlertDescription>
</Alert> </Alert>
</div> </div>
...@@ -107,7 +113,7 @@ export function ProviderSettingsGrid() { ...@@ -107,7 +113,7 @@ export function ProviderSettingsGrid() {
return ( return (
<div className="p-6"> <div className="p-6">
<h2 className="text-lg font-medium mb-6">AI Providers</h2> <h2 className="text-lg font-medium mb-6">{t("settings:ai.providers")}</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{providers {providers
?.filter((p) => p.type !== "local") ?.filter((p) => p.type !== "local")
...@@ -142,7 +148,9 @@ export function ProviderSettingsGrid() { ...@@ -142,7 +148,9 @@ export function ProviderSettingsGrid() {
> >
<Edit className="h-4 w-4" /> <Edit className="h-4 w-4" />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>Edit Provider</TooltipContent> <TooltipContent>
{t("settings:ai.editProvider")}
</TooltipContent>
</Tooltip> </Tooltip>
<Tooltip> <Tooltip>
<TooltipTrigger <TooltipTrigger
...@@ -158,7 +166,9 @@ export function ProviderSettingsGrid() { ...@@ -158,7 +166,9 @@ export function ProviderSettingsGrid() {
> >
<Trash2 className="h-4 w-4" /> <Trash2 className="h-4 w-4" />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>Delete Provider</TooltipContent> <TooltipContent>
{t("settings:ai.deleteProvider")}
</TooltipContent>
</Tooltip> </Tooltip>
</div> </div>
)} )}
...@@ -166,11 +176,11 @@ export function ProviderSettingsGrid() { ...@@ -166,11 +176,11 @@ export function ProviderSettingsGrid() {
{provider.name} {provider.name}
{isProviderSetup(provider.id) ? ( {isProviderSetup(provider.id) ? (
<span className="ml-3 text-sm font-medium text-green-500 bg-green-50 dark:bg-green-900/30 border border-green-500/50 dark:border-green-500/50 px-2 py-1 rounded-full"> <span className="ml-3 text-sm font-medium text-green-500 bg-green-50 dark:bg-green-900/30 border border-green-500/50 dark:border-green-500/50 px-2 py-1 rounded-full">
Ready {t("common:ready")}
</span> </span>
) : ( ) : (
<span className="text-sm text-gray-500 bg-gray-50 dark:bg-gray-900 dark:text-gray-300 px-2 py-1 rounded-full"> <span className="text-sm text-gray-500 bg-gray-50 dark:bg-gray-900 dark:text-gray-300 px-2 py-1 rounded-full">
Needs Setup {t("common:needsSetup")}
</span> </span>
)} )}
</CardTitle> </CardTitle>
...@@ -178,7 +188,7 @@ export function ProviderSettingsGrid() { ...@@ -178,7 +188,7 @@ export function ProviderSettingsGrid() {
{provider.hasFreeTier && ( {provider.hasFreeTier && (
<span className="text-blue-600 mt-2 dark:text-blue-400 text-sm font-medium bg-blue-100 dark:bg-blue-900/30 px-2 py-1 rounded-full inline-flex items-center"> <span className="text-blue-600 mt-2 dark:text-blue-400 text-sm font-medium bg-blue-100 dark:bg-blue-900/30 px-2 py-1 rounded-full inline-flex items-center">
<GiftIcon className="w-4 h-4 mr-1" /> <GiftIcon className="w-4 h-4 mr-1" />
Free tier available {t("settings:ai.freeTierAvailable")}
</span> </span>
)} )}
</CardDescription> </CardDescription>
...@@ -195,10 +205,10 @@ export function ProviderSettingsGrid() { ...@@ -195,10 +205,10 @@ export function ProviderSettingsGrid() {
<CardHeader className="p-4 flex flex-col items-center justify-center h-full"> <CardHeader className="p-4 flex flex-col items-center justify-center h-full">
<PlusIcon className="h-8 w-8 text-muted-foreground mb-2" /> <PlusIcon className="h-8 w-8 text-muted-foreground mb-2" />
<CardTitle className="text-lg font-medium text-center"> <CardTitle className="text-lg font-medium text-center">
Add custom provider {t("settings:ai.addCustomProvider")}
</CardTitle> </CardTitle>
<CardDescription className="text-center"> <CardDescription className="text-center">
Connect to a custom LLM API endpoint {t("settings:ai.connectCustomEndpoint")}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
</Card> </Card>
...@@ -224,19 +234,24 @@ export function ProviderSettingsGrid() { ...@@ -224,19 +234,24 @@ export function ProviderSettingsGrid() {
> >
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Delete Custom Provider</AlertDialogTitle> <AlertDialogTitle>
{t("settings:ai.deleteCustomProvider")}
</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
This will permanently delete this custom provider and all its {t("settings:ai.deleteProviderConfirmation")}
associated models. This action cannot be undone.
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel> <AlertDialogCancel disabled={isDeleting}>
{t("common:cancel")}
</AlertDialogCancel>
<AlertDialogAction <AlertDialogAction
onClick={handleDeleteProvider} onClick={handleDeleteProvider}
disabled={isDeleting} disabled={isDeleting}
> >
{isDeleting ? "Deleting..." : "Delete Provider"} {isDeleting
? t("common:deleting")
: t("settings:ai.deleteProviderAction")}
</AlertDialogAction> </AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
......
...@@ -10,9 +10,11 @@ import { ...@@ -10,9 +10,11 @@ import {
import { toast } from "sonner"; import { toast } from "sonner";
import { ipc } from "@/ipc/types"; import { ipc } from "@/ipc/types";
import type { ReleaseChannel } from "@/lib/schemas"; import type { ReleaseChannel } from "@/lib/schemas";
import { useTranslation } from "react-i18next";
export function ReleaseChannelSelector() { export function ReleaseChannelSelector() {
const { settings, updateSettings } = useSettings(); const { settings, updateSettings } = useSettings();
const { t } = useTranslation("settings");
if (!settings) { if (!settings) {
return null; return null;
...@@ -52,7 +54,7 @@ export function ReleaseChannelSelector() { ...@@ -52,7 +54,7 @@ export function ReleaseChannelSelector() {
htmlFor="release-channel" htmlFor="release-channel"
className="text-sm font-medium text-gray-700 dark:text-gray-300" className="text-sm font-medium text-gray-700 dark:text-gray-300"
> >
Release Channel {t("general.releaseChannel")}
</label> </label>
<Select <Select
value={settings.releaseChannel} value={settings.releaseChannel}
...@@ -62,14 +64,13 @@ export function ReleaseChannelSelector() { ...@@ -62,14 +64,13 @@ export function ReleaseChannelSelector() {
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="stable">Stable</SelectItem> <SelectItem value="stable">{t("general.stable")}</SelectItem>
<SelectItem value="beta">Beta</SelectItem> <SelectItem value="beta">{t("general.beta")}</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<div className="text-sm text-gray-500 dark:text-gray-400"> <div className="text-sm text-gray-500 dark:text-gray-400">
<p>Stable is recommended for most users. </p> <p>{t("general.releaseChannelDescription")}</p>
<p>Beta receives more frequent updates but may have more bugs.</p>
</div> </div>
</div> </div>
); );
......
...@@ -9,9 +9,11 @@ import { ...@@ -9,9 +9,11 @@ import {
import { useSettings } from "@/hooks/useSettings"; import { useSettings } from "@/hooks/useSettings";
import { showError } from "@/lib/toast"; import { showError } from "@/lib/toast";
import { ipc } from "@/ipc/types"; import { ipc } from "@/ipc/types";
import { useTranslation } from "react-i18next";
export function RuntimeModeSelector() { export function RuntimeModeSelector() {
const { settings, updateSettings } = useSettings(); const { settings, updateSettings } = useSettings();
const { t } = useTranslation("settings");
if (!settings) { if (!settings) {
return null; return null;
...@@ -32,7 +34,7 @@ export function RuntimeModeSelector() { ...@@ -32,7 +34,7 @@ export function RuntimeModeSelector() {
<div className="space-y-1"> <div className="space-y-1">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Label className="text-sm font-medium" htmlFor="runtime-mode"> <Label className="text-sm font-medium" htmlFor="runtime-mode">
Runtime Mode {t("general.runtimeMode")}
</Label> </Label>
<Select <Select
value={settings.runtimeMode2 ?? "host"} value={settings.runtimeMode2 ?? "host"}
...@@ -48,8 +50,7 @@ export function RuntimeModeSelector() { ...@@ -48,8 +50,7 @@ export function RuntimeModeSelector() {
</Select> </Select>
</div> </div>
<div className="text-sm text-gray-500 dark:text-gray-400"> <div className="text-sm text-gray-500 dark:text-gray-400">
Choose whether to run apps directly on the local machine or in Docker {t("general.runtimeModeDescription")}
containers
</div> </div>
</div> </div>
{isDockerMode && ( {isDockerMode && (
......
import { useTranslation } from "react-i18next";
import { useNavigate } from "@tanstack/react-router"; import { useNavigate } from "@tanstack/react-router";
import { import {
ChevronRight, ChevronRight,
...@@ -45,6 +46,7 @@ type NodeInstallStep = ...@@ -45,6 +46,7 @@ type NodeInstallStep =
| "finished-checking"; | "finished-checking";
export function SetupBanner() { export function SetupBanner() {
const { t } = useTranslation("home");
const posthog = usePostHog(); const posthog = usePostHog();
const navigate = useNavigate(); const navigate = useNavigate();
const [isOnboardingVisible, setIsOnboardingVisible] = useState(true); const [isOnboardingVisible, setIsOnboardingVisible] = useState(true);
...@@ -157,7 +159,7 @@ export function SetupBanner() { ...@@ -157,7 +159,7 @@ export function SetupBanner() {
if (itemsNeedAction.length === 0) { if (itemsNeedAction.length === 0) {
return ( return (
<h1 className="text-center text-5xl font-bold mb-8 bg-clip-text text-transparent bg-gradient-to-r from-gray-900 to-gray-600 dark:from-gray-100 dark:to-gray-400 tracking-tight"> <h1 className="text-center text-5xl font-bold mb-8 bg-clip-text text-transparent bg-gradient-to-r from-gray-900 to-gray-600 dark:from-gray-100 dark:to-gray-400 tracking-tight">
Build a new app {t("setup.buildNewApp")}
</h1> </h1>
); );
} }
...@@ -181,7 +183,7 @@ export function SetupBanner() { ...@@ -181,7 +183,7 @@ export function SetupBanner() {
return ( return (
<> <>
<p className="text-xl font-medium text-zinc-700 dark:text-zinc-300 p-4 pt-6"> <p className="text-xl font-medium text-zinc-700 dark:text-zinc-300 p-4 pt-6">
Setup Dyad {t("setup.setupDyad")}
</p> </p>
<OnboardingBanner <OnboardingBanner
isVisible={isOnboardingVisible} isVisible={isOnboardingVisible}
...@@ -204,7 +206,7 @@ export function SetupBanner() { ...@@ -204,7 +206,7 @@ export function SetupBanner() {
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{getStatusIcon(isNodeSetupComplete, nodeCheckError)} {getStatusIcon(isNodeSetupComplete, nodeCheckError)}
<span className="font-medium text-sm"> <span className="font-medium text-sm">
1. Install Node.js (App Runtime) {t("setup.installNodeJs")}
</span> </span>
</div> </div>
</div> </div>
...@@ -212,26 +214,33 @@ export function SetupBanner() { ...@@ -212,26 +214,33 @@ export function SetupBanner() {
<AccordionContent className="px-4 pt-2 pb-4 bg-white dark:bg-zinc-900 border-t border-inherit"> <AccordionContent className="px-4 pt-2 pb-4 bg-white dark:bg-zinc-900 border-t border-inherit">
{nodeCheckError && ( {nodeCheckError && (
<p className="text-sm text-red-600 dark:text-red-400"> <p className="text-sm text-red-600 dark:text-red-400">
Error checking Node.js status. Try installing Node.js. {t("setup.errorCheckingNode")}
</p> </p>
)} )}
{isNodeSetupComplete ? ( {isNodeSetupComplete ? (
<p className="text-sm"> <p className="text-sm">
Node.js ({nodeSystemInfo!.nodeVersion}) installed.{" "} {t("setup.nodeInstalled", {
version: nodeSystemInfo!.nodeVersion,
})}{" "}
{nodeSystemInfo!.pnpmVersion && ( {nodeSystemInfo!.pnpmVersion && (
<span className="text-xs text-gray-500"> <span className="text-xs text-gray-500">
{" "} {" "}
(optional) pnpm ({nodeSystemInfo!.pnpmVersion}) installed. {t("setup.pnpmInstalled", {
version: nodeSystemInfo!.pnpmVersion,
})}
</span> </span>
)} )}
</p> </p>
) : ( ) : (
<div className="text-sm"> <div className="text-sm">
<p>Node.js is required to run apps locally.</p> <p>{t("setup.nodeRequired")}</p>
{nodeInstallStep === "waiting-for-continue" && ( {nodeInstallStep === "waiting-for-continue" && (
<p className="mt-1"> <p className="mt-1">
After you have installed Node.js, click "Continue". If the {
installer didn't work, try{" "} t("setup.afterInstallNode").split(
t("setup.moreDownloadOptions"),
)[0]
}
<a <a
className="text-blue-500 dark:text-blue-400 hover:underline" className="text-blue-500 dark:text-blue-400 hover:underline"
onClick={() => { onClick={() => {
...@@ -240,7 +249,7 @@ export function SetupBanner() { ...@@ -240,7 +249,7 @@ export function SetupBanner() {
); );
}} }}
> >
more download options {t("setup.moreDownloadOptions")}
</a> </a>
. .
</p> </p>
...@@ -256,7 +265,7 @@ export function SetupBanner() { ...@@ -256,7 +265,7 @@ export function SetupBanner() {
onClick={() => setShowManualConfig(!showManualConfig)} onClick={() => setShowManualConfig(!showManualConfig)}
className="text-sm text-blue-600 dark:text-blue-400 hover:underline" className="text-sm text-blue-600 dark:text-blue-400 hover:underline"
> >
Node.js already installed? Configure path manually → {t("setup.nodeAlreadyInstalled")}
</button> </button>
{showManualConfig && ( {showManualConfig && (
......
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
...@@ -16,6 +17,7 @@ import { showSuccess, showError } from "@/lib/toast"; ...@@ -16,6 +17,7 @@ import { showSuccess, showError } from "@/lib/toast";
import { isSupabaseConnected } from "@/lib/schemas"; import { isSupabaseConnected } from "@/lib/schemas";
export function SupabaseIntegration() { export function SupabaseIntegration() {
const { t } = useTranslation(["home", "common"]);
const { settings, updateSettings } = useSettings(); const { settings, updateSettings } = useSettings();
const [isDisconnecting, setIsDisconnecting] = useState(false); const [isDisconnecting, setIsDisconnecting] = useState(false);
...@@ -35,10 +37,10 @@ export function SupabaseIntegration() { ...@@ -35,10 +37,10 @@ export function SupabaseIntegration() {
enableSupabaseWriteSqlMigration: false, enableSupabaseWriteSqlMigration: false,
}); });
if (result) { if (result) {
showSuccess("Successfully disconnected all Supabase organizations"); showSuccess(t("integrations.supabase.disconnectedAll"));
await refetchOrganizations(); await refetchOrganizations();
} else { } else {
showError("Failed to disconnect from Supabase"); showError(t("integrations.supabase.failedDisconnect"));
} }
} catch (err: any) { } catch (err: any) {
showError( showError(
...@@ -52,9 +54,9 @@ export function SupabaseIntegration() { ...@@ -52,9 +54,9 @@ export function SupabaseIntegration() {
const handleDeleteOrganization = async (organizationSlug: string) => { const handleDeleteOrganization = async (organizationSlug: string) => {
try { try {
await deleteOrganization({ organizationSlug }); await deleteOrganization({ organizationSlug });
showSuccess("Organization disconnected successfully"); showSuccess(t("integrations.supabase.orgDisconnected"));
} catch (err: any) { } catch (err: any) {
showError(err.message || "Failed to disconnect organization"); showError(err.message || t("integrations.supabase.failedDisconnect"));
} }
}; };
...@@ -63,7 +65,7 @@ export function SupabaseIntegration() { ...@@ -63,7 +65,7 @@ export function SupabaseIntegration() {
await updateSettings({ await updateSettings({
enableSupabaseWriteSqlMigration: enabled, enableSupabaseWriteSqlMigration: enabled,
}); });
showSuccess("Setting updated"); showSuccess(t("integrations.supabase.settingUpdated"));
} catch (err: any) { } catch (err: any) {
showError(err.message || "Failed to update setting"); showError(err.message || "Failed to update setting");
} }
...@@ -89,11 +91,12 @@ export function SupabaseIntegration() { ...@@ -89,11 +91,12 @@ export function SupabaseIntegration() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300"> <h3 className="text-sm font-medium text-gray-700 dark:text-gray-300">
Supabase Integration {t("integrations.supabase.title")}
</h3> </h3>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1"> <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{organizations.length} organization {t("integrations.supabase.organizationsConnected", {
{organizations.length !== 1 ? "s" : ""} connected to Supabase. count: organizations.length,
})}
</p> </p>
</div> </div>
<Button <Button
...@@ -103,7 +106,9 @@ export function SupabaseIntegration() { ...@@ -103,7 +106,9 @@ export function SupabaseIntegration() {
disabled={isDisconnecting} disabled={isDisconnecting}
className="flex items-center gap-2" className="flex items-center gap-2"
> >
{isDisconnecting ? "Disconnecting..." : "Disconnect All"} {isDisconnecting
? t("common:disconnecting")
: t("integrations.supabase.disconnectAll")}
<DatabaseZap className="h-4 w-4" /> <DatabaseZap className="h-4 w-4" />
</Button> </Button>
</div> </div>
...@@ -141,7 +146,9 @@ export function SupabaseIntegration() { ...@@ -141,7 +146,9 @@ export function SupabaseIntegration() {
<Trash2 className="h-3.5 w-3.5 mr-1" /> <Trash2 className="h-3.5 w-3.5 mr-1" />
<span className="text-xs">Disconnect</span> <span className="text-xs">Disconnect</span>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>Disconnect organization</TooltipContent> <TooltipContent>
{t("integrations.supabase.disconnectOrganization")}
</TooltipContent>
</Tooltip> </Tooltip>
</div> </div>
))} ))}
...@@ -160,13 +167,10 @@ export function SupabaseIntegration() { ...@@ -160,13 +167,10 @@ export function SupabaseIntegration() {
htmlFor="supabase-migrations" htmlFor="supabase-migrations"
className="text-sm font-medium" className="text-sm font-medium"
> >
Write SQL migration files {t("integrations.supabase.writeSqlMigrations")}
</Label> </Label>
<p className="text-xs text-gray-500 dark:text-gray-400"> <p className="text-xs text-gray-500 dark:text-gray-400">
Generate SQL migration files when modifying your Supabase schema. {t("integrations.supabase.writeSqlDescription")}
This helps you track database changes in version control, though
these files aren't used for chat context, which uses the live
schema.
</p> </p>
</div> </div>
</div> </div>
......
import { useTranslation } from "react-i18next";
import { ipc } from "@/ipc/types"; import { ipc } from "@/ipc/types";
import React from "react"; import React from "react";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
...@@ -9,6 +10,7 @@ const hideBannerAtom = atom(false); ...@@ -9,6 +10,7 @@ const hideBannerAtom = atom(false);
export function PrivacyBanner() { export function PrivacyBanner() {
const [hideBanner, setHideBanner] = useAtom(hideBannerAtom); const [hideBanner, setHideBanner] = useAtom(hideBannerAtom);
const { settings, updateSettings } = useSettings(); const { settings, updateSettings } = useSettings();
const { t } = useTranslation("settings");
// TODO: Implement state management for banner visibility and user choice // TODO: Implement state management for banner visibility and user choice
// TODO: Implement functionality for Accept, Reject, Ask me later buttons // TODO: Implement functionality for Accept, Reject, Ask me later buttons
// TODO: Add state to hide/show banner based on user choice // TODO: Add state to hide/show banner based on user choice
...@@ -26,10 +28,8 @@ export function PrivacyBanner() { ...@@ -26,10 +28,8 @@ export function PrivacyBanner() {
Share anonymous data? Share anonymous data?
</h4> </h4>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1"> <p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
Help improve Dyad with anonymous usage data. {t("telemetry.privacyNotice")}
<em className="block italic mt-0.5"> <br />
Note: this does not log your code or messages.
</em>
<a <a
onClick={() => { onClick={() => {
ipc.system.openExternalUrl( ipc.system.openExternalUrl(
...@@ -50,7 +50,7 @@ export function PrivacyBanner() { ...@@ -50,7 +50,7 @@ export function PrivacyBanner() {
}} }}
data-testid="telemetry-accept-button" data-testid="telemetry-accept-button"
> >
Accept {t("telemetry.acceptAndContinue")}
</Button> </Button>
<Button <Button
variant="secondary" variant="secondary"
......
import { useSettings } from "@/hooks/useSettings"; import { useSettings } from "@/hooks/useSettings";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { useTranslation } from "react-i18next";
export function TelemetrySwitch() { export function TelemetrySwitch() {
const { settings, updateSettings } = useSettings(); const { settings, updateSettings } = useSettings();
const { t } = useTranslation("settings");
return ( return (
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Switch <Switch
...@@ -19,7 +21,7 @@ export function TelemetrySwitch() { ...@@ -19,7 +21,7 @@ export function TelemetrySwitch() {
}); });
}} }}
/> />
<Label htmlFor="telemetry-switch">Telemetry</Label> <Label htmlFor="telemetry-switch">{t("telemetry.enable")}</Label>
</div> </div>
); );
} }
...@@ -7,6 +7,7 @@ import { ...@@ -7,6 +7,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { useTranslation } from "react-i18next";
interface OptionInfo { interface OptionInfo {
value: string; value: string;
...@@ -38,6 +39,7 @@ const options: OptionInfo[] = [ ...@@ -38,6 +39,7 @@ const options: OptionInfo[] = [
export const ThinkingBudgetSelector: React.FC = () => { export const ThinkingBudgetSelector: React.FC = () => {
const { settings, updateSettings } = useSettings(); const { settings, updateSettings } = useSettings();
const { t } = useTranslation("settings");
const handleValueChange = (value: string) => { const handleValueChange = (value: string) => {
updateSettings({ thinkingBudget: value as "low" | "medium" | "high" }); updateSettings({ thinkingBudget: value as "low" | "medium" | "high" });
...@@ -57,14 +59,14 @@ export const ThinkingBudgetSelector: React.FC = () => { ...@@ -57,14 +59,14 @@ export const ThinkingBudgetSelector: React.FC = () => {
htmlFor="thinking-budget" htmlFor="thinking-budget"
className="text-sm font-medium text-gray-700 dark:text-gray-300" className="text-sm font-medium text-gray-700 dark:text-gray-300"
> >
Thinking Budget {t("ai.thinkingBudget")}
</label> </label>
<Select <Select
value={currentValue} value={currentValue}
onValueChange={(v) => v && handleValueChange(v)} onValueChange={(v) => v && handleValueChange(v)}
> >
<SelectTrigger className="w-[180px]" id="thinking-budget"> <SelectTrigger className="w-[180px]" id="thinking-budget">
<SelectValue placeholder="Select budget" /> <SelectValue placeholder={t("ai.selectThinkingBudget")} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{options.map((option) => ( {options.map((option) => (
......
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { useSettings } from "@/hooks/useSettings"; import { useSettings } from "@/hooks/useSettings";
import { showSuccess, showError } from "@/lib/toast"; import { showSuccess, showError } from "@/lib/toast";
export function VercelIntegration() { export function VercelIntegration() {
const { t } = useTranslation(["home", "common"]);
const { settings, updateSettings } = useSettings(); const { settings, updateSettings } = useSettings();
const [isDisconnecting, setIsDisconnecting] = useState(false); const [isDisconnecting, setIsDisconnecting] = useState(false);
...@@ -14,14 +16,12 @@ export function VercelIntegration() { ...@@ -14,14 +16,12 @@ export function VercelIntegration() {
vercelAccessToken: undefined, vercelAccessToken: undefined,
}); });
if (result) { if (result) {
showSuccess("Successfully disconnected from Vercel"); showSuccess(t("integrations.vercel.disconnected"));
} else { } else {
showError("Failed to disconnect from Vercel"); showError(t("integrations.vercel.failedDisconnect"));
} }
} catch (err: any) { } catch (err: any) {
showError( showError(err.message || t("integrations.vercel.errorDisconnect"));
err.message || "An error occurred while disconnecting from Vercel",
);
} finally { } finally {
setIsDisconnecting(false); setIsDisconnecting(false);
} }
...@@ -37,10 +37,10 @@ export function VercelIntegration() { ...@@ -37,10 +37,10 @@ export function VercelIntegration() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300"> <h3 className="text-sm font-medium text-gray-700 dark:text-gray-300">
Vercel Integration {t("integrations.vercel.title")}
</h3> </h3>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1"> <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
Your account is connected to Vercel. {t("integrations.vercel.connected")}
</p> </p>
</div> </div>
...@@ -51,7 +51,9 @@ export function VercelIntegration() { ...@@ -51,7 +51,9 @@ export function VercelIntegration() {
disabled={isDisconnecting} disabled={isDisconnecting}
className="flex items-center gap-2" className="flex items-center gap-2"
> >
{isDisconnecting ? "Disconnecting..." : "Disconnect from Vercel"} {isDisconnecting
? t("common:disconnecting")
: t("integrations.vercel.disconnect")}
<svg className="h-4 w-4" fill="currentColor" viewBox="0 0 24 24"> <svg className="h-4 w-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M24 22.525H0l12-21.05 12 21.05z" /> <path d="M24 22.525H0l12-21.05 12 21.05z" />
</svg> </svg>
......
...@@ -9,6 +9,7 @@ import { ...@@ -9,6 +9,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { useTranslation } from "react-i18next";
const ZOOM_LEVEL_LABELS: Record<ZoomLevel, string> = { const ZOOM_LEVEL_LABELS: Record<ZoomLevel, string> = {
"90": "90%", "90": "90%",
...@@ -28,6 +29,7 @@ const ZOOM_LEVEL_DESCRIPTIONS: Record<ZoomLevel, string> = { ...@@ -28,6 +29,7 @@ const ZOOM_LEVEL_DESCRIPTIONS: Record<ZoomLevel, string> = {
export function ZoomSelector() { export function ZoomSelector() {
const { settings, updateSettings } = useSettings(); const { settings, updateSettings } = useSettings();
const { t } = useTranslation("settings");
const currentZoomLevel: ZoomLevel = useMemo(() => { const currentZoomLevel: ZoomLevel = useMemo(() => {
const value = settings?.zoomLevel ?? DEFAULT_ZOOM_LEVEL; const value = settings?.zoomLevel ?? DEFAULT_ZOOM_LEVEL;
return ZoomLevelSchema.safeParse(value).success return ZoomLevelSchema.safeParse(value).success
...@@ -38,9 +40,9 @@ export function ZoomSelector() { ...@@ -38,9 +40,9 @@ export function ZoomSelector() {
return ( return (
<div className="space-y-2"> <div className="space-y-2">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<Label htmlFor="zoom-level">Zoom level</Label> <Label htmlFor="zoom-level">{t("general.zoom")}</Label>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Adjusts the zoom level to make content easier to read. {t("general.zoomDescription")}
</p> </p>
</div> </div>
<Select <Select
...@@ -50,7 +52,7 @@ export function ZoomSelector() { ...@@ -50,7 +52,7 @@ export function ZoomSelector() {
} }
> >
<SelectTrigger id="zoom-level" className="w-[220px]"> <SelectTrigger id="zoom-level" className="w-[220px]">
<SelectValue placeholder="Select zoom level" /> <SelectValue placeholder={t("general.selectZoom")} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{Object.entries(ZOOM_LEVEL_LABELS).map(([value, label]) => ( {Object.entries(ZOOM_LEVEL_LABELS).map(([value, label]) => (
......
import { FileText, X, MessageSquare, Upload } from "lucide-react"; import { FileText, X, MessageSquare, Upload } from "lucide-react";
import type { FileAttachment } from "@/ipc/types"; import type { FileAttachment } from "@/ipc/types";
import { useTranslation } from "react-i18next";
interface AttachmentsListProps { interface AttachmentsListProps {
attachments: FileAttachment[]; attachments: FileAttachment[];
...@@ -10,6 +11,8 @@ export function AttachmentsList({ ...@@ -10,6 +11,8 @@ export function AttachmentsList({
attachments, attachments,
onRemove, onRemove,
}: AttachmentsListProps) { }: AttachmentsListProps) {
const { t } = useTranslation("chat");
if (attachments.length === 0) return null; if (attachments.length === 0) return null;
return ( return (
...@@ -61,7 +64,7 @@ export function AttachmentsList({ ...@@ -61,7 +64,7 @@ export function AttachmentsList({
<button <button
onClick={() => onRemove(index)} onClick={() => onRemove(index)}
className="hover:bg-muted-foreground/20 rounded-full p-0.5" className="hover:bg-muted-foreground/20 rounded-full p-0.5"
aria-label="Remove attachment" aria-label={t("removeAttachment")}
> >
<X size={12} /> <X size={12} />
</button> </button>
......
import { XCircle, AlertTriangle } from "lucide-react"; // Assuming lucide-react is used import { XCircle, AlertTriangle } from "lucide-react"; // Assuming lucide-react is used
import { useTranslation } from "react-i18next";
interface ChatErrorProps { interface ChatErrorProps {
error: string | null; error: string | null;
...@@ -6,6 +7,8 @@ interface ChatErrorProps { ...@@ -6,6 +7,8 @@ interface ChatErrorProps {
} }
export function ChatError({ error, onDismiss }: ChatErrorProps) { export function ChatError({ error, onDismiss }: ChatErrorProps) {
const { t } = useTranslation("chat");
if (!error) { if (!error) {
return null; return null;
} }
...@@ -23,7 +26,7 @@ export function ChatError({ error, onDismiss }: ChatErrorProps) { ...@@ -23,7 +26,7 @@ export function ChatError({ error, onDismiss }: ChatErrorProps) {
<button <button
onClick={onDismiss} onClick={onDismiss}
className="absolute top-1 right-1 p-1 rounded-full hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-red-400" className="absolute top-1 right-1 p-1 rounded-full hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-red-400"
aria-label="Dismiss error" aria-label={t("dismissError")}
> >
<XCircle className="h-4 w-4 text-red-500 hover:text-red-700" /> <XCircle className="h-4 w-4 text-red-500 hover:text-red-700" />
</button> </button>
......
...@@ -6,6 +6,7 @@ import { ...@@ -6,6 +6,7 @@ import {
Info, Info,
} from "lucide-react"; } from "lucide-react";
import { PanelRightClose } from "lucide-react"; import { PanelRightClose } from "lucide-react";
import { useTranslation } from "react-i18next";
import { useAtom, useAtomValue } from "jotai"; import { useAtom, useAtomValue } from "jotai";
import { selectedAppIdAtom } from "@/atoms/appAtoms"; import { selectedAppIdAtom } from "@/atoms/appAtoms";
import { useVersions } from "@/hooks/useVersions"; import { useVersions } from "@/hooks/useVersions";
...@@ -43,6 +44,7 @@ export function ChatHeader({ ...@@ -43,6 +44,7 @@ export function ChatHeader({
onTogglePreview, onTogglePreview,
onVersionClick, onVersionClick,
}: ChatHeaderProps) { }: ChatHeaderProps) {
const { t } = useTranslation("chat");
const appId = useAtomValue(selectedAppIdAtom); const appId = useAtomValue(selectedAppIdAtom);
const { versions, loading: versionsLoading } = useVersions(appId); const { versions, loading: versionsLoading } = useVersions(appId);
const { navigate } = useRouter(); const { navigate } = useRouter();
...@@ -78,7 +80,7 @@ export function ChatHeader({ ...@@ -78,7 +80,7 @@ export function ChatHeader({
// If this throws, it will automatically show an error toast // If this throws, it will automatically show an error toast
await renameBranch({ oldBranchName: "master", newBranchName: "main" }); await renameBranch({ oldBranchName: "master", newBranchName: "main" });
showSuccess("Master branch renamed to main"); showSuccess(t("header.masterRenamed"));
}; };
const handleNewChat = async () => { const handleNewChat = async () => {
...@@ -92,7 +94,7 @@ export function ChatHeader({ ...@@ -92,7 +94,7 @@ export function ChatHeader({
}); });
await invalidateChats(); await invalidateChats();
} catch (error) { } catch (error) {
showError(`Failed to create new chat: ${(error as any).toString()}`); showError(t("failedCreateChat", { error: (error as any).toString() }));
} }
} else { } else {
navigate({ to: "/" }); navigate({ to: "/" });
...@@ -123,14 +125,14 @@ export function ChatHeader({ ...@@ -123,14 +125,14 @@ export function ChatHeader({
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
{isAnyCheckoutVersionInProgress ? ( {isAnyCheckoutVersionInProgress ? (
<> <>
<span> <span>{t("header.switchingToLatest")}</span>
Please wait, switching back to latest version...
</span>
</> </>
) : ( ) : (
<> <>
<strong>Warning:</strong> <strong>
<span>You are not on a branch</span> {t("header.warningNotOnBranch").split(":")[0]}:
</strong>
<span>{t("header.notOnBranch")}</span>
<Info size={14} /> <Info size={14} />
</> </>
)} )}
...@@ -139,8 +141,8 @@ export function ChatHeader({ ...@@ -139,8 +141,8 @@ export function ChatHeader({
<TooltipContent> <TooltipContent>
<p> <p>
{isAnyCheckoutVersionInProgress {isAnyCheckoutVersionInProgress
? "Version checkout is currently in progress" ? t("header.checkoutInProgress")
: "Checkout main branch, otherwise changes will not be saved properly"} : t("header.checkoutMainBranch")}
</p> </p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
...@@ -148,11 +150,9 @@ export function ChatHeader({ ...@@ -148,11 +150,9 @@ export function ChatHeader({
</> </>
)} )}
{currentBranchName && currentBranchName !== "<no-branch>" && ( {currentBranchName && currentBranchName !== "<no-branch>" && (
<span> <span>{t("header.onBranch", { name: currentBranchName })}</span>
You are on branch: <strong>{currentBranchName}</strong>.
</span>
)} )}
{branchInfoLoading && <span>Checking branch...</span>} {branchInfoLoading && <span>{t("header.checkingBranch")}</span>}
</span> </span>
</div> </div>
{currentBranchName === "master" ? ( {currentBranchName === "master" ? (
...@@ -162,7 +162,9 @@ export function ChatHeader({ ...@@ -162,7 +162,9 @@ export function ChatHeader({
onClick={handleRenameMasterToMain} onClick={handleRenameMasterToMain}
disabled={isRenamingBranch || branchInfoLoading} disabled={isRenamingBranch || branchInfoLoading}
> >
{isRenamingBranch ? "Renaming..." : "Rename master to main"} {isRenamingBranch
? t("header.renaming")
: t("header.renameMasterToMain")}
</Button> </Button>
) : isAnyCheckoutVersionInProgress && !isCheckingOutVersion ? null : ( ) : isAnyCheckoutVersionInProgress && !isCheckingOutVersion ? null : (
<Button <Button
...@@ -172,8 +174,8 @@ export function ChatHeader({ ...@@ -172,8 +174,8 @@ export function ChatHeader({
disabled={isCheckingOutVersion || branchInfoLoading} disabled={isCheckingOutVersion || branchInfoLoading}
> >
{isCheckingOutVersion {isCheckingOutVersion
? "Checking out..." ? t("header.checkingOut")
: "Switch to main branch"} : t("header.switchToMainBranch")}
</Button> </Button>
)} )}
</div> </div>
...@@ -194,7 +196,7 @@ export function ChatHeader({ ...@@ -194,7 +196,7 @@ export function ChatHeader({
className="hidden @2xs:flex items-center justify-start gap-2 mx-2 py-3" className="hidden @2xs:flex items-center justify-start gap-2 mx-2 py-3"
> >
<PlusCircle size={16} /> <PlusCircle size={16} />
<span>New Chat</span> <span>{t("newChat")}</span>
</Button> </Button>
<Button <Button
onClick={onVersionClick} onClick={onVersionClick}
...@@ -204,7 +206,7 @@ export function ChatHeader({ ...@@ -204,7 +206,7 @@ export function ChatHeader({
<History size={16} /> <History size={16} />
{versionsLoading {versionsLoading
? "..." ? "..."
: `Version ${versions.length}${versionPostfix}`} : `${t("header.versionCount", { count: versions.length })}${versionPostfix}`}
</Button> </Button>
</div> </div>
......
...@@ -8,6 +8,7 @@ import { ...@@ -8,6 +8,7 @@ import {
AlertDialogHeader, AlertDialogHeader,
AlertDialogTitle, AlertDialogTitle,
} from "@/components/ui/alert-dialog"; } from "@/components/ui/alert-dialog";
import { useTranslation } from "react-i18next";
interface DeleteChatDialogProps { interface DeleteChatDialogProps {
isOpen: boolean; isOpen: boolean;
...@@ -22,28 +23,28 @@ export function DeleteChatDialog({ ...@@ -22,28 +23,28 @@ export function DeleteChatDialog({
onConfirmDelete, onConfirmDelete,
chatTitle, chatTitle,
}: DeleteChatDialogProps) { }: DeleteChatDialogProps) {
const { t } = useTranslation("chat");
const { t: tc } = useTranslation("common");
return ( return (
<AlertDialog open={isOpen} onOpenChange={onOpenChange}> <AlertDialog open={isOpen} onOpenChange={onOpenChange}>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Delete Chat</AlertDialogTitle> <AlertDialogTitle>{t("deleteChat")}</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
Are you sure you want to delete "{chatTitle || "this chat"}"? This {t("deleteChatConfirmation", { title: chatTitle || "this chat" })}
action cannot be undone and all messages in this chat will be
permanently lost.
<br /> <br />
<br /> <br />
<strong>Note:</strong> Any code changes that have already been <strong>{t("deleteChatNote")}</strong>
accepted will be kept.
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogCancel>{tc("cancel")}</AlertDialogCancel>
<AlertDialogAction <AlertDialogAction
onClick={onConfirmDelete} onClick={onConfirmDelete}
className="bg-red-600 text-white hover:bg-red-700 dark:bg-red-600 dark:text-white dark:hover:bg-red-700" className="bg-red-600 text-white hover:bg-red-700 dark:bg-red-600 dark:text-white dark:hover:bg-red-700"
> >
Delete Chat {t("deleteChat")}
</AlertDialogAction> </AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
......
import { Paperclip } from "lucide-react"; import { Paperclip } from "lucide-react";
import { useTranslation } from "react-i18next";
interface DragDropOverlayProps { interface DragDropOverlayProps {
isDraggingOver: boolean; isDraggingOver: boolean;
} }
export function DragDropOverlay({ isDraggingOver }: DragDropOverlayProps) { export function DragDropOverlay({ isDraggingOver }: DragDropOverlayProps) {
const { t } = useTranslation("chat");
if (!isDraggingOver) return null; if (!isDraggingOver) return null;
return ( return (
<div className="absolute inset-0 bg-blue-100/30 dark:bg-blue-900/30 flex items-center justify-center rounded-lg z-10 pointer-events-none"> <div className="absolute inset-0 bg-blue-100/30 dark:bg-blue-900/30 flex items-center justify-center rounded-lg z-10 pointer-events-none">
<div className="bg-background p-4 rounded-lg shadow-lg text-center"> <div className="bg-background p-4 rounded-lg shadow-lg text-center">
<Paperclip className="mx-auto mb-2 text-blue-500" /> <Paperclip className="mx-auto mb-2 text-blue-500" />
<p className="text-sm font-medium">Drop files to attach</p> <p className="text-sm font-medium">{t("dropFilesToAttach")}</p>
</div> </div>
</div> </div>
); );
......
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next";
import { ipc } from "@/ipc/types"; import { ipc } from "@/ipc/types";
import { showError, showSuccess } from "@/lib/toast"; import { showError, showSuccess } from "@/lib/toast";
import { import {
...@@ -28,6 +29,8 @@ export function RenameChatDialog({ ...@@ -28,6 +29,8 @@ export function RenameChatDialog({
onOpenChange, onOpenChange,
onRename, onRename,
}: RenameChatDialogProps) { }: RenameChatDialogProps) {
const { t } = useTranslation("chat");
const { t: tc } = useTranslation("common");
const [newTitle, setNewTitle] = useState(""); const [newTitle, setNewTitle] = useState("");
// Reset title when dialog opens // Reset title when dialog opens
...@@ -50,7 +53,7 @@ export function RenameChatDialog({ ...@@ -50,7 +53,7 @@ export function RenameChatDialog({
chatId, chatId,
title: newTitle.trim(), title: newTitle.trim(),
}); });
showSuccess("Chat renamed successfully"); showSuccess(t("chatRenamed"));
// Call the parent's onRename callback to refresh the chat list // Call the parent's onRename callback to refresh the chat list
onRename(); onRename();
...@@ -58,7 +61,7 @@ export function RenameChatDialog({ ...@@ -58,7 +61,7 @@ export function RenameChatDialog({
// Close the dialog // Close the dialog
handleOpenChange(false); handleOpenChange(false);
} catch (error) { } catch (error) {
showError(`Failed to rename chat: ${(error as any).toString()}`); showError(t("failedRenameChat", { error: (error as any).toString() }));
} }
}; };
...@@ -70,20 +73,20 @@ export function RenameChatDialog({ ...@@ -70,20 +73,20 @@ export function RenameChatDialog({
<Dialog open={isOpen} onOpenChange={handleOpenChange}> <Dialog open={isOpen} onOpenChange={handleOpenChange}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>Rename Chat</DialogTitle> <DialogTitle>{t("renameChat")}</DialogTitle>
<DialogDescription>Enter a new name for this chat.</DialogDescription> <DialogDescription>{t("renameChatDescription")}</DialogDescription>
</DialogHeader> </DialogHeader>
<div className="grid gap-4 py-4"> <div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4"> <div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="chat-title" className="text-right"> <Label htmlFor="chat-title" className="text-right">
Title {t("chatTitle")}
</Label> </Label>
<Input <Input
id="chat-title" id="chat-title"
value={newTitle} value={newTitle}
onChange={(e) => setNewTitle(e.target.value)} onChange={(e) => setNewTitle(e.target.value)}
className="col-span-3" className="col-span-3"
placeholder="Enter chat title..." placeholder={t("enterChatTitle")}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter") { if (e.key === "Enter") {
handleSave(); handleSave();
...@@ -94,10 +97,10 @@ export function RenameChatDialog({ ...@@ -94,10 +97,10 @@ export function RenameChatDialog({
</div> </div>
<DialogFooter> <DialogFooter>
<Button variant="outline" onClick={handleClose}> <Button variant="outline" onClick={handleClose}>
Cancel {tc("cancel")}
</Button> </Button>
<Button onClick={handleSave} disabled={!newTitle.trim()}> <Button onClick={handleSave} disabled={!newTitle.trim()}>
Save {tc("save")}
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
......
...@@ -33,6 +33,7 @@ import { showError, showSuccess } from "@/lib/toast"; ...@@ -33,6 +33,7 @@ import { showError, showSuccess } from "@/lib/toast";
import { useMutation } from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query";
import { useCheckProblems } from "@/hooks/useCheckProblems"; import { useCheckProblems } from "@/hooks/useCheckProblems";
import { isPreviewOpenAtom } from "@/atoms/viewAtoms"; import { isPreviewOpenAtom } from "@/atoms/viewAtoms";
import { useTranslation } from "react-i18next";
export type PreviewMode = export type PreviewMode =
| "preview" | "preview"
...@@ -44,6 +45,7 @@ export type PreviewMode = ...@@ -44,6 +45,7 @@ export type PreviewMode =
// Preview Header component with preview mode toggle // Preview Header component with preview mode toggle
export const ActionHeader = () => { export const ActionHeader = () => {
const { t } = useTranslation("home");
const [previewMode, setPreviewMode] = useAtom(previewModeAtom); const [previewMode, setPreviewMode] = useAtom(previewModeAtom);
const [isPreviewOpen, setIsPreviewOpen] = useAtom(isPreviewOpenAtom); const [isPreviewOpen, setIsPreviewOpen] = useAtom(isPreviewOpenAtom);
const selectedAppId = useAtomValue(selectedAppIdAtom); const selectedAppId = useAtomValue(selectedAppIdAtom);
...@@ -218,14 +220,14 @@ export const ActionHeader = () => { ...@@ -218,14 +220,14 @@ export const ActionHeader = () => {
"preview", "preview",
previewRef, previewRef,
<Eye size={iconSize} />, <Eye size={iconSize} />,
"Preview", t("preview.title"),
"preview-mode-button", "preview-mode-button",
)} )}
{renderButton( {renderButton(
"problems", "problems",
problemsRef, problemsRef,
<AlertTriangle size={iconSize} />, <AlertTriangle size={iconSize} />,
"Problems", t("preview.problems"),
"problems-mode-button", "problems-mode-button",
displayCount && ( displayCount && (
<span className="ml-0.5 px-1 py-0.5 text-xs font-medium bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300 rounded-full min-w-[16px] text-center"> <span className="ml-0.5 px-1 py-0.5 text-xs font-medium bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300 rounded-full min-w-[16px] text-center">
...@@ -237,28 +239,28 @@ export const ActionHeader = () => { ...@@ -237,28 +239,28 @@ export const ActionHeader = () => {
"code", "code",
codeRef, codeRef,
<Code size={iconSize} />, <Code size={iconSize} />,
"Code", t("preview.code"),
"code-mode-button", "code-mode-button",
)} )}
{renderButton( {renderButton(
"configure", "configure",
configureRef, configureRef,
<Wrench size={iconSize} />, <Wrench size={iconSize} />,
"Configure", t("preview.configure"),
"configure-mode-button", "configure-mode-button",
)} )}
{renderButton( {renderButton(
"security", "security",
securityRef, securityRef,
<Shield size={iconSize} />, <Shield size={iconSize} />,
"Security", t("preview.security"),
"security-mode-button", "security-mode-button",
)} )}
{renderButton( {renderButton(
"publish", "publish",
publishRef, publishRef,
<Globe size={iconSize} />, <Globe size={iconSize} />,
"Publish", t("preview.publish"),
"publish-mode-button", "publish-mode-button",
)} )}
</div> </div>
...@@ -277,24 +279,24 @@ export const ActionHeader = () => { ...@@ -277,24 +279,24 @@ export const ActionHeader = () => {
> >
<MoreVertical size={16} /> <MoreVertical size={16} />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>More options</TooltipContent> <TooltipContent>{t("preview.moreOptions")}</TooltipContent>
</Tooltip> </Tooltip>
<DropdownMenuContent align="end" className="w-60"> <DropdownMenuContent align="end" className="w-60">
<DropdownMenuItem onClick={onCleanRestart}> <DropdownMenuItem onClick={onCleanRestart}>
<Cog size={16} /> <Cog size={16} />
<div className="flex flex-col"> <div className="flex flex-col">
<span>Rebuild</span> <span>{t("preview.rebuild")}</span>
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
Re-installs node_modules and restarts {t("preview.rebuildDescription")}
</span> </span>
</div> </div>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={onClearSessionData}> <DropdownMenuItem onClick={onClearSessionData}>
<Trash2 size={16} /> <Trash2 size={16} />
<div className="flex flex-col"> <div className="flex flex-col">
<span>Clear Cache</span> <span>{t("preview.clearCache")}</span>
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
Clears cookies and local storage and other app cache {t("preview.clearCacheDescription")}
</span> </span>
</div> </div>
</DropdownMenuItem> </DropdownMenuItem>
......
...@@ -10,6 +10,7 @@ import { ...@@ -10,6 +10,7 @@ import {
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { selectedFileAtom } from "@/atoms/viewAtoms"; import { selectedFileAtom } from "@/atoms/viewAtoms";
import { useTranslation } from "react-i18next";
interface App { interface App {
id?: number; id?: number;
...@@ -23,6 +24,7 @@ export interface CodeViewProps { ...@@ -23,6 +24,7 @@ export interface CodeViewProps {
// Code view component that displays app files or status messages // Code view component that displays app files or status messages
export const CodeView = ({ loading, app }: CodeViewProps) => { export const CodeView = ({ loading, app }: CodeViewProps) => {
const { t } = useTranslation("home");
const selectedFile = useAtomValue(selectedFileAtom); const selectedFile = useAtomValue(selectedFileAtom);
const { refreshApp } = useLoadApp(app?.id ?? null); const { refreshApp } = useLoadApp(app?.id ?? null);
const [isFullscreen, setIsFullscreen] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false);
...@@ -48,12 +50,14 @@ export const CodeView = ({ loading, app }: CodeViewProps) => { ...@@ -48,12 +50,14 @@ export const CodeView = ({ loading, app }: CodeViewProps) => {
}, [isFullscreen]); }, [isFullscreen]);
if (loading) { if (loading) {
return <div className="text-center py-4">Loading files...</div>; return <div className="text-center py-4">{t("preview.loadingFiles")}</div>;
} }
if (!app) { if (!app) {
return ( return (
<div className="text-center py-4 text-gray-500">No app selected</div> <div className="text-center py-4 text-gray-500">
{t("preview.noAppSelected")}
</div>
); );
} }
...@@ -76,9 +80,11 @@ export const CodeView = ({ loading, app }: CodeViewProps) => { ...@@ -76,9 +80,11 @@ export const CodeView = ({ loading, app }: CodeViewProps) => {
> >
<RefreshCw size={16} /> <RefreshCw size={16} />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>Refresh Files</TooltipContent> <TooltipContent>{t("preview.refreshFiles")}</TooltipContent>
</Tooltip> </Tooltip>
<div className="text-sm text-gray-500">{app.files.length} files</div> <div className="text-sm text-gray-500">
{app.files.length} {t("preview.files")}
</div>
<div className="flex-1" /> <div className="flex-1" />
<Tooltip> <Tooltip>
<TooltipTrigger <TooltipTrigger
...@@ -92,7 +98,9 @@ export const CodeView = ({ loading, app }: CodeViewProps) => { ...@@ -92,7 +98,9 @@ export const CodeView = ({ loading, app }: CodeViewProps) => {
{isFullscreen ? <Minimize2 size={16} /> : <Maximize2 size={16} />} {isFullscreen ? <Minimize2 size={16} /> : <Maximize2 size={16} />}
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
{isFullscreen ? "Exit full screen" : "Enter full screen"} {isFullscreen
? t("preview.exitFullScreen")
: t("preview.enterFullScreen")}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</div> </div>
...@@ -111,7 +119,7 @@ export const CodeView = ({ loading, app }: CodeViewProps) => { ...@@ -111,7 +119,7 @@ export const CodeView = ({ loading, app }: CodeViewProps) => {
/> />
) : ( ) : (
<div className="text-center py-4 text-gray-500"> <div className="text-center py-4 text-gray-500">
Select a file to view {t("preview.selectFileToView")}
</div> </div>
)} )}
</div> </div>
...@@ -120,5 +128,9 @@ export const CodeView = ({ loading, app }: CodeViewProps) => { ...@@ -120,5 +128,9 @@ export const CodeView = ({ loading, app }: CodeViewProps) => {
); );
} }
return <div className="text-center py-4 text-gray-500">No files found</div>; return (
<div className="text-center py-4 text-gray-500">
{t("preview.noFilesFound")}
</div>
);
}; };
...@@ -12,6 +12,7 @@ import { ...@@ -12,6 +12,7 @@ import {
TooltipTrigger, TooltipTrigger,
TooltipContent, TooltipContent,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { useTranslation } from "react-i18next";
interface ConsoleEntryProps { interface ConsoleEntryProps {
type: "server" | "client" | "edge-function" | "network-requests"; type: "server" | "client" | "edge-function" | "network-requests";
...@@ -42,6 +43,7 @@ export const ConsoleEntryComponent = (props: ConsoleEntryProps) => { ...@@ -42,6 +43,7 @@ export const ConsoleEntryComponent = (props: ConsoleEntryProps) => {
isExpanded = false, isExpanded = false,
onToggleExpand, onToggleExpand,
} = props; } = props;
const { t } = useTranslation(["home", "common"]);
const setChatInput = useSetAtom(chatInputValueAtom); const setChatInput = useSetAtom(chatInputValueAtom);
const isTruncated = message.length > MAX_MESSAGE_LENGTH; const isTruncated = message.length > MAX_MESSAGE_LENGTH;
...@@ -114,11 +116,11 @@ export const ConsoleEntryComponent = (props: ConsoleEntryProps) => { ...@@ -114,11 +116,11 @@ export const ConsoleEntryComponent = (props: ConsoleEntryProps) => {
> >
{isExpanded ? ( {isExpanded ? (
<> <>
Show less <ChevronUp size={12} /> {t("common:showLess")} <ChevronUp size={12} />
</> </>
) : ( ) : (
<> <>
Show more <ChevronDown size={12} /> {t("common:showMore")} <ChevronDown size={12} />
</> </>
)} )}
</button> </button>
...@@ -137,7 +139,7 @@ export const ConsoleEntryComponent = (props: ConsoleEntryProps) => { ...@@ -137,7 +139,7 @@ export const ConsoleEntryComponent = (props: ConsoleEntryProps) => {
> >
<MessageSquare size={12} className="text-gray-500" /> <MessageSquare size={12} className="text-gray-500" />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>Send to chat</TooltipContent> <TooltipContent>{t("home:preview.sendToChat")}</TooltipContent>
</Tooltip> </Tooltip>
</div> </div>
); );
......
...@@ -4,6 +4,7 @@ import { ...@@ -4,6 +4,7 @@ import {
TooltipTrigger, TooltipTrigger,
TooltipContent, TooltipContent,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { useTranslation } from "react-i18next";
interface ConsoleFiltersProps { interface ConsoleFiltersProps {
levelFilter: "all" | "info" | "warn" | "error"; levelFilter: "all" | "info" | "warn" | "error";
...@@ -39,6 +40,7 @@ export const ConsoleFilters = ({ ...@@ -39,6 +40,7 @@ export const ConsoleFilters = ({
totalLogs, totalLogs,
showFilters, showFilters,
}: ConsoleFiltersProps) => { }: ConsoleFiltersProps) => {
const { t } = useTranslation("home");
const hasActiveFilters = const hasActiveFilters =
levelFilter !== "all" || typeFilter !== "all" || sourceFilter !== ""; levelFilter !== "all" || typeFilter !== "all" || sourceFilter !== "";
...@@ -58,10 +60,10 @@ export const ConsoleFilters = ({ ...@@ -58,10 +60,10 @@ export const ConsoleFilters = ({
} }
className="text-xs px-2 py-1 border border-border rounded bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors" className="text-xs px-2 py-1 border border-border rounded bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
> >
<option value="all">All Levels</option> <option value="all">{t("preview.consoleFilters.allLevels")}</option>
<option value="info">Info</option> <option value="info">{t("preview.consoleFilters.info")}</option>
<option value="warn">Warn</option> <option value="warn">{t("preview.consoleFilters.warn")}</option>
<option value="error">Error</option> <option value="error">{t("preview.consoleFilters.error")}</option>
</select> </select>
{/* Type filter */} {/* Type filter */}
...@@ -79,11 +81,15 @@ export const ConsoleFilters = ({ ...@@ -79,11 +81,15 @@ export const ConsoleFilters = ({
} }
className="text-xs px-2 py-1 border border-border rounded bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors" className="text-xs px-2 py-1 border border-border rounded bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
> >
<option value="all">All Types</option> <option value="all">{t("preview.consoleFilters.allTypes")}</option>
<option value="server">Server</option> <option value="server">{t("preview.consoleFilters.server")}</option>
<option value="client">Client</option> <option value="client">{t("preview.consoleFilters.client")}</option>
<option value="edge-function">Edge Function</option> <option value="edge-function">
<option value="network-requests">Network Requests</option> {t("preview.consoleFilters.edgeFunction")}
</option>
<option value="network-requests">
{t("preview.consoleFilters.networkRequests")}
</option>
</select> </select>
{/* Source filter */} {/* Source filter */}
...@@ -93,7 +99,7 @@ export const ConsoleFilters = ({ ...@@ -93,7 +99,7 @@ export const ConsoleFilters = ({
onChange={(e) => onSourceFilterChange(e.target.value)} onChange={(e) => onSourceFilterChange(e.target.value)}
className="text-xs px-2 py-1 border border-border rounded bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors" className="text-xs px-2 py-1 border border-border rounded bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
> >
<option value="">All Sources</option> <option value="">{t("preview.consoleFilters.allSources")}</option>
{uniqueSources.map((source) => ( {uniqueSources.map((source) => (
<option key={source} value={source}> <option key={source} value={source}>
{source} {source}
...@@ -109,7 +115,7 @@ export const ConsoleFilters = ({ ...@@ -109,7 +115,7 @@ export const ConsoleFilters = ({
className="text-xs px-2 py-1 flex items-center gap-1 border border-border rounded bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors" className="text-xs px-2 py-1 flex items-center gap-1 border border-border rounded bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
> >
<X size={12} /> <X size={12} />
Clear Filters {t("preview.consoleFilters.clearFilters")}
</button> </button>
)} )}
...@@ -126,10 +132,12 @@ export const ConsoleFilters = ({ ...@@ -126,10 +132,12 @@ export const ConsoleFilters = ({
> >
<Trash2 size={14} /> <Trash2 size={14} />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>Clear logs</TooltipContent> <TooltipContent>{t("preview.consoleFilters.clearLogs")}</TooltipContent>
</Tooltip> </Tooltip>
<div className="ml-auto text-xs text-gray-500">{totalLogs} logs</div> <div className="ml-auto text-xs text-gray-500">
{totalLogs} {t("preview.consoleFilters.logs")}
</div>
</div> </div>
); );
}; };
...@@ -17,6 +17,7 @@ import { ...@@ -17,6 +17,7 @@ import {
TooltipTrigger, TooltipTrigger,
TooltipContent, TooltipContent,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { useTranslation } from "react-i18next";
interface FileEditorProps { interface FileEditorProps {
appId: number | null; appId: number | null;
...@@ -37,6 +38,7 @@ const Breadcrumb: React.FC<BreadcrumbProps> = ({ ...@@ -37,6 +38,7 @@ const Breadcrumb: React.FC<BreadcrumbProps> = ({
onSave, onSave,
isSaving, isSaving,
}) => { }) => {
const { t } = useTranslation("home");
const segments = path.split("/").filter(Boolean); const segments = path.split("/").filter(Boolean);
return ( return (
...@@ -74,7 +76,9 @@ const Breadcrumb: React.FC<BreadcrumbProps> = ({ ...@@ -74,7 +76,9 @@ const Breadcrumb: React.FC<BreadcrumbProps> = ({
<Save size={12} /> <Save size={12} />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
{hasUnsavedChanges ? "Save changes" : "No unsaved changes"} {hasUnsavedChanges
? t("preview.saveChanges")
: t("preview.noUnsavedChanges")}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
{hasUnsavedChanges && ( {hasUnsavedChanges && (
...@@ -95,6 +99,7 @@ export const FileEditor = ({ ...@@ -95,6 +99,7 @@ export const FileEditor = ({
filePath, filePath,
initialLine = null, initialLine = null,
}: FileEditorProps) => { }: FileEditorProps) => {
const { t } = useTranslation("home");
const { content, loading, error } = useLoadAppFile(appId, filePath); const { content, loading, error } = useLoadAppFile(appId, filePath);
const { theme } = useTheme(); const { theme } = useTheme();
const [value, setValue] = useState<string | undefined>(undefined); const [value, setValue] = useState<string | undefined>(undefined);
...@@ -206,7 +211,7 @@ export const FileEditor = ({ ...@@ -206,7 +211,7 @@ export const FileEditor = ({
if (warning) { if (warning) {
showWarning(warning); showWarning(warning);
} else { } else {
showSuccess("File saved"); showSuccess(t("preview.fileSaved"));
} }
originalValueRef.current = currentValueRef.current; originalValueRef.current = currentValueRef.current;
...@@ -231,7 +236,7 @@ export const FileEditor = ({ ...@@ -231,7 +236,7 @@ export const FileEditor = ({
}, [initialLine, filePath, content, navigateToLine]); }, [initialLine, filePath, content, navigateToLine]);
if (loading) { if (loading) {
return <div className="p-4">Loading file content...</div>; return <div className="p-4">{t("preview.loadingFileContent")}</div>;
} }
if (error) { if (error) {
...@@ -239,7 +244,9 @@ export const FileEditor = ({ ...@@ -239,7 +244,9 @@ export const FileEditor = ({
} }
if (!content) { if (!content) {
return <div className="p-4 text-gray-500">No content available</div>; return (
<div className="p-4 text-gray-500">{t("preview.noContentAvailable")}</div>
);
} }
return ( return (
......
...@@ -13,6 +13,7 @@ import { useSetAtom } from "jotai"; ...@@ -13,6 +13,7 @@ import { useSetAtom } from "jotai";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import type { AppFileSearchResult } from "@/ipc/types"; import type { AppFileSearchResult } from "@/ipc/types";
import { useSearchAppFiles } from "@/hooks/useSearchAppFiles"; import { useSearchAppFiles } from "@/hooks/useSearchAppFiles";
import { useTranslation } from "react-i18next";
interface FileTreeProps { interface FileTreeProps {
appId: number | null; appId: number | null;
...@@ -100,6 +101,7 @@ const buildFileTree = (files: string[]): TreeNode[] => { ...@@ -100,6 +101,7 @@ const buildFileTree = (files: string[]): TreeNode[] => {
// File tree component // File tree component
export const FileTree = ({ appId, files }: FileTreeProps) => { export const FileTree = ({ appId, files }: FileTreeProps) => {
const { t } = useTranslation("home");
const [searchValue, setSearchValue] = useState(""); const [searchValue, setSearchValue] = useState("");
const prevAppIdRef = useRef<number | null>(appId); const prevAppIdRef = useRef<number | null>(appId);
...@@ -168,7 +170,7 @@ export const FileTree = ({ appId, files }: FileTreeProps) => { ...@@ -168,7 +170,7 @@ export const FileTree = ({ appId, files }: FileTreeProps) => {
<Input <Input
value={searchValue} value={searchValue}
onChange={(event) => setSearchValue(event.target.value)} onChange={(event) => setSearchValue(event.target.value)}
placeholder="Search file contents" placeholder={t("preview.searchFileContents")}
className="h-8 pl-7 pr-16 text-sm" className="h-8 pl-7 pr-16 text-sm"
data-testid="file-tree-search" data-testid="file-tree-search"
disabled={!appId} disabled={!appId}
...@@ -177,7 +179,7 @@ export const FileTree = ({ appId, files }: FileTreeProps) => { ...@@ -177,7 +179,7 @@ export const FileTree = ({ appId, files }: FileTreeProps) => {
<button <button
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground" className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
onClick={() => setSearchValue("")} onClick={() => setSearchValue("")}
aria-label="Clear search" aria-label={t("preview.clearSearch")}
> >
<X size={14} /> <X size={14} />
</button> </button>
...@@ -193,8 +195,8 @@ export const FileTree = ({ appId, files }: FileTreeProps) => { ...@@ -193,8 +195,8 @@ export const FileTree = ({ appId, files }: FileTreeProps) => {
<div className="mt-1 flex items-center justify-between text-[11px] text-muted-foreground"> <div className="mt-1 flex items-center justify-between text-[11px] text-muted-foreground">
<span> <span>
{searchLoading {searchLoading
? "Searching files..." ? t("preview.searchingFiles")
: `${matchesByPath.size} match${matchesByPath.size === 1 ? "" : "es"}`} : t("preview.match", { count: matchesByPath.size })}
</span> </span>
</div> </div>
)} )}
...@@ -211,7 +213,7 @@ export const FileTree = ({ appId, files }: FileTreeProps) => { ...@@ -211,7 +213,7 @@ export const FileTree = ({ appId, files }: FileTreeProps) => {
!searchError && !searchError &&
matchesByPath.size === 0 ? ( matchesByPath.size === 0 ? (
<div className="px-3 py-2 text-xs text-muted-foreground"> <div className="px-3 py-2 text-xs text-muted-foreground">
No files matched your search. {t("preview.noFilesMatchedSearch")}
</div> </div>
) : isSearchMode ? ( ) : isSearchMode ? (
<div className="px-2 py-1"> <div className="px-2 py-1">
......
...@@ -19,6 +19,7 @@ import { PublishPanel } from "./PublishPanel"; ...@@ -19,6 +19,7 @@ import { PublishPanel } from "./PublishPanel";
import { SecurityPanel } from "./SecurityPanel"; import { SecurityPanel } from "./SecurityPanel";
import { PlanPanel } from "./PlanPanel"; import { PlanPanel } from "./PlanPanel";
import { useSupabase } from "@/hooks/useSupabase"; import { useSupabase } from "@/hooks/useSupabase";
import { useTranslation } from "react-i18next";
interface ConsoleHeaderProps { interface ConsoleHeaderProps {
isOpen: boolean; isOpen: boolean;
...@@ -31,24 +32,29 @@ const ConsoleHeader = ({ ...@@ -31,24 +32,29 @@ const ConsoleHeader = ({
isOpen, isOpen,
onToggle, onToggle,
latestMessage, latestMessage,
}: ConsoleHeaderProps) => ( }: ConsoleHeaderProps) => {
<div const { t } = useTranslation("home");
onClick={onToggle} return (
className="flex items-start gap-2 px-4 py-1.5 border-t border-border cursor-pointer hover:bg-[var(--background-darkest)] transition-colors" <div
> onClick={onToggle}
<Logs size={16} className="mt-0.5" /> className="flex items-start gap-2 px-4 py-1.5 border-t border-border cursor-pointer hover:bg-[var(--background-darkest)] transition-colors"
<div className="flex flex-col"> >
<span className="text-sm font-medium">System Messages</span> <Logs size={16} className="mt-0.5" />
{!isOpen && latestMessage && ( <div className="flex flex-col">
<span className="text-xs text-gray-500 truncate max-w-[200px] md:max-w-[400px]"> <span className="text-sm font-medium">
{latestMessage} {t("preview.systemMessages")}
</span> </span>
)} {!isOpen && latestMessage && (
<span className="text-xs text-gray-500 truncate max-w-[200px] md:max-w-[400px]">
{latestMessage}
</span>
)}
</div>
<div className="flex-1" />
{isOpen ? <ChevronDown size={16} /> : <ChevronUp size={16} />}
</div> </div>
<div className="flex-1" /> );
{isOpen ? <ChevronDown size={16} /> : <ChevronUp size={16} />} };
</div>
);
// Main PreviewPanel component // Main PreviewPanel component
export function PreviewPanel() { export function PreviewPanel() {
......
...@@ -18,6 +18,7 @@ import { useStreamChat } from "@/hooks/useStreamChat"; ...@@ -18,6 +18,7 @@ import { useStreamChat } from "@/hooks/useStreamChat";
import { useCheckProblems } from "@/hooks/useCheckProblems"; import { useCheckProblems } from "@/hooks/useCheckProblems";
import { createProblemFixPrompt } from "@/shared/problem_prompt"; import { createProblemFixPrompt } from "@/shared/problem_prompt";
import { showError } from "@/lib/toast"; import { showError } from "@/lib/toast";
import { useTranslation } from "react-i18next";
interface ProblemItemProps { interface ProblemItemProps {
problem: Problem; problem: Problem;
...@@ -26,6 +27,7 @@ interface ProblemItemProps { ...@@ -26,6 +27,7 @@ interface ProblemItemProps {
} }
const ProblemItem = ({ problem, checked, onToggle }: ProblemItemProps) => { const ProblemItem = ({ problem, checked, onToggle }: ProblemItemProps) => {
const { t } = useTranslation(["home", "common"]);
return ( return (
<div <div
role="checkbox" role="checkbox"
...@@ -39,7 +41,7 @@ const ProblemItem = ({ problem, checked, onToggle }: ProblemItemProps) => { ...@@ -39,7 +41,7 @@ const ProblemItem = ({ problem, checked, onToggle }: ProblemItemProps) => {
onCheckedChange={onToggle} onCheckedChange={onToggle}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
className="mt-0.5" className="mt-0.5"
aria-label="Select problem" aria-label={t("home:preview.problems_panel.selectProblem")}
/> />
<div className="flex-shrink-0 mt-0.5"> <div className="flex-shrink-0 mt-0.5">
<XCircle size={16} className="text-red-500" /> <XCircle size={16} className="text-red-500" />
...@@ -82,6 +84,7 @@ const RecheckButton = ({ ...@@ -82,6 +84,7 @@ const RecheckButton = ({
className = "h-7 px-3 text-xs", className = "h-7 px-3 text-xs",
onBeforeRecheck, onBeforeRecheck,
}: RecheckButtonProps) => { }: RecheckButtonProps) => {
const { t } = useTranslation(["home", "common"]);
const { checkProblems, isChecking } = useCheckProblems(appId); const { checkProblems, isChecking } = useCheckProblems(appId);
const [showingFeedback, setShowingFeedback] = useState(false); const [showingFeedback, setShowingFeedback] = useState(false);
...@@ -115,7 +118,9 @@ const RecheckButton = ({ ...@@ -115,7 +118,9 @@ const RecheckButton = ({
size={14} size={14}
className={`mr-1 ${isShowingChecking ? "animate-spin" : ""}`} className={`mr-1 ${isShowingChecking ? "animate-spin" : ""}`}
/> />
{isShowingChecking ? "Checking..." : "Run checks"} {isShowingChecking
? t("home:preview.problems_panel.checkingProblems")
: t("home:preview.problems_panel.runChecks")}
</Button> </Button>
); );
}; };
...@@ -137,6 +142,7 @@ const ProblemsSummary = ({ ...@@ -137,6 +142,7 @@ const ProblemsSummary = ({
onFixSelected, onFixSelected,
onSelectAll, onSelectAll,
}: ProblemsSummaryProps) => { }: ProblemsSummaryProps) => {
const { t } = useTranslation(["home", "common"]);
const { problems } = problemReport; const { problems } = problemReport;
const totalErrors = problems.length; const totalErrors = problems.length;
...@@ -146,7 +152,7 @@ const ProblemsSummary = ({ ...@@ -146,7 +152,7 @@ const ProblemsSummary = ({
return ( return (
<div className="flex flex-col items-center justify-center h-32 text-center"> <div className="flex flex-col items-center justify-center h-32 text-center">
<p className="mt-6 text-sm font-medium text-muted-foreground mb-3"> <p className="mt-6 text-sm font-medium text-muted-foreground mb-3">
No problems found {t("home:preview.problems_panel.noProblemsFound")}
</p> </p>
<div className="w-12 h-12 rounded-full bg-green-100 dark:bg-green-900/20 flex items-center justify-center mb-3"> <div className="w-12 h-12 rounded-full bg-green-100 dark:bg-green-900/20 flex items-center justify-center mb-3">
<Check size={20} className="text-green-600 dark:text-green-400" /> <Check size={20} className="text-green-600 dark:text-green-400" />
...@@ -164,7 +170,7 @@ const ProblemsSummary = ({ ...@@ -164,7 +170,7 @@ const ProblemsSummary = ({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<XCircle size={16} className="text-red-500" /> <XCircle size={16} className="text-red-500" />
<span className="text-sm font-medium"> <span className="text-sm font-medium">
{totalErrors} {totalErrors === 1 ? "error" : "errors"} {t("home:preview.problems_panel.error", { count: totalErrors })}
</span> </span>
</div> </div>
)} )}
...@@ -178,7 +184,7 @@ const ProblemsSummary = ({ ...@@ -178,7 +184,7 @@ const ProblemsSummary = ({
onClick={onSelectAll} onClick={onSelectAll}
className="h-7 px-3 text-xs" className="h-7 px-3 text-xs"
> >
Select all {t("common:selectAll")}
</Button> </Button>
) : ( ) : (
<Button <Button
...@@ -187,7 +193,7 @@ const ProblemsSummary = ({ ...@@ -187,7 +193,7 @@ const ProblemsSummary = ({
onClick={onClearAll} onClick={onClearAll}
className="h-7 px-3 text-xs" className="h-7 px-3 text-xs"
> >
Clear all {t("common:clearAll")}
</Button> </Button>
)} )}
<Button <Button
...@@ -199,7 +205,9 @@ const ProblemsSummary = ({ ...@@ -199,7 +205,9 @@ const ProblemsSummary = ({
disabled={selectedCount === 0} disabled={selectedCount === 0}
> >
<Wrench size={14} className="mr-1" /> <Wrench size={14} className="mr-1" />
{`Fix ${selectedCount} ${selectedCount === 1 ? "problem" : "problems"}`} {t("home:preview.problems_panel.fixProblems", {
count: selectedCount,
})}
</Button> </Button>
</div> </div>
</div> </div>
...@@ -215,6 +223,7 @@ export function Problems() { ...@@ -215,6 +223,7 @@ export function Problems() {
} }
export function _Problems() { export function _Problems() {
const { t } = useTranslation(["home", "common"]);
const selectedAppId = useAtomValue(selectedAppIdAtom); const selectedAppId = useAtomValue(selectedAppIdAtom);
const { problemReport } = useCheckProblems(selectedAppId); const { problemReport } = useCheckProblems(selectedAppId);
const [selectedKeys, setSelectedKeys] = useState<Set<string>>(new Set()); const [selectedKeys, setSelectedKeys] = useState<Set<string>>(new Set());
...@@ -238,9 +247,11 @@ export function _Problems() { ...@@ -238,9 +247,11 @@ export function _Problems() {
<div className="w-16 h-16 rounded-full bg-[var(--background-darkest)] flex items-center justify-center mb-4"> <div className="w-16 h-16 rounded-full bg-[var(--background-darkest)] flex items-center justify-center mb-4">
<AlertTriangle size={24} className="text-muted-foreground" /> <AlertTriangle size={24} className="text-muted-foreground" />
</div> </div>
<h3 className="text-lg font-medium mb-2">No App Selected</h3> <h3 className="text-lg font-medium mb-2">
{t("home:preview.problems_panel.noAppSelectedTitle")}
</h3>
<p className="text-sm text-muted-foreground max-w-md"> <p className="text-sm text-muted-foreground max-w-md">
Select an app to view TypeScript problems and diagnostic information. {t("home:preview.problems_panel.noAppSelectedDescription")}
</p> </p>
</div> </div>
); );
...@@ -252,9 +263,11 @@ export function _Problems() { ...@@ -252,9 +263,11 @@ export function _Problems() {
<div className="w-16 h-16 rounded-full bg-[var(--background-darkest)] flex items-center justify-center mb-4"> <div className="w-16 h-16 rounded-full bg-[var(--background-darkest)] flex items-center justify-center mb-4">
<AlertTriangle size={24} className="text-muted-foreground" /> <AlertTriangle size={24} className="text-muted-foreground" />
</div> </div>
<h3 className="text-lg font-medium mb-2">No Problems Report</h3> <h3 className="text-lg font-medium mb-2">
{t("home:preview.problems_panel.noProblemsReportTitle")}
</h3>
<p className="text-sm text-muted-foreground max-w-md mb-4"> <p className="text-sm text-muted-foreground max-w-md mb-4">
Run checks to scan your app for TypeScript errors and other problems. {t("home:preview.problems_panel.noProblemsReportDescription")}
</p> </p>
<RecheckButton <RecheckButton
appId={selectedAppId} appId={selectedAppId}
......
...@@ -14,9 +14,11 @@ import { ...@@ -14,9 +14,11 @@ import {
} from "@/hooks/useAgentTools"; } from "@/hooks/useAgentTools";
import { Loader2, ChevronRight } from "lucide-react"; import { Loader2, ChevronRight } from "lucide-react";
import { AgentToolConsent } from "@/lib/schemas"; import { AgentToolConsent } from "@/lib/schemas";
import { useTranslation } from "react-i18next";
export function AgentToolsSettings() { export function AgentToolsSettings() {
const { tools, isLoading, setConsent } = useAgentTools(); const { tools, isLoading, setConsent } = useAgentTools();
const { t } = useTranslation("settings");
const [showAutoApproved, setShowAutoApproved] = useState(false); const [showAutoApproved, setShowAutoApproved] = useState(false);
const handleConsentChange = ( const handleConsentChange = (
...@@ -42,7 +44,7 @@ export function AgentToolsSettings() { ...@@ -42,7 +44,7 @@ export function AgentToolsSettings() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Configure permissions for Agent built-in tools. {t("agentPermissions.description")}
</p> </p>
{/* Requires approval tools */} {/* Requires approval tools */}
...@@ -70,7 +72,11 @@ export function AgentToolsSettings() { ...@@ -70,7 +72,11 @@ export function AgentToolsSettings() {
<ChevronRight <ChevronRight
className={`size-4 transition-transform ${showAutoApproved ? "rotate-90" : ""}`} className={`size-4 transition-transform ${showAutoApproved ? "rotate-90" : ""}`}
/> />
<span>Default allowed tools ({autoApprovedTools.length})</span> <span>
{t("agentPermissions.defaultAllowedTools", {
count: autoApprovedTools.length,
})}
</span>
</button> </button>
{showAutoApproved && ( {showAutoApproved && (
<div className="space-y-2 pl-6"> <div className="space-y-2 pl-6">
...@@ -103,6 +109,7 @@ function ToolConsentRow({ ...@@ -103,6 +109,7 @@ function ToolConsentRow({
consent: AgentToolConsent; consent: AgentToolConsent;
onConsentChange: (consent: AgentToolConsent) => void; onConsentChange: (consent: AgentToolConsent) => void;
}) { }) {
const { t } = useTranslation("settings");
return ( return (
<div className="border rounded p-3"> <div className="border rounded p-3">
<div className="flex items-center justify-between gap-4"> <div className="flex items-center justify-between gap-4">
...@@ -120,9 +127,13 @@ function ToolConsentRow({ ...@@ -120,9 +127,13 @@ function ToolConsentRow({
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="ask">Ask</SelectItem> <SelectItem value="ask">{t("agentPermissions.ask")}</SelectItem>
<SelectItem value="always">Always allow</SelectItem> <SelectItem value="always">
<SelectItem value="never">Never allow</SelectItem> {t("agentPermissions.alwaysAllow")}
</SelectItem>
<SelectItem value="never">
{t("agentPermissions.neverAllow")}
</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
......
...@@ -15,6 +15,7 @@ import { showError, showInfo, showSuccess } from "@/lib/toast"; ...@@ -15,6 +15,7 @@ import { showError, showInfo, showSuccess } from "@/lib/toast";
import { Edit2, Plus, Save, Trash2, X } from "lucide-react"; import { Edit2, Plus, Save, Trash2, X } from "lucide-react";
import { useDeepLink } from "@/contexts/DeepLinkContext"; import { useDeepLink } from "@/contexts/DeepLinkContext";
import { AddMcpServerDeepLinkData } from "@/ipc/deep_link_data"; import { AddMcpServerDeepLinkData } from "@/ipc/deep_link_data";
import { useTranslation } from "react-i18next";
type KeyValue = { key: string; value: string }; type KeyValue = { key: string; value: string };
...@@ -60,6 +61,7 @@ function KeyValueEditor({ ...@@ -60,6 +61,7 @@ function KeyValueEditor({
isSaving: boolean; isSaving: boolean;
itemLabel?: string; itemLabel?: string;
}) { }) {
const { t } = useTranslation(["settings", "common"]);
const initial = useMemo(() => parseJsonToArray(json), [json]); const initial = useMemo(() => parseJsonToArray(json), [json]);
const [envVars, setEnvVars] = useState<KeyValue[]>(initial); const [envVars, setEnvVars] = useState<KeyValue[]>(initial);
const [editingKey, setEditingKey] = useState<string | null>(null); const [editingKey, setEditingKey] = useState<string | null>(null);
...@@ -80,11 +82,11 @@ function KeyValueEditor({ ...@@ -80,11 +82,11 @@ function KeyValueEditor({
const handleAdd = async () => { const handleAdd = async () => {
if (!newKey.trim() || !newValue.trim()) { if (!newKey.trim() || !newValue.trim()) {
showError("Both key and value are required"); showError(t("toolsMcp.keyValueRequired"));
return; return;
} }
if (envVars.some((e) => e.key === newKey.trim())) { if (envVars.some((e) => e.key === newKey.trim())) {
showError(`${itemLabel} with this key already exists`); showError(t("settings:toolsMcp.duplicateKey"));
return; return;
} }
const next = [...envVars, { key: newKey.trim(), value: newValue.trim() }]; const next = [...envVars, { key: newKey.trim(), value: newValue.trim() }];
...@@ -104,7 +106,7 @@ function KeyValueEditor({ ...@@ -104,7 +106,7 @@ function KeyValueEditor({
const handleSaveEdit = async () => { const handleSaveEdit = async () => {
if (!editingKey) return; if (!editingKey) return;
if (!editingKeyValue.trim() || !editingValue.trim()) { if (!editingKeyValue.trim() || !editingValue.trim()) {
showError("Both key and value are required"); showError(t("toolsMcp.keyValueRequired"));
return; return;
} }
if ( if (
...@@ -112,7 +114,7 @@ function KeyValueEditor({ ...@@ -112,7 +114,7 @@ function KeyValueEditor({
(e) => e.key === editingKeyValue.trim() && e.key !== editingKey, (e) => e.key === editingKeyValue.trim() && e.key !== editingKey,
) )
) { ) {
showError(`${itemLabel} with this key already exists`); showError(t("settings:toolsMcp.duplicateKey"));
return; return;
} }
const next = envVars.map((e) => const next = envVars.map((e) =>
...@@ -144,10 +146,16 @@ function KeyValueEditor({ ...@@ -144,10 +146,16 @@ function KeyValueEditor({
{isAddingNew ? ( {isAddingNew ? (
<div className="space-y-3 p-3 border rounded-md bg-muted/50"> <div className="space-y-3 p-3 border rounded-md bg-muted/50">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor={`env-new-key-${id}`}>Key</Label> <Label htmlFor={`env-new-key-${id}`}>
{t("settings:toolsMcp.key")}
</Label>
<Input <Input
id={`env-new-key-${id}`} id={`env-new-key-${id}`}
placeholder={itemLabel === "Header" ? "Key" : "e.g., PATH"} placeholder={
itemLabel === "Header"
? t("settings:toolsMcp.key")
: t("settings:toolsMcp.keyPlaceholder")
}
value={newKey} value={newKey}
onChange={(e) => setNewKey(e.target.value)} onChange={(e) => setNewKey(e.target.value)}
autoFocus autoFocus
...@@ -155,11 +163,15 @@ function KeyValueEditor({ ...@@ -155,11 +163,15 @@ function KeyValueEditor({
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor={`env-new-value-${id}`}>Value</Label> <Label htmlFor={`env-new-value-${id}`}>
{t("settings:toolsMcp.value")}
</Label>
<Input <Input
id={`env-new-value-${id}`} id={`env-new-value-${id}`}
placeholder={ placeholder={
itemLabel === "Header" ? "Value" : "e.g., /usr/local/bin" itemLabel === "Header"
? t("settings:toolsMcp.value")
: t("settings:toolsMcp.valuePlaceholder")
} }
value={newValue} value={newValue}
onChange={(e) => setNewValue(e.target.value)} onChange={(e) => setNewValue(e.target.value)}
...@@ -173,7 +185,7 @@ function KeyValueEditor({ ...@@ -173,7 +185,7 @@ function KeyValueEditor({
disabled={disabled || isSaving} disabled={disabled || isSaving}
> >
<Save size={14} /> <Save size={14} />
{isSaving ? "Saving..." : "Save"} {isSaving ? t("common:saving") : t("common:save")}
</Button> </Button>
<Button <Button
onClick={() => { onClick={() => {
...@@ -185,7 +197,7 @@ function KeyValueEditor({ ...@@ -185,7 +197,7 @@ function KeyValueEditor({
size="sm" size="sm"
> >
<X size={14} /> <X size={14} />
Cancel {t("common:cancel")}
</Button> </Button>
</div> </div>
</div> </div>
...@@ -197,7 +209,7 @@ function KeyValueEditor({ ...@@ -197,7 +209,7 @@ function KeyValueEditor({
disabled={disabled} disabled={disabled}
> >
<Plus size={14} /> <Plus size={14} />
Add {itemLabel} {t("settings:toolsMcp.addEnvVar")}
</Button> </Button>
)} )}
......
/**
* Locale-aware formatting utilities using the browser's Intl API.
* These are available in Electron's Chromium without additional libraries.
*/
export function formatDate(date: Date, locale: string): string {
return new Intl.DateTimeFormat(locale, {
dateStyle: "medium",
timeStyle: "short",
}).format(date);
}
export function formatNumber(value: number, locale: string): string {
return new Intl.NumberFormat(locale).format(value);
}
const ONE_MINUTE_IN_MS = 1000 * 60;
const ONE_HOUR_IN_MS = ONE_MINUTE_IN_MS * 60;
const ONE_DAY_IN_MS = ONE_HOUR_IN_MS * 24;
export function formatRelativeTime(date: Date, locale: string): string {
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
const diffMs = date.getTime() - Date.now();
const absDiffMs = Math.abs(diffMs);
if (absDiffMs < ONE_HOUR_IN_MS) {
// Less than 1 hour — show minutes
const diffMinutes = Math.round(diffMs / ONE_MINUTE_IN_MS);
return rtf.format(diffMinutes, "minute");
}
if (absDiffMs < ONE_DAY_IN_MS) {
// Less than 1 day — show hours
const diffHours = Math.round(diffMs / ONE_HOUR_IN_MS);
return rtf.format(diffHours, "hour");
}
// Otherwise show days
const diffDays = Math.round(diffMs / ONE_DAY_IN_MS);
return rtf.format(diffDays, "day");
}
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
// Import all English locale bundles (bundled with the app)
import enCommon from "./locales/en/common.json";
import enSettings from "./locales/en/settings.json";
import enChat from "./locales/en/chat.json";
import enHome from "./locales/en/home.json";
import enErrors from "./locales/en/errors.json";
// Chinese Simplified
import zhCNCommon from "./locales/zh-CN/common.json";
import zhCNSettings from "./locales/zh-CN/settings.json";
import zhCNChat from "./locales/zh-CN/chat.json";
import zhCNHome from "./locales/zh-CN/home.json";
import zhCNErrors from "./locales/zh-CN/errors.json";
// Brazilian Portuguese
import ptBRCommon from "./locales/pt-BR/common.json";
import ptBRSettings from "./locales/pt-BR/settings.json";
import ptBRChat from "./locales/pt-BR/chat.json";
import ptBRHome from "./locales/pt-BR/home.json";
import ptBRErrors from "./locales/pt-BR/errors.json";
const resources = {
en: {
common: enCommon,
settings: enSettings,
chat: enChat,
home: enHome,
errors: enErrors,
},
"zh-CN": {
common: zhCNCommon,
settings: zhCNSettings,
chat: zhCNChat,
home: zhCNHome,
errors: zhCNErrors,
},
"pt-BR": {
common: ptBRCommon,
settings: ptBRSettings,
chat: ptBRChat,
home: ptBRHome,
errors: ptBRErrors,
},
};
i18n.use(initReactI18next).init({
resources,
lng: "en", // Default; overridden by user setting on startup
fallbackLng: "en",
defaultNS: "common",
ns: ["common", "settings", "chat", "home", "errors"],
interpolation: {
escapeValue: false, // React already escapes rendered output
},
});
export default i18n;
{
"newChat": "New Chat",
"recentChats": "Recent Chats",
"searchChats": "Search chats",
"loadingChats": "Loading chats...",
"noChatsFound": "No chats found",
"renameChat": "Rename Chat",
"deleteChat": "Delete Chat",
"renameChatDescription": "Enter a new name for this chat.",
"chatTitle": "Title",
"enterChatTitle": "Enter chat title...",
"chatRenamed": "Chat renamed successfully",
"failedRenameChat": "Failed to rename chat: {{error}}",
"failedCreateChat": "Failed to create new chat: {{error}}",
"chatDeleted": "Chat deleted successfully",
"failedDeleteChat": "Failed to delete chat: {{error}}",
"deleteChatConfirmation": "Are you sure you want to delete \"{{title}}\"? This action cannot be undone and all messages in this chat will be permanently lost.",
"deleteChatNote": "Note: Any code changes that have already been accepted will be kept.",
"scrollToBottom": "Scroll to bottom",
"dismissError": "Dismiss error",
"askDyadToBuild": "Ask Dyad to build...",
"cancelGeneration": "Cancel generation",
"sendMessage": "Send message",
"loadingProposal": "Loading proposal...",
"errorLoadingProposal": "Error loading proposal: {{message}}",
"visualEditor": "Visual editor (Pro)",
"visualEditorDescription": "Visual editing lets you make UI changes without AI and is a Pro-only feature",
"summarizeNewChatTip": "Creating a new chat makes the AI more focused and efficient",
"summarizeToNewChat": "Summarize to new chat",
"refactorFile": "Refactor {{path}} and make it more modular",
"refactorDescription": "Refactor the file to improve maintainability",
"writeCodeProperly": "Write code properly",
"writeCodeProperlyDescription": "Write code properly (useful when AI generates the code in the wrong format)",
"rebuildApp": "Rebuild app",
"rebuildAppDescription": "Rebuild the application",
"restartApp": "Restart app",
"restartAppDescription": "Restart the development server",
"refreshApp": "Refresh app",
"refreshAppDescription": "Refresh the application preview",
"keepGoing": "Keep going",
"tipProposal": "Tip proposal",
"securityRisks": "Security Risks",
"sqlQueries": "SQL Queries",
"packagesAdded": "Packages Added",
"serverFunctionsChanged": "Server Functions Changed",
"filesChanged": "Files Changed",
"securityRisksFound": "Security risks found",
"approve": "Approve",
"reject": "Reject",
"noChanges": "No changes",
"sqlQuery": "SQL Query",
"approved": "Approved",
"rejected": "Rejected",
"copy": "Copy",
"requestId": "Request ID",
"copyRequestId": "Copy Request ID",
"maxTokensUsed": "Max tokens used: {{count}}",
"allowToolToRun": "Allow {{toolName}} to run?",
"queueCount": "(1 of {{total}})",
"allowOnce": "Allow once",
"alwaysAllow": "Always allow",
"removeAttachment": "Remove attachment",
"recentChatActivity": "Recent chat activity",
"loadingActivity": "Loading activity...",
"noRecentChats": "No recent chats",
"uncommittedChanges": "You have {{count}} uncommitted change(s).",
"reviewAndCommit": "Review & commit",
"reviewCommitChanges": "Review & Commit Changes",
"reviewChangesDescription": "Review your changes and enter a commit message.",
"commitMessage": "Commit message",
"enterCommitMessage": "Enter commit message...",
"changedFiles": "Changed files ({{count}})",
"committing": "Committing...",
"commit": "Commit",
"added": "Added",
"modified": "Modified",
"deleted": "Deleted",
"renamed": "Renamed",
"updateFiles": "Update files",
"closeVersionPane": "Close version pane",
"versionHistory": "Version History",
"noVersionsAvailable": "No versions available",
"versionLabel": "Version {{number}} ({{hash}})",
"dbSnapshot": "DB",
"dbSnapshotExpired": "DB snapshot may have expired (older than 24 hours)",
"dbSnapshotAvailable": "Database snapshot available at timestamp {{timestamp}}",
"restoreToVersion": "Restore to this version",
"restoring": "Restoring...",
"restoringToVersion": "Restoring to this version...",
"revertedToVersion": "Reverted all changes back to version {{version}}",
"revertedToHash": "Reverted all changes back to version {{hash}}",
"contextUsed": "Used:",
"contextLimit": "Limit:",
"contextLimitWarning": "You're close to the context limit for this chat.",
"summarizeIntoNewChat": "Summarize into new chat",
"fixAllErrors": "Fix All Errors ({{count}})",
"todosCompleted": "{{completed}} of {{total}} To-dos Completed",
"allTasksCompleted": "All tasks completed",
"noTaskInProgress": "No task in progress",
"tokenUsage": "Tokens: {{count}}",
"tokenPercentUsed": "{{percent}}% of {{limit}}K",
"tokenUsageBreakdown": "Token Usage Breakdown",
"messageHistory": "Message History",
"codebase": "Codebase",
"mentionedApps": "Mentioned Apps",
"systemPrompt": "System Prompt",
"currentInput": "Current Input",
"total": "Total",
"failedCountTokens": "Failed to count tokens",
"optimizeTokens": "Optimize your tokens with",
"dyadProSmartContext": "Dyad Pro's Smart Context",
"attachFiles": "Attach files",
"themes": "Themes",
"noTheme": "No Theme",
"moreThemes": "More themes",
"newTheme": "New Theme",
"allCustomThemes": "All Custom Themes",
"showTokenUsage": "Show token usage",
"hideTokenUsage": "Hide token usage",
"attachFileContext": "Attach file as chat context",
"attachFileContextExample": "Example use case: screenshot of the app to point out a UI issue",
"uploadFileCodebase": "Upload file to codebase",
"uploadFileCodebaseExample": "Example use case: add an image to use for your app",
"dropFilesToAttach": "Drop files to attach",
"selectedComponents": "Selected Components ({{count}})",
"clearAllComponents": "Clear all selected components",
"deselectComponent": "Deselect component",
"chatMode": {
"openMenu": "Open mode menu",
"toggleShortcut": "{{shortcut}} to toggle",
"agentV2": "Agent v2",
"agentV2Description": "Better at bigger tasks and debugging",
"build": "Build",
"buildDescription": "Generate and edit code",
"ask": "Ask",
"askDescription": "Ask questions about the app",
"buildWithMcp": "Build with MCP",
"buildWithMcpDescription": "Like Build, but can use tools (MCP) to generate code"
},
"modelPicker": {
"modelLabel": "Model:",
"cloudModels": "Cloud Models",
"loadingModels": "Loading models...",
"noCloudModels": "No cloud models available",
"otherProviders": "Other AI providers",
"providerCount_one": "{{count}} provider",
"providerCount_other": "{{count}} providers",
"dyadTurbo": "Dyad Turbo",
"modelCount_one": "{{count}} model",
"modelCount_other": "{{count}} models",
"providerModels": "{{name}} Models",
"localModels": "Local models",
"localModelsDescription": "LM Studio, Ollama",
"ollama": "Ollama",
"ollamaModels": "Ollama Models",
"errorLoading": "Error loading",
"noneAvailable": "None available",
"isOllamaRunning": "Is Ollama running?",
"noLocalModels": "No local models found",
"ensureOllamaRunning": "Ensure Ollama is running and models are pulled.",
"lmStudio": "LM Studio",
"lmStudioModels": "LM Studio Models",
"noLoadedModels": "No loaded models found",
"ensureLMStudioRunning": "Ensure LM Studio is running and models are loaded."
},
"header": {
"switchingToLatest": "Please wait, switching back to latest version...",
"warningNotOnBranch": "Warning: You are not on a branch",
"notOnBranch": "You are not on a branch",
"checkoutInProgress": "Version checkout is currently in progress",
"checkoutMainBranch": "Checkout main branch, otherwise changes will not be saved properly",
"checkingBranch": "Checking branch...",
"onBranch": "You are on branch: {{name}}.",
"renameMasterToMain": "Rename master to main",
"renaming": "Renaming...",
"switchToMainBranch": "Switch to main branch",
"checkingOut": "Checking out...",
"masterRenamed": "Master branch renamed to main",
"versionCount": "Version {{count}}"
},
"errorBox": {
"accessWithDyadPro": "Access with Dyad Pro",
"orSwitchModel": "or switch to another model.",
"upgradeToDyadPro": "Upgrade to Dyad Pro",
"troubleshootingGuide": "Troubleshooting guide",
"invalidProKey": "Looks like you don't have a valid Dyad Pro key.",
"today": "today.",
"creditsUsed": "You have used all of your Dyad AI credits this month.",
"reloadOrUpgrade": "Reload or upgrade your subscription",
"getMoreCredits": "and get more AI credits",
"readDocs": "Read docs"
},
"promo": {
"tiredOfWaiting": "Tired of waiting on AI?",
"getDyadPro": "Get Dyad Pro",
"fasterEdits": "for faster edits with Turbo Edits.",
"saveOnCosts": "Save up to 3x on AI costs with",
"debuggingLoop": "Getting stuck in a debugging loop? Try a different model.",
"joinBuilders": "Join 600+ builders in the",
"dyadSubreddit": "Dyad subreddit",
"foundBug": "Found a bug? Click Help > Report a Bug",
"reportBadResponse": "Want to report a bad AI response? Upload the chat by clicking Help",
"watch": "Watch",
"creatorBuildApp": "the creator of Dyad build a Bible app step-by-step",
"gettingStuck": "Getting stuck? Read our",
"debuggingTips": "debugging tips",
"advancedTip": "Advanced tip: Customize your",
"aiRules": "AI rules",
"keepFocused": "Want to keep the AI focused? Start a new chat.",
"whatsNext": "Want to know what's next? Check out our",
"roadmap": "roadmap",
"likeDyad": "Like Dyad? Star it on",
"gitHub": "GitHub"
},
"consent": {
"toolWantsToRun": "Tool wants to run",
"requestsConsent": "requests your consent.",
"inputRequired": "Input Required"
},
"agentModeActivated": "Agent Mode Activated",
"agentModeTip": "Tip: Create a new chat to give the agent a clean context for better results.",
"neverShowAgain": "Never show again"
}
{
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
"confirm": "Confirm",
"loading": "Loading...",
"copyToClipboard": "Copy to clipboard",
"copied": "Copied!",
"close": "Close",
"back": "Back",
"goBack": "Go Back",
"reset": "Reset",
"error": "Error",
"success": "Success",
"warning": "Warning",
"info": "Info",
"yes": "Yes",
"no": "No",
"retry": "Retry",
"ok": "OK",
"create": "Create",
"update": "Update",
"edit": "Edit",
"add": "Add",
"remove": "Remove",
"search": "Search",
"refresh": "Refresh",
"enabled": "Enabled",
"disabled": "Disabled",
"saving": "Saving...",
"deleting": "Deleting...",
"creating": "Creating...",
"updating": "Updating...",
"connecting": "Connecting...",
"disconnecting": "Disconnecting...",
"importing": "Importing...",
"uploading": "Uploading...",
"preparing": "Preparing...",
"checking": "Checking...",
"notSet": "Not Set",
"set": "Set",
"custom": "Custom",
"ready": "Ready",
"needsSetup": "Needs Setup",
"accept": "Accept",
"decline": "Decline",
"showMore": "Show more",
"showLess": "Show less",
"selectAll": "Select all",
"clearAll": "Clear all",
"noResults": "No results found",
"itemCount_one": "{{count}} item",
"itemCount_other": "{{count}} items",
"pro": "Pro",
"free": "Free",
"recommended": "Recommended",
"experimental": "experimental",
"new": "New",
"builtIn": "Built-in",
"advanced": "Advanced",
"required": "Required",
"optional": "Optional"
}
{
"unknown": "An unknown error occurred",
"failedToCreate": "Failed to create: {{error}}",
"failedToUpdate": "Failed to update: {{error}}",
"failedToDelete": "Failed to delete: {{error}}",
"failedToLoad": "Failed to load: {{error}}",
"networkError": "A network error occurred. Please check your connection.",
"copyError": "Copy error message",
"errorOccurred": "An error occurred"
}
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论