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
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
RUN THE FOLLOWING CHECKS before you do a commit.
......
# Internationalization (i18n) Design
## Overview
This document describes the i18n system for Dyad. The goal is to support multiple languages across the Electron renderer and main process with type-safe translation keys, minimal boilerplate, and incremental adoption.
## Library: `react-i18next` + `i18next`
Use `react-i18next` (the de facto standard for React i18n) rather than building a custom solution.
Rationale:
- Mature ecosystem with broad community support
- Built-in pluralization, interpolation, nesting, and context support
- ICU message format support via plugin when needed
- TypeScript support for key autocompletion
- Works in both renderer (React) and main process (plain `i18next`)
- Lazy-loading of translation namespaces out of the box
### Dependencies
```
npm install i18next react-i18next
```
No additional plugins are needed initially. Translation files are bundled with the app (not fetched remotely), so no HTTP backend is required.
## Translation file structure
```
src/
i18n/
index.ts # i18next initialization
types.ts # Generated types for key autocompletion
locales/
en/
common.json # Shared strings (buttons, labels, generic)
settings.json # Settings page
chat.json # Chat UI
home.json # Home page
errors.json # Error/toast messages
zh-CN/
common.json
settings.json
...
ja/
common.json
...
```
### Namespace strategy
Split translations by feature area (namespace = one JSON file). This keeps files manageable and allows lazy-loading namespaces for routes that aren't immediately visible.
| Namespace | Scope |
| -------------- | ------------------------------------------- |
| `common` | Buttons, generic labels, confirmations, nav |
| `settings` | All settings page sections |
| `chat` | Chat input, messages, streaming indicators |
| `home` | Home page, app list, templates |
| `errors` | Toast messages, error dialogs, validation |
| `hub` | Hub/library/marketplace |
| `integrations` | GitHub, Supabase, Neon, Vercel connectors |
### Translation file format
Standard flat-key JSON with nesting where it aids organization:
```json
// en/settings.json
{
"title": "Settings",
"general": {
"title": "General",
"language": "Language",
"zoom": "Zoom Level",
"theme": "Theme"
},
"ai": {
"title": "AI",
"model": "Model",
"provider": "Provider",
"apiKey": "API Key"
},
"agent": {
"toolPermissions": "Configure permissions for Agent built-in tools.",
"permissionOption": {
"ask": "Ask",
"always": "Always allow",
"never": "Never allow"
}
}
}
```
```json
// en/common.json
{
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
"confirm": "Confirm",
"loading": "Loading...",
"copyToClipboard": "Copy to clipboard",
"copied": "Copied!",
"itemCount_one": "{{count}} item",
"itemCount_other": "{{count}} items"
}
```
Pluralization uses i18next's built-in suffix convention (`_one`, `_other`, `_zero`, etc.), which handles most languages. For languages with complex plural rules (e.g., Arabic, Polish), i18next resolves the correct form automatically.
## Initialization
```typescript
// src/i18n/index.ts
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
// Import all 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";
// ... other languages imported similarly
const resources = {
en: {
common: enCommon,
settings: enSettings,
chat: enChat,
home: enHome,
errors: enErrors,
},
// "zh-CN": { ... },
// "ja": { ... },
};
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
},
});
export default i18n;
```
Import `src/i18n/index.ts` at the app entry point (`src/main.tsx` or equivalent) before rendering.
## React usage
### `useTranslation` hook
```tsx
import { useTranslation } from "react-i18next";
function AgentToolsSettings() {
const { t } = useTranslation("settings");
return (
<div>
<p className="text-sm text-muted-foreground">
{t("agent.toolPermissions")}
</p>
<SelectItem value="ask">{t("agent.permissionOption.ask")}</SelectItem>
<SelectItem value="always">
{t("agent.permissionOption.always")}
</SelectItem>
</div>
);
}
```
### Multiple namespaces
```tsx
const { t } = useTranslation(["settings", "common"]);
// Keys from the first namespace work directly
t("general.title"); // → "General" (from settings)
// Keys from other namespaces use prefix
t("common:save"); // → "Save" (from common)
```
### Interpolation
```tsx
t("errors:fileNotFound", { path: "/some/file.txt" });
// "File not found: {{path}}" → "File not found: /some/file.txt"
```
### Components with embedded markup
Use the `Trans` component for strings that contain JSX:
```tsx
import { Trans } from "react-i18next";
<Trans i18nKey="home:welcome" t={t}>
Welcome to <strong>Dyad</strong>
</Trans>;
```
## Type safety
### Generating types from translation files
Create a type declaration so that `t("...")` calls get autocompletion and compile-time checking of keys.
```typescript
// src/i18n/types.ts
import "i18next";
import type enCommon from "./locales/en/common.json";
import type enSettings from "./locales/en/settings.json";
import type enChat from "./locales/en/chat.json";
import type enHome from "./locales/en/home.json";
import type enErrors from "./locales/en/errors.json";
declare module "i18next" {
interface CustomTypeOptions {
defaultNS: "common";
resources: {
common: typeof enCommon;
settings: typeof enSettings;
chat: typeof enChat;
home: typeof enHome;
errors: typeof enErrors;
};
}
}
```
This gives full autocomplete for `t("settings:general.title")` etc., and TypeScript errors for invalid keys.
## Language setting integration
### User settings
Add a `language` field to `UserSettingsSchema` in `src/lib/schemas.ts`:
```typescript
// In UserSettingsSchema
language: z.string().default("en"),
```
### Settings UI
Add a language selector to the General settings section (similar to the existing zoom selector):
```tsx
function LanguageSelector() {
const { t } = useTranslation("settings");
const [settings, setSettings] = useSettings();
const languages = [
{ value: "en", label: "English" },
{ value: "zh-CN", label: "简体中文" },
{ value: "ja", label: "日本語" },
{ value: "ko", label: "한국어" },
{ value: "es", label: "Español" },
{ value: "fr", label: "Français" },
{ value: "de", label: "Deutsch" },
];
const handleChange = (value: string) => {
i18n.changeLanguage(value);
setSettings({ ...settings, language: value });
};
return (
<Select value={settings.language} onValueChange={handleChange}>
{languages.map((lang) => (
<SelectItem key={lang.value} value={lang.value}>
{lang.label}
</SelectItem>
))}
</Select>
);
}
```
Language labels are shown in their native script (not translated) so users can always find their language regardless of the current UI language.
### Startup sync
On app startup, read the persisted language from user settings and call `i18n.changeLanguage(savedLanguage)` before the first render. This can be done in the settings loading hook or in `src/i18n/index.ts` by reading the setting synchronously.
## Electron main process strings
Some user-facing strings originate in the main process (e.g., native dialogs, menu items, error messages sent over IPC). For these:
1. Import `i18next` directly (without `react-i18next`) in main process code.
2. Share the same locale JSON files.
3. Initialize a separate i18next instance in `src/main/i18n.ts`.
```typescript
// src/main/i18n.ts
import i18n from "i18next";
import enErrors from "../i18n/locales/en/errors.json";
const mainI18n = i18n.createInstance();
mainI18n.init({
resources: { en: { errors: enErrors } },
lng: "en",
fallbackLng: "en",
defaultNS: "errors",
});
export default mainI18n;
```
When the user changes language in the renderer, send the new language to the main process via IPC so it can call `mainI18n.changeLanguage(lng)`.
## Date, number, and relative time formatting
Use the browser's `Intl` API (already available in Electron's Chromium) rather than adding a formatting library:
```typescript
// Utility in src/i18n/format.ts
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);
}
export function formatRelativeTime(date: Date, locale: string): string {
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
const diffMs = date.getTime() - Date.now();
const diffDays = Math.round(diffMs / (1000 * 60 * 60 * 24));
if (Math.abs(diffDays) < 1) {
const diffHours = Math.round(diffMs / (1000 * 60 * 60));
return rtf.format(diffHours, "hour");
}
return rtf.format(diffDays, "day");
}
```
The existing `date-fns` dependency also supports locale-aware formatting if more complex date operations are needed.
## Incremental adoption strategy
Migrating all strings at once is impractical. Instead, adopt incrementally:
### Phase 1: Infrastructure
- Install dependencies, create `src/i18n/` directory structure, initialize i18next.
- Add `language` to UserSettings schema.
- Create `en/common.json` with the most common shared strings (button labels, generic terms).
- Add the language selector to settings.
### Phase 2: Settings page
- Extract all settings page strings into `en/settings.json`.
- Replace hardcoded strings in settings components with `t()` calls.
- This is a self-contained area with many strings, good for validating the approach.
### Phase 3: Core UI
- Extract chat, home, and error strings into their respective namespace files.
- Convert toast messages in `src/lib/toast.tsx` and callers.
- Convert dialog and modal text.
### Phase 4: First additional language
- Add one complete translation (e.g., `zh-CN`) to validate the full loop.
- Fix any layout issues from longer/shorter translated strings.
- Verify RTL considerations if an RTL language is planned.
### Phase 5: Remaining strings and languages
- Extract remaining hardcoded strings (integrations, hub, etc.).
- Add more language translations.
- Set up a translation workflow (see below).
## Translation workflow
### For contributors
- English is the source of truth. All new strings are added to `en/*.json` first.
- Other language files must mirror the English key structure. Missing keys fall back to English automatically.
### Lint rule
Add a CI check that verifies all keys present in `en/*.json` exist in every other locale. Missing keys produce warnings (not errors, since fallback handles them), making it easy to see translation coverage.
### Extraction (optional tooling)
Consider `i18next-parser` to scan source files for `t("...")` calls and auto-generate/update the English JSON files. This catches strings that were added in code but not in the JSON.
```json
// package.json script
"i18n:extract": "i18next-parser 'src/**/*.{ts,tsx}'"
```
## Key conventions
| Convention | Example |
| ------------------------------ | ------------------------------------------------------ |
| Namespace maps to feature area | `settings`, `chat`, `common` |
| Nested keys use dot notation | `settings:general.title` |
| Action labels are imperative | `"save": "Save"`, `"delete": "Delete"` |
| Descriptions are sentence case | `"toolPermissions": "Configure permissions..."` |
| Plurals use i18next suffixes | `_one`, `_other` |
| Interpolation uses `{{var}}` | `"hello": "Hello, {{name}}"` |
| No string concatenation | Use interpolation instead of `t("a") + value + t("b")` |
## What NOT to translate
- Log messages and debug output (keep in English for debugging)
- IPC channel names and internal identifiers
- Database column names and schema identifiers
- Error stack traces
- Third-party API responses
......@@ -57,6 +57,7 @@
"geist": "^1.3.1",
"glob": "^11.0.2",
"html-to-image": "^1.11.13",
"i18next": "^25.8.0",
"isomorphic-git": "^1.30.1",
"jotai": "^2.12.2",
"jsonrepair": "^3.13.1",
......@@ -70,6 +71,7 @@
"posthog-js": "^1.236.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-i18next": "^16.5.4",
"react-konva": "^19.2.1",
"react-markdown": "^10.1.0",
"react-resizable-panels": "^2.1.7",
......@@ -14489,6 +14491,15 @@
"dev": true,
"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": {
"version": "1.11.13",
"resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz",
......@@ -14638,6 +14649,37 @@
"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": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
......@@ -19710,6 +19752,33 @@
"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": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
......@@ -22758,7 +22827,7 @@
"version": "5.9.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
......@@ -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": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz",
......
......@@ -94,6 +94,7 @@
"geist": "^1.3.1",
"glob": "^11.0.2",
"html-to-image": "^1.11.13",
"i18next": "^25.8.0",
"isomorphic-git": "^1.30.1",
"jotai": "^2.12.2",
"jsonrepair": "^3.13.1",
......@@ -107,6 +108,7 @@
"posthog-js": "^1.236.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-i18next": "^16.5.4",
"react-konva": "^19.2.1",
"react-markdown": "^10.1.0",
"react-resizable-panels": "^2.1.7",
......
......@@ -36,3 +36,8 @@ Actions performed using the default `GITHUB_TOKEN` (including labels added by `g
```bash
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";
import { chatInputValueAtom } from "@/atoms/chatAtoms";
import { usePlanEvents } from "@/hooks/usePlanEvents";
import { useZoomShortcuts } from "@/hooks/useZoomShortcuts";
import i18n from "@/i18n";
import { LanguageSchema } from "@/lib/schemas";
export default function RootLayout({ children }: { children: ReactNode }) {
const { refreshAppIframe } = useRunApp();
......@@ -62,6 +64,16 @@ export default function RootLayout({ children }: { children: ReactNode }) {
return () => {};
}, [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
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
......
......@@ -2,6 +2,7 @@ import { useSettings } from "@/hooks/useSettings";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { showInfo } from "@/lib/toast";
import { useTranslation } from "react-i18next";
export function AutoApproveSwitch({
showToast = true,
......@@ -9,6 +10,7 @@ export function AutoApproveSwitch({
showToast?: boolean;
}) {
const { settings, updateSettings } = useSettings();
const { t } = useTranslation("settings");
return (
<div className="flex items-center space-x-2">
<Switch
......@@ -22,7 +24,7 @@ export function AutoApproveSwitch({
}
}}
/>
<Label htmlFor="auto-approve">Auto-approve</Label>
<Label htmlFor="auto-approve">{t("workflow.autoApprove")}</Label>
</div>
);
}
import { useSettings } from "@/hooks/useSettings";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { useTranslation } from "react-i18next";
import { showInfo } from "@/lib/toast";
......@@ -10,6 +11,7 @@ export function AutoFixProblemsSwitch({
showToast?: boolean;
}) {
const { settings, updateSettings } = useSettings();
const { t } = useTranslation("settings");
return (
<div className="flex items-center space-x-2">
<Switch
......@@ -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>
);
}
......@@ -3,9 +3,11 @@ import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { toast } from "sonner";
import { ipc } from "@/ipc/types";
import { useTranslation } from "react-i18next";
export function AutoUpdateSwitch() {
const { settings, updateSettings } = useSettings();
const { t } = useTranslation("settings");
if (!settings) {
return null;
......@@ -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>
);
}
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate, useRouterState } from "@tanstack/react-router";
import { formatDistanceToNow } from "date-fns";
......@@ -34,6 +35,7 @@ import { ChatSearchDialog } from "./ChatSearchDialog";
import { useSelectChat } from "@/hooks/useSelectChat";
export function ChatList({ show }: { show?: boolean }) {
const { t } = useTranslation("chat");
const navigate = useNavigate();
const [selectedChatId, setSelectedChatId] = useAtom(selectedChatIdAtom);
const [selectedAppId] = useAtom(selectedAppIdAtom);
......@@ -115,7 +117,7 @@ export function ChatList({ show }: { show?: boolean }) {
await invalidateChats();
} catch (error) {
// DO A TOAST
showError(`Failed to create new chat: ${(error as any).toString()}`);
showError(t("failedCreateChat", { error: (error as any).toString() }));
}
} else {
// If no app is selected, navigate to home page
......@@ -126,7 +128,7 @@ export function ChatList({ show }: { show?: boolean }) {
const handleDeleteChat = async (chatId: number) => {
try {
await ipc.chat.deleteChat(chatId);
showSuccess("Chat deleted successfully");
showSuccess(t("chatDeleted"));
// If the deleted chat was selected, navigate to home
if (selectedChatId === chatId) {
......@@ -137,7 +139,7 @@ export function ChatList({ show }: { show?: boolean }) {
// Refresh the chat list
await invalidateChats();
} 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 }) {
className="overflow-y-auto h-[calc(100vh-112px)]"
data-testid="chat-list-container"
>
<SidebarGroupLabel>Recent Chats</SidebarGroupLabel>
<SidebarGroupLabel>{t("recentChats")}</SidebarGroupLabel>
<SidebarGroupContent>
<div className="flex flex-col space-y-4">
<Button
......@@ -185,7 +187,7 @@ export function ChatList({ show }: { show?: boolean }) {
className="flex items-center justify-start gap-2 mx-2 py-3"
>
<PlusCircle size={16} />
<span>New Chat</span>
<span>{t("newChat")}</span>
</Button>
<Button
onClick={() => setIsSearchDialogOpen(!isSearchDialogOpen)}
......@@ -194,16 +196,16 @@ export function ChatList({ show }: { show?: boolean }) {
data-testid="search-chats-button"
>
<Search size={16} />
<span>Search chats</span>
<span>{t("searchChats")}</span>
</Button>
{loading ? (
<div className="py-3 px-4 text-sm text-gray-500">
Loading chats...
{t("loadingChats")}
</div>
) : chats.length === 0 ? (
<div className="py-3 px-4 text-sm text-gray-500">
No chats found
{t("noChatsFound")}
</div>
) : (
<SidebarMenu className="space-y-1">
......@@ -226,7 +228,7 @@ export function ChatList({ show }: { show?: boolean }) {
>
<div className="flex flex-col w-full">
<span className="truncate">
{chat.title || "New Chat"}
{chat.title || t("newChat")}
</span>
<span className="text-xs text-gray-500">
{formatDistanceToNow(new Date(chat.createdAt), {
......@@ -262,19 +264,19 @@ export function ChatList({ show }: { show?: boolean }) {
className="px-3 py-2"
>
<Edit3 className="mr-2 h-4 w-4" />
<span>Rename Chat</span>
<span>{t("renameChat")}</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
handleDeleteChatClick(
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"
>
<Trash2 className="mr-2 h-4 w-4" />
<span>Delete Chat</span>
<span>{t("deleteChat")}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
......
import { useState, useRef, useEffect, useCallback } from "react";
import { useTranslation } from "react-i18next";
import { useAtomValue, useSetAtom } from "jotai";
import {
chatMessagesByIdAtom,
......@@ -35,6 +36,7 @@ export function ChatPanel({
isPreviewOpen,
onTogglePreview,
}: ChatPanelProps) {
const { t } = useTranslation("chat");
const messagesById = useAtomValue(chatMessagesByIdAtom);
const setMessagesById = useSetAtom(chatMessagesByIdAtom);
const [isVersionPaneOpen, setIsVersionPaneOpen] = useState(false);
......@@ -184,7 +186,7 @@ export function ChatPanel({
>
<ArrowDown className="h-4 w-4" />
</TooltipTrigger>
<TooltipContent>Scroll to bottom</TooltipContent>
<TooltipContent>{t("scrollToBottom")}</TooltipContent>
</Tooltip>
</div>
)}
......
import { useTranslation } from "react-i18next";
import React from "react";
import {
AlertDialog,
......@@ -19,31 +20,25 @@ interface CommunityCodeConsentDialogProps {
export const CommunityCodeConsentDialog: React.FC<
CommunityCodeConsentDialogProps
> = ({ isOpen, onAccept, onCancel }) => {
const { t } = useTranslation(["home", "common"]);
return (
<AlertDialog open={isOpen} onOpenChange={(open) => !open && onCancel()}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Community Code Notice</AlertDialogTitle>
<AlertDialogTitle>{t("home:communityCodeNotice")}</AlertDialogTitle>
<AlertDialogDescription className="space-y-3">
<p>
This code was created by a Dyad community member, not our core
team.
</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>
<p>{t("home:communityCodeWarning")}</p>
<p>{t("home:communityCodeRisk")}</p>
<p>{t("home:communityCodeReview")}</p>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={onCancel}>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={onAccept}>Accept</AlertDialogAction>
<AlertDialogCancel onClick={onCancel}>
{t("common:cancel")}
</AlertDialogCancel>
<AlertDialogAction onClick={onAccept}>
{t("common:accept")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
......
import { useTranslation } from "react-i18next";
import React, { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
......@@ -33,6 +34,7 @@ export function CreateAppDialog({
onOpenChange,
template,
}: CreateAppDialogProps) {
const { t } = useTranslation(["home", "common"]);
const setSelectedAppId = useSetAtom(selectedAppIdAtom);
const [appName, setAppName] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
......@@ -84,27 +86,27 @@ export function CreateAppDialog({
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Create New App</DialogTitle>
<DialogTitle>{t("home:createNewApp")}</DialogTitle>
<DialogDescription>
{`Create a new app using the ${template?.title} template.`}
{t("home:createAppUsingTemplate", { template: template?.title })}
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit}>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="appName">App Name</Label>
<Label htmlFor="appName">{t("home:appName")}</Label>
<Input
id="appName"
value={appName}
onChange={(e) => setAppName(e.target.value)}
placeholder="Enter app name..."
placeholder={t("home:enterAppName")}
className={nameExists ? "border-red-500" : ""}
disabled={isSubmitting}
/>
{nameExists && (
<p className="text-sm text-red-500">
An app with this name already exists
{t("home:appNameAlreadyExists")}
</p>
)}
</div>
......@@ -117,7 +119,7 @@ export function CreateAppDialog({
onClick={() => onOpenChange(false)}
disabled={isSubmitting}
>
Cancel
{t("common:cancel")}
</Button>
<Button
type="submit"
......@@ -127,7 +129,7 @@ export function CreateAppDialog({
{isSubmitting && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
)}
{isSubmitting ? "Creating..." : "Create App"}
{isSubmitting ? t("common:creating") : t("home:createApp")}
</Button>
</DialogFooter>
</form>
......
......@@ -9,10 +9,12 @@ import {
} from "@/components/ui/select";
import type { ChatMode } from "@/lib/schemas";
import { isDyadProEnabled, getEffectiveDefaultChatMode } from "@/lib/schemas";
import { useTranslation } from "react-i18next";
export function DefaultChatModeSelector() {
const { settings, updateSettings, envVars } = useSettings();
const { isQuotaExceeded, isLoading: isQuotaLoading } = useFreeAgentQuota();
const { t } = useTranslation("settings");
if (!settings) {
return null;
......@@ -53,7 +55,7 @@ export function DefaultChatModeSelector() {
htmlFor="default-chat-mode"
className="text-sm font-medium text-gray-700 dark:text-gray-300"
>
Default Chat Mode
{t("workflow.defaultChatMode")}
</label>
<Select
value={effectiveDefault}
......@@ -89,7 +91,7 @@ export function DefaultChatModeSelector() {
</Select>
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
The chat mode used when creating new chats.
{t("workflow.defaultChatModeDescription")}
</div>
</div>
);
......
import { useTranslation } from "react-i18next";
import React from "react";
import { Trash2, Loader2 } from "lucide-react";
import {
......@@ -28,6 +29,7 @@ export function DeleteConfirmationDialog({
trigger,
isDeleting = false,
}: DeleteConfirmationDialogProps) {
const { t } = useTranslation(["home", "common"]);
return (
<AlertDialog>
{trigger ? (
......@@ -37,29 +39,32 @@ export function DeleteConfirmationDialog({
className={buttonVariants({ variant: "ghost", size: "icon" })}
data-testid="delete-prompt-button"
disabled={isDeleting}
title={`Delete ${itemType.toLowerCase()}`}
title={`${t("common:delete")} ${itemType.toLowerCase()}`}
>
<Trash2 className="h-4 w-4" />
</AlertDialogTrigger>
)}
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete {itemType}</AlertDialogTitle>
<AlertDialogTitle>
{t("home:deleteItemTitle", { itemType })}
</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete "{itemName}"? This action cannot be
undone.
{t("home:deleteItemConfirmation", { itemName })}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel>
<AlertDialogCancel disabled={isDeleting}>
{t("common:cancel")}
</AlertDialogCancel>
<AlertDialogAction onClick={onDelete} disabled={isDeleting}>
{isDeleting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Deleting...
{t("common:deleting")}
</>
) : (
"Delete"
t("common:delete")
)}
</AlertDialogAction>
</AlertDialogFooter>
......
import { useTranslation } from "react-i18next";
import {
AlertDialog,
AlertDialogAction,
......@@ -27,6 +28,7 @@ export function ForceCloseDialog({
onClose,
performanceData,
}: ForceCloseDialogProps) {
const { t } = useTranslation(["home", "common"]);
const formatTimestamp = (timestamp: number) => {
return new Date(timestamp).toLocaleString();
};
......@@ -37,19 +39,16 @@ export function ForceCloseDialog({
<AlertDialogHeader>
<div className="flex items-center gap-2">
<AlertTriangle className="h-5 w-5 text-yellow-500" />
<AlertDialogTitle>Force Close Detected</AlertDialogTitle>
<AlertDialogTitle>{t("home:forceCloseDetected")}</AlertDialogTitle>
</div>
<AlertDialogDescription render={<div />}>
<div className="space-y-4 pt-2 text-muted-foreground">
<div className="text-base">
The app was not closed properly the last time it was running.
This could indicate a crash or unexpected termination.
</div>
<div className="text-base">{t("home:forceCloseDescription")}</div>
{performanceData && (
<div className="rounded-lg border bg-muted/50 p-4 space-y-3">
<div className="font-semibold text-sm text-foreground">
Last Known State:{" "}
{t("home:lastKnownState")}{" "}
<span className="font-normal text-muted-foreground">
{formatTimestamp(performanceData.timestamp)}
</span>
......@@ -59,18 +58,22 @@ export function ForceCloseDialog({
{/* Process Metrics */}
<div className="space-y-2">
<div className="font-medium text-foreground">
Process Metrics
{t("home:processMetrics")}
</div>
<div className="space-y-1">
<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">
{performanceData.memoryUsageMB} MB
</span>
</div>
{performanceData.cpuUsagePercent !== undefined && (
<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">
{performanceData.cpuUsagePercent}%
</span>
......@@ -84,7 +87,7 @@ export function ForceCloseDialog({
performanceData.systemCpuPercent !== undefined) && (
<div className="space-y-2">
<div className="font-medium text-foreground">
System Metrics
{t("home:systemMetrics")}
</div>
<div className="space-y-1">
{performanceData.systemMemoryUsageMB !== undefined &&
......@@ -92,7 +95,7 @@ export function ForceCloseDialog({
undefined && (
<div className="flex justify-between">
<span className="text-muted-foreground">
Memory:
{t("home:memory")}
</span>
<span className="font-mono">
{performanceData.systemMemoryUsageMB} /{" "}
......@@ -103,7 +106,7 @@ export function ForceCloseDialog({
{performanceData.systemCpuPercent !== undefined && (
<div className="flex justify-between">
<span className="text-muted-foreground">
CPU:
{t("home:cpu")}
</span>
<span className="font-mono">
{performanceData.systemCpuPercent}%
......@@ -120,7 +123,9 @@ export function ForceCloseDialog({
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogAction onClick={onClose}>OK</AlertDialogAction>
<AlertDialogAction onClick={onClose}>
{t("common:ok")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
......
import { useState, useEffect, useCallback, useRef } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import {
Github,
......@@ -75,6 +76,7 @@ function ConnectedGitHubConnector({
triggerAutoSync,
onAutoSyncComplete,
}: ConnectedGitHubConnectorProps) {
const { t } = useTranslation(["home", "common"]);
const [isSyncing, setIsSyncing] = useState(false);
const [syncError, setSyncError] = useState<string | null>(null);
const [syncSuccess, setSyncSuccess] = useState<boolean>(false);
......@@ -98,7 +100,9 @@ function ConnectedGitHubConnector({
await ipc.github.disconnect({ appId });
refreshApp();
} catch (err: any) {
setDisconnectError(err.message || "Failed to disconnect repository.");
setDisconnectError(
err.message || t("integrations.github.failedDisconnectRepo"),
);
} finally {
setIsDisconnecting(false);
}
......
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { Github } from "lucide-react";
import { useSettings } from "@/hooks/useSettings";
import { showSuccess, showError } from "@/lib/toast";
export function GitHubIntegration() {
const { t } = useTranslation(["home", "common"]);
const { settings, updateSettings } = useSettings();
const [isDisconnecting, setIsDisconnecting] = useState(false);
......@@ -16,14 +18,12 @@ export function GitHubIntegration() {
githubUser: undefined,
});
if (result) {
showSuccess("Successfully disconnected from GitHub");
showSuccess(t("integrations.github.disconnected"));
} else {
showError("Failed to disconnect from GitHub");
showError(t("integrations.github.failedDisconnect"));
}
} catch (err: any) {
showError(
err.message || "An error occurred while disconnecting from GitHub",
);
showError(err.message || t("integrations.github.errorDisconnect"));
} finally {
setIsDisconnecting(false);
}
......@@ -39,10 +39,10 @@ export function GitHubIntegration() {
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300">
GitHub Integration
{t("integrations.github.title")}
</h3>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
Your account is connected to GitHub.
{t("integrations.github.connected")}
</p>
</div>
......@@ -53,7 +53,9 @@ export function GitHubIntegration() {
disabled={isDisconnecting}
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" />
</Button>
</div>
......
import { useTranslation } from "react-i18next";
import { useState, useEffect } from "react";
import {
Dialog,
......@@ -42,6 +43,7 @@ interface ImportAppDialogProps {
export const AI_RULES_PROMPT =
"Generate an AI_RULES.md file for this app. Describe the tech stack in 5-10 bullet points and describe clear rules about what libraries to use for what.";
export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
const { t } = useTranslation(["home", "common"]);
const [selectedPath, setSelectedPath] = useState<string | null>(null);
const [hasAiRules, setHasAiRules] = useState<boolean | null>(null);
const [customAppName, setCustomAppName] = useState<string>("");
......@@ -92,7 +94,9 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
});
setGithubNameExists(result.exists);
} catch (error: unknown) {
showError("Failed to check app name: " + (error as any).toString());
showError(
t("home:failedCheckAppName", { error: (error as any).toString() }),
);
} finally {
setIsCheckingGithubName(false);
}
......@@ -120,7 +124,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
return;
}
setSelectedAppId(result.app.id);
showSuccess(`Successfully imported ${result.app.name}`);
showSuccess(t("home:successfullyImported", { name: result.app.name }));
const chatId = await ipc.chat.createChat(result.app.id);
navigate({ to: "/chat", search: { id: chatId } });
if (!result.hasAiRules) {
......@@ -131,7 +135,9 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
}
onClose();
} catch (error: unknown) {
showError("Failed to import repository: " + (error as any).toString());
showError(
t("home:failedImportRepo", { error: (error as any).toString() }),
);
} finally {
setImporting(false);
}
......@@ -154,7 +160,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
return;
}
setSelectedAppId(result.app.id);
showSuccess(`Successfully imported ${result.app.name}`);
showSuccess(t("home:successfullyImported", { name: result.app.name }));
const chatId = await ipc.chat.createChat(result.app.id);
navigate({ to: "/chat", search: { id: chatId } });
if (!result.hasAiRules) {
......@@ -165,7 +171,9 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
}
onClose();
} catch (error: unknown) {
showError("Failed to import repository: " + (error as any).toString());
showError(
t("home:failedImportRepo", { error: (error as any).toString() }),
);
} finally {
setImporting(false);
}
......@@ -184,7 +192,9 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
});
setGithubNameExists(result.exists);
} catch (error: unknown) {
showError("Failed to check app name: " + (error as any).toString());
showError(
t("home:failedCheckAppName", { error: (error as any).toString() }),
);
} finally {
setIsCheckingGithubName(false);
}
......@@ -247,9 +257,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
},
onSuccess: async (result) => {
showSuccess(
!hasAiRules
? "App imported successfully. Dyad will automatically generate an AI_RULES.md now."
: "App imported successfully",
!hasAiRules ? t("home:appImportedWithRules") : t("home:appImported"),
);
onClose();
......@@ -304,17 +312,16 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-2xl w-[calc(100vw-2rem)] max-h-[98vh] overflow-y-auto flex flex-col p-0">
<DialogHeader className="sticky top-0 bg-background border-b px-6 py-4">
<DialogTitle>Import App</DialogTitle>
<DialogTitle>{t("home:importApp")}</DialogTitle>
<DialogDescription className="text-sm">
Import existing app from local folder or clone from Github.
{t("home:importAppDescription")}
</DialogDescription>
</DialogHeader>
<div className="px-6 pb-6 overflow-y-auto flex-1">
<Alert className="border-blue-500/20 text-blue-500 mb-2">
<Info className="h-4 w-4 flex-shrink-0" />
<AlertDescription className="text-xs sm:text-sm">
App import is an experimental feature. If you encounter any
issues, please report them using the Help button.
{t("home:importExperimental")}
</AlertDescription>
</Alert>
<Tabs defaultValue="local-folder" className="w-full">
......@@ -323,20 +330,22 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
value="local-folder"
className="text-xs sm:text-sm px-2 py-2"
>
Local Folder
{t("home:localFolder")}
</TabsTrigger>
<TabsTrigger
value="github-repos"
className="text-xs sm:text-sm px-2 py-2"
>
<span className="hidden sm:inline">Your GitHub Repos</span>
<span className="sm:hidden">GitHub Repos</span>
<span className="hidden sm:inline">
{t("home:yourGithubRepos")}
</span>
<span className="sm:hidden">{t("home:githubRepos")}</span>
</TabsTrigger>
<TabsTrigger
value="github-url"
className="text-xs sm:text-sm px-2 py-2"
>
GitHub URL
{t("home:githubUrl")}
</TabsTrigger>
</TabsList>
<TabsContent value="local-folder" className="space-y-4">
......@@ -353,8 +362,8 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
<Folder className="mr-2 h-4 w-4" />
)}
{selectFolderMutation.isPending
? "Selecting folder..."
: "Select Folder"}
? t("home:selectingFolder")
: t("home:selectFolder")}
</Button>
) : (
<div className="space-y-4">
......@@ -362,7 +371,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
<div className="flex items-start justify-between gap-2">
<div className="min-w-0 flex-1 overflow-hidden">
<p className="text-sm font-medium mb-1">
Selected folder:
{t("home:selectedFolder")}
</p>
<p className="text-xs sm:text-sm text-muted-foreground break-words">
{selectedPath}
......@@ -376,7 +385,9 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
disabled={importAppMutation.isPending}
>
<X className="h-4 w-4" />
<span className="sr-only">Clear selection</span>
<span className="sr-only">
{t("home:clearSelection")}
</span>
</Button>
</div>
</div>
......@@ -395,29 +406,24 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
htmlFor="copy-to-dyad-apps"
className="text-xs sm:text-sm cursor-pointer"
>
Copy to the{" "}
<code className="bg-muted px-1 py-0.5 rounded text-xs">
dyad-apps
</code>{" "}
folder
{t("home:copyToDyadApps")}
</label>
</div>
<div className="space-y-2">
{nameExists && (
<p className="text-xs sm:text-sm text-yellow-500">
An app with this name already exists. Please choose a
different name:
{t("home:appNameExists")}
</p>
)}
<div className="relative">
<Label className="text-xs sm:text-sm ml-2 mb-2">
App name
{t("home:appName")}
</Label>
<Input
value={customAppName}
onChange={handleAppNameChange}
placeholder="Enter new app name"
placeholder={t("home:enterNewAppName")}
className="w-full pr-8 text-sm"
disabled={importAppMutation.isPending}
/>
......@@ -432,12 +438,12 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
<Accordion>
<AccordionItem value="advanced-options">
<AccordionTrigger className="text-xs sm:text-sm hover:no-underline">
Advanced options
{t("home:advancedOptions")}
</AccordionTrigger>
<AccordionContent className="space-y-4">
<div className="grid gap-2">
<Label className="text-xs sm:text-sm ml-2 mb-2">
Install command
{t("home:installCommand")}
</Label>
<Input
value={installCommand}
......@@ -451,7 +457,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
</div>
<div className="grid gap-2">
<Label className="text-xs sm:text-sm ml-2 mb-2">
Start command
{t("home:startCommand")}
</Label>
<Input
value={startCommand}
......@@ -463,7 +469,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
</div>
{!commandsValid && (
<p className="text-xs sm:text-sm text-red-500">
Both commands are required when customizing.
{t("home:bothCommandsRequired")}
</p>
)}
</AccordionContent>
......@@ -479,8 +485,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
<Info className="h-4 w-4" />
</span>
<AlertDescription className="text-xs sm:text-sm">
No AI_RULES.md found. Dyad will automatically generate
one after importing.
{t("home:noAiRulesFound")}
</AlertDescription>
</Alert>
)}
......@@ -488,7 +493,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
{importAppMutation.isPending && (
<div className="flex items-center justify-center space-x-2 text-xs sm:text-sm text-muted-foreground animate-pulse">
<Loader2 className="h-4 w-4 animate-spin" />
<span>Importing app...</span>
<span>{t("home:importingApp")}</span>
</div>
)}
</div>
......@@ -502,7 +507,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
disabled={importAppMutation.isPending}
className="w-full sm:w-auto"
>
Cancel
{t("common:cancel")}
</Button>
<Button
onClick={handleImport}
......@@ -514,7 +519,11 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
}
className="w-full sm:w-auto min-w-[80px]"
>
{importAppMutation.isPending ? <>Importing...</> : "Import"}
{importAppMutation.isPending ? (
<>{t("common:importing")}</>
) : (
t("home:import")
)}
</Button>
</DialogFooter>
</TabsContent>
......@@ -538,12 +547,12 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
<div className="space-y-2">
<Label className="text-xs sm:text-sm ml-2 mb-2">
App name (optional)
{t("home:appNameOptional")}
</Label>
<Input
value={githubAppName}
onChange={handleGithubAppNameChange}
placeholder="Leave empty to use repository name"
placeholder={t("home:leaveEmptyForRepo")}
className="w-full pr-8 text-sm"
disabled={importing}
/>
......@@ -554,8 +563,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
)}
{githubNameExists && (
<p className="text-xs sm:text-sm text-yellow-500">
An app with this name already exists. Please choose a
different name.
{t("home:appNameExists")}
</p>
)}
</div>
......@@ -563,7 +571,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
<div className="flex flex-col space-y-2 max-h-64 overflow-y-auto overflow-x-hidden">
{!loading && repos.length === 0 && (
<p className="text-xs sm:text-sm text-muted-foreground text-center py-4">
No repositories found
{t("home:noRepositoriesFound")}
</p>
)}
{repos.map((repo) => (
......@@ -589,7 +597,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
{importing ? (
<Loader2 className="animate-spin h-4 w-4" />
) : (
"Import"
t("home:import")
)}
</Button>
</div>
......@@ -601,12 +609,12 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
<Accordion>
<AccordionItem value="advanced-options">
<AccordionTrigger className="text-xs sm:text-sm hover:no-underline">
Advanced options
{t("home:advancedOptions")}
</AccordionTrigger>
<AccordionContent className="space-y-4">
<div className="grid gap-2">
<Label className="text-xs sm:text-sm">
Install command
{t("home:installCommand")}
</Label>
<Input
value={installCommand}
......@@ -620,7 +628,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
</div>
<div className="grid gap-2">
<Label className="text-xs sm:text-sm">
Start command
{t("home:startCommand")}
</Label>
<Input
value={startCommand}
......@@ -634,7 +642,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
</div>
{!commandsValid && (
<p className="text-xs sm:text-sm text-red-500">
Both commands are required when customizing.
{t("home:bothCommandsRequired")}
</p>
)}
</AccordionContent>
......@@ -647,9 +655,11 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
</TabsContent>
<TabsContent value="github-url" className="space-y-4">
<div className="space-y-2">
<Label className="text-xs sm:text-sm">Repository URL</Label>
<Label className="text-xs sm:text-sm">
{t("home:repositoryUrl")}
</Label>
<Input
placeholder="https://github.com/user/repo.git"
placeholder={t("home:repositoryUrlPlaceholder")}
value={url}
onChange={(e) => setUrl(e.target.value)}
disabled={importing}
......@@ -659,12 +669,12 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
</div>
<div className="space-y-2">
<Label className="text-xs sm:text-sm">
App name (optional)
{t("home:appNameOptional")}
</Label>
<Input
value={githubAppName}
onChange={handleGithubAppNameChange}
placeholder="Leave empty to use repository name"
placeholder={t("home:leaveEmptyForRepo")}
disabled={importing}
className="text-sm"
/>
......@@ -675,8 +685,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
)}
{githubNameExists && (
<p className="text-xs sm:text-sm text-yellow-500">
An app with this name already exists. Please choose a
different name.
{t("home:appNameExists")}
</p>
)}
</div>
......@@ -684,12 +693,12 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
<Accordion>
<AccordionItem value="advanced-options">
<AccordionTrigger className="text-xs sm:text-sm hover:no-underline">
Advanced options
{t("home:advancedOptions")}
</AccordionTrigger>
<AccordionContent className="space-y-4">
<div className="grid gap-2">
<Label className="text-xs sm:text-sm">
Install command
{t("home:installCommand")}
</Label>
<Input
value={installCommand}
......@@ -701,7 +710,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
</div>
<div className="grid gap-2">
<Label className="text-xs sm:text-sm">
Start command
{t("home:startCommand")}
</Label>
<Input
value={startCommand}
......@@ -713,7 +722,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
</div>
{!commandsValid && (
<p className="text-xs sm:text-sm text-red-500">
Both commands are required when customizing.
{t("home:bothCommandsRequired")}
</p>
)}
</AccordionContent>
......@@ -728,10 +737,10 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
{importing ? (
<>
<Loader2 className="animate-spin mr-2 h-4 w-4" />
Importing...
{t("common:importing")}
</>
) : (
"Import"
t("home:import")
)}
</Button>
</TabsContent>
......
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 {
SelectValue,
} from "@/components/ui/select";
import { MAX_CHAT_TURNS_IN_CONTEXT } from "@/constants/settings_constants";
import { useTranslation } from "react-i18next";
interface OptionInfo {
value: string;
......@@ -49,6 +50,7 @@ const options: OptionInfo[] = [
export const MaxChatTurnsSelector: React.FC = () => {
const { settings, updateSettings } = useSettings();
const { t } = useTranslation("settings");
const handleValueChange = (value: string) => {
if (value === "default") {
......@@ -74,14 +76,14 @@ export const MaxChatTurnsSelector: React.FC = () => {
htmlFor="max-chat-turns"
className="text-sm font-medium text-gray-700 dark:text-gray-300"
>
Maximum number of chat turns used in context
{t("ai.maxChatTurns")}
</label>
<Select
value={currentValue}
onValueChange={(v) => v && handleValueChange(v)}
>
<SelectTrigger className="w-[180px]" id="max-chat-turns">
<SelectValue placeholder="Select turns" />
<SelectValue placeholder={t("ai.selectMaxChatTurns")} />
</SelectTrigger>
<SelectContent>
{options.map((option) => (
......
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { ipc } from "@/ipc/types";
import { toast } from "sonner";
......@@ -10,6 +11,7 @@ import { useTheme } from "@/contexts/ThemeContext";
import { NeonDisconnectButton } from "@/components/NeonDisconnectButton";
export function NeonConnector() {
const { t } = useTranslation("home");
const { settings, refreshSettings } = useSettings();
const { lastDeepLink, clearLastDeepLink } = useDeepLink();
const { isDarkMode } = useTheme();
......@@ -18,7 +20,7 @@ export function NeonConnector() {
const handleDeepLink = async () => {
if (lastDeepLink?.type === "neon-oauth-return") {
await refreshSettings();
toast.success("Successfully connected to Neon!");
toast.success(t("integrations.neon.connectedSuccess"));
clearLastDeepLink();
}
};
......@@ -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 items-start justify-between">
<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
variant="outline"
onClick={() => {
......@@ -43,7 +47,7 @@ export function NeonConnector() {
</Button>
</div>
<p className="text-sm text-gray-500 dark:text-gray-400 pb-3">
You are connected to Neon Database
{t("integrations.neon.connectedToNeon")}
</p>
<NeonDisconnectButton />
</div>
......@@ -54,9 +58,11 @@ export function NeonConnector() {
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 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">
Neon Database has a good free tier with backups and up to 10 projects.
{t("integrations.neon.freeTier")}
</p>
<div
onClick={async () => {
......@@ -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"
data-testid="connect-neon-button"
>
<span className="mr-2">Connect to</span>
<span className="mr-2">{t("integrations.neon.connectTo")}</span>
<NeonSvg isDarkMode={isDarkMode} />
</div>
</div>
......
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { toast } from "sonner";
import { useSettings } from "@/hooks/useSettings";
......@@ -7,6 +8,7 @@ interface NeonDisconnectButtonProps {
}
export function NeonDisconnectButton({ className }: NeonDisconnectButtonProps) {
const { t } = useTranslation("home");
const { updateSettings, settings } = useSettings();
const handleDisconnect = async () => {
......@@ -14,10 +16,10 @@ export function NeonDisconnectButton({ className }: NeonDisconnectButtonProps) {
await updateSettings({
neon: undefined,
});
toast.success("Disconnected from Neon successfully");
toast.success(t("integrations.neon.disconnected"));
} catch (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) {
className={className}
size="sm"
>
Disconnect from Neon
{t("integrations.neon.disconnect")}
</Button>
);
}
import { useTranslation } from "react-i18next";
import { useSettings } from "@/hooks/useSettings";
import { NeonDisconnectButton } from "@/components/NeonDisconnectButton";
export function NeonIntegration() {
const { t } = useTranslation("home");
const { settings } = useSettings();
const isConnected = !!settings?.neon?.accessToken;
......@@ -14,10 +16,10 @@ export function NeonIntegration() {
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300">
Neon Integration
{t("integrations.neon.title")}
</h3>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
Your account is connected to Neon.
{t("integrations.neon.connected")}
</p>
</div>
......
......@@ -5,9 +5,11 @@ import { useSettings } from "@/hooks/useSettings";
import { showError, showSuccess } from "@/lib/toast";
import { ipc } from "@/ipc/types";
import { FolderOpen, RotateCcw, CheckCircle, AlertCircle } from "lucide-react";
import { useTranslation } from "react-i18next";
export function NodePathSelector() {
const { settings, updateSettings } = useSettings();
const { t } = useTranslation("settings");
const [isSelectingPath, setIsSelectingPath] = useState(false);
const [nodeStatus, setNodeStatus] = useState<{
version: string | null;
......@@ -103,9 +105,7 @@ export function NodePathSelector() {
<div className="space-y-4">
<div className="space-y-2">
<div className="flex gap-2">
<Label className="text-sm font-medium">
Node.js Path Configuration
</Label>
<Label className="text-sm font-medium">{t("general.nodePath")}</Label>
<Button
onClick={handleSelectNodePath}
......@@ -115,7 +115,9 @@ export function NodePathSelector() {
className="flex items-center gap-2"
>
<FolderOpen className="w-4 h-4" />
{isSelectingPath ? "Selecting..." : "Browse for Node.js"}
{isSelectingPath
? t("general.selecting")
: t("general.browseForNode")}
</Button>
{isCustomPath && (
......@@ -126,7 +128,7 @@ export function NodePathSelector() {
className="flex items-center gap-2"
>
<RotateCcw className="w-4 h-4" />
Reset to Default
{t("general.resetToDefault")}
</Button>
)}
</div>
......@@ -135,7 +137,9 @@ export function NodePathSelector() {
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<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>
{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">
......@@ -160,7 +164,7 @@ export function NodePathSelector() {
) : (
<div className="flex items-center gap-1 text-yellow-600 dark:text-yellow-400">
<AlertCircle className="w-4 h-4" />
<span className="text-xs">Not found</span>
<span className="text-xs">{t("general.notFound")}</span>
</div>
)}
</div>
......@@ -170,13 +174,10 @@ export function NodePathSelector() {
{/* Help Text */}
<div className="text-sm text-gray-500 dark:text-gray-400">
{nodeStatus.isValid ? (
<p>Node.js is properly configured and ready to use.</p>
<p>{t("general.nodeConfigured")}</p>
) : (
<>
<p>
Select the folder where Node.js is installed if it's not in your
system PATH.
</p>
<p>{t("general.nodeSelectFolder")}</p>
</>
)}
</div>
......
import { useTranslation } from "react-i18next";
// @ts-ignore
import openAiLogo from "../../assets/ai-logos/openai-logo.svg";
// @ts-ignore
......@@ -39,6 +40,7 @@ export function ProBanner() {
}
export function ManageDyadProButton({ className }: { className?: string }) {
const { t } = useTranslation("home");
return (
<Button
variant="outline"
......@@ -52,13 +54,14 @@ export function ManageDyadProButton({ className }: { className?: string }) {
}}
>
<Wallet aria-hidden="true" className="w-5 h-5" />
Manage Dyad Pro
{t("proBanner.manageDyadPro")}
<ArrowUpRight aria-hidden="true" className="w-5 h-5" />
</Button>
);
}
export function SetupDyadProButton() {
const { t } = useTranslation("home");
return (
<Button
variant="outline"
......@@ -69,12 +72,13 @@ export function SetupDyadProButton() {
}}
>
<KeyRound aria-hidden="true" />
Already have Dyad Pro? Add your key
{t("proBanner.alreadyHavePro")}
</Button>
);
}
export function AiAccessBanner() {
const { t } = useTranslation("home");
return (
<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]"
......@@ -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="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">
Access leading AI models with one plan
{t("proBanner.accessLeadingModels")}
</div>
<button
type="button"
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"
>
Get Dyad Pro
{t("proBanner.getDyadPro")}
</button>
</div>
......@@ -141,6 +145,7 @@ export function AiAccessBanner() {
}
export function SmartContextBanner() {
const { t } = useTranslation("home");
return (
<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]"
......@@ -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="flex flex-col items-center text-center">
<div className="text-xl font-semibold tracking-tight text-emerald-900 dark:text-emerald-100">
Up to 3x cheaper
{t("proBanner.upTo3xCheaper")}
</div>
<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>
<button
......@@ -173,7 +178,7 @@ export function SmartContextBanner() {
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"
>
Get Dyad Pro
{t("proBanner.getDyadPro")}
</button>
</div>
</div>
......@@ -182,6 +187,7 @@ export function SmartContextBanner() {
}
export function TurboBanner() {
const { t } = useTranslation("home");
return (
<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]"
......@@ -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="flex flex-col items-center text-center">
<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 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>
<button
......@@ -214,7 +220,7 @@ export function TurboBanner() {
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"
>
Get Dyad Pro
{t("proBanner.getDyadPro")}
</button>
</div>
</div>
......
......@@ -15,6 +15,7 @@ import { Skeleton } from "./ui/skeleton";
import { Alert, AlertDescription, AlertTitle } from "./ui/alert";
import { AlertTriangle } from "lucide-react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import {
......@@ -37,6 +38,7 @@ import { CreateCustomProviderDialog } from "./CreateCustomProviderDialog";
export function ProviderSettingsGrid() {
const navigate = useNavigate();
const { t } = useTranslation(["settings", "common"]);
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [editingProvider, setEditingProvider] =
useState<LanguageModelProvider | null>(null);
......@@ -75,7 +77,9 @@ export function ProviderSettingsGrid() {
if (isLoading) {
return (
<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">
{[1, 2, 3, 4, 5].map((i) => (
<Card key={i} className="border-border">
......@@ -93,12 +97,14 @@ export function ProviderSettingsGrid() {
if (error) {
return (
<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">
<AlertTriangle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertTitle>{t("common:error")}</AlertTitle>
<AlertDescription>
Failed to load AI providers: {error.message}
{t("settings:ai.failedToLoadProviders", { message: error.message })}
</AlertDescription>
</Alert>
</div>
......@@ -107,7 +113,7 @@ export function ProviderSettingsGrid() {
return (
<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">
{providers
?.filter((p) => p.type !== "local")
......@@ -142,7 +148,9 @@ export function ProviderSettingsGrid() {
>
<Edit className="h-4 w-4" />
</TooltipTrigger>
<TooltipContent>Edit Provider</TooltipContent>
<TooltipContent>
{t("settings:ai.editProvider")}
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger
......@@ -158,7 +166,9 @@ export function ProviderSettingsGrid() {
>
<Trash2 className="h-4 w-4" />
</TooltipTrigger>
<TooltipContent>Delete Provider</TooltipContent>
<TooltipContent>
{t("settings:ai.deleteProvider")}
</TooltipContent>
</Tooltip>
</div>
)}
......@@ -166,11 +176,11 @@ export function ProviderSettingsGrid() {
{provider.name}
{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">
Ready
{t("common:ready")}
</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">
Needs Setup
{t("common:needsSetup")}
</span>
)}
</CardTitle>
......@@ -178,7 +188,7 @@ export function ProviderSettingsGrid() {
{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">
<GiftIcon className="w-4 h-4 mr-1" />
Free tier available
{t("settings:ai.freeTierAvailable")}
</span>
)}
</CardDescription>
......@@ -195,10 +205,10 @@ export function ProviderSettingsGrid() {
<CardHeader className="p-4 flex flex-col items-center justify-center h-full">
<PlusIcon className="h-8 w-8 text-muted-foreground mb-2" />
<CardTitle className="text-lg font-medium text-center">
Add custom provider
{t("settings:ai.addCustomProvider")}
</CardTitle>
<CardDescription className="text-center">
Connect to a custom LLM API endpoint
{t("settings:ai.connectCustomEndpoint")}
</CardDescription>
</CardHeader>
</Card>
......@@ -224,19 +234,24 @@ export function ProviderSettingsGrid() {
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Custom Provider</AlertDialogTitle>
<AlertDialogTitle>
{t("settings:ai.deleteCustomProvider")}
</AlertDialogTitle>
<AlertDialogDescription>
This will permanently delete this custom provider and all its
associated models. This action cannot be undone.
{t("settings:ai.deleteProviderConfirmation")}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel>
<AlertDialogCancel disabled={isDeleting}>
{t("common:cancel")}
</AlertDialogCancel>
<AlertDialogAction
onClick={handleDeleteProvider}
disabled={isDeleting}
>
{isDeleting ? "Deleting..." : "Delete Provider"}
{isDeleting
? t("common:deleting")
: t("settings:ai.deleteProviderAction")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
......
......@@ -10,9 +10,11 @@ import {
import { toast } from "sonner";
import { ipc } from "@/ipc/types";
import type { ReleaseChannel } from "@/lib/schemas";
import { useTranslation } from "react-i18next";
export function ReleaseChannelSelector() {
const { settings, updateSettings } = useSettings();
const { t } = useTranslation("settings");
if (!settings) {
return null;
......@@ -52,7 +54,7 @@ export function ReleaseChannelSelector() {
htmlFor="release-channel"
className="text-sm font-medium text-gray-700 dark:text-gray-300"
>
Release Channel
{t("general.releaseChannel")}
</label>
<Select
value={settings.releaseChannel}
......@@ -62,14 +64,13 @@ export function ReleaseChannelSelector() {
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="stable">Stable</SelectItem>
<SelectItem value="beta">Beta</SelectItem>
<SelectItem value="stable">{t("general.stable")}</SelectItem>
<SelectItem value="beta">{t("general.beta")}</SelectItem>
</SelectContent>
</Select>
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
<p>Stable is recommended for most users. </p>
<p>Beta receives more frequent updates but may have more bugs.</p>
<p>{t("general.releaseChannelDescription")}</p>
</div>
</div>
);
......
......@@ -9,9 +9,11 @@ import {
import { useSettings } from "@/hooks/useSettings";
import { showError } from "@/lib/toast";
import { ipc } from "@/ipc/types";
import { useTranslation } from "react-i18next";
export function RuntimeModeSelector() {
const { settings, updateSettings } = useSettings();
const { t } = useTranslation("settings");
if (!settings) {
return null;
......@@ -32,7 +34,7 @@ export function RuntimeModeSelector() {
<div className="space-y-1">
<div className="flex items-center space-x-2">
<Label className="text-sm font-medium" htmlFor="runtime-mode">
Runtime Mode
{t("general.runtimeMode")}
</Label>
<Select
value={settings.runtimeMode2 ?? "host"}
......@@ -48,8 +50,7 @@ export function RuntimeModeSelector() {
</Select>
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
Choose whether to run apps directly on the local machine or in Docker
containers
{t("general.runtimeModeDescription")}
</div>
</div>
{isDockerMode && (
......
import { useTranslation } from "react-i18next";
import { useNavigate } from "@tanstack/react-router";
import {
ChevronRight,
......@@ -45,6 +46,7 @@ type NodeInstallStep =
| "finished-checking";
export function SetupBanner() {
const { t } = useTranslation("home");
const posthog = usePostHog();
const navigate = useNavigate();
const [isOnboardingVisible, setIsOnboardingVisible] = useState(true);
......@@ -157,7 +159,7 @@ export function SetupBanner() {
if (itemsNeedAction.length === 0) {
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">
Build a new app
{t("setup.buildNewApp")}
</h1>
);
}
......@@ -181,7 +183,7 @@ export function SetupBanner() {
return (
<>
<p className="text-xl font-medium text-zinc-700 dark:text-zinc-300 p-4 pt-6">
Setup Dyad
{t("setup.setupDyad")}
</p>
<OnboardingBanner
isVisible={isOnboardingVisible}
......@@ -204,7 +206,7 @@ export function SetupBanner() {
<div className="flex items-center gap-3">
{getStatusIcon(isNodeSetupComplete, nodeCheckError)}
<span className="font-medium text-sm">
1. Install Node.js (App Runtime)
{t("setup.installNodeJs")}
</span>
</div>
</div>
......@@ -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">
{nodeCheckError && (
<p className="text-sm text-red-600 dark:text-red-400">
Error checking Node.js status. Try installing Node.js.
{t("setup.errorCheckingNode")}
</p>
)}
{isNodeSetupComplete ? (
<p className="text-sm">
Node.js ({nodeSystemInfo!.nodeVersion}) installed.{" "}
{t("setup.nodeInstalled", {
version: nodeSystemInfo!.nodeVersion,
})}{" "}
{nodeSystemInfo!.pnpmVersion && (
<span className="text-xs text-gray-500">
{" "}
(optional) pnpm ({nodeSystemInfo!.pnpmVersion}) installed.
{t("setup.pnpmInstalled", {
version: nodeSystemInfo!.pnpmVersion,
})}
</span>
)}
</p>
) : (
<div className="text-sm">
<p>Node.js is required to run apps locally.</p>
<p>{t("setup.nodeRequired")}</p>
{nodeInstallStep === "waiting-for-continue" && (
<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
className="text-blue-500 dark:text-blue-400 hover:underline"
onClick={() => {
......@@ -240,7 +249,7 @@ export function SetupBanner() {
);
}}
>
more download options
{t("setup.moreDownloadOptions")}
</a>
.
</p>
......@@ -256,7 +265,7 @@ export function SetupBanner() {
onClick={() => setShowManualConfig(!showManualConfig)}
className="text-sm text-blue-600 dark:text-blue-400 hover:underline"
>
Node.js already installed? Configure path manually →
{t("setup.nodeAlreadyInstalled")}
</button>
{showManualConfig && (
......
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
......@@ -49,6 +50,7 @@ import { useTheme } from "@/contexts/ThemeContext";
import { isSupabaseConnected } from "@/lib/schemas";
export function SupabaseConnector({ appId }: { appId: number }) {
const { t } = useTranslation(["home", "common"]);
const { settings, refreshSettings } = useSettings();
const { app, refreshApp } = useLoadApp(appId);
const { lastDeepLink, clearLastDeepLink } = useDeepLink();
......@@ -101,17 +103,21 @@ export function SupabaseConnector({ appId }: { appId: number }) {
(p) => p.id === projectId && p.organizationSlug === organizationSlug,
);
if (!project) {
throw new Error("Project not found");
throw new Error(t("integrations.supabase.projectNotFound"));
}
await setAppProject({
projectId,
appId,
organizationSlug,
});
toast.success("Project connected to app successfully");
toast.success(t("integrations.supabase.projectConnected"));
await refreshApp();
} catch (error) {
toast.error("Failed to connect project to app: " + error);
toast.error(
t("integrations.supabase.failedConnectProject", {
error: String(error),
}),
);
}
};
......@@ -153,20 +159,20 @@ export function SupabaseConnector({ appId }: { appId: number }) {
const handleUnsetProject = async () => {
try {
await unsetAppProject(appId);
toast.success("Project disconnected from app successfully");
toast.success(t("integrations.supabase.disconnectProject"));
await refreshApp();
} catch (error) {
console.error("Failed to disconnect project:", error);
toast.error("Failed to disconnect project from app");
toast.error(t("integrations.supabase.failedDisconnectProject"));
}
};
const handleDeleteOrganization = async (organizationSlug: string) => {
try {
await deleteOrganization({ organizationSlug });
toast.success("Organization disconnected successfully");
} catch (error) {
toast.error("Failed to disconnect organization: " + error);
toast.success(t("integrations.supabase.orgDisconnected"));
} catch {
toast.error(t("integrations.supabase.failedDisconnect"));
}
};
......@@ -176,7 +182,7 @@ export function SupabaseConnector({ appId }: { appId: number }) {
<Card className="mt-1">
<CardHeader>
<CardTitle className="flex items-center justify-between">
Supabase Project{" "}
{t("integrations.supabase.project")}{" "}
<Button
variant="outline"
onClick={() => {
......@@ -195,7 +201,7 @@ export function SupabaseConnector({ appId }: { appId: number }) {
</Button>
</CardTitle>
<CardDescription className="flex flex-col gap-1.5 text-sm">
This app is connected to project:{" "}
{t("integrations.supabase.connectedToProject")}{" "}
<Badge
variant="secondary"
className="ml-2 text-base font-bold px-3 py-1"
......@@ -207,7 +213,9 @@ export function SupabaseConnector({ appId }: { appId: number }) {
<CardContent>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="supabase-branch-select">Database Branch</Label>
<Label htmlFor="supabase-branch-select">
{t("integrations.supabase.databaseBranch")}
</Label>
{branchesError ? (
<Alert>
<Info className="h-4 w-4" />
......@@ -224,7 +232,9 @@ export function SupabaseConnector({ appId }: { appId: number }) {
(b) => b.projectRef === supabaseBranchProjectId,
);
if (!branch) {
throw new Error("Branch not found");
throw new Error(
t("integrations.supabase.branchNotFound"),
);
}
// Keep the same organizationSlug from the app
await setAppProject({
......@@ -233,10 +243,14 @@ export function SupabaseConnector({ appId }: { appId: number }) {
appId,
organizationSlug: app.supabaseOrganizationSlug,
});
toast.success("Branch selected");
toast.success(t("integrations.supabase.branchSelected"));
await refreshApp();
} catch (error) {
toast.error("Failed to set branch: " + error);
toast.error(
t("integrations.supabase.failedSetBranch", {
error: String(error),
}),
);
}
}}
disabled={isLoadingBranches || isSettingAppProject}
......@@ -245,7 +259,9 @@ export function SupabaseConnector({ appId }: { appId: number }) {
id="supabase-branch-select"
data-testid="supabase-branch-select"
>
<SelectValue placeholder="Select a branch" />
<SelectValue
placeholder={t("integrations.supabase.selectBranch")}
/>
</SelectTrigger>
<SelectContent>
{branches.map((branch) => (
......@@ -263,7 +279,7 @@ export function SupabaseConnector({ appId }: { appId: number }) {
</div>
<Button variant="destructive" onClick={handleUnsetProject}>
Disconnect Project
{t("integrations.supabase.disconnectProject")}
</Button>
</div>
</CardContent>
......@@ -283,7 +299,7 @@ export function SupabaseConnector({ appId }: { appId: number }) {
<Card className="mt-1">
<CardHeader>
<CardTitle className="flex items-center justify-between">
Supabase Projects
{t("integrations.supabase.projects")}
<div className="flex items-center gap-2">
<Tooltip>
<TooltipTrigger
......@@ -300,7 +316,9 @@ export function SupabaseConnector({ appId }: { appId: number }) {
className={`h-4 w-4 ${isFetchingProjects ? "animate-spin" : ""}`}
/>
</TooltipTrigger>
<TooltipContent>Refresh projects</TooltipContent>
<TooltipContent>
{t("integrations.supabase.refreshProjects")}
</TooltipContent>
</Tooltip>
<Button
variant="outline"
......@@ -309,12 +327,12 @@ export function SupabaseConnector({ appId }: { appId: number }) {
className="gap-1"
>
<Plus className="h-4 w-4" />
Add Organization
{t("integrations.supabase.addOrganization")}
</Button>
</div>
</CardTitle>
<CardDescription>
Select a Supabase project to connect to this app
{t("integrations.supabase.selectProjectDescription")}
</CardDescription>
</CardHeader>
<CardContent>
......@@ -325,20 +343,24 @@ export function SupabaseConnector({ appId }: { appId: number }) {
</div>
) : projectsError ? (
<div className="text-red-500">
Error loading projects: {projectsError.message}
{t("integrations.supabase.errorLoadingProjects", {
message: projectsError.message,
})}
<Button
variant="outline"
className="mt-2"
onClick={() => refetchProjects()}
>
Retry
{t("common:retry")}
</Button>
</div>
) : (
<div className="space-y-4">
{/* Connected organizations list */}
<div className="space-y-2">
<Label>Connected Organizations</Label>
<Label>
{t("integrations.supabase.connectedOrganizations")}
</Label>
<div className="space-y-1">
{organizations.map((org) => (
<div
......@@ -372,7 +394,9 @@ export function SupabaseConnector({ appId }: { appId: number }) {
<Trash2 className="h-3.5 w-3.5 mr-1" />
<span className="text-xs">Disconnect</span>
</TooltipTrigger>
<TooltipContent>Disconnect organization</TooltipContent>
<TooltipContent>
{t("integrations.supabase.disconnectOrganization")}
</TooltipContent>
</Tooltip>
</div>
))}
......@@ -381,7 +405,7 @@ export function SupabaseConnector({ appId }: { appId: number }) {
{projects.length === 0 ? (
<p className="text-sm text-gray-500">
No projects found in your connected Supabase organizations.
{t("integrations.supabase.noProjectsFound")}
</p>
) : (
<div className="space-y-2">
......@@ -391,7 +415,9 @@ export function SupabaseConnector({ appId }: { appId: number }) {
onValueChange={(v) => v && handleProjectSelect(v)}
>
<SelectTrigger id="project-select">
<SelectValue placeholder="Select a project" />
<SelectValue
placeholder={t("integrations.supabase.selectAProject")}
/>
</SelectTrigger>
<SelectContent>
{Object.entries(groupedProjects).map(
......
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
......@@ -16,6 +17,7 @@ import { showSuccess, showError } from "@/lib/toast";
import { isSupabaseConnected } from "@/lib/schemas";
export function SupabaseIntegration() {
const { t } = useTranslation(["home", "common"]);
const { settings, updateSettings } = useSettings();
const [isDisconnecting, setIsDisconnecting] = useState(false);
......@@ -35,10 +37,10 @@ export function SupabaseIntegration() {
enableSupabaseWriteSqlMigration: false,
});
if (result) {
showSuccess("Successfully disconnected all Supabase organizations");
showSuccess(t("integrations.supabase.disconnectedAll"));
await refetchOrganizations();
} else {
showError("Failed to disconnect from Supabase");
showError(t("integrations.supabase.failedDisconnect"));
}
} catch (err: any) {
showError(
......@@ -52,9 +54,9 @@ export function SupabaseIntegration() {
const handleDeleteOrganization = async (organizationSlug: string) => {
try {
await deleteOrganization({ organizationSlug });
showSuccess("Organization disconnected successfully");
showSuccess(t("integrations.supabase.orgDisconnected"));
} catch (err: any) {
showError(err.message || "Failed to disconnect organization");
showError(err.message || t("integrations.supabase.failedDisconnect"));
}
};
......@@ -63,7 +65,7 @@ export function SupabaseIntegration() {
await updateSettings({
enableSupabaseWriteSqlMigration: enabled,
});
showSuccess("Setting updated");
showSuccess(t("integrations.supabase.settingUpdated"));
} catch (err: any) {
showError(err.message || "Failed to update setting");
}
......@@ -89,11 +91,12 @@ export function SupabaseIntegration() {
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300">
Supabase Integration
{t("integrations.supabase.title")}
</h3>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{organizations.length} organization
{organizations.length !== 1 ? "s" : ""} connected to Supabase.
{t("integrations.supabase.organizationsConnected", {
count: organizations.length,
})}
</p>
</div>
<Button
......@@ -103,7 +106,9 @@ export function SupabaseIntegration() {
disabled={isDisconnecting}
className="flex items-center gap-2"
>
{isDisconnecting ? "Disconnecting..." : "Disconnect All"}
{isDisconnecting
? t("common:disconnecting")
: t("integrations.supabase.disconnectAll")}
<DatabaseZap className="h-4 w-4" />
</Button>
</div>
......@@ -141,7 +146,9 @@ export function SupabaseIntegration() {
<Trash2 className="h-3.5 w-3.5 mr-1" />
<span className="text-xs">Disconnect</span>
</TooltipTrigger>
<TooltipContent>Disconnect organization</TooltipContent>
<TooltipContent>
{t("integrations.supabase.disconnectOrganization")}
</TooltipContent>
</Tooltip>
</div>
))}
......@@ -160,13 +167,10 @@ export function SupabaseIntegration() {
htmlFor="supabase-migrations"
className="text-sm font-medium"
>
Write SQL migration files
{t("integrations.supabase.writeSqlMigrations")}
</Label>
<p className="text-xs text-gray-500 dark:text-gray-400">
Generate SQL migration files when modifying your Supabase schema.
This helps you track database changes in version control, though
these files aren't used for chat context, which uses the live
schema.
{t("integrations.supabase.writeSqlDescription")}
</p>
</div>
</div>
......
import { useTranslation } from "react-i18next";
import { ipc } from "@/ipc/types";
import React from "react";
import { Button } from "./ui/button";
......@@ -9,6 +10,7 @@ const hideBannerAtom = atom(false);
export function PrivacyBanner() {
const [hideBanner, setHideBanner] = useAtom(hideBannerAtom);
const { settings, updateSettings } = useSettings();
const { t } = useTranslation("settings");
// TODO: Implement state management for banner visibility and user choice
// TODO: Implement functionality for Accept, Reject, Ask me later buttons
// TODO: Add state to hide/show banner based on user choice
......@@ -26,10 +28,8 @@ export function PrivacyBanner() {
Share anonymous data?
</h4>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
Help improve Dyad with anonymous usage data.
<em className="block italic mt-0.5">
Note: this does not log your code or messages.
</em>
{t("telemetry.privacyNotice")}
<br />
<a
onClick={() => {
ipc.system.openExternalUrl(
......@@ -50,7 +50,7 @@ export function PrivacyBanner() {
}}
data-testid="telemetry-accept-button"
>
Accept
{t("telemetry.acceptAndContinue")}
</Button>
<Button
variant="secondary"
......
import { useSettings } from "@/hooks/useSettings";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { useTranslation } from "react-i18next";
export function TelemetrySwitch() {
const { settings, updateSettings } = useSettings();
const { t } = useTranslation("settings");
return (
<div className="flex items-center space-x-2">
<Switch
......@@ -19,7 +21,7 @@ export function TelemetrySwitch() {
});
}}
/>
<Label htmlFor="telemetry-switch">Telemetry</Label>
<Label htmlFor="telemetry-switch">{t("telemetry.enable")}</Label>
</div>
);
}
......@@ -7,6 +7,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useTranslation } from "react-i18next";
interface OptionInfo {
value: string;
......@@ -38,6 +39,7 @@ const options: OptionInfo[] = [
export const ThinkingBudgetSelector: React.FC = () => {
const { settings, updateSettings } = useSettings();
const { t } = useTranslation("settings");
const handleValueChange = (value: string) => {
updateSettings({ thinkingBudget: value as "low" | "medium" | "high" });
......@@ -57,14 +59,14 @@ export const ThinkingBudgetSelector: React.FC = () => {
htmlFor="thinking-budget"
className="text-sm font-medium text-gray-700 dark:text-gray-300"
>
Thinking Budget
{t("ai.thinkingBudget")}
</label>
<Select
value={currentValue}
onValueChange={(v) => v && handleValueChange(v)}
>
<SelectTrigger className="w-[180px]" id="thinking-budget">
<SelectValue placeholder="Select budget" />
<SelectValue placeholder={t("ai.selectThinkingBudget")} />
</SelectTrigger>
<SelectContent>
{options.map((option) => (
......
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { useSettings } from "@/hooks/useSettings";
import { showSuccess, showError } from "@/lib/toast";
export function VercelIntegration() {
const { t } = useTranslation(["home", "common"]);
const { settings, updateSettings } = useSettings();
const [isDisconnecting, setIsDisconnecting] = useState(false);
......@@ -14,14 +16,12 @@ export function VercelIntegration() {
vercelAccessToken: undefined,
});
if (result) {
showSuccess("Successfully disconnected from Vercel");
showSuccess(t("integrations.vercel.disconnected"));
} else {
showError("Failed to disconnect from Vercel");
showError(t("integrations.vercel.failedDisconnect"));
}
} catch (err: any) {
showError(
err.message || "An error occurred while disconnecting from Vercel",
);
showError(err.message || t("integrations.vercel.errorDisconnect"));
} finally {
setIsDisconnecting(false);
}
......@@ -37,10 +37,10 @@ export function VercelIntegration() {
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300">
Vercel Integration
{t("integrations.vercel.title")}
</h3>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
Your account is connected to Vercel.
{t("integrations.vercel.connected")}
</p>
</div>
......@@ -51,7 +51,9 @@ export function VercelIntegration() {
disabled={isDisconnecting}
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">
<path d="M24 22.525H0l12-21.05 12 21.05z" />
</svg>
......
......@@ -9,6 +9,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useTranslation } from "react-i18next";
const ZOOM_LEVEL_LABELS: Record<ZoomLevel, string> = {
"90": "90%",
......@@ -28,6 +29,7 @@ const ZOOM_LEVEL_DESCRIPTIONS: Record<ZoomLevel, string> = {
export function ZoomSelector() {
const { settings, updateSettings } = useSettings();
const { t } = useTranslation("settings");
const currentZoomLevel: ZoomLevel = useMemo(() => {
const value = settings?.zoomLevel ?? DEFAULT_ZOOM_LEVEL;
return ZoomLevelSchema.safeParse(value).success
......@@ -38,9 +40,9 @@ export function ZoomSelector() {
return (
<div className="space-y-2">
<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">
Adjusts the zoom level to make content easier to read.
{t("general.zoomDescription")}
</p>
</div>
<Select
......@@ -50,7 +52,7 @@ export function ZoomSelector() {
}
>
<SelectTrigger id="zoom-level" className="w-[220px]">
<SelectValue placeholder="Select zoom level" />
<SelectValue placeholder={t("general.selectZoom")} />
</SelectTrigger>
<SelectContent>
{Object.entries(ZOOM_LEVEL_LABELS).map(([value, label]) => (
......
import { FileText, X, MessageSquare, Upload } from "lucide-react";
import type { FileAttachment } from "@/ipc/types";
import { useTranslation } from "react-i18next";
interface AttachmentsListProps {
attachments: FileAttachment[];
......@@ -10,6 +11,8 @@ export function AttachmentsList({
attachments,
onRemove,
}: AttachmentsListProps) {
const { t } = useTranslation("chat");
if (attachments.length === 0) return null;
return (
......@@ -61,7 +64,7 @@ export function AttachmentsList({
<button
onClick={() => onRemove(index)}
className="hover:bg-muted-foreground/20 rounded-full p-0.5"
aria-label="Remove attachment"
aria-label={t("removeAttachment")}
>
<X size={12} />
</button>
......
import { XCircle, AlertTriangle } from "lucide-react"; // Assuming lucide-react is used
import { useTranslation } from "react-i18next";
interface ChatErrorProps {
error: string | null;
......@@ -6,6 +7,8 @@ interface ChatErrorProps {
}
export function ChatError({ error, onDismiss }: ChatErrorProps) {
const { t } = useTranslation("chat");
if (!error) {
return null;
}
......@@ -23,7 +26,7 @@ export function ChatError({ error, onDismiss }: ChatErrorProps) {
<button
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"
aria-label="Dismiss error"
aria-label={t("dismissError")}
>
<XCircle className="h-4 w-4 text-red-500 hover:text-red-700" />
</button>
......
......@@ -6,6 +6,7 @@ import {
Info,
} from "lucide-react";
import { PanelRightClose } from "lucide-react";
import { useTranslation } from "react-i18next";
import { useAtom, useAtomValue } from "jotai";
import { selectedAppIdAtom } from "@/atoms/appAtoms";
import { useVersions } from "@/hooks/useVersions";
......@@ -43,6 +44,7 @@ export function ChatHeader({
onTogglePreview,
onVersionClick,
}: ChatHeaderProps) {
const { t } = useTranslation("chat");
const appId = useAtomValue(selectedAppIdAtom);
const { versions, loading: versionsLoading } = useVersions(appId);
const { navigate } = useRouter();
......@@ -78,7 +80,7 @@ export function ChatHeader({
// If this throws, it will automatically show an error toast
await renameBranch({ oldBranchName: "master", newBranchName: "main" });
showSuccess("Master branch renamed to main");
showSuccess(t("header.masterRenamed"));
};
const handleNewChat = async () => {
......@@ -92,7 +94,7 @@ export function ChatHeader({
});
await invalidateChats();
} catch (error) {
showError(`Failed to create new chat: ${(error as any).toString()}`);
showError(t("failedCreateChat", { error: (error as any).toString() }));
}
} else {
navigate({ to: "/" });
......@@ -123,14 +125,14 @@ export function ChatHeader({
<span className="flex items-center gap-1">
{isAnyCheckoutVersionInProgress ? (
<>
<span>
Please wait, switching back to latest version...
</span>
<span>{t("header.switchingToLatest")}</span>
</>
) : (
<>
<strong>Warning:</strong>
<span>You are not on a branch</span>
<strong>
{t("header.warningNotOnBranch").split(":")[0]}:
</strong>
<span>{t("header.notOnBranch")}</span>
<Info size={14} />
</>
)}
......@@ -139,8 +141,8 @@ export function ChatHeader({
<TooltipContent>
<p>
{isAnyCheckoutVersionInProgress
? "Version checkout is currently in progress"
: "Checkout main branch, otherwise changes will not be saved properly"}
? t("header.checkoutInProgress")
: t("header.checkoutMainBranch")}
</p>
</TooltipContent>
</Tooltip>
......@@ -148,11 +150,9 @@ export function ChatHeader({
</>
)}
{currentBranchName && currentBranchName !== "<no-branch>" && (
<span>
You are on branch: <strong>{currentBranchName}</strong>.
</span>
<span>{t("header.onBranch", { name: currentBranchName })}</span>
)}
{branchInfoLoading && <span>Checking branch...</span>}
{branchInfoLoading && <span>{t("header.checkingBranch")}</span>}
</span>
</div>
{currentBranchName === "master" ? (
......@@ -162,7 +162,9 @@ export function ChatHeader({
onClick={handleRenameMasterToMain}
disabled={isRenamingBranch || branchInfoLoading}
>
{isRenamingBranch ? "Renaming..." : "Rename master to main"}
{isRenamingBranch
? t("header.renaming")
: t("header.renameMasterToMain")}
</Button>
) : isAnyCheckoutVersionInProgress && !isCheckingOutVersion ? null : (
<Button
......@@ -172,8 +174,8 @@ export function ChatHeader({
disabled={isCheckingOutVersion || branchInfoLoading}
>
{isCheckingOutVersion
? "Checking out..."
: "Switch to main branch"}
? t("header.checkingOut")
: t("header.switchToMainBranch")}
</Button>
)}
</div>
......@@ -194,7 +196,7 @@ export function ChatHeader({
className="hidden @2xs:flex items-center justify-start gap-2 mx-2 py-3"
>
<PlusCircle size={16} />
<span>New Chat</span>
<span>{t("newChat")}</span>
</Button>
<Button
onClick={onVersionClick}
......@@ -204,7 +206,7 @@ export function ChatHeader({
<History size={16} />
{versionsLoading
? "..."
: `Version ${versions.length}${versionPostfix}`}
: `${t("header.versionCount", { count: versions.length })}${versionPostfix}`}
</Button>
</div>
......
......@@ -19,6 +19,7 @@ import {
} from "lucide-react";
import type React from "react";
import { useCallback, useEffect, useRef, useState, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useSettings } from "@/hooks/useSettings";
import { ipc } from "@/ipc/types";
......@@ -94,6 +95,7 @@ import { showError as showErrorToast } from "@/lib/toast";
const showTokenBarAtom = atom(false);
export function ChatInput({ chatId }: { chatId?: number }) {
const { t } = useTranslation("chat");
const posthog = usePostHog();
const [inputValue, setInputValue] = useAtom(chatInputValueAtom);
const { settings } = useSettings();
......@@ -409,12 +411,12 @@ export function ChatInput({ chatId }: { chatId?: number }) {
{/* Display loading or error state for proposal */}
{isProposalLoading && (
<div className="p-4 text-sm text-muted-foreground">
Loading proposal...
{t("loadingProposal")}
</div>
)}
{proposalError && (
<div className="p-4 text-sm text-red-600">
Error loading proposal: {proposalError.message}
{t("errorLoadingProposal", { message: proposalError.message })}
</div>
)}
<div className="p-4" data-testid="chat-input-container">
......@@ -531,11 +533,10 @@ export function ChatInput({ chatId }: { chatId?: number }) {
}
>
<Lock size={16} />
<span className="font-medium">Visual editor (Pro)</span>
<span className="font-medium">{t("visualEditor")}</span>
</TooltipTrigger>
<TooltipContent>
Visual editing lets you make UI changes without AI and is a
Pro-only feature
{t("visualEditorDescription")}
</TooltipContent>
</Tooltip>
</div>
......@@ -559,7 +560,7 @@ export function ChatInput({ chatId }: { chatId?: number }) {
onChange={setInputValue}
onSubmit={handleSubmit}
onPaste={handlePaste}
placeholder="Ask Dyad to build..."
placeholder={t("askDyadToBuild")}
excludeCurrentApp={true}
disableSendButton={disableSendButton}
messageHistory={userMessageHistory}
......@@ -571,14 +572,14 @@ export function ChatInput({ chatId }: { chatId?: number }) {
render={
<button
onClick={handleCancel}
aria-label="Cancel generation"
aria-label={t("cancelGeneration")}
className="px-2 py-2 mt-1 mr-1 hover:bg-(--background-darkest) text-(--sidebar-accent-fg) rounded-lg"
/>
}
>
<StopCircleIcon size={20} />
</TooltipTrigger>
<TooltipContent>Cancel generation</TooltipContent>
<TooltipContent>{t("cancelGeneration")}</TooltipContent>
</Tooltip>
) : (
<Tooltip>
......@@ -590,14 +591,14 @@ export function ChatInput({ chatId }: { chatId?: number }) {
(!inputValue.trim() && attachments.length === 0) ||
disableSendButton
}
aria-label="Send message"
aria-label={t("sendMessage")}
className="px-2 py-2 mt-1 mr-1 hover:bg-(--background-darkest) text-(--sidebar-accent-fg) rounded-lg disabled:opacity-50"
/>
}
>
<SendHorizontalIcon size={20} />
</TooltipTrigger>
<TooltipContent>Send message</TooltipContent>
<TooltipContent>{t("sendMessage")}</TooltipContent>
</Tooltip>
)}
</div>
......@@ -651,18 +652,20 @@ function SuggestionButton({
}
function SummarizeInNewChatButton() {
const { t } = useTranslation("chat");
const { handleSummarize } = useSummarizeInNewChat();
return (
<SuggestionButton
onClick={handleSummarize}
tooltipText="Creating a new chat makes the AI more focused and efficient"
tooltipText={t("summarizeNewChatTip")}
>
Summarize to new chat
{t("summarizeToNewChat")}
</SuggestionButton>
);
}
function RefactorFileButton({ path }: { path: string }) {
const { t } = useTranslation("chat");
const chatId = useAtomValue(selectedChatIdAtom);
const { streamMessage } = useStreamChat();
const onClick = () => {
......@@ -671,24 +674,22 @@ function RefactorFileButton({ path }: { path: string }) {
return;
}
streamMessage({
prompt: `Refactor ${path} and make it more modular`,
prompt: t("refactorFile", { path }),
chatId,
redo: false,
});
};
return (
<SuggestionButton
onClick={onClick}
tooltipText={`Refactor the file to improve maintainability: \n${path}`}
>
<span className="max-w-[200px] overflow-hidden whitespace-nowrap text-ellipsis">
Refactor {path.split("/").slice(-2).join("/")}
<SuggestionButton onClick={onClick} tooltipText={t("refactorDescription")}>
<span className="max-w-[180px] overflow-hidden whitespace-nowrap text-ellipsis">
{t("refactorFile", { path: path.split("/").slice(-2).join("/") })}
</span>
</SuggestionButton>
);
}
function WriteCodeProperlyButton() {
const { t } = useTranslation("chat");
const chatId = useAtomValue(selectedChatIdAtom);
const { streamMessage } = useStreamChat();
const onClick = () => {
......@@ -705,14 +706,15 @@ function WriteCodeProperlyButton() {
return (
<SuggestionButton
onClick={onClick}
tooltipText="Write code properly (useful when AI generates the code in the wrong format)"
tooltipText={t("writeCodeProperlyDescription")}
>
Write code properly
{t("writeCodeProperly")}
</SuggestionButton>
);
}
function RebuildButton() {
const { t } = useTranslation("chat");
const { restartApp } = useRunApp();
const posthog = usePostHog();
const selectedAppId = useAtomValue(selectedAppIdAtom);
......@@ -725,13 +727,17 @@ function RebuildButton() {
}, [selectedAppId, posthog, restartApp]);
return (
<SuggestionButton onClick={onClick} tooltipText="Rebuild the application">
Rebuild app
<SuggestionButton
onClick={onClick}
tooltipText={t("rebuildAppDescription")}
>
{t("rebuildApp")}
</SuggestionButton>
);
}
function RestartButton() {
const { t } = useTranslation("chat");
const { restartApp } = useRunApp();
const posthog = usePostHog();
const selectedAppId = useAtomValue(selectedAppIdAtom);
......@@ -746,14 +752,15 @@ function RestartButton() {
return (
<SuggestionButton
onClick={onClick}
tooltipText="Restart the development server"
tooltipText={t("restartAppDescription")}
>
Restart app
{t("restartApp")}
</SuggestionButton>
);
}
function RefreshButton() {
const { t } = useTranslation("chat");
const { refreshAppIframe } = useRunApp();
const posthog = usePostHog();
......@@ -765,14 +772,15 @@ function RefreshButton() {
return (
<SuggestionButton
onClick={onClick}
tooltipText="Refresh the application preview"
tooltipText={t("refreshAppDescription")}
>
Refresh app
{t("refreshApp")}
</SuggestionButton>
);
}
function KeepGoingButton() {
const { t } = useTranslation("chat");
const { streamMessage } = useStreamChat();
const chatId = useAtomValue(selectedChatIdAtom);
const onClick = () => {
......@@ -786,8 +794,8 @@ function KeepGoingButton() {
});
};
return (
<SuggestionButton onClick={onClick} tooltipText="Keep going">
Keep going
<SuggestionButton onClick={onClick} tooltipText={t("keepGoing")}>
{t("keepGoing")}
</SuggestionButton>
);
}
......@@ -846,10 +854,11 @@ function ChatInputActions({
isApproving,
isRejecting,
}: ChatInputActionsProps) {
const { t } = useTranslation("chat");
const [isDetailsVisible, setIsDetailsVisible] = useState(false);
if (proposal.type === "tip-proposal") {
return <div>Tip proposal</div>;
return <div>{t("tipProposal")}</div>;
}
if (proposal.type === "action-proposal") {
return <ActionProposalActions proposal={proposal}></ActionProposalActions>;
......@@ -904,7 +913,7 @@ function ChatInputActions({
</button>
{proposal.securityRisks.length > 0 && (
<span className="bg-red-100 text-red-700 text-xs font-medium px-2 py-0.5 rounded-full flex-shrink-0">
Security risks found
{t("securityRisksFound")}
</span>
)}
</div>
......@@ -924,7 +933,7 @@ function ChatInputActions({
) : (
<Check size={16} className="mr-1" />
)}
Approve
{t("approve")}
</Button>
<Button
className="px-8"
......@@ -939,7 +948,7 @@ function ChatInputActions({
) : (
<X size={16} className="mr-1" />
)}
Reject
{t("reject")}
</Button>
<div className="flex items-center space-x-1 ml-auto">
<AutoApproveSwitch />
......@@ -952,7 +961,7 @@ function ChatInputActions({
<div className="p-3 border-t border-border bg-muted/50 text-sm">
{!!proposal.securityRisks.length && (
<div className="mb-3">
<h4 className="font-semibold mb-1">Security Risks</h4>
<h4 className="font-semibold mb-1">{t("securityRisks")}</h4>
<ul className="space-y-1">
{proposal.securityRisks.map((risk, index) => (
<li key={index} className="flex items-start space-x-2">
......@@ -979,7 +988,7 @@ function ChatInputActions({
{proposal.sqlQueries?.length > 0 && (
<div className="mb-3">
<h4 className="font-semibold mb-1">SQL Queries</h4>
<h4 className="font-semibold mb-1">{t("sqlQueries")}</h4>
<ul className="space-y-2">
{proposal.sqlQueries.map((query, index) => (
<SqlQueryItem key={index} query={query} />
......@@ -990,7 +999,7 @@ function ChatInputActions({
{proposal.packagesAdded?.length > 0 && (
<div className="mb-3">
<h4 className="font-semibold mb-1">Packages Added</h4>
<h4 className="font-semibold mb-1">{t("packagesAdded")}</h4>
<ul className="space-y-1">
{proposal.packagesAdded.map((pkg, index) => (
<li
......@@ -1017,7 +1026,9 @@ function ChatInputActions({
{serverFunctions.length > 0 && (
<div className="mb-3">
<h4 className="font-semibold mb-1">Server Functions Changed</h4>
<h4 className="font-semibold mb-1">
{t("serverFunctionsChanged")}
</h4>
<ul className="space-y-1">
{serverFunctions.map((file: FileChange, index: number) => (
<li key={index} className="flex items-center space-x-2">
......@@ -1039,7 +1050,7 @@ function ChatInputActions({
{otherFilesChanged.length > 0 && (
<div>
<h4 className="font-semibold mb-1">Files Changed</h4>
<h4 className="font-semibold mb-1">{t("filesChanged")}</h4>
<ul className="space-y-1">
{otherFilesChanged.map((file: FileChange, index: number) => (
<li key={index} className="flex items-center space-x-2">
......@@ -1094,6 +1105,8 @@ function ProposalSummary({
packagesAdded?: string[];
filesChanged?: FileChange[];
}) {
const { t } = useTranslation("chat");
// If no changes, show a simple message
if (
!sqlQueries.length &&
......@@ -1101,7 +1114,7 @@ function ProposalSummary({
!packagesAdded.length &&
!filesChanged.length
) {
return <span>No changes</span>;
return <span>{t("noChanges")}</span>;
}
// Build parts array with only the segments that have content
......@@ -1137,6 +1150,7 @@ function ProposalSummary({
// SQL Query item with expandable functionality
function SqlQueryItem({ query }: { query: SqlQuery }) {
const { t } = useTranslation("chat");
const [isExpanded, setIsExpanded] = useState(false);
const queryContent = query.content;
......@@ -1151,7 +1165,7 @@ function SqlQueryItem({ query }: { query: SqlQuery }) {
<div className="flex items-center gap-2">
<Database size={16} className="text-muted-foreground flex-shrink-0" />
<span className="text-sm font-medium">
{queryDescription || "SQL Query"}
{queryDescription || t("sqlQuery")}
</span>
</div>
<div>
......
......@@ -8,6 +8,7 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { useTranslation } from "react-i18next";
interface DeleteChatDialogProps {
isOpen: boolean;
......@@ -22,28 +23,28 @@ export function DeleteChatDialog({
onConfirmDelete,
chatTitle,
}: DeleteChatDialogProps) {
const { t } = useTranslation("chat");
const { t: tc } = useTranslation("common");
return (
<AlertDialog open={isOpen} onOpenChange={onOpenChange}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Chat</AlertDialogTitle>
<AlertDialogTitle>{t("deleteChat")}</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete "{chatTitle || "this chat"}"? This
action cannot be undone and all messages in this chat will be
permanently lost.
{t("deleteChatConfirmation", { title: chatTitle || "this chat" })}
<br />
<br />
<strong>Note:</strong> Any code changes that have already been
accepted will be kept.
<strong>{t("deleteChatNote")}</strong>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogCancel>{tc("cancel")}</AlertDialogCancel>
<AlertDialogAction
onClick={onConfirmDelete}
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>
</AlertDialogFooter>
</AlertDialogContent>
......
import { Paperclip } from "lucide-react";
import { useTranslation } from "react-i18next";
interface DragDropOverlayProps {
isDraggingOver: boolean;
}
export function DragDropOverlay({ isDraggingOver }: DragDropOverlayProps) {
const { t } = useTranslation("chat");
if (!isDraggingOver) return null;
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="bg-background p-4 rounded-lg shadow-lg text-center">
<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>
);
......
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { ipc } from "@/ipc/types";
import { showError, showSuccess } from "@/lib/toast";
import {
......@@ -28,6 +29,8 @@ export function RenameChatDialog({
onOpenChange,
onRename,
}: RenameChatDialogProps) {
const { t } = useTranslation("chat");
const { t: tc } = useTranslation("common");
const [newTitle, setNewTitle] = useState("");
// Reset title when dialog opens
......@@ -50,7 +53,7 @@ export function RenameChatDialog({
chatId,
title: newTitle.trim(),
});
showSuccess("Chat renamed successfully");
showSuccess(t("chatRenamed"));
// Call the parent's onRename callback to refresh the chat list
onRename();
......@@ -58,7 +61,7 @@ export function RenameChatDialog({
// Close the dialog
handleOpenChange(false);
} 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({
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Rename Chat</DialogTitle>
<DialogDescription>Enter a new name for this chat.</DialogDescription>
<DialogTitle>{t("renameChat")}</DialogTitle>
<DialogDescription>{t("renameChatDescription")}</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="chat-title" className="text-right">
Title
{t("chatTitle")}
</Label>
<Input
id="chat-title"
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
className="col-span-3"
placeholder="Enter chat title..."
placeholder={t("enterChatTitle")}
onKeyDown={(e) => {
if (e.key === "Enter") {
handleSave();
......@@ -94,10 +97,10 @@ export function RenameChatDialog({
</div>
<DialogFooter>
<Button variant="outline" onClick={handleClose}>
Cancel
{tc("cancel")}
</Button>
<Button onClick={handleSave} disabled={!newTitle.trim()}>
Save
{tc("save")}
</Button>
</DialogFooter>
</DialogContent>
......
......@@ -33,6 +33,7 @@ import { showError, showSuccess } from "@/lib/toast";
import { useMutation } from "@tanstack/react-query";
import { useCheckProblems } from "@/hooks/useCheckProblems";
import { isPreviewOpenAtom } from "@/atoms/viewAtoms";
import { useTranslation } from "react-i18next";
export type PreviewMode =
| "preview"
......@@ -44,6 +45,7 @@ export type PreviewMode =
// Preview Header component with preview mode toggle
export const ActionHeader = () => {
const { t } = useTranslation("home");
const [previewMode, setPreviewMode] = useAtom(previewModeAtom);
const [isPreviewOpen, setIsPreviewOpen] = useAtom(isPreviewOpenAtom);
const selectedAppId = useAtomValue(selectedAppIdAtom);
......@@ -218,14 +220,14 @@ export const ActionHeader = () => {
"preview",
previewRef,
<Eye size={iconSize} />,
"Preview",
t("preview.title"),
"preview-mode-button",
)}
{renderButton(
"problems",
problemsRef,
<AlertTriangle size={iconSize} />,
"Problems",
t("preview.problems"),
"problems-mode-button",
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">
......@@ -237,28 +239,28 @@ export const ActionHeader = () => {
"code",
codeRef,
<Code size={iconSize} />,
"Code",
t("preview.code"),
"code-mode-button",
)}
{renderButton(
"configure",
configureRef,
<Wrench size={iconSize} />,
"Configure",
t("preview.configure"),
"configure-mode-button",
)}
{renderButton(
"security",
securityRef,
<Shield size={iconSize} />,
"Security",
t("preview.security"),
"security-mode-button",
)}
{renderButton(
"publish",
publishRef,
<Globe size={iconSize} />,
"Publish",
t("preview.publish"),
"publish-mode-button",
)}
</div>
......@@ -277,24 +279,24 @@ export const ActionHeader = () => {
>
<MoreVertical size={16} />
</TooltipTrigger>
<TooltipContent>More options</TooltipContent>
<TooltipContent>{t("preview.moreOptions")}</TooltipContent>
</Tooltip>
<DropdownMenuContent align="end" className="w-60">
<DropdownMenuItem onClick={onCleanRestart}>
<Cog size={16} />
<div className="flex flex-col">
<span>Rebuild</span>
<span>{t("preview.rebuild")}</span>
<span className="text-xs text-muted-foreground">
Re-installs node_modules and restarts
{t("preview.rebuildDescription")}
</span>
</div>
</DropdownMenuItem>
<DropdownMenuItem onClick={onClearSessionData}>
<Trash2 size={16} />
<div className="flex flex-col">
<span>Clear Cache</span>
<span>{t("preview.clearCache")}</span>
<span className="text-xs text-muted-foreground">
Clears cookies and local storage and other app cache
{t("preview.clearCacheDescription")}
</span>
</div>
</DropdownMenuItem>
......
......@@ -10,6 +10,7 @@ import {
} from "@/components/ui/tooltip";
import { useAtomValue } from "jotai";
import { selectedFileAtom } from "@/atoms/viewAtoms";
import { useTranslation } from "react-i18next";
interface App {
id?: number;
......@@ -23,6 +24,7 @@ export interface CodeViewProps {
// Code view component that displays app files or status messages
export const CodeView = ({ loading, app }: CodeViewProps) => {
const { t } = useTranslation("home");
const selectedFile = useAtomValue(selectedFileAtom);
const { refreshApp } = useLoadApp(app?.id ?? null);
const [isFullscreen, setIsFullscreen] = useState(false);
......@@ -48,12 +50,14 @@ export const CodeView = ({ loading, app }: CodeViewProps) => {
}, [isFullscreen]);
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) {
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) => {
>
<RefreshCw size={16} />
</TooltipTrigger>
<TooltipContent>Refresh Files</TooltipContent>
<TooltipContent>{t("preview.refreshFiles")}</TooltipContent>
</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" />
<Tooltip>
<TooltipTrigger
......@@ -92,7 +98,9 @@ export const CodeView = ({ loading, app }: CodeViewProps) => {
{isFullscreen ? <Minimize2 size={16} /> : <Maximize2 size={16} />}
</TooltipTrigger>
<TooltipContent>
{isFullscreen ? "Exit full screen" : "Enter full screen"}
{isFullscreen
? t("preview.exitFullScreen")
: t("preview.enterFullScreen")}
</TooltipContent>
</Tooltip>
</div>
......@@ -111,7 +119,7 @@ export const CodeView = ({ loading, app }: CodeViewProps) => {
/>
) : (
<div className="text-center py-4 text-gray-500">
Select a file to view
{t("preview.selectFileToView")}
</div>
)}
</div>
......@@ -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 {
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
import { useTranslation } from "react-i18next";
interface ConsoleEntryProps {
type: "server" | "client" | "edge-function" | "network-requests";
......@@ -42,6 +43,7 @@ export const ConsoleEntryComponent = (props: ConsoleEntryProps) => {
isExpanded = false,
onToggleExpand,
} = props;
const { t } = useTranslation(["home", "common"]);
const setChatInput = useSetAtom(chatInputValueAtom);
const isTruncated = message.length > MAX_MESSAGE_LENGTH;
......@@ -114,11 +116,11 @@ export const ConsoleEntryComponent = (props: ConsoleEntryProps) => {
>
{isExpanded ? (
<>
Show less <ChevronUp size={12} />
{t("common:showLess")} <ChevronUp size={12} />
</>
) : (
<>
Show more <ChevronDown size={12} />
{t("common:showMore")} <ChevronDown size={12} />
</>
)}
</button>
......@@ -137,7 +139,7 @@ export const ConsoleEntryComponent = (props: ConsoleEntryProps) => {
>
<MessageSquare size={12} className="text-gray-500" />
</TooltipTrigger>
<TooltipContent>Send to chat</TooltipContent>
<TooltipContent>{t("home:preview.sendToChat")}</TooltipContent>
</Tooltip>
</div>
);
......
......@@ -4,6 +4,7 @@ import {
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
import { useTranslation } from "react-i18next";
interface ConsoleFiltersProps {
levelFilter: "all" | "info" | "warn" | "error";
......@@ -39,6 +40,7 @@ export const ConsoleFilters = ({
totalLogs,
showFilters,
}: ConsoleFiltersProps) => {
const { t } = useTranslation("home");
const hasActiveFilters =
levelFilter !== "all" || typeFilter !== "all" || sourceFilter !== "";
......@@ -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"
>
<option value="all">All Levels</option>
<option value="info">Info</option>
<option value="warn">Warn</option>
<option value="error">Error</option>
<option value="all">{t("preview.consoleFilters.allLevels")}</option>
<option value="info">{t("preview.consoleFilters.info")}</option>
<option value="warn">{t("preview.consoleFilters.warn")}</option>
<option value="error">{t("preview.consoleFilters.error")}</option>
</select>
{/* Type filter */}
......@@ -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"
>
<option value="all">All Types</option>
<option value="server">Server</option>
<option value="client">Client</option>
<option value="edge-function">Edge Function</option>
<option value="network-requests">Network Requests</option>
<option value="all">{t("preview.consoleFilters.allTypes")}</option>
<option value="server">{t("preview.consoleFilters.server")}</option>
<option value="client">{t("preview.consoleFilters.client")}</option>
<option value="edge-function">
{t("preview.consoleFilters.edgeFunction")}
</option>
<option value="network-requests">
{t("preview.consoleFilters.networkRequests")}
</option>
</select>
{/* Source filter */}
......@@ -93,7 +99,7 @@ export const ConsoleFilters = ({
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"
>
<option value="">All Sources</option>
<option value="">{t("preview.consoleFilters.allSources")}</option>
{uniqueSources.map((source) => (
<option key={source} value={source}>
{source}
......@@ -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"
>
<X size={12} />
Clear Filters
{t("preview.consoleFilters.clearFilters")}
</button>
)}
......@@ -126,10 +132,12 @@ export const ConsoleFilters = ({
>
<Trash2 size={14} />
</TooltipTrigger>
<TooltipContent>Clear logs</TooltipContent>
<TooltipContent>{t("preview.consoleFilters.clearLogs")}</TooltipContent>
</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>
);
};
......@@ -17,6 +17,7 @@ import {
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
import { useTranslation } from "react-i18next";
interface FileEditorProps {
appId: number | null;
......@@ -37,6 +38,7 @@ const Breadcrumb: React.FC<BreadcrumbProps> = ({
onSave,
isSaving,
}) => {
const { t } = useTranslation("home");
const segments = path.split("/").filter(Boolean);
return (
......@@ -74,7 +76,9 @@ const Breadcrumb: React.FC<BreadcrumbProps> = ({
<Save size={12} />
</TooltipTrigger>
<TooltipContent>
{hasUnsavedChanges ? "Save changes" : "No unsaved changes"}
{hasUnsavedChanges
? t("preview.saveChanges")
: t("preview.noUnsavedChanges")}
</TooltipContent>
</Tooltip>
{hasUnsavedChanges && (
......@@ -95,6 +99,7 @@ export const FileEditor = ({
filePath,
initialLine = null,
}: FileEditorProps) => {
const { t } = useTranslation("home");
const { content, loading, error } = useLoadAppFile(appId, filePath);
const { theme } = useTheme();
const [value, setValue] = useState<string | undefined>(undefined);
......@@ -206,7 +211,7 @@ export const FileEditor = ({
if (warning) {
showWarning(warning);
} else {
showSuccess("File saved");
showSuccess(t("preview.fileSaved"));
}
originalValueRef.current = currentValueRef.current;
......@@ -231,7 +236,7 @@ export const FileEditor = ({
}, [initialLine, filePath, content, navigateToLine]);
if (loading) {
return <div className="p-4">Loading file content...</div>;
return <div className="p-4">{t("preview.loadingFileContent")}</div>;
}
if (error) {
......@@ -239,7 +244,9 @@ export const FileEditor = ({
}
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 (
......
......@@ -13,6 +13,7 @@ import { useSetAtom } from "jotai";
import { Input } from "@/components/ui/input";
import type { AppFileSearchResult } from "@/ipc/types";
import { useSearchAppFiles } from "@/hooks/useSearchAppFiles";
import { useTranslation } from "react-i18next";
interface FileTreeProps {
appId: number | null;
......@@ -100,6 +101,7 @@ const buildFileTree = (files: string[]): TreeNode[] => {
// File tree component
export const FileTree = ({ appId, files }: FileTreeProps) => {
const { t } = useTranslation("home");
const [searchValue, setSearchValue] = useState("");
const prevAppIdRef = useRef<number | null>(appId);
......@@ -168,7 +170,7 @@ export const FileTree = ({ appId, files }: FileTreeProps) => {
<Input
value={searchValue}
onChange={(event) => setSearchValue(event.target.value)}
placeholder="Search file contents"
placeholder={t("preview.searchFileContents")}
className="h-8 pl-7 pr-16 text-sm"
data-testid="file-tree-search"
disabled={!appId}
......@@ -177,7 +179,7 @@ export const FileTree = ({ appId, files }: FileTreeProps) => {
<button
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
onClick={() => setSearchValue("")}
aria-label="Clear search"
aria-label={t("preview.clearSearch")}
>
<X size={14} />
</button>
......@@ -193,8 +195,8 @@ export const FileTree = ({ appId, files }: FileTreeProps) => {
<div className="mt-1 flex items-center justify-between text-[11px] text-muted-foreground">
<span>
{searchLoading
? "Searching files..."
: `${matchesByPath.size} match${matchesByPath.size === 1 ? "" : "es"}`}
? t("preview.searchingFiles")
: t("preview.match", { count: matchesByPath.size })}
</span>
</div>
)}
......@@ -211,7 +213,7 @@ export const FileTree = ({ appId, files }: FileTreeProps) => {
!searchError &&
matchesByPath.size === 0 ? (
<div className="px-3 py-2 text-xs text-muted-foreground">
No files matched your search.
{t("preview.noFilesMatchedSearch")}
</div>
) : isSearchMode ? (
<div className="px-2 py-1">
......
......@@ -19,6 +19,7 @@ import { PublishPanel } from "./PublishPanel";
import { SecurityPanel } from "./SecurityPanel";
import { PlanPanel } from "./PlanPanel";
import { useSupabase } from "@/hooks/useSupabase";
import { useTranslation } from "react-i18next";
interface ConsoleHeaderProps {
isOpen: boolean;
......@@ -31,24 +32,29 @@ const ConsoleHeader = ({
isOpen,
onToggle,
latestMessage,
}: ConsoleHeaderProps) => (
<div
onClick={onToggle}
className="flex items-start gap-2 px-4 py-1.5 border-t border-border cursor-pointer hover:bg-[var(--background-darkest)] transition-colors"
>
<Logs size={16} className="mt-0.5" />
<div className="flex flex-col">
<span className="text-sm font-medium">System Messages</span>
{!isOpen && latestMessage && (
<span className="text-xs text-gray-500 truncate max-w-[200px] md:max-w-[400px]">
{latestMessage}
}: ConsoleHeaderProps) => {
const { t } = useTranslation("home");
return (
<div
onClick={onToggle}
className="flex items-start gap-2 px-4 py-1.5 border-t border-border cursor-pointer hover:bg-[var(--background-darkest)] transition-colors"
>
<Logs size={16} className="mt-0.5" />
<div className="flex flex-col">
<span className="text-sm font-medium">
{t("preview.systemMessages")}
</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 className="flex-1" />
{isOpen ? <ChevronDown size={16} /> : <ChevronUp size={16} />}
</div>
);
);
};
// Main PreviewPanel component
export function PreviewPanel() {
......
......@@ -18,6 +18,7 @@ import { useStreamChat } from "@/hooks/useStreamChat";
import { useCheckProblems } from "@/hooks/useCheckProblems";
import { createProblemFixPrompt } from "@/shared/problem_prompt";
import { showError } from "@/lib/toast";
import { useTranslation } from "react-i18next";
interface ProblemItemProps {
problem: Problem;
......@@ -26,6 +27,7 @@ interface ProblemItemProps {
}
const ProblemItem = ({ problem, checked, onToggle }: ProblemItemProps) => {
const { t } = useTranslation(["home", "common"]);
return (
<div
role="checkbox"
......@@ -39,7 +41,7 @@ const ProblemItem = ({ problem, checked, onToggle }: ProblemItemProps) => {
onCheckedChange={onToggle}
onClick={(e) => e.stopPropagation()}
className="mt-0.5"
aria-label="Select problem"
aria-label={t("home:preview.problems_panel.selectProblem")}
/>
<div className="flex-shrink-0 mt-0.5">
<XCircle size={16} className="text-red-500" />
......@@ -82,6 +84,7 @@ const RecheckButton = ({
className = "h-7 px-3 text-xs",
onBeforeRecheck,
}: RecheckButtonProps) => {
const { t } = useTranslation(["home", "common"]);
const { checkProblems, isChecking } = useCheckProblems(appId);
const [showingFeedback, setShowingFeedback] = useState(false);
......@@ -115,7 +118,9 @@ const RecheckButton = ({
size={14}
className={`mr-1 ${isShowingChecking ? "animate-spin" : ""}`}
/>
{isShowingChecking ? "Checking..." : "Run checks"}
{isShowingChecking
? t("home:preview.problems_panel.checkingProblems")
: t("home:preview.problems_panel.runChecks")}
</Button>
);
};
......@@ -137,6 +142,7 @@ const ProblemsSummary = ({
onFixSelected,
onSelectAll,
}: ProblemsSummaryProps) => {
const { t } = useTranslation(["home", "common"]);
const { problems } = problemReport;
const totalErrors = problems.length;
......@@ -146,7 +152,7 @@ const ProblemsSummary = ({
return (
<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">
No problems found
{t("home:preview.problems_panel.noProblemsFound")}
</p>
<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" />
......@@ -164,7 +170,7 @@ const ProblemsSummary = ({
<div className="flex items-center gap-2">
<XCircle size={16} className="text-red-500" />
<span className="text-sm font-medium">
{totalErrors} {totalErrors === 1 ? "error" : "errors"}
{t("home:preview.problems_panel.error", { count: totalErrors })}
</span>
</div>
)}
......@@ -178,7 +184,7 @@ const ProblemsSummary = ({
onClick={onSelectAll}
className="h-7 px-3 text-xs"
>
Select all
{t("common:selectAll")}
</Button>
) : (
<Button
......@@ -187,7 +193,7 @@ const ProblemsSummary = ({
onClick={onClearAll}
className="h-7 px-3 text-xs"
>
Clear all
{t("common:clearAll")}
</Button>
)}
<Button
......@@ -199,7 +205,9 @@ const ProblemsSummary = ({
disabled={selectedCount === 0}
>
<Wrench size={14} className="mr-1" />
{`Fix ${selectedCount} ${selectedCount === 1 ? "problem" : "problems"}`}
{t("home:preview.problems_panel.fixProblems", {
count: selectedCount,
})}
</Button>
</div>
</div>
......@@ -215,6 +223,7 @@ export function Problems() {
}
export function _Problems() {
const { t } = useTranslation(["home", "common"]);
const selectedAppId = useAtomValue(selectedAppIdAtom);
const { problemReport } = useCheckProblems(selectedAppId);
const [selectedKeys, setSelectedKeys] = useState<Set<string>>(new Set());
......@@ -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">
<AlertTriangle size={24} className="text-muted-foreground" />
</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">
Select an app to view TypeScript problems and diagnostic information.
{t("home:preview.problems_panel.noAppSelectedDescription")}
</p>
</div>
);
......@@ -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">
<AlertTriangle size={24} className="text-muted-foreground" />
</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">
Run checks to scan your app for TypeScript errors and other problems.
{t("home:preview.problems_panel.noProblemsReportDescription")}
</p>
<RecheckButton
appId={selectedAppId}
......
......@@ -14,9 +14,11 @@ import {
} from "@/hooks/useAgentTools";
import { Loader2, ChevronRight } from "lucide-react";
import { AgentToolConsent } from "@/lib/schemas";
import { useTranslation } from "react-i18next";
export function AgentToolsSettings() {
const { tools, isLoading, setConsent } = useAgentTools();
const { t } = useTranslation("settings");
const [showAutoApproved, setShowAutoApproved] = useState(false);
const handleConsentChange = (
......@@ -42,7 +44,7 @@ export function AgentToolsSettings() {
return (
<div className="space-y-6">
<p className="text-sm text-muted-foreground">
Configure permissions for Agent built-in tools.
{t("agentPermissions.description")}
</p>
{/* Requires approval tools */}
......@@ -70,7 +72,11 @@ export function AgentToolsSettings() {
<ChevronRight
className={`size-4 transition-transform ${showAutoApproved ? "rotate-90" : ""}`}
/>
<span>Default allowed tools ({autoApprovedTools.length})</span>
<span>
{t("agentPermissions.defaultAllowedTools", {
count: autoApprovedTools.length,
})}
</span>
</button>
{showAutoApproved && (
<div className="space-y-2 pl-6">
......@@ -103,6 +109,7 @@ function ToolConsentRow({
consent: AgentToolConsent;
onConsentChange: (consent: AgentToolConsent) => void;
}) {
const { t } = useTranslation("settings");
return (
<div className="border rounded p-3">
<div className="flex items-center justify-between gap-4">
......@@ -120,9 +127,13 @@ function ToolConsentRow({
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="ask">Ask</SelectItem>
<SelectItem value="always">Always allow</SelectItem>
<SelectItem value="never">Never allow</SelectItem>
<SelectItem value="ask">{t("agentPermissions.ask")}</SelectItem>
<SelectItem value="always">
{t("agentPermissions.alwaysAllow")}
</SelectItem>
<SelectItem value="never">
{t("agentPermissions.neverAllow")}
</SelectItem>
</SelectContent>
</Select>
</div>
......
......@@ -15,6 +15,7 @@ import { showError, showInfo, showSuccess } from "@/lib/toast";
import { Edit2, Plus, Save, Trash2, X } from "lucide-react";
import { useDeepLink } from "@/contexts/DeepLinkContext";
import { AddMcpServerDeepLinkData } from "@/ipc/deep_link_data";
import { useTranslation } from "react-i18next";
type KeyValue = { key: string; value: string };
......@@ -60,6 +61,7 @@ function KeyValueEditor({
isSaving: boolean;
itemLabel?: string;
}) {
const { t } = useTranslation(["settings", "common"]);
const initial = useMemo(() => parseJsonToArray(json), [json]);
const [envVars, setEnvVars] = useState<KeyValue[]>(initial);
const [editingKey, setEditingKey] = useState<string | null>(null);
......@@ -80,11 +82,11 @@ function KeyValueEditor({
const handleAdd = async () => {
if (!newKey.trim() || !newValue.trim()) {
showError("Both key and value are required");
showError(t("toolsMcp.keyValueRequired"));
return;
}
if (envVars.some((e) => e.key === newKey.trim())) {
showError(`${itemLabel} with this key already exists`);
showError(t("settings:toolsMcp.duplicateKey"));
return;
}
const next = [...envVars, { key: newKey.trim(), value: newValue.trim() }];
......@@ -104,7 +106,7 @@ function KeyValueEditor({
const handleSaveEdit = async () => {
if (!editingKey) return;
if (!editingKeyValue.trim() || !editingValue.trim()) {
showError("Both key and value are required");
showError(t("toolsMcp.keyValueRequired"));
return;
}
if (
......@@ -112,7 +114,7 @@ function KeyValueEditor({
(e) => e.key === editingKeyValue.trim() && e.key !== editingKey,
)
) {
showError(`${itemLabel} with this key already exists`);
showError(t("settings:toolsMcp.duplicateKey"));
return;
}
const next = envVars.map((e) =>
......@@ -144,10 +146,16 @@ function KeyValueEditor({
{isAddingNew ? (
<div className="space-y-3 p-3 border rounded-md bg-muted/50">
<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
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}
onChange={(e) => setNewKey(e.target.value)}
autoFocus
......@@ -155,11 +163,15 @@ function KeyValueEditor({
/>
</div>
<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
id={`env-new-value-${id}`}
placeholder={
itemLabel === "Header" ? "Value" : "e.g., /usr/local/bin"
itemLabel === "Header"
? t("settings:toolsMcp.value")
: t("settings:toolsMcp.valuePlaceholder")
}
value={newValue}
onChange={(e) => setNewValue(e.target.value)}
......@@ -173,7 +185,7 @@ function KeyValueEditor({
disabled={disabled || isSaving}
>
<Save size={14} />
{isSaving ? "Saving..." : "Save"}
{isSaving ? t("common:saving") : t("common:save")}
</Button>
<Button
onClick={() => {
......@@ -185,7 +197,7 @@ function KeyValueEditor({
size="sm"
>
<X size={14} />
Cancel
{t("common:cancel")}
</Button>
</div>
</div>
......@@ -197,7 +209,7 @@ function KeyValueEditor({
disabled={disabled}
>
<Plus size={14} />
Add {itemLabel}
{t("settings:toolsMcp.addEnvVar")}
</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"
}
{
"buildingApp": "Building your app",
"settingUp": "We're setting up your app with AI magic.",
"mightTakeMoment": "This might take a moment...",
"moreIdeas": "More ideas",
"buildMeA": "Build me a {{label}}",
"failedCreateApp": "Failed to create app. {{error}}",
"whatsNew": "What's new in v{{version}}?",
"releaseNotesTitle": "Release notes for v{{version}}",
"importApp": "Import App",
"importAppDescription": "Import existing app from local folder or clone from Github.",
"importExperimental": "App import is an experimental feature. If you encounter any issues, please report them using the Help button.",
"localFolder": "Local Folder",
"githubRepos": "GitHub Repos",
"yourGithubRepos": "Your GitHub Repos",
"githubUrl": "GitHub URL",
"selectFolder": "Select Folder",
"selectingFolder": "Selecting folder...",
"selectedFolder": "Selected folder:",
"clearSelection": "Clear selection",
"copyToDyadApps": "Copy to the dyad-apps folder",
"appNameExists": "An app with this name already exists. Please choose a different name:",
"appName": "App name",
"appNameOptional": "App name (optional)",
"enterNewAppName": "Enter new app name",
"leaveEmptyForRepo": "Leave empty to use repository name",
"advancedOptions": "Advanced options",
"installCommand": "Install command",
"startCommand": "Start command",
"bothCommandsRequired": "Both commands are required when customizing.",
"noAiRulesFound": "No AI_RULES.md found. Dyad will automatically generate one after importing.",
"importingApp": "Importing app...",
"import": "Import",
"noRepositoriesFound": "No repositories found",
"repositoryUrl": "Repository URL",
"repositoryUrlPlaceholder": "https://github.com/user/repo.git",
"appImportedWithRules": "App imported successfully. Dyad will automatically generate an AI_RULES.md now.",
"appImported": "App imported successfully",
"successfullyImported": "Successfully imported {{name}}",
"failedFetchRepos": "Failed to fetch repositories.",
"failedCheckAppName": "Failed to check app name: {{error}}",
"failedImportRepo": "Failed to import repository: {{error}}",
"createNewApp": "Create New App",
"createAppUsingTemplate": "Create a new app using the {{template}} template.",
"enterAppName": "Enter app name...",
"appNameAlreadyExists": "An app with this name already exists",
"createApp": "Create App",
"forceCloseDetected": "Force Close Detected",
"forceCloseDescription": "The app was not closed properly the last time it was running. This could indicate a crash or unexpected termination.",
"lastKnownState": "Last Known State:",
"processMetrics": "Process Metrics",
"memory": "Memory:",
"cpu": "CPU:",
"systemMetrics": "System Metrics",
"communityCodeNotice": "Community Code Notice",
"communityCodeWarning": "This code was created by a Dyad community member, not our core team.",
"communityCodeRisk": "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.",
"communityCodeReview": "We recommend reviewing the code on GitHub first. Only proceed if you're comfortable with these risks.",
"deleteItemTitle": "Delete {{itemType}}",
"deleteItemConfirmation": "Are you sure you want to delete \"{{itemName}}\"? This action cannot be undone.",
"proBanner": {
"manageDyadPro": "Manage Dyad Pro",
"alreadyHavePro": "Already have Dyad Pro? Add your key",
"accessLeadingModels": "Access leading AI models with one plan",
"getDyadPro": "Get Dyad Pro",
"upTo3xCheaper": "Up to 3x cheaper",
"byUsingSmartContext": "by using Smart Context",
"generateCode4x": "Generate code 4-10x faster",
"withTurboModels": "with Turbo Models & Turbo Edits"
},
"setup": {
"buildNewApp": "Build a new app",
"setupDyad": "Setup Dyad",
"installNodeJs": "1. Install Node.js (App Runtime)",
"errorCheckingNode": "Error checking Node.js status. Try installing Node.js.",
"nodeInstalled": "Node.js ({{version}}) installed.",
"pnpmInstalled": "(optional) pnpm ({{version}}) installed.",
"nodeRequired": "Node.js is required to run apps locally.",
"afterInstallNode": "After you have installed Node.js, click 'Continue'. If the installer didn't work, try more download options.",
"moreDownloadOptions": "more download options",
"nodeAlreadyInstalled": "Node.js already installed? Configure path manually",
"browseForNodeFolder": "Browse for Node.js folder",
"nodeTroubleshooting": "Node.js troubleshooting guide",
"stillStuck": "Still stuck? Click the Help button in the bottom-left corner and then Report a Bug.",
"installNodeRuntime": "Install Node.js Runtime",
"checkingNodeSetup": "Checking Node.js setup...",
"continueInstalled": "Continue | I installed Node.js",
"nodeNotDetected": "Node.js not detected. Closing and re-opening Dyad usually fixes this.",
"setupAiAccess": "2. Setup AI Access",
"notSureWatchVideo": "Not sure what to do? Watch the Get Started video above",
"setupGeminiApiKey": "Setup Google Gemini API Key",
"setupOpenRouterApiKey": "Setup OpenRouter API Key",
"setupDyadPro": "Setup Dyad Pro",
"accessAllModels": "Access all AI models with one plan",
"setupOtherProviders": "Setup other AI providers",
"openAiAnthropicMore": "OpenAI, Anthropic and more",
"freeModelsAvailable": "Free models available"
},
"customTheme": {
"createTitle": "Create Custom Theme",
"createDescription": "Create a custom theme using manual configuration or AI-powered generation.",
"aiPoweredGenerator": "AI-Powered Generator",
"manualConfiguration": "Manual Configuration",
"themeName": "Theme Name",
"themeNamePlaceholder": "My Custom Theme",
"descriptionOptional": "Description (optional)",
"descriptionPlaceholder": "A brief description of your theme",
"themePrompt": "Theme Prompt",
"themePromptPlaceholder": "Enter your theme system prompt...",
"saveTheme": "Save Theme",
"enterThemeName": "Please enter a theme name",
"enterThemePrompt": "Please enter a theme prompt",
"generatePromptFirst": "Please generate a prompt first",
"themeCreated": "Custom theme created successfully",
"failedCreateTheme": "Failed to create theme: {{error}}"
},
"editTheme": {
"title": "Edit Theme",
"description": "Modify your custom theme settings and prompt.",
"themeName": "Theme Name",
"themeNamePlaceholder": "Theme name",
"descriptionOptional": "Description (optional)",
"descriptionPlaceholder": "A brief description of your theme",
"themePrompt": "Theme Prompt",
"themePromptPlaceholder": "Enter your theme system prompt...",
"themeUpdated": "Theme updated successfully",
"failedUpdateTheme": "Failed to update theme: {{error}}"
},
"prompt": {
"newPrompt": "New Prompt",
"editPrompt": "Edit prompt",
"createTitle": "Create New Prompt",
"editTitle": "Edit Prompt",
"createDescription": "Create a new prompt template for your library.",
"editDescription": "Edit your prompt template.",
"titlePlaceholder": "Title",
"descriptionPlaceholder": "Description (optional)",
"contentPlaceholder": "Content"
},
"help": {
"needHelp": "Need help with Dyad?",
"helpOptions": "If you need help or want to report an issue, here are some options:",
"chatWithHelpBot": "Chat with Dyad help bot (Pro)",
"helpBotDescription": "Opens an in-app help chat assistant that searches through Dyad's docs.",
"openDocs": "Open Docs",
"openDocsDescription": "Get help with common questions and issues.",
"reportBug": "Report a Bug",
"reportBugDescription": "We'll auto-fill your report with system info and logs. You can review it for any sensitive info before submitting.",
"preparingReport": "Preparing Report...",
"uploadChatSession": "Upload Chat Session",
"uploadChatDescription": "Share chat logs and code for troubleshooting. Data is used only to resolve your issue and auto-deleted after a limited time.",
"preparingUpload": "Preparing Upload...",
"helpBotTitle": "Dyad Help Bot",
"askQuestion": "Ask a question about using Dyad.",
"conversationLogged": "This conversation may be logged and used to improve the product. Please do not put any sensitive information in here.",
"typeQuestion": "Type your question...",
"sending": "Sending...",
"send": "Send",
"uploadComplete": "Upload Complete",
"chatLogsUploaded": "Chat Logs Uploaded Successfully",
"mustOpenIssue": "You must open a GitHub issue for us to investigate. Without a linked issue, your report will not be reviewed.",
"openGithubIssue": "Open GitHub Issue",
"okToUpload": "OK to upload chat session?",
"reviewSubmission": "Please review the information that will be submitted. Your chat messages, system information, and a snapshot of your codebase will be included.",
"chatMessages": "Chat Messages",
"you": "You",
"assistant": "Assistant",
"codebaseSnapshot": "Codebase Snapshot",
"systemInformation": "System Information",
"dyadVersion": "Dyad Version:",
"platform": "Platform:",
"architecture": "Architecture:",
"nodeVersion": "Node Version:",
"pnpmVersion": "PNPM Version:",
"nodePath": "Node Path:",
"proUserId": "Pro User ID:",
"telemetryId": "Telemetry ID:",
"model": "Model:",
"logs": "Logs",
"upload": "Upload"
},
"screenshot": {
"takeScreenshot": "Take a screenshot?",
"takeScreenshotRecommended": "Take a screenshot (recommended)",
"betterResponses": "You'll get better and faster responses if you do this!",
"fileWithoutScreenshot": "File bug report without screenshot",
"mightNotHelp": "We'll still try to respond but might not be able to help as much.",
"failedScreenshot": "Failed to take screenshot: {{error}}",
"capturedToClipboard": "Screenshot captured to clipboard! Please paste in GitHub issue.",
"createGithubIssue": "Create GitHub issue"
},
"preview": {
"title": "Preview",
"problems": "Problems",
"code": "Code",
"configure": "Configure",
"security": "Security",
"publish": "Publish",
"moreOptions": "More options",
"rebuild": "Rebuild",
"rebuildDescription": "Re-installs node_modules and restarts",
"clearCache": "Clear Cache",
"clearCacheDescription": "Clears cookies and local storage and other app cache",
"loadingFiles": "Loading files...",
"noAppSelected": "No app selected",
"refreshFiles": "Refresh Files",
"files": "files",
"exitFullScreen": "Exit full screen",
"enterFullScreen": "Enter full screen",
"selectFileToView": "Select a file to view",
"noFilesFound": "No files found",
"searchFileContents": "Search file contents",
"clearSearch": "Clear search",
"searchingFiles": "Searching files...",
"match_one": "{{count}} match",
"match_other": "{{count}} matches",
"noFilesMatchedSearch": "No files matched your search.",
"saveChanges": "Save changes",
"noUnsavedChanges": "No unsaved changes",
"fileSaved": "File saved",
"loadingFileContent": "Loading file content...",
"noContentAvailable": "No content available",
"sendToChat": "Send to chat",
"systemMessages": "System Messages",
"consoleFilters": {
"allLevels": "All Levels",
"info": "Info",
"warn": "Warn",
"error": "Error",
"allTypes": "All Types",
"server": "Server",
"client": "Client",
"edgeFunction": "Edge Function",
"networkRequests": "Network Requests",
"allSources": "All Sources",
"clearFilters": "Clear Filters",
"clearLogs": "Clear logs",
"logs": "logs"
},
"problems_panel": {
"selectProblem": "Select problem",
"noProblemsFound": "No problems found",
"runChecks": "Run checks",
"checkingProblems": "Checking...",
"noAppSelectedTitle": "No App Selected",
"noAppSelectedDescription": "Select an app to view TypeScript problems and diagnostic information.",
"noProblemsReportTitle": "No Problems Report",
"noProblemsReportDescription": "Run checks to scan your app for TypeScript errors and other problems.",
"fixProblems": "Fix {{count}} problem(s)",
"error_one": "{{count}} error",
"error_other": "{{count}} errors"
},
"publish_panel": {
"noAppSelectedTitle": "No App Selected",
"noAppSelectedDescription": "Select an app to view publishing options.",
"publishApp": "Publish App",
"github": "GitHub",
"githubDescription": "Sync your code to GitHub for collaboration.",
"vercel": "Vercel",
"vercelDescription": "Publish your app by deploying it to Vercel.",
"githubRequiredForVercel": "GitHub Required for Vercel Deployment",
"githubRequiredDescription": "Deploying to Vercel requires connecting to GitHub first. Please set up your GitHub repository above."
},
"security_panel": {
"justNow": "just now",
"minutesAgo_one": "{{count}} minute ago",
"minutesAgo_other": "{{count}} minutes ago",
"hoursAgo_one": "{{count}} hour ago",
"hoursAgo_other": "{{count}} hours ago",
"daysAgo_one": "{{count}} day ago",
"daysAgo_other": "{{count}} days ago",
"critical": "critical",
"high": "high",
"medium": "medium",
"low": "low",
"openDocs": "Open Security Review docs",
"editSecurityRules": "Edit Security Rules",
"runningReview": "Running Security Review...",
"runReview": "Run Security Review",
"noAppSelectedTitle": "No App Selected",
"noAppSelectedDescription": "Select an app to run a security review",
"reviewRunning": "Security review is running",
"reviewRunningDescription": "Results will be available soon.",
"noReviewFoundTitle": "No Security Review Found",
"noReviewFoundDescription": "Run a security review to identify potential vulnerabilities in your application.",
"noIssuesFoundTitle": "No Security Issues Found",
"noIssuesFoundDescription": "Your application passed the security review with no issues detected.",
"lastReviewed": "Last reviewed",
"level": "Level",
"issue": "Issue",
"action": "Action",
"selectAllIssues": "Select all issues",
"fixIssue": "Fix Issue",
"fixingIssue": "Fixing Issue...",
"fixIssueCount_one": "Fix {{count}} Issue",
"fixIssueCount_other": "Fix {{count}} Issues",
"fixingIssueCount_one": "Fixing {{count}} Issue...",
"fixingIssueCount_other": "Fixing {{count}} Issues...",
"editRulesTitle": "Edit Security Rules",
"editRulesDescription": "This allows you to add additional context about your project specifically for security reviews. This content is saved to the SECURITY_RULES.md file. This can help catch additional issues or avoid flagging issues that are not relevant for your app.",
"securityRulesPlaceholder": "# SECURITY_RULES.md..."
},
"visualEditing": {
"deselectComponent": "Deselect Component",
"styledDynamically": "This component is styled dynamically",
"margin": "Margin",
"horizontal": "Horizontal",
"vertical": "Vertical",
"padding": "Padding",
"border": "Border",
"width": "Width",
"radius": "Radius",
"color": "Color",
"backgroundColor": "Background Color",
"background": "Background",
"textStyle": "Text Style",
"fontSize": "Font Size",
"fontWeight": "Font Weight",
"fontFamily": "Font Family",
"textColor": "Text Color"
},
"annotator": {
"select": "Select",
"draw": "Draw",
"text": "Text",
"color": "Color",
"deleteSelected": "Delete Selected",
"undo": "Undo",
"redo": "Redo",
"addToChat": "Add to Chat",
"closeAnnotator": "Close Annotator",
"proFeatureTitle": "Annotator is a Pro Feature",
"proFeatureDescription": "Unlock the ability to annotate screenshots and enhance your workflow with Dyad Pro.",
"getDyadPro": "Get Dyad Pro"
}
},
"integrations": {
"github": {
"title": "GitHub Integration",
"connected": "Your account is connected to GitHub.",
"disconnect": "Disconnect from GitHub",
"disconnected": "Successfully disconnected from GitHub",
"failedDisconnect": "Failed to disconnect from GitHub",
"errorDisconnect": "An error occurred while disconnecting from GitHub",
"connectedToRepo": "Connected to GitHub Repo:",
"syncToGithub": "Sync to GitHub",
"syncing": "Syncing...",
"disconnectFromRepo": "Disconnect from repo",
"seeTroubleshooting": "See troubleshooting guide",
"rebaseInProgress": "A rebase is already in progress. Choose how to proceed.",
"abortRebase": "Abort rebase",
"aborting": "Aborting...",
"continueRebase": "Continue rebase",
"continuing": "Continuing...",
"safeForcesPush": "Safe Force Push",
"safeForcePushing": "Safe force pushing...",
"rebaseAndSync": "Rebase and Sync",
"conflictsDetected": "There are conflicts in the repository. Please resolve them in the editor.",
"pushedSuccess": "Successfully pushed to GitHub!",
"forcePushWarning": "Force Push Warning",
"forcePushDescription": "You are about to perform a force push to your GitHub repository.",
"dangerousNonReversible": "This is dangerous and non-reversible and will:",
"overwriteRemote": "Overwrite the remote repository history",
"deleteRemoteCommits": "Permanently delete commits that exist on the remote but not locally",
"onlyProceedCertain": "Only proceed if you're certain this is what you want to do.",
"forcePush": "Force Push",
"forcePushing": "Force Pushing...",
"connectToGithub": "Connect to GitHub",
"githubConnection": "GitHub Connection",
"goTo": "1. Go to:",
"enterCode": "2. Enter code:",
"setupGithubRepo": "Set up your GitHub repo",
"createNewRepo": "Create new repo",
"connectExistingRepo": "Connect to existing repo",
"repositoryName": "Repository Name",
"checkingAvailability": "Checking availability...",
"repoAvailable": "Repository name is available!",
"selectRepository": "Select Repository",
"loadingRepositories": "Loading repositories...",
"selectARepository": "Select a repository",
"branch": "Branch",
"loadingBranches": "Loading branches...",
"selectABranch": "Select a branch",
"headCurrent": "HEAD (Current)",
"typeCustomBranch": "Type custom branch name",
"enterBranchName": "Enter branch name (e.g., feature/new-feature)",
"createRepo": "Create Repo",
"connectToRepo": "Connect to Repo",
"repoCreatedLinked": "Repository created and linked!",
"connectedToRepo2": "Connected to repository!",
"rebaseAborted": "Rebase aborted. You can try syncing again.",
"rebaseContinued": "Rebase continued. You can sync when ready.",
"mergeConflicts": "Merge conflicts detected. Please resolve them in the editor.",
"failedAbortRebase": "Failed to abort rebase.",
"failedContinueRebase": "Failed to continue rebase.",
"failedSync": "Failed to sync to GitHub.",
"failedDisconnectRepo": "Failed to disconnect repository."
},
"githubBranch": {
"selectBranch": "Select branch",
"branchLabel": "Branch:",
"refreshBranches": "Refresh branches",
"createNewBranch": "Create new branch",
"createBranchTitle": "Create New Branch",
"createBranchDescription": "Create a new branch.",
"branchName": "Branch Name",
"branchNamePlaceholder": "feature/my-new-feature",
"sourceBranch": "Source Branch",
"sourceBranchPlaceholder": "Select source (optional, defaults to HEAD)",
"createBranch": "Create Branch",
"renameBranch": "Rename Branch",
"renameBranchDescription": "Enter a new name for branch '{{name}}'.",
"newName": "New Name",
"rename": "Rename",
"mergeBranch": "Merge Branch",
"mergeBranchConfirmation": "Are you sure you want to merge '{{source}}' into '{{target}}'?",
"merge": "Merge",
"merging": "Merging...",
"deleteBranch": "Delete Branch",
"deleteBranchConfirmation": "This will permanently delete the branch '{{name}}'. This action cannot be undone.",
"mergeInProgress": "Merge in Progress",
"rebaseInProgress": "Rebase in Progress",
"abortAction": "This action will abort the current operation",
"operationInProgress": "A {{type}} operation is currently in progress...",
"unresolvedConflicts": "Unresolved conflicts detected",
"abortWarning": "Aborting will discard any conflict resolution work you've already done.",
"abortConfirmation": "Are you sure you want to abort the {{type}} and switch branches?",
"keepWorking": "Keep working",
"abortAndSwitch": "Abort {{type}} & Switch",
"branches": "Branches",
"branchesDescription": "Manage your branches, merge, delete, and more.",
"nativeGitRequired": "Native Git Required",
"nativeGitDescription": "Some Git actions (like rebase, merge abort, and advanced branch operations) require Native Git to be enabled.",
"enableInSettings": "Enable in Settings",
"mergeInto": "Merge into {{branch}}",
"branchCreated": "Branch '{{name}}' created",
"switchedToBranch": "Switched to branch '{{name}}'",
"abortedAndSwitched": "Aborted ongoing {{type}} and switched to branch '{{name}}'",
"branchDeleted": "Branch '{{name}}' deleted",
"branchRenamed": "Renamed '{{oldName}}' to '{{newName}}'",
"branchMerged": "Merged '{{source}}' into '{{target}}'",
"mergeConflict": "Merge conflict detected. Please resolve them in the editor.",
"failedLoadBranches": "Failed to load branches",
"failedCreateBranch": "Failed to create branch",
"failedSwitchBranch": "Failed to switch branch",
"failedDeleteBranch": "Failed to delete branch",
"failedRenameBranch": "Failed to rename branch",
"failedMergeBranch": "Failed to merge branch"
},
"githubCollaborator": {
"title": "Collaborators",
"description": "Manage who has access to this project via GitHub.",
"usernamePlaceholder": "GitHub username",
"inviting": "Inviting...",
"invite": "Invite",
"currentTeam": "Current Team",
"loadingCollaborators": "Loading collaborators...",
"noCollaborators": "No collaborators found.",
"admin": "Admin",
"editor": "Editor",
"viewer": "Viewer",
"removeCollaborator": "Remove collaborator?",
"removeConfirmation": "Are you sure you want to remove {{name}} from this project? This action cannot be undone.",
"invited": "Invited {{name}} to the project.",
"removed": "Removed {{name}} from the project.",
"failedLoad": "Failed to load collaborators: {{error}}"
},
"vercel": {
"title": "Vercel Integration",
"connected": "Your account is connected to Vercel.",
"disconnect": "Disconnect from Vercel",
"disconnected": "Successfully disconnected from Vercel",
"failedDisconnect": "Failed to disconnect from Vercel",
"errorDisconnect": "An error occurred while disconnecting from Vercel",
"connectedToProject": "Connected to Vercel Project:",
"liveUrl": "Live URL:",
"refreshDeployments": "Refresh Deployments",
"refreshing": "Refreshing...",
"disconnectFromProject": "Disconnect from project",
"recentDeployments": "Recent Deployments:",
"view": "View",
"connectToVercel": "Connect to Vercel",
"setupInstructions": "To connect your app to Vercel, you'll need to create an access token:",
"signUpFirst": "If you don't have a Vercel account, sign up first",
"goToSettings": "Go to Vercel settings to create a token",
"copyToken": "Copy the token and paste it below",
"signUp": "Sign Up for Vercel",
"openSettings": "Open Vercel Settings",
"accessToken": "Vercel Access Token",
"enterToken": "Enter your Vercel access token",
"savingToken": "Saving Token...",
"saveToken": "Save Access Token",
"tokenSaved": "Successfully connected to Vercel! You can now set up your project below.",
"setupProject": "Set up your Vercel project",
"createNewProject": "Create new project",
"connectExistingProject": "Connect to existing project",
"projectName": "Project Name",
"selectProject": "Select Project",
"loadingProjects": "Loading projects...",
"selectAProject": "Select a project",
"projectAvailable": "Project name is available!",
"createProject": "Create Project",
"connectToProject": "Connect to Project",
"projectCreatedLinked": "Project created and linked!",
"connectedToProject2": "Connected to project!",
"failedCreateProject": "Failed to create project.",
"failedConnectProject": "Failed to connect to project.",
"failedSaveToken": "Failed to save access token.",
"failedCheckAvailability": "Failed to check project availability."
},
"supabase": {
"title": "Supabase Integration",
"organizationsConnected_one": "{{count}} organization connected to Supabase.",
"organizationsConnected_other": "{{count}} organizations connected to Supabase.",
"disconnectAll": "Disconnect All",
"disconnectOrganization": "Disconnect organization",
"writeSqlMigrations": "Write SQL migration files",
"writeSqlDescription": "Generate SQL migration files when modifying your Supabase schema. This helps you track database changes in version control, though these files aren't used for chat context, which uses the live schema.",
"disconnectedAll": "Successfully disconnected all Supabase organizations",
"failedDisconnect": "Failed to disconnect from Supabase",
"orgDisconnected": "Organization disconnected successfully",
"settingUpdated": "Setting updated",
"project": "Supabase Project",
"connectedToProject": "This app is connected to project:",
"databaseBranch": "Database Branch",
"selectBranch": "Select a branch",
"disconnectProject": "Disconnect Project",
"projectConnected": "Project connected to app successfully",
"failedConnectProject": "Failed to connect project to app: {{error}}",
"projects": "Supabase Projects",
"selectProjectDescription": "Select a Supabase project to connect to this app",
"refreshProjects": "Refresh projects",
"addOrganization": "Add Organization",
"errorLoadingProjects": "Error loading projects: {{message}}",
"connectedOrganizations": "Connected Organizations",
"noProjectsFound": "No projects found in your connected Supabase organizations.",
"selectAProject": "Select a project",
"projectNotFound": "Project not found",
"branchNotFound": "Branch not found",
"branchSelected": "Branch selected",
"failedSetBranch": "Failed to set branch: {{error}}",
"failedDisconnectProject": "Failed to disconnect project from app"
},
"neon": {
"title": "Neon Integration",
"connected": "Your account is connected to Neon.",
"database": "Neon Database",
"connectedToNeon": "You are connected to Neon Database",
"freeTier": "Neon Database has a good free tier with backups and up to 10 projects.",
"connectTo": "Connect to",
"connectedSuccess": "Successfully connected to Neon!",
"disconnect": "Disconnect from Neon",
"disconnected": "Disconnected from Neon successfully",
"failedDisconnect": "Failed to disconnect from Neon"
}
}
}
{
"title": "Settings",
"general": {
"title": "General Settings",
"language": "Language",
"languageDescription": "Choose your preferred display language.",
"theme": "Theme",
"themeSystem": "System",
"themeLight": "Light",
"themeDark": "Dark",
"zoom": "Zoom level",
"zoomDescription": "Adjusts the zoom level to make content easier to read.",
"selectZoom": "Select zoom level",
"appVersion": "App Version:",
"autoUpdate": "Auto-update app",
"autoUpdateDescription": "This will automatically update the app when new versions are available.",
"releaseChannel": "Release channel",
"releaseChannelDescription": "Controls which update channel the app uses.",
"stable": "Stable",
"beta": "Beta",
"selectReleaseChannel": "Select release channel",
"runtimeMode": "Runtime Mode",
"runtimeModeDescription": "Select the runtime to use for running the app.",
"selectRuntimeMode": "Select runtime mode",
"nodePath": "Node.js Path Configuration",
"browseForNode": "Browse for Node.js",
"selecting": "Selecting...",
"resetToDefault": "Reset to Default",
"customPath": "Custom Path:",
"systemPath": "System PATH:",
"notFound": "Not found",
"nodeConfigured": "Node.js is properly configured and ready to use.",
"nodeSelectFolder": "Select the folder where Node.js is installed if it's not in your system PATH."
},
"workflow": {
"title": "Workflow Settings",
"defaultChatMode": "Default Chat Mode",
"defaultChatModeDescription": "Controls the default chat mode when you open a new chat.",
"selectDefaultChatMode": "Select default chat mode",
"autoApprove": "Auto-approve changes",
"autoApproveDescription": "This will automatically approve code changes and run them.",
"autoFixProblems": "Auto-fix problems",
"autoFixProblemsDescription": "This will automatically fix TypeScript errors."
},
"ai": {
"title": "AI Settings",
"thinkingBudget": "Thinking Budget",
"thinkingBudgetDescription": "Controls how long the AI model can think. Higher budgets mean more thorough reasoning.",
"selectThinkingBudget": "Select thinking budget",
"low": "Low",
"medium": "Medium",
"high": "High",
"maxChatTurns": "Max Chat Turns in Context",
"maxChatTurnsDescription": "Limits how many recent chat turns are included in context.",
"selectMaxChatTurns": "Select max turns",
"unlimited": "Unlimited",
"turnCount_one": "{{count}} turn",
"turnCount_other": "{{count}} turns",
"providers": "AI Providers",
"failedToLoadProviders": "Failed to load AI providers: {{message}}",
"freeTierAvailable": "Free tier available",
"addCustomProvider": "Add custom provider",
"connectCustomEndpoint": "Connect to a custom LLM API endpoint",
"editProvider": "Edit Provider",
"deleteProvider": "Delete Provider",
"deleteCustomProvider": "Delete Custom Provider",
"deleteProviderConfirmation": "This will permanently delete this custom provider and all its associated models. This action cannot be undone.",
"deleteProviderAction": "Delete Provider",
"configureProvider": "Configure {{name}}",
"setupComplete": "Setup Complete",
"notSetup": "Not Setup",
"createApiKey": "Create your API key with {{name}}",
"manageApiKeys": "Manage API Keys",
"setupApiKey": "Setup API Key",
"providerNotFound": "Provider Not Found",
"providerNotFoundDescription": "The provider with ID \"{{provider}}\" could not be found.",
"errorLoadingProvider": "Error Loading Provider Details",
"errorLoadingSettings": "Error Loading Settings",
"couldNotLoadProvider": "Could not load provider data: {{message}}",
"couldNotLoadSettings": "Could not load configuration data: {{message}}",
"enableDyadPro": "Enable Dyad Pro",
"toggleDyadPro": "Toggle to enable Dyad Pro",
"apiKeyEmpty": "API Key cannot be empty.",
"errorTogglingPro": "Error toggling Dyad Pro: {{error}}",
"failedSaveApiKey": "Failed to save API key.",
"failedDeleteApiKey": "Failed to delete API key."
},
"telemetry": {
"title": "Telemetry",
"description": "This records anonymous usage data to improve the product.",
"telemetryId": "Telemetry ID:",
"enable": "Enable Telemetry",
"privacyNotice": "We use anonymous telemetry data to improve the product. No personal data is collected.",
"acceptAndContinue": "Accept and Continue",
"shareAnonymousData": "Share anonymous data?",
"helpImprove": "Help improve Dyad with anonymous usage data.",
"noCodeOrMessages": "Note: this does not log your code or messages.",
"learnMore": "Learn more",
"accept": "Accept",
"reject": "Reject",
"later": "Later"
},
"integrations": {
"title": "Integrations"
},
"agentPermissions": {
"title": "Agent Permissions (Pro)",
"description": "Configure permissions for Agent built-in tools.",
"defaultAllowedTools": "Default allowed tools ({{count}})",
"ask": "Ask",
"alwaysAllow": "Always allow",
"neverAllow": "Never allow"
},
"toolsMcp": {
"title": "Tools (MCP)",
"name": "Name",
"namePlaceholder": "My MCP Server",
"transport": "Transport",
"stdio": "stdio",
"http": "http",
"command": "Command",
"commandPlaceholder": "node",
"args": "Args",
"argsPlaceholder": "path/to/mcp-server.js --flag",
"url": "URL",
"urlPlaceholder": "http://localhost:3000",
"addServer": "Add Server",
"environmentVariables": "Environment Variables",
"key": "Key",
"keyPlaceholder": "e.g., PATH",
"value": "Value",
"valuePlaceholder": "e.g., /usr/local/bin",
"addEnvVar": "Add Environment Variable",
"noEnvVars": "No environment variables configured",
"keyValueRequired": "Both key and value are required",
"duplicateKey": "Environment variable with this key already exists",
"noToolsDiscovered": "No tools discovered.",
"noServersConfigured": "No servers configured yet.",
"prefilledServer": "Prefilled {{name}} MCP server",
"deny": "Deny"
},
"experiments": {
"title": "Experiments",
"enableNativeGit": "Enable Native Git",
"enableNativeGitDescription": "This doesn't require any external Git installation and offers a faster, native-Git performance experience."
},
"dangerZone": {
"title": "Danger Zone",
"resetEverything": "Reset Everything",
"resetDescription": "This will delete all your apps, chats, and settings. This action cannot be undone.",
"resetConfirmation": "Are you sure you want to reset everything? This will delete all your apps, chats, and settings. This action cannot be undone.",
"resetting": "Resetting...",
"resetSuccess": "Successfully reset everything. Restart the application."
},
"apiKey": {
"fromSettings": "API Key from Settings",
"currentKey": "Current Key (Settings)",
"thisKeyActive": "This key is currently active.",
"setKey": "Set {{name}} API Key",
"updateKey": "Update {{name}} API Key",
"enterKey": "Enter new {{name}} API Key here",
"pasteAndSave": "Paste from clipboard and save",
"saveKey": "Save Key",
"overrideEnvVar": "Setting a key here will override the environment variable (if set).",
"fromEnvVar": "API Key from Environment Variable",
"envVarKey": "Environment Variable Key ({{name}})",
"envVarActive": "This key is currently active (no settings key set).",
"envVarOverridden": "This key is currently being overridden by the key set in Settings.",
"envVarNotSet": "Environment Variable Not Set",
"envVarNotSetDescription": "The {{name}} environment variable is not set.",
"envVarHelpText": "This key is set outside the application. If present, it will be used only if no key is configured in the Settings section above. Requires app restart to detect changes."
},
"azure": {
"configured": "Azure OpenAI Configured",
"configuredDescription": "Dyad will use the credentials saved in Settings for Azure OpenAI models.",
"usingEnvVars": "Using Environment Variables",
"usingEnvVarsDescription": "AZURE_API_KEY and AZURE_RESOURCE_NAME are set. Values saved below will override them.",
"configRequired": "Azure OpenAI Configuration Required",
"configRequiredDescription": "Provide your Azure resource name and API key below, or configure the AZURE_API_KEY and AZURE_RESOURCE_NAME environment variables.",
"resourceName": "Resource Name",
"resourceNamePlaceholder": "your-azure-openai-resource",
"apiKey": "API Key",
"apiKeyPlaceholder": "Enter your Azure OpenAI API key",
"saveSettings": "Save Settings",
"saved": "Saved",
"configNeeded": "Configuration Needed",
"configNeededDescription": "Azure OpenAI requests require both a resource name and API key. Enter them above or supply the environment variables instead.",
"saveError": "Save Error",
"envVarsOptional": "Environment Variables (optional)",
"envVarsHelpText": "You can continue to configure Azure via environment variables. If both variables are present and no settings are saved, Dyad will use them automatically.",
"envVarsPrecedence": "Values saved in Settings take precedence over environment variables. Restart Dyad after changing environment variables."
},
"vertex": {
"projectId": "Project ID",
"projectIdPlaceholder": "your-gcp-project-id",
"location": "Location",
"locationPlaceholder": "us-central1",
"locationHelp": "If you see a \"model not found\" error, try a different region. Some partner models (MaaS) are only available in specific locations (e.g., us-central1, us-west2).",
"serviceAccountKey": "Service Account JSON Key",
"serviceAccountKeyPlaceholder": "Paste the full JSON contents of your service account key here",
"saveSettings": "Save Settings",
"saved": "Saved",
"configRequired": "Configuration Required",
"configRequiredDescription": "Provide Project, Location, and a service account JSON key with Vertex AI access.",
"saveError": "Save Error",
"invalidJson": "Service account JSON is invalid: {{message}}"
},
"models": {
"title": "Models",
"description": "Manage specific models available through this provider.",
"errorLoading": "Error Loading Models",
"noCustomModels": "No custom models have been added for this provider yet.",
"addCustomModel": "Add Custom Model",
"deleteConfirmTitle": "Are you sure you want to delete this model?",
"deleteConfirmDescription": "This action cannot be undone. This will permanently delete the custom model \"{{name}}\" (API Name: {{apiName}}).",
"yesDeleteIt": "Yes, delete it",
"contextTokens": "Context: {{count}} tokens",
"maxOutputTokens": "Max Output: {{count}} tokens",
"modelIdRequired": "Model API name is required",
"modelNameRequired": "Model display name is required",
"invalidMaxOutput": "Max Output Tokens must be a valid number",
"invalidContextWindow": "Context Window must be a valid number",
"addModelTitle": "Add Custom Model",
"addModelDescription": "Configure a new language model for the selected provider.",
"editModelTitle": "Edit Custom Model",
"editModelDescription": "Modify the configuration of the selected language model.",
"modelId": "Model ID*",
"modelIdHelp": "This must match the model expected by the API",
"modelName": "Name*",
"modelNameHelp": "Human-friendly name for the model",
"modelDescription": "Description",
"modelDescriptionHelp": "Optional: Describe the model's capabilities",
"maxOutputTokensLabel": "Max Output Tokens",
"maxOutputTokensHelp": "Optional: e.g., 4096",
"contextWindowLabel": "Context Window",
"contextWindowHelp": "Optional: e.g., 8192",
"adding": "Adding...",
"addModel": "Add Model",
"updateModel": "Update Model",
"modelCreated": "Custom model created successfully!",
"modelUpdated": "Custom model updated successfully!",
"failedUpdateSettings": "Failed to update settings"
},
"customProvider": {
"addTitle": "Add Custom Provider",
"editTitle": "Edit Custom Provider",
"addDescription": "Connect to a custom language model provider API.",
"editDescription": "Update your custom language model provider configuration.",
"providerId": "Provider ID",
"providerIdPlaceholder": "E.g., my-provider",
"providerIdHelp": "A unique identifier for this provider (no spaces).",
"displayName": "Display Name",
"displayNamePlaceholder": "E.g., My Provider",
"displayNameHelp": "The name that will be displayed in the UI.",
"apiBaseUrl": "API Base URL",
"apiBaseUrlPlaceholder": "E.g., https://api.example.com/v1",
"apiBaseUrlHelp": "The base URL for the API endpoint.",
"envVar": "Environment Variable (Optional)",
"envVarPlaceholder": "E.g., MY_PROVIDER_API_KEY",
"envVarHelp": "Environment variable name for the API key.",
"addProvider": "Add Provider",
"updateProvider": "Update Provider",
"failedCreate": "Failed to create custom provider"
},
"manageDyadPro": "Manage Dyad Pro Subscription",
"setupDyadPro": "Setup Dyad Pro Subscription"
}
{
"newChat": "Novo Chat",
"recentChats": "Chats Recentes",
"searchChats": "Pesquisar chats",
"loadingChats": "Carregando chats...",
"noChatsFound": "Nenhum chat encontrado",
"renameChat": "Renomear Chat",
"deleteChat": "Excluir Chat",
"renameChatDescription": "Insira um novo nome para este chat.",
"chatTitle": "Título",
"enterChatTitle": "Insira o título do chat...",
"chatRenamed": "Chat renomeado com sucesso",
"failedRenameChat": "Falha ao renomear o chat: {{error}}",
"failedCreateChat": "Falha ao criar novo chat: {{error}}",
"chatDeleted": "Chat excluído com sucesso",
"failedDeleteChat": "Falha ao excluir o chat: {{error}}",
"deleteChatConfirmation": "Tem certeza de que deseja excluir \"{{title}}\"? Esta ação não pode ser desfeita e todas as mensagens deste chat serão perdidas permanentemente.",
"deleteChatNote": "Nota: Quaisquer alterações de código já aceitas serão mantidas.",
"scrollToBottom": "Rolar para o final",
"dismissError": "Dispensar erro",
"askDyadToBuild": "Peça ao Dyad para construir...",
"cancelGeneration": "Cancelar geração",
"sendMessage": "Enviar mensagem",
"loadingProposal": "Carregando proposta...",
"errorLoadingProposal": "Erro ao carregar proposta: {{message}}",
"visualEditor": "Editor visual (Pro)",
"visualEditorDescription": "A edição visual permite fazer alterações na interface sem IA e é um recurso exclusivo do Pro",
"summarizeNewChatTip": "Criar um novo chat torna a IA mais focada e eficiente",
"summarizeToNewChat": "Resumir em novo chat",
"refactorFile": "Refatorar {{path}} e torná-lo mais modular",
"refactorDescription": "Refatorar o arquivo para melhorar a manutenibilidade",
"writeCodeProperly": "Escrever código corretamente",
"writeCodeProperlyDescription": "Escrever código corretamente (útil quando a IA gera o código no formato errado)",
"rebuildApp": "Reconstruir app",
"rebuildAppDescription": "Reconstruir o aplicativo",
"restartApp": "Reiniciar app",
"restartAppDescription": "Reiniciar o servidor de desenvolvimento",
"refreshApp": "Atualizar app",
"refreshAppDescription": "Atualizar a pré-visualização do aplicativo",
"keepGoing": "Continuar",
"tipProposal": "Dica de proposta",
"securityRisks": "Riscos de Segurança",
"sqlQueries": "Consultas SQL",
"packagesAdded": "Pacotes Adicionados",
"serverFunctionsChanged": "Funções do Servidor Alteradas",
"filesChanged": "Arquivos Alterados",
"securityRisksFound": "Riscos de segurança encontrados",
"approve": "Aprovar",
"reject": "Rejeitar",
"noChanges": "Sem alterações",
"sqlQuery": "Consulta SQL",
"approved": "Aprovado",
"rejected": "Rejeitado",
"copy": "Copiar",
"requestId": "ID da Requisição",
"copyRequestId": "Copiar ID da Requisição",
"maxTokensUsed": "Máximo de tokens utilizados: {{count}}",
"allowToolToRun": "Permitir que {{toolName}} seja executado?",
"queueCount": "(1 de {{total}})",
"allowOnce": "Permitir uma vez",
"alwaysAllow": "Sempre permitir",
"removeAttachment": "Remover anexo",
"recentChatActivity": "Atividade recente de chat",
"loadingActivity": "Carregando atividade...",
"noRecentChats": "Nenhum chat recente",
"uncommittedChanges": "Você tem {{count}} alteração(ões) não comitada(s).",
"reviewAndCommit": "Revisar e comitar",
"reviewCommitChanges": "Revisar e Comitar Alterações",
"reviewChangesDescription": "Revise suas alterações e insira uma mensagem de commit.",
"commitMessage": "Mensagem de commit",
"enterCommitMessage": "Insira a mensagem de commit...",
"changedFiles": "Arquivos alterados ({{count}})",
"committing": "Comitando...",
"commit": "Comitar",
"added": "Adicionado",
"modified": "Modificado",
"deleted": "Excluído",
"renamed": "Renomeado",
"updateFiles": "Atualizar arquivos",
"closeVersionPane": "Fechar painel de versões",
"versionHistory": "Histórico de Versões",
"noVersionsAvailable": "Nenhuma versão disponível",
"versionLabel": "Versão {{number}} ({{hash}})",
"dbSnapshot": "BD",
"dbSnapshotExpired": "Snapshot do BD pode ter expirado (mais de 24 horas)",
"dbSnapshotAvailable": "Snapshot do banco de dados disponível no timestamp {{timestamp}}",
"restoreToVersion": "Restaurar para esta versão",
"restoring": "Restaurando...",
"restoringToVersion": "Restaurando para esta versão...",
"revertedToVersion": "Todas as alterações foram revertidas para a versão {{version}}",
"revertedToHash": "Todas as alterações foram revertidas para a versão {{hash}}",
"contextUsed": "Usado:",
"contextLimit": "Limite:",
"contextLimitWarning": "Você está próximo do limite de contexto para este chat.",
"summarizeIntoNewChat": "Resumir em novo chat",
"fixAllErrors": "Corrigir Todos os Erros ({{count}})",
"todosCompleted": "{{completed}} de {{total}} tarefas concluídas",
"allTasksCompleted": "Todas as tarefas concluídas",
"noTaskInProgress": "Nenhuma tarefa em andamento",
"tokenUsage": "Tokens: {{count}}",
"tokenPercentUsed": "{{percent}}% de {{limit}}K",
"tokenUsageBreakdown": "Detalhamento do Uso de Tokens",
"messageHistory": "Histórico de Mensagens",
"codebase": "Base de Código",
"mentionedApps": "Apps Mencionados",
"systemPrompt": "Prompt do Sistema",
"currentInput": "Entrada Atual",
"total": "Total",
"failedCountTokens": "Falha ao contar tokens",
"optimizeTokens": "Otimize seus tokens com",
"dyadProSmartContext": "Contexto Inteligente do Dyad Pro",
"attachFiles": "Anexar arquivos",
"themes": "Temas",
"noTheme": "Sem Tema",
"moreThemes": "Mais temas",
"newTheme": "Novo Tema",
"allCustomThemes": "Todos os Temas Personalizados",
"showTokenUsage": "Mostrar uso de tokens",
"hideTokenUsage": "Ocultar uso de tokens",
"attachFileContext": "Anexar arquivo como contexto do chat",
"attachFileContextExample": "Exemplo de uso: captura de tela do app para apontar um problema na interface",
"uploadFileCodebase": "Enviar arquivo para a base de código",
"uploadFileCodebaseExample": "Exemplo de uso: adicionar uma imagem para usar no seu app",
"dropFilesToAttach": "Solte os arquivos para anexar",
"selectedComponents": "Componentes Selecionados ({{count}})",
"clearAllComponents": "Limpar todos os componentes selecionados",
"deselectComponent": "Desmarcar componente",
"chatMode": {
"openMenu": "Abrir menu de modo",
"toggleShortcut": "{{shortcut}} para alternar",
"agentV2": "Agente v2",
"agentV2Description": "Melhor para tarefas maiores e depuração",
"build": "Construir",
"buildDescription": "Gerar e editar código",
"ask": "Perguntar",
"askDescription": "Fazer perguntas sobre o app",
"buildWithMcp": "Construir com MCP",
"buildWithMcpDescription": "Como Construir, mas pode usar ferramentas (MCP) para gerar código"
},
"modelPicker": {
"modelLabel": "Modelo:",
"cloudModels": "Modelos na Nuvem",
"loadingModels": "Carregando modelos...",
"noCloudModels": "Nenhum modelo na nuvem disponível",
"otherProviders": "Outros provedores de IA",
"providerCount_one": "{{count}} provedor",
"providerCount_other": "{{count}} provedores",
"dyadTurbo": "Dyad Turbo",
"modelCount_one": "{{count}} modelo",
"modelCount_other": "{{count}} modelos",
"providerModels": "Modelos {{name}}",
"localModels": "Modelos locais",
"localModelsDescription": "LM Studio, Ollama",
"ollama": "Ollama",
"ollamaModels": "Modelos Ollama",
"errorLoading": "Erro ao carregar",
"noneAvailable": "Nenhum disponível",
"isOllamaRunning": "O Ollama está em execução?",
"noLocalModels": "Nenhum modelo local encontrado",
"ensureOllamaRunning": "Certifique-se de que o Ollama está em execução e os modelos foram baixados.",
"lmStudio": "LM Studio",
"lmStudioModels": "Modelos LM Studio",
"noLoadedModels": "Nenhum modelo carregado encontrado",
"ensureLMStudioRunning": "Certifique-se de que o LM Studio está em execução e os modelos estão carregados."
},
"header": {
"switchingToLatest": "Aguarde, voltando para a versão mais recente...",
"warningNotOnBranch": "Aviso: Você não está em uma branch",
"notOnBranch": "Você não está em uma branch",
"checkoutInProgress": "O checkout de versão está em andamento",
"checkoutMainBranch": "Faça checkout da branch main, caso contrário as alterações não serão salvas corretamente",
"checkingBranch": "Verificando branch...",
"onBranch": "Você está na branch: {{name}}.",
"renameMasterToMain": "Renomear master para main",
"renaming": "Renomeando...",
"switchToMainBranch": "Mudar para a branch main",
"checkingOut": "Fazendo checkout...",
"masterRenamed": "Branch master renomeada para main",
"versionCount": "Versão {{count}}"
},
"errorBox": {
"accessWithDyadPro": "Acessar com Dyad Pro",
"orSwitchModel": "ou mudar para outro modelo.",
"upgradeToDyadPro": "Atualizar para Dyad Pro",
"troubleshootingGuide": "Guia de solução de problemas",
"invalidProKey": "Parece que você não tem uma chave válida do Dyad Pro.",
"today": "hoje.",
"creditsUsed": "Você utilizou todos os seus créditos de IA do Dyad neste mês.",
"reloadOrUpgrade": "Recarregue ou atualize sua assinatura",
"getMoreCredits": "e obtenha mais créditos de IA",
"readDocs": "Ler documentação"
},
"promo": {
"tiredOfWaiting": "Cansado de esperar pela IA?",
"getDyadPro": "Obtenha o Dyad Pro",
"fasterEdits": "para edições mais rápidas com Turbo Edits.",
"saveOnCosts": "Economize até 3x nos custos de IA com",
"debuggingLoop": "Preso em um loop de depuração? Tente um modelo diferente.",
"joinBuilders": "Junte-se a mais de 600 criadores no",
"dyadSubreddit": "subreddit do Dyad",
"foundBug": "Encontrou um bug? Clique em Ajuda > Reportar um Bug",
"reportBadResponse": "Quer reportar uma resposta ruim da IA? Envie o chat clicando em Ajuda",
"watch": "Assista",
"creatorBuildApp": "o criador do Dyad construir um app bíblico passo a passo",
"gettingStuck": "Travando? Leia nossas",
"debuggingTips": "dicas de depuração",
"advancedTip": "Dica avançada: Personalize suas",
"aiRules": "regras de IA",
"keepFocused": "Quer manter a IA focada? Inicie um novo chat.",
"whatsNext": "Quer saber o que vem a seguir? Confira nosso",
"roadmap": "roadmap",
"likeDyad": "Gostou do Dyad? Dê uma estrela no",
"gitHub": "GitHub"
},
"consent": {
"toolWantsToRun": "Ferramenta quer executar",
"requestsConsent": "solicita seu consentimento.",
"inputRequired": "Entrada Necessária"
},
"agentModeActivated": "Modo Agente Ativado",
"agentModeTip": "Dica: Crie um novo chat para dar ao agente um contexto limpo para melhores resultados.",
"neverShowAgain": "Não mostrar novamente"
}
{
"save": "Salvar",
"cancel": "Cancelar",
"delete": "Excluir",
"confirm": "Confirmar",
"loading": "Carregando...",
"copyToClipboard": "Copiar para a área de transferência",
"copied": "Copiado!",
"close": "Fechar",
"back": "Voltar",
"goBack": "Voltar",
"reset": "Redefinir",
"error": "Erro",
"success": "Sucesso",
"warning": "Aviso",
"info": "Informação",
"yes": "Sim",
"no": "Não",
"retry": "Tentar novamente",
"ok": "OK",
"create": "Criar",
"update": "Atualizar",
"edit": "Editar",
"add": "Adicionar",
"remove": "Remover",
"search": "Pesquisar",
"refresh": "Atualizar",
"enabled": "Ativado",
"disabled": "Desativado",
"saving": "Salvando...",
"deleting": "Excluindo...",
"creating": "Criando...",
"updating": "Atualizando...",
"connecting": "Conectando...",
"disconnecting": "Desconectando...",
"importing": "Importando...",
"uploading": "Enviando...",
"preparing": "Preparando...",
"checking": "Verificando...",
"notSet": "Não definido",
"set": "Definido",
"custom": "Personalizado",
"ready": "Pronto",
"needsSetup": "Requer configuração",
"accept": "Aceitar",
"decline": "Recusar",
"showMore": "Mostrar mais",
"showLess": "Mostrar menos",
"selectAll": "Selecionar tudo",
"clearAll": "Limpar tudo",
"noResults": "Nenhum resultado encontrado",
"itemCount_one": "{{count}} item",
"itemCount_other": "{{count}} itens",
"pro": "Pro",
"free": "Grátis",
"recommended": "Recomendado",
"experimental": "experimental",
"new": "Novo",
"builtIn": "Integrado",
"advanced": "Avançado",
"required": "Obrigatório",
"optional": "Opcional"
}
{
"unknown": "Ocorreu um erro desconhecido",
"failedToCreate": "Falha ao criar: {{error}}",
"failedToUpdate": "Falha ao atualizar: {{error}}",
"failedToDelete": "Falha ao excluir: {{error}}",
"failedToLoad": "Falha ao carregar: {{error}}",
"networkError": "Ocorreu um erro de rede. Verifique sua conexão.",
"copyError": "Copiar mensagem de erro",
"errorOccurred": "Ocorreu um erro"
}
{
"buildingApp": "Construindo seu app",
"settingUp": "Estamos configurando seu app com a magia da IA.",
"mightTakeMoment": "Isso pode levar um momento...",
"moreIdeas": "Mais ideias",
"buildMeA": "Crie para mim um(a) {{label}}",
"failedCreateApp": "Falha ao criar o app. {{error}}",
"whatsNew": "O que há de novo na v{{version}}?",
"releaseNotesTitle": "Notas de lançamento da v{{version}}",
"importApp": "Importar App",
"importAppDescription": "Importe um app existente de uma pasta local ou clone do Github.",
"importExperimental": "A importação de apps é um recurso experimental. Se encontrar algum problema, por favor reporte usando o botão Ajuda.",
"localFolder": "Pasta Local",
"githubRepos": "Repositórios do GitHub",
"yourGithubRepos": "Seus Repositórios do GitHub",
"githubUrl": "URL do GitHub",
"selectFolder": "Selecionar Pasta",
"selectingFolder": "Selecionando pasta...",
"selectedFolder": "Pasta selecionada:",
"clearSelection": "Limpar seleção",
"copyToDyadApps": "Copiar para a pasta dyad-apps",
"appNameExists": "Já existe um app com este nome. Escolha um nome diferente:",
"appName": "Nome do app",
"appNameOptional": "Nome do app (opcional)",
"enterNewAppName": "Insira o novo nome do app",
"leaveEmptyForRepo": "Deixe vazio para usar o nome do repositório",
"advancedOptions": "Opções avançadas",
"installCommand": "Comando de instalação",
"startCommand": "Comando de inicialização",
"bothCommandsRequired": "Ambos os comandos são obrigatórios ao personalizar.",
"noAiRulesFound": "Nenhum AI_RULES.md encontrado. O Dyad gerará um automaticamente após a importação.",
"importingApp": "Importando app...",
"import": "Importar",
"noRepositoriesFound": "Nenhum repositório encontrado",
"repositoryUrl": "URL do Repositório",
"repositoryUrlPlaceholder": "https://github.com/usuario/repo.git",
"appImportedWithRules": "App importado com sucesso. O Dyad gerará automaticamente um AI_RULES.md agora.",
"appImported": "App importado com sucesso",
"successfullyImported": "{{name}} importado com sucesso",
"failedFetchRepos": "Falha ao buscar repositórios.",
"failedCheckAppName": "Falha ao verificar nome do app: {{error}}",
"failedImportRepo": "Falha ao importar repositório: {{error}}",
"createNewApp": "Criar Novo App",
"createAppUsingTemplate": "Crie um novo app usando o template {{template}}.",
"enterAppName": "Insira o nome do app...",
"appNameAlreadyExists": "Já existe um app com este nome",
"createApp": "Criar App",
"forceCloseDetected": "Fechamento Forçado Detectado",
"forceCloseDescription": "O app não foi fechado corretamente na última vez que foi executado. Isso pode indicar uma falha ou encerramento inesperado.",
"lastKnownState": "Último Estado Conhecido:",
"processMetrics": "Métricas do Processo",
"memory": "Memória:",
"cpu": "CPU:",
"systemMetrics": "Métricas do Sistema",
"communityCodeNotice": "Aviso sobre Código da Comunidade",
"communityCodeWarning": "Este código foi criado por um membro da comunidade Dyad, não pela nossa equipe principal.",
"communityCodeRisk": "O código da comunidade pode ser muito útil, mas como é desenvolvido de forma independente, pode conter bugs, riscos de segurança ou causar problemas no seu sistema. Não podemos oferecer suporte oficial caso ocorram problemas.",
"communityCodeReview": "Recomendamos revisar o código no GitHub primeiro. Prossiga apenas se estiver confortável com esses riscos.",
"deleteItemTitle": "Excluir {{itemType}}",
"deleteItemConfirmation": "Tem certeza de que deseja excluir \"{{itemName}}\"? Esta ação não pode ser desfeita.",
"proBanner": {
"manageDyadPro": "Gerenciar Dyad Pro",
"alreadyHavePro": "Já tem o Dyad Pro? Adicione sua chave",
"accessLeadingModels": "Acesse os melhores modelos de IA com um único plano",
"getDyadPro": "Obtenha o Dyad Pro",
"upTo3xCheaper": "Até 3x mais barato",
"byUsingSmartContext": "usando o Contexto Inteligente",
"generateCode4x": "Gere código 4-10x mais rápido",
"withTurboModels": "com Turbo Models e Turbo Edits"
},
"setup": {
"buildNewApp": "Criar um novo app",
"setupDyad": "Configurar Dyad",
"installNodeJs": "1. Instalar Node.js (Runtime do App)",
"errorCheckingNode": "Erro ao verificar o status do Node.js. Tente instalar o Node.js.",
"nodeInstalled": "Node.js ({{version}}) instalado.",
"pnpmInstalled": "(opcional) pnpm ({{version}}) instalado.",
"nodeRequired": "O Node.js é necessário para executar apps localmente.",
"afterInstallNode": "Após instalar o Node.js, clique em 'Continuar'. Se o instalador não funcionou, tente mais opções de download.",
"moreDownloadOptions": "mais opções de download",
"nodeAlreadyInstalled": "Node.js já instalado? Configure o caminho manualmente",
"browseForNodeFolder": "Procurar pasta do Node.js",
"nodeTroubleshooting": "Guia de solução de problemas do Node.js",
"stillStuck": "Ainda com problemas? Clique no botão Ajuda no canto inferior esquerdo e depois em Reportar um Bug.",
"installNodeRuntime": "Instalar Runtime do Node.js",
"checkingNodeSetup": "Verificando configuração do Node.js...",
"continueInstalled": "Continuar | Instalei o Node.js",
"nodeNotDetected": "Node.js não detectado. Fechar e reabrir o Dyad geralmente resolve isso.",
"setupAiAccess": "2. Configurar Acesso à IA",
"notSureWatchVideo": "Não sabe o que fazer? Assista ao vídeo de introdução acima",
"setupGeminiApiKey": "Configurar Chave de API do Google Gemini",
"setupOpenRouterApiKey": "Configurar Chave de API do OpenRouter",
"setupDyadPro": "Configurar Dyad Pro",
"accessAllModels": "Acesse todos os modelos de IA com um único plano",
"setupOtherProviders": "Configurar outros provedores",
"openAiAnthropicMore": "OpenAI, Anthropic e mais",
"freeModelsAvailable": "Modelos gratuitos disponíveis"
},
"customTheme": {
"createTitle": "Criar Tema Personalizado",
"createDescription": "Crie um tema personalizado usando configuração manual ou geração com IA.",
"aiPoweredGenerator": "Gerador com IA",
"manualConfiguration": "Configuração Manual",
"themeName": "Nome do Tema",
"themeNamePlaceholder": "Meu Tema Personalizado",
"descriptionOptional": "Descrição (opcional)",
"descriptionPlaceholder": "Uma breve descrição do seu tema",
"themePrompt": "Prompt do Tema",
"themePromptPlaceholder": "Insira o prompt de sistema do seu tema...",
"saveTheme": "Salvar Tema",
"enterThemeName": "Por favor, insira um nome para o tema",
"enterThemePrompt": "Por favor, insira um prompt para o tema",
"generatePromptFirst": "Por favor, gere um prompt primeiro",
"themeCreated": "Tema personalizado criado com sucesso",
"failedCreateTheme": "Falha ao criar tema: {{error}}"
},
"editTheme": {
"title": "Editar Tema",
"description": "Modifique as configurações e o prompt do seu tema personalizado.",
"themeName": "Nome do Tema",
"themeNamePlaceholder": "Nome do tema",
"descriptionOptional": "Descrição (opcional)",
"descriptionPlaceholder": "Uma breve descrição do seu tema",
"themePrompt": "Prompt do Tema",
"themePromptPlaceholder": "Insira o prompt de sistema do seu tema...",
"themeUpdated": "Tema atualizado com sucesso",
"failedUpdateTheme": "Falha ao atualizar tema: {{error}}"
},
"prompt": {
"newPrompt": "Novo Prompt",
"editPrompt": "Editar prompt",
"createTitle": "Criar Novo Prompt",
"editTitle": "Editar Prompt",
"createDescription": "Crie um novo template de prompt para sua biblioteca.",
"editDescription": "Edite seu template de prompt.",
"titlePlaceholder": "Título",
"descriptionPlaceholder": "Descrição (opcional)",
"contentPlaceholder": "Conteúdo"
},
"help": {
"needHelp": "Precisa de ajuda com o Dyad?",
"helpOptions": "Se precisar de ajuda ou quiser reportar um problema, aqui estão algumas opções:",
"chatWithHelpBot": "Conversar com o bot de ajuda do Dyad (Pro)",
"helpBotDescription": "Abre um assistente de chat de ajuda no app que pesquisa a documentação do Dyad.",
"openDocs": "Abrir Documentação",
"openDocsDescription": "Obtenha ajuda com perguntas e problemas comuns.",
"reportBug": "Reportar um Bug",
"reportBugDescription": "Preencheremos automaticamente seu relatório com informações do sistema e logs. Você pode revisar se há informações sensíveis antes de enviar.",
"preparingReport": "Preparando Relatório...",
"uploadChatSession": "Enviar Sessão de Chat",
"uploadChatDescription": "Compartilhe logs de chat e código para solução de problemas. Os dados são usados apenas para resolver seu problema e excluídos automaticamente após um tempo limitado.",
"preparingUpload": "Preparando Envio...",
"helpBotTitle": "Bot de Ajuda do Dyad",
"askQuestion": "Faça uma pergunta sobre o uso do Dyad.",
"conversationLogged": "Esta conversa pode ser registrada e usada para melhorar o produto. Por favor, não insira informações sensíveis aqui.",
"typeQuestion": "Digite sua pergunta...",
"sending": "Enviando...",
"send": "Enviar",
"uploadComplete": "Envio Concluído",
"chatLogsUploaded": "Logs de Chat Enviados com Sucesso",
"mustOpenIssue": "Você deve abrir uma issue no GitHub para que possamos investigar. Sem uma issue vinculada, seu relatório não será analisado.",
"openGithubIssue": "Abrir Issue no GitHub",
"okToUpload": "Deseja enviar a sessão de chat?",
"reviewSubmission": "Revise as informações que serão enviadas. Suas mensagens de chat, informações do sistema e um snapshot da sua base de código serão incluídos.",
"chatMessages": "Mensagens de Chat",
"you": "Você",
"assistant": "Assistente",
"codebaseSnapshot": "Snapshot da Base de Código",
"systemInformation": "Informações do Sistema",
"dyadVersion": "Versão do Dyad:",
"platform": "Plataforma:",
"architecture": "Arquitetura:",
"nodeVersion": "Versão do Node:",
"pnpmVersion": "Versão do PNPM:",
"nodePath": "Caminho do Node:",
"proUserId": "ID do Usuário Pro:",
"telemetryId": "ID de Telemetria:",
"model": "Modelo:",
"logs": "Logs",
"upload": "Enviar"
},
"screenshot": {
"takeScreenshot": "Tirar uma captura de tela?",
"takeScreenshotRecommended": "Tirar uma captura de tela (recomendado)",
"betterResponses": "Você terá respostas melhores e mais rápidas se fizer isso!",
"fileWithoutScreenshot": "Enviar relatório de bug sem captura de tela",
"mightNotHelp": "Ainda tentaremos responder, mas talvez não possamos ajudar tanto.",
"failedScreenshot": "Falha ao tirar captura de tela: {{error}}",
"capturedToClipboard": "Captura de tela copiada para a área de transferência! Cole na issue do GitHub.",
"createGithubIssue": "Criar issue no GitHub"
},
"preview": {
"title": "Pré-visualização",
"problems": "Problemas",
"code": "Código",
"configure": "Configurar",
"security": "Segurança",
"publish": "Publicar",
"moreOptions": "Mais opções",
"rebuild": "Reconstruir",
"rebuildDescription": "Reinstala o node_modules e reinicia",
"clearCache": "Limpar Cache",
"clearCacheDescription": "Limpa cookies, armazenamento local e outros caches do app",
"loadingFiles": "Carregando arquivos...",
"noAppSelected": "Nenhum app selecionado",
"refreshFiles": "Atualizar Arquivos",
"files": "arquivos",
"exitFullScreen": "Sair da tela cheia",
"enterFullScreen": "Entrar em tela cheia",
"selectFileToView": "Selecione um arquivo para visualizar",
"noFilesFound": "Nenhum arquivo encontrado",
"searchFileContents": "Pesquisar conteúdo dos arquivos",
"clearSearch": "Limpar pesquisa",
"searchingFiles": "Pesquisando arquivos...",
"match_one": "{{count}} resultado",
"match_other": "{{count}} resultados",
"noFilesMatchedSearch": "Nenhum arquivo corresponde à sua pesquisa.",
"saveChanges": "Salvar alterações",
"noUnsavedChanges": "Sem alterações não salvas",
"fileSaved": "Arquivo salvo",
"loadingFileContent": "Carregando conteúdo do arquivo...",
"noContentAvailable": "Nenhum conteúdo disponível",
"sendToChat": "Enviar para o chat",
"systemMessages": "Mensagens do Sistema",
"consoleFilters": {
"allLevels": "Todos os Níveis",
"info": "Info",
"warn": "Aviso",
"error": "Erro",
"allTypes": "Todos os Tipos",
"server": "Servidor",
"client": "Cliente",
"edgeFunction": "Edge Function",
"networkRequests": "Requisições de Rede",
"allSources": "Todas as Fontes",
"clearFilters": "Limpar Filtros",
"clearLogs": "Limpar logs",
"logs": "logs"
},
"problems_panel": {
"selectProblem": "Selecionar problema",
"noProblemsFound": "Nenhum problema encontrado",
"runChecks": "Executar verificações",
"checkingProblems": "Verificando...",
"noAppSelectedTitle": "Nenhum App Selecionado",
"noAppSelectedDescription": "Selecione um app para ver problemas de TypeScript e informações de diagnóstico.",
"noProblemsReportTitle": "Nenhum Relatório de Problemas",
"noProblemsReportDescription": "Execute verificações para escanear seu app em busca de erros de TypeScript e outros problemas.",
"fixProblems": "Corrigir {{count}} problema(s)",
"error_one": "{{count}} erro",
"error_other": "{{count}} erros"
},
"publish_panel": {
"noAppSelectedTitle": "Nenhum App Selecionado",
"noAppSelectedDescription": "Selecione um app para ver as opções de publicação.",
"publishApp": "Publicar App",
"github": "GitHub",
"githubDescription": "Sincronize seu código com o GitHub para colaboração.",
"vercel": "Vercel",
"vercelDescription": "Publique seu app implantando-o no Vercel.",
"githubRequiredForVercel": "GitHub Necessário para Deploy no Vercel",
"githubRequiredDescription": "O deploy no Vercel requer conexão com o GitHub primeiro. Configure seu repositório do GitHub acima."
},
"security_panel": {
"justNow": "agora mesmo",
"minutesAgo_one": "{{count}} minuto atrás",
"minutesAgo_other": "{{count}} minutos atrás",
"hoursAgo_one": "{{count}} hora atrás",
"hoursAgo_other": "{{count}} horas atrás",
"daysAgo_one": "{{count}} dia atrás",
"daysAgo_other": "{{count}} dias atrás",
"critical": "crítico",
"high": "alto",
"medium": "médio",
"low": "baixo",
"openDocs": "Abrir documentação de Revisão de Segurança",
"editSecurityRules": "Editar Regras de Segurança",
"runningReview": "Executando Revisão de Segurança...",
"runReview": "Executar Revisão de Segurança",
"noAppSelectedTitle": "Nenhum App Selecionado",
"noAppSelectedDescription": "Selecione um app para executar uma revisão de segurança",
"reviewRunning": "A revisão de segurança está em execução",
"reviewRunningDescription": "Os resultados estarão disponíveis em breve.",
"noReviewFoundTitle": "Nenhuma Revisão de Segurança Encontrada",
"noReviewFoundDescription": "Execute uma revisão de segurança para identificar possíveis vulnerabilidades no seu aplicativo.",
"noIssuesFoundTitle": "Nenhum Problema de Segurança Encontrado",
"noIssuesFoundDescription": "Seu aplicativo passou na revisão de segurança sem nenhum problema detectado.",
"lastReviewed": "Última revisão",
"level": "Nível",
"issue": "Problema",
"action": "Ação",
"selectAllIssues": "Selecionar todos os problemas",
"fixIssue": "Corrigir Problema",
"fixingIssue": "Corrigindo Problema...",
"fixIssueCount_one": "Corrigir {{count}} Problema",
"fixIssueCount_other": "Corrigir {{count}} Problemas",
"fixingIssueCount_one": "Corrigindo {{count}} Problema...",
"fixingIssueCount_other": "Corrigindo {{count}} Problemas...",
"editRulesTitle": "Editar Regras de Segurança",
"editRulesDescription": "Permite adicionar contexto adicional sobre o seu projeto especificamente para revisões de segurança. Este conteúdo é salvo no arquivo SECURITY_RULES.md. Isso pode ajudar a detectar problemas adicionais ou evitar alertas de problemas que não são relevantes para o seu app.",
"securityRulesPlaceholder": "# SECURITY_RULES.md..."
},
"visualEditing": {
"deselectComponent": "Desmarcar Componente",
"styledDynamically": "Este componente é estilizado dinamicamente",
"margin": "Margem",
"horizontal": "Horizontal",
"vertical": "Vertical",
"padding": "Preenchimento",
"border": "Borda",
"width": "Largura",
"radius": "Raio",
"color": "Cor",
"backgroundColor": "Cor de Fundo",
"background": "Fundo",
"textStyle": "Estilo do Texto",
"fontSize": "Tamanho da Fonte",
"fontWeight": "Peso da Fonte",
"fontFamily": "Família da Fonte",
"textColor": "Cor do Texto"
},
"annotator": {
"select": "Selecionar",
"draw": "Desenhar",
"text": "Texto",
"color": "Cor",
"deleteSelected": "Excluir Selecionado",
"undo": "Desfazer",
"redo": "Refazer",
"addToChat": "Adicionar ao Chat",
"closeAnnotator": "Fechar Anotador",
"proFeatureTitle": "O Anotador é um Recurso Pro",
"proFeatureDescription": "Desbloqueie a capacidade de anotar capturas de tela e aprimore seu fluxo de trabalho com o Dyad Pro.",
"getDyadPro": "Obtenha o Dyad Pro"
}
},
"integrations": {
"github": {
"title": "Integração com GitHub",
"connected": "Sua conta está conectada ao GitHub.",
"disconnect": "Desconectar do GitHub",
"disconnected": "Desconectado do GitHub com sucesso",
"failedDisconnect": "Falha ao desconectar do GitHub",
"errorDisconnect": "Ocorreu um erro ao desconectar do GitHub",
"connectedToRepo": "Conectado ao Repositório do GitHub:",
"syncToGithub": "Sincronizar com GitHub",
"syncing": "Sincronizando...",
"disconnectFromRepo": "Desconectar do repositório",
"seeTroubleshooting": "Ver guia de solução de problemas",
"rebaseInProgress": "Um rebase já está em andamento. Escolha como prosseguir.",
"abortRebase": "Abortar rebase",
"aborting": "Abortando...",
"continueRebase": "Continuar rebase",
"continuing": "Continuando...",
"safeForcesPush": "Force Push Seguro",
"safeForcePushing": "Fazendo force push seguro...",
"rebaseAndSync": "Rebase e Sincronizar",
"conflictsDetected": "Existem conflitos no repositório. Resolva-os no editor.",
"pushedSuccess": "Enviado ao GitHub com sucesso!",
"forcePushWarning": "Aviso de Force Push",
"forcePushDescription": "Você está prestes a realizar um force push para o seu repositório do GitHub.",
"dangerousNonReversible": "Isso é perigoso e irreversível e irá:",
"overwriteRemote": "Sobrescrever o histórico do repositório remoto",
"deleteRemoteCommits": "Excluir permanentemente commits que existem no remoto, mas não localmente",
"onlyProceedCertain": "Prossiga apenas se tiver certeza de que é isso que deseja fazer.",
"forcePush": "Force Push",
"forcePushing": "Fazendo Force Push...",
"connectToGithub": "Conectar ao GitHub",
"githubConnection": "Conexão com GitHub",
"goTo": "1. Acesse:",
"enterCode": "2. Insira o código:",
"setupGithubRepo": "Configurar seu repositório do GitHub",
"createNewRepo": "Criar novo repositório",
"connectExistingRepo": "Conectar a repositório existente",
"repositoryName": "Nome do Repositório",
"checkingAvailability": "Verificando disponibilidade...",
"repoAvailable": "O nome do repositório está disponível!",
"selectRepository": "Selecionar Repositório",
"loadingRepositories": "Carregando repositórios...",
"selectARepository": "Selecione um repositório",
"branch": "Branch",
"loadingBranches": "Carregando branches...",
"selectABranch": "Selecione uma branch",
"headCurrent": "HEAD (Atual)",
"typeCustomBranch": "Digite o nome de uma branch personalizada",
"enterBranchName": "Insira o nome da branch (ex.: feature/novo-recurso)",
"createRepo": "Criar Repositório",
"connectToRepo": "Conectar ao Repositório",
"repoCreatedLinked": "Repositório criado e vinculado!",
"connectedToRepo2": "Conectado ao repositório!",
"rebaseAborted": "Rebase abortado. Você pode tentar sincronizar novamente.",
"rebaseContinued": "Rebase continuado. Você pode sincronizar quando estiver pronto.",
"mergeConflicts": "Conflitos de merge detectados. Resolva-os no editor.",
"failedAbortRebase": "Falha ao abortar o rebase.",
"failedContinueRebase": "Falha ao continuar o rebase.",
"failedSync": "Falha ao sincronizar com o GitHub.",
"failedDisconnectRepo": "Falha ao desconectar o repositório."
},
"githubBranch": {
"selectBranch": "Selecionar branch",
"branchLabel": "Branch:",
"refreshBranches": "Atualizar branches",
"createNewBranch": "Criar nova branch",
"createBranchTitle": "Criar Nova Branch",
"createBranchDescription": "Crie uma nova branch.",
"branchName": "Nome da Branch",
"branchNamePlaceholder": "feature/meu-novo-recurso",
"sourceBranch": "Branch de Origem",
"sourceBranchPlaceholder": "Selecione a origem (opcional, padrão é HEAD)",
"createBranch": "Criar Branch",
"renameBranch": "Renomear Branch",
"renameBranchDescription": "Insira um novo nome para a branch '{{name}}'.",
"newName": "Novo Nome",
"rename": "Renomear",
"mergeBranch": "Fazer Merge da Branch",
"mergeBranchConfirmation": "Tem certeza de que deseja fazer merge de '{{source}}' em '{{target}}'?",
"merge": "Merge",
"merging": "Fazendo merge...",
"deleteBranch": "Excluir Branch",
"deleteBranchConfirmation": "Isso excluirá permanentemente a branch '{{name}}'. Esta ação não pode ser desfeita.",
"mergeInProgress": "Merge em Andamento",
"rebaseInProgress": "Rebase em Andamento",
"abortAction": "Esta ação abortará a operação atual",
"operationInProgress": "Uma operação de {{type}} está em andamento...",
"unresolvedConflicts": "Conflitos não resolvidos detectados",
"abortWarning": "Abortar descartará todo o trabalho de resolução de conflitos que você já fez.",
"abortConfirmation": "Tem certeza de que deseja abortar o {{type}} e mudar de branch?",
"keepWorking": "Continuar trabalhando",
"abortAndSwitch": "Abortar {{type}} e Mudar",
"branches": "Branches",
"branchesDescription": "Gerencie suas branches, faça merge, exclua e mais.",
"nativeGitRequired": "Git Nativo Necessário",
"nativeGitDescription": "Algumas ações do Git (como rebase, abort de merge e operações avançadas de branch) requerem que o Git Nativo esteja ativado.",
"enableInSettings": "Ativar nas Configurações",
"mergeInto": "Fazer merge em {{branch}}",
"branchCreated": "Branch '{{name}}' criada",
"switchedToBranch": "Mudou para a branch '{{name}}'",
"abortedAndSwitched": "{{type}} em andamento abortado e mudou para a branch '{{name}}'",
"branchDeleted": "Branch '{{name}}' excluída",
"branchRenamed": "'{{oldName}}' renomeada para '{{newName}}'",
"branchMerged": "Merge de '{{source}}' em '{{target}}' realizado",
"mergeConflict": "Conflito de merge detectado. Resolva-os no editor.",
"failedLoadBranches": "Falha ao carregar branches",
"failedCreateBranch": "Falha ao criar branch",
"failedSwitchBranch": "Falha ao mudar de branch",
"failedDeleteBranch": "Falha ao excluir branch",
"failedRenameBranch": "Falha ao renomear branch",
"failedMergeBranch": "Falha ao fazer merge da branch"
},
"githubCollaborator": {
"title": "Colaboradores",
"description": "Gerencie quem tem acesso a este projeto via GitHub.",
"usernamePlaceholder": "Nome de usuário do GitHub",
"inviting": "Convidando...",
"invite": "Convidar",
"currentTeam": "Equipe Atual",
"loadingCollaborators": "Carregando colaboradores...",
"noCollaborators": "Nenhum colaborador encontrado.",
"admin": "Administrador",
"editor": "Editor",
"viewer": "Visualizador",
"removeCollaborator": "Remover colaborador?",
"removeConfirmation": "Tem certeza de que deseja remover {{name}} deste projeto? Esta ação não pode ser desfeita.",
"invited": "{{name}} convidado para o projeto.",
"removed": "{{name}} removido do projeto.",
"failedLoad": "Falha ao carregar colaboradores: {{error}}"
},
"vercel": {
"title": "Integração com Vercel",
"connected": "Sua conta está conectada ao Vercel.",
"disconnect": "Desconectar do Vercel",
"disconnected": "Desconectado do Vercel com sucesso",
"failedDisconnect": "Falha ao desconectar do Vercel",
"errorDisconnect": "Ocorreu um erro ao desconectar do Vercel",
"connectedToProject": "Conectado ao Projeto Vercel:",
"liveUrl": "URL Ativa:",
"refreshDeployments": "Atualizar Deploys",
"refreshing": "Atualizando...",
"disconnectFromProject": "Desconectar do projeto",
"recentDeployments": "Deploys Recentes:",
"view": "Visualizar",
"connectToVercel": "Conectar ao Vercel",
"setupInstructions": "Para conectar seu app ao Vercel, você precisará criar um token de acesso:",
"signUpFirst": "Se não tiver uma conta Vercel, cadastre-se primeiro",
"goToSettings": "Acesse as configurações do Vercel para criar um token",
"copyToken": "Copie o token e cole abaixo",
"signUp": "Cadastrar-se no Vercel",
"openSettings": "Abrir Configurações do Vercel",
"accessToken": "Token de Acesso do Vercel",
"enterToken": "Insira seu token de acesso do Vercel",
"savingToken": "Salvando Token...",
"saveToken": "Salvar Token de Acesso",
"tokenSaved": "Conectado ao Vercel com sucesso! Agora você pode configurar seu projeto abaixo.",
"setupProject": "Configurar seu projeto Vercel",
"createNewProject": "Criar novo projeto",
"connectExistingProject": "Conectar a projeto existente",
"projectName": "Nome do Projeto",
"selectProject": "Selecionar Projeto",
"loadingProjects": "Carregando projetos...",
"selectAProject": "Selecione um projeto",
"projectAvailable": "O nome do projeto está disponível!",
"createProject": "Criar Projeto",
"connectToProject": "Conectar ao Projeto",
"projectCreatedLinked": "Projeto criado e vinculado!",
"connectedToProject2": "Conectado ao projeto!",
"failedCreateProject": "Falha ao criar projeto.",
"failedConnectProject": "Falha ao conectar ao projeto.",
"failedSaveToken": "Falha ao salvar o token de acesso.",
"failedCheckAvailability": "Falha ao verificar a disponibilidade do projeto."
},
"supabase": {
"title": "Integração com Supabase",
"organizationsConnected_one": "{{count}} organização conectada ao Supabase.",
"organizationsConnected_other": "{{count}} organizações conectadas ao Supabase.",
"disconnectAll": "Desconectar Todas",
"disconnectOrganization": "Desconectar organização",
"writeSqlMigrations": "Escrever arquivos de migração SQL",
"writeSqlDescription": "Gera arquivos de migração SQL ao modificar seu esquema do Supabase. Isso ajuda a rastrear alterações no banco de dados no controle de versão, embora esses arquivos não sejam usados para contexto do chat, que utiliza o esquema ativo.",
"disconnectedAll": "Todas as organizações do Supabase foram desconectadas com sucesso",
"failedDisconnect": "Falha ao desconectar do Supabase",
"orgDisconnected": "Organização desconectada com sucesso",
"settingUpdated": "Configuração atualizada",
"project": "Projeto Supabase",
"connectedToProject": "Este app está conectado ao projeto:",
"databaseBranch": "Branch do Banco de Dados",
"selectBranch": "Selecione uma branch",
"disconnectProject": "Desconectar Projeto",
"projectConnected": "Projeto conectado ao app com sucesso",
"failedConnectProject": "Falha ao conectar projeto ao app: {{error}}",
"projects": "Projetos Supabase",
"selectProjectDescription": "Selecione um projeto Supabase para conectar a este app",
"refreshProjects": "Atualizar projetos",
"addOrganization": "Adicionar Organização",
"errorLoadingProjects": "Erro ao carregar projetos: {{message}}",
"connectedOrganizations": "Organizações Conectadas",
"noProjectsFound": "Nenhum projeto encontrado nas suas organizações Supabase conectadas.",
"selectAProject": "Selecione um projeto",
"projectNotFound": "Projeto não encontrado",
"branchNotFound": "Branch não encontrada",
"branchSelected": "Branch selecionada",
"failedSetBranch": "Falha ao definir branch: {{error}}",
"failedDisconnectProject": "Falha ao desconectar projeto do app"
},
"neon": {
"title": "Integração com Neon",
"connected": "Sua conta está conectada ao Neon.",
"database": "Banco de Dados Neon",
"connectedToNeon": "Você está conectado ao Banco de Dados Neon",
"freeTier": "O Neon Database tem um bom plano gratuito com backups e até 10 projetos.",
"connectTo": "Conectar ao",
"connectedSuccess": "Conectado ao Neon com sucesso!",
"disconnect": "Desconectar do Neon",
"disconnected": "Desconectado do Neon com sucesso",
"failedDisconnect": "Falha ao desconectar do Neon"
}
}
}
{
"title": "Configurações",
"general": {
"title": "Configurações Gerais",
"language": "Idioma",
"languageDescription": "Escolha seu idioma de exibição preferido.",
"theme": "Tema",
"themeSystem": "Sistema",
"themeLight": "Claro",
"themeDark": "Escuro",
"zoom": "Nível de zoom",
"zoomDescription": "Ajusta o nível de zoom para facilitar a leitura do conteúdo.",
"selectZoom": "Selecionar nível de zoom",
"appVersion": "Versão do App:",
"autoUpdate": "Atualização automática",
"autoUpdateDescription": "Atualiza automaticamente o app quando novas versões estiverem disponíveis.",
"releaseChannel": "Canal de lançamento",
"releaseChannelDescription": "Controla qual canal de atualização o app utiliza.",
"stable": "Estável",
"beta": "Beta",
"selectReleaseChannel": "Selecionar canal de lançamento",
"runtimeMode": "Modo de Execução",
"runtimeModeDescription": "Selecione o runtime a ser usado para executar o app.",
"selectRuntimeMode": "Selecionar modo de execução",
"nodePath": "Configuração do Caminho do Node.js",
"browseForNode": "Procurar Node.js",
"selecting": "Selecionando...",
"resetToDefault": "Restaurar padrão",
"customPath": "Caminho personalizado:",
"systemPath": "PATH do sistema:",
"notFound": "Não encontrado",
"nodeConfigured": "O Node.js está configurado corretamente e pronto para uso.",
"nodeSelectFolder": "Selecione a pasta onde o Node.js está instalado, caso não esteja no PATH do sistema."
},
"workflow": {
"title": "Configurações de Fluxo de Trabalho",
"defaultChatMode": "Modo de Chat Padrão",
"defaultChatModeDescription": "Controla o modo de chat padrão ao abrir um novo chat.",
"selectDefaultChatMode": "Selecionar modo de chat padrão",
"autoApprove": "Aprovar alterações automaticamente",
"autoApproveDescription": "Aprova automaticamente as alterações de código e as executa.",
"autoFixProblems": "Corrigir problemas automaticamente",
"autoFixProblemsDescription": "Corrige automaticamente erros de TypeScript."
},
"ai": {
"title": "Configurações de IA",
"thinkingBudget": "Orçamento de Raciocínio",
"thinkingBudgetDescription": "Controla quanto tempo o modelo de IA pode raciocinar. Orçamentos maiores significam raciocínio mais aprofundado.",
"selectThinkingBudget": "Selecionar orçamento de raciocínio",
"low": "Baixo",
"medium": "Médio",
"high": "Alto",
"maxChatTurns": "Máximo de Turnos de Chat no Contexto",
"maxChatTurnsDescription": "Limita quantos turnos recentes de chat são incluídos no contexto.",
"selectMaxChatTurns": "Selecionar máximo de turnos",
"unlimited": "Ilimitado",
"turnCount_one": "{{count}} turno",
"turnCount_other": "{{count}} turnos",
"providers": "Provedores de IA",
"failedToLoadProviders": "Falha ao carregar provedores de IA: {{message}}",
"freeTierAvailable": "Plano gratuito disponível",
"addCustomProvider": "Adicionar provedor personalizado",
"connectCustomEndpoint": "Conectar a um endpoint de API LLM personalizado",
"editProvider": "Editar Provedor",
"deleteProvider": "Excluir Provedor",
"deleteCustomProvider": "Excluir Provedor Personalizado",
"deleteProviderConfirmation": "Isso excluirá permanentemente este provedor personalizado e todos os seus modelos associados. Esta ação não pode ser desfeita.",
"deleteProviderAction": "Excluir Provedor",
"configureProvider": "Configurar {{name}}",
"setupComplete": "Configuração Concluída",
"notSetup": "Não Configurado",
"createApiKey": "Crie sua chave de API com {{name}}",
"manageApiKeys": "Gerenciar Chaves de API",
"setupApiKey": "Configurar Chave de API",
"providerNotFound": "Provedor Não Encontrado",
"providerNotFoundDescription": "O provedor com ID \"{{provider}}\" não foi encontrado.",
"errorLoadingProvider": "Erro ao Carregar Detalhes do Provedor",
"errorLoadingSettings": "Erro ao Carregar Configurações",
"couldNotLoadProvider": "Não foi possível carregar os dados do provedor: {{message}}",
"couldNotLoadSettings": "Não foi possível carregar os dados de configuração: {{message}}",
"enableDyadPro": "Ativar Dyad Pro",
"toggleDyadPro": "Alternar para ativar o Dyad Pro",
"apiKeyEmpty": "A Chave de API não pode estar vazia.",
"errorTogglingPro": "Erro ao alternar Dyad Pro: {{error}}",
"failedSaveApiKey": "Falha ao salvar a chave de API.",
"failedDeleteApiKey": "Falha ao excluir a chave de API."
},
"telemetry": {
"title": "Telemetria",
"description": "Registra dados de uso anônimos para melhorar o produto.",
"telemetryId": "ID de Telemetria:",
"enable": "Ativar Telemetria",
"privacyNotice": "Usamos dados de telemetria anônimos para melhorar o produto. Nenhum dado pessoal é coletado.",
"acceptAndContinue": "Aceitar e Continuar"
},
"integrations": {
"title": "Integrações"
},
"agentPermissions": {
"title": "Permissões do Agente (Pro)",
"description": "Configure as permissões para as ferramentas integradas do Agente.",
"defaultAllowedTools": "Ferramentas permitidas por padrão ({{count}})",
"ask": "Perguntar",
"alwaysAllow": "Sempre permitir",
"neverAllow": "Nunca permitir"
},
"toolsMcp": {
"title": "Ferramentas (MCP)",
"name": "Nome",
"namePlaceholder": "Meu Servidor MCP",
"transport": "Transporte",
"stdio": "stdio",
"http": "http",
"command": "Comando",
"commandPlaceholder": "node",
"args": "Argumentos",
"argsPlaceholder": "caminho/para/servidor-mcp.js --flag",
"url": "URL",
"urlPlaceholder": "http://localhost:3000",
"addServer": "Adicionar Servidor",
"environmentVariables": "Variáveis de Ambiente",
"key": "Chave",
"keyPlaceholder": "ex.: PATH",
"value": "Valor",
"valuePlaceholder": "ex.: /usr/local/bin",
"addEnvVar": "Adicionar Variável de Ambiente",
"noEnvVars": "Nenhuma variável de ambiente configurada",
"keyValueRequired": "A chave e o valor são obrigatórios",
"duplicateKey": "Já existe uma variável de ambiente com esta chave",
"noToolsDiscovered": "Nenhuma ferramenta encontrada.",
"noServersConfigured": "Nenhum servidor configurado ainda.",
"prefilledServer": "Servidor MCP {{name}} pré-preenchido",
"deny": "Negar"
},
"experiments": {
"title": "Experimentos",
"enableNativeGit": "Ativar Git Nativo",
"enableNativeGitDescription": "Não requer nenhuma instalação externa do Git e oferece uma experiência de desempenho mais rápida com Git nativo."
},
"dangerZone": {
"title": "Zona de Perigo",
"resetEverything": "Redefinir Tudo",
"resetDescription": "Isso excluirá todos os seus apps, chats e configurações. Esta ação não pode ser desfeita.",
"resetConfirmation": "Tem certeza de que deseja redefinir tudo? Isso excluirá todos os seus apps, chats e configurações. Esta ação não pode ser desfeita.",
"resetting": "Redefinindo...",
"resetSuccess": "Tudo foi redefinido com sucesso. Reinicie o aplicativo."
},
"apiKey": {
"fromSettings": "Chave de API das Configurações",
"currentKey": "Chave Atual (Configurações)",
"thisKeyActive": "Esta chave está ativa no momento.",
"setKey": "Definir Chave de API {{name}}",
"updateKey": "Atualizar Chave de API {{name}}",
"enterKey": "Insira a nova Chave de API {{name}} aqui",
"pasteAndSave": "Colar da área de transferência e salvar",
"saveKey": "Salvar Chave",
"overrideEnvVar": "Definir uma chave aqui substituirá a variável de ambiente (se definida).",
"fromEnvVar": "Chave de API da Variável de Ambiente",
"envVarKey": "Chave da Variável de Ambiente ({{name}})",
"envVarActive": "Esta chave está ativa no momento (nenhuma chave definida nas configurações).",
"envVarOverridden": "Esta chave está sendo substituída pela chave definida nas Configurações.",
"envVarNotSet": "Variável de Ambiente Não Definida",
"envVarNotSetDescription": "A variável de ambiente {{name}} não está definida.",
"envVarHelpText": "Esta chave é definida fora do aplicativo. Se presente, ela será usada apenas se nenhuma chave estiver configurada na seção de Configurações acima. Requer reinicialização do app para detectar alterações."
},
"azure": {
"configured": "Azure OpenAI Configurado",
"configuredDescription": "O Dyad usará as credenciais salvas nas Configurações para os modelos Azure OpenAI.",
"usingEnvVars": "Usando Variáveis de Ambiente",
"usingEnvVarsDescription": "AZURE_API_KEY e AZURE_RESOURCE_NAME estão definidas. Os valores salvos abaixo as substituirão.",
"configRequired": "Configuração do Azure OpenAI Necessária",
"configRequiredDescription": "Forneça o nome do recurso Azure e a chave de API abaixo, ou configure as variáveis de ambiente AZURE_API_KEY e AZURE_RESOURCE_NAME.",
"resourceName": "Nome do Recurso",
"resourceNamePlaceholder": "seu-recurso-azure-openai",
"apiKey": "Chave de API",
"apiKeyPlaceholder": "Insira sua chave de API do Azure OpenAI",
"saveSettings": "Salvar Configurações",
"saved": "Salvo",
"configNeeded": "Configuração Necessária",
"configNeededDescription": "As requisições do Azure OpenAI exigem um nome de recurso e uma chave de API. Insira-os acima ou forneça as variáveis de ambiente.",
"saveError": "Erro ao Salvar",
"envVarsOptional": "Variáveis de Ambiente (opcional)",
"envVarsHelpText": "Você pode continuar configurando o Azure via variáveis de ambiente. Se ambas as variáveis estiverem presentes e nenhuma configuração estiver salva, o Dyad as usará automaticamente.",
"envVarsPrecedence": "Os valores salvos nas Configurações têm prioridade sobre as variáveis de ambiente. Reinicie o Dyad após alterar as variáveis de ambiente."
},
"vertex": {
"projectId": "ID do Projeto",
"projectIdPlaceholder": "seu-projeto-gcp-id",
"location": "Localização",
"locationPlaceholder": "us-central1",
"locationHelp": "Se aparecer um erro de \"modelo não encontrado\", tente uma região diferente. Alguns modelos de parceiros (MaaS) estão disponíveis apenas em localizações específicas (ex.: us-central1, us-west2).",
"serviceAccountKey": "Chave JSON da Conta de Serviço",
"serviceAccountKeyPlaceholder": "Cole o conteúdo JSON completo da chave da sua conta de serviço aqui",
"saveSettings": "Salvar Configurações",
"saved": "Salvo",
"configRequired": "Configuração Necessária",
"configRequiredDescription": "Forneça o Projeto, a Localização e uma chave JSON de conta de serviço com acesso ao Vertex AI.",
"saveError": "Erro ao Salvar",
"invalidJson": "O JSON da conta de serviço é inválido: {{message}}"
},
"models": {
"title": "Modelos",
"description": "Gerencie modelos específicos disponíveis por meio deste provedor.",
"errorLoading": "Erro ao Carregar Modelos",
"noCustomModels": "Nenhum modelo personalizado foi adicionado a este provedor ainda.",
"addCustomModel": "Adicionar Modelo Personalizado",
"deleteConfirmTitle": "Tem certeza de que deseja excluir este modelo?",
"deleteConfirmDescription": "Esta ação não pode ser desfeita. O modelo personalizado \"{{name}}\" (Nome da API: {{apiName}}) será excluído permanentemente.",
"yesDeleteIt": "Sim, excluir",
"contextTokens": "Contexto: {{count}} tokens",
"maxOutputTokens": "Saída Máxima: {{count}} tokens",
"modelIdRequired": "O nome da API do modelo é obrigatório",
"modelNameRequired": "O nome de exibição do modelo é obrigatório",
"invalidMaxOutput": "Máximo de Tokens de Saída deve ser um número válido",
"invalidContextWindow": "A Janela de Contexto deve ser um número válido",
"addModelTitle": "Adicionar Modelo Personalizado",
"addModelDescription": "Configure um novo modelo de linguagem para o provedor selecionado.",
"editModelTitle": "Editar Modelo Personalizado",
"editModelDescription": "Modifique a configuração do modelo de linguagem selecionado.",
"modelId": "ID do Modelo*",
"modelIdHelp": "Deve corresponder ao modelo esperado pela API",
"modelName": "Nome*",
"modelNameHelp": "Nome amigável para o modelo",
"modelDescription": "Descrição",
"modelDescriptionHelp": "Opcional: Descreva as capacidades do modelo",
"maxOutputTokensLabel": "Máximo de Tokens de Saída",
"maxOutputTokensHelp": "Opcional: ex.: 4096",
"contextWindowLabel": "Janela de Contexto",
"contextWindowHelp": "Opcional: ex.: 8192",
"adding": "Adicionando...",
"addModel": "Adicionar Modelo",
"updateModel": "Atualizar Modelo",
"modelCreated": "Modelo personalizado criado com sucesso!",
"modelUpdated": "Modelo personalizado atualizado com sucesso!",
"failedUpdateSettings": "Falha ao atualizar configurações"
},
"customProvider": {
"addTitle": "Adicionar Provedor Personalizado",
"editTitle": "Editar Provedor Personalizado",
"addDescription": "Conecte-se a uma API de provedor de modelo de linguagem personalizado.",
"editDescription": "Atualize a configuração do seu provedor de modelo de linguagem personalizado.",
"providerId": "ID do Provedor",
"providerIdPlaceholder": "Ex.: meu-provedor",
"providerIdHelp": "Um identificador único para este provedor (sem espaços).",
"displayName": "Nome de Exibição",
"displayNamePlaceholder": "Ex.: Meu Provedor",
"displayNameHelp": "O nome que será exibido na interface.",
"apiBaseUrl": "URL Base da API",
"apiBaseUrlPlaceholder": "Ex.: https://api.example.com/v1",
"apiBaseUrlHelp": "A URL base para o endpoint da API.",
"envVar": "Variável de Ambiente (Opcional)",
"envVarPlaceholder": "Ex.: MINHA_CHAVE_API_PROVEDOR",
"envVarHelp": "Nome da variável de ambiente para a chave de API.",
"addProvider": "Adicionar Provedor",
"updateProvider": "Atualizar Provedor",
"failedCreate": "Falha ao criar provedor personalizado"
},
"manageDyadPro": "Gerenciar Assinatura Dyad Pro",
"setupDyadPro": "Configurar Assinatura Dyad Pro"
}
{
"newChat": "新建聊天",
"recentChats": "最近的聊天",
"searchChats": "搜索聊天",
"loadingChats": "正在加载聊天...",
"noChatsFound": "未找到聊天",
"renameChat": "重命名聊天",
"deleteChat": "删除聊天",
"renameChatDescription": "为此聊天输入新名称。",
"chatTitle": "标题",
"enterChatTitle": "输入聊天标题...",
"chatRenamed": "聊天重命名成功",
"failedRenameChat": "重命名聊天失败:{{error}}",
"failedCreateChat": "创建新聊天失败:{{error}}",
"chatDeleted": "聊天已删除",
"failedDeleteChat": "删除聊天失败:{{error}}",
"deleteChatConfirmation": "确定要删除「{{title}}」吗?此操作无法撤销,聊天中的所有消息将永久丢失。",
"deleteChatNote": "注意:已接受的代码更改将被保留。",
"scrollToBottom": "滚动到底部",
"dismissError": "忽略错误",
"askDyadToBuild": "让 Dyad 来构建...",
"cancelGeneration": "取消生成",
"sendMessage": "发送消息",
"loadingProposal": "正在加载提案...",
"errorLoadingProposal": "加载提案出错:{{message}}",
"visualEditor": "可视化编辑器 (Pro)",
"visualEditorDescription": "可视化编辑让您无需 AI 即可进行 UI 更改,这是 Pro 专属功能",
"summarizeNewChatTip": "创建新聊天可以让 AI 更加专注和高效",
"summarizeToNewChat": "总结到新聊天",
"refactorFile": "重构 {{path}} 并使其更模块化",
"refactorDescription": "重构文件以提高可维护性",
"writeCodeProperly": "正确编写代码",
"writeCodeProperlyDescription": "正确编写代码(当 AI 以错误格式生成代码时有用)",
"rebuildApp": "重新构建应用",
"rebuildAppDescription": "重新构建应用程序",
"restartApp": "重启应用",
"restartAppDescription": "重启开发服务器",
"refreshApp": "刷新应用",
"refreshAppDescription": "刷新应用预览",
"keepGoing": "继续",
"tipProposal": "提案提示",
"securityRisks": "安全风险",
"sqlQueries": "SQL 查询",
"packagesAdded": "新增的包",
"serverFunctionsChanged": "已更改的服务器函数",
"filesChanged": "已更改的文件",
"securityRisksFound": "发现安全风险",
"approve": "批准",
"reject": "拒绝",
"noChanges": "无更改",
"sqlQuery": "SQL 查询",
"approved": "已批准",
"rejected": "已拒绝",
"copy": "复制",
"requestId": "请求 ID",
"copyRequestId": "复制请求 ID",
"maxTokensUsed": "已用最大令牌数:{{count}}",
"allowToolToRun": "允许 {{toolName}} 运行?",
"queueCount": "(第 1 个,共 {{total}} 个)",
"allowOnce": "允许一次",
"alwaysAllow": "始终允许",
"removeAttachment": "移除附件",
"recentChatActivity": "最近的聊天活动",
"loadingActivity": "正在加载活动...",
"noRecentChats": "没有最近的聊天",
"uncommittedChanges": "您有 {{count}} 个未提交的更改。",
"reviewAndCommit": "审查并提交",
"reviewCommitChanges": "审查并提交更改",
"reviewChangesDescription": "审查您的更改并输入提交信息。",
"commitMessage": "提交信息",
"enterCommitMessage": "输入提交信息...",
"changedFiles": "已更改的文件 ({{count}})",
"committing": "正在提交...",
"commit": "提交",
"added": "已添加",
"modified": "已修改",
"deleted": "已删除",
"renamed": "已重命名",
"updateFiles": "更新文件",
"closeVersionPane": "关闭版本面板",
"versionHistory": "版本历史",
"noVersionsAvailable": "没有可用的版本",
"versionLabel": "版本 {{number}} ({{hash}})",
"dbSnapshot": "数据库",
"dbSnapshotExpired": "数据库快照可能已过期(超过 24 小时)",
"dbSnapshotAvailable": "数据库快照可用,时间戳 {{timestamp}}",
"restoreToVersion": "恢复到此版本",
"restoring": "正在恢复...",
"restoringToVersion": "正在恢复到此版本...",
"revertedToVersion": "已将所有更改回退到版本 {{version}}",
"revertedToHash": "已将所有更改回退到版本 {{hash}}",
"contextUsed": "已使用:",
"contextLimit": "限制:",
"contextLimitWarning": "您已接近此聊天的上下文限制。",
"summarizeIntoNewChat": "总结到新聊天",
"fixAllErrors": "修复所有错误 ({{count}})",
"todosCompleted": "{{completed}}/{{total}} 个待办事项已完成",
"allTasksCompleted": "所有任务已完成",
"noTaskInProgress": "没有正在进行的任务",
"tokenUsage": "令牌数:{{count}}",
"tokenPercentUsed": "已使用 {{percent}}%(上限 {{limit}}K)",
"tokenUsageBreakdown": "令牌使用详情",
"messageHistory": "消息历史",
"codebase": "代码库",
"mentionedApps": "提及的应用",
"systemPrompt": "系统提示词",
"currentInput": "当前输入",
"total": "总计",
"failedCountTokens": "令牌计数失败",
"optimizeTokens": "通过以下方式优化您的令牌",
"dyadProSmartContext": "Dyad Pro 的智能上下文",
"attachFiles": "附加文件",
"themes": "主题",
"noTheme": "无主题",
"moreThemes": "更多主题",
"newTheme": "新建主题",
"allCustomThemes": "所有自定义主题",
"showTokenUsage": "显示令牌使用情况",
"hideTokenUsage": "隐藏令牌使用情况",
"attachFileContext": "附加文件作为聊天上下文",
"attachFileContextExample": "示例用途:应用截图以指出 UI 问题",
"uploadFileCodebase": "上传文件到代码库",
"uploadFileCodebaseExample": "示例用途:添加图片供应用使用",
"dropFilesToAttach": "拖放文件以附加",
"selectedComponents": "已选择的组件 ({{count}})",
"clearAllComponents": "清除所有已选择的组件",
"deselectComponent": "取消选择组件",
"chatMode": {
"openMenu": "打开模式菜单",
"toggleShortcut": "{{shortcut}} 切换",
"agentV2": "Agent v2",
"agentV2Description": "更擅长处理大型任务和调试",
"build": "构建",
"buildDescription": "生成和编辑代码",
"ask": "提问",
"askDescription": "关于应用的提问",
"buildWithMcp": "使用 MCP 构建",
"buildWithMcpDescription": "类似构建模式,但可以使用工具 (MCP) 来生成代码"
},
"modelPicker": {
"modelLabel": "模型:",
"cloudModels": "云端模型",
"loadingModels": "正在加载模型...",
"noCloudModels": "没有可用的云端模型",
"otherProviders": "其他 AI 提供商",
"providerCount_one": "{{count}} 个提供商",
"providerCount_other": "{{count}} 个提供商",
"dyadTurbo": "Dyad Turbo",
"modelCount_one": "{{count}} 个模型",
"modelCount_other": "{{count}} 个模型",
"providerModels": "{{name}} 模型",
"localModels": "本地模型",
"localModelsDescription": "LM Studio、Ollama",
"ollama": "Ollama",
"ollamaModels": "Ollama 模型",
"errorLoading": "加载出错",
"noneAvailable": "没有可用的",
"isOllamaRunning": "Ollama 正在运行吗?",
"noLocalModels": "未找到本地模型",
"ensureOllamaRunning": "确保 Ollama 正在运行且模型已拉取。",
"lmStudio": "LM Studio",
"lmStudioModels": "LM Studio 模型",
"noLoadedModels": "未找到已加载的模型",
"ensureLMStudioRunning": "确保 LM Studio 正在运行且模型已加载。"
},
"header": {
"switchingToLatest": "请稍候,正在切换到最新版本...",
"warningNotOnBranch": "警告:您不在任何分支上",
"notOnBranch": "您不在任何分支上",
"checkoutInProgress": "版本检出正在进行中",
"checkoutMainBranch": "检出 main 分支,否则更改将无法正确保存",
"checkingBranch": "正在检查分支...",
"onBranch": "您当前在分支:{{name}}。",
"renameMasterToMain": "将 master 重命名为 main",
"renaming": "正在重命名...",
"switchToMainBranch": "切换到 main 分支",
"checkingOut": "正在检出...",
"masterRenamed": "master 分支已重命名为 main",
"versionCount": "版本 {{count}}"
},
"errorBox": {
"accessWithDyadPro": "使用 Dyad Pro 访问",
"orSwitchModel": "或切换到其他模型。",
"upgradeToDyadPro": "升级到 Dyad Pro",
"troubleshootingGuide": "故障排除指南",
"invalidProKey": "您似乎没有有效的 Dyad Pro 密钥。",
"today": "今天。",
"creditsUsed": "您本月的 Dyad AI 额度已用完。",
"reloadOrUpgrade": "重新加载或升级您的订阅",
"getMoreCredits": "获取更多 AI 额度",
"readDocs": "阅读文档"
},
"promo": {
"tiredOfWaiting": "厌倦了等待 AI?",
"getDyadPro": "获取 Dyad Pro",
"fasterEdits": "使用 Turbo Edits 实现更快的编辑。",
"saveOnCosts": "使用以下功能节省高达 3 倍的 AI 成本",
"debuggingLoop": "陷入调试循环?尝试换一个模型。",
"joinBuilders": "加入 600 多名开发者,访问",
"dyadSubreddit": "Dyad 论坛",
"foundBug": "发现了 Bug?点击「帮助」>「报告 Bug」",
"reportBadResponse": "想要报告不良的 AI 回复?点击「帮助」上传聊天",
"watch": "观看",
"creatorBuildApp": "Dyad 创建者一步步构建圣经应用",
"gettingStuck": "遇到困难?阅读我们的",
"debuggingTips": "调试技巧",
"advancedTip": "高级技巧:自定义您的",
"aiRules": "AI 规则",
"keepFocused": "想让 AI 更专注?开始新聊天。",
"whatsNext": "想知道接下来有什么?查看我们的",
"roadmap": "路线图",
"likeDyad": "喜欢 Dyad?在 GitHub 上给个 Star",
"gitHub": "GitHub"
},
"consent": {
"toolWantsToRun": "工具请求运行",
"requestsConsent": "请求您的同意。",
"inputRequired": "需要输入"
},
"agentModeActivated": "Agent 模式已激活",
"agentModeTip": "提示:创建新聊天以给 Agent 一个干净的上下文,获得更好的结果。",
"neverShowAgain": "不再显示"
}
{
"save": "保存",
"cancel": "取消",
"delete": "删除",
"confirm": "确认",
"loading": "加载中...",
"copyToClipboard": "复制到剪贴板",
"copied": "已复制!",
"close": "关闭",
"back": "返回",
"goBack": "返回",
"reset": "重置",
"error": "错误",
"success": "成功",
"warning": "警告",
"info": "提示",
"yes": "是",
"no": "否",
"retry": "重试",
"ok": "确定",
"create": "创建",
"update": "更新",
"edit": "编辑",
"add": "添加",
"remove": "移除",
"search": "搜索",
"refresh": "刷新",
"enabled": "已启用",
"disabled": "已禁用",
"saving": "保存中...",
"deleting": "删除中...",
"creating": "创建中...",
"updating": "更新中...",
"connecting": "连接中...",
"disconnecting": "断开连接中...",
"importing": "导入中...",
"uploading": "上传中...",
"preparing": "准备中...",
"checking": "检查中...",
"notSet": "未设置",
"set": "已设置",
"custom": "自定义",
"ready": "就绪",
"needsSetup": "需要设置",
"accept": "接受",
"decline": "拒绝",
"showMore": "显示更多",
"showLess": "收起",
"selectAll": "全选",
"clearAll": "清除全部",
"noResults": "未找到结果",
"itemCount_one": "{{count}} 个项目",
"itemCount_other": "{{count}} 个项目",
"pro": "Pro",
"free": "免费",
"recommended": "推荐",
"experimental": "实验性",
"new": "新",
"builtIn": "内置",
"advanced": "高级",
"required": "必填",
"optional": "可选"
}
{
"unknown": "发生了未知错误",
"failedToCreate": "创建失败:{{error}}",
"failedToUpdate": "更新失败:{{error}}",
"failedToDelete": "删除失败:{{error}}",
"failedToLoad": "加载失败:{{error}}",
"networkError": "发生网络错误,请检查您的网络连接。",
"copyError": "复制错误信息",
"errorOccurred": "发生了一个错误"
}
{
"buildingApp": "正在构建您的应用",
"settingUp": "我们正在用 AI 魔法为您设置应用。",
"mightTakeMoment": "这可能需要一些时间...",
"moreIdeas": "更多创意",
"buildMeA": "帮我构建一个{{label}}",
"failedCreateApp": "创建应用失败。{{error}}",
"whatsNew": "v{{version}} 有什么新功能?",
"releaseNotesTitle": "v{{version}} 发行说明",
"importApp": "导入应用",
"importAppDescription": "从本地文件夹导入现有应用或从 GitHub 克隆。",
"importExperimental": "应用导入是一项实验性功能。如果遇到任何问题,请使用「帮助」按钮报告。",
"localFolder": "本地文件夹",
"githubRepos": "GitHub 仓库",
"yourGithubRepos": "您的 GitHub 仓库",
"githubUrl": "GitHub URL",
"selectFolder": "选择文件夹",
"selectingFolder": "正在选择文件夹...",
"selectedFolder": "已选择文件夹:",
"clearSelection": "清除选择",
"copyToDyadApps": "复制到 dyad-apps 文件夹",
"appNameExists": "此名称的应用已存在。请选择其他名称:",
"appName": "应用名称",
"appNameOptional": "应用名称(可选)",
"enterNewAppName": "输入新的应用名称",
"leaveEmptyForRepo": "留空则使用仓库名称",
"advancedOptions": "高级选项",
"installCommand": "安装命令",
"startCommand": "启动命令",
"bothCommandsRequired": "自定义时两个命令都是必填的。",
"noAiRulesFound": "未找到 AI_RULES.md。导入后 Dyad 将自动生成一个。",
"importingApp": "正在导入应用...",
"import": "导入",
"noRepositoriesFound": "未找到仓库",
"repositoryUrl": "仓库 URL",
"repositoryUrlPlaceholder": "https://github.com/user/repo.git",
"appImportedWithRules": "应用导入成功。Dyad 将自动生成 AI_RULES.md。",
"appImported": "应用导入成功",
"successfullyImported": "成功导入 {{name}}",
"failedFetchRepos": "获取仓库失败。",
"failedCheckAppName": "检查应用名称失败:{{error}}",
"failedImportRepo": "导入仓库失败:{{error}}",
"createNewApp": "创建新应用",
"createAppUsingTemplate": "使用 {{template}} 模板创建新应用。",
"enterAppName": "输入应用名称...",
"appNameAlreadyExists": "此名称的应用已存在",
"createApp": "创建应用",
"forceCloseDetected": "检测到强制关闭",
"forceCloseDescription": "上次运行时应用未正常关闭。这可能表示崩溃或意外终止。",
"lastKnownState": "最后已知状态:",
"processMetrics": "进程指标",
"memory": "内存:",
"cpu": "CPU:",
"systemMetrics": "系统指标",
"communityCodeNotice": "社区代码通知",
"communityCodeWarning": "此代码由 Dyad 社区成员创建,并非我们的核心团队。",
"communityCodeRisk": "社区代码可能非常有用,但由于是独立构建的,可能存在缺陷、安全风险或导致系统问题。如果出现问题,我们无法提供官方支持。",
"communityCodeReview": "我们建议先在 GitHub 上查看代码。只有在您了解这些风险后再继续。",
"deleteItemTitle": "删除{{itemType}}",
"deleteItemConfirmation": "确定要删除「{{itemName}}」吗?此操作无法撤销。",
"proBanner": {
"manageDyadPro": "管理 Dyad Pro",
"alreadyHavePro": "已有 Dyad Pro?添加您的密钥",
"accessLeadingModels": "一个计划即可访问领先的 AI 模型",
"getDyadPro": "获取 Dyad Pro",
"upTo3xCheaper": "便宜高达 3 倍",
"byUsingSmartContext": "通过使用智能上下文",
"generateCode4x": "代码生成速度提升 4-10 倍",
"withTurboModels": "使用 Turbo 模型和 Turbo Edits"
},
"setup": {
"buildNewApp": "构建新应用",
"setupDyad": "设置 Dyad",
"installNodeJs": "1. 安装 Node.js(应用运行时)",
"errorCheckingNode": "检查 Node.js 状态出错。请尝试安装 Node.js。",
"nodeInstalled": "Node.js ({{version}}) 已安装。",
"pnpmInstalled": "(可选)pnpm ({{version}}) 已安装。",
"nodeRequired": "运行本地应用需要 Node.js。",
"afterInstallNode": "安装 Node.js 后,点击「继续」。如果安装程序不起作用,请尝试更多下载选项。",
"moreDownloadOptions": "更多下载选项",
"nodeAlreadyInstalled": "Node.js 已安装?手动配置路径",
"browseForNodeFolder": "浏览 Node.js 文件夹",
"nodeTroubleshooting": "Node.js 故障排除指南",
"stillStuck": "仍然遇到问题?点击左下角的「帮助」按钮,然后点击「报告 Bug」。",
"installNodeRuntime": "安装 Node.js 运行时",
"checkingNodeSetup": "正在检查 Node.js 设置...",
"continueInstalled": "继续 | 我已安装 Node.js",
"nodeNotDetected": "未检测到 Node.js。关闭并重新打开 Dyad 通常可以解决此问题。",
"setupAiAccess": "2. 设置 AI 访问",
"notSureWatchVideo": "不确定该怎么做?观看上方的入门视频",
"setupGeminiApiKey": "设置 Google Gemini API 密钥",
"setupOpenRouterApiKey": "设置 OpenRouter API 密钥",
"setupDyadPro": "设置 Dyad Pro",
"accessAllModels": "一个计划即可访问所有 AI 模型",
"setupOtherProviders": "设置其他 AI 提供商",
"openAiAnthropicMore": "OpenAI、Anthropic 等",
"freeModelsAvailable": "有免费模型可用"
},
"customTheme": {
"createTitle": "创建自定义主题",
"createDescription": "使用手动配置或 AI 驱动生成来创建自定义主题。",
"aiPoweredGenerator": "AI 驱动生成器",
"manualConfiguration": "手动配置",
"themeName": "主题名称",
"themeNamePlaceholder": "我的自定义主题",
"descriptionOptional": "描述(可选)",
"descriptionPlaceholder": "您的主题的简要描述",
"themePrompt": "主题提示词",
"themePromptPlaceholder": "输入您的主题系统提示词...",
"saveTheme": "保存主题",
"enterThemeName": "请输入主题名称",
"enterThemePrompt": "请输入主题提示词",
"generatePromptFirst": "请先生成提示词",
"themeCreated": "自定义主题创建成功",
"failedCreateTheme": "创建主题失败:{{error}}"
},
"editTheme": {
"title": "编辑主题",
"description": "修改您的自定义主题设置和提示词。",
"themeName": "主题名称",
"themeNamePlaceholder": "主题名称",
"descriptionOptional": "描述(可选)",
"descriptionPlaceholder": "您的主题的简要描述",
"themePrompt": "主题提示词",
"themePromptPlaceholder": "输入您的主题系统提示词...",
"themeUpdated": "主题更新成功",
"failedUpdateTheme": "更新主题失败:{{error}}"
},
"prompt": {
"newPrompt": "新建提示词",
"editPrompt": "编辑提示词",
"createTitle": "创建新提示词",
"editTitle": "编辑提示词",
"createDescription": "为您的提示词库创建新的提示词模板。",
"editDescription": "编辑您的提示词模板。",
"titlePlaceholder": "标题",
"descriptionPlaceholder": "描述(可选)",
"contentPlaceholder": "内容"
},
"help": {
"needHelp": "需要 Dyad 帮助?",
"helpOptions": "如果您需要帮助或想报告问题,以下是一些选项:",
"chatWithHelpBot": "与 Dyad 帮助机器人聊天 (Pro)",
"helpBotDescription": "打开应用内帮助聊天助手,搜索 Dyad 文档。",
"openDocs": "打开文档",
"openDocsDescription": "获取常见问题的帮助。",
"reportBug": "报告 Bug",
"reportBugDescription": "我们将自动填充系统信息和日志。提交前您可以检查是否有敏感信息。",
"preparingReport": "正在准备报告...",
"uploadChatSession": "上传聊天会话",
"uploadChatDescription": "分享聊天日志和代码以进行故障排除。数据仅用于解决您的问题,并在限定时间后自动删除。",
"preparingUpload": "正在准备上传...",
"helpBotTitle": "Dyad 帮助机器人",
"askQuestion": "关于使用 Dyad 的问题。",
"conversationLogged": "此对话可能会被记录并用于改进产品。请勿在此输入敏感信息。",
"typeQuestion": "输入您的问题...",
"sending": "发送中...",
"send": "发送",
"uploadComplete": "上传完成",
"chatLogsUploaded": "聊天日志上传成功",
"mustOpenIssue": "您必须在 GitHub 上创建 Issue 才能让我们调查。没有关联的 Issue,您的报告将不会被审核。",
"openGithubIssue": "打开 GitHub Issue",
"okToUpload": "确定上传聊天会话吗?",
"reviewSubmission": "请查看将要提交的信息。您的聊天消息、系统信息和代码库快照将被包含在内。",
"chatMessages": "聊天消息",
"you": "您",
"assistant": "助手",
"codebaseSnapshot": "代码库快照",
"systemInformation": "系统信息",
"dyadVersion": "Dyad 版本:",
"platform": "平台:",
"architecture": "架构:",
"nodeVersion": "Node 版本:",
"pnpmVersion": "PNPM 版本:",
"nodePath": "Node 路径:",
"proUserId": "Pro 用户 ID:",
"telemetryId": "遥测 ID:",
"model": "模型:",
"logs": "日志",
"upload": "上传"
},
"screenshot": {
"takeScreenshot": "是否截图?",
"takeScreenshotRecommended": "截图(推荐)",
"betterResponses": "这样做可以获得更好、更快的回复!",
"fileWithoutScreenshot": "不截图直接提交 Bug 报告",
"mightNotHelp": "我们仍会尝试回复,但可能无法提供同样多的帮助。",
"failedScreenshot": "截图失败:{{error}}",
"capturedToClipboard": "截图已复制到剪贴板!请在 GitHub Issue 中粘贴。",
"createGithubIssue": "创建 GitHub Issue"
},
"preview": {
"title": "预览",
"problems": "问题",
"code": "代码",
"configure": "配置",
"security": "安全",
"publish": "发布",
"moreOptions": "更多选项",
"rebuild": "重新构建",
"rebuildDescription": "重新安装 node_modules 并重启",
"clearCache": "清除缓存",
"clearCacheDescription": "清除 Cookie、本地存储及其他应用缓存",
"loadingFiles": "正在加载文件...",
"noAppSelected": "未选择应用",
"refreshFiles": "刷新文件",
"files": "文件",
"exitFullScreen": "退出全屏",
"enterFullScreen": "进入全屏",
"selectFileToView": "选择要查看的文件",
"noFilesFound": "未找到文件",
"searchFileContents": "搜索文件内容",
"clearSearch": "清除搜索",
"searchingFiles": "正在搜索文件...",
"match_one": "{{count}} 个匹配",
"match_other": "{{count}} 个匹配",
"noFilesMatchedSearch": "没有文件匹配您的搜索。",
"saveChanges": "保存更改",
"noUnsavedChanges": "没有未保存的更改",
"fileSaved": "文件已保存",
"loadingFileContent": "正在加载文件内容...",
"noContentAvailable": "没有可用内容",
"sendToChat": "发送到聊天",
"systemMessages": "系统消息",
"consoleFilters": {
"allLevels": "所有级别",
"info": "信息",
"warn": "警告",
"error": "错误",
"allTypes": "所有类型",
"server": "服务端",
"client": "客户端",
"edgeFunction": "边缘函数",
"networkRequests": "网络请求",
"allSources": "所有来源",
"clearFilters": "清除筛选",
"clearLogs": "清除日志",
"logs": "日志"
},
"problems_panel": {
"selectProblem": "选择问题",
"noProblemsFound": "未发现问题",
"runChecks": "运行检查",
"checkingProblems": "检查中...",
"noAppSelectedTitle": "未选择应用",
"noAppSelectedDescription": "选择一个应用以查看 TypeScript 问题和诊断信息。",
"noProblemsReportTitle": "无问题报告",
"noProblemsReportDescription": "运行检查以扫描您的应用中的 TypeScript 错误和其他问题。",
"fixProblems": "修复 {{count}} 个问题",
"error_one": "{{count}} 个错误",
"error_other": "{{count}} 个错误"
},
"publish_panel": {
"noAppSelectedTitle": "未选择应用",
"noAppSelectedDescription": "选择一个应用以查看发布选项。",
"publishApp": "发布应用",
"github": "GitHub",
"githubDescription": "将代码同步到 GitHub 进行协作。",
"vercel": "Vercel",
"vercelDescription": "通过部署到 Vercel 发布您的应用。",
"githubRequiredForVercel": "部署到 Vercel 需要先连接 GitHub",
"githubRequiredDescription": "部署到 Vercel 需要先连接 GitHub。请先在上方设置您的 GitHub 仓库。"
},
"security_panel": {
"justNow": "刚刚",
"minutesAgo_one": "{{count}} 分钟前",
"minutesAgo_other": "{{count}} 分钟前",
"hoursAgo_one": "{{count}} 小时前",
"hoursAgo_other": "{{count}} 小时前",
"daysAgo_one": "{{count}} 天前",
"daysAgo_other": "{{count}} 天前",
"critical": "严重",
"high": "高",
"medium": "中",
"low": "低",
"openDocs": "打开安全审查文档",
"editSecurityRules": "编辑安全规则",
"runningReview": "正在运行安全审查...",
"runReview": "运行安全审查",
"noAppSelectedTitle": "未选择应用",
"noAppSelectedDescription": "选择一个应用以运行安全审查",
"reviewRunning": "安全审查正在运行",
"reviewRunningDescription": "结果很快就会出来。",
"noReviewFoundTitle": "未找到安全审查",
"noReviewFoundDescription": "运行安全审查以识别应用中的潜在漏洞。",
"noIssuesFoundTitle": "未发现安全问题",
"noIssuesFoundDescription": "您的应用通过了安全审查,未检测到问题。",
"lastReviewed": "上次审查",
"level": "级别",
"issue": "问题",
"action": "操作",
"selectAllIssues": "选择所有问题",
"fixIssue": "修复问题",
"fixingIssue": "正在修复问题...",
"fixIssueCount_one": "修复 {{count}} 个问题",
"fixIssueCount_other": "修复 {{count}} 个问题",
"fixingIssueCount_one": "正在修复 {{count}} 个问题...",
"fixingIssueCount_other": "正在修复 {{count}} 个问题...",
"editRulesTitle": "编辑安全规则",
"editRulesDescription": "允许您为安全审查添加关于项目的额外上下文信息。此内容保存在 SECURITY_RULES.md 文件中。这有助于发现更多问题或避免标记与您的应用无关的问题。",
"securityRulesPlaceholder": "# SECURITY_RULES.md..."
},
"visualEditing": {
"deselectComponent": "取消选择组件",
"styledDynamically": "此组件使用动态样式",
"margin": "外边距",
"horizontal": "水平",
"vertical": "垂直",
"padding": "内边距",
"border": "边框",
"width": "宽度",
"radius": "圆角",
"color": "颜色",
"backgroundColor": "背景颜色",
"background": "背景",
"textStyle": "文字样式",
"fontSize": "字号",
"fontWeight": "字重",
"fontFamily": "字体",
"textColor": "文字颜色"
},
"annotator": {
"select": "选择",
"draw": "绘画",
"text": "文字",
"color": "颜色",
"deleteSelected": "删除选中",
"undo": "撤销",
"redo": "重做",
"addToChat": "添加到聊天",
"closeAnnotator": "关闭标注器",
"proFeatureTitle": "标注器是 Pro 功能",
"proFeatureDescription": "解锁截图标注功能,使用 Dyad Pro 提升您的工作流程。",
"getDyadPro": "获取 Dyad Pro"
}
},
"integrations": {
"github": {
"title": "GitHub 集成",
"connected": "您的账号已连接到 GitHub。",
"disconnect": "断开与 GitHub 的连接",
"disconnected": "已成功断开与 GitHub 的连接",
"failedDisconnect": "断开与 GitHub 的连接失败",
"errorDisconnect": "断开 GitHub 连接时发生错误",
"connectedToRepo": "已连接到 GitHub 仓库:",
"syncToGithub": "同步到 GitHub",
"syncing": "正在同步...",
"disconnectFromRepo": "断开与仓库的连接",
"seeTroubleshooting": "查看故障排除指南",
"rebaseInProgress": "变基操作正在进行中。请选择如何继续。",
"abortRebase": "中止变基",
"aborting": "正在中止...",
"continueRebase": "继续变基",
"continuing": "正在继续...",
"safeForcesPush": "安全强制推送",
"safeForcePushing": "正在安全强制推送...",
"rebaseAndSync": "变基并同步",
"conflictsDetected": "仓库中存在冲突。请在编辑器中解决。",
"pushedSuccess": "已成功推送到 GitHub!",
"forcePushWarning": "强制推送警告",
"forcePushDescription": "您即将对 GitHub 仓库执行强制推送。",
"dangerousNonReversible": "这是危险且不可逆的操作,将会:",
"overwriteRemote": "覆盖远程仓库的历史记录",
"deleteRemoteCommits": "永久删除远程存在但本地不存在的提交",
"onlyProceedCertain": "仅在您确定这是您想要的操作时才继续。",
"forcePush": "强制推送",
"forcePushing": "正在强制推送...",
"connectToGithub": "连接到 GitHub",
"githubConnection": "GitHub 连接",
"goTo": "1. 前往:",
"enterCode": "2. 输入代码:",
"setupGithubRepo": "设置您的 GitHub 仓库",
"createNewRepo": "创建新仓库",
"connectExistingRepo": "连接到现有仓库",
"repositoryName": "仓库名称",
"checkingAvailability": "正在检查可用性...",
"repoAvailable": "仓库名称可用!",
"selectRepository": "选择仓库",
"loadingRepositories": "正在加载仓库...",
"selectARepository": "选择一个仓库",
"branch": "分支",
"loadingBranches": "正在加载分支...",
"selectABranch": "选择一个分支",
"headCurrent": "HEAD(当前)",
"typeCustomBranch": "输入自定义分支名称",
"enterBranchName": "输入分支名称(例如 feature/new-feature)",
"createRepo": "创建仓库",
"connectToRepo": "连接到仓库",
"repoCreatedLinked": "仓库已创建并关联!",
"connectedToRepo2": "已连接到仓库!",
"rebaseAborted": "变基已中止。您可以重新尝试同步。",
"rebaseContinued": "变基已继续。您可以在准备好后进行同步。",
"mergeConflicts": "检测到合并冲突。请在编辑器中解决。",
"failedAbortRebase": "中止变基失败。",
"failedContinueRebase": "继续变基失败。",
"failedSync": "同步到 GitHub 失败。",
"failedDisconnectRepo": "断开仓库连接失败。"
},
"githubBranch": {
"selectBranch": "选择分支",
"branchLabel": "分支:",
"refreshBranches": "刷新分支",
"createNewBranch": "创建新分支",
"createBranchTitle": "创建新分支",
"createBranchDescription": "创建一个新分支。",
"branchName": "分支名称",
"branchNamePlaceholder": "feature/my-new-feature",
"sourceBranch": "源分支",
"sourceBranchPlaceholder": "选择来源(可选,默认为 HEAD)",
"createBranch": "创建分支",
"renameBranch": "重命名分支",
"renameBranchDescription": "为分支 '{{name}}' 输入新名称。",
"newName": "新名称",
"rename": "重命名",
"mergeBranch": "合并分支",
"mergeBranchConfirmation": "确定要将 '{{source}}' 合并到 '{{target}}' 吗?",
"merge": "合并",
"merging": "正在合并...",
"deleteBranch": "删除分支",
"deleteBranchConfirmation": "这将永久删除分支 '{{name}}'。此操作无法撤销。",
"mergeInProgress": "合并进行中",
"rebaseInProgress": "变基进行中",
"abortAction": "此操作将中止当前操作",
"operationInProgress": "{{type}} 操作正在进行中...",
"unresolvedConflicts": "检测到未解决的冲突",
"abortWarning": "中止将丢弃您已完成的所有冲突解决工作。",
"abortConfirmation": "确定要中止 {{type}} 并切换分支吗?",
"keepWorking": "继续工作",
"abortAndSwitch": "中止 {{type}} 并切换",
"branches": "分支",
"branchesDescription": "管理您的分支,包括合并、删除等操作。",
"nativeGitRequired": "需要原生 Git",
"nativeGitDescription": "某些 Git 操作(如变基、合并中止和高级分支操作)需要启用原生 Git。",
"enableInSettings": "在设置中启用",
"mergeInto": "合并到 {{branch}}",
"branchCreated": "分支 '{{name}}' 已创建",
"switchedToBranch": "已切换到分支 '{{name}}'",
"abortedAndSwitched": "已中止 {{type}} 并切换到分支 '{{name}}'",
"branchDeleted": "分支 '{{name}}' 已删除",
"branchRenamed": "已将 '{{oldName}}' 重命名为 '{{newName}}'",
"branchMerged": "已将 '{{source}}' 合并到 '{{target}}'",
"mergeConflict": "检测到合并冲突。请在编辑器中解决。",
"failedLoadBranches": "加载分支失败",
"failedCreateBranch": "创建分支失败",
"failedSwitchBranch": "切换分支失败",
"failedDeleteBranch": "删除分支失败",
"failedRenameBranch": "重命名分支失败",
"failedMergeBranch": "合并分支失败"
},
"githubCollaborator": {
"title": "协作者",
"description": "通过 GitHub 管理谁可以访问此项目。",
"usernamePlaceholder": "GitHub 用户名",
"inviting": "正在邀请...",
"invite": "邀请",
"currentTeam": "当前团队",
"loadingCollaborators": "正在加载协作者...",
"noCollaborators": "未找到协作者。",
"admin": "管理员",
"editor": "编辑者",
"viewer": "查看者",
"removeCollaborator": "移除协作者?",
"removeConfirmation": "确定要将 {{name}} 从此项目中移除吗?此操作无法撤销。",
"invited": "已邀请 {{name}} 加入项目。",
"removed": "已将 {{name}} 从项目中移除。",
"failedLoad": "加载协作者失败:{{error}}"
},
"vercel": {
"title": "Vercel 集成",
"connected": "您的账号已连接到 Vercel。",
"disconnect": "断开与 Vercel 的连接",
"disconnected": "已成功断开与 Vercel 的连接",
"failedDisconnect": "断开与 Vercel 的连接失败",
"errorDisconnect": "断开 Vercel 连接时发生错误",
"connectedToProject": "已连接到 Vercel 项目:",
"liveUrl": "线上 URL:",
"refreshDeployments": "刷新部署",
"refreshing": "正在刷新...",
"disconnectFromProject": "断开与项目的连接",
"recentDeployments": "最近的部署:",
"view": "查看",
"connectToVercel": "连接到 Vercel",
"setupInstructions": "要将您的应用连接到 Vercel,您需要创建一个访问令牌:",
"signUpFirst": "如果您没有 Vercel 账号,请先注册",
"goToSettings": "前往 Vercel 设置创建令牌",
"copyToken": "复制令牌并粘贴到下方",
"signUp": "注册 Vercel",
"openSettings": "打开 Vercel 设置",
"accessToken": "Vercel 访问令牌",
"enterToken": "输入您的 Vercel 访问令牌",
"savingToken": "正在保存令牌...",
"saveToken": "保存访问令牌",
"tokenSaved": "已成功连接到 Vercel!您现在可以在下方设置项目。",
"setupProject": "设置您的 Vercel 项目",
"createNewProject": "创建新项目",
"connectExistingProject": "连接到现有项目",
"projectName": "项目名称",
"selectProject": "选择项目",
"loadingProjects": "正在加载项目...",
"selectAProject": "选择一个项目",
"projectAvailable": "项目名称可用!",
"createProject": "创建项目",
"connectToProject": "连接到项目",
"projectCreatedLinked": "项目已创建并关联!",
"connectedToProject2": "已连接到项目!",
"failedCreateProject": "创建项目失败。",
"failedConnectProject": "连接到项目失败。",
"failedSaveToken": "保存访问令牌失败。",
"failedCheckAvailability": "检查项目可用性失败。"
},
"supabase": {
"title": "Supabase 集成",
"organizationsConnected_one": "{{count}} 个组织已连接到 Supabase。",
"organizationsConnected_other": "{{count}} 个组织已连接到 Supabase。",
"disconnectAll": "断开所有连接",
"disconnectOrganization": "断开组织连接",
"writeSqlMigrations": "生成 SQL 迁移文件",
"writeSqlDescription": "修改 Supabase 架构时生成 SQL 迁移文件。这有助于在版本控制中跟踪数据库更改,但这些文件不用于聊天上下文(使用实时架构)。",
"disconnectedAll": "已成功断开所有 Supabase 组织的连接",
"failedDisconnect": "断开与 Supabase 的连接失败",
"orgDisconnected": "组织已成功断开连接",
"settingUpdated": "设置已更新",
"project": "Supabase 项目",
"connectedToProject": "此应用已连接到项目:",
"databaseBranch": "数据库分支",
"selectBranch": "选择一个分支",
"disconnectProject": "断开项目连接",
"projectConnected": "项目已成功连接到应用",
"failedConnectProject": "连接项目到应用失败:{{error}}",
"projects": "Supabase 项目",
"selectProjectDescription": "选择要连接到此应用的 Supabase 项目",
"refreshProjects": "刷新项目",
"addOrganization": "添加组织",
"errorLoadingProjects": "加载项目出错:{{message}}",
"connectedOrganizations": "已连接的组织",
"noProjectsFound": "在您已连接的 Supabase 组织中未找到项目。",
"selectAProject": "选择一个项目",
"projectNotFound": "未找到项目",
"branchNotFound": "未找到分支",
"branchSelected": "已选择分支",
"failedSetBranch": "设置分支失败:{{error}}",
"failedDisconnectProject": "断开应用的项目连接失败"
},
"neon": {
"title": "Neon 集成",
"connected": "您的账号已连接到 Neon。",
"database": "Neon 数据库",
"connectedToNeon": "您已连接到 Neon 数据库",
"freeTier": "Neon 数据库有很好的免费套餐,包含备份和最多 10 个项目。",
"connectTo": "连接到",
"connectedSuccess": "已成功连接到 Neon!",
"disconnect": "断开与 Neon 的连接",
"disconnected": "已成功断开与 Neon 的连接",
"failedDisconnect": "断开与 Neon 的连接失败"
}
}
}
{
"title": "设置",
"general": {
"title": "常规设置",
"language": "语言",
"languageDescription": "选择您偏好的显示语言。",
"theme": "主题",
"themeSystem": "跟随系统",
"themeLight": "浅色",
"themeDark": "深色",
"zoom": "缩放级别",
"zoomDescription": "调整缩放级别以便于阅读内容。",
"selectZoom": "选择缩放级别",
"appVersion": "应用版本:",
"autoUpdate": "自动更新应用",
"autoUpdateDescription": "当有新版本可用时,将自动更新应用。",
"releaseChannel": "发布通道",
"releaseChannelDescription": "控制应用使用的更新通道。",
"stable": "稳定版",
"beta": "测试版",
"selectReleaseChannel": "选择发布通道",
"runtimeMode": "运行时模式",
"runtimeModeDescription": "选择用于运行应用的运行时。",
"selectRuntimeMode": "选择运行时模式",
"nodePath": "Node.js 路径配置",
"browseForNode": "浏览 Node.js",
"selecting": "选择中...",
"resetToDefault": "恢复默认",
"customPath": "自定义路径:",
"systemPath": "系统 PATH:",
"notFound": "未找到",
"nodeConfigured": "Node.js 已正确配置,可以使用。",
"nodeSelectFolder": "如果 Node.js 不在系统 PATH 中,请选择其安装文件夹。"
},
"workflow": {
"title": "工作流设置",
"defaultChatMode": "默认聊天模式",
"defaultChatModeDescription": "控制打开新聊天时的默认聊天模式。",
"selectDefaultChatMode": "选择默认聊天模式",
"autoApprove": "自动批准更改",
"autoApproveDescription": "将自动批准代码更改并运行。",
"autoFixProblems": "自动修复问题",
"autoFixProblemsDescription": "将自动修复 TypeScript 错误。"
},
"ai": {
"title": "AI 设置",
"thinkingBudget": "思考预算",
"thinkingBudgetDescription": "控制 AI 模型的思考时长。更高的预算意味着更深入的推理。",
"selectThinkingBudget": "选择思考预算",
"low": "低",
"medium": "中",
"high": "高",
"maxChatTurns": "上下文中最大聊天轮数",
"maxChatTurnsDescription": "限制上下文中包含的最近聊天轮数。",
"selectMaxChatTurns": "选择最大轮数",
"unlimited": "无限制",
"turnCount_one": "{{count}} 轮",
"turnCount_other": "{{count}} 轮",
"providers": "AI 提供商",
"failedToLoadProviders": "加载 AI 提供商失败:{{message}}",
"freeTierAvailable": "有免费套餐",
"addCustomProvider": "添加自定义提供商",
"connectCustomEndpoint": "连接到自定义 LLM API 端点",
"editProvider": "编辑提供商",
"deleteProvider": "删除提供商",
"deleteCustomProvider": "删除自定义提供商",
"deleteProviderConfirmation": "这将永久删除此自定义提供商及其所有关联模型。此操作无法撤销。",
"deleteProviderAction": "删除提供商",
"configureProvider": "配置 {{name}}",
"setupComplete": "设置完成",
"notSetup": "未设置",
"createApiKey": "在 {{name}} 创建您的 API 密钥",
"manageApiKeys": "管理 API 密钥",
"setupApiKey": "设置 API 密钥",
"providerNotFound": "未找到提供商",
"providerNotFoundDescription": "未找到 ID 为「{{provider}}」的提供商。",
"errorLoadingProvider": "加载提供商详情出错",
"errorLoadingSettings": "加载设置出错",
"couldNotLoadProvider": "无法加载提供商数据:{{message}}",
"couldNotLoadSettings": "无法加载配置数据:{{message}}",
"enableDyadPro": "启用 Dyad Pro",
"toggleDyadPro": "切换以启用 Dyad Pro",
"apiKeyEmpty": "API 密钥不能为空。",
"errorTogglingPro": "切换 Dyad Pro 时出错:{{error}}",
"failedSaveApiKey": "保存 API 密钥失败。",
"failedDeleteApiKey": "删除 API 密钥失败。"
},
"telemetry": {
"title": "遥测",
"description": "这将记录匿名使用数据以改进产品。",
"telemetryId": "遥测 ID:",
"enable": "启用遥测",
"privacyNotice": "我们使用匿名遥测数据来改进产品。不会收集个人数据。",
"acceptAndContinue": "接受并继续"
},
"integrations": {
"title": "集成"
},
"agentPermissions": {
"title": "Agent 权限 (Pro)",
"description": "配置 Agent 内置工具的权限。",
"defaultAllowedTools": "默认允许的工具 ({{count}})",
"ask": "询问",
"alwaysAllow": "始终允许",
"neverAllow": "从不允许"
},
"toolsMcp": {
"title": "工具 (MCP)",
"name": "名称",
"namePlaceholder": "我的 MCP 服务器",
"transport": "传输方式",
"stdio": "stdio",
"http": "http",
"command": "命令",
"commandPlaceholder": "node",
"args": "参数",
"argsPlaceholder": "path/to/mcp-server.js --flag",
"url": "URL",
"urlPlaceholder": "http://localhost:3000",
"addServer": "添加服务器",
"environmentVariables": "环境变量",
"key": "键",
"keyPlaceholder": "例如 PATH",
"value": "值",
"valuePlaceholder": "例如 /usr/local/bin",
"addEnvVar": "添加环境变量",
"noEnvVars": "未配置环境变量",
"keyValueRequired": "键和值都是必填项",
"duplicateKey": "该键名的环境变量已存在",
"noToolsDiscovered": "未发现工具。",
"noServersConfigured": "尚未配置服务器。",
"prefilledServer": "已预填 {{name}} MCP 服务器",
"deny": "拒绝"
},
"experiments": {
"title": "实验性功能",
"enableNativeGit": "启用原生 Git",
"enableNativeGitDescription": "无需任何外部 Git 安装,提供更快的原生 Git 性能体验。"
},
"dangerZone": {
"title": "危险区域",
"resetEverything": "重置所有内容",
"resetDescription": "这将删除您所有的应用、聊天记录和设置。此操作无法撤销。",
"resetConfirmation": "您确定要重置所有内容吗?这将删除您所有的应用、聊天记录和设置。此操作无法撤销。",
"resetting": "正在重置...",
"resetSuccess": "已成功重置所有内容。请重新启动应用。"
},
"apiKey": {
"fromSettings": "来自设置的 API 密钥",
"currentKey": "当前密钥(设置)",
"thisKeyActive": "此密钥当前处于活跃状态。",
"setKey": "设置 {{name}} API 密钥",
"updateKey": "更新 {{name}} API 密钥",
"enterKey": "在此输入新的 {{name}} API 密钥",
"pasteAndSave": "从剪贴板粘贴并保存",
"saveKey": "保存密钥",
"overrideEnvVar": "在此设置密钥将覆盖环境变量(如果已设置)。",
"fromEnvVar": "来自环境变量的 API 密钥",
"envVarKey": "环境变量密钥 ({{name}})",
"envVarActive": "此密钥当前处于活跃状态(未设置设置密钥)。",
"envVarOverridden": "此密钥当前被设置中的密钥覆盖。",
"envVarNotSet": "环境变量未设置",
"envVarNotSetDescription": "{{name}} 环境变量未设置。",
"envVarHelpText": "此密钥在应用外部设置。如果存在,仅在上方设置部分未配置密钥时使用。需要重启应用才能检测到更改。"
},
"azure": {
"configured": "Azure OpenAI 已配置",
"configuredDescription": "Dyad 将使用设置中保存的凭据来访问 Azure OpenAI 模型。",
"usingEnvVars": "使用环境变量",
"usingEnvVarsDescription": "AZURE_API_KEY 和 AZURE_RESOURCE_NAME 已设置。下方保存的值将覆盖它们。",
"configRequired": "需要配置 Azure OpenAI",
"configRequiredDescription": "请在下方提供您的 Azure 资源名称和 API 密钥,或配置 AZURE_API_KEY 和 AZURE_RESOURCE_NAME 环境变量。",
"resourceName": "资源名称",
"resourceNamePlaceholder": "your-azure-openai-resource",
"apiKey": "API 密钥",
"apiKeyPlaceholder": "输入您的 Azure OpenAI API 密钥",
"saveSettings": "保存设置",
"saved": "已保存",
"configNeeded": "需要配置",
"configNeededDescription": "Azure OpenAI 请求需要资源名称和 API 密钥。请在上方输入,或提供环境变量。",
"saveError": "保存错误",
"envVarsOptional": "环境变量(可选)",
"envVarsHelpText": "您可以继续通过环境变量配置 Azure。如果两个变量都存在且未保存设置,Dyad 将自动使用它们。",
"envVarsPrecedence": "设置中保存的值优先于环境变量。更改环境变量后请重启 Dyad。"
},
"vertex": {
"projectId": "项目 ID",
"projectIdPlaceholder": "your-gcp-project-id",
"location": "区域",
"locationPlaceholder": "us-central1",
"locationHelp": "如果出现「模型未找到」错误,请尝试其他区域。某些合作伙伴模型 (MaaS) 仅在特定区域可用(例如 us-central1、us-west2)。",
"serviceAccountKey": "服务账户 JSON 密钥",
"serviceAccountKeyPlaceholder": "在此粘贴您的服务账户密钥的完整 JSON 内容",
"saveSettings": "保存设置",
"saved": "已保存",
"configRequired": "需要配置",
"configRequiredDescription": "请提供项目、区域和具有 Vertex AI 访问权限的服务账户 JSON 密钥。",
"saveError": "保存错误",
"invalidJson": "服务账户 JSON 无效:{{message}}"
},
"models": {
"title": "模型",
"description": "管理此提供商可用的特定模型。",
"errorLoading": "加载模型出错",
"noCustomModels": "尚未为此提供商添加自定义模型。",
"addCustomModel": "添加自定义模型",
"deleteConfirmTitle": "确定要删除此模型吗?",
"deleteConfirmDescription": "此操作无法撤销。这将永久删除自定义模型「{{name}}」(API 名称:{{apiName}})。",
"yesDeleteIt": "是的,删除它",
"contextTokens": "上下文:{{count}} 令牌",
"maxOutputTokens": "最大输出:{{count}} 令牌",
"modelIdRequired": "模型 API 名称为必填项",
"modelNameRequired": "模型显示名称为必填项",
"invalidMaxOutput": "最大输出令牌数必须是有效数字",
"invalidContextWindow": "上下文窗口必须是有效数字",
"addModelTitle": "添加自定义模型",
"addModelDescription": "为所选提供商配置新的语言模型。",
"editModelTitle": "编辑自定义模型",
"editModelDescription": "修改所选语言模型的配置。",
"modelId": "模型 ID*",
"modelIdHelp": "必须与 API 期望的模型匹配",
"modelName": "名称*",
"modelNameHelp": "模型的友好显示名称",
"modelDescription": "描述",
"modelDescriptionHelp": "可选:描述模型的功能",
"maxOutputTokensLabel": "最大输出令牌数",
"maxOutputTokensHelp": "可选:例如 4096",
"contextWindowLabel": "上下文窗口",
"contextWindowHelp": "可选:例如 8192",
"adding": "添加中...",
"addModel": "添加模型",
"updateModel": "更新模型",
"modelCreated": "自定义模型创建成功!",
"modelUpdated": "自定义模型更新成功!",
"failedUpdateSettings": "更新设置失败"
},
"customProvider": {
"addTitle": "添加自定义提供商",
"editTitle": "编辑自定义提供商",
"addDescription": "连接到自定义语言模型提供商 API。",
"editDescription": "更新您的自定义语言模型提供商配置。",
"providerId": "提供商 ID",
"providerIdPlaceholder": "例如 my-provider",
"providerIdHelp": "此提供商的唯一标识符(不含空格)。",
"displayName": "显示名称",
"displayNamePlaceholder": "例如 我的提供商",
"displayNameHelp": "将在界面中显示的名称。",
"apiBaseUrl": "API 基础 URL",
"apiBaseUrlPlaceholder": "例如 https://api.example.com/v1",
"apiBaseUrlHelp": "API 端点的基础 URL。",
"envVar": "环境变量(可选)",
"envVarPlaceholder": "例如 MY_PROVIDER_API_KEY",
"envVarHelp": "API 密钥的环境变量名称。",
"addProvider": "添加提供商",
"updateProvider": "更新提供商",
"failedCreate": "创建自定义提供商失败"
},
"manageDyadPro": "管理 Dyad Pro 订阅",
"setupDyadPro": "设置 Dyad Pro 订阅"
}
import "i18next";
import type enCommon from "./locales/en/common.json";
import type enSettings from "./locales/en/settings.json";
import type enChat from "./locales/en/chat.json";
import type enHome from "./locales/en/home.json";
import type enErrors from "./locales/en/errors.json";
declare module "i18next" {
interface CustomTypeOptions {
defaultNS: "common";
resources: {
common: typeof enCommon;
settings: typeof enSettings;
chat: typeof enChat;
home: typeof enHome;
errors: typeof enErrors;
};
}
}
......@@ -244,6 +244,18 @@ export type ZoomLevel = z.infer<typeof ZoomLevelSchema>;
export const ZOOM_LEVELS: readonly ZoomLevel[] = ZoomLevelSchema.options;
export const DEFAULT_ZOOM_LEVEL: ZoomLevel = "100";
export const LanguageSchema = z.enum([
"en",
"zh-CN",
"ja",
"ko",
"es",
"fr",
"de",
"pt-BR",
]);
export type Language = z.infer<typeof LanguageSchema>;
export const DeviceModeSchema = z.enum(["desktop", "tablet", "mobile"]);
export type DeviceMode = z.infer<typeof DeviceModeSchema>;
......@@ -307,6 +319,7 @@ export const UserSettingsSchema = z
defaultChatMode: ChatModeSchema.optional(),
acceptedCommunityCode: z.boolean().optional(),
zoomLevel: ZoomLevelSchema.optional(),
language: LanguageSchema.optional(),
previewDeviceMode: DeviceModeSchema.optional(),
enableAutoFixProblems: z.boolean().optional(),
......
import { useTranslation } from "react-i18next";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { useAtom, useSetAtom } from "jotai";
import { homeChatInputValueAtom } from "../atoms/chatAtoms";
......@@ -48,6 +49,7 @@ export interface HomeSubmitOptions {
}
export default function HomePage() {
const { t } = useTranslation("home");
const [inputValue, setInputValue] = useAtom(homeChatInputValueAtom);
const navigate = useNavigate();
const search = useSearch({ from: "/" });
......@@ -206,7 +208,7 @@ export default function HomePage() {
navigate({ to: "/chat", search: { id: result.chatId } });
} catch (error) {
console.error("Failed to create chat:", error);
showError("Failed to create app. " + (error as any).toString());
showError(t("failedCreateApp", { error: (error as any).toString() }));
setIsLoading(false); // Ensure loading state is reset on error
}
// No finally block needed for setIsLoading(false) here if navigation happens on success
......@@ -223,11 +225,11 @@ export default function HomePage() {
<div className="absolute top-0 left-0 w-full h-full border-8 border-t-primary rounded-full animate-spin"></div>
</div>
<h2 className="text-2xl font-bold mb-2 text-gray-800 dark:text-gray-200">
Building your app
{t("buildingApp")}
</h2>
<p className="text-gray-600 dark:text-gray-400 text-center max-w-md mb-8">
We're setting up your app with AI magic. <br />
This might take a moment...
{t("settingUp")} <br />
{t("mightTakeMoment")}
</p>
</div>
</div>
......@@ -263,7 +265,9 @@ export default function HomePage() {
<button
type="button"
key={index}
onClick={() => setInputValue(`Build me a ${item.label}`)}
onClick={() =>
setInputValue(t("buildMeA", { label: item.label }))
}
className="flex items-center gap-3 px-4 py-2 rounded-xl border border-gray-200
bg-white/50 backdrop-blur-sm
transition-all duration-200
......@@ -307,7 +311,7 @@ export default function HomePage() {
/>
</svg>
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
More ideas
{t("moreIdeas")}
</span>
</button>
</div>
......@@ -319,7 +323,7 @@ export default function HomePage() {
<Dialog open={releaseNotesOpen} onOpenChange={setReleaseNotesOpen}>
<DialogContent className="max-w-4xl bg-(--docs-bg) pr-0 pt-4 pl-4 gap-1">
<DialogHeader>
<DialogTitle>What's new in v{appVersion}?</DialogTitle>
<DialogTitle>{t("whatsNew", { version: appVersion })}</DialogTitle>
<Button
variant="ghost"
size="sm"
......@@ -340,7 +344,7 @@ export default function HomePage() {
<iframe
src={releaseUrl}
className="w-full h-full border-0 rounded-lg"
title={`Release notes for v${appVersion}`}
title={t("releaseNotesTitle", { version: appVersion })}
/>
</div>
)}
......
......@@ -30,6 +30,7 @@ import { NodePathSelector } from "@/components/NodePathSelector";
import { ToolsMcpSettings } from "@/components/settings/ToolsMcpSettings";
import { AgentToolsSettings } from "@/components/settings/AgentToolsSettings";
import { ZoomSelector } from "@/components/ZoomSelector";
import { LanguageSelector } from "@/components/LanguageSelector";
import { DefaultChatModeSelector } from "@/components/DefaultChatModeSelector";
import { ContextCompactionSwitch } from "@/components/ContextCompactionSwitch";
import { useSetAtom } from "jotai";
......@@ -286,6 +287,10 @@ export function GeneralSettings({ appVersion }: { appVersion: string | null }) {
</div>
</div>
<div className="mt-4">
<LanguageSelector />
</div>
<div id={SETTING_IDS.zoom} className="mt-4">
<ZoomSelector />
</div>
......
......@@ -5,6 +5,9 @@ import { RouterProvider } from "@tanstack/react-router";
import { PostHogProvider } from "posthog-js/react";
import posthog from "posthog-js";
import { getTelemetryUserId, isTelemetryOptedIn } from "./hooks/useSettings";
// Initialize i18next before any rendering
import "./i18n";
import {
QueryCache,
QueryClient,
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论